Skip to content
Open
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
122 changes: 122 additions & 0 deletions tests/memory/long_term_memory_backends/test_mem0_ltm_backend.py
Original file line number Diff line number Diff line change
@@ -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"}
]
35 changes: 15 additions & 20 deletions veadk/memory/long_term_memory_backends/mem0_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
)
Expand All @@ -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:
Expand All @@ -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}")
Expand Down