Skip to content

Commit 54f482d

Browse files
authored
Python: Update Mem0Provider to use v2 search API filters parameter (#2766)
* short fix to move id parameters to filters object * added tests * small fix * mem0 dependency update
1 parent 754dfb2 commit 54f482d

File tree

6 files changed

+129
-15
lines changed

6 files changed

+129
-15
lines changed

python/packages/core/agent_framework/openai/_shared.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def _check_openai_version_for_callable_api_key() -> None:
6363
raise ServiceInitializationError(
6464
f"Callable API keys require OpenAI SDK >= 1.106.0, but you have {openai.__version__}. "
6565
f"Please upgrade with 'pip install openai>=1.106.0' or provide a string API key instead. "
66-
f"Note: If you're using mem0ai, you may need to upgrade to mem0ai>=0.1.118 "
66+
f"Note: If you're using mem0ai, you may need to upgrade to mem0ai>=1.0.0 "
6767
f"to allow newer OpenAI versions."
6868
)
6969
except ServiceInitializationError:

python/packages/mem0/agent_framework_mem0/_provider.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,12 @@ async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], *
150150
if not input_text.strip():
151151
return Context(messages=None)
152152

153+
# Build filters from init parameters
154+
filters = self._build_filters()
155+
153156
search_response: MemorySearchResponse_v1_1 | MemorySearchResponse_v2 = await self.mem0_client.search( # type: ignore[misc]
154157
query=input_text,
155-
user_id=self.user_id,
156-
agent_id=self.agent_id,
157-
run_id=self._per_operation_thread_id if self.scope_to_per_operation_thread_id else self.thread_id,
158+
filters=filters,
158159
)
159160

160161
# Depending on the API version, the response schema varies slightly
@@ -185,6 +186,29 @@ def _validate_filters(self) -> None:
185186
"At least one of the filters: agent_id, user_id, application_id, or thread_id is required."
186187
)
187188

189+
def _build_filters(self) -> dict[str, Any]:
190+
"""Build search filters from initialization parameters.
191+
192+
Returns:
193+
Filter dictionary for mem0 v2 search API containing initialization parameters.
194+
In the v2 API, filters holds the user_id, agent_id, run_id (thread_id), and app_id
195+
(application_id) which are required for scoping memory search operations.
196+
"""
197+
filters: dict[str, Any] = {}
198+
199+
if self.user_id:
200+
filters["user_id"] = self.user_id
201+
if self.agent_id:
202+
filters["agent_id"] = self.agent_id
203+
if self.scope_to_per_operation_thread_id and self._per_operation_thread_id:
204+
filters["run_id"] = self._per_operation_thread_id
205+
elif self.thread_id:
206+
filters["run_id"] = self.thread_id
207+
if self.application_id:
208+
filters["app_id"] = self.application_id
209+
210+
return filters
211+
188212
def _validate_per_operation_thread_id(self, thread_id: str | None) -> None:
189213
"""Validates that a new thread ID doesn't conflict with an existing one when scoped.
190214

python/packages/mem0/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ classifiers = [
2424
]
2525
dependencies = [
2626
"agent-framework-core",
27-
"mem0ai>=0.1.117",
27+
"mem0ai>=1.0.0",
2828
]
2929

3030
[tool.uv]

python/packages/mem0/tests/test_mem0_context_provider.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ async def test_model_invoking_single_message(self, mock_mem0_client: AsyncMock)
338338
mock_mem0_client.search.assert_called_once()
339339
call_args = mock_mem0_client.search.call_args
340340
assert call_args.kwargs["query"] == "What's the weather?"
341-
assert call_args.kwargs["user_id"] == "user123"
341+
assert call_args.kwargs["filters"] == {"user_id": "user123"}
342342

343343
assert isinstance(context, Context)
344344
expected_instructions = (
@@ -373,8 +373,7 @@ async def test_model_invoking_with_agent_id(self, mock_mem0_client: AsyncMock) -
373373
await provider.invoking(message)
374374

375375
call_args = mock_mem0_client.search.call_args
376-
assert call_args.kwargs["agent_id"] == "agent123"
377-
assert call_args.kwargs["user_id"] is None
376+
assert call_args.kwargs["filters"] == {"agent_id": "agent123"}
378377

379378
async def test_model_invoking_with_scope_to_per_operation_thread_id(self, mock_mem0_client: AsyncMock) -> None:
380379
"""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
392391
await provider.invoking(message)
393392

394393
call_args = mock_mem0_client.search.call_args
395-
assert call_args.kwargs["run_id"] == "operation_thread"
394+
assert call_args.kwargs["filters"] == {"user_id": "user123", "run_id": "operation_thread"}
396395

397396
async def test_model_invoking_no_memories_returns_none_instructions(self, mock_mem0_client: AsyncMock) -> None:
398397
"""Test that no memories returns context with None instructions."""
@@ -510,3 +509,87 @@ def test_validate_per_operation_thread_id_disabled_scope(self, mock_mem0_client:
510509

511510
# Should not raise exception even with different thread ID
512511
provider._validate_per_operation_thread_id("different_thread")
512+
513+
514+
class TestMem0ProviderBuildFilters:
515+
"""Test the _build_filters method."""
516+
517+
def test_build_filters_with_user_id_only(self, mock_mem0_client: AsyncMock) -> None:
518+
"""Test building filters with only user_id."""
519+
provider = Mem0Provider(user_id="user123", mem0_client=mock_mem0_client)
520+
521+
filters = provider._build_filters()
522+
assert filters == {"user_id": "user123"}
523+
524+
def test_build_filters_with_all_parameters(self, mock_mem0_client: AsyncMock) -> None:
525+
"""Test building filters with all initialization parameters."""
526+
provider = Mem0Provider(
527+
user_id="user123",
528+
agent_id="agent456",
529+
thread_id="thread789",
530+
application_id="app999",
531+
mem0_client=mock_mem0_client,
532+
)
533+
534+
filters = provider._build_filters()
535+
assert filters == {
536+
"user_id": "user123",
537+
"agent_id": "agent456",
538+
"run_id": "thread789",
539+
"app_id": "app999",
540+
}
541+
542+
def test_build_filters_excludes_none_values(self, mock_mem0_client: AsyncMock) -> None:
543+
"""Test that None values are excluded from filters."""
544+
provider = Mem0Provider(
545+
user_id="user123",
546+
agent_id=None,
547+
thread_id=None,
548+
application_id=None,
549+
mem0_client=mock_mem0_client,
550+
)
551+
552+
filters = provider._build_filters()
553+
assert filters == {"user_id": "user123"}
554+
assert "agent_id" not in filters
555+
assert "run_id" not in filters
556+
assert "app_id" not in filters
557+
558+
def test_build_filters_with_per_operation_thread_id(self, mock_mem0_client: AsyncMock) -> None:
559+
"""Test that per-operation thread ID takes precedence over base thread_id."""
560+
provider = Mem0Provider(
561+
user_id="user123",
562+
thread_id="base_thread",
563+
scope_to_per_operation_thread_id=True,
564+
mem0_client=mock_mem0_client,
565+
)
566+
provider._per_operation_thread_id = "operation_thread"
567+
568+
filters = provider._build_filters()
569+
assert filters == {
570+
"user_id": "user123",
571+
"run_id": "operation_thread", # Per-operation thread, not base_thread
572+
}
573+
574+
def test_build_filters_uses_base_thread_when_no_per_operation(self, mock_mem0_client: AsyncMock) -> None:
575+
"""Test that base thread_id is used when per-operation thread is not set."""
576+
provider = Mem0Provider(
577+
user_id="user123",
578+
thread_id="base_thread",
579+
scope_to_per_operation_thread_id=True,
580+
mem0_client=mock_mem0_client,
581+
)
582+
# _per_operation_thread_id is None
583+
584+
filters = provider._build_filters()
585+
assert filters == {
586+
"user_id": "user123",
587+
"run_id": "base_thread", # Falls back to base thread_id
588+
}
589+
590+
def test_build_filters_returns_empty_dict_when_no_parameters(self, mock_mem0_client: AsyncMock) -> None:
591+
"""Test that _build_filters returns an empty dict when no parameters are set."""
592+
provider = Mem0Provider(mem0_client=mock_mem0_client)
593+
594+
filters = provider._build_filters()
595+
assert filters == {}

python/samples/getting_started/context_providers/mem0/mem0_basic.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ async def main() -> None:
5454
result = await agent.run(query)
5555
print(f"Agent: {result}\n")
5656

57+
# Mem0 processes and indexes memories asynchronously.
58+
# Wait for memories to be indexed before querying in a new thread.
59+
# In production, consider implementing retry logic or using Mem0's
60+
# eventual consistency handling instead of a fixed delay.
61+
print("Waiting for memories to be processed...")
62+
await asyncio.sleep(12) # Empirically determined delay for Mem0 indexing
63+
5764
print("\nRequest within a new thread:")
5865
# Create a new thread for the agent.
5966
# The new thread has no context of the previous conversation.

python/uv.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)