Files
microsoft--semantic-kernel/python/semantic_kernel/connectors/in_memory.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

920 lines
36 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import ast
import sys
from collections.abc import AsyncIterable, Callable, Mapping, Sequence
from typing import Any, ClassVar, Final, Generic, TypeVar, cast
from numpy import dot
from pydantic import Field
from scipy.spatial.distance import cityblock, cosine, euclidean, hamming, sqeuclidean
from typing_extensions import override
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.data.vector import (
DISTANCE_FUNCTION_DIRECTION_HELPER,
DistanceFunction,
GetFilteredRecordOptions,
KernelSearchResults,
SearchType,
TModel,
VectorSearch,
VectorSearchOptions,
VectorSearchResult,
VectorStore,
VectorStoreCollection,
VectorStoreCollectionDefinition,
)
from semantic_kernel.exceptions import VectorSearchExecutionException, VectorStoreModelValidationError
from semantic_kernel.exceptions.vector_store_exceptions import VectorStoreModelException, VectorStoreOperationException
from semantic_kernel.kernel_types import OneOrMany
from semantic_kernel.utils.feature_stage_decorator import release_candidate
from semantic_kernel.utils.list_handler import empty_generator
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
TKey = TypeVar("TKey", bound=str | int | float)
IN_MEMORY_SCORE_KEY: Final[str] = "in_memory_search_score"
DISTANCE_FUNCTION_MAP: Final[dict[DistanceFunction | str, Callable[..., Any]]] = {
DistanceFunction.COSINE_DISTANCE: cosine,
DistanceFunction.COSINE_SIMILARITY: cosine,
DistanceFunction.EUCLIDEAN_DISTANCE: euclidean,
DistanceFunction.EUCLIDEAN_SQUARED_DISTANCE: sqeuclidean,
DistanceFunction.MANHATTAN: cityblock,
DistanceFunction.HAMMING: hamming,
DistanceFunction.DOT_PROD: dot,
DistanceFunction.DEFAULT: cosine,
}
TAKey = TypeVar("TAKey", bound=str)
TAValue = TypeVar("TAValue", bound=str | int | float | list[float] | None)
class AttributeDict(dict[TAKey, TAValue], Generic[TAKey, TAValue]):
"""A dict subclass that allows attribute access to keys.
This is used to allow the filters to work either way, using:
- `lambda x: x.key == 'id'` or `lambda x: x['key'] == 'id'`
"""
def __getattr__(self, name) -> TAValue:
"""Allow attribute-style access to dict keys."""
try:
return self[name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value) -> None:
"""Allow setting dict keys via attribute access."""
self[name] = value
def __delattr__(self, name) -> None:
"""Allow deleting dict keys via attribute access."""
try:
del self[name]
except KeyError:
raise AttributeError(name)
class ReadOnlyAttributeDict(Mapping[TAKey, TAValue], Generic[TAKey, TAValue]):
"""A read-only mapping that allows attribute access to keys."""
def __init__(self, data: Mapping[TAKey, TAValue]):
"""Initialize the read-only mapping wrapper."""
self._data = data
def __getitem__(self, key: TAKey) -> TAValue:
"""Get a value by key."""
return self._wrap_value(self._data[key])
def __iter__(self):
"""Iterate over keys."""
return iter(self._data)
def __len__(self) -> int:
"""Return the number of keys."""
return len(self._data)
def __getattr__(self, name: str) -> TAValue:
"""Allow attribute-style access to mapping keys."""
try:
return self._wrap_value(self._data[cast(TAKey, name)])
except KeyError:
raise AttributeError(name)
@staticmethod
def _wrap_value(value: Any) -> Any:
"""Wrap nested mappings to preserve read-only attribute access."""
if isinstance(value, Mapping) and not isinstance(value, ReadOnlyAttributeDict):
return ReadOnlyAttributeDict(value)
return value
class _SafeFilterEvaluator:
"""Evaluate a restricted filter AST without using eval()."""
def __init__(
self,
*,
direct_call_functions: dict[str, Callable[..., Any]],
blocked_attributes: set[str],
max_literal_collection_size: int,
max_sequence_repeat_size: int,
):
self._direct_call_functions = direct_call_functions
self._blocked_attributes = blocked_attributes
self._max_literal_collection_size = max_literal_collection_size
self._max_sequence_repeat_size = max_sequence_repeat_size
def evaluate(self, node: ast.AST, context: Mapping[str, Any]) -> Any:
"""Evaluate a supported AST node."""
evaluator = getattr(self, f"_eval_{type(node).__name__}", None)
if evaluator is None:
raise VectorStoreOperationException(
f"AST node type '{type(node).__name__}' is not supported during filter evaluation."
)
return evaluator(node, context)
def _eval_Constant(self, node: ast.Constant, context: Mapping[str, Any]) -> Any:
"""Evaluate a constant literal."""
del context
if isinstance(node.value, str) and len(node.value) > self._max_literal_collection_size:
raise VectorStoreOperationException(
"String literals in filter expressions exceed the maximum allowed size."
)
return node.value
def _eval_Name(self, node: ast.Name, context: Mapping[str, Any]) -> Any:
"""Evaluate a variable reference."""
if node.id not in context:
raise VectorStoreOperationException(f"Use of name '{node.id}' is not allowed in filter expressions.")
return context[node.id]
def _eval_Attribute(self, node: ast.Attribute, context: Mapping[str, Any]) -> Any:
"""Evaluate an attribute access."""
if node.attr in self._blocked_attributes:
raise VectorStoreOperationException(
f"Access to attribute '{node.attr}' is not allowed in filter expressions."
)
value = self.evaluate(node.value, context)
try:
return ReadOnlyAttributeDict._wrap_value(getattr(value, node.attr))
except AttributeError as e:
raise VectorStoreOperationException(
f"Attribute '{node.attr}' is not available in filter expressions."
) from e
def _eval_Subscript(self, node: ast.Subscript, context: Mapping[str, Any]) -> Any:
"""Evaluate an index or slice operation."""
value = self.evaluate(node.value, context)
slice_value = self.evaluate(node.slice, context)
try:
return ReadOnlyAttributeDict._wrap_value(value[slice_value])
except Exception as e:
raise VectorStoreOperationException(f"Error evaluating subscript access: {e}") from e
def _eval_Slice(self, node: ast.Slice, context: Mapping[str, Any]) -> slice:
"""Evaluate a slice node."""
lower = self._evaluate_optional(node.lower, context)
upper = self._evaluate_optional(node.upper, context)
step = self._evaluate_optional(node.step, context)
return slice(lower, upper, step)
def _eval_List(self, node: ast.List, context: Mapping[str, Any]) -> list[Any]:
"""Evaluate a list literal."""
self._ensure_literal_collection_size(len(node.elts))
return [self.evaluate(element, context) for element in node.elts]
def _eval_Tuple(self, node: ast.Tuple, context: Mapping[str, Any]) -> tuple[Any, ...]:
"""Evaluate a tuple literal."""
self._ensure_literal_collection_size(len(node.elts))
return tuple(self.evaluate(element, context) for element in node.elts)
def _eval_Set(self, node: ast.Set, context: Mapping[str, Any]) -> set[Any]:
"""Evaluate a set literal."""
self._ensure_literal_collection_size(len(node.elts))
return {self.evaluate(element, context) for element in node.elts}
def _eval_Dict(self, node: ast.Dict, context: Mapping[str, Any]) -> dict[Any, Any]:
"""Evaluate a dict literal."""
self._ensure_literal_collection_size(len(node.keys))
result: dict[Any, Any] = {}
for key, value in zip(node.keys, node.values, strict=True):
if key is None:
raise VectorStoreOperationException("Dictionary unpacking is not allowed in filter expressions.")
result[self.evaluate(key, context)] = self.evaluate(value, context)
return result
def _eval_BoolOp(self, node: ast.BoolOp, context: Mapping[str, Any]) -> Any:
"""Evaluate boolean operators with Python short-circuit semantics."""
if isinstance(node.op, ast.And):
result = self.evaluate(node.values[0], context)
for value in node.values[1:]:
if not result:
return result
result = self.evaluate(value, context)
return result
if isinstance(node.op, ast.Or):
result = self.evaluate(node.values[0], context)
for value in node.values[1:]:
if result:
return result
result = self.evaluate(value, context)
return result
raise VectorStoreOperationException(
f"Boolean operator '{type(node.op).__name__}' is not allowed in filter expressions."
)
def _eval_UnaryOp(self, node: ast.UnaryOp, context: Mapping[str, Any]) -> Any:
"""Evaluate a unary operator."""
operand = self.evaluate(node.operand, context)
if isinstance(node.op, ast.Not):
return not operand
raise VectorStoreOperationException(
f"Unary operator '{type(node.op).__name__}' is not allowed in filter expressions."
)
def _eval_Compare(self, node: ast.Compare, context: Mapping[str, Any]) -> bool:
"""Evaluate a comparison expression."""
left = self.evaluate(node.left, context)
for operator_node, comparator in zip(node.ops, node.comparators, strict=True):
right = self.evaluate(comparator, context)
if not self._compare(operator_node, left, right):
return False
left = right
return True
def _eval_BinOp(self, node: ast.BinOp, context: Mapping[str, Any]) -> Any:
"""Evaluate a binary operator."""
left = self.evaluate(node.left, context)
right = self.evaluate(node.right, context)
if isinstance(node.op, ast.Add):
return self._safe_add(left, right)
if isinstance(node.op, ast.Sub):
return self._safe_numeric_operation(node.op, left, right, lambda a, b: a - b)
if isinstance(node.op, ast.Mult):
return self._safe_mult(left, right)
if isinstance(node.op, ast.Div):
return self._safe_numeric_operation(node.op, left, right, lambda a, b: a / b)
if isinstance(node.op, ast.Mod):
return self._safe_numeric_operation(node.op, left, right, lambda a, b: a % b)
if isinstance(node.op, ast.FloorDiv):
return self._safe_numeric_operation(node.op, left, right, lambda a, b: a // b)
raise VectorStoreOperationException(
f"Binary operator '{type(node.op).__name__}' is not allowed in filter expressions."
)
def _eval_Call(self, node: ast.Call, context: Mapping[str, Any]) -> Any:
"""Evaluate a function or method call."""
args = [self.evaluate(arg, context) for arg in node.args]
if isinstance(node.func, ast.Name):
try:
func = self._direct_call_functions[node.func.id]
except KeyError as e:
raise VectorStoreOperationException(
f"Function '{node.func.id}' is only supported as a method call in filter expressions."
) from e
return func(*args)
if isinstance(node.func, ast.Attribute):
target = self.evaluate(node.func.value, context)
if node.func.attr == "contains":
if len(args) != 1:
raise VectorStoreOperationException("Method 'contains' expects exactly one argument.")
return args[0] in target
try:
func = getattr(target, node.func.attr)
except AttributeError as e:
raise VectorStoreOperationException(
f"Method '{node.func.attr}' is not available in filter expressions."
) from e
if not callable(func):
raise VectorStoreOperationException(
f"Attribute '{node.func.attr}' is not callable in filter expressions."
)
return func(*args)
raise VectorStoreOperationException(
f"Call target node type '{type(node.func).__name__}' is not allowed in filter expressions."
)
def _compare(self, operator_node: ast.AST, left: Any, right: Any) -> bool:
"""Evaluate a comparison operator."""
if isinstance(operator_node, ast.Eq):
return left == right
if isinstance(operator_node, ast.NotEq):
return left != right
if isinstance(operator_node, ast.Lt):
return left < right
if isinstance(operator_node, ast.LtE):
return left <= right
if isinstance(operator_node, ast.Gt):
return left > right
if isinstance(operator_node, ast.GtE):
return left >= right
if isinstance(operator_node, ast.In):
return left in right
if isinstance(operator_node, ast.NotIn):
return left not in right
if isinstance(operator_node, ast.Is):
return left is right
if isinstance(operator_node, ast.IsNot):
return left is not right
raise VectorStoreOperationException(
f"Comparison operator '{type(operator_node).__name__}' is not allowed in filter expressions."
)
def _safe_add(self, left: Any, right: Any) -> Any:
"""Safely evaluate addition."""
if isinstance(left, (int, float)) and isinstance(right, (int, float)):
return left + right
if isinstance(left, str) and isinstance(right, str):
return self._ensure_sequence_result_size(left, right, lambda a, b: a + b)
if isinstance(left, list) and isinstance(right, list):
return self._ensure_sequence_result_size(left, right, lambda a, b: a + b)
if isinstance(left, tuple) and isinstance(right, tuple):
return self._ensure_sequence_result_size(left, right, lambda a, b: a + b)
raise VectorStoreOperationException(
"Addition in filter expressions is only allowed for numeric values and same-type sequences."
)
def _safe_mult(self, left: Any, right: Any) -> Any:
"""Safely evaluate multiplication."""
if isinstance(left, (int, float)) and isinstance(right, (int, float)):
return left * right
if isinstance(left, int) and isinstance(right, (str, list, tuple)):
return self._safe_repeat(right, left)
if isinstance(right, int) and isinstance(left, (str, list, tuple)):
return self._safe_repeat(left, right)
raise VectorStoreOperationException(
"Multiplication in filter expressions is only allowed for numeric values and bounded sequence repetition."
)
def _safe_repeat(self, value: str | list[Any] | tuple[Any, ...], repeat_count: int) -> Any:
"""Safely repeat a sequence."""
if repeat_count <= 0 or len(value) == 0:
return value * repeat_count
if len(value) > self._max_sequence_repeat_size // repeat_count:
raise VectorStoreOperationException(
"Sequence repetition in filter expressions exceeds the maximum allowed size."
)
return value * repeat_count
def _safe_numeric_operation(
self,
operator_node: ast.AST,
left: Any,
right: Any,
operation: Callable[[float | int, float | int], Any],
) -> Any:
"""Safely evaluate a numeric binary operation."""
if not isinstance(left, (int, float)) or not isinstance(right, (int, float)):
raise VectorStoreOperationException(
f"Operator '{type(operator_node).__name__}' is only allowed for numeric values in filter expressions."
)
return operation(left, right)
def _ensure_literal_collection_size(self, size: int) -> None:
"""Reject excessively large literal collections."""
if size > self._max_literal_collection_size:
raise VectorStoreOperationException(
"Collection literals in filter expressions exceed the maximum allowed size."
)
def _ensure_sequence_result_size(
self,
left: str | list[Any] | tuple[Any, ...],
right: str | list[Any] | tuple[Any, ...],
operation: Callable[[Any, Any], Any],
) -> Any:
"""Reject oversized sequence concatenation results."""
if len(left) + len(right) > self._max_sequence_repeat_size:
raise VectorStoreOperationException(
"Sequence operations in filter expressions exceed the maximum allowed size."
)
return operation(left, right)
def _evaluate_optional(self, node: ast.AST | None, context: Mapping[str, Any]) -> Any:
"""Evaluate an optional AST node."""
return self.evaluate(node, context) if node is not None else None
class InMemoryCollection(
VectorStoreCollection[TKey, TModel],
VectorSearch[TKey, TModel],
Generic[TKey, TModel],
):
"""In Memory Collection."""
inner_storage: dict[TKey, AttributeDict] = Field(default_factory=dict)
supported_key_types: ClassVar[set[str] | None] = {"str", "int", "float"}
supported_search_types: ClassVar[set[SearchType]] = {SearchType.VECTOR}
# Conservative defaults: callers can raise these per collection instance or subclass if needed.
max_filter_source_length: int = Field(default=2_048, exclude=True)
max_filter_ast_node_count: int = Field(default=128, exclude=True)
max_filter_literal_collection_size: int = Field(default=256, exclude=True)
max_filter_sequence_repeat_size: int = Field(default=1_024, exclude=True)
# Allowlist of AST node types permitted in filter expressions.
# This can be overridden in subclasses to extend or restrict allowed operations.
allowed_filter_ast_nodes: ClassVar[set[type]] = {
ast.Expression,
ast.Lambda,
ast.arguments,
ast.arg,
# Comparisons and boolean operations
ast.Compare,
ast.BoolOp,
ast.UnaryOp,
ast.And,
ast.Or,
ast.Not,
ast.Eq,
ast.NotEq,
ast.Lt,
ast.LtE,
ast.Gt,
ast.GtE,
ast.In,
ast.NotIn,
ast.Is,
ast.IsNot,
# Data access
ast.Name,
ast.Load,
ast.Attribute,
ast.Subscript,
ast.Slice,
# Literals
ast.Constant,
ast.List,
ast.Tuple,
ast.Set,
ast.Dict,
# Basic arithmetic (useful for computed comparisons)
ast.BinOp,
ast.Add,
ast.Sub,
ast.Mult,
ast.Div,
ast.Mod,
ast.FloorDiv,
# Function calls (restricted to safe builtins separately)
ast.Call,
}
# Allowlist of function/method names that can be called in filter expressions.
allowed_filter_functions: ClassVar[set[str]] = {
"len",
"str",
"int",
"float",
"bool",
"abs",
"min",
"max",
"sum",
"any",
"all",
"lower",
"upper",
"strip",
"startswith",
"endswith",
"contains",
"get",
"keys",
"values",
"items",
}
direct_filter_functions: ClassVar[dict[str, Callable[..., Any]]] = {
"len": len,
"str": str,
"int": int,
"float": float,
"bool": bool,
"abs": abs,
"min": min,
"max": max,
"sum": sum,
"any": any,
"all": all,
}
# Blocklist of dangerous attribute names that cannot be accessed in filter expressions.
# These attributes can be used to escape the sandbox and execute arbitrary code.
blocked_filter_attributes: ClassVar[set[str]] = {
# Object introspection - can lead to class/module access
"__class__",
"__base__",
"__bases__",
"__mro__",
"__subclasses__",
# Code and function internals
"__code__",
"__globals__",
"__closure__",
"__func__",
"__self__",
"__dict__",
"__slots__",
# Attribute access hooks
"__getattr__",
"__getattribute__",
"__setattr__",
"__delattr__",
"__setitem__",
"__delitem__",
# Import and builtins
"__builtins__",
"__import__",
"__loader__",
"__spec__",
# Module attributes
"__name__",
"__qualname__",
"__module__",
"__file__",
"__path__",
"__package__",
# Descriptor protocol
"__get__",
"__set__",
"__delete__",
# Metaclass and creation
"__new__",
"__init__",
"__init_subclass__",
"__prepare__",
"__call__",
# Other dangerous attributes
"__reduce__",
"__reduce_ex__",
"__getstate__",
"__setstate__",
"func_globals", # Python 2 compatibility name
"gi_frame", # Generator frame access
"gi_code",
"f_globals", # Frame globals
"f_locals",
"f_builtins",
"co_consts", # Code object constants
}
def __init__(
self,
record_type: type[TModel],
definition: VectorStoreCollectionDefinition | None = None,
collection_name: str | None = None,
embedding_generator: EmbeddingGeneratorBase | None = None,
**kwargs: Any,
):
"""Create a In Memory Collection.
In Memory collections are ephemeral and exist only in memory.
They do not persist data to disk or any external storage.
> [Important]
> Filters are powerful things, so make sure to not allow untrusted input here.
> Filters for this collection are parsed into Python's `ast` module and evaluated by a restricted interpreter.
> We only allow certain AST nodes and functions to be used in filter expressions, and we reject expressions
> that exceed reasonable size and complexity limits.
>
> The default filter limits are:
> - `max_filter_source_length=2048`
> - `max_filter_ast_node_count=128`
> - `max_filter_literal_collection_size=256`
> - `max_filter_sequence_repeat_size=1024`
> You can override these limits by passing them through `kwargs` or by setting them on the collection
> instance after initialization.
"""
super().__init__(
record_type=record_type,
definition=definition,
collection_name=collection_name,
embedding_generator=embedding_generator,
**kwargs,
)
def _validate_data_model(self):
"""Check if the In Memory Score key is not used."""
super()._validate_data_model()
if IN_MEMORY_SCORE_KEY in self.definition.names:
raise VectorStoreModelValidationError(f"Field name '{IN_MEMORY_SCORE_KEY}' is reserved for internal use.")
@override
async def _inner_delete(self, keys: Sequence[TKey], **kwargs: Any) -> None:
for key in keys:
self.inner_storage.pop(key, None)
@override
async def _inner_get(
self, keys: Sequence[TKey] | None = None, options: GetFilteredRecordOptions | None = None, **kwargs: Any
) -> Any | OneOrMany[TModel] | None:
if not keys:
if options is not None:
raise NotImplementedError("Get without keys is not yet implemented.")
return None
return [self.inner_storage[key] for key in keys if key in self.inner_storage]
@override
async def _inner_upsert(self, records: Sequence[Any], **kwargs: Any) -> Sequence[TKey]:
updated_keys = []
for record in records:
record = AttributeDict(record)
self.inner_storage[record[self._key_field_name]] = record
updated_keys.append(record[self._key_field_name])
return updated_keys
def _deserialize_store_models_to_dicts(self, records: Sequence[Any], **kwargs: Any) -> Sequence[dict[str, Any]]:
return records
def _serialize_dicts_to_store_models(self, records: Sequence[dict[str, Any]], **kwargs: Any) -> Sequence[Any]:
return records
@override
async def ensure_collection_exists(self, **kwargs: Any) -> None:
pass
@override
async def ensure_collection_deleted(self, **kwargs: Any) -> None:
self.inner_storage = {}
@override
async def collection_exists(self, **kwargs: Any) -> bool:
return True
@override
async def _inner_search(
self,
search_type: SearchType,
options: VectorSearchOptions,
values: Any | None = None,
vector: Sequence[float | int] | None = None,
**kwargs: Any,
) -> KernelSearchResults[VectorSearchResult[TModel]]:
"""Inner search method."""
if not vector:
vector = await self._generate_vector_from_values(values, options)
return_records: dict[TKey, float] = {}
field = self.definition.try_get_vector_field(options.vector_property_name)
if not field:
raise VectorStoreModelException(
f"Vector field '{options.vector_property_name}' not found in the data model definition."
)
if field.distance_function not in DISTANCE_FUNCTION_MAP:
raise VectorSearchExecutionException(
f"Distance function '{field.distance_function}' is not supported. "
f"Supported functions are: {list(DISTANCE_FUNCTION_MAP.keys())}"
)
distance_func = DISTANCE_FUNCTION_MAP[field.distance_function] # type: ignore[assignment]
for key, record in self._get_filtered_records(options).items():
if vector and field is not None:
return_records[key] = self._calculate_vector_similarity(
vector,
record[field.storage_name or field.name],
distance_func,
invert_score=field.distance_function == DistanceFunction.COSINE_SIMILARITY,
)
if field.distance_function == DistanceFunction.DEFAULT:
reverse_func = DISTANCE_FUNCTION_DIRECTION_HELPER[DistanceFunction.COSINE_DISTANCE]
else:
reverse_func = DISTANCE_FUNCTION_DIRECTION_HELPER[field.distance_function] # type: ignore[assignment]
sorted_records = dict(
sorted(
return_records.items(),
key=lambda item: item[1],
reverse=reverse_func(1, 0),
)
)
if sorted_records:
return KernelSearchResults(
results=self._get_vector_search_results_from_results(
self._generate_return_list(sorted_records, options), options
),
total_count=len(return_records) if options and options.include_total_count else None,
)
return KernelSearchResults(results=empty_generator())
async def _generate_return_list(
self, return_records: dict[TKey, float], options: VectorSearchOptions | None
) -> AsyncIterable[dict]:
top = 3 if not options else options.top
skip = 0 if not options else options.skip
returned = 0
for idx, key in enumerate(return_records.keys()):
if idx >= skip:
returned += 1
rec = self.inner_storage[key]
rec[IN_MEMORY_SCORE_KEY] = return_records[key]
yield rec
if returned >= top:
break
def _get_filtered_records(self, options: VectorSearchOptions) -> dict[TKey, AttributeDict]:
if not options.filter:
return self.inner_storage
try:
callable_filters = [
self._parse_and_validate_filter(filter) if isinstance(filter, str) else filter
for filter in ([options.filter] if not isinstance(options.filter, list) else options.filter)
]
except Exception as e:
raise VectorStoreOperationException(f"Error evaluating filter: {e}") from e
filtered_records: dict[TKey, AttributeDict] = {}
for key, record in self.inner_storage.items():
for filter in callable_filters:
if self._run_filter(filter, record):
filtered_records[key] = record
return filtered_records
def _parse_and_validate_filter(self, filter_str: str) -> Callable:
"""Parse and validate a string filter as a lambda expression, then return the callable.
Uses an allowlist approach - only explicitly permitted AST node types and function names
are allowed. This can be customized by overriding `allowed_filter_ast_nodes` and
`allowed_filter_functions` class attributes.
"""
if len(filter_str) > self.max_filter_source_length:
raise VectorStoreOperationException("Filter string exceeds the maximum allowed length.")
try:
tree = ast.parse(filter_str, mode="eval")
except SyntaxError as e:
raise VectorStoreOperationException(f"Filter string is not valid Python: {e}") from e
# Only allow lambda expressions at the top level
if not (isinstance(tree, ast.Expression) and isinstance(tree.body, ast.Lambda)):
raise VectorStoreOperationException(
"Filter string must be a lambda expression, e.g. 'lambda x: x.key == 1'"
)
# Get the lambda parameter name(s) to allow them as valid Name nodes
lambda_node = tree.body
lambda_param_names = {arg.arg for arg in lambda_node.args.args}
lambda_param_order = [arg.arg for arg in lambda_node.args.args]
# Walk the AST to validate all nodes against the allowlist
for node_count, node in enumerate(ast.walk(tree), start=1):
if node_count > self.max_filter_ast_node_count:
raise VectorStoreOperationException("Filter expression exceeds the maximum allowed complexity.")
node_type = type(node)
# Check if the node type is allowed
if node_type not in self.allowed_filter_ast_nodes:
raise VectorStoreOperationException(
f"AST node type '{node_type.__name__}' is not allowed in filter expressions."
)
# For Attribute nodes, validate that dangerous dunder attributes are not accessed
if isinstance(node, ast.Attribute) and node.attr in self.blocked_filter_attributes:
raise VectorStoreOperationException(
f"Access to attribute '{node.attr}' is not allowed in filter expressions. "
"This attribute could be used to escape the filter sandbox."
)
# For Name nodes, only allow the lambda parameter
if isinstance(node, ast.Name) and node.id not in lambda_param_names:
raise VectorStoreOperationException(
f"Use of name '{node.id}' is not allowed in filter expressions. "
f"Only the lambda parameter(s) ({', '.join(lambda_param_names)}) can be used."
)
# For Call nodes, validate that only allowed functions are called
if isinstance(node, ast.Call):
func_name: str
if isinstance(node.func, ast.Name):
func_name = node.func.id
elif isinstance(node.func, ast.Attribute):
func_name = node.func.attr
else:
raise VectorStoreOperationException(
f"Call target node type '{type(node.func).__name__}' is not allowed in filter expressions. "
"Only direct function and method calls are supported."
)
if func_name not in self.allowed_filter_functions:
raise VectorStoreOperationException(
f"Function '{func_name}' is not allowed in filter expressions. "
f"Allowed functions: {', '.join(sorted(self.allowed_filter_functions))}"
)
if (
isinstance(node, (ast.List, ast.Tuple, ast.Set))
and len(node.elts) > self.max_filter_literal_collection_size
):
raise VectorStoreOperationException(
"Collection literals in filter expressions exceed the maximum allowed size."
)
if isinstance(node, ast.Dict) and len(node.keys) > self.max_filter_literal_collection_size:
raise VectorStoreOperationException(
"Collection literals in filter expressions exceed the maximum allowed size."
)
if (
isinstance(node, ast.Constant)
and isinstance(node.value, str)
and len(node.value) > self.max_filter_literal_collection_size
):
raise VectorStoreOperationException(
"String literals in filter expressions exceed the maximum allowed size."
)
evaluator = _SafeFilterEvaluator(
direct_call_functions=self.direct_filter_functions,
blocked_attributes=self.blocked_filter_attributes,
max_literal_collection_size=self.max_filter_literal_collection_size,
max_sequence_repeat_size=self.max_filter_sequence_repeat_size,
)
def filter_callable(*args: Any) -> Any:
if len(args) != len(lambda_param_order):
raise VectorStoreOperationException(
f"Filter expected {len(lambda_param_order)} argument(s), but received {len(args)}."
)
context = {
name: ReadOnlyAttributeDict._wrap_value(value)
for name, value in zip(lambda_param_order, args, strict=True)
}
return evaluator.evaluate(lambda_node.body, context)
return filter_callable
def _run_filter(self, filter: Callable, record: AttributeDict[TAKey, TAValue]) -> bool:
"""Run the filter on the record, supporting attribute access."""
try:
return filter(ReadOnlyAttributeDict(record))
except Exception as e:
raise VectorStoreOperationException(f"Error running filter: {e}") from e
@override
def _lambda_parser(self, node: ast.AST) -> Any:
"""Not used by InMemoryCollection, but required by the interface."""
pass
def _calculate_vector_similarity(
self,
search_vector: Sequence[float | int],
record_vector: Sequence[float | int],
distance_func: Callable,
invert_score: bool = False,
) -> float:
calc = distance_func(record_vector, search_vector)
if invert_score:
return 1.0 - float(calc)
return float(calc)
def _get_record_from_result(self, result: Any) -> Any:
return result
def _get_score_from_result(self, result: Any) -> float | None:
return result.get(IN_MEMORY_SCORE_KEY)
@release_candidate
class InMemoryStore(VectorStore):
"""Create a In Memory Vector Store."""
def __init__(
self,
embedding_generator: EmbeddingGeneratorBase | None = None,
**kwargs: Any,
):
"""Create a In Memory Vector Store."""
super().__init__(embedding_generator=embedding_generator, **kwargs)
@override
async def list_collection_names(self, **kwargs) -> Sequence[str]:
return []
@override
def get_collection(
self,
record_type: type[TModel],
*,
definition: VectorStoreCollectionDefinition | None = None,
collection_name: str | None = None,
embedding_generator: EmbeddingGeneratorBase | None = None,
**kwargs: Any,
) -> InMemoryCollection:
"""Get a collection."""
return InMemoryCollection(
record_type=record_type,
definition=definition,
collection_name=collection_name,
embedding_generator=embedding_generator or self.embedding_generator,
)