From 78aad639658cdcfcf76a96ce5886594a1f36aaf8 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Wed, 10 Dec 2025 14:41:05 -0800 Subject: [PATCH 1/4] short fix to move id parameters to filters object --- .../mem0/agent_framework_mem0/_provider.py | 34 +++++++++++++++++-- .../mem0/tests/test_mem0_context_provider.py | 29 +++++++++++++--- .../context_providers/mem0/mem0_basic.py | 7 ++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/python/packages/mem0/agent_framework_mem0/_provider.py b/python/packages/mem0/agent_framework_mem0/_provider.py index 6d726f3a0f..2976ea445c 100644 --- a/python/packages/mem0/agent_framework_mem0/_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_provider.py @@ -55,6 +55,10 @@ def __init__( user_id: The user ID for scoping memories or None. scope_to_per_operation_thread_id: Whether to scope memories to per-operation thread ID. context_prompt: The prompt to prepend to retrieved memories. + + Note: + Currently, filters are set at initialization time via user_id, agent_id, thread_id, + and application_id. Run-level filtering support is planned for a future release. """ should_close_client = False if mem0_client is None: @@ -150,11 +154,12 @@ async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], * if not input_text.strip(): return Context(messages=None) + # Build filters from init parameters + filters = self._build_filters() + search_response: MemorySearchResponse_v1_1 | MemorySearchResponse_v2 = await self.mem0_client.search( # type: ignore[misc] query=input_text, - user_id=self.user_id, - agent_id=self.agent_id, - run_id=self._per_operation_thread_id if self.scope_to_per_operation_thread_id else self.thread_id, + filters=filters, ) # Depending on the API version, the response schema varies slightly @@ -185,6 +190,29 @@ def _validate_filters(self) -> None: "At least one of the filters: agent_id, user_id, application_id, or thread_id is required." ) + def _build_filters(self) -> dict[str, Any]: + """Build search filters from initialization parameters. + + Returns: + Filter dictionary for mem0 v2 search API containing initialization parameters. + In the v2 API, filters holds the user_id, agent_id, run_id (thread_id), and app_id + (application_id) which are required for scoping memory search operations. + """ + filters: dict[str, Any] = {} + + if self.user_id: + filters["user_id"] = self.user_id + if self.agent_id: + filters["agent_id"] = self.agent_id + if self.scope_to_per_operation_thread_id and self._per_operation_thread_id: + filters["run_id"] = self._per_operation_thread_id + elif self.thread_id: + filters["run_id"] = self.thread_id + if self.application_id: + filters["app_id"] = self.application_id + + return filters + def _validate_per_operation_thread_id(self, thread_id: str | None) -> None: """Validates that a new thread ID doesn't conflict with an existing one when scoped. diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index 4c1be141dc..d3dcf3b540 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -338,7 +338,7 @@ async def test_model_invoking_single_message(self, mock_mem0_client: AsyncMock) mock_mem0_client.search.assert_called_once() call_args = mock_mem0_client.search.call_args assert call_args.kwargs["query"] == "What's the weather?" - assert call_args.kwargs["user_id"] == "user123" + assert call_args.kwargs["filters"] == {"user_id": "user123"} assert isinstance(context, Context) expected_instructions = ( @@ -373,8 +373,7 @@ async def test_model_invoking_with_agent_id(self, mock_mem0_client: AsyncMock) - await provider.invoking(message) call_args = mock_mem0_client.search.call_args - assert call_args.kwargs["agent_id"] == "agent123" - assert call_args.kwargs["user_id"] is None + assert call_args.kwargs["filters"] == {"agent_id": "agent123"} async def test_model_invoking_with_scope_to_per_operation_thread_id(self, mock_mem0_client: AsyncMock) -> None: """Test invoking with scope_to_per_operation_thread_id enabled.""" @@ -392,7 +391,7 @@ async def test_model_invoking_with_scope_to_per_operation_thread_id(self, mock_m await provider.invoking(message) call_args = mock_mem0_client.search.call_args - assert call_args.kwargs["run_id"] == "operation_thread" + assert call_args.kwargs["filters"] == {"user_id": "user123", "run_id": "operation_thread"} async def test_model_invoking_no_memories_returns_none_instructions(self, mock_mem0_client: AsyncMock) -> None: """Test that no memories returns context with None instructions.""" @@ -510,3 +509,25 @@ def test_validate_per_operation_thread_id_disabled_scope(self, mock_mem0_client: # Should not raise exception even with different thread ID provider._validate_per_operation_thread_id("different_thread") + + +class TestMem0ProviderBuildFilters: + """Test the _build_filters method.""" + + def test_build_filters_with_all_parameters(self, mock_mem0_client: AsyncMock) -> None: + """Test building filters with all initialization parameters.""" + provider = Mem0Provider( + user_id="user123", + agent_id="agent456", + thread_id="thread789", + application_id="app999", + mem0_client=mock_mem0_client, + ) + + filters = provider._build_filters() + assert filters == { + "user_id": "user123", + "agent_id": "agent456", + "run_id": "thread789", + "app_id": "app999", + } diff --git a/python/samples/getting_started/context_providers/mem0/mem0_basic.py b/python/samples/getting_started/context_providers/mem0/mem0_basic.py index 0c2252d66a..5fef82d390 100644 --- a/python/samples/getting_started/context_providers/mem0/mem0_basic.py +++ b/python/samples/getting_started/context_providers/mem0/mem0_basic.py @@ -54,6 +54,13 @@ async def main() -> None: result = await agent.run(query) print(f"Agent: {result}\n") + # Mem0 processes and indexes memories asynchronously. + # Wait for memories to be indexed before querying in a new thread. + # In production, consider implementing retry logic or using Mem0's + # eventual consistency handling instead of a fixed delay. + print("Waiting for memories to be processed...") + await asyncio.sleep(12) # Empirically determined delay for Mem0 indexing + print("\nRequest within a new thread:") # Create a new thread for the agent. # The new thread has no context of the previous conversation. From 5d3634df7ee038d5e17e927f10b1b58f3eda3e2d Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Wed, 10 Dec 2025 15:04:06 -0800 Subject: [PATCH 2/4] added tests --- .../mem0/tests/test_mem0_context_provider.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index d3dcf3b540..7464aad913 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -514,6 +514,13 @@ def test_validate_per_operation_thread_id_disabled_scope(self, mock_mem0_client: class TestMem0ProviderBuildFilters: """Test the _build_filters method.""" + def test_build_filters_with_user_id_only(self, mock_mem0_client: AsyncMock) -> None: + """Test building filters with only user_id.""" + provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client) + + filters = provider._build_filters() + assert filters == {"user_id": "user123"} + def test_build_filters_with_all_parameters(self, mock_mem0_client: AsyncMock) -> None: """Test building filters with all initialization parameters.""" provider = Mem0Provider( @@ -531,3 +538,58 @@ def test_build_filters_with_all_parameters(self, mock_mem0_client: AsyncMock) -> "run_id": "thread789", "app_id": "app999", } + + def test_build_filters_excludes_none_values(self, mock_mem0_client: AsyncMock) -> None: + """Test that None values are excluded from filters.""" + provider = Mem0Provider( + user_id="user123", + agent_id=None, + thread_id=None, + application_id=None, + mem0_client=mock_mem0_client, + ) + + filters = provider._build_filters() + assert filters == {"user_id": "user123"} + assert "agent_id" not in filters + assert "run_id" not in filters + assert "app_id" not in filters + + def test_build_filters_with_per_operation_thread_id(self, mock_mem0_client: AsyncMock) -> None: + """Test that per-operation thread ID takes precedence over base thread_id.""" + provider = Mem0Provider( + user_id="user123", + thread_id="base_thread", + scope_to_per_operation_thread_id=True, + mem0_client=mock_mem0_client, + ) + provider._per_operation_thread_id = "operation_thread" + + filters = provider._build_filters() + assert filters == { + "user_id": "user123", + "run_id": "operation_thread", # Per-operation thread, not base_thread + } + + def test_build_filters_uses_base_thread_when_no_per_operation(self, mock_mem0_client: AsyncMock) -> None: + """Test that base thread_id is used when per-operation thread is not set.""" + provider = Mem0Provider( + user_id="user123", + thread_id="base_thread", + scope_to_per_operation_thread_id=True, + mem0_client=mock_mem0_client, + ) + # _per_operation_thread_id is None + + filters = provider._build_filters() + assert filters == { + "user_id": "user123", + "run_id": "base_thread", # Falls back to base thread_id + } + + def test_build_filters_returns_empty_dict_when_no_parameters(self, mock_mem0_client: AsyncMock) -> None: + """Test that _build_filters returns an empty dict when no parameters are set.""" + provider = Mem0Provider(mem0_client=mock_mem0_client) + + filters = provider._build_filters() + assert filters == {} From a6602619f0765ecdc853d465e33e36f36bebb563 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Wed, 10 Dec 2025 15:20:40 -0800 Subject: [PATCH 3/4] small fix --- python/packages/mem0/agent_framework_mem0/_provider.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/packages/mem0/agent_framework_mem0/_provider.py b/python/packages/mem0/agent_framework_mem0/_provider.py index 2976ea445c..48e508f411 100644 --- a/python/packages/mem0/agent_framework_mem0/_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_provider.py @@ -55,10 +55,6 @@ def __init__( user_id: The user ID for scoping memories or None. scope_to_per_operation_thread_id: Whether to scope memories to per-operation thread ID. context_prompt: The prompt to prepend to retrieved memories. - - Note: - Currently, filters are set at initialization time via user_id, agent_id, thread_id, - and application_id. Run-level filtering support is planned for a future release. """ should_close_client = False if mem0_client is None: From a713652833fceafbabbf3ffeab44efc5a7bd625f Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Tue, 16 Dec 2025 08:58:51 -0800 Subject: [PATCH 4/4] mem0 dependency update --- .../packages/core/agent_framework/openai/_shared.py | 2 +- python/packages/mem0/pyproject.toml | 2 +- python/uv.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/packages/core/agent_framework/openai/_shared.py b/python/packages/core/agent_framework/openai/_shared.py index 511c1f3379..e0df8844e4 100644 --- a/python/packages/core/agent_framework/openai/_shared.py +++ b/python/packages/core/agent_framework/openai/_shared.py @@ -63,7 +63,7 @@ def _check_openai_version_for_callable_api_key() -> None: raise ServiceInitializationError( f"Callable API keys require OpenAI SDK >= 1.106.0, but you have {openai.__version__}. " f"Please upgrade with 'pip install openai>=1.106.0' or provide a string API key instead. " - f"Note: If you're using mem0ai, you may need to upgrade to mem0ai>=0.1.118 " + f"Note: If you're using mem0ai, you may need to upgrade to mem0ai>=1.0.0 " f"to allow newer OpenAI versions." ) except ServiceInitializationError: diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index 140abca32b..3e80d6cdf9 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core", - "mem0ai>=0.1.117", + "mem0ai>=1.0.0", ] [tool.uv] diff --git a/python/uv.lock b/python/uv.lock index 581b9c6173..0f3e36ede6 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -530,7 +530,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "mem0ai", specifier = ">=0.1.117" }, + { name = "mem0ai", specifier = ">=1.0.0" }, ] [[package]] @@ -1341,7 +1341,7 @@ name = "clr-loader" version = "0.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/c2/da52aaf19424e3f0abec003d08dd1ccae52c88a3b41e31151a03bed18488/clr_loader-0.2.9.tar.gz", hash = "sha256:6af3d582c3de55ce9e9e676d2b3dbf6bc680c4ea8f76c58786739a5bdcf6b52d", size = 84829, upload-time = "2025-12-05T16:57:12.466Z" } wheels = [ @@ -1820,7 +1820,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -4473,8 +4473,8 @@ name = "powerfx" version = "0.0.33" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, + { name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/41/8f95f72f4f3b7ea54357c449bf5bd94813b6321dec31db9ffcbf578e2fa3/powerfx-0.0.33.tar.gz", hash = "sha256:85e8330bef8a7a207c3e010aa232df0ae38825e94d590c73daf3a3f44115cb09", size = 3236647, upload-time = "2025-11-20T19:31:09.414Z" } wheels = [ @@ -5143,7 +5143,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } wheels = [