Skip to content
Open
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
13 changes: 12 additions & 1 deletion python/semantic_kernel/connectors/chroma.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def _get_score_from_result(self, result: Any) -> float | None:
return result["distance"]

@override
def _lambda_parser(self, node: ast.AST) -> dict[str, Any] | str | int | float | bool | None: # type: ignore
def _lambda_parser(self, node: ast.AST) -> dict[str, Any] | list[Any] | str | int | float | bool | None: # type: ignore
# Comparison operations
match node:
case ast.Compare():
Expand Down Expand Up @@ -430,6 +430,17 @@ def _lambda_parser(self, node: ast.AST) -> dict[str, Any] | str | int | float |
if isinstance(value, (int, float, bool)) or value is None:
return value
raise VectorStoreOperationException(f"Unsupported constant type: {type(value)}")
case ast.List():
# Elements go into a typed value list, not a SQL string, so string literals are kept as written.
items: list[Any] = []
for elt in node.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
items.append(elt.value)
elif isinstance(elt, ast.Constant) and isinstance(elt.value, bytes):
items.append(elt.value.decode("utf-8"))
else:
items.append(self._lambda_parser(elt))
return items
raise NotImplementedError(f"Unsupported AST node: {type(node)}")


Expand Down
2 changes: 2 additions & 0 deletions python/semantic_kernel/connectors/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ def _lambda_parser(self, node: ast.AST) -> Any:
return node.id
case ast.Constant():
return node.value
case ast.List():
return [self._lambda_parser(elt) for elt in node.elts]
raise NotImplementedError(f"Unsupported AST node: {type(node)}")

@override
Expand Down
15 changes: 6 additions & 9 deletions python/semantic_kernel/connectors/pinecone.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,15 +541,10 @@ def _lambda_parser(self, node: ast.AST) -> Any:
case ast.Not():
operand = self._lambda_parser(node.operand)
# Pinecone only supports $not over $in (becomes $nin)
if (
isinstance(operand, dict)
and len(operand) == 1
and isinstance(next(operand.values()), dict) # type: ignore
and "$in" in next(operand.values()) # type: ignore
):
field = next(operand.keys()) # type: ignore
values = next(operand.values())["$in"] # type: ignore
return {field: {"$nin": values}}
if isinstance(operand, dict) and len(operand) == 1:
field, condition = next(iter(operand.items()))
if isinstance(condition, dict) and "$in" in condition:
return {field: {"$nin": condition["$in"]}}
raise NotImplementedError(
"$not is only supported over $in (i.e., for ![...].contains(field)). "
"Other NOT expressions are not supported by Pinecone."
Expand All @@ -574,6 +569,8 @@ def _lambda_parser(self, node: ast.AST) -> Any:
if node.value is None:
raise NotImplementedError("Pinecone does not support null checks in vector search pre-filters.")
return node.value
case ast.List():
return [self._lambda_parser(elt) for elt in node.elts]
raise NotImplementedError(f"Unsupported AST node: {type(node)}")

@override
Expand Down
11 changes: 11 additions & 0 deletions python/semantic_kernel/connectors/weaviate.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,17 @@ def _lambda_parser(self, node: ast.AST) -> "_Filters | FilterValues":
if isinstance(value, (int, float, bool)) or value is None:
return value
raise VectorStoreOperationException(f"Unsupported constant type: {type(value)}")
case ast.List():
# Elements go into a typed value list, not a SQL string, so string literals are kept as written.
items: list[Any] = []
for elt in node.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
items.append(elt.value)
elif isinstance(elt, ast.Constant) and isinstance(elt.value, bytes):
items.append(elt.value.decode("utf-8"))
else:
items.append(self._lambda_parser(elt))
return items
raise NotImplementedError(f"Unsupported AST node: {type(node)}")

async def _inner_vectorized_search(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,22 @@ async def test_mongodb_atlas_collection_collection_exists(mongodb_atlas_unit_tes
with patch.object(collection, "_get_database", new=mock_get_database) as mock_get:
mock_get.return_value.list_collection_names.return_value = ["test_collection"]
assert await collection.collection_exists()


@mark.parametrize(
"filter_str, expected",
[
("lambda x: x.content in ['a', 'b']", {"content": {"$in": ["a", "b"]}}),
("lambda x: x.content not in ['a', 'b']", {"content": {"$nin": ["a", "b"]}}),
],
)
def test_mongodb_atlas_collection_build_filter_list(
mongodb_atlas_unit_test_env, definition, mock_mongo_client, filter_str, expected
):
collection = MongoDBAtlasCollection(
record_type=dict,
definition=definition,
collection_name="test_collection",
mongo_client=mock_mongo_client,
)
assert collection._build_filter(filter_str) == expected
14 changes: 14 additions & 0 deletions python/tests/unit/connectors/memory/test_chroma.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,17 @@ async def test_chroma_collection_search(chroma_collection, mock_client, include_
async for res in results.results:
assert res.record["id"] == "1"
assert res.score == 0.1


def test_chroma_collection_build_filter_in_list(chroma_collection):
assert chroma_collection._build_filter("lambda x: x.content in ['a', 'b']") == {"content": {"$in": ["a", "b"]}}


def test_chroma_collection_build_filter_not_in_list(chroma_collection):
assert chroma_collection._build_filter("lambda x: x.content not in ['a', 'b']") == {"content": {"$nin": ["a", "b"]}}


def test_chroma_collection_build_filter_in_list_keeps_apostrophes(chroma_collection):
assert chroma_collection._build_filter("lambda x: x.content in [\"O'Reilly\", 'b']") == {
"content": {"$in": ["O'Reilly", "b"]}
}
17 changes: 17 additions & 0 deletions python/tests/unit/connectors/memory/test_pinecone.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,20 @@ async def test_search_embed(collection):
async for result in query_response.results:
assert result.record == record
assert result.score == 0.1


async def test_build_filter_in_list(collection):
assert collection._build_filter("lambda x: x.content in ['a', 'b']") == {"content": {"$in": ["a", "b"]}}


async def test_build_filter_not_in_list(collection):
assert collection._build_filter("lambda x: x.content not in ['a', 'b']") == {"content": {"$nin": ["a", "b"]}}


async def test_build_filter_not_over_in_becomes_nin(collection):
assert collection._build_filter("lambda x: not (x.content in ['a', 'b'])") == {"content": {"$nin": ["a", "b"]}}


async def test_build_filter_not_over_other_expression_raises(collection):
with raises(NotImplementedError, match="only supported over"):
collection._build_filter("lambda x: not (x.content == 'a')")
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,39 @@ async def test_weaviate_collection_deserialize_data(
await collection.get(key=data.id)

mock_inner_get.assert_called_once_with([data.id], include_vectors=False, options=None)


def test_weaviate_collection_build_filter_in_list(clear_weaviate_env, record_type, definition) -> None:
"""A list literal on the right side of `in` is translated to a contains_any filter."""
collection = WeaviateCollection(
record_type=record_type,
definition=definition,
collection_name="TestCollection",
async_client=AsyncMock(spec=WeaviateAsyncClient),
env_file_path="fake_env_file_path.env",
)

result = collection._build_filter("lambda x: x.content in ['a', 'b']")

assert result.target == "content"
assert result.value == ["a", "b"]
assert result.operator.name == "CONTAINS_ANY"


def test_weaviate_collection_build_filter_in_list_keeps_apostrophes(
clear_weaviate_env, record_type, definition
) -> None:
"""String values in a list literal reach contains_any as written, without doubled apostrophes."""
collection = WeaviateCollection(
record_type=record_type,
definition=definition,
collection_name="TestCollection",
async_client=AsyncMock(spec=WeaviateAsyncClient),
env_file_path="fake_env_file_path.env",
)

result = collection._build_filter("lambda x: x.content in [\"O'Reilly\", 'b']")

assert result.target == "content"
assert result.value == ["O'Reilly", "b"]
assert result.operator.name == "CONTAINS_ANY"
Loading