Skip to content
Merged
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
42 changes: 29 additions & 13 deletions mem0/memory/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1161,23 +1161,23 @@ def search(
def _process_metadata_filters(self, metadata_filters: Dict[str, Any]) -> Dict[str, Any]:
"""
Process enhanced metadata filters and convert them to vector store compatible format.

Args:
metadata_filters: Enhanced metadata filters with operators

Returns:
Dict of processed filters compatible with vector store
"""
processed_filters = {}

def process_condition(key: str, condition: Any) -> Dict[str, Any]:
if not isinstance(condition, dict):
# Simple equality: {"key": "value"}
if condition == "*":
# Wildcard: match everything for this field (implementation depends on vector store)
return {key: "*"}
return {key: condition}

result = {}
for operator, value in condition.items():
# Map platform operators to universal format that can be translated by each vector store
Expand All @@ -1193,14 +1193,22 @@ def process_condition(key: str, condition: Any) -> Dict[str, Any]:
raise ValueError(f"Unsupported metadata filter operator: {operator}")
return result

def merge_filters(target: Dict[str, Any], source: Dict[str, Any]) -> None:
"""Merge source into target, deep-merging nested operator dicts for the same key."""
for key, value in source.items():
if key in target and isinstance(target[key], dict) and isinstance(value, dict):
target[key].update(value)
else:
target[key] = value

for key, value in metadata_filters.items():
if key == "AND":
# Logical AND: combine multiple conditions
if not isinstance(value, list):
raise ValueError("AND operator requires a list of conditions")
for condition in value:
for sub_key, sub_value in condition.items():
processed_filters.update(process_condition(sub_key, sub_value))
merge_filters(processed_filters, process_condition(sub_key, sub_value))
elif key == "OR":
# Logical OR: Pass through to vector store for implementation-specific handling
if not isinstance(value, list) or not value:
Expand All @@ -1210,7 +1218,7 @@ def process_condition(key: str, condition: Any) -> Dict[str, Any]:
for condition in value:
or_condition = {}
for sub_key, sub_value in condition.items():
or_condition.update(process_condition(sub_key, sub_value))
merge_filters(or_condition, process_condition(sub_key, sub_value))
processed_filters["$or"].append(or_condition)
elif key == "NOT":
# Logical NOT: Pass through to vector store for implementation-specific handling
Expand All @@ -1220,11 +1228,11 @@ def process_condition(key: str, condition: Any) -> Dict[str, Any]:
for condition in value:
not_condition = {}
for sub_key, sub_value in condition.items():
not_condition.update(process_condition(sub_key, sub_value))
merge_filters(not_condition, process_condition(sub_key, sub_value))
processed_filters["$not"].append(not_condition)
else:
processed_filters.update(process_condition(key, value))
merge_filters(processed_filters, process_condition(key, value))

return processed_filters

def _has_advanced_operators(self, filters: Dict[str, Any]) -> bool:
Expand Down Expand Up @@ -2482,14 +2490,22 @@ def process_condition(key: str, condition: Any) -> Dict[str, Any]:
raise ValueError(f"Unsupported metadata filter operator: {operator}")
return result

def merge_filters(target: Dict[str, Any], source: Dict[str, Any]) -> None:
"""Merge source into target, deep-merging nested operator dicts for the same key."""
for key, value in source.items():
if key in target and isinstance(target[key], dict) and isinstance(value, dict):
target[key].update(value)
else:
target[key] = value

for key, value in metadata_filters.items():
if key == "AND":
# Logical AND: combine multiple conditions
if not isinstance(value, list):
raise ValueError("AND operator requires a list of conditions")
for condition in value:
for sub_key, sub_value in condition.items():
processed_filters.update(process_condition(sub_key, sub_value))
merge_filters(processed_filters, process_condition(sub_key, sub_value))
elif key == "OR":
# Logical OR: Pass through to vector store for implementation-specific handling
if not isinstance(value, list) or not value:
Expand All @@ -2499,7 +2515,7 @@ def process_condition(key: str, condition: Any) -> Dict[str, Any]:
for condition in value:
or_condition = {}
for sub_key, sub_value in condition.items():
or_condition.update(process_condition(sub_key, sub_value))
merge_filters(or_condition, process_condition(sub_key, sub_value))
processed_filters["$or"].append(or_condition)
elif key == "NOT":
# Logical NOT: Pass through to vector store for implementation-specific handling
Expand All @@ -2509,10 +2525,10 @@ def process_condition(key: str, condition: Any) -> Dict[str, Any]:
for condition in value:
not_condition = {}
for sub_key, sub_value in condition.items():
not_condition.update(process_condition(sub_key, sub_value))
merge_filters(not_condition, process_condition(sub_key, sub_value))
processed_filters["$not"].append(not_condition)
else:
processed_filters.update(process_condition(key, value))
merge_filters(processed_filters, process_condition(key, value))

return processed_filters

Expand Down
32 changes: 32 additions & 0 deletions tests/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,38 @@ def test_multiple_keys_with_multiple_operators(self, mock_sqlite, mock_llm_facto
"score": {"gt": 0.5, "lt": 0.9},
}

def test_and_same_key_different_operators_merged(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with same key in separate conditions must merge operators (issue #4850)."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"price": {"gt": 10}}, {"price": {"lt": 20}}]
})
assert result == {"price": {"gt": 10, "lt": 20}}

def test_and_same_key_three_operators_merged(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with three conditions on the same key must merge all operators."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"price": {"gte": 5}}, {"price": {"lte": 100}}, {"price": {"ne": 50}}]
})
assert result == {"price": {"gte": 5, "lte": 100, "ne": 50}}

def test_and_mixed_keys_with_same_key_overlap(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with a mix of same-key and different-key conditions."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"price": {"gt": 10}}, {"category": "electronics"}, {"price": {"lt": 20}}]
})
assert result == {"price": {"gt": 10, "lt": 20}, "category": "electronics"}

def test_and_simple_equality_no_merge(self, mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""AND with simple equality values on the same key — last value wins."""
memory = self._make_memory(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory)
result = memory._process_metadata_filters({
"AND": [{"status": "active"}, {"status": "pending"}]
})
assert result == {"status": "pending"}


# --- Issue #3040: reset() should clean up graph database ---

Expand Down
Loading