From dd872a89b20d6d0d88984aa2ac273e1a9af4afa2 Mon Sep 17 00:00:00 2001 From: Younes Slaoui Date: Wed, 27 May 2026 18:51:10 -0700 Subject: [PATCH 1/2] fix(mem0): support mem0ai 2.x in Mem0ContextProvider --- .../agent_framework_mem0/_context_provider.py | 15 +++--- python/packages/mem0/pyproject.toml | 2 +- .../mem0/tests/test_mem0_context_provider.py | 47 +++++++++++++++---- python/uv.lock | 8 ++-- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/python/packages/mem0/agent_framework_mem0/_context_provider.py b/python/packages/mem0/agent_framework_mem0/_context_provider.py index c6be7089909..9007be8b993 100644 --- a/python/packages/mem0/agent_framework_mem0/_context_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_context_provider.py @@ -108,16 +108,17 @@ async def before_run( filters = self._build_filters() - # AsyncMemory (OSS) expects user_id/agent_id/run_id as direct kwargs - # AsyncMemoryClient (Platform) expects them in a filters dict - search_kwargs: dict[str, Any] = {"query": input_text} + # mem0 >=2.0: both OSS (AsyncMemory) and Platform (AsyncMemoryClient) take + # entity IDs in a filters dict (top-level entity kwargs are rejected). OSS only + # recognizes user_id/agent_id/run_id as entities, so app_id (Platform-only) is + # dropped for the OSS client to avoid it being treated as a non-matching + # metadata filter that would silently exclude all results. if isinstance(self.mem0_client, AsyncMemory): - search_kwargs.update(filters) - else: - search_kwargs["filters"] = filters + filters = {key: value for key, value in filters.items() if key != "app_id"} search_response: _MemorySearchResponse_v1_1 | _MemorySearchResponse_v2 = await self.mem0_client.search( # type: ignore[misc] - **search_kwargs, + query=input_text, + filters=filters, ) if isinstance(search_response, list): diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index d7db0a2b95b..2cbeae76c30 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.6.0,<2", - "mem0ai>=1.0.0,<2", + "mem0ai>=2.0.0,<3", ] [tool.uv] diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index bf40577878b..98c4c178ddd 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -177,8 +177,8 @@ async def test_search_query_combines_input_messages(self, mock_mem0_client: Asyn call_kwargs = mock_mem0_client.search.call_args.kwargs assert call_kwargs["query"] == "Hello\nWorld" - async def test_oss_client_passes_direct_kwargs(self, mock_oss_mem0_client: AsyncMock) -> None: - """OSS AsyncMemory client should receive user_id as direct kwarg, not in filters.""" + async def test_oss_client_passes_filters_dict(self, mock_oss_mem0_client: AsyncMock) -> None: + """OSS AsyncMemory client should receive entity IDs in a filters dict (mem0 >=2.0).""" mock_oss_mem0_client.search.return_value = [{"memory": "User likes Python"}] provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_oss_mem0_client, user_id="u1") session = AgentSession(session_id="test-session") @@ -190,11 +190,11 @@ async def test_oss_client_passes_direct_kwargs(self, mock_oss_mem0_client: Async call_kwargs = mock_oss_mem0_client.search.call_args.kwargs assert call_kwargs["query"] == "Hello" - assert call_kwargs["user_id"] == "u1" - assert "filters" not in call_kwargs + assert call_kwargs["filters"] == {"user_id": "u1"} + assert "user_id" not in call_kwargs async def test_oss_client_all_scoping_params(self, mock_oss_mem0_client: AsyncMock) -> None: - """OSS client with all scoping parameters passes them as direct kwargs.""" + """OSS client passes user_id/agent_id in filters; app_id is dropped (OSS has no app_id entity).""" mock_oss_mem0_client.search.return_value = [] provider = Mem0ContextProvider( source_id="mem0", mem0_client=mock_oss_mem0_client, user_id="u1", agent_id="a1", application_id="app1" @@ -207,9 +207,24 @@ async def test_oss_client_all_scoping_params(self, mock_oss_mem0_client: AsyncMo ) # type: ignore[arg-type] call_kwargs = mock_oss_mem0_client.search.call_args.kwargs - assert call_kwargs["user_id"] == "u1" - assert call_kwargs["agent_id"] == "a1" - assert "filters" not in call_kwargs + assert call_kwargs["filters"] == {"user_id": "u1", "agent_id": "a1"} + assert "app_id" not in call_kwargs["filters"] + assert "user_id" not in call_kwargs + + async def test_oss_client_excludes_app_id(self, mock_oss_mem0_client: AsyncMock) -> None: + """OSS client with only application_id set: app_id must not leak into search filters.""" + mock_oss_mem0_client.search.return_value = [] + provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_oss_mem0_client, application_id="app1") + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", contents=["Hello"])], session_id="s1") + + await provider.before_run( + agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {}) + ) # type: ignore[arg-type] + + call_kwargs = mock_oss_mem0_client.search.call_args.kwargs + assert call_kwargs["filters"] == {} + assert "app_id" not in call_kwargs["filters"] async def test_platform_client_passes_filters_dict(self, mock_mem0_client: AsyncMock) -> None: """Platform AsyncMemoryClient should receive scoping params in a filters dict.""" @@ -227,6 +242,22 @@ async def test_platform_client_passes_filters_dict(self, mock_mem0_client: Async assert "filters" in call_kwargs assert call_kwargs["filters"]["user_id"] == "u1" + async def test_platform_client_keeps_app_id(self, mock_mem0_client: AsyncMock) -> None: + """Platform AsyncMemoryClient keeps app_id in filters (it is a recognized Platform entity).""" + mock_mem0_client.search.return_value = [] + provider = Mem0ContextProvider( + source_id="mem0", mem0_client=mock_mem0_client, user_id="u1", application_id="app1" + ) + session = AgentSession(session_id="test-session") + ctx = SessionContext(input_messages=[Message(role="user", contents=["Hello"])], session_id="s1") + + await provider.before_run( + agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {}) + ) # type: ignore[arg-type] + + call_kwargs = mock_mem0_client.search.call_args.kwargs + assert call_kwargs["filters"] == {"user_id": "u1", "app_id": "app1"} + # -- after_run tests ----------------------------------------------------------- diff --git a/python/uv.lock b/python/uv.lock index dee89c9f0a0..dc4ce1269cf 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -719,7 +719,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "mem0ai", specifier = ">=1.0.0,<2" }, + { name = "mem0ai", specifier = ">=2.0.0,<3" }, ] [[package]] @@ -3913,7 +3913,7 @@ wheels = [ [[package]] name = "mem0ai" -version = "1.0.11" +version = "2.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3924,9 +3924,9 @@ dependencies = [ { name = "qdrant-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "sqlalchemy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/1e/2f8a8cc4b8e7f6126f3367d27dc65eac5cd4ceb854888faa3a8f62a2c0a0/mem0ai-1.0.11.tar.gz", hash = "sha256:ddb803bedc22bd514606d262407782e88df929f6991b59f6972fb8a25cc06001", size = 201758, upload-time = "2026-04-06T11:31:43.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/51/e22728c4b4e94e06f97464a3262b895ca67f62381d5cb34fe1c6f672c946/mem0ai-2.0.4.tar.gz", hash = "sha256:0cffdbdaa961aa4371c707e5f90f25961ae0cf170169abf6aaf6adba14c6bca1", size = 216092, upload-time = "2026-05-27T17:45:46.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/b5/f822c94e1b901f8a700af134c2473646de9a7db26364566f6a72d527d235/mem0ai-1.0.11-py3-none-any.whl", hash = "sha256:bcf4d678dc0a4d4e8eccaebe05562eae022fcdc825a0e3095d02f28cf61a5b6d", size = 297138, upload-time = "2026-04-06T11:31:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/71/ab/46eac3d46c17427af579cb44ff1513b552d85b32998748f2c66248513955/mem0ai-2.0.4-py3-none-any.whl", hash = "sha256:920a39474bf4c2a5a7d4bb814e237aacd699c435d8e20db257c3b43568e7175e", size = 303753, upload-time = "2026-05-27T17:45:44.598Z" }, ] [[package]] From 251ee3c6e04c28684e21bf1431d3214e44b7e168 Mon Sep 17 00:00:00 2001 From: Younes Slaoui Date: Wed, 27 May 2026 19:16:38 -0700 Subject: [PATCH 2/2] fix(mem0): reject application_id-only on OSS client; honest test --- .../agent_framework_mem0/_context_provider.py | 12 +++++++-- .../mem0/tests/test_mem0_context_provider.py | 27 ++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/python/packages/mem0/agent_framework_mem0/_context_provider.py b/python/packages/mem0/agent_framework_mem0/_context_provider.py index 9007be8b993..5c96c5b34d2 100644 --- a/python/packages/mem0/agent_framework_mem0/_context_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_context_provider.py @@ -60,7 +60,10 @@ def __init__( source_id: Unique identifier for this provider instance. mem0_client: A pre-created Mem0 MemoryClient or None to create a default client. api_key: The API key for authenticating with the Mem0 API. - application_id: The application ID for scoping memories. + application_id: The application ID for scoping memories. Platform-only: + the OSS ``AsyncMemory`` client does not recognize an application + scope (it scopes only by user_id/agent_id/run_id), so an OSS client + requires user_id or agent_id and application_id alone is rejected. agent_id: The agent ID for scoping memories. user_id: The user ID for scoping memories. context_prompt: The prompt to prepend to retrieved memories. @@ -170,9 +173,14 @@ def get_role_value(role: Any) -> str: # -- Internal methods ------------------------------------------------------ def _validate_filters(self) -> None: - """Validates that at least one filter is provided.""" + """Validates that at least one usable filter is provided for the configured client.""" if not self.agent_id and not self.user_id and not self.application_id: raise ValueError("At least one of the filters: agent_id, user_id, or application_id is required.") + if isinstance(self.mem0_client, AsyncMemory) and not self.user_id and not self.agent_id: + raise ValueError( + "application_id is not supported by the OSS AsyncMemory client, which scopes " + "memories only by user_id/agent_id. Provide user_id or agent_id." + ) def _build_filters(self) -> dict[str, Any]: """Build search filters from initialization parameters.""" diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index 98c4c178ddd..738643b9b72 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -211,20 +211,18 @@ async def test_oss_client_all_scoping_params(self, mock_oss_mem0_client: AsyncMo assert "app_id" not in call_kwargs["filters"] assert "user_id" not in call_kwargs - async def test_oss_client_excludes_app_id(self, mock_oss_mem0_client: AsyncMock) -> None: - """OSS client with only application_id set: app_id must not leak into search filters.""" - mock_oss_mem0_client.search.return_value = [] + async def test_oss_client_rejects_application_id_only(self, mock_oss_mem0_client: AsyncMock) -> None: + """OSS client with only application_id set: before_run raises and search is never called.""" provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_oss_mem0_client, application_id="app1") session = AgentSession(session_id="test-session") ctx = SessionContext(input_messages=[Message(role="user", contents=["Hello"])], session_id="s1") - await provider.before_run( - agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {}) - ) # type: ignore[arg-type] + with pytest.raises(ValueError, match="application_id is not supported"): + await provider.before_run( + agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {}) + ) # type: ignore[arg-type] - call_kwargs = mock_oss_mem0_client.search.call_args.kwargs - assert call_kwargs["filters"] == {} - assert "app_id" not in call_kwargs["filters"] + mock_oss_mem0_client.search.assert_not_awaited() async def test_platform_client_passes_filters_dict(self, mock_mem0_client: AsyncMock) -> None: """Platform AsyncMemoryClient should receive scoping params in a filters dict.""" @@ -388,6 +386,17 @@ def test_passes_with_application_id(self, mock_mem0_client: AsyncMock) -> None: provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client, application_id="app1") provider._validate_filters() + def test_oss_application_id_only_raises(self, mock_oss_mem0_client: AsyncMock) -> None: + """OSS client with only application_id is rejected (application_id is Platform-only).""" + provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_oss_mem0_client, application_id="app1") + with pytest.raises(ValueError, match="application_id is not supported"): + provider._validate_filters() + + def test_oss_passes_with_user_id(self, mock_oss_mem0_client: AsyncMock) -> None: + """OSS client with user_id is accepted.""" + provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_oss_mem0_client, user_id="u1") + provider._validate_filters() # should not raise + # -- _build_filters tests -----------------------------------------------------