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