diff --git a/mem0/memory/main.py b/mem0/memory/main.py index 17b13bcba..ec14b6815 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -977,11 +977,11 @@ class Memory(MemoryBase): } if operator in operator_map: - result[key] = {operator_map[operator]: value} + result.setdefault(key, {})[operator_map[operator]] = value else: raise ValueError(f"Unsupported metadata filter operator: {operator}") return result - + for key, value in metadata_filters.items(): if key == "AND": # Logical AND: combine multiple conditions @@ -2099,7 +2099,7 @@ class AsyncMemory(MemoryBase): } if operator in operator_map: - result[key] = {operator_map[operator]: value} + result.setdefault(key, {})[operator_map[operator]] = value else: raise ValueError(f"Unsupported metadata filter operator: {operator}") return result diff --git a/tests/test_memory.py b/tests/test_memory.py index 900d4db5d..6c8b69086 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -713,3 +713,47 @@ async def test_async_delete_memory_history_has_timestamps(mock_sqlite, mock_llm_ assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00" assert call_kwargs["updated_at"] is not None datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp + + +@patch('mem0.utils.factory.EmbedderFactory.create') +@patch('mem0.utils.factory.VectorStoreFactory.create') +@patch('mem0.utils.factory.LlmFactory.create') +@patch('mem0.memory.storage.SQLiteManager') +class TestProcessMetadataFiltersMerge: + """Regression tests for issue #3952: multiple operators on the same key must be merged.""" + + def _make_memory(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory): + mock_embedder_factory.return_value = MagicMock() + mock_vector_factory.return_value = MagicMock() + mock_llm_factory.return_value = MagicMock() + mock_sqlite.return_value = MagicMock() + from mem0.memory.main import Memory as MemoryClass + return MemoryClass(MemoryConfig()) + + def test_multiple_operators_same_key_merged(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory): + """Filters like created_at: {gte: X, lte: Y} must preserve both operators.""" + memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory) + result = memory._process_metadata_filters({ + "created_at": {"gte": 1000, "lte": 2000} + }) + assert result == {"created_at": {"gte": 1000, "lte": 2000}} + + def test_single_operator_still_works(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory): + """Single operator filters must continue to work.""" + memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory) + result = memory._process_metadata_filters({ + "created_at": {"gte": 1000} + }) + assert result == {"created_at": {"gte": 1000}} + + def test_multiple_keys_with_multiple_operators(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory): + """Multiple keys each with multiple operators.""" + memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory) + result = memory._process_metadata_filters({ + "created_at": {"gte": 1000, "lte": 2000}, + "score": {"gt": 0.5, "lt": 0.9}, + }) + assert result == { + "created_at": {"gte": 1000, "lte": 2000}, + "score": {"gt": 0.5, "lt": 0.9}, + }