|
1 | 1 | # HRBuddy |
2 | 2 | # core/rag_engine.py |
3 | 3 |
|
4 | | -# Required Libraries |
5 | 4 | from langchain_community.vectorstores import Chroma |
| 5 | +from langchain_community.retrievers import BM25Retriever |
6 | 6 | from langchain_ollama import OllamaEmbeddings |
| 7 | +from langchain_core.documents import Document |
7 | 8 | import ollama |
8 | 9 | from core.logger import log |
9 | 10 | from core.config_loader import cfg |
10 | 11 |
|
11 | | -# Main Class |
| 12 | + |
| 13 | +def _prepare_bm25_chunks(chunks): |
| 14 | + bm25_chunks = [] |
| 15 | + for doc in chunks: |
| 16 | + heading = doc.metadata.get("heading", "") |
| 17 | + text = f"{heading}\n{doc.page_content}" if heading else doc.page_content |
| 18 | + bm25_chunks.append(Document(page_content=text, metadata=doc.metadata)) |
| 19 | + return bm25_chunks |
| 20 | + |
| 21 | + |
| 22 | +class _HybridRetriever: |
| 23 | + def __init__(self, retrievers, weights=None, top_k=6): |
| 24 | + self.retrievers = retrievers |
| 25 | + self.weights = weights if weights else [1.0 / len(retrievers)] * len(retrievers) |
| 26 | + self.top_k = top_k |
| 27 | + |
| 28 | + def invoke(self, query): |
| 29 | + all_docs = [] |
| 30 | + for retriever, weight in zip(self.retrievers, self.weights): |
| 31 | + docs = retriever.invoke(query) |
| 32 | + for rank, doc in enumerate(docs): |
| 33 | + doc.metadata["_score"] = doc.metadata.get("_score", 0) + weight / (rank + 1) |
| 34 | + all_docs.append(doc) |
| 35 | + |
| 36 | + seen = set() |
| 37 | + unique = [] |
| 38 | + for doc in all_docs: |
| 39 | + key = doc.page_content[:200] |
| 40 | + if key not in seen: |
| 41 | + seen.add(key) |
| 42 | + unique.append(doc) |
| 43 | + |
| 44 | + unique.sort(key=lambda d: d.metadata.get("_score", 0), reverse=True) |
| 45 | + return unique[:self.top_k] |
| 46 | + |
| 47 | + |
12 | 48 | class HRBuddyEngine: |
13 | 49 | def __init__(self, chunks): |
14 | | - """ |
15 | | - Initialization parameters are pulled entirely from cfg (JSON). |
16 | | - """ |
| 50 | + hs = cfg.get("hybrid_search", {}) |
| 51 | + top_k = hs.get("top_k", 6) |
| 52 | + fetch_k = hs.get("fetch_k", top_k * 3) |
| 53 | + |
17 | 54 | log.info(f"[RAG] Initializing with model: {cfg['vector_store']['embedding_model']}") |
18 | | - |
19 | | - # Embeddings |
| 55 | + |
20 | 56 | self.embeddings = OllamaEmbeddings( |
21 | 57 | model=cfg["vector_store"]["embedding_model"], |
22 | 58 | base_url=cfg.get("ollama_base_url", "http://localhost:11434") |
23 | 59 | ) |
24 | | - |
25 | | - # Vector DB |
| 60 | + |
26 | 61 | self.vectorstore = Chroma.from_documents( |
27 | | - documents=chunks, |
| 62 | + documents=chunks, |
28 | 63 | embedding=self.embeddings |
29 | 64 | ) |
30 | | - |
31 | | - # k (number of documents) |
32 | | - search_k = cfg["vector_store"].get("search_top_k", 2) |
33 | | - self.retriever = self.vectorstore.as_retriever(search_kwargs={"k": search_k}) |
34 | | - |
35 | | - log.info(f"[RAG] Search depth set to k={search_k}") |
36 | | - |
37 | | - # Generate Response |
| 65 | + semantic_retriever = self.vectorstore.as_retriever( |
| 66 | + search_type="mmr", |
| 67 | + search_kwargs={"k": top_k, "fetch_k": fetch_k} |
| 68 | + ) |
| 69 | + |
| 70 | + bm25_chunks = _prepare_bm25_chunks(chunks) |
| 71 | + bm25_retriever = BM25Retriever.from_documents( |
| 72 | + documents=bm25_chunks, k=top_k, preprocess_func=str.lower |
| 73 | + ) |
| 74 | + |
| 75 | + if hs.get("enabled", True): |
| 76 | + semantic_w = hs.get("semantic_weight", 0.7) |
| 77 | + bm25_w = hs.get("bm25_weight", 0.3) |
| 78 | + log.info( |
| 79 | + f"[RAG] Hybrid search enabled " |
| 80 | + f"(semantic={semantic_w}, bm25={bm25_w}, top_k={top_k})" |
| 81 | + ) |
| 82 | + self.retriever = _HybridRetriever( |
| 83 | + retrievers=[semantic_retriever, bm25_retriever], |
| 84 | + weights=[semantic_w, bm25_w], |
| 85 | + top_k=top_k |
| 86 | + ) |
| 87 | + else: |
| 88 | + log.info("[RAG] Hybrid search disabled, falling back to semantic only") |
| 89 | + self.retriever = semantic_retriever |
| 90 | + |
38 | 91 | def generate_response(self, user_input, history, session_id): |
39 | 92 | docs = self.retriever.invoke(user_input) |
40 | 93 | context_block = "\n\n".join([doc.page_content for doc in docs]) |
|
0 commit comments