# 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, )