parent
53fc90d9ed
commit
ac921e8df2
@ -1,79 +0,0 @@
|
|||||||
"""Math utils."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
Matrix = Union[List[List[float]], List[np.ndarray], np.ndarray]
|
|
||||||
|
|
||||||
|
|
||||||
def cosine_similarity(X: Matrix, Y: Matrix) -> np.ndarray:
|
|
||||||
"""Row-wise cosine similarity between two equal-width matrices."""
|
|
||||||
if len(X) == 0 or len(Y) == 0:
|
|
||||||
return np.array([])
|
|
||||||
|
|
||||||
X = np.array(X)
|
|
||||||
Y = np.array(Y)
|
|
||||||
if X.shape[1] != Y.shape[1]:
|
|
||||||
raise ValueError(
|
|
||||||
"Number of columns in X and Y must be the same. X has"
|
|
||||||
f" shape {X.shape} and Y has shape {Y.shape}."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
import simsimd as simd
|
|
||||||
|
|
||||||
X = np.array(X, dtype=np.float32)
|
|
||||||
Y = np.array(Y, dtype=np.float32)
|
|
||||||
Z = 1 - simd.cdist(X, Y, metric="cosine")
|
|
||||||
if isinstance(Z, float):
|
|
||||||
return np.array([Z])
|
|
||||||
return Z
|
|
||||||
except ImportError:
|
|
||||||
logger.info(
|
|
||||||
"Unable to import simsimd, defaulting to NumPy"
|
|
||||||
" implementation. If you want to use simsimd please"
|
|
||||||
" install with `pip install simsimd`."
|
|
||||||
)
|
|
||||||
X_norm = np.linalg.norm(X, axis=1)
|
|
||||||
Y_norm = np.linalg.norm(Y, axis=1)
|
|
||||||
# Ignore divide by zero errors run time warnings as those are handled below.
|
|
||||||
with np.errstate(divide="ignore", invalid="ignore"):
|
|
||||||
similarity = np.dot(X, Y.T) / np.outer(X_norm, Y_norm)
|
|
||||||
similarity[np.isnan(similarity) | np.isinf(similarity)] = 0.0
|
|
||||||
return similarity
|
|
||||||
|
|
||||||
|
|
||||||
def cosine_similarity_top_k(
|
|
||||||
X: Matrix,
|
|
||||||
Y: Matrix,
|
|
||||||
top_k: Optional[int] = 5,
|
|
||||||
score_threshold: Optional[float] = None,
|
|
||||||
) -> Tuple[List[Tuple[int, int]], List[float]]:
|
|
||||||
"""Row-wise cosine similarity with optional top-k and score threshold filtering.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
X: Matrix.
|
|
||||||
Y: Matrix, same width as X.
|
|
||||||
top_k: Max number of results to return.
|
|
||||||
score_threshold: Minimum cosine similarity of results.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of two lists. First contains two-tuples of indices (X_idx, Y_idx),
|
|
||||||
second contains corresponding cosine similarities.
|
|
||||||
"""
|
|
||||||
if len(X) == 0 or len(Y) == 0:
|
|
||||||
return [], []
|
|
||||||
score_array = cosine_similarity(X, Y)
|
|
||||||
score_threshold = score_threshold or -1.0
|
|
||||||
score_array[score_array < score_threshold] = 0
|
|
||||||
top_k = min(top_k or len(score_array), np.count_nonzero(score_array))
|
|
||||||
top_k_idxs = np.argpartition(score_array, -top_k, axis=None)[-top_k:]
|
|
||||||
top_k_idxs = top_k_idxs[np.argsort(score_array.ravel()[top_k_idxs])][
|
|
||||||
::-1
|
|
||||||
]
|
|
||||||
ret_idxs = np.unravel_index(top_k_idxs, score_array.shape)
|
|
||||||
scores = score_array.ravel()[top_k_idxs].tolist()
|
|
||||||
return list(zip(*ret_idxs)), scores # type: ignore
|
|
@ -1,194 +0,0 @@
|
|||||||
import threading
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from langchain.chains import RetrievalQA
|
|
||||||
from langchain.chains.question_answering import load_qa_chain
|
|
||||||
from langchain.embeddings.openai import OpenAIEmbeddings
|
|
||||||
from langchain.text_splitter import CharacterTextSplitter
|
|
||||||
from langchain.vectorstores import Chroma
|
|
||||||
from swarms.models.popular_llms import OpenAIChat
|
|
||||||
from swarms.memory.base_vectordb import BaseVectorDatabase
|
|
||||||
|
|
||||||
|
|
||||||
def synchronized_mem(method):
|
|
||||||
"""
|
|
||||||
Decorator that synchronizes access to a method using a lock.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
method: The method to be decorated.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The decorated method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
with self.lock:
|
|
||||||
try:
|
|
||||||
return method(self, *args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to execute {method.__name__}: {e}")
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class LangchainChromaVectorMemory(BaseVectorDatabase):
|
|
||||||
"""
|
|
||||||
A class representing a vector memory for storing and retrieving text entries.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
loc (str): The location of the vector memory.
|
|
||||||
chunk_size (int): The size of each text chunk.
|
|
||||||
chunk_overlap_frac (float): The fraction of overlap between text chunks.
|
|
||||||
embeddings (OpenAIEmbeddings): The embeddings used for text representation.
|
|
||||||
count (int): The current count of text entries in the vector memory.
|
|
||||||
lock (threading.Lock): A lock for thread safety.
|
|
||||||
db (Chroma): The Chroma database for storing text entries.
|
|
||||||
qa (RetrievalQA): The retrieval QA system for answering questions.
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
__init__: Initializes the VectorMemory object.
|
|
||||||
_init_db: Initializes the Chroma database.
|
|
||||||
_init_retriever: Initializes the retrieval QA system.
|
|
||||||
add_entry: Adds an entry to the vector memory.
|
|
||||||
search_memory: Searches the vector memory for similar entries.
|
|
||||||
ask_question: Asks a question to the vector memory.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
loc=None,
|
|
||||||
chunk_size: int = 1000,
|
|
||||||
chunk_overlap_frac: float = 0.1,
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Initializes the VectorMemory object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
loc (str): The location of the vector memory. If None, defaults to "./tmp/vector_memory".
|
|
||||||
chunk_size (int): The size of each text chunk.
|
|
||||||
chunk_overlap_frac (float): The fraction of overlap between text chunks.
|
|
||||||
"""
|
|
||||||
if loc is None:
|
|
||||||
loc = "./tmp/vector_memory"
|
|
||||||
self.loc = Path(loc)
|
|
||||||
self.chunk_size = chunk_size
|
|
||||||
self.chunk_overlap = chunk_size * chunk_overlap_frac
|
|
||||||
self.embeddings = OpenAIEmbeddings()
|
|
||||||
self.count = 0
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
self.db = self._init_db()
|
|
||||||
self.qa = self._init_retriever()
|
|
||||||
|
|
||||||
def _init_db(self):
|
|
||||||
"""
|
|
||||||
Initializes the Chroma database.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Chroma: The initialized Chroma database.
|
|
||||||
"""
|
|
||||||
texts = [
|
|
||||||
"init"
|
|
||||||
] # TODO find how to initialize Chroma without any text
|
|
||||||
chroma_db = Chroma.from_texts(
|
|
||||||
texts=texts,
|
|
||||||
embedding=self.embeddings,
|
|
||||||
persist_directory=str(self.loc),
|
|
||||||
)
|
|
||||||
self.count = chroma_db._collection.count()
|
|
||||||
return chroma_db
|
|
||||||
|
|
||||||
def _init_retriever(self):
|
|
||||||
"""
|
|
||||||
Initializes the retrieval QA system.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
RetrievalQA: The initialized retrieval QA system.
|
|
||||||
"""
|
|
||||||
model = OpenAIChat(
|
|
||||||
model_name="gpt-3.5-turbo",
|
|
||||||
)
|
|
||||||
qa_chain = load_qa_chain(model, chain_type="stuff")
|
|
||||||
retriever = self.db.as_retriever(
|
|
||||||
search_type="mmr", search_kwargs={"k": 10}
|
|
||||||
)
|
|
||||||
qa = RetrievalQA(
|
|
||||||
combine_documents_chain=qa_chain, retriever=retriever
|
|
||||||
)
|
|
||||||
return qa
|
|
||||||
|
|
||||||
@synchronized_mem
|
|
||||||
def add(self, entry: str):
|
|
||||||
"""
|
|
||||||
Add an entry to the internal memory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
entry (str): The entry to be added.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the entry was successfully added, False otherwise.
|
|
||||||
"""
|
|
||||||
text_splitter = CharacterTextSplitter(
|
|
||||||
chunk_size=self.chunk_size,
|
|
||||||
chunk_overlap=self.chunk_overlap,
|
|
||||||
separator=" ",
|
|
||||||
)
|
|
||||||
texts = text_splitter.split_text(entry)
|
|
||||||
|
|
||||||
self.db.add_texts(texts)
|
|
||||||
self.count += self.db._collection.count()
|
|
||||||
self.db.persist()
|
|
||||||
return True
|
|
||||||
|
|
||||||
@synchronized_mem
|
|
||||||
def search_memory(
|
|
||||||
self, query: str, k=10, type="mmr", distance_threshold=0.5
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Searching the vector memory for similar entries.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query (str): The query to search for.
|
|
||||||
k (int): The number of results to return.
|
|
||||||
type (str): The type of search to perform: "cos" or "mmr".
|
|
||||||
distance_threshold (float): The similarity threshold to use for the search. Results with distance > similarity_threshold will be dropped.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[str]: A list of the top k results.
|
|
||||||
"""
|
|
||||||
self.count = self.db._collection.count()
|
|
||||||
if k > self.count:
|
|
||||||
k = self.count - 1
|
|
||||||
if k <= 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if type == "mmr":
|
|
||||||
texts = self.db.max_marginal_relevance_search(
|
|
||||||
query=query, k=k, fetch_k=min(20, self.count)
|
|
||||||
)
|
|
||||||
texts = [text.page_content for text in texts]
|
|
||||||
elif type == "cos":
|
|
||||||
texts = self.db.similarity_search_with_score(query=query, k=k)
|
|
||||||
texts = [
|
|
||||||
text[0].page_content
|
|
||||||
for text in texts
|
|
||||||
if text[-1] < distance_threshold
|
|
||||||
]
|
|
||||||
|
|
||||||
return texts
|
|
||||||
|
|
||||||
@synchronized_mem
|
|
||||||
def query(self, question: str):
|
|
||||||
"""
|
|
||||||
Ask a question to the vector memory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
question (str): The question to ask.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The answer to the question.
|
|
||||||
"""
|
|
||||||
answer = self.qa.run(question)
|
|
||||||
return answer
|
|
@ -1,92 +0,0 @@
|
|||||||
"""Utility functions for working with vectors and vectorstores."""
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from typing import List, Tuple, Type
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from swarms.memory.cosine_similarity import cosine_similarity
|
|
||||||
from swarms.structs.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class DistanceStrategy(str, Enum):
|
|
||||||
"""Enumerator of the Distance strategies for calculating distances
|
|
||||||
between vectors."""
|
|
||||||
|
|
||||||
EUCLIDEAN_DISTANCE = "EUCLIDEAN_DISTANCE"
|
|
||||||
MAX_INNER_PRODUCT = "MAX_INNER_PRODUCT"
|
|
||||||
DOT_PRODUCT = "DOT_PRODUCT"
|
|
||||||
JACCARD = "JACCARD"
|
|
||||||
COSINE = "COSINE"
|
|
||||||
|
|
||||||
|
|
||||||
def maximal_marginal_relevance(
|
|
||||||
query_embedding: np.ndarray,
|
|
||||||
embedding_list: list,
|
|
||||||
lambda_mult: float = 0.5,
|
|
||||||
k: int = 4,
|
|
||||||
) -> List[int]:
|
|
||||||
"""
|
|
||||||
Calculate maximal marginal relevance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query_embedding (np.ndarray): The embedding of the query.
|
|
||||||
embedding_list (list): List of embeddings to select from.
|
|
||||||
lambda_mult (float, optional): The weight for query score. Defaults to 0.5.
|
|
||||||
k (int, optional): The number of embeddings to select. Defaults to 4.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[int]: List of indices of selected embeddings.
|
|
||||||
"""
|
|
||||||
if min(k, len(embedding_list)) <= 0:
|
|
||||||
return []
|
|
||||||
if query_embedding.ndim == 1:
|
|
||||||
query_embedding = np.expand_dims(query_embedding, axis=0)
|
|
||||||
similarity_to_query = cosine_similarity(
|
|
||||||
query_embedding, embedding_list
|
|
||||||
)[0]
|
|
||||||
most_similar = int(np.argmax(similarity_to_query))
|
|
||||||
idxs = [most_similar]
|
|
||||||
selected = np.array([embedding_list[most_similar]])
|
|
||||||
while len(idxs) < min(k, len(embedding_list)):
|
|
||||||
best_score = -np.inf
|
|
||||||
idx_to_add = -1
|
|
||||||
similarity_to_selected = cosine_similarity(
|
|
||||||
embedding_list, selected
|
|
||||||
)
|
|
||||||
for i, query_score in enumerate(similarity_to_query):
|
|
||||||
if i in idxs:
|
|
||||||
continue
|
|
||||||
redundant_score = max(similarity_to_selected[i])
|
|
||||||
equation_score = (
|
|
||||||
lambda_mult * query_score
|
|
||||||
- (1 - lambda_mult) * redundant_score
|
|
||||||
)
|
|
||||||
if equation_score > best_score:
|
|
||||||
best_score = equation_score
|
|
||||||
idx_to_add = i
|
|
||||||
idxs.append(idx_to_add)
|
|
||||||
selected = np.append(
|
|
||||||
selected, [embedding_list[idx_to_add]], axis=0
|
|
||||||
)
|
|
||||||
return idxs
|
|
||||||
|
|
||||||
|
|
||||||
def filter_complex_metadata(
|
|
||||||
documents: List[Document],
|
|
||||||
*,
|
|
||||||
allowed_types: Tuple[Type, ...] = (str, bool, int, float),
|
|
||||||
) -> List[Document]:
|
|
||||||
"""Filter out metadata types that are not supported for a vector store."""
|
|
||||||
updated_documents = []
|
|
||||||
for document in documents:
|
|
||||||
filtered_metadata = {}
|
|
||||||
for key, value in document.metadata.items():
|
|
||||||
if not isinstance(value, allowed_types):
|
|
||||||
continue
|
|
||||||
filtered_metadata[key] = value
|
|
||||||
|
|
||||||
document.metadata = filtered_metadata
|
|
||||||
updated_documents.append(document)
|
|
||||||
|
|
||||||
return updated_documents
|
|
Loading…
Reference in new issue