IndiaFinBench / rag /pipeline.py
Rajveer Singh Pall
Deploy IndiaFinBench research site
8f41246
Raw
History Blame Contribute Delete
6.79 kB
"""
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 ──────────────────────────────────────────────────────────
@property
def index_ready(self) -> bool:
return self._retriever is not None
@property
def index_path(self) -> Path:
return self.cfg.index_dir