Skip to content

Commit 6169df0

Browse files
authored
Python: fix(mem0): isolate entity retrieval and correct app_id payload (microsoft#6242)
* fix(mem0): parallel memory retrieval logic and strict type compliance * fix(mem0): align parallel retrieval types for pyright and mypy * fix(mem0): handle asyncio.CancelledError in search response and update test description * fix(mem0): improve error handling for asyncio.CancelledError and update test names for clarity * fix(mem0): improve retrieval response handling
1 parent 3312012 commit 6169df0

2 files changed

Lines changed: 222 additions & 82 deletions

File tree

python/packages/mem0/agent_framework_mem0/_context_provider.py

Lines changed: 118 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,34 @@
88

99
from __future__ import annotations
1010

11+
import asyncio
12+
import logging
1113
import sys
14+
from collections.abc import Awaitable
1215
from contextlib import AbstractAsyncContextManager
13-
from typing import TYPE_CHECKING, Any, ClassVar
16+
from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias, TypedDict
1417

1518
from agent_framework import Message
1619
from agent_framework._sessions import AgentSession, ContextProvider, SessionContext
1720
from mem0 import AsyncMemory, AsyncMemoryClient
1821

1922
if sys.version_info >= (3, 11):
20-
from typing import NotRequired, Self, TypedDict # pragma: no cover
23+
from typing import Self # pragma: no cover
2124
else:
22-
from typing_extensions import NotRequired, Self, TypedDict # pragma: no cover
25+
from typing_extensions import Self # pragma: no cover
2326

2427
if 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

3641
class 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

Comments
 (0)