Download src/vector_store.py from mukiibi/AskXeno: direct link, hf CLI and curl.
- Browser
- Download file 7.25 kB
-
https://huggingface.co/spaces/mukiibi/AskXeno/resolve/main/src/vector_store.py
- Command line
-
hf download hf://spaces/mukiibi/AskXeno/src/vector_store.py
-
curl -L -o vector_store.py https://huggingface.co/spaces/mukiibi/AskXeno/resolve/main/src/vector_store.py
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 | |