rag-engine / src /rag_engine /pipeline.py
Jayan12321's picture
Upload folder using huggingface_hub
e43c82d verified
Raw History Blame Contribute Delete
5.87 kB
"""Complete end-to-end RAG Pipeline orchestrator.
Coordinates all 5 core stages of Retrieval-Augmented Generation:
1. Ingestion: Load raw text, PDFs, or demo articles into Document objects.
2. Chunking: Split documents into smaller, overlapping TextChunk passages.
3. Indexing: Index chunks into both Dense Vector Store and BM25 Keyword Search.
4. Hybrid Retrieval: Retrieve and rank chunks using Reciprocal Rank Fusion (RRF).
5. Generation: Augment prompt with retrieved chunks and generate answer with LLM.
"""
import time
from pathlib import Path
from config.settings import get_settings
from rag_engine.documents import Document, load_text_file, load_pdf_file, save_documents_jsonl, load_documents_jsonl
from rag_engine.chunking import chunk_documents
from rag_engine.vector_store import VectorStore
from rag_engine.bm25_search import BM25SearchIndex
from rag_engine.hybrid_search import HybridRetriever
from rag_engine.generator import generate_answer
from rag_engine.eval import evaluate_response
class RAGPipeline:
"""The central RAG engine coordinating ingestion, search, and generation."""
def __init__(self, settings=None):
self.settings = settings or get_settings()
self.documents = []
self.chunks = []
self.vector_store = VectorStore()
self.bm25_index = BM25SearchIndex()
self.retriever = HybridRetriever(
vector_store=self.vector_store,
bm25_index=self.bm25_index,
dense_weight=self.settings.dense_weight,
bm25_weight=self.settings.bm25_weight,
)
def add_document(self, text, title=None, doc_id=None, source="upload"):
"""Add a single raw text document and index it."""
doc = Document(text=text, title=title, doc_id=doc_id, source=source)
self.documents.append(doc)
new_chunks = chunk_documents(
[doc],
chunk_size=self.settings.chunk_size,
chunk_overlap=self.settings.chunk_overlap,
)
self.chunks.extend(new_chunks)
self.vector_store.add_chunks(new_chunks)
self.bm25_index.build(self.chunks)
save_documents_jsonl(self.documents, self.settings.documents_file)
return doc
def add_pdf(self, filepath):
"""Extract text from a PDF file and index it."""
doc = load_pdf_file(filepath)
return self.add_document(text=doc.text, title=doc.title, doc_id=doc.doc_id, source=doc.source)
def load_demo_corpus(self):
"""Load bundled educational demo articles."""
demo_dir = Path(__file__).resolve().parents[2] / "tests" / "fixtures" / "demo_corpus"
if not demo_dir.exists():
return []
loaded = []
for txt_file in sorted(demo_dir.glob("*.txt")):
doc = load_text_file(txt_file)
self.documents.append(doc)
loaded.append(doc)
if loaded:
self.chunks = chunk_documents(
self.documents,
chunk_size=self.settings.chunk_size,
chunk_overlap=self.settings.chunk_overlap,
)
self.vector_store.clear()
self.vector_store.add_chunks(self.chunks)
self.bm25_index.build(self.chunks)
save_documents_jsonl(self.documents, self.settings.documents_file)
return loaded
def query(self, question, top_k=None):
"""Execute the end-to-end RAG workflow for a user question.
Steps:
1. Query dense vector store and BM25 keyword index.
2. Merge results using Reciprocal Rank Fusion.
3. Format retrieved context into LLM prompt.
4. Synthesize final answer with source citations.
5. Evaluate faithfulness and relevancy.
Returns:
Dict containing:
- 'question': input question
- 'answer': generated answer
- 'sources': cited document titles
- 'retrieved_chunks': top ranked chunks
- 'dense_results': vector search results
- 'bm25_results': keyword search results
- 'evaluation': faithfulness & relevancy metrics
- 'latency_ms': elapsed execution time
"""
start_time = time.perf_counter()
top_k = top_k or self.settings.final_top_k
# Step 1: Hybrid Retrieval
retrieval_output = self.retriever.retrieve(
query=question,
top_k=top_k,
dense_k=self.settings.dense_top_k,
bm25_k=self.settings.bm25_top_k,
)
retrieved_chunks = retrieval_output["results"]
# Step 2: Generation
gen_output = generate_answer(
question=question,
retrieved_items=retrieved_chunks,
settings=self.settings,
)
# Step 3: Evaluation
evaluation = evaluate_response(
question=question,
answer=gen_output["answer"],
retrieved_items=retrieved_chunks,
)
elapsed_ms = round((time.perf_counter() - start_time) * 1000, 1)
return {
"question": question,
"answer": gen_output["answer"],
"sources": gen_output["sources"],
"used_llm": gen_output["used_llm"],
"retrieved_chunks": retrieved_chunks,
"dense_results": retrieval_output["dense_results"],
"bm25_results": retrieval_output["bm25_results"],
"evaluation": evaluation,
"latency_ms": elapsed_ms,
}
def get_stats(self):
"""Return summary statistics of the indexed knowledge base."""
return {
"document_count": len(self.documents),
"chunk_count": len(self.chunks),
"vector_store_size": len(self.vector_store),
"bm25_index_size": len(self.bm25_index),
}