Skip to content

Commit 9414ccf

Browse files
committed
Python: fold negative literals in InMemoryCollection string filters
InMemoryCollection parses string filters itself and checks them against an AST allowlist that doesn't include USub, so a filter such as 'lambda x: x.price > -5' was rejected. The fix for #14571 folds negative literals in _build_filter, which InMemoryCollection doesn't use. Apply the same _FoldUnaryNumericConstants transform before validation. Unary minus on anything other than a numeric literal is still rejected.
1 parent b714e97 commit 9414ccf

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

‎python/semantic_kernel/connectors/in_memory.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
VectorStore,
2525
VectorStoreCollection,
2626
VectorStoreCollectionDefinition,
27+
_FoldUnaryNumericConstants,
2728
)
2829
from semantic_kernel.exceptions import VectorSearchExecutionException, VectorStoreModelValidationError
2930
from semantic_kernel.exceptions.vector_store_exceptions import VectorStoreModelException, VectorStoreOperationException
@@ -790,6 +791,10 @@ def _parse_and_validate_filter(self, filter_str: str) -> Callable:
790791
except SyntaxError as e:
791792
raise VectorStoreOperationException(f"Filter string is not valid Python: {e}") from e
792793

794+
# Fold negative literals such as `-5` into constants, as other connectors do (#14571),
795+
# so they pass the allowlist without allowing unary minus in general.
796+
tree = _FoldUnaryNumericConstants().visit(tree)
797+
793798
# Only allow lambda expressions at the top level
794799
if not (isinstance(tree, ast.Expression) and isinstance(tree.body, ast.Lambda)):
795800
raise VectorStoreOperationException(

‎python/tests/unit/connectors/memory/test_in_memory.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,29 @@ async def test_multiple_callable_filters_are_combined_with_and(collection):
349349
assert list(results) == ["1"]
350350

351351

352+
@mark.parametrize(
353+
"filter_str, expected",
354+
[
355+
("lambda x: x.vector[0] > -1", ["1"]),
356+
("lambda x: x.vector[0] == -2.5", ["2"]),
357+
("lambda x: -3 < x.vector[0] < -2", ["2"]),
358+
],
359+
)
360+
async def test_string_filter_with_negative_literal(collection, filter_str, expected):
361+
# Python parses -1 as UnaryOp(USub, Constant(1)), which the allowlist rejects unless it is folded (#14571).
362+
await collection.upsert([
363+
{"id": "1", "vector": [1, 2, 3, 4, 5]},
364+
{"id": "2", "vector": [-2.5, 4, 3, 2, 1]},
365+
])
366+
results = collection._get_filtered_records(type("opt", (), {"filter": filter_str})())
367+
assert list(results) == expected
368+
369+
370+
async def test_string_filter_still_rejects_unary_minus_on_a_field(collection):
371+
with raises(VectorStoreOperationException, match="'USub' is not allowed"):
372+
collection._get_filtered_records(type("opt", (), {"filter": "lambda x: -x.vector[0] > 1"})())
373+
374+
352375
async def test_invalid_filter_not_lambda(collection):
353376
with raises(VectorStoreOperationException, match="must be a lambda expression"):
354377
collection._get_filtered_records(type("opt", (), {"filter": "x.id == '1'"})())

0 commit comments

Comments
 (0)