Spaces:
Sleeping
Sleeping
| """ | |
| src/generation/rag_pipeline.py | |
| -------------------------------- | |
| Scientific RAG pipeline with: | |
| - grounded answer generation | |
| - source citation | |
| - hallucination control (out-of-corpus refusal) | |
| - optional reranking stage | |
| Compatible with lab_s6 rag_pipeline.py API. | |
| """ | |
| import time | |
| from typing import Optional | |
| # --------------------------------------------------------------------------- | |
| # Prompt templates | |
| # --------------------------------------------------------------------------- | |
| SCIENTIFIC_RAG_PROMPT = """\ | |
| You are an expert scientific assistant. Answer ONLY from the provided context. | |
| If the context does not contain the answer, reply EXACTLY: | |
| "I cannot answer this question from the available scientific corpus." | |
| Do NOT hallucinate or add information beyond the context. | |
| ### Context | |
| {context} | |
| ### Question | |
| {question} | |
| ### Answer (cite papers in [Title, Year] format when possible) | |
| """ | |
| NO_RAG_PROMPT = """\ | |
| You are a knowledgeable AI assistant. Answer the following question to the best of your ability. | |
| ### Question | |
| {question} | |
| ### Answer | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Citation helper | |
| # --------------------------------------------------------------------------- | |
| def format_sources(metadatas: list[dict], distances: list[float], rerank_scores: list[float] = None) -> list[dict]: | |
| """Format retrieved chunk metadata into source cards.""" | |
| sources = [] | |
| for i, (meta, dist) in enumerate(zip(metadatas, distances)): | |
| score_label = f"{rerank_scores[i]:.3f}" if rerank_scores else f"{1 - dist:.3f}" | |
| sources.append({ | |
| "rank": i + 1, | |
| "paper_id": meta.get("paper_id", ""), | |
| "title": meta.get("title", "Unknown"), | |
| "authors": meta.get("authors_str", ""), | |
| "year": meta.get("year", ""), | |
| "section": meta.get("section", ""), | |
| "score": score_label, | |
| "chunk_preview": "", # filled below | |
| }) | |
| return sources | |
| # --------------------------------------------------------------------------- | |
| # Core pipeline functions | |
| # --------------------------------------------------------------------------- | |
| def answer_without_rag(question: str, llm, max_new_tokens: int = 400) -> str: | |
| """Baseline: direct LLM generation with no retrieval context.""" | |
| prompt = NO_RAG_PROMPT.format(question=question) | |
| return llm.generate(prompt, max_new_tokens=max_new_tokens) | |
| def answer_with_rag( | |
| question: str, | |
| embedder, | |
| store, | |
| llm, | |
| k: int = 5, | |
| max_new_tokens: int = 400, | |
| reranker=None, | |
| reranker_k: int = 20, | |
| metadata_filter: Optional[dict] = None, | |
| ) -> dict: | |
| """ | |
| Full scientific RAG pipeline. | |
| Parameters | |
| ---------- | |
| question : user question | |
| embedder : Embedder instance | |
| store : ScientificChromaStore instance | |
| llm : LLM backend (generate method) | |
| k : final number of chunks in the context | |
| max_new_tokens: generation budget | |
| reranker : CrossEncoderReranker instance (optional) | |
| reranker_k : first-stage retrieval size when reranking | |
| metadata_filter: Chroma 'where' dict for filtering | |
| Returns | |
| ------- | |
| dict with keys: | |
| question, retrieved, metadatas, distances, rerank_scores, | |
| sources, prompt, answer, latency | |
| """ | |
| timings = {} | |
| # Stage 1: Embed question | |
| t0 = time.time() | |
| q_vec = embedder.encode_one(question) | |
| timings["embed_s"] = round(time.time() - t0, 3) | |
| # Stage 2: Dense retrieval | |
| t0 = time.time() | |
| first_k = reranker_k if reranker else k | |
| res = store.query(q_vec, k=first_k, where=metadata_filter) | |
| timings["retrieve_s"] = round(time.time() - t0, 3) | |
| rerank_scores = None | |
| # Stage 3 (optional): Cross-encoder reranking | |
| if reranker and res["documents"]: | |
| t0 = time.time() | |
| ranked = reranker.rerank(question, res, top_k=k) | |
| timings["rerank_s"] = round(time.time() - t0, 3) | |
| from src.retrieval.reranker import ranked_to_result | |
| res = ranked_to_result(ranked) | |
| rerank_scores = res.get("rerank_scores") | |
| else: | |
| # Keep only top-k from dense retrieval | |
| for key in ["documents", "metadatas", "distances"]: | |
| res[key] = res[key][:k] | |
| # Stage 4: Build grounded prompt with context | |
| context_parts = [] | |
| for i, (doc, meta) in enumerate(zip(res["documents"], res["metadatas"])): | |
| citation = f"[{meta.get('title', 'Unknown')} ({meta.get('year', '?')})]" | |
| context_parts.append(f"Source {i+1} {citation}:\n{doc}") | |
| context = "\n\n---\n\n".join(context_parts) | |
| prompt = SCIENTIFIC_RAG_PROMPT.format(context=context, question=question) | |
| # Stage 5: Generate | |
| t0 = time.time() | |
| answer = llm.generate(prompt, max_new_tokens=max_new_tokens) | |
| timings["generate_s"] = round(time.time() - t0, 3) | |
| timings["total_s"] = round(sum(timings.values()), 3) | |
| # Format source cards | |
| sources = format_sources(res["metadatas"], res["distances"], rerank_scores) | |
| for i, src in enumerate(sources): | |
| src["chunk_preview"] = res["documents"][i][:200] + "..." | |
| return { | |
| "question": question, | |
| "retrieved": res["documents"], | |
| "metadatas": res["metadatas"], | |
| "distances": res["distances"], | |
| "rerank_scores": rerank_scores, | |
| "sources": sources, | |
| "prompt": prompt, | |
| "answer": answer, | |
| "latency": timings, | |
| "used_reranker": reranker is not None, | |
| } | |
| def run_comparison( | |
| question: str, | |
| embedder, | |
| store, | |
| llm, | |
| reranker=None, | |
| k: int = 5, | |
| ) -> dict: | |
| """ | |
| Run all three experiment conditions for one question: | |
| Exp1: No RAG, Exp2: RAG, Exp3: RAG + Reranker | |
| Returns dict with keys: no_rag, rag, rag_reranker | |
| """ | |
| result = { | |
| "question": question, | |
| "no_rag": answer_without_rag(question, llm), | |
| "rag": answer_with_rag(question, embedder, store, llm, k=k), | |
| } | |
| if reranker: | |
| result["rag_reranker"] = answer_with_rag( | |
| question, embedder, store, llm, k=k, reranker=reranker | |
| ) | |
| return result | |