from __future__ import annotations
import uuid
from typing import Any, Callable, Iterable, List, Optional, Sequence, Tuple, Type
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_core.vectorstores import VectorStore
class AgentDBVectorStore(VectorStore):
def __init__(
self,
db_path: str,
collection_name: str,
dimension: int,
embedding: Embeddings,
) -> None:
try:
import agentdb as _agentdb
except ImportError as exc: raise ImportError(
"The 'datacules-agentdb' package is required. "
"Install it with: pip install datacules-agentdb"
) from exc
self._db_path = db_path
self._collection_name = collection_name
self._dimension = dimension
self._embedding = embedding
self._db = _agentdb.AgentDB.open(db_path)
self._collection = self._db.collection(collection_name, dimension)
def add_texts(
self,
texts: Iterable[str],
metadatas: Optional[List[dict]] = None,
ids: Optional[List[str]] = None,
**kwargs: Any,
) -> List[str]:
text_list = list(texts)
if not text_list:
return []
if ids is None:
ids = [str(uuid.uuid4()) for _ in text_list]
if metadatas is None:
metadatas = [{} for _ in text_list]
elif len(metadatas) != len(text_list):
raise ValueError(
f"metadatas length ({len(metadatas)}) must match texts length "
f"({len(text_list)})"
)
vectors = self._embedding.embed_documents(text_list)
entries = []
for doc_id, text, vector, meta in zip(ids, text_list, vectors, metadatas):
stored_meta = {"text": text, **meta}
entries.append(
{
"id": doc_id,
"vector": [float(v) for v in vector],
"metadata": stored_meta,
}
)
self._collection.upsert_batch(entries)
return ids
def add_documents(
self,
documents: List[Document],
ids: Optional[List[str]] = None,
**kwargs: Any,
) -> List[str]:
texts = [doc.page_content for doc in documents]
metadatas = [doc.metadata for doc in documents]
return self.add_texts(texts, metadatas=metadatas, ids=ids, **kwargs)
def similarity_search(
self,
query: str,
k: int = 4,
filter: Optional[dict] = None,
**kwargs: Any,
) -> List[Document]:
query_vector = self._embedding.embed_query(query)
return self.similarity_search_by_vector(query_vector, k=k, filter=filter, **kwargs)
def similarity_search_by_vector(
self,
embedding: List[float],
k: int = 4,
filter: Optional[dict] = None,
**kwargs: Any,
) -> List[Document]:
results = self._collection.search(
[float(v) for v in embedding],
top_k=k,
filter=filter,
)
documents: List[Document] = []
for result in results:
meta = result.metadata or {}
text = meta.pop("text", "")
meta["_score"] = result.score
meta["_id"] = result.id
documents.append(Document(page_content=text, metadata=meta))
return documents
def similarity_search_with_score(
self,
query: str,
k: int = 4,
filter: Optional[dict] = None,
**kwargs: Any,
) -> List[Tuple[Document, float]]:
query_vector = self._embedding.embed_query(query)
results = self._collection.search(
[float(v) for v in query_vector],
top_k=k,
filter=filter,
)
docs_and_scores: List[Tuple[Document, float]] = []
for result in results:
meta = result.metadata or {}
text = meta.pop("text", "")
meta["_id"] = result.id
docs_and_scores.append(
(Document(page_content=text, metadata=meta), result.score)
)
return docs_and_scores
@classmethod
def from_texts(
cls: Type["AgentDBVectorStore"],
texts: List[str],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
db_path: str = "agentdb.db",
collection_name: str = "default",
dimension: Optional[int] = None,
ids: Optional[List[str]] = None,
**kwargs: Any,
) -> "AgentDBVectorStore":
if dimension is None:
if not texts:
raise ValueError(
"Cannot infer embedding dimension from an empty texts list. "
"Pass dimension= explicitly."
)
probe = embedding.embed_query(texts[0])
dimension = len(probe)
store = cls(
db_path=db_path,
collection_name=collection_name,
dimension=dimension,
embedding=embedding,
)
store.add_texts(texts, metadatas=metadatas, ids=ids)
return store
@classmethod
def from_documents(
cls: Type["AgentDBVectorStore"],
documents: List[Document],
embedding: Embeddings,
db_path: str = "agentdb.db",
collection_name: str = "default",
dimension: Optional[int] = None,
ids: Optional[List[str]] = None,
**kwargs: Any,
) -> "AgentDBVectorStore":
texts = [doc.page_content for doc in documents]
metadatas = [doc.metadata for doc in documents]
return cls.from_texts(
texts=texts,
embedding=embedding,
metadatas=metadatas,
db_path=db_path,
collection_name=collection_name,
dimension=dimension,
ids=ids,
**kwargs,
)
@property
def embeddings(self) -> Embeddings:
return self._embedding
def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[bool]:
if not ids:
return None
for doc_id in ids:
self._collection.delete(doc_id)
return True
def __len__(self) -> int:
return self._collection.count()
def __repr__(self) -> str:
return (
f"AgentDBVectorStore("
f"db_path={self._db_path!r}, "
f"collection={self._collection_name!r}, "
f"dim={self._dimension})"
)