From 17f8c1ffa79ee867a1fbe7552e9a91f6fc4a58c0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:46:39 +0000 Subject: [PATCH] fix: use simple dict filters for local mem0 Memory instead of AND/OR format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #4030 The local mem0 Memory class expects simple dict filters like {'user_id': 'bob'} instead of {'AND': [{'user_id': 'bob'}]}. The AND/OR filter format was being incorrectly converted to a RediSearch query like '@AND:{[{"user_id": "bob"}]} @user_id:{bob}' which caused the error 'Invalid filter expression'. This change: - Adds a for_local_memory parameter to _create_filter_for_search() - Returns simple dict filters for local Memory instances - Keeps AND/OR format for MemoryClient (cloud API) - Adds tests covering the issue scenario with valkey/redis Co-Authored-By: João --- .../src/crewai/memory/storage/mem0_storage.py | 45 +++++-- lib/crewai/tests/storage/test_mem0_storage.py | 123 +++++++++++++++++- 2 files changed, 154 insertions(+), 14 deletions(-) diff --git a/lib/crewai/src/crewai/memory/storage/mem0_storage.py b/lib/crewai/src/crewai/memory/storage/mem0_storage.py index 73820ab11..e99c6f366 100644 --- a/lib/crewai/src/crewai/memory/storage/mem0_storage.py +++ b/lib/crewai/src/crewai/memory/storage/mem0_storage.py @@ -65,16 +65,40 @@ class Mem0Storage(Storage): else Memory() ) - def _create_filter_for_search(self): + def _create_filter_for_search(self, for_local_memory: bool = False): """ Returns: - dict: A filter dictionary containing AND conditions for querying data. - - Includes user_id and agent_id if both are present. - - Includes user_id if only user_id is present. - - Includes agent_id if only agent_id is present. + dict: A filter dictionary for querying data. + + For MemoryClient (for_local_memory=False): + Returns a dict with AND/OR conditions: + - Includes user_id and agent_id if both are present (OR). + - Includes user_id if only user_id is present (AND). + - Includes agent_id if only agent_id is present (AND). - Includes run_id if memory_type is 'short_term' and - mem0_run_id is present. + mem0_run_id is present (AND). + + For local Memory (for_local_memory=True): + Returns a simple dict with field-value pairs that the local + mem0 Memory class can convert to a valid filter expression. + Note: When both user_id and agent_id are present, only user_id + is included since local Memory doesn't support OR conditions. """ + if for_local_memory: + filters: dict[str, Any] = {} + if self.memory_type == "short_term" and self.mem0_run_id: + filters["run_id"] = self.mem0_run_id + else: + user_id = self.config.get("user_id", "") + agent_id = self.config.get("agent_id", "") + + if user_id: + filters["user_id"] = user_id + if agent_id: + filters["agent_id"] = agent_id + + return filters + filter = defaultdict(list) if self.memory_type == "short_term" and self.mem0_run_id: @@ -176,16 +200,17 @@ class Mem0Storage(Storage): if self.memory_type == "short_term": params["run_id"] = self.mem0_run_id - # Discard the filters for now since we create the filters - # automatically when the crew is created. - - params["filters"] = self._create_filter_for_search() params["threshold"] = score_threshold if isinstance(self.memory, Memory): + # For local Memory, use simple dict filters instead of AND/OR format + params["filters"] = self._create_filter_for_search(for_local_memory=True) del params["metadata"], params["version"], params["output_format"] if params.get("run_id"): del params["run_id"] + else: + # For MemoryClient, use AND/OR filter format + params["filters"] = self._create_filter_for_search(for_local_memory=False) results = self.memory.search(**params) diff --git a/lib/crewai/tests/storage/test_mem0_storage.py b/lib/crewai/tests/storage/test_mem0_storage.py index f219f0b45..c7558f1f5 100644 --- a/lib/crewai/tests/storage/test_mem0_storage.py +++ b/lib/crewai/tests/storage/test_mem0_storage.py @@ -362,7 +362,11 @@ def test_save_method_with_memory_client( def test_search_method_with_memory_oss(mem0_storage_with_mocked_config): - """Test search method for different memory types""" + """Test search method for local Memory (OSS). + + Local Memory expects simple dict filters like {"run_id": "my_run_id"} + instead of {"AND": [{"run_id": "my_run_id"}]}. + """ mem0_storage, _, _ = mem0_storage_with_mocked_config mock_results = { "results": [ @@ -374,11 +378,12 @@ def test_search_method_with_memory_oss(mem0_storage_with_mocked_config): results = mem0_storage.search("test query", limit=5, score_threshold=0.5) + # Local Memory uses simple dict filters instead of AND format mem0_storage.memory.search.assert_called_once_with( query="test query", limit=5, user_id="test_user", - filters={"AND": [{"run_id": "my_run_id"}]}, + filters={"run_id": "my_run_id"}, threshold=0.5, ) @@ -446,6 +451,11 @@ def test_save_memory_using_agent_entity(mock_mem0_memory_client): def test_search_method_with_agent_entity(): + """Test search with local Memory using agent_id only. + + Local Memory expects simple dict filters like {"agent_id": "agent-123"} + instead of {"AND": [{"agent_id": "agent-123"}]}. + """ config = { "agent_id": "agent-123", } @@ -464,10 +474,11 @@ def test_search_method_with_agent_entity(): mem0_storage.memory.search = MagicMock(return_value=mock_results) results = mem0_storage.search("test query", limit=5, score_threshold=0.5) + # Local Memory uses simple dict filters instead of AND/OR format mem0_storage.memory.search.assert_called_once_with( query="test query", limit=5, - filters={"AND": [{"agent_id": "agent-123"}]}, + filters={"agent_id": "agent-123"}, threshold=0.5, ) @@ -476,6 +487,11 @@ def test_search_method_with_agent_entity(): def test_search_method_with_agent_id_and_user_id(): + """Test search with local Memory using both agent_id and user_id. + + Local Memory expects simple dict filters like {"user_id": "user-123", "agent_id": "agent-123"} + instead of {"OR": [{"user_id": "user-123"}, {"agent_id": "agent-123"}]}. + """ mock_memory = MagicMock(spec=Memory) mock_results = { "results": [ @@ -492,13 +508,112 @@ def test_search_method_with_agent_id_and_user_id(): mem0_storage.memory.search = MagicMock(return_value=mock_results) results = mem0_storage.search("test query", limit=5, score_threshold=0.5) + # Local Memory uses simple dict filters instead of OR format mem0_storage.memory.search.assert_called_once_with( query="test query", limit=5, user_id="user-123", - filters={"OR": [{"user_id": "user-123"}, {"agent_id": "agent-123"}]}, + filters={"user_id": "user-123", "agent_id": "agent-123"}, threshold=0.5, ) assert len(results) == 2 assert results[0]["content"] == "Result 1" + + +def test_search_method_with_user_id_only_local_memory(): + """Test search with local Memory using user_id only. + + This test covers the issue reported in GitHub issue #4030 where using + external memory with mem0 and valkey/redis fails with: + 'Invalid filter expression: @AND:{[{'user_id': 'bob'}]} @user_id:{bob}' + + Local Memory expects simple dict filters like {"user_id": "bob"} + instead of {"AND": [{"user_id": "bob"}]}. + """ + mock_memory = MagicMock(spec=Memory) + mock_results = { + "results": [ + {"score": 0.9, "memory": "Result 1"}, + {"score": 0.4, "memory": "Result 2"}, + ] + } + + with patch.object(Memory, "__new__", return_value=mock_memory): + mem0_storage = Mem0Storage( + type="external", config={"user_id": "bob"} + ) + + mem0_storage.memory.search = MagicMock(return_value=mock_results) + results = mem0_storage.search("test query", limit=5, score_threshold=0.5) + + # Local Memory uses simple dict filters instead of AND format + # This fixes the issue where {"AND": [{"user_id": "bob"}]} was being + # incorrectly converted to "@AND:{[{'user_id': 'bob'}]} @user_id:{bob}" + mem0_storage.memory.search.assert_called_once_with( + query="test query", + limit=5, + user_id="bob", + filters={"user_id": "bob"}, + threshold=0.5, + ) + + assert len(results) == 2 + assert results[0]["content"] == "Result 1" + + +def test_search_method_with_local_mem0_config(): + """Test search with local Memory using local_mem0_config. + + This test simulates the exact scenario from GitHub issue #4030 where + external memory is configured with local_mem0_config for valkey/redis. + """ + mock_memory = MagicMock(spec=Memory) + mock_results = { + "results": [ + {"score": 0.9, "memory": "Result 1"}, + ] + } + + local_config = { + "vector_store": { + "provider": "valkey", + "config": { + "collection_name": "mem0test1", + "valkey_url": "valkey://localhost:6380", + "embedding_model_dims": 1024, + "index_type": "hnsw" + } + }, + "llm": { + "provider": "mock_llm", + "config": {"model": "mock-model"} + }, + "embedder": { + "provider": "mock_embedder", + "config": {"model": "mock-model"} + } + } + + config = { + "user_id": "bob", + "local_mem0_config": local_config + } + + with patch.object(Memory, "from_config", return_value=mock_memory): + mem0_storage = Mem0Storage(type="external", config=config) + + mem0_storage.memory.search = MagicMock(return_value=mock_results) + results = mem0_storage.search("test query", limit=5, score_threshold=0.5) + + # Verify that filters use simple dict format for local Memory + mem0_storage.memory.search.assert_called_once_with( + query="test query", + limit=5, + user_id="bob", + filters={"user_id": "bob"}, + threshold=0.5, + ) + + assert len(results) == 1 + assert results[0]["content"] == "Result 1"