Spaces:
Runtime error
Runtime error
| """ | |
| rag/scripts/run_evaluation.py | |
| ------------------------------ | |
| CLI entry point for Phase 3 evaluation. | |
| Stages: | |
| 1. Load frozen index (must exist β run build_index first). | |
| 2. Load or generate the 50-item eval dataset. | |
| 3. Run retrieval-only ablation (B0βB5) β no LLM calls, fast. | |
| 4. Optionally run generation evaluation on B2 (hybrid) and B0 (dense) | |
| using the configured LLM backend + Gemini faithfulness judge. | |
| 5. Print structured terminal report. | |
| 6. Save JSON report to data/eval/results_<timestamp>.json. | |
| Usage: | |
| python -m rag.scripts.run_evaluation | |
| python -m rag.scripts.run_evaluation --no-generation | |
| python -m rag.scripts.run_evaluation --eval-set data/eval/eval_set.json | |
| python -m rag.scripts.run_evaluation --configs B0,B2,B5 # subset of ablation | |
| """ | |
| import argparse | |
| import dataclasses | |
| import json | |
| import logging | |
| import os | |
| import sys | |
| import time | |
| from datetime import datetime | |
| from pathlib import Path | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s %(levelname)-8s %(message)s", | |
| datefmt="%H:%M:%S", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| def parse_args() -> argparse.Namespace: | |
| p = argparse.ArgumentParser( | |
| description="Run Phase 3 evaluation for the IndiaFinBench RAG pipeline." | |
| ) | |
| p.add_argument("--index-dir", type=Path, default=Path("rag/index")) | |
| p.add_argument("--data-dir", type=Path, default=Path("data/parsed")) | |
| p.add_argument("--eval-set", type=Path, default=Path("data/eval/eval_set.json"), | |
| help="Path to eval_set.json. Generated if missing and --generate-eval set.") | |
| p.add_argument("--output-dir", type=Path, default=Path("data/eval")) | |
| p.add_argument("--no-generation", action="store_true", | |
| help="Skip generation evaluation (retrieval metrics only).") | |
| p.add_argument("--generate-eval", action="store_true", | |
| help="Generate synthetic eval set via Gemini if eval_set.json missing.") | |
| p.add_argument("--n-synthetic", type=int, default=35, | |
| help="Number of synthetic QA pairs to generate.") | |
| p.add_argument("--configs", type=str, default=None, | |
| help="Comma-separated subset of ablation config IDs to run, " | |
| "e.g. B0_dense_only,B2_hybrid,B5_higher_k") | |
| p.add_argument("--gemini-key", type=str, default=None, | |
| help="Gemini API key (overrides GEMINI_API_KEY env var).") | |
| p.add_argument("--groq-key", type=str, default=None, | |
| help="Groq API key (overrides GROQ_API_KEY env var).") | |
| p.add_argument("--max-gen-items",type=int, default=None, | |
| help="Limit generation eval to first N items (useful for quick tests).") | |
| return p.parse_args() | |
| def main() -> None: | |
| # Force UTF-8 stdout so box-drawing chars in the terminal report work on Windows | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| args = parse_args() | |
| # Apply key overrides before any imports that might read them | |
| if args.gemini_key: | |
| os.environ["GEMINI_API_KEY"] = args.gemini_key | |
| if args.groq_key: | |
| os.environ["GROQ_API_KEY"] = args.groq_key | |
| # ββ Imports βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from rag.config import RAGConfig | |
| from rag.data_loader import DataLoader | |
| from rag.embeddings import BGEEmbedder | |
| from rag.evaluation import ( | |
| ABLATION_CONFIGS, | |
| FAITHFULNESS_JUDGE_MODEL, | |
| FAITHFULNESS_JUDGE_PROMPT_VERSION, | |
| evaluate_generation, | |
| load_or_generate_eval_set, | |
| print_terminal_report, | |
| run_ablation, | |
| save_report, | |
| ) | |
| from rag.bm25_index import BM25Index | |
| from rag.index import FAISSIndex | |
| from rag.pipeline import RAGPipeline | |
| from rag.preprocessing import TextPreprocessor | |
| from rag.chunking import RecursiveCharacterSplitter | |
| # ββ Load index ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not (args.index_dir / "faiss.index").exists(): | |
| logger.error( | |
| "Index not found at %s. Run: python -m rag.scripts.build_index first.", | |
| args.index_dir, | |
| ) | |
| sys.exit(1) | |
| cfg = RAGConfig(data_dir=args.data_dir, index_dir=args.index_dir) | |
| logger.info("Loading embedder: %s", cfg.embedding_model) | |
| t_emb = time.perf_counter() | |
| embedder = BGEEmbedder( | |
| model_name = cfg.embedding_model, | |
| device = cfg.embedding_device, | |
| batch_size = cfg.embedding_batch_size, | |
| ) | |
| logger.info(" Embedder loaded in %.1fs (dim=%d)", time.perf_counter() - t_emb, embedder.dim) | |
| logger.info("Loading FAISS + BM25 index from %s", args.index_dir) | |
| faiss_idx = FAISSIndex.load(args.index_dir, embedder.dim) | |
| bm25_idx = BM25Index.load(args.index_dir) | |
| logger.info(" Index: %d vectors, %d BM25 chunks", faiss_idx.size, bm25_idx.size) | |
| # ββ Load or generate eval set βββββββββββββββββββββββββββββββββββββββββββββ | |
| docs: list | None = None | |
| chunks: list | None = None | |
| if not args.eval_set.exists(): | |
| if not args.generate_eval: | |
| logger.error( | |
| "Eval set not found at %s. " | |
| "Run with --generate-eval to create it, or provide --eval-set path.", | |
| args.eval_set, | |
| ) | |
| sys.exit(1) | |
| logger.info("Generating synthetic eval set (%d items)β¦", args.n_synthetic) | |
| loader = DataLoader(cfg.data_dir) | |
| preprocessor = TextPreprocessor() | |
| splitter = RecursiveCharacterSplitter( | |
| target_chunk_chars=cfg.target_chunk_chars, | |
| overlap_chars=cfg.overlap_chars, | |
| min_chunk_chars=cfg.min_chunk_chars, | |
| ) | |
| docs = loader.load() | |
| for d in docs: | |
| d.raw_text = preprocessor.process(d.raw_text) | |
| chunks = [c for d in docs for c in splitter.split_document(d)] | |
| eval_items = load_or_generate_eval_set( | |
| path = args.eval_set, | |
| docs = docs, | |
| chunks = chunks, | |
| n_synthetic = args.n_synthetic, | |
| api_key = args.gemini_key, | |
| ) | |
| n_with_gt = sum(1 for i in eval_items if i.relevant_chunk_ids) | |
| logger.info( | |
| "Eval set: %d items total (%d with ground-truth chunk IDs, %d adversarial).", | |
| len(eval_items), n_with_gt, len(eval_items) - n_with_gt, | |
| ) | |
| # ββ Filter ablation configs βββββββββββββββββββββββββββββββββββββββββββββββ | |
| selected_configs = ABLATION_CONFIGS | |
| if args.configs: | |
| ids = {c.strip() for c in args.configs.split(",")} | |
| selected_configs = [c for c in ABLATION_CONFIGS if c["id"] in ids] | |
| if not selected_configs: | |
| logger.error("No matching configs found for: %s", args.configs) | |
| sys.exit(1) | |
| # ββ Stage 1: Retrieval ablation βββββββββββββββββββββββββββββββββββββββββββ | |
| logger.info("Stage 1: Retrieval-only ablation (%d configs)β¦", len(selected_configs)) | |
| ablation_results = run_ablation( | |
| base_faiss = faiss_idx, | |
| base_bm25 = bm25_idx, | |
| embedder = embedder, | |
| base_cfg = cfg, | |
| eval_items = eval_items, | |
| configs = selected_configs, | |
| ) | |
| # ββ Stage 2: Generation evaluation (optional) βββββββββββββββββββββββββββββ | |
| gen_results: dict = {} | |
| if not args.no_generation: | |
| gemini_key = args.gemini_key or os.environ.get("GEMINI_API_KEY") | |
| if not gemini_key: | |
| logger.warning( | |
| "GEMINI_API_KEY not set β skipping generation evaluation. " | |
| "Set it or pass --gemini-key to enable faithfulness scoring." | |
| ) | |
| else: | |
| import google.generativeai as genai # type: ignore[import] | |
| genai.configure(api_key=gemini_key) | |
| judge_model = genai.GenerativeModel( | |
| FAITHFULNESS_JUDGE_MODEL, | |
| generation_config={"temperature": 0.0, "max_output_tokens": 1024}, | |
| ) | |
| # Run generation eval on B2 (proposed) and B0 (dense baseline) | |
| for target_id in ("B2_hybrid", "B0_dense_only"): | |
| target_cfg = next( | |
| (c for c in selected_configs if c["id"] == target_id), None | |
| ) | |
| if target_cfg is None: | |
| continue | |
| logger.info("Stage 2: Generation eval for %sβ¦", target_id) | |
| # Wire up a full pipeline with the appropriate mode | |
| pipeline = RAGPipeline(config=cfg) | |
| pipeline.load_index() | |
| gm, gf = evaluate_generation( | |
| pipeline = pipeline, | |
| eval_items = eval_items, | |
| embedder = embedder, | |
| gemini_model= judge_model, | |
| mode = target_cfg["mode"], | |
| max_items = args.max_gen_items, | |
| ) | |
| gen_results[target_id] = (gm, gf) | |
| # ββ Print terminal report βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print_terminal_report(ablation_results, gen_results, eval_items) | |
| # ββ Save JSON report ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| report_path = args.output_dir / f"results_{timestamp}.json" | |
| config_snapshot = { | |
| "embedding_model": cfg.embedding_model, | |
| "target_chunk_chars": cfg.target_chunk_chars, | |
| "overlap_chars": cfg.overlap_chars, | |
| "top_k": cfg.top_k, | |
| "candidates": cfg.candidates, | |
| "rrf_k": cfg.rrf_k, | |
| "max_per_source": cfg.max_per_source, | |
| "llm_backend": cfg.llm_backend, | |
| "temperature": cfg.temperature, | |
| "index_dir": str(args.index_dir), | |
| "eval_items": len(eval_items), | |
| "items_with_gt": n_with_gt, | |
| "judge_model": FAITHFULNESS_JUDGE_MODEL, | |
| "judge_prompt_ver": FAITHFULNESS_JUDGE_PROMPT_VERSION, | |
| "timestamp": timestamp, | |
| } | |
| save_report(ablation_results, gen_results, config_snapshot, report_path) | |
| print(f"\n Report saved β {report_path}") | |
| print() | |
| if __name__ == "__main__": | |
| main() | |