Sakhi-AI / rag_engine.py
Prof-chaos-5
sfcs
874f8b6
Raw
History Blame Contribute Delete
3.74 kB
"""
RAG Engine for Sakhi.
Fetches top chunks from FAISS, deduplicates, and merges contiguous text blocks.
"""
import os
import pickle
import logging
from config import EMBED_MODEL, TOP_K_FETCH, TOP_K_MERGED, FAISS_INDEX_PATH, CHUNKS_PATH
logger = logging.getLogger(__name__)
class RAGEngine:
def __init__(self):
self.model = None
self.index = None
self.chunks = []
self._initialized = False
def initialize(self):
if self._initialized:
return
from build_index import ensure_index_exists
ensure_index_exists()
try:
import faiss
import torch
from sentence_transformers import SentenceTransformer
device = "cuda" if torch.cuda.is_available() else "cpu"
self.model = SentenceTransformer(
EMBED_MODEL,
device=device,
)
self.index = faiss.read_index(FAISS_INDEX_PATH)
with open(CHUNKS_PATH, "rb") as f:
self.chunks = pickle.load(f)
logger.info(
f"Loaded {len(self.chunks)} chunks."
)
self._initialized = True
except Exception as e:
logger.exception(e)
def retrieve(self, query: str) -> list[dict]:
self.initialize()
if not self.chunks or self.index is None or self.model is None:
return []
try:
import faiss
# Fetch wider net of chunks initially
query_embedding = self.model.encode([query], convert_to_numpy=True, normalize_embeddings=True)
scores, indices = self.index.search(query_embedding, min(TOP_K_FETCH, len(self.chunks)))
retrieved = []
for score, idx in zip(scores[0], indices[0]):
if 0 <= idx < len(self.chunks):
chunk = self.chunks[idx].copy()
chunk["score"] = float(score)
retrieved.append(chunk)
# Intelligent Merging: Sort by source, then by chunk_idx
# This ensures adjacent paragraphs in the PDF are merged into one readable block
unique_chunks = {c['chunk_idx']: c for c in retrieved}.values()
sorted_chunks = sorted(unique_chunks, key=lambda x: (x.get('source', ''), x.get('chunk_idx', 0)))
merged_results = []
curr = None
for c in sorted_chunks:
if not curr:
curr = c.copy()
else:
# If chunks are from the same document and are adjacent/overlapping
if curr['source'] == c['source'] and (c['chunk_idx'] - curr['chunk_idx']) <= 2:
curr['text'] += "\n" + c['text']
curr['chunk_idx'] = c['chunk_idx'] # Move the end pointer
else:
merged_results.append(curr)
curr = c.copy()
if curr:
merged_results.append(curr)
# Return top merged blocks
return sorted(merged_results, key=lambda x: x.get('score', 0), reverse=True)[:TOP_K_MERGED]
except Exception as e:
logger.error(f"FAISS retrieval error: {e}")
return []
def format_context(self, chunks: list[dict]) -> str:
if not chunks:
return "No specific PDF context found. Please use general knowledge to explain."
context_parts = []
for i, chunk in enumerate(chunks, 1):
source = chunk.get("source", "Unknown")
context_parts.append(f"--- Document: {source} ---\n{chunk.get('text', '')}\n")
return "\n".join(context_parts)