AskXeno / src /vector_store.py
github-actions
Sync from GitHub
61d9448
Raw
History Blame Contribute Delete
7.25 kB
"""
Vector Store module for XENO Bot
Handles ChromaDB vector store operations
"""
from typing import Any, List, Tuple, cast
import chromadb
import numpy as np
import torch
from langchain_chroma import Chroma
from sentence_transformers import SentenceTransformer, util
from src.config import (CHROMA_DB_PATH, COLLECTION_NAME, EMBEDDING_MODEL,
RAG_MAX_RESULTS, RAG_TOP_K)
from src.knowledge_base import get_knowledge_base_data
_embedding_model = None
def get_embedding_model() -> SentenceTransformer:
"""Lazily load and cache the local embedding model."""
global _embedding_model
if _embedding_model is None:
_embedding_model = SentenceTransformer(EMBEDDING_MODEL)
return _embedding_model
def encode_documents_for_collection(documents: List[str]) -> List[List[float]]:
"""Encode knowledge-base documents for persistent Chroma storage."""
if not documents:
return []
encoded = get_embedding_model().encode(documents)
if hasattr(encoded, "ndim") and encoded.ndim == 1:
return [cast(List[float], encoded.tolist())]
if hasattr(encoded, "tolist"):
return cast(List[List[float]], encoded.tolist())
return cast(List[List[float]], [list(item) for item in encoded])
def initialize_vector_store() -> Tuple[chromadb.Collection, Chroma, Any]:
"""
Initialize ChromaDB vector store
Returns:
Tuple of (collection, vector_store, retriever)
"""
# Get knowledge base data
documents, metadatas, ids = get_knowledge_base_data()
# Initialize ChromaDB client
try:
client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
# Try to get existing collection
try:
collection = client.get_collection(name=COLLECTION_NAME)
print(f"Loaded existing ChromaDB collection: {COLLECTION_NAME}")
except:
# Create new collection if it doesn't exist
print(f"Creating new ChromaDB collection: {COLLECTION_NAME}")
collection = client.create_collection(
name=COLLECTION_NAME,
metadata={"embedding_model": EMBEDDING_MODEL},
)
if documents:
collection.add(
documents=documents,
metadatas=metadatas,
ids=ids,
embeddings=encode_documents_for_collection(documents),
)
# Create vector store and retriever
vector_store = Chroma(client=client, collection_name=COLLECTION_NAME)
retriever = vector_store.as_retriever(
search_type="similarity", search_kwargs={"k": RAG_TOP_K}
)
return collection, vector_store, retriever
except Exception as e:
print(f"Failed to initialize ChromaDB: {e}")
raise
def generate_embeddings(
query: str, documents: List[Any], timer=None
) -> Tuple[List[float], List[List[float]]]:
"""
Generate embeddings for query and documents
Args:
query: User query
documents: List of retrieved documents
timer: Optional timer object for tracking
Returns:
Tuple of (query_embedding, doc_embeddings)
"""
if timer:
with timer.time_step("embedding_generation"):
return _generate_embeddings_impl(query, documents)
else:
return _generate_embeddings_impl(query, documents)
def _generate_embeddings_impl(
query: str, documents: List[Any]
) -> Tuple[List[float], List[List[float]]]:
"""Internal implementation of embedding generation"""
model = get_embedding_model()
query_embedding = model.encode(query)
if hasattr(query_embedding, "tolist"):
query_embedding = query_embedding.tolist()
query_embedding = cast(List[float], query_embedding)
doc_contents = [doc.page_content for doc in documents]
if not doc_contents:
return query_embedding, []
doc_matrix = model.encode(doc_contents)
# Convert model output to list[list[float]] while handling one/many documents.
if hasattr(doc_matrix, "ndim") and doc_matrix.ndim == 1:
doc_embeddings = [doc_matrix.tolist()]
elif hasattr(doc_matrix, "tolist"):
doc_embeddings = doc_matrix.tolist()
else:
doc_embeddings = [list(embedding) for embedding in doc_matrix]
if doc_embeddings and isinstance(doc_embeddings[0], float):
doc_embeddings = [doc_embeddings]
doc_embeddings = cast(List[List[float]], doc_embeddings)
return query_embedding, doc_embeddings
def calculate_similarity(
query_embedding: List[float], doc_embeddings: List[List[float]], timer=None
) -> List[float]:
"""
Calculate cosine similarity between query and documents
Args:
query_embedding: Query embedding vector
doc_embeddings: List of document embedding vectors
timer: Optional timer object for tracking
Returns:
List of cosine similarity scores
"""
if timer:
with timer.time_step("similarity_calculation"):
return _calculate_similarity_impl(query_embedding, doc_embeddings)
else:
return _calculate_similarity_impl(query_embedding, doc_embeddings)
def _calculate_similarity_impl(
query_embedding: List[float], doc_embeddings: List[List[float]]
) -> List[float]:
"""Internal implementation of similarity calculation"""
cosine_scores = util.cos_sim(
torch.tensor(query_embedding).float(), torch.tensor(doc_embeddings).float()
)[0].tolist()
return cosine_scores
def process_context(
results: List[Any],
cosine_scores: List[float],
max_results: int = RAG_MAX_RESULTS,
timer=None,
) -> Tuple[str, List[str], List[Tuple[str, str]]]:
"""
Process retrieved context and format for LLM
Args:
results: List of retrieved documents
cosine_scores: List of similarity scores
max_results: Maximum number of results to include
timer: Optional timer object for tracking
Returns:
Tuple of (formatted_context, source_ids, knowledge_pairs)
"""
if timer:
with timer.time_step("context_processing"):
return _process_context_impl(results, cosine_scores, max_results)
else:
return _process_context_impl(results, cosine_scores, max_results)
def _process_context_impl(
results: List[Any], cosine_scores: List[float], max_results: int
) -> Tuple[str, List[str], List[Tuple[str, str]]]:
"""Internal implementation of context processing"""
sorted_indices = np.argsort(cosine_scores)[::-1][:max_results]
formatted_context = ""
source_ids = []
knowledge_pairs = []
for i, idx in enumerate(sorted_indices, 1):
result = results[idx]
cosine_scores[idx]
question = result.metadata.get("question", "N/A")
answer = result.metadata.get("content", "N/A")
formatted_context += f"Knowledge Entry {i}:\n"
formatted_context += f"Q: {question}\n"
formatted_context += f"A: {answer}\n"
formatted_context += "-" * 40 + "\n"
source_ids.append(result.metadata.get("id", "N/A"))
knowledge_pairs.append((question, answer))
return formatted_context, source_ids, knowledge_pairs