brags 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brags/__init__.py +0 -0
- brags/config_parser/data_types.py +79 -0
- brags/config_parser/parser.py +10 -0
- brags/factories/baseclasses/baseembedding.py +8 -0
- brags/factories/baseclasses/basellm.py +8 -0
- brags/factories/baseclasses/basevectorstore.py +12 -0
- brags/factories/embedding/embeddingFactory.py +14 -0
- brags/factories/embedding/implementations/ensembleEmbedding.py +381 -0
- brags/factories/embedding/implementations/huggingFaceEmbedding.py +10 -0
- brags/factories/llm/implementations/googleGenAILLM.py +15 -0
- brags/factories/llm/implementations/ollamaLLM.py +14 -0
- brags/factories/llm/llmFactory.py +14 -0
- brags/factories/vectorStore/implementations/chroma_vector_store.py +67 -0
- brags/factories/vectorStore/implementations/faiss_vector_store.py +82 -0
- brags/factories/vectorStore/vector_store_factory.py +13 -0
- brags/main.py +79 -0
- brags/main_test.py +73 -0
- brags/pipeline/assembler.py +73 -0
- brags/rag_config.example.yaml +110 -0
- brags/test_documents.py +132 -0
- brags/utils/logging_setup.py +24 -0
- brags/utils/print_rag_config.py +59 -0
- brags/version.py +9 -0
- brags-0.0.1.dist-info/LICENSE +674 -0
- brags-0.0.1.dist-info/METADATA +14 -0
- brags-0.0.1.dist-info/RECORD +27 -0
- brags-0.0.1.dist-info/WHEEL +4 -0
brags/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Any, Optional, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LLMConfig(BaseModel):
|
|
6
|
+
provider: str
|
|
7
|
+
model_name: str
|
|
8
|
+
temperature: float
|
|
9
|
+
max_tokens: int
|
|
10
|
+
api_keys: Optional[Dict[str, str]] = None
|
|
11
|
+
huggingface_api_token: Optional[str] = None
|
|
12
|
+
ollama_host: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EmbeddingConfig(BaseModel):
|
|
16
|
+
provider: str
|
|
17
|
+
model_name: str
|
|
18
|
+
dimensions: int
|
|
19
|
+
normalize: bool
|
|
20
|
+
ensemble_weights: Optional[Dict[str, float]] = None
|
|
21
|
+
cache_dir: Optional[str] = "./embedding_cache"
|
|
22
|
+
tfidf_config: Optional[Dict[str, Any]] = None
|
|
23
|
+
lda_config: Optional[Dict[str, Any]] = None
|
|
24
|
+
bm25_enabled: Optional[bool] = True
|
|
25
|
+
|
|
26
|
+
class VectorStoreConfig(BaseModel):
|
|
27
|
+
type: str
|
|
28
|
+
persist_path: str
|
|
29
|
+
similarity_metric: str
|
|
30
|
+
top_k: int
|
|
31
|
+
allow_dangerous_deserialization: Optional[bool] = False
|
|
32
|
+
save_if_not_local: Optional[bool] = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ChunkingConfig(BaseModel):
|
|
36
|
+
chunk_size: int
|
|
37
|
+
chunk_overlap: int
|
|
38
|
+
splitter: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RerankingConfig(BaseModel):
|
|
42
|
+
enabled: bool
|
|
43
|
+
model_name: Optional[str] = None
|
|
44
|
+
top_k: Optional[int] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HallucinationCheckerConfig(BaseModel):
|
|
48
|
+
enabled: bool
|
|
49
|
+
same_as_retriever: bool
|
|
50
|
+
method: str
|
|
51
|
+
threshold: Optional[float] = None
|
|
52
|
+
provider: Optional[str] = None
|
|
53
|
+
model_name: Optional[str] = None
|
|
54
|
+
temperature: Optional[float] = None
|
|
55
|
+
max_tokens: Optional[int] = None
|
|
56
|
+
api_keys: Optional[Dict[str, str]] = None
|
|
57
|
+
huggingface_api_token: Optional[str] = None
|
|
58
|
+
ollama_host: Optional[str] = None
|
|
59
|
+
prompt_template: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LoggingConfig(BaseModel):
|
|
63
|
+
level: str
|
|
64
|
+
log_to_file: bool
|
|
65
|
+
log_file_path: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RAGConfig(BaseModel):
|
|
69
|
+
llm: LLMConfig
|
|
70
|
+
embedding: EmbeddingConfig
|
|
71
|
+
vector_store: VectorStoreConfig
|
|
72
|
+
chunking: ChunkingConfig
|
|
73
|
+
reranking: RerankingConfig
|
|
74
|
+
hallucination_checker: HallucinationCheckerConfig
|
|
75
|
+
logging: LoggingConfig
|
|
76
|
+
rag_mode: str
|
|
77
|
+
streaming: bool
|
|
78
|
+
use_cache: bool
|
|
79
|
+
debug: bool
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
class BaseVectorStore(ABC):
|
|
5
|
+
@abstractmethod
|
|
6
|
+
def create(self, embedder: Any, documents: list = None, save_if_not_local=False) -> Any:
|
|
7
|
+
"""Return a LangChain VectorStore instance"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def remove_by_path(self, embedder, path: str) -> Any:
|
|
12
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from brags.config_parser.data_types import EmbeddingConfig
|
|
2
|
+
from brags.factories.baseclasses.baseembedding import BaseEmbedding
|
|
3
|
+
from .implementations.huggingFaceEmbedding import HuggingFaceEmbedding
|
|
4
|
+
from .implementations.ensembleEmbedding import EnsembleEmbedding
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EmbeddingFactory:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def create(config: EmbeddingConfig) -> BaseEmbedding:
|
|
10
|
+
if config.provider == "huggingface":
|
|
11
|
+
return HuggingFaceEmbedding(config)
|
|
12
|
+
elif config.provider == "ensemble":
|
|
13
|
+
return EnsembleEmbedding(config)
|
|
14
|
+
raise ValueError(f"Unsupported embedding provider: {config.provider}")
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
from ....config_parser.data_types import EmbeddingConfig
|
|
2
|
+
import numpy as np
|
|
3
|
+
import os
|
|
4
|
+
import pickle
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
|
7
|
+
from langchain.schema import Document
|
|
8
|
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
9
|
+
from gensim import corpora
|
|
10
|
+
from gensim.models import LdaModel
|
|
11
|
+
|
|
12
|
+
from ....factories.baseclasses.baseembedding import BaseEmbedding
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EnsembleEmbedding(BaseEmbedding):
|
|
16
|
+
"""
|
|
17
|
+
Ensemble embedding that combines multiple retrieval methods:
|
|
18
|
+
- Dense embeddings (HuggingFace)
|
|
19
|
+
- TF-IDF (sparse representations)
|
|
20
|
+
- LDA topic modeling
|
|
21
|
+
- BM25 (optional)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: EmbeddingConfig):
|
|
25
|
+
self.config = config
|
|
26
|
+
self.cache_dir = config.cache_dir or "./embedding_cache"
|
|
27
|
+
|
|
28
|
+
# Default ensemble weights
|
|
29
|
+
self.weights = config.ensemble_weights or {
|
|
30
|
+
'vector': 0.4,
|
|
31
|
+
'tfidf': 0.3,
|
|
32
|
+
'lda': 0.2,
|
|
33
|
+
'bm25': 0.1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Configuration for individual components
|
|
37
|
+
self.tfidf_config = config.tfidf_config or {
|
|
38
|
+
'max_features': 10000,
|
|
39
|
+
'stop_words': 'english',
|
|
40
|
+
'ngram_range': (1, 2),
|
|
41
|
+
'min_df': 2,
|
|
42
|
+
'max_df': 0.8
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
self.lda_config = config.lda_config or {
|
|
46
|
+
'num_topics': 20,
|
|
47
|
+
'passes': 10,
|
|
48
|
+
'random_state': 42,
|
|
49
|
+
'alpha': 'auto'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
self.bm25_enabled = config.bm25_enabled if config.bm25_enabled is not None else True
|
|
53
|
+
|
|
54
|
+
# Will be initialized when create() is called
|
|
55
|
+
self._embedding_instance = None
|
|
56
|
+
self._initialized = False
|
|
57
|
+
|
|
58
|
+
def create(self):
|
|
59
|
+
"""Create the ensemble embedding instance"""
|
|
60
|
+
if self._embedding_instance is None:
|
|
61
|
+
self._embedding_instance = EnsembleEmbeddingInstance(
|
|
62
|
+
config=self.config,
|
|
63
|
+
weights=self.weights,
|
|
64
|
+
cache_dir=self.cache_dir,
|
|
65
|
+
tfidf_config=self.tfidf_config,
|
|
66
|
+
lda_config=self.lda_config,
|
|
67
|
+
bm25_enabled=self.bm25_enabled
|
|
68
|
+
)
|
|
69
|
+
return self._embedding_instance
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EnsembleEmbeddingInstance:
|
|
73
|
+
"""
|
|
74
|
+
The actual embedding instance that handles ensemble operations
|
|
75
|
+
Compatible with LangChain's embedding interface
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
config: EmbeddingConfig,
|
|
81
|
+
weights: Dict[str, float],
|
|
82
|
+
cache_dir: str,
|
|
83
|
+
tfidf_config: Dict[str, Any],
|
|
84
|
+
lda_config: Dict[str, Any],
|
|
85
|
+
bm25_enabled: bool
|
|
86
|
+
):
|
|
87
|
+
self.config = config
|
|
88
|
+
self.weights = weights
|
|
89
|
+
self.cache_dir = cache_dir
|
|
90
|
+
self.tfidf_config = tfidf_config
|
|
91
|
+
self.lda_config = lda_config
|
|
92
|
+
self.bm25_enabled = bm25_enabled
|
|
93
|
+
|
|
94
|
+
# Initialize base vector embedding
|
|
95
|
+
self.vector_embedding = HuggingFaceEmbeddings(
|
|
96
|
+
model_name=config.model_name,
|
|
97
|
+
encode_kwargs={'normalize_embeddings': config.normalize}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Components will be initialized when documents are provided
|
|
101
|
+
self.tfidf_vectorizer = None
|
|
102
|
+
self.tfidf_matrix = None
|
|
103
|
+
self.lda_model = None
|
|
104
|
+
self.doc_topic_matrix = None
|
|
105
|
+
self.dictionary = None
|
|
106
|
+
self.bm25 = None
|
|
107
|
+
self.documents = None
|
|
108
|
+
self.document_texts = None
|
|
109
|
+
|
|
110
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
|
111
|
+
|
|
112
|
+
def initialize_with_documents(self, documents: List[Document]):
|
|
113
|
+
"""Initialize the ensemble components with a document corpus"""
|
|
114
|
+
if self.documents is not None:
|
|
115
|
+
# Already initialized
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
self.documents = documents
|
|
119
|
+
self.document_texts = [doc.page_content for doc in documents]
|
|
120
|
+
|
|
121
|
+
print(f"Initializing ensemble embedding with {len(documents)} documents...")
|
|
122
|
+
|
|
123
|
+
# Initialize all components
|
|
124
|
+
self._init_tfidf()
|
|
125
|
+
self._init_lda()
|
|
126
|
+
if self.bm25_enabled:
|
|
127
|
+
self._init_bm25()
|
|
128
|
+
|
|
129
|
+
print("Ensemble embedding initialized")
|
|
130
|
+
|
|
131
|
+
def _init_tfidf(self):
|
|
132
|
+
"""Initialize TF-IDF component"""
|
|
133
|
+
cache_path = os.path.join(self.cache_dir, "ensemble_tfidf.pkl")
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
if os.path.exists(cache_path):
|
|
137
|
+
with open(cache_path, 'rb') as f:
|
|
138
|
+
self.tfidf_vectorizer, self.tfidf_matrix = pickle.load(f)
|
|
139
|
+
print("Loaded cached TF-IDF components")
|
|
140
|
+
else:
|
|
141
|
+
raise FileNotFoundError("Cache not found")
|
|
142
|
+
|
|
143
|
+
except (FileNotFoundError, EOFError):
|
|
144
|
+
print("Building TF-IDF components...")
|
|
145
|
+
self.tfidf_vectorizer = TfidfVectorizer(**self.tfidf_config)
|
|
146
|
+
self.tfidf_matrix = self.tfidf_vectorizer.fit_transform(self.document_texts)
|
|
147
|
+
|
|
148
|
+
with open(cache_path, 'wb') as f:
|
|
149
|
+
pickle.dump((self.tfidf_vectorizer, self.tfidf_matrix), f)
|
|
150
|
+
print("TF-IDF components cached")
|
|
151
|
+
|
|
152
|
+
def _init_lda(self):
|
|
153
|
+
"""Initialize LDA component"""
|
|
154
|
+
cache_path = os.path.join(self.cache_dir, "ensemble_lda.pkl")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
if os.path.exists(cache_path):
|
|
158
|
+
with open(cache_path, 'rb') as f:
|
|
159
|
+
self.lda_model, self.doc_topic_matrix, self.dictionary = pickle.load(f)
|
|
160
|
+
print("Loaded cached LDA components")
|
|
161
|
+
else:
|
|
162
|
+
raise FileNotFoundError("Cache not found")
|
|
163
|
+
|
|
164
|
+
except (FileNotFoundError, EOFError):
|
|
165
|
+
print("Building LDA components...")
|
|
166
|
+
|
|
167
|
+
# Preprocess texts
|
|
168
|
+
processed_texts = []
|
|
169
|
+
for text in self.document_texts:
|
|
170
|
+
words = text.lower().split()
|
|
171
|
+
words = [w for w in words if len(w) > 3 and w.isalpha()]
|
|
172
|
+
processed_texts.append(words)
|
|
173
|
+
|
|
174
|
+
# Create dictionary and corpus
|
|
175
|
+
self.dictionary = corpora.Dictionary(processed_texts)
|
|
176
|
+
self.dictionary.filter_extremes(no_below=2, no_above=0.8)
|
|
177
|
+
corpus = [self.dictionary.doc2bow(text) for text in processed_texts]
|
|
178
|
+
|
|
179
|
+
# Dynamic topic count
|
|
180
|
+
num_topics = min(self.lda_config['num_topics'], len(self.documents) // 5)
|
|
181
|
+
|
|
182
|
+
# Train LDA model
|
|
183
|
+
self.lda_model = LdaModel(
|
|
184
|
+
corpus=corpus,
|
|
185
|
+
id2word=self.dictionary,
|
|
186
|
+
num_topics=num_topics,
|
|
187
|
+
passes=self.lda_config['passes'],
|
|
188
|
+
random_state=self.lda_config['random_state'],
|
|
189
|
+
alpha=self.lda_config['alpha'],
|
|
190
|
+
per_word_topics=True
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Get document-topic distributions
|
|
194
|
+
self.doc_topic_matrix = np.zeros((len(self.documents), num_topics))
|
|
195
|
+
for i, doc_bow in enumerate(corpus):
|
|
196
|
+
topic_dist = self.lda_model.get_document_topics(doc_bow, minimum_probability=0)
|
|
197
|
+
for topic_id, prob in topic_dist:
|
|
198
|
+
self.doc_topic_matrix[i, topic_id] = prob
|
|
199
|
+
|
|
200
|
+
with open(cache_path, 'wb') as f:
|
|
201
|
+
pickle.dump((self.lda_model, self.doc_topic_matrix, self.dictionary), f)
|
|
202
|
+
print("LDA components cached")
|
|
203
|
+
|
|
204
|
+
def _init_bm25(self):
|
|
205
|
+
"""Initialize BM25 component"""
|
|
206
|
+
try:
|
|
207
|
+
from rank_bm25 import BM25Okapi
|
|
208
|
+
|
|
209
|
+
cache_path = os.path.join(self.cache_dir, "ensemble_bm25.pkl")
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
if os.path.exists(cache_path):
|
|
213
|
+
with open(cache_path, 'rb') as f:
|
|
214
|
+
self.bm25 = pickle.load(f)
|
|
215
|
+
print(" Loaded cached BM25 index")
|
|
216
|
+
else:
|
|
217
|
+
raise FileNotFoundError("Cache not found")
|
|
218
|
+
|
|
219
|
+
except (FileNotFoundError, EOFError):
|
|
220
|
+
print(" Building BM25 index...")
|
|
221
|
+
tokenized_docs = [doc.lower().split() for doc in self.document_texts]
|
|
222
|
+
self.bm25 = BM25Okapi(tokenized_docs)
|
|
223
|
+
|
|
224
|
+
with open(cache_path, 'wb') as f:
|
|
225
|
+
pickle.dump(self.bm25, f)
|
|
226
|
+
print("BM25 index cached")
|
|
227
|
+
|
|
228
|
+
except ImportError:
|
|
229
|
+
print("BM25 not available. Install rank_bm25 to enable BM25 scoring.")
|
|
230
|
+
self.bm25 = None
|
|
231
|
+
self.weights['bm25'] = 0
|
|
232
|
+
# Renormalize weights
|
|
233
|
+
total = sum(v for k, v in self.weights.items() if k != 'bm25')
|
|
234
|
+
if total > 0:
|
|
235
|
+
for k in ['vector', 'tfidf', 'lda']:
|
|
236
|
+
if k in self.weights:
|
|
237
|
+
self.weights[k] /= total
|
|
238
|
+
|
|
239
|
+
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
|
240
|
+
"""
|
|
241
|
+
Embed documents using ensemble approach
|
|
242
|
+
Returns combined embeddings that incorporate multiple similarity methods
|
|
243
|
+
"""
|
|
244
|
+
# For document embedding, we primarily use the vector embedding
|
|
245
|
+
# The ensemble scoring happens at retrieval time
|
|
246
|
+
return self.vector_embedding.embed_documents(texts)
|
|
247
|
+
|
|
248
|
+
def embed_query(self, text: str) -> List[float]:
|
|
249
|
+
"""
|
|
250
|
+
Embed query using ensemble approach
|
|
251
|
+
Returns combined embedding for query
|
|
252
|
+
"""
|
|
253
|
+
# For query embedding, we primarily use the vector embedding
|
|
254
|
+
# The ensemble scoring happens at retrieval time
|
|
255
|
+
return self.vector_embedding.embed_query(text)
|
|
256
|
+
|
|
257
|
+
def get_ensemble_similarities(self, query: str, top_k: int = 10) -> List[Dict[str, Any]]:
|
|
258
|
+
"""
|
|
259
|
+
Get document similarities using ensemble approach
|
|
260
|
+
Returns list of documents with ensemble scores
|
|
261
|
+
"""
|
|
262
|
+
if self.documents is None:
|
|
263
|
+
raise ValueError("Ensemble embedding not initialized with documents. Call initialize_with_documents() first.")
|
|
264
|
+
|
|
265
|
+
# Get query representations for different methods
|
|
266
|
+
query_representations = self._get_query_representations(query)
|
|
267
|
+
|
|
268
|
+
# Score documents using all methods
|
|
269
|
+
method_scores = self._score_documents(query, query_representations)
|
|
270
|
+
|
|
271
|
+
# Combine scores
|
|
272
|
+
final_scores = self._ensemble_scoring(method_scores)
|
|
273
|
+
|
|
274
|
+
# Get top-k results
|
|
275
|
+
top_indices = np.argsort(final_scores)[::-1][:top_k]
|
|
276
|
+
|
|
277
|
+
results = []
|
|
278
|
+
for idx in top_indices:
|
|
279
|
+
results.append({
|
|
280
|
+
'document_index': idx,
|
|
281
|
+
'document': self.documents[idx],
|
|
282
|
+
'ensemble_score': float(final_scores[idx]),
|
|
283
|
+
'method_scores': {
|
|
284
|
+
'vector': float(method_scores['vector'][idx]),
|
|
285
|
+
'tfidf': float(method_scores['tfidf'][idx]),
|
|
286
|
+
'lda': float(method_scores['lda'][idx]),
|
|
287
|
+
'bm25': float(method_scores['bm25'][idx]) if self.bm25 else 0.0
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
return results
|
|
292
|
+
|
|
293
|
+
def _get_query_representations(self, query: str) -> Dict[str, Any]:
|
|
294
|
+
"""Get query representations for different methods"""
|
|
295
|
+
representations = {}
|
|
296
|
+
|
|
297
|
+
# Vector representation
|
|
298
|
+
representations['vector'] = np.array(self.vector_embedding.embed_query(query))
|
|
299
|
+
|
|
300
|
+
# TF-IDF representation
|
|
301
|
+
if self.tfidf_vectorizer:
|
|
302
|
+
representations['tfidf'] = self.tfidf_vectorizer.transform([query])
|
|
303
|
+
|
|
304
|
+
# LDA representation
|
|
305
|
+
if self.lda_model and self.dictionary:
|
|
306
|
+
query_words = [w for w in query.lower().split() if len(w) > 3 and w.isalpha()]
|
|
307
|
+
query_bow = self.dictionary.doc2bow(query_words)
|
|
308
|
+
query_topics = self.lda_model.get_document_topics(query_bow, minimum_probability=0)
|
|
309
|
+
|
|
310
|
+
query_topic_vec = np.zeros(self.lda_model.num_topics)
|
|
311
|
+
for topic_id, prob in query_topics:
|
|
312
|
+
query_topic_vec[topic_id] = prob
|
|
313
|
+
representations['lda'] = query_topic_vec
|
|
314
|
+
|
|
315
|
+
# BM25 representation
|
|
316
|
+
if self.bm25:
|
|
317
|
+
representations['bm25'] = query.lower().split()
|
|
318
|
+
|
|
319
|
+
return representations
|
|
320
|
+
|
|
321
|
+
def _score_documents(self, query: str, query_representations: Dict[str, Any]) -> Dict[str, np.ndarray]:
|
|
322
|
+
"""Score all documents using different methods"""
|
|
323
|
+
scores = {}
|
|
324
|
+
num_docs = len(self.documents)
|
|
325
|
+
|
|
326
|
+
# Vector similarity scores
|
|
327
|
+
if 'vector' in query_representations:
|
|
328
|
+
doc_embeddings = np.array(self.embed_documents(self.document_texts))
|
|
329
|
+
query_vec = query_representations['vector'].reshape(1, -1)
|
|
330
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
331
|
+
vector_similarities = cosine_similarity(query_vec, doc_embeddings).flatten()
|
|
332
|
+
scores['vector'] = vector_similarities
|
|
333
|
+
else:
|
|
334
|
+
scores['vector'] = np.zeros(num_docs)
|
|
335
|
+
|
|
336
|
+
# TF-IDF similarity scores
|
|
337
|
+
if 'tfidf' in query_representations and self.tfidf_matrix is not None:
|
|
338
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
339
|
+
tfidf_similarities = cosine_similarity(
|
|
340
|
+
query_representations['tfidf'],
|
|
341
|
+
self.tfidf_matrix
|
|
342
|
+
).flatten()
|
|
343
|
+
scores['tfidf'] = tfidf_similarities
|
|
344
|
+
else:
|
|
345
|
+
scores['tfidf'] = np.zeros(num_docs)
|
|
346
|
+
|
|
347
|
+
# LDA similarity scores
|
|
348
|
+
if 'lda' in query_representations and self.doc_topic_matrix is not None:
|
|
349
|
+
from sklearn.metrics.pairwise import cosine_similarity
|
|
350
|
+
query_topic_vec = query_representations['lda'].reshape(1, -1)
|
|
351
|
+
lda_similarities = cosine_similarity(query_topic_vec, self.doc_topic_matrix).flatten()
|
|
352
|
+
scores['lda'] = lda_similarities
|
|
353
|
+
else:
|
|
354
|
+
scores['lda'] = np.zeros(num_docs)
|
|
355
|
+
|
|
356
|
+
# BM25 scores
|
|
357
|
+
if 'bm25' in query_representations and self.bm25:
|
|
358
|
+
bm25_scores = np.array(self.bm25.get_scores(query_representations['bm25']))
|
|
359
|
+
scores['bm25'] = bm25_scores
|
|
360
|
+
else:
|
|
361
|
+
scores['bm25'] = np.zeros(num_docs)
|
|
362
|
+
|
|
363
|
+
return scores
|
|
364
|
+
|
|
365
|
+
def _ensemble_scoring(self, scores: Dict[str, np.ndarray]) -> np.ndarray:
|
|
366
|
+
"""Combine scores from different methods"""
|
|
367
|
+
# Normalize scores to [0, 1] range
|
|
368
|
+
normalized_scores = {}
|
|
369
|
+
for method, score_array in scores.items():
|
|
370
|
+
if len(score_array) > 0 and score_array.max() > 0:
|
|
371
|
+
normalized_scores[method] = score_array / score_array.max()
|
|
372
|
+
else:
|
|
373
|
+
normalized_scores[method] = score_array
|
|
374
|
+
|
|
375
|
+
# Weighted combination
|
|
376
|
+
ensemble_scores = np.zeros(len(self.documents))
|
|
377
|
+
for method, weight in self.weights.items():
|
|
378
|
+
if method in normalized_scores:
|
|
379
|
+
ensemble_scores += weight * normalized_scores[method]
|
|
380
|
+
|
|
381
|
+
return ensemble_scores
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
|
2
|
+
|
|
3
|
+
from brags.factories.baseclasses.baseembedding import BaseEmbedding
|
|
4
|
+
|
|
5
|
+
class HuggingFaceEmbedding(BaseEmbedding):
|
|
6
|
+
def __init__(self, config):
|
|
7
|
+
self.config = config
|
|
8
|
+
|
|
9
|
+
def create(self):
|
|
10
|
+
return HuggingFaceEmbeddings(model_name=self.config.model_name)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
2
|
+
|
|
3
|
+
from brags.factories.baseclasses.basellm import BaseLLM
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GoogleGenAILLM(BaseLLM):
|
|
7
|
+
def __init__(self, config):
|
|
8
|
+
self.config = config
|
|
9
|
+
|
|
10
|
+
def create(self):
|
|
11
|
+
return ChatGoogleGenerativeAI(
|
|
12
|
+
model=self.config.model_name,
|
|
13
|
+
temperature=self.config.temperature,
|
|
14
|
+
google_api_key=self.config.api_keys.get("gemini_api_key"),
|
|
15
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from langchain_community.llms import Ollama
|
|
2
|
+
|
|
3
|
+
from brags.config_parser.data_types import LLMConfig
|
|
4
|
+
from brags.factories.baseclasses.basellm import BaseLLM
|
|
5
|
+
|
|
6
|
+
class OllamaLLM(BaseLLM):
|
|
7
|
+
def __init__(self, config: LLMConfig):
|
|
8
|
+
self.config = config
|
|
9
|
+
|
|
10
|
+
def create(self):
|
|
11
|
+
return Ollama(
|
|
12
|
+
model=self.config.model_name,
|
|
13
|
+
base_url=self.config.ollama_host or "http://localhost:11434",
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from brags.config_parser.data_types import LLMConfig
|
|
2
|
+
from brags.factories.baseclasses.basellm import BaseLLM
|
|
3
|
+
from .implementations.googleGenAILLM import GoogleGenAILLM
|
|
4
|
+
from .implementations.ollamaLLM import OllamaLLM
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LLMFactory:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def create(config: LLMConfig) -> BaseLLM:
|
|
10
|
+
if config.provider == "gemini":
|
|
11
|
+
return GoogleGenAILLM(config)
|
|
12
|
+
elif config.provider == "ollama":
|
|
13
|
+
return OllamaLLM(config)
|
|
14
|
+
raise ValueError(f"Unsupported LLM provider: {config.provider}")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from langchain_community.vectorstores import Chroma
|
|
5
|
+
|
|
6
|
+
from ....config_parser.data_types import VectorStoreConfig
|
|
7
|
+
from ...baseclasses.basevectorstore import BaseVectorStore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChromaVectorStore(BaseVectorStore):
|
|
11
|
+
def __init__(self, config: VectorStoreConfig):
|
|
12
|
+
self.config = config
|
|
13
|
+
self.logger = logging.getLogger("Adora")
|
|
14
|
+
|
|
15
|
+
def create(self, embedder, documents=None, save_if_not_local=False):
|
|
16
|
+
persist_path = self.config.persist_path
|
|
17
|
+
os.makedirs(persist_path, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
if documents:
|
|
20
|
+
self.logger.info("ChromaVectorStore: Adding documents to vector store")
|
|
21
|
+
|
|
22
|
+
if os.path.exists(os.path.join(persist_path, "chroma.sqlite3")):
|
|
23
|
+
# Load existing store
|
|
24
|
+
store = Chroma(
|
|
25
|
+
persist_directory=persist_path,
|
|
26
|
+
embedding_function=embedder,
|
|
27
|
+
)
|
|
28
|
+
store.add_documents(documents)
|
|
29
|
+
self.logger.info("ChromaVectorStore: Added documents to existing DB")
|
|
30
|
+
else:
|
|
31
|
+
# Create a new store
|
|
32
|
+
store = Chroma.from_documents(
|
|
33
|
+
documents=documents,
|
|
34
|
+
embedding=embedder,
|
|
35
|
+
persist_directory=persist_path,
|
|
36
|
+
)
|
|
37
|
+
self.logger.info("ChromaVectorStore: Created new DB with provided documents")
|
|
38
|
+
|
|
39
|
+
if save_if_not_local:
|
|
40
|
+
self.logger.info("Saving Chroma DB to disk")
|
|
41
|
+
store.persist()
|
|
42
|
+
self.logger.info("ChromaVectorStore: Save complete")
|
|
43
|
+
|
|
44
|
+
return store
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
self.logger.info("ChromaVectorStore: Documents not provided, loading from disk")
|
|
48
|
+
return Chroma(
|
|
49
|
+
persist_directory=persist_path,
|
|
50
|
+
embedding_function=embedder,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def remove_by_path(self, embedder, path: str):
|
|
54
|
+
"""Remove all documents with metadata['source'] == path from the Chroma store."""
|
|
55
|
+
self.logger.info(f"ChromaVectorStore: Removing documents with source={path}")
|
|
56
|
+
|
|
57
|
+
store = Chroma(
|
|
58
|
+
persist_directory=self.config.persist_path,
|
|
59
|
+
embedding_function=embedder,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Delete by metadata
|
|
63
|
+
store.delete(where={"source": path})
|
|
64
|
+
store.persist()
|
|
65
|
+
|
|
66
|
+
self.logger.info(f"ChromaVectorStore: Removed documents with source={path}")
|
|
67
|
+
return store
|