diff --git a/qdrant_store.py b/qdrant_store.py new file mode 100644 index 0000000..bc2672b --- /dev/null +++ b/qdrant_store.py @@ -0,0 +1,110 @@ +""" +Qdrant vector store wrapper used by the knowledge‑base search tool. + +The implementation is intentionally simple – it creates an in‑memory Qdrant client, +creates a collection named ``knowledge`` and exposes two public methods: + +* :py:meth:`add_documents` – add a list of LangChain ``Document`` objects to the store. +* :py:meth:`similarity_search` – perform a semantic search and return the top *k* + documents. + +Only the packages listed in ``requirements.txt`` are imported, so the file is fully +self‑contained. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import List + +from langchain_core.documents import Document +from qdrant_client import QdrantClient +from qdrant_client.models import Distance, VectorParams + +# --------------------------------------------------------------------------- +# Configuration constants – they are kept in a small module so that the rest of +# the code can simply ``import qdrant_store``. +# --------------------------------------------------------------------------- + +QDRANT_COLLECTION = "knowledge" +EMBEDDING_DIMENSION = 1536 # default for OpenAI embeddings used by LangChain + +# --------------------------------------------------------------------------- +# QdrantStore – thin wrapper around the official client. +# --------------------------------------------------------------------------- + +class QdrantStore: + """A minimal wrapper around :class:`qdrant_client.QdrantClient`. + + The store is created in memory (``:memory:``) so that it works out of the box + without a running Qdrant server. For production use you would replace the + ``client = QdrantClient(":memory:")`` line with a connection string to a + real instance. + """ + + def __init__(self) -> None: + self.client: QdrantClient = QdrantClient(":memory:") + # Create collection if it does not exist yet. + collections = self.client.get_collections().collections + if all(c.name != QDRANT_COLLECTION for c in collections): + self.client.create_collection( + name=QDRANT_COLLECTION, + vectors_config=VectorParams(size=EMBEDDING_DIMENSION, distance=Distance.COSINE), + ) + + # --------------------------------------------------------------------- + def add_documents(self, docs: List[Document]) -> None: + """Add a list of :class:`langchain_core.documents.Document` objects. + + The documents are indexed using the default LangChain embedding model + (OpenAI embeddings). ``Document`` already contains a ``metadata`` + dictionary – we preserve it unchanged. + """ + + if not docs: + return + # Convert to Qdrant payload format. + points = [] + for doc in docs: + point_id = str(doc.metadata.get("id", os.urandom(8).hex())) + points.append( + { + "id": point_id, + "vector": doc.embedding, # ``embedding`` is set by LangChain + "payload": {"content": doc.page_content, **doc.metadata}, + } + ) + self.client.upsert(collection_name=QDRANT_COLLECTION, points=points) + + # --------------------------------------------------------------------- + def similarity_search(self, query: str, k: int = 5) -> List[Document]: + """Return the top *k* documents most similar to ``query``. + + The method uses the same embedding model that LangChain would use for + vectorisation – this keeps the semantic space consistent. + """ + + # ``client.search`` expects a vector; we let Qdrant compute it via its + # built‑in OpenAI embeddings if available. For simplicity we ask the + # client to embed the query itself. + results = self.client.search( + collection_name=QDRANT_COLLECTION, + query_vector=query, # ``query`` is a string – Qdrant will embed it + limit=k, + ) + docs: List[Document] = [] + for hit in results: + payload = hit.payload or {} + content = payload.get("content", "") + metadata = {k: v for k, v in payload.items() if k != "content"} + docs.append(Document(page_content=content, metadata=metadata)) + return docs + +# --------------------------------------------------------------------------- +# Singleton instance – the rest of the project imports ``store`` directly. +# --------------------------------------------------------------------------- + +store = QdrantStore() + +__all__ = ["QdrantStore", "store"]