diff --git a/tests/memory/long_term_memory_backends/test_mem0_ltm_backend.py b/tests/memory/long_term_memory_backends/test_mem0_ltm_backend.py new file mode 100644 index 000000000..30982ad39 --- /dev/null +++ b/tests/memory/long_term_memory_backends/test_mem0_ltm_backend.py @@ -0,0 +1,122 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Mem0LTMBackend must scope memories by LongTermMemory.index (app_name). + +Other LTM backends (VikingDB collection, Redis/OpenSearch index name, +OpenViking app_name, TOS ContextBucket name) all use ``index`` to keep +distinct apps under the same credentials from sharing memories. Mem0 +accepts the same isolation via ``agent_id``; without it, two +``LongTermMemory(backend="mem0", app_name=...)`` instances that share a +Mem0 project leak memories across apps for the same user_id. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from veadk.configs.database_configs import Mem0Config +from veadk.memory.long_term_memory_backends import mem0_backend +from veadk.memory.long_term_memory_backends.mem0_backend import Mem0LTMBackend + + +class _FakeMemoryClient: + """Shared in-memory stand-in that records add/search kwargs like Mem0 1.x. + + Class-level ``_store`` simulates one Mem0 project shared by every client + created with the same credentials. + """ + + _store: dict[tuple[str, str | None], list[str]] = {} + + def __init__(self, host: str | None = None, api_key: str | None = None): + self.host = host + self.api_key = api_key + self.add_calls: list[dict[str, Any]] = [] + self.search_calls: list[dict[str, Any]] = [] + + def add(self, messages, **kwargs): + self.add_calls.append({"messages": messages, **kwargs}) + user_id = kwargs.get("user_id", "default_user") + agent_id = kwargs.get("agent_id") + key = (user_id, agent_id) + for message in messages: + self._store.setdefault(key, []).append(message["content"]) + return {"results": []} + + def search(self, query: str, **kwargs): + self.search_calls.append({"query": query, **kwargs}) + user_id = kwargs.get("user_id", "default_user") + agent_id = kwargs.get("agent_id") + memories = list(self._store.get((user_id, agent_id), [])) + top_k = kwargs.get("top_k", len(memories)) + return {"results": [{"memory": m} for m in memories[:top_k]]} + + +@pytest.fixture +def fake_mem0(monkeypatch): + _FakeMemoryClient._store = {} + monkeypatch.setattr(mem0_backend, "MemoryClient", _FakeMemoryClient) + return _FakeMemoryClient + + +def _backend(index: str) -> Mem0LTMBackend: + return Mem0LTMBackend( + index=index, + mem0_config=Mem0Config(api_key="test-key", base_url="https://mem0.example"), + ) + + +def test_save_and_search_pass_index_as_agent_id(fake_mem0): + backend = _backend("app_alpha") + + assert backend.save_memory(user_id="u1", event_strings=["alpha secret"]) + results = backend.search_memory(user_id="u1", query="secret", top_k=5) + + assert backend._mem0_client.add_calls[0]["agent_id"] == "app_alpha" + assert backend._mem0_client.search_calls[0]["agent_id"] == "app_alpha" + assert results == ["alpha secret"] + + +def test_different_indexes_do_not_share_memories_for_same_user(fake_mem0): + alpha = _backend("app_alpha") + beta = _backend("app_beta") + + alpha.save_memory(user_id="u1", event_strings=["alpha-only memory"]) + beta.save_memory(user_id="u1", event_strings=["beta-only memory"]) + + assert alpha.search_memory(user_id="u1", query="memory", top_k=5) == [ + "alpha-only memory" + ] + assert beta.search_memory(user_id="u1", query="memory", top_k=5) == [ + "beta-only memory" + ] + + +def test_save_memory_signature_matches_base_backend(fake_mem0): + """BaseLongTermMemoryBackend.save_memory(user_id, event_strings, ...). + + Callers (and LongTermMemory) may pass args positionally; Mem0 must not + swap the parameters. + """ + backend = _backend("app_alpha") + + # positional: user_id first, then event_strings (base-class order) + assert backend.save_memory("u1", ["positional ok"]) is True + assert backend._mem0_client.add_calls[0]["user_id"] == "u1" + assert backend._mem0_client.add_calls[0]["messages"] == [ + {"role": "user", "content": "positional ok"} + ] diff --git a/veadk/memory/long_term_memory_backends/mem0_backend.py b/veadk/memory/long_term_memory_backends/mem0_backend.py index 7e59404fb..d7a6ce3bc 100644 --- a/veadk/memory/long_term_memory_backends/mem0_backend.py +++ b/veadk/memory/long_term_memory_backends/mem0_backend.py @@ -76,17 +76,12 @@ def precheck_index_naming(self): pass @override - def save_memory( - self, event_strings: list[str], user_id: str = "default_user", **kwargs - ) -> bool: - """Save memory to Mem0 + def save_memory(self, user_id: str, event_strings: list[str], **kwargs) -> bool: + """Save memory to Mem0. - Args: - event_strings: List of event strings to save - **kwargs: Additional parameters, including 'user_id' for Mem0 - - Returns: - bool: True if saved successfully, False otherwise + ``self.index`` (LongTermMemory app_name) is passed as Mem0 ``agent_id`` + so distinct apps that share one Mem0 project stay isolated, matching + how VikingDB/Redis/OpenSearch backends use ``index``. """ try: logger.info( @@ -98,6 +93,7 @@ def save_memory( result = self._mem0_client.add( [{"role": "user", "content": event_string}], user_id=user_id, + agent_id=self.index, output_format="v1.1", async_mode=True, ) @@ -111,17 +107,12 @@ def save_memory( @override def search_memory( - self, query: str, top_k: int, user_id: str = "default_user", **kwargs + self, user_id: str, query: str, top_k: int, **kwargs ) -> list[str]: - """Search memory from Mem0 - - Args: - query: Search query - top_k: Number of results to return - **kwargs: Additional parameters, including 'user_id' for Mem0 + """Search memory from Mem0. - Returns: - list[str]: List of memory strings + Filters by the same ``agent_id=self.index`` used on save so results + stay scoped to this LongTermMemory instance. """ try: @@ -130,7 +121,11 @@ def search_memory( ) memories = self._mem0_client.search( - query, user_id=user_id, output_format="v1.1", top_k=top_k + query, + user_id=user_id, + agent_id=self.index, + output_format="v1.1", + top_k=top_k, ) logger.debug(f"return relevant memories: {memories}")