refactor: update MemoryClient to use filters for entity parameters

This commit is contained in:
kartik-mem0
2026-04-14 16:17:50 +05:30
parent 133625bbc6
commit 28cc8fcee4
29 changed files with 1440 additions and 453 deletions
+149
View File
@@ -0,0 +1,149 @@
"""Tests for MemoryClient entity parameter rejection."""
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def mock_memory_client():
"""Create a mock MemoryClient for testing entity param rejection."""
with patch("mem0.client.main.httpx.Client") as mock_httpx:
# Create a mock client instance
mock_http_client = MagicMock()
mock_http_client.get.return_value = MagicMock(
json=lambda: {"org_id": "org1", "project_id": "proj1", "user_email": "test@test.com"},
raise_for_status=lambda: None
)
mock_httpx.return_value = mock_http_client
with patch("mem0.client.main.capture_client_event"):
from mem0.client.main import MemoryClient
client = MemoryClient(api_key="test-api-key")
yield client
class TestSearchEntityParamRejection:
"""Tests that top-level entity params are rejected in search()."""
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
"""search() should reject user_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"user_id"):
mock_memory_client.search("test query", user_id="u1")
def test_search_rejects_agent_id_kwarg(self, mock_memory_client):
"""search() should reject agent_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"agent_id"):
mock_memory_client.search("test query", agent_id="a1")
def test_search_rejects_app_id_kwarg(self, mock_memory_client):
"""search() should reject app_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"app_id"):
mock_memory_client.search("test query", app_id="app1")
def test_search_rejects_run_id_kwarg(self, mock_memory_client):
"""search() should reject run_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"run_id"):
mock_memory_client.search("test query", run_id="r1")
def test_search_rejects_multiple_entity_params(self, mock_memory_client):
"""search() should reject multiple top-level entity params."""
with pytest.raises(ValueError, match=r"user_id|agent_id"):
mock_memory_client.search("test query", user_id="u1", agent_id="a1")
class TestGetAllEntityParamRejection:
"""Tests that top-level entity params are rejected in get_all()."""
def test_get_all_rejects_user_id_kwarg(self, mock_memory_client):
"""get_all() should reject user_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"user_id"):
mock_memory_client.get_all(user_id="u1")
def test_get_all_rejects_agent_id_kwarg(self, mock_memory_client):
"""get_all() should reject agent_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"agent_id"):
mock_memory_client.get_all(agent_id="a1")
def test_get_all_rejects_app_id_kwarg(self, mock_memory_client):
"""get_all() should reject app_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"app_id"):
mock_memory_client.get_all(app_id="app1")
def test_get_all_rejects_run_id_kwarg(self, mock_memory_client):
"""get_all() should reject run_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"run_id"):
mock_memory_client.get_all(run_id="r1")
class TestFilterOperatorPassthrough:
"""Tests that AND/OR/NOT filter operators are passed through to the API."""
def test_search_passes_and_filters(self, mock_memory_client):
"""search() should pass AND filters to the API."""
mock_memory_client.client.post.return_value = MagicMock(
json=lambda: {"results": []},
raise_for_status=lambda: None
)
mock_memory_client.search(
"test query",
filters={"AND": [{"user_id": "u1"}, {"created_at": {"gte": "2024-01-01"}}]}
)
# Verify the POST was called with filters intact
call_args = mock_memory_client.client.post.call_args
payload = call_args.kwargs.get("json", call_args.args[1] if len(call_args.args) > 1 else {})
assert payload["filters"] == {"AND": [{"user_id": "u1"}, {"created_at": {"gte": "2024-01-01"}}]}
def test_search_passes_or_filters(self, mock_memory_client):
"""search() should pass OR filters to the API."""
mock_memory_client.client.post.return_value = MagicMock(
json=lambda: {"results": []},
raise_for_status=lambda: None
)
mock_memory_client.search(
"test query",
filters={"OR": [{"user_id": "u1"}, {"agent_id": "a1"}]}
)
call_args = mock_memory_client.client.post.call_args
payload = call_args.kwargs.get("json", call_args.args[1] if len(call_args.args) > 1 else {})
assert payload["filters"] == {"OR": [{"user_id": "u1"}, {"agent_id": "a1"}]}
def test_search_passes_not_filters(self, mock_memory_client):
"""search() should pass NOT filters to the API."""
mock_memory_client.client.post.return_value = MagicMock(
json=lambda: {"results": []},
raise_for_status=lambda: None
)
mock_memory_client.search(
"test query",
filters={"AND": [{"user_id": "u1"}, {"NOT": {"categories": {"in": ["spam"]}}}]}
)
call_args = mock_memory_client.client.post.call_args
payload = call_args.kwargs.get("json", call_args.args[1] if len(call_args.args) > 1 else {})
assert payload["filters"] == {"AND": [{"user_id": "u1"}, {"NOT": {"categories": {"in": ["spam"]}}}]}
def test_search_passes_complex_nested_filters(self, mock_memory_client):
"""search() should pass complex nested AND/OR/NOT filters to the API."""
mock_memory_client.client.post.return_value = MagicMock(
json=lambda: {"results": []},
raise_for_status=lambda: None
)
complex_filter = {
"AND": [
{"user_id": "u1"},
{"created_at": {"gte": "2024-01-01"}},
{"NOT": {"OR": [{"categories": {"in": ["spam"]}}, {"categories": {"in": ["test"]}}]}}
]
}
mock_memory_client.search("test query", filters=complex_filter)
call_args = mock_memory_client.client.post.call_args
payload = call_args.kwargs.get("json", call_args.args[1] if len(call_args.args) > 1 else {})
assert payload["filters"] == complex_filter
+7 -4
View File
@@ -57,7 +57,10 @@ def test_add(memory_instance, version):
memory_instance.config.version = version
memory_instance._add_to_vector_store = Mock(return_value=[{"memory": "Test memory", "event": "ADD"}])
result = memory_instance.add(messages=[{"role": "user", "content": "Test message"}], user_id="test_user")
result = memory_instance.add(
messages=[{"role": "user", "content": "Test message"}],
filters={"user_id": "test_user"},
)
assert "results" in result
assert result["results"] == [{"memory": "Test memory", "event": "ADD"}]
@@ -105,7 +108,7 @@ def test_search(memory_instance, version):
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
patch("mem0.memory.main.extract_entities", return_value=[]):
result = memory_instance.search("test query", user_id="test_user")
result = memory_instance.search("test query", filters={"user_id": "test_user"})
assert "results" in result
assert len(result["results"]) == 2
@@ -185,7 +188,7 @@ def test_delete_all(memory_instance, version):
memory_instance.vector_store.reset = Mock()
memory_instance._delete_memory = Mock()
result = memory_instance.delete_all(user_id="test_user")
result = memory_instance.delete_all(filters={"user_id": "test_user"})
assert memory_instance._delete_memory.call_count == 2
# Ensure the collection is NOT dropped — only matched memories should be removed
@@ -206,7 +209,7 @@ def test_get_all(memory_instance, version, expected_result):
mock_memories = [Mock(id="1", payload={"data": "Memory 1", "user_id": "test_user"})]
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
result = memory_instance.get_all(user_id="test_user")
result = memory_instance.get_all(filters={"user_id": "test_user"})
assert isinstance(result, dict)
assert "results" in result
+48 -11
View File
@@ -33,20 +33,20 @@ def memory_client():
def test_create_memory(memory_client):
data = "Name is John Doe."
result = memory_client.add([{"role": "user", "content": data}], user_id="test_user")
result = memory_client.add([{"role": "user", "content": data}], filters={"user_id": "test_user"})
assert result["results"][0]["memory"] == data
def test_get_memory(memory_client):
data = "Name is John Doe."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
memory_client.add([{"role": "user", "content": data}], filters={"user_id": "test_user"})
result = memory_client.get("1")
assert result["memory"] == data
def test_update_memory(memory_client):
data = "Name is John Doe."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
memory_client.add([{"role": "user", "content": data}], filters={"user_id": "test_user"})
new_data = "Name is John Kapoor."
update_result = memory_client.update("1", text=new_data)
assert update_result["message"] == "Memory updated successfully!"
@@ -54,14 +54,14 @@ def test_update_memory(memory_client):
def test_delete_memory(memory_client):
data = "Name is John Doe."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
memory_client.add([{"role": "user", "content": data}], filters={"user_id": "test_user"})
delete_result = memory_client.delete("1")
assert delete_result["message"] == "Memory deleted successfully!"
def test_history(memory_client):
data = "I like Indian food."
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
memory_client.add([{"role": "user", "content": data}], filters={"user_id": "test_user"})
memory_client.update("1", text="I like Italian food.")
history = memory_client.history("1")
assert history[0]["memory"] == "I like Indian food."
@@ -71,9 +71,9 @@ def test_history(memory_client):
def test_list_memories(memory_client):
data1 = "Name is John Doe."
data2 = "Name is John Doe. I like to code in Python."
memory_client.add([{"role": "user", "content": data1}], user_id="test_user")
memory_client.add([{"role": "user", "content": data2}], user_id="test_user")
memories = memory_client.get_all(user_id="test_user")
memory_client.add([{"role": "user", "content": data1}], filters={"user_id": "test_user"})
memory_client.add([{"role": "user", "content": data2}], filters={"user_id": "test_user"})
memories = memory_client.get_all(filters={"user_id": "test_user"})
assert data1 in memories
assert data2 in memories
@@ -469,7 +469,7 @@ def test_add_infer_false_embeds_once(mock_sqlite, mock_llm_factory, mock_vector_
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
memory.add("foo", user_id="test_user", infer=False)
memory.add("foo", filters={"user_id": "test_user"}, infer=False)
assert embedder.embed.call_count == 1
mock_vector_store.insert.assert_called_once()
@@ -511,7 +511,7 @@ def test_add_infer_true_caches_embedding_on_llm_rewrite(mock_sqlite, mock_llm_fa
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
memory.add("I like Python", user_id="test_user", infer=True)
memory.add("I like Python", filters={"user_id": "test_user"}, infer=True)
# V3 pipeline: embed called once for search query (Phase 1),
# embed_batch called once for extracted memories (Phase 3)
@@ -568,7 +568,7 @@ def test_update_infer_true_caches_embedding_on_llm_rewrite(mock_sqlite, mock_llm
from mem0.memory.main import Memory as MemoryClass
memory = MemoryClass(MemoryConfig())
memory.add("I love Python now", user_id="test_user", infer=True)
memory.add("I love Python now", filters={"user_id": "test_user"}, infer=True)
# V3 pipeline: embed called once for search query (Phase 1),
# embed_batch called once for extracted memories (Phase 3)
@@ -785,3 +785,40 @@ def test_reset_skips_graph_when_graph_disabled(mock_sqlite, mock_llm_factory, mo
# graph should remain None after reset
assert memory.graph is None
# ─── Entity Param Rejection Tests ─────────────────────────────────────────────
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_search_rejects_user_id_kwarg(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""search() should reject user_id as top-level kwarg."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
config = MemoryConfig()
memory = Memory(config)
with pytest.raises(ValueError, match=r"user_id.*filters"):
memory.search("test query", user_id="u1")
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.storage.SQLiteManager')
def test_get_all_rejects_user_id_kwarg(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""get_all() should reject user_id as top-level kwarg."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_factory.return_value = MagicMock()
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
config = MemoryConfig()
memory = Memory(config)
with pytest.raises(ValueError, match=r"user_id.*filters"):
memory.get_all(user_id="u1")