Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions python/packages/mem0/agent_framework_mem0/_context_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -108,16 +111,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.
Comment thread
youneshima marked this conversation as resolved.
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"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we update the corresponding platform add path to use this same 2.x filters shape before bumping the dependency? before_run() now searches with filters={...}, but after_run() still calls AsyncMemoryClient.add() with top-level user_id/agent_id and metadata={"application_id": ...}; in mem0ai 2.0.4, that becomes a /v3/memories/add/ payload whose identity fields are not under filters. That can reject platform stores or write memories under a scope this new search path will not read back, and the existing store test still asserts the old payload shape.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in replacement PR #7004, commit 5018e46: Platform AsyncMemoryClient.add now sends user_id, agent_id, and app_id under filters, while OSS AsyncMemory.add keeps its top-level user_id/agent_id signature.


search_response: _MemorySearchResponse_v1_1 | _MemorySearchResponse_v2 = await self.mem0_client.search( # type: ignore[misc]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above _MemorySearchResponse_v1_1 can be deprecated

**search_kwargs,
query=input_text,
filters=filters,
Comment thread
youneshima marked this conversation as resolved.
)

if isinstance(search_response, list):
Expand Down Expand Up @@ -169,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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the intent but messaging can be improved, right now even if users are sending application_id (in OSS client) it would seem as if it matters but actually it doesn't

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."""
Expand Down
2 changes: 1 addition & 1 deletion python/packages/mem0/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
56 changes: 48 additions & 8 deletions python/packages/mem0/tests/test_mem0_context_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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"
Expand All @@ -207,9 +207,22 @@ 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_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")

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]

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."""
Expand All @@ -227,6 +240,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 -----------------------------------------------------------

Expand Down Expand Up @@ -357,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 -----------------------------------------------------

Expand Down
8 changes: 4 additions & 4 deletions python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.