Spaces:
Runtime error
Runtime error
| """ | |
| rag/pipeline.py | |
| --------------- | |
| RAGPipeline: the top-level orchestrator that wires ingestion, indexing, | |
| retrieval, and generation into a single callable interface. | |
| Public API (intentionally minimal, mirrors demo/rag/rag.py): | |
| pipe = RAGPipeline() | |
| pipe.build_index() # run once; serialises to rag/index/ | |
| pipe.load_index() # fast path on subsequent starts | |
| result = pipe.ask("What are the KYC norms under SEBI?") | |
| # result: {"answer": str, "sources": list[dict]} | {"error": str} | |
| The demo integration in demo/rag/rag.py is a thin shim over this module. | |
| """ | |
| import logging | |
| from pathlib import Path | |
| from rag.bm25_index import BM25Index | |
| from rag.chunking import RecursiveCharacterSplitter | |
| from rag.config import RAGConfig | |
| from rag.data_loader import DataLoader | |
| from rag.embeddings import BGEEmbedder | |
| from rag.generator import LLMGenerator | |
| from rag.index import FAISSIndex | |
| from rag.models import RetrievalResult | |
| from rag.preprocessing import TextPreprocessor | |
| from rag.retriever import HybridRetriever | |
| logger = logging.getLogger(__name__) | |
| class RAGPipeline: | |
| def __init__(self, config: RAGConfig | None = None) -> None: | |
| self.cfg = config or RAGConfig() | |
| self._embedder = BGEEmbedder( | |
| model_name = self.cfg.embedding_model, | |
| device = self.cfg.embedding_device, | |
| batch_size = self.cfg.embedding_batch_size, | |
| ) | |
| self._generator = None | |
| if getattr(self.cfg, "enable_generation", True): | |
| self._generator = LLMGenerator( | |
| backend = self.cfg.llm_backend, | |
| max_tokens = self.cfg.max_tokens, | |
| temperature = self.cfg.temperature, | |
| ) | |
| self._retriever: HybridRetriever | None = None | |
| # ββ Index build (offline) βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_index(self) -> None: | |
| """ | |
| Full ingestion pipeline: load β preprocess β chunk β embed β index. | |
| Idempotent: safe to re-run; overwrites rag/index/ on disk. | |
| Typical runtime on CPU: 2β4 minutes for the 192-document corpus. | |
| """ | |
| loader = DataLoader(self.cfg.data_dir) | |
| preprocessor = TextPreprocessor() | |
| splitter = RecursiveCharacterSplitter( | |
| target_chunk_chars = self.cfg.target_chunk_chars, | |
| overlap_chars = self.cfg.overlap_chars, | |
| min_chunk_chars = self.cfg.min_chunk_chars, | |
| ) | |
| docs = loader.load() | |
| logger.info("Loaded %d documents", len(docs)) | |
| for doc in docs: | |
| doc.raw_text = preprocessor.process(doc.raw_text) | |
| all_chunks = [] | |
| for doc in docs: | |
| all_chunks.extend(splitter.split_document(doc)) | |
| logger.info("Created %d chunks", len(all_chunks)) | |
| texts = [c.text for c in all_chunks] | |
| embeddings = self._embedder.encode_corpus(texts) | |
| faiss_idx = FAISSIndex(self._embedder.dim) | |
| faiss_idx.build(embeddings, all_chunks) | |
| faiss_idx.save(self.cfg.index_dir) | |
| bm25_idx = BM25Index() | |
| bm25_idx.build(all_chunks) | |
| bm25_idx.save(self.cfg.index_dir) | |
| self._wire_retriever(faiss_idx, bm25_idx) | |
| logger.info( | |
| "Index built: %d vectors in FAISS, %d chunks in BM25. Saved to %s.", | |
| faiss_idx.size, bm25_idx.size, self.cfg.index_dir, | |
| ) | |
| # ββ Index load (online startup) βββββββββββββββββββββββββββββββββββββββββββ | |
| def load_index(self) -> None: | |
| faiss_idx = FAISSIndex.load(self.cfg.index_dir, self._embedder.dim) | |
| bm25_idx = BM25Index.load(self.cfg.index_dir) | |
| self._wire_retriever(faiss_idx, bm25_idx) | |
| logger.info( | |
| "Index loaded: %d vectors (%s).", | |
| faiss_idx.size, self.cfg.index_dir, | |
| ) | |
| def _wire_retriever(self, faiss_idx: FAISSIndex, bm25_idx: BM25Index) -> None: | |
| self._retriever = HybridRetriever( | |
| faiss_index = faiss_idx, | |
| bm25_index = bm25_idx, | |
| embedder = self._embedder, | |
| top_k = self.cfg.top_k, | |
| candidates = self.cfg.candidates, | |
| rrf_k = self.cfg.rrf_k, | |
| max_per_source = self.cfg.max_per_source, | |
| ) | |
| # ββ Query (online) ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def ask(self, query: str, mode: str = "hybrid") -> dict: | |
| """ | |
| Full RAG pipeline: retrieve β generate. | |
| Args: | |
| query: Natural language question. | |
| mode: "hybrid" | "dense" | "bm25" β retriever mode for ablation. | |
| Returns: | |
| {"answer": str, "sources": list[dict]} on success | |
| {"error": str} on failure | |
| """ | |
| if not query or not query.strip(): | |
| return {"error": "Empty query."} | |
| if self._retriever is None: | |
| return {"error": "Index not loaded. Call build_index() or load_index() first."} | |
| try: | |
| results: list[RetrievalResult] = self._retriever.retrieve( | |
| query.strip(), mode=mode | |
| ) | |
| if self._generator is None: | |
| return {"error": "Generation disabled in current configuration."} | |
| answer = self._generator.generate(query.strip(), results) | |
| sources = [ | |
| { | |
| "chunk_id": r.chunk.chunk_id, | |
| "doc_id": r.chunk.doc_id, | |
| "title": r.chunk.title, | |
| "source": r.chunk.source, | |
| "text": r.chunk.text, | |
| "rrf_score": round(r.rrf_score, 6), | |
| "dense_score": round(r.dense_score, 6), | |
| "bm25_score": round(r.bm25_score, 4), | |
| } | |
| for r in results | |
| ] | |
| return {"answer": answer, "sources": sources} | |
| except Exception as exc: # noqa: BLE001 | |
| logger.exception("RAG pipeline error for query: %r", query) | |
| return {"error": f"RAG pipeline error: {exc!s}"[:400]} | |
| # ββ Index status ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index_ready(self) -> bool: | |
| return self._retriever is not None | |
| def index_path(self) -> Path: | |
| return self.cfg.index_dir | |