88
99from __future__ import annotations
1010
11+ import asyncio
12+ import logging
1113import sys
14+ from collections .abc import Awaitable
1215from contextlib import AbstractAsyncContextManager
13- from typing import TYPE_CHECKING , Any , ClassVar
16+ from typing import TYPE_CHECKING , Any , ClassVar , TypeAlias , TypedDict
1417
1518from agent_framework import Message
1619from agent_framework ._sessions import AgentSession , ContextProvider , SessionContext
1720from mem0 import AsyncMemory , AsyncMemoryClient
1821
1922if sys .version_info >= (3 , 11 ):
20- from typing import NotRequired , Self , TypedDict # pragma: no cover
23+ from typing import Self # pragma: no cover
2124else :
22- from typing_extensions import NotRequired , Self , TypedDict # pragma: no cover
25+ from typing_extensions import Self # pragma: no cover
2326
2427if TYPE_CHECKING :
2528 from agent_framework ._agents import SupportsAgentRun
2629
30+ logger = logging .getLogger (__name__ )
31+ MemoryRecord : TypeAlias = dict [str , object ]
2732
28- class _MemorySearchResponse_v1_1 (TypedDict ):
29- results : list [dict [str , Any ]]
30- relations : NotRequired [list [dict [str , Any ]]]
3133
34+ class SearchResults (TypedDict ):
35+ results : list [MemoryRecord ]
3236
33- _MemorySearchResponse_v2 = list [dict [str , Any ]]
37+
38+ SearchResponse : TypeAlias = list [MemoryRecord ] | SearchResults
3439
3540
3641class Mem0ContextProvider (ContextProvider ):
@@ -106,28 +111,85 @@ async def before_run(
106111 if not input_text .strip ():
107112 return
108113
109- filters = self ._build_filters ()
110-
111- # AsyncMemory (OSS) expects user_id/agent_id/run_id as direct kwargs
112- # AsyncMemoryClient (Platform) expects them in a filters dict
113- search_kwargs : dict [str , Any ] = {"query" : input_text }
114- if isinstance (self .mem0_client , AsyncMemory ):
115- search_kwargs .update (filters )
116- else :
117- search_kwargs ["filters" ] = filters
114+ # Query entity partitions independently to bypass strict logical AND limitations
115+ # Mem0 OSS and Platform SDKs expose inconsistent search typings.
116+ search_tasks : list [Awaitable [Any ]] = []
118117
119- search_response : _MemorySearchResponse_v1_1 | _MemorySearchResponse_v2 = await self .mem0_client .search ( # type: ignore[misc]
120- ** search_kwargs ,
121- )
118+ # 1. Query User partition independently
119+ if self .user_id :
120+ user_kwargs = self ._build_search_kwargs (input_text , "user_id" , self .user_id )
121+ search_tasks .append (self .mem0_client .search (** user_kwargs )) # type: ignore[reportUnknownMemberType, reportUnknownArgumentType]
122122
123- if isinstance (search_response , list ):
124- memories = search_response
125- elif isinstance (search_response , dict ) and "results" in search_response :
126- memories = search_response ["results" ]
127- else :
128- memories = [search_response ]
123+ # 2. Query Agent partition independently
124+ if self .agent_id :
125+ agent_kwargs = self ._build_search_kwargs (input_text , "agent_id" , self .agent_id )
126+ search_tasks .append (self .mem0_client .search (** agent_kwargs )) # type: ignore[reportUnknownMemberType, reportUnknownArgumentType]
127+
128+ # Fall back to an app-scoped search when only application_id is configured
129+ if not search_tasks and self .application_id :
130+ app_kwargs : dict [str , Any ] = {"query" : input_text }
131+ if isinstance (self .mem0_client , AsyncMemory ):
132+ app_kwargs ["app_id" ] = self .application_id
133+ else :
134+ app_kwargs ["filters" ] = {"app_id" : self .application_id }
135+ search_tasks .append (self .mem0_client .search (** app_kwargs )) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
136+ if not search_tasks :
137+ return
129138
130- line_separated_memories = "\n " .join (memory .get ("memory" , "" ) for memory in memories )
139+ results : list [SearchResponse | BaseException ] = await asyncio .gather (* search_tasks , return_exceptions = True )
140+
141+ # Merge and deduplicate results
142+ memories : list [MemoryRecord ] = []
143+ seen_memory_ids : set [str ] = set ()
144+ failed_tasks_count : int = 0
145+
146+ for search_response in results :
147+ if isinstance (search_response , asyncio .CancelledError ):
148+ raise search_response
149+
150+ if isinstance (search_response , BaseException ):
151+ failed_tasks_count += 1
152+ logger .error (
153+ "Mem0 partition search task failed: %s" ,
154+ search_response ,
155+ exc_info = (type (search_response ), search_response , search_response .__traceback__ ),
156+ )
157+ continue
158+
159+ current_memories : list [MemoryRecord ] = []
160+ if isinstance (search_response , list ):
161+ current_memories = [mem for mem in search_response if isinstance (mem , dict )]
162+ elif isinstance (search_response , dict ):
163+ results_field = search_response .get ("results" )
164+ if isinstance (results_field , list ):
165+ current_memories = [
166+ item
167+ for item in results_field
168+ if isinstance (item , dict ) # pyright: ignore[reportUnknownVariableType]
169+ ]
170+ else :
171+ logger .warning (
172+ "Unexpected Mem0 search response format: %s" ,
173+ type (results_field ).__name__ ,
174+ )
175+
176+ for mem in current_memories :
177+ mem_id = mem .get ("id" )
178+ if mem_id is not None and not isinstance (mem_id , str ):
179+ mem_id = str (mem_id )
180+
181+ if mem_id is not None and mem_id in seen_memory_ids :
182+ continue
183+
184+ if mem_id is not None :
185+ seen_memory_ids .add (mem_id )
186+
187+ memories .append (mem )
188+
189+ if failed_tasks_count == len (search_tasks ):
190+ logger .error ("All Mem0 retrieval tasks failed. Context provider is unable to verify memory state." )
191+
192+ line_separated_memories = "\n " .join (str (memory .get ("memory" , "" )) for memory in memories )
131193 if line_separated_memories :
132194 context .extend_messages (
133195 self .source_id ,
@@ -159,12 +221,21 @@ def get_role_value(role: Any) -> str:
159221 ]
160222
161223 if messages :
162- await self .mem0_client .add ( # type: ignore[misc]
163- messages = messages ,
164- user_id = self .user_id ,
165- agent_id = self .agent_id ,
166- metadata = {"application_id" : self .application_id },
167- )
224+ add_kwargs : dict [str , Any ] = {
225+ "messages" : messages ,
226+ "user_id" : self .user_id ,
227+ "agent_id" : self .agent_id ,
228+ }
229+
230+ # Inject the application scope using the matching signature format for each SDK variant
231+ if isinstance (self .mem0_client , AsyncMemory ):
232+ if self .application_id :
233+ add_kwargs ["app_id" ] = self .application_id
234+ else :
235+ if self .application_id :
236+ add_kwargs ["filters" ] = {"app_id" : self .application_id }
237+
238+ await self .mem0_client .add (** add_kwargs ) # type: ignore[misc, call-arg]
168239
169240 # -- Internal methods ------------------------------------------------------
170241
@@ -173,15 +244,21 @@ def _validate_filters(self) -> None:
173244 if not self .agent_id and not self .user_id and not self .application_id :
174245 raise ValueError ("At least one of the filters: agent_id, user_id, or application_id is required." )
175246
176- def _build_filters (self ) -> dict [str , Any ]:
177- """Build search filters from initialization parameters."""
178- filters : dict [str , Any ] = {}
179- if self .user_id :
180- filters ["user_id" ] = self .user_id
181- if self .agent_id :
182- filters ["agent_id" ] = self .agent_id
183- if self .application_id :
184- filters ["app_id" ] = self .application_id
247+ def _build_search_kwargs (self , input_text : str , entity_key : str , entity_value : str ) -> dict [str , Any ]:
248+ """Build search keyword arguments formatted for OSS vs Platform clients."""
249+ filters : dict [str , Any ] = {"query" : input_text }
250+
251+ if isinstance (self .mem0_client , AsyncMemory ):
252+ # AsyncMemory (OSS) expects direct kwargs
253+ filters [entity_key ] = entity_value
254+ if self .application_id :
255+ filters ["app_id" ] = self .application_id
256+ else :
257+ # AsyncMemoryClient (Platform) expects a filters dict
258+ filters ["filters" ] = {entity_key : entity_value }
259+ if self .application_id :
260+ filters ["filters" ]["app_id" ] = self .application_id
261+
185262 return filters
186263
187264
0 commit comments