diff --git a/mem0/vector_stores/chroma.py b/mem0/vector_stores/chroma.py index ad8f5cf34..6bdc9a4bf 100644 --- a/mem0/vector_stores/chroma.py +++ b/mem0/vector_stores/chroma.py @@ -316,8 +316,32 @@ class ChromaDB(VectorStoreBase): processed_filters.append(or_conditions[0]) elif key == "$not": - # Handle NOT conditions - ChromaDB doesn't have direct NOT, so we'll skip for now - continue + negate_op = { + "eq": "$ne", "ne": "$eq", + "gt": "$lte", "gte": "$lt", + "lt": "$gte", "lte": "$gt", + "in": "$nin", "nin": "$in", + } + negated_per_group = [] + for condition in value: + negated_fields = [] + for sub_key, sub_value in condition.items(): + if isinstance(sub_value, dict): + for op, val in sub_value.items(): + neg = negate_op.get(op) + if neg: + negated_fields.append({sub_key: {neg: val}}) + else: + negated_fields.append({sub_key: {"$ne": sub_value}}) + if len(negated_fields) > 1: + negated_per_group.append({"$or": negated_fields}) + elif len(negated_fields) == 1: + negated_per_group.append(negated_fields[0]) + + if len(negated_per_group) > 1: + processed_filters.append({"$and": negated_per_group}) + elif len(negated_per_group) == 1: + processed_filters.append(negated_per_group[0]) else: # Regular condition diff --git a/tests/vector_stores/test_chroma.py b/tests/vector_stores/test_chroma.py index f355a0580..9c43c24c8 100644 --- a/tests/vector_stores/test_chroma.py +++ b/tests/vector_stores/test_chroma.py @@ -252,6 +252,48 @@ def test_generate_where_clause_non_string_values(): assert result == expected +def test_generate_where_clause_not_single_equality(): + """Test $not with a single equality condition.""" + filters = {"$not": [{"status": "archived"}]} + result = ChromaDB._generate_where_clause(filters) + assert result == {"status": {"$ne": "archived"}} + + +def test_generate_where_clause_not_multiple_conditions(): + """Test $not with multiple conditions (OR semantics, negated to AND).""" + filters = {"$not": [{"status": "archived"}, {"type": "draft"}]} + result = ChromaDB._generate_where_clause(filters) + assert result == {"$and": [{"status": {"$ne": "archived"}}, {"type": {"$ne": "draft"}}]} + + +def test_generate_where_clause_not_with_operators(): + """Test $not negates comparison operators correctly.""" + filters = {"$not": [{"count": {"gt": 5}}]} + result = ChromaDB._generate_where_clause(filters) + assert result == {"count": {"$lte": 5}} + + +def test_generate_where_clause_not_in_to_nin(): + """Test $not converts 'in' to $nin.""" + filters = {"$not": [{"status": {"in": ["archived", "deleted"]}}]} + result = ChromaDB._generate_where_clause(filters) + assert result == {"status": {"$nin": ["archived", "deleted"]}} + + +def test_generate_where_clause_not_multi_field_condition(): + """Test $not with multi-field condition uses De Morgan's (AND -> OR).""" + filters = {"$not": [{"status": "archived", "type": "draft"}]} + result = ChromaDB._generate_where_clause(filters) + assert result == {"$or": [{"status": {"$ne": "archived"}}, {"type": {"$ne": "draft"}}]} + + +def test_generate_where_clause_not_combined_with_other_filters(): + """Test $not combined with regular filters.""" + filters = {"user_id": "alice", "$not": [{"status": "archived"}]} + result = ChromaDB._generate_where_clause(filters) + assert result == {"$and": [{"user_id": {"$eq": "alice"}}, {"status": {"$ne": "archived"}}]} + + def test_chroma_config_accepts_default_tmp_path(): """Test that ChromaDbConfig accepts the default /tmp/chroma path.""" config = ChromaDbConfig(path="/tmp/chroma")