""" Senti AI — Qdrant Vector Store Client. Production vector store using Qdrant. Collections: senti_knowledge → laws, regulations, finance docs senti_memory → user memory embeddings (future) Uses `query_points()` API (qdrant-client >= 1.12). """ from qdrant_client import QdrantClient from qdrant_client.models import ( Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue, ) import os import uuid from typing import Optional QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333") QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", "") _qdrant_instance = None def get_qdrant_client() -> QdrantClient: global _qdrant_instance if _qdrant_instance is not None: return _qdrant_instance import os # Use in-memory for test environment if os.environ.get('SENTI_ENV') == 'test' or \ os.environ.get('PYTEST_CURRENT_TEST'): _qdrant_instance = QdrantClient(location=":memory:") return _qdrant_instance # Use persistent storage for dev/prod storage_path = os.path.join( os.path.dirname(__file__), '..', '..', 'storage', 'vector_store' ) os.makedirs(storage_path, exist_ok=True) try: _qdrant_instance = QdrantClient(path=storage_path) except Exception: # If lock exists, fall back to in-memory _qdrant_instance = QdrantClient(location=":memory:") return _qdrant_instance class SentiVectorStore: """ Production vector store using Qdrant. Collections: senti_knowledge → laws, regulations, finance docs senti_memory → user memory embeddings (future) """ COLLECTIONS = { "knowledge": "senti_knowledge", "memory": "senti_memory", } EMBEDDING_DIM = 1024 # BGE-M3 dimension def __init__(self): self.client = get_qdrant_client() def create_collections(self) -> None: """Create all required collections if they don't exist.""" existing = [ c.name for c in self.client.get_collections().collections ] for name, col_name in self.COLLECTIONS.items(): if col_name not in existing: self.client.create_collection( collection_name=col_name, vectors_config=VectorParams( size=self.EMBEDDING_DIM, distance=Distance.COSINE, ), ) print(f"[QDRANT] Created collection: {col_name}") else: count = self.client.get_collection(col_name).points_count print(f"[QDRANT] Collection {col_name}: {count} documents") def add_documents( self, collection: str, texts: list[str], embeddings: list[list[float]], metadata: list[dict], ) -> int: """Add documents with embeddings to a collection.""" col_name = self.COLLECTIONS.get(collection, collection) points = [] for text, embedding, meta in zip(texts, embeddings, metadata): points.append( PointStruct( id=str(uuid.uuid4()), vector=embedding, payload={ "text": text, "source": meta.get("source", "unknown"), "category": meta.get("category", "general"), "jurisdiction": meta.get("jurisdiction", "KE"), "effective_date": meta.get("effective_date", ""), "chunk_type": meta.get("chunk_type", "content"), }, ) ) self.client.upsert( collection_name=col_name, points=points, ) return len(points) def search( self, collection: str, query_embedding: list[float], limit: int = 5, filters: Optional[dict] = None, ) -> list[dict]: """ Search for similar documents. Returns list with text, score, and metadata. Uses `query_points()` (qdrant-client >= 1.12). Falls back to keyword-only if collection is empty. """ col_name = self.COLLECTIONS.get(collection, collection) qdrant_filter = None if filters: conditions = [] for key, value in filters.items(): conditions.append( FieldCondition(key=key, match=MatchValue(value=value)) ) if conditions: qdrant_filter = Filter(must=conditions) try: response = self.client.query_points( collection_name=col_name, query=query_embedding, limit=limit, query_filter=qdrant_filter, with_payload=True, ) return [ { "text": pt.payload.get("text", ""), "score": pt.score if hasattr(pt, "score") else 0.0, "source": pt.payload.get("source", ""), "category": pt.payload.get("category", ""), "jurisdiction": pt.payload.get("jurisdiction", "KE"), "effective_date": pt.payload.get("effective_date", ""), } for pt in response.points ] except Exception as e: print(f"[QDRANT] Search failed: {e}") return [] def get_count(self, collection: str) -> int: col_name = self.COLLECTIONS.get(collection, collection) return self.client.get_collection(col_name).points_count def health_check(self) -> bool: try: self.client.get_collections() return True except Exception: return False vector_store = SentiVectorStore()