chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory
|
||||
from semantic_kernel.memory.volatile_memory_store import VolatileMemoryStore
|
||||
|
||||
__all__ = ["SemanticTextMemory", "VolatileMemoryStore"]
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
|
||||
from numpy import ndarray
|
||||
|
||||
from semantic_kernel.memory.memory_record import MemoryRecord
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated
|
||||
else:
|
||||
from typing_extensions import deprecated
|
||||
|
||||
|
||||
@deprecated("This class will be removed in a future version.")
|
||||
class MemoryQueryResult:
|
||||
"""The memory query result."""
|
||||
|
||||
is_reference: bool
|
||||
external_source_name: str | None
|
||||
id: str
|
||||
description: str | None
|
||||
text: str | None
|
||||
additional_metadata: str | None
|
||||
relevance: float
|
||||
embedding: ndarray | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_reference: bool,
|
||||
external_source_name: str | None,
|
||||
id: str,
|
||||
description: str | None,
|
||||
text: str | None,
|
||||
additional_metadata: str | None,
|
||||
embedding: ndarray | None,
|
||||
relevance: float,
|
||||
) -> None:
|
||||
"""Initialize a new instance of MemoryQueryResult.
|
||||
|
||||
Args:
|
||||
is_reference (bool): Whether the record is a reference record.
|
||||
external_source_name (Optional[str]): The name of the external source.
|
||||
id (str): A unique for the record.
|
||||
description (Optional[str]): The description of the record.
|
||||
text (Optional[str]): The text of the record.
|
||||
additional_metadata (Optional[str]): Custom metadata for the record.
|
||||
embedding (ndarray): The embedding of the record.
|
||||
relevance (float): The relevance of the record to a known query.
|
||||
|
||||
Returns:
|
||||
None: None.
|
||||
"""
|
||||
self.is_reference = is_reference
|
||||
self.external_source_name = external_source_name
|
||||
self.id = id
|
||||
self.description = description
|
||||
self.text = text
|
||||
self.additional_metadata = additional_metadata
|
||||
self.relevance = relevance
|
||||
self.embedding = embedding
|
||||
|
||||
@staticmethod
|
||||
def from_memory_record(
|
||||
record: MemoryRecord,
|
||||
relevance: float,
|
||||
) -> "MemoryQueryResult":
|
||||
"""Create a new instance of MemoryQueryResult from a MemoryRecord.
|
||||
|
||||
Args:
|
||||
record (MemoryRecord): The MemoryRecord to create the MemoryQueryResult from.
|
||||
relevance (float): The relevance of the record to a known query.
|
||||
|
||||
Returns:
|
||||
MemoryQueryResult: The created MemoryQueryResult.
|
||||
"""
|
||||
return MemoryQueryResult(
|
||||
is_reference=record._is_reference,
|
||||
external_source_name=record._external_source_name,
|
||||
id=record._id,
|
||||
description=record._description,
|
||||
text=record._text,
|
||||
additional_metadata=record._additional_metadata,
|
||||
embedding=record._embedding,
|
||||
relevance=relevance,
|
||||
)
|
||||
@@ -0,0 +1,154 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from numpy import ndarray
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated
|
||||
else:
|
||||
from typing_extensions import deprecated
|
||||
|
||||
|
||||
@deprecated("This class will be removed in a future version.")
|
||||
class MemoryRecord:
|
||||
"""The in-built memory record."""
|
||||
|
||||
_key: str
|
||||
_timestamp: datetime | None
|
||||
_is_reference: bool
|
||||
_external_source_name: str | None
|
||||
_id: str
|
||||
_description: str | None
|
||||
_text: str | None
|
||||
_additional_metadata: str | None
|
||||
_embedding: ndarray
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_reference: bool,
|
||||
external_source_name: str | None,
|
||||
id: str,
|
||||
description: str | None,
|
||||
text: str | None,
|
||||
additional_metadata: str | None,
|
||||
embedding: ndarray | None,
|
||||
key: str | None = None,
|
||||
timestamp: datetime | None = None,
|
||||
) -> None:
|
||||
"""Initialize a new instance of MemoryRecord.
|
||||
|
||||
Args:
|
||||
is_reference (bool): Whether the record is a reference record.
|
||||
external_source_name (Optional[str]): The name of the external source.
|
||||
id (str): A unique for the record.
|
||||
description (Optional[str]): The description of the record.
|
||||
text (Optional[str]): The text of the record.
|
||||
additional_metadata (Optional[str]): Custom metadata for the record.
|
||||
embedding (ndarray): The embedding of the record.
|
||||
key (Optional[str]): The key of the record.
|
||||
timestamp (Optional[datetime]): The timestamp of the record.
|
||||
"""
|
||||
self._key = key
|
||||
self._timestamp = timestamp
|
||||
self._is_reference = is_reference
|
||||
self._external_source_name = external_source_name
|
||||
self._id = id
|
||||
self._description = description
|
||||
self._text = text
|
||||
self._additional_metadata = additional_metadata
|
||||
self._embedding = embedding
|
||||
|
||||
@staticmethod
|
||||
def reference_record(
|
||||
external_id: str,
|
||||
source_name: str,
|
||||
description: str | None,
|
||||
additional_metadata: str | None,
|
||||
embedding: ndarray,
|
||||
) -> "MemoryRecord":
|
||||
"""Create a reference record.
|
||||
|
||||
Args:
|
||||
external_id (str): The external id of the record.
|
||||
source_name (str): The name of the external source.
|
||||
description (Optional[str]): The description of the record.
|
||||
additional_metadata (Optional[str]): Custom metadata for the record.
|
||||
embedding (ndarray): The embedding of the record.
|
||||
|
||||
Returns:
|
||||
MemoryRecord: The reference record.
|
||||
"""
|
||||
return MemoryRecord(
|
||||
is_reference=True,
|
||||
external_source_name=source_name,
|
||||
id=external_id,
|
||||
description=description,
|
||||
text=None,
|
||||
additional_metadata=additional_metadata,
|
||||
embedding=embedding,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def local_record(
|
||||
id: str,
|
||||
text: str,
|
||||
description: str | None,
|
||||
additional_metadata: str | None,
|
||||
embedding: ndarray,
|
||||
timestamp: datetime | None = None,
|
||||
) -> "MemoryRecord":
|
||||
"""Create a local record.
|
||||
|
||||
Args:
|
||||
id (str): A unique for the record.
|
||||
text (str): The text of the record.
|
||||
description (Optional[str]): The description of the record.
|
||||
additional_metadata (Optional[str]): Custom metadata for the record.
|
||||
embedding (ndarray): The embedding of the record.
|
||||
timestamp (Optional[datetime]): The timestamp of the record.
|
||||
|
||||
Returns:
|
||||
MemoryRecord: The local record.
|
||||
"""
|
||||
return MemoryRecord(
|
||||
is_reference=False,
|
||||
external_source_name=None,
|
||||
id=id,
|
||||
description=description,
|
||||
text=text,
|
||||
additional_metadata=additional_metadata,
|
||||
timestamp=timestamp,
|
||||
embedding=embedding,
|
||||
)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Get the unique identifier for the memory record."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def embedding(self) -> ndarray:
|
||||
"""Get the embedding of the memory record."""
|
||||
return self._embedding
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""Get the text of the memory record."""
|
||||
return self._text
|
||||
|
||||
@property
|
||||
def additional_metadata(self):
|
||||
"""Get the additional metadata of the memory record."""
|
||||
return self._additional_metadata
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Get the description of the memory record."""
|
||||
return self._description
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
"""Get the timestamp of the memory record."""
|
||||
return self._timestamp
|
||||
@@ -0,0 +1,203 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from numpy import ndarray
|
||||
|
||||
from semantic_kernel.memory.memory_record import MemoryRecord
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated
|
||||
else:
|
||||
from typing_extensions import deprecated
|
||||
|
||||
|
||||
@deprecated("This class will be removed in a future version.")
|
||||
class MemoryStoreBase(ABC):
|
||||
"""Base class for memory store."""
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Enter the context manager."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
"""Exit the context manager."""
|
||||
await self.close()
|
||||
|
||||
async def close(self):
|
||||
"""Close the connection."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_collection(self, collection_name: str) -> None:
|
||||
"""Creates a new collection in the data store.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_collections(
|
||||
self,
|
||||
) -> list[str]:
|
||||
"""Gets all collection names in the data store.
|
||||
|
||||
Returns:
|
||||
List[str]: A group of collection names.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_collection(self, collection_name: str) -> None:
|
||||
"""Deletes a collection from the data store.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def does_collection_exist(self, collection_name: str) -> bool:
|
||||
"""Determines if a collection exists in the data store.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
|
||||
Returns:
|
||||
bool: True if given collection exists, False if not.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upsert(self, collection_name: str, record: MemoryRecord) -> str:
|
||||
"""Upserts a memory record into the data store.
|
||||
|
||||
Does not guarantee that the collection exists.
|
||||
If the record already exists, it will be updated.
|
||||
If the record does not exist, it will be created.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
record (MemoryRecord): The memory record to upsert.
|
||||
|
||||
Returns:
|
||||
str: The unique identifier for the memory record.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def upsert_batch(self, collection_name: str, records: list[MemoryRecord]) -> list[str]:
|
||||
"""Upserts a group of memory records into the data store.
|
||||
|
||||
Does not guarantee that the collection exists.
|
||||
If the record already exists, it will be updated.
|
||||
If the record does not exist, it will be created.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
records (MemoryRecord): The memory records to upsert.
|
||||
|
||||
Returns:
|
||||
List[str]: The unique identifiers for the memory records.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get(self, collection_name: str, key: str, with_embedding: bool) -> MemoryRecord:
|
||||
"""Gets a memory record from the data store. Does not guarantee that the collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
key (str): The unique id associated with the memory record to get.
|
||||
with_embedding (bool): If true, the embedding will be returned in the memory record.
|
||||
|
||||
Returns:
|
||||
MemoryRecord: The memory record if found
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_batch(
|
||||
self,
|
||||
collection_name: str,
|
||||
keys: list[str],
|
||||
with_embeddings: bool,
|
||||
) -> list[MemoryRecord]:
|
||||
"""Gets a batch of memory records from the data store. Does not guarantee that the collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
keys (List[str]): The unique ids associated with the memory records to get.
|
||||
with_embeddings (bool): If true, the embedding will be returned in the memory records.
|
||||
|
||||
Returns:
|
||||
List[MemoryRecord]: The memory records associated with the unique keys provided.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def remove(self, collection_name: str, key: str) -> None:
|
||||
"""Removes a memory record from the data store. Does not guarantee that the collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
key (str): The unique id associated with the memory record to remove.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def remove_batch(self, collection_name: str, keys: list[str]) -> None:
|
||||
"""Removes a batch of memory records from the data store. Does not guarantee that the collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
keys (List[str]): The unique ids associated with the memory records to remove.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_nearest_matches(
|
||||
self,
|
||||
collection_name: str,
|
||||
embedding: ndarray,
|
||||
limit: int,
|
||||
min_relevance_score: float,
|
||||
with_embeddings: bool,
|
||||
) -> list[tuple[MemoryRecord, float]]:
|
||||
"""Gets the nearest matches to an embedding of type float. Does not guarantee that the collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
embedding (ndarray): The embedding to compare the collection's embeddings with.
|
||||
limit (int): The maximum number of similarity results to return.
|
||||
min_relevance_score (float): The minimum relevance threshold for returned results.
|
||||
with_embeddings (bool): If true, the embeddings will be returned in the memory records.
|
||||
|
||||
Returns:
|
||||
List[Tuple[MemoryRecord, float]]: A list of tuples where item1 is a MemoryRecord and item2
|
||||
is its similarity score as a float.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_nearest_match(
|
||||
self,
|
||||
collection_name: str,
|
||||
embedding: ndarray,
|
||||
min_relevance_score: float,
|
||||
with_embedding: bool,
|
||||
) -> tuple[MemoryRecord, float]:
|
||||
"""Gets the nearest match to an embedding of type float. Does not guarantee that the collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name associated with a collection of embeddings.
|
||||
embedding (ndarray): The embedding to compare the collection's embeddings with.
|
||||
min_relevance_score (float): The minimum relevance threshold for returned result.
|
||||
with_embedding (bool): If true, the embeddings will be returned in the memory record.
|
||||
|
||||
Returns:
|
||||
Tuple[MemoryRecord, float]: A tuple consisting of the MemoryRecord and the similarity score as a float.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
|
||||
from semantic_kernel.memory.memory_query_result import MemoryQueryResult
|
||||
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated
|
||||
else:
|
||||
from typing_extensions import deprecated
|
||||
|
||||
|
||||
@deprecated("This class will be removed in a future version.")
|
||||
class NullMemory(SemanticTextMemoryBase):
|
||||
"""Class for null memory."""
|
||||
|
||||
async def save_information(
|
||||
self,
|
||||
collection: str,
|
||||
text: str,
|
||||
id: str,
|
||||
description: str | None = None,
|
||||
additional_metadata: str | None = None,
|
||||
) -> None:
|
||||
"""Nullifies behavior of SemanticTextMemoryBase save_information."""
|
||||
return
|
||||
|
||||
async def save_reference(
|
||||
self,
|
||||
collection: str,
|
||||
text: str,
|
||||
external_id: str,
|
||||
external_source_name: str,
|
||||
description: str | None = None,
|
||||
additional_metadata: str | None = None,
|
||||
) -> None:
|
||||
"""Nullifies behavior of SemanticTextMemoryBase save_reference."""
|
||||
return
|
||||
|
||||
async def get(self, collection: str, query: str) -> MemoryQueryResult | None:
|
||||
"""Nullifies behavior of SemanticTextMemoryBase get."""
|
||||
return None
|
||||
|
||||
async def search(
|
||||
self,
|
||||
collection: str,
|
||||
query: str,
|
||||
limit: int = 1,
|
||||
min_relevance_score: float = 0.7,
|
||||
) -> list[MemoryQueryResult]:
|
||||
"""Nullifies behavior of SemanticTextMemoryBase search."""
|
||||
return []
|
||||
|
||||
async def get_collections(self) -> list[str]:
|
||||
"""Nullifies behavior of SemanticTextMemoryBase get_collections."""
|
||||
return []
|
||||
|
||||
|
||||
NullMemory.instance = NullMemory() # type: ignore
|
||||
@@ -0,0 +1,165 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from pydantic import PrivateAttr
|
||||
|
||||
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
|
||||
from semantic_kernel.memory.memory_query_result import MemoryQueryResult
|
||||
from semantic_kernel.memory.memory_record import MemoryRecord
|
||||
from semantic_kernel.memory.memory_store_base import MemoryStoreBase
|
||||
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated
|
||||
else:
|
||||
from typing_extensions import deprecated
|
||||
|
||||
|
||||
@deprecated("This class will be removed in a future version.")
|
||||
class SemanticTextMemory(SemanticTextMemoryBase):
|
||||
"""Class for semantic text memory."""
|
||||
|
||||
_storage: MemoryStoreBase = PrivateAttr()
|
||||
_embeddings_generator: EmbeddingGeneratorBase = PrivateAttr()
|
||||
|
||||
def __init__(self, storage: MemoryStoreBase, embeddings_generator: EmbeddingGeneratorBase) -> None:
|
||||
"""Initialize a new instance of SemanticTextMemory.
|
||||
|
||||
Args:
|
||||
storage (MemoryStoreBase): The MemoryStoreBase to use for storage.
|
||||
embeddings_generator (EmbeddingGeneratorBase): The EmbeddingGeneratorBase
|
||||
to use for generating embeddings.
|
||||
"""
|
||||
super().__init__()
|
||||
self._storage = storage
|
||||
self._embeddings_generator = embeddings_generator
|
||||
|
||||
async def save_information(
|
||||
self,
|
||||
collection: str,
|
||||
text: str,
|
||||
id: str,
|
||||
description: str | None = None,
|
||||
additional_metadata: str | None = None,
|
||||
embeddings_kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Save information to the memory (calls the memory store's upsert method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to save the information to.
|
||||
text (str): The text to save.
|
||||
id (str): The id of the information.
|
||||
description (Optional[str]): The description of the information.
|
||||
additional_metadata (Optional[str]): Additional metadata of the information.
|
||||
embeddings_kwargs (Optional[Dict[str, Any]]): The embeddings kwargs of the information.
|
||||
"""
|
||||
if not await self._storage.does_collection_exist(collection_name=collection):
|
||||
await self._storage.create_collection(collection_name=collection)
|
||||
|
||||
embedding = (await self._embeddings_generator.generate_embeddings([text], **(embeddings_kwargs or {})))[0]
|
||||
data = MemoryRecord.local_record(
|
||||
id=id,
|
||||
text=text,
|
||||
description=description,
|
||||
additional_metadata=additional_metadata,
|
||||
embedding=embedding,
|
||||
)
|
||||
|
||||
await self._storage.upsert(collection_name=collection, record=data)
|
||||
|
||||
async def save_reference(
|
||||
self,
|
||||
collection: str,
|
||||
text: str,
|
||||
external_id: str,
|
||||
external_source_name: str,
|
||||
description: str | None = None,
|
||||
additional_metadata: str | None = None,
|
||||
embeddings_kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Save a reference to the memory (calls the memory store's upsert method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to save the reference to.
|
||||
text (str): The text to save.
|
||||
external_id (str): The external id of the reference.
|
||||
external_source_name (str): The external source name of the reference.
|
||||
description (Optional[str]): The description of the reference.
|
||||
additional_metadata (Optional[str]): Additional metadata of the reference.
|
||||
embeddings_kwargs (Optional[Dict[str, Any]]): The embeddings kwargs of the reference.
|
||||
"""
|
||||
if not await self._storage.does_collection_exist(collection_name=collection):
|
||||
await self._storage.create_collection(collection_name=collection)
|
||||
|
||||
embedding = (await self._embeddings_generator.generate_embeddings([text], **(embeddings_kwargs or {})))[0]
|
||||
data = MemoryRecord.reference_record(
|
||||
external_id=external_id,
|
||||
source_name=external_source_name,
|
||||
description=description,
|
||||
additional_metadata=additional_metadata,
|
||||
embedding=embedding,
|
||||
)
|
||||
|
||||
await self._storage.upsert(collection_name=collection, record=data)
|
||||
|
||||
async def get(
|
||||
self,
|
||||
collection: str,
|
||||
key: str,
|
||||
) -> MemoryQueryResult | None:
|
||||
"""Get information from the memory (calls the memory store's get method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to get the information from.
|
||||
key (str): The key of the information.
|
||||
|
||||
Returns:
|
||||
Optional[MemoryQueryResult]: The MemoryQueryResult if found, None otherwise.
|
||||
"""
|
||||
record = await self._storage.get(collection_name=collection, key=key)
|
||||
return MemoryQueryResult.from_memory_record(record, 1.0) if record else None
|
||||
|
||||
async def search(
|
||||
self,
|
||||
collection: str,
|
||||
query: str,
|
||||
limit: int = 1,
|
||||
min_relevance_score: float = 0.0,
|
||||
with_embeddings: bool = False,
|
||||
embeddings_kwargs: dict[str, Any] | None = None,
|
||||
) -> list[MemoryQueryResult]:
|
||||
"""Search the memory (calls the memory store's get_nearest_matches method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to search in.
|
||||
query (str): The query to search for.
|
||||
limit (int): The maximum number of results to return. (default: {1})
|
||||
min_relevance_score (float): The minimum relevance score to return. (default: {0.0})
|
||||
with_embeddings (bool): Whether to return the embeddings of the results. (default: {False})
|
||||
embeddings_kwargs (Optional[Dict[str, Any]]): The embeddings kwargs of the information.
|
||||
|
||||
Returns:
|
||||
List[MemoryQueryResult]: The list of MemoryQueryResult found.
|
||||
"""
|
||||
query_embedding = (await self._embeddings_generator.generate_embeddings([query], **(embeddings_kwargs or {})))[
|
||||
0
|
||||
]
|
||||
results = await self._storage.get_nearest_matches(
|
||||
collection_name=collection,
|
||||
embedding=query_embedding,
|
||||
limit=limit,
|
||||
min_relevance_score=min_relevance_score,
|
||||
with_embeddings=with_embeddings,
|
||||
)
|
||||
|
||||
return [MemoryQueryResult.from_memory_record(r[0], r[1]) for r in results]
|
||||
|
||||
async def get_collections(self) -> list[str]:
|
||||
"""Get the list of collections in the memory (calls the memory store's get_collections method).
|
||||
|
||||
Returns:
|
||||
List[str]: The list of all the memory collection names.
|
||||
"""
|
||||
return await self._storage.get_collections()
|
||||
@@ -0,0 +1,116 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.memory.memory_query_result import MemoryQueryResult
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated
|
||||
else:
|
||||
from typing_extensions import deprecated
|
||||
|
||||
SemanticTextMemoryT = TypeVar("SemanticTextMemoryT", bound="SemanticTextMemoryBase")
|
||||
|
||||
|
||||
@deprecated("This class will be removed in a future version.")
|
||||
class SemanticTextMemoryBase(KernelBaseModel):
|
||||
"""Base class for semantic text memory."""
|
||||
|
||||
@abstractmethod
|
||||
async def save_information(
|
||||
self,
|
||||
collection: str,
|
||||
text: str,
|
||||
id: str,
|
||||
description: str | None = None,
|
||||
additional_metadata: str | None = None,
|
||||
embeddings_kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Save information to the memory (calls the memory store's upsert method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to save the information to.
|
||||
text (str): The text to save.
|
||||
id (str): The id of the information.
|
||||
description (Optional[str]): The description of the information.
|
||||
additional_metadata (Optional[str]): Additional metadata of the information.
|
||||
embeddings_kwargs (Optional[Dict[str, Any]]): The embeddings kwargs of the information.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save_reference(
|
||||
self,
|
||||
collection: str,
|
||||
text: str,
|
||||
external_id: str,
|
||||
external_source_name: str,
|
||||
description: str | None = None,
|
||||
additional_metadata: str | None = None,
|
||||
) -> None:
|
||||
"""Save a reference to the memory (calls the memory store's upsert method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to save the reference to.
|
||||
text (str): The text to save.
|
||||
external_id (str): The external id of the reference.
|
||||
external_source_name (str): The external source name of the reference.
|
||||
description (Optional[str]): The description of the reference.
|
||||
additional_metadata (Optional[str]): Additional metadata of the reference.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get(
|
||||
self,
|
||||
collection: str,
|
||||
key: str,
|
||||
) -> "MemoryQueryResult | None":
|
||||
"""Get information from the memory (calls the memory store's get method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to get the information from.
|
||||
key (str): The key of the information.
|
||||
|
||||
Returns:
|
||||
Optional[MemoryQueryResult]: The MemoryQueryResult if found, None otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def search(
|
||||
self,
|
||||
collection: str,
|
||||
query: str,
|
||||
limit: int = 1,
|
||||
min_relevance_score: float = 0.7,
|
||||
) -> list["MemoryQueryResult"]:
|
||||
"""Search the memory (calls the memory store's get_nearest_matches method).
|
||||
|
||||
Args:
|
||||
collection (str): The collection to search in.
|
||||
query (str): The query to search for.
|
||||
limit (int): The maximum number of results to return. (default: {1})
|
||||
min_relevance_score (float): The minimum relevance score to return. (default: {0.0})
|
||||
with_embeddings (bool): Whether to return the embeddings of the results. (default: {False})
|
||||
|
||||
Returns:
|
||||
List[MemoryQueryResult]: The list of MemoryQueryResult found.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_collections(self) -> list[str]:
|
||||
"""Get the list of collections in the memory (calls the memory store's get_collections method).
|
||||
|
||||
Returns:
|
||||
List[str]: The list of all the memory collection names.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,324 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
|
||||
from numpy import array, linalg, ndarray
|
||||
|
||||
from semantic_kernel.exceptions import ServiceResourceNotFoundError
|
||||
from semantic_kernel.memory.memory_record import MemoryRecord
|
||||
from semantic_kernel.memory.memory_store_base import MemoryStoreBase
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated
|
||||
else:
|
||||
from typing_extensions import deprecated
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@deprecated("This class will be removed in a future version. Please use the InMemoryStore and Collection instead.")
|
||||
class VolatileMemoryStore(MemoryStoreBase):
|
||||
"""A volatile memory store that stores data in memory."""
|
||||
|
||||
_store: dict[str, dict[str, MemoryRecord]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initializes a new instance of the VolatileMemoryStore class."""
|
||||
self._store = {}
|
||||
|
||||
async def create_collection(self, collection_name: str) -> None:
|
||||
"""Creates a new collection if it does not exist.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to create.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if collection_name in self._store:
|
||||
pass
|
||||
else:
|
||||
self._store[collection_name] = {}
|
||||
|
||||
async def get_collections(
|
||||
self,
|
||||
) -> list[str]:
|
||||
"""Gets the list of collections.
|
||||
|
||||
Returns:
|
||||
List[str]: The list of collections.
|
||||
"""
|
||||
return list(self._store.keys())
|
||||
|
||||
async def delete_collection(self, collection_name: str) -> None:
|
||||
"""Deletes a collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to delete.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if collection_name in self._store:
|
||||
del self._store[collection_name]
|
||||
|
||||
async def does_collection_exist(self, collection_name: str) -> bool:
|
||||
"""Checks if a collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the collection exists; otherwise, False.
|
||||
"""
|
||||
return collection_name in self._store
|
||||
|
||||
async def upsert(self, collection_name: str, record: MemoryRecord) -> str:
|
||||
"""Upserts a record.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to upsert the record into.
|
||||
record (MemoryRecord): The record to upsert.
|
||||
|
||||
Returns:
|
||||
str: The unique database key of the record.
|
||||
"""
|
||||
if collection_name not in self._store:
|
||||
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
|
||||
|
||||
record._key = record._id
|
||||
self._store[collection_name][record._key] = record
|
||||
return record._key
|
||||
|
||||
async def upsert_batch(self, collection_name: str, records: list[MemoryRecord]) -> list[str]:
|
||||
"""Upserts a batch of records.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to upsert the records into.
|
||||
records (List[MemoryRecord]): The records to upsert.
|
||||
|
||||
Returns:
|
||||
List[str]: The unique database keys of the records.
|
||||
"""
|
||||
if collection_name not in self._store:
|
||||
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
|
||||
|
||||
for record in records:
|
||||
record._key = record._id
|
||||
self._store[collection_name][record._key] = record
|
||||
return [record._key for record in records]
|
||||
|
||||
async def get(self, collection_name: str, key: str, with_embedding: bool = False) -> MemoryRecord:
|
||||
"""Gets a record.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to get the record from.
|
||||
key (str): The unique database key of the record.
|
||||
with_embedding (bool): Whether to include the embedding in the result. (default: {False})
|
||||
|
||||
Returns:
|
||||
MemoryRecord: The record.
|
||||
"""
|
||||
if collection_name not in self._store:
|
||||
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
|
||||
|
||||
if key not in self._store[collection_name]:
|
||||
raise ServiceResourceNotFoundError(f"Key '{key}' not found in collection '{collection_name}'")
|
||||
|
||||
result = self._store[collection_name][key]
|
||||
|
||||
if not with_embedding:
|
||||
# create copy of results without embeddings
|
||||
result = deepcopy(result)
|
||||
result._embedding = None
|
||||
return result
|
||||
|
||||
async def get_batch(
|
||||
self, collection_name: str, keys: list[str], with_embeddings: bool = False
|
||||
) -> list[MemoryRecord]:
|
||||
"""Gets a batch of records.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to get the records from.
|
||||
keys (List[str]): The unique database keys of the records.
|
||||
with_embeddings (bool): Whether to include the embeddings in the results. (default: {False})
|
||||
|
||||
Returns:
|
||||
List[MemoryRecord]: The records.
|
||||
"""
|
||||
if collection_name not in self._store:
|
||||
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
|
||||
|
||||
results = [self._store[collection_name][key] for key in keys if key in self._store[collection_name]]
|
||||
|
||||
if not with_embeddings:
|
||||
# create copy of results without embeddings
|
||||
for result in results:
|
||||
result = deepcopy(result)
|
||||
result._embedding = None
|
||||
return results
|
||||
|
||||
async def remove(self, collection_name: str, key: str) -> None:
|
||||
"""Removes a record.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to remove the record from.
|
||||
key (str): The unique database key of the record to remove.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if collection_name not in self._store:
|
||||
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
|
||||
|
||||
if key not in self._store[collection_name]:
|
||||
raise ServiceResourceNotFoundError(f"Key '{key}' not found in collection '{collection_name}'")
|
||||
|
||||
del self._store[collection_name][key]
|
||||
|
||||
async def remove_batch(self, collection_name: str, keys: list[str]) -> None:
|
||||
"""Removes a batch of records.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to remove the records from.
|
||||
keys (List[str]): The unique database keys of the records to remove.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if collection_name not in self._store:
|
||||
raise ServiceResourceNotFoundError(f"Collection '{collection_name}' does not exist")
|
||||
|
||||
for key in keys:
|
||||
if key in self._store[collection_name]:
|
||||
del self._store[collection_name][key]
|
||||
|
||||
async def get_nearest_match(
|
||||
self,
|
||||
collection_name: str,
|
||||
embedding: ndarray,
|
||||
min_relevance_score: float = 0.0,
|
||||
with_embedding: bool = False,
|
||||
) -> tuple[MemoryRecord, float]:
|
||||
"""Gets the nearest match to an embedding using cosine similarity.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to get the nearest match from.
|
||||
embedding (ndarray): The embedding to find the nearest match to.
|
||||
min_relevance_score (float): The minimum relevance score of the match. (default: {0.0})
|
||||
with_embedding (bool): Whether to include the embedding in the result. (default: {False})
|
||||
|
||||
Returns:
|
||||
Tuple[MemoryRecord, float]: The record and the relevance score.
|
||||
"""
|
||||
return self.get_nearest_matches(
|
||||
collection_name=collection_name,
|
||||
embedding=embedding,
|
||||
limit=1,
|
||||
min_relevance_score=min_relevance_score,
|
||||
with_embeddings=with_embedding,
|
||||
)
|
||||
|
||||
async def get_nearest_matches(
|
||||
self,
|
||||
collection_name: str,
|
||||
embedding: ndarray,
|
||||
limit: int,
|
||||
min_relevance_score: float = 0.0,
|
||||
with_embeddings: bool = False,
|
||||
) -> list[tuple[MemoryRecord, float]]:
|
||||
"""Gets the nearest matches to an embedding using cosine similarity.
|
||||
|
||||
Args:
|
||||
collection_name (str): The name of the collection to get the nearest matches from.
|
||||
embedding (ndarray): The embedding to find the nearest matches to.
|
||||
limit (int): The maximum number of matches to return.
|
||||
min_relevance_score (float): The minimum relevance score of the matches. (default: {0.0})
|
||||
with_embeddings (bool): Whether to include the embeddings in the results. (default: {False})
|
||||
|
||||
Returns:
|
||||
List[Tuple[MemoryRecord, float]]: The records and their relevance scores.
|
||||
"""
|
||||
if collection_name not in self._store:
|
||||
logger.warning(
|
||||
f"Collection '{collection_name}' does not exist in collections: "
|
||||
f"{', '.join([collection for collection in await self.get_collections()])}"
|
||||
)
|
||||
return []
|
||||
|
||||
# Get all the records in the collection
|
||||
memory_records = list(self._store[collection_name].values())
|
||||
|
||||
# Convert the collection of embeddings into a numpy array (stacked)
|
||||
embeddings = array([x._embedding for x in memory_records], dtype=float)
|
||||
embeddings = embeddings.reshape(embeddings.shape[0], -1)
|
||||
|
||||
# If the query embedding has shape (1, embedding_size),
|
||||
# reshape it to (embedding_size,)
|
||||
if len(embedding.shape) == 2:
|
||||
embedding = embedding.reshape(
|
||||
embedding.shape[1],
|
||||
)
|
||||
|
||||
# Use numpy to get the cosine similarity between the query
|
||||
# embedding and all the embeddings in the collection
|
||||
similarity_scores = self.compute_similarity_scores(embedding, embeddings)
|
||||
|
||||
# Then, sort the results by the similarity score
|
||||
sorted_results = sorted(
|
||||
zip(memory_records, similarity_scores),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Then, filter out the results that are below the minimum relevance score
|
||||
filtered_results = [x for x in sorted_results if x[1] >= min_relevance_score]
|
||||
|
||||
# Then, take the top N results
|
||||
top_results = filtered_results[:limit]
|
||||
|
||||
if not with_embeddings:
|
||||
# create copy of results without embeddings
|
||||
for result in top_results:
|
||||
result = deepcopy(result)
|
||||
result[0]._embedding = None
|
||||
return top_results
|
||||
|
||||
def compute_similarity_scores(self, embedding: ndarray, embedding_array: ndarray) -> ndarray:
|
||||
"""Computes the cosine similarity scores between a query embedding and a group of embeddings.
|
||||
|
||||
Args:
|
||||
embedding (ndarray): The query embedding.
|
||||
embedding_array (ndarray): The group of embeddings.
|
||||
|
||||
Returns:
|
||||
ndarray: The cosine similarity scores.
|
||||
"""
|
||||
query_norm = linalg.norm(embedding)
|
||||
collection_norm = linalg.norm(embedding_array, axis=1)
|
||||
|
||||
# Compute indices for which the similarity scores can be computed
|
||||
valid_indices = (query_norm != 0) & (collection_norm != 0)
|
||||
|
||||
# Initialize the similarity scores with -1 to distinguish the cases
|
||||
# between zero similarity from orthogonal vectors and invalid similarity
|
||||
similarity_scores = array([-1.0] * embedding_array.shape[0])
|
||||
|
||||
if valid_indices.any():
|
||||
similarity_scores[valid_indices] = embedding.dot(embedding_array[valid_indices].T) / (
|
||||
query_norm * collection_norm[valid_indices]
|
||||
)
|
||||
if not valid_indices.all():
|
||||
logger.warning(
|
||||
"Some vectors in the embedding collection are zero vectors."
|
||||
"Ignoring cosine similarity score computation for those vectors."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid vectors, cannot compute cosine similarity scores"
|
||||
f"for zero vectors"
|
||||
f"{embedding_array} or {embedding}"
|
||||
)
|
||||
return similarity_scores
|
||||
Reference in New Issue
Block a user