File size: 7,248 Bytes
3740bd7 61d9448 3740bd7 61d9448 3740bd7 61d9448 3740bd7 61d9448 3740bd7 61d9448 3740bd7 61d9448 3740bd7 61d9448 3740bd7 61d9448 3740bd7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | """
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
|