from __future__ import annotations import json import logging import os import sys import time from dataclasses import dataclass, field from typing import Any logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s") logger = logging.getLogger(__name__) @dataclass class EvalSample: question: str ground_truth: str answer: str = "" contexts: list[str] = field(default_factory=list) confidence: float = 0.0 latency_ms: float = 0.0 error: str | None = None @dataclass class EvalResult: samples: list[EvalSample] metrics: dict[str, float] = field(default_factory=dict) timestamp: str = "" def load_test_set(path: str | None = None) -> list[dict]: if path and os.path.exists(path): with open(path) as f: return json.load(f) return [ {"question": "What is the main topic of the first document?", "ground_truth": ""}, {"question": "What technical skills are mentioned?", "ground_truth": ""}, {"question": "What experience does the candidate have?", "ground_truth": ""}, ] def run_evaluation( test_set: list[dict], index: Any, output_path: str = "evaluation_results.json", ) -> EvalResult: from rag import answer samples: list[EvalSample] = [] total = len(test_set) for i, item in enumerate(test_set): question = item["question"] ground_truth = item.get("ground_truth", "") logger.info("Evaluating [%d/%d]: %s", i + 1, total, question[:60]) sample = EvalSample(question=question, ground_truth=ground_truth) start = time.monotonic() try: result = answer(question, index) sample.answer = result.get("answer", "") sample.confidence = result.get("confidence", 0.0) raw_chunks = result.get("retrieved_chunks", []) sample.contexts = [c.get("text", "") for c in raw_chunks if c.get("text")] except Exception as e: logger.error("Eval failed for '%s': %s", question[:40], e) sample.error = str(e) sample.latency_ms = round((time.monotonic() - start) * 1000, 1) samples.append(sample) result = _compute_ragas_metrics(samples) result.timestamp = time.strftime("%Y-%m-%dT%H:%M:%S") _save_results(result, output_path) return result def _compute_ragas_metrics(samples: list[EvalSample]) -> EvalResult: completed = [s for s in samples if s.answer and not s.error] if not completed: logger.warning("No successful samples to evaluate") return EvalResult(samples=samples, metrics={"error_rate": 1.0}) try: from datasets import Dataset from langchain_openai import ChatOpenAI from ragas import evaluate from ragas.llms.base import LangchainLLMWrapper from ragas.metrics import ( answer_relevancy, context_precision, context_recall, faithfulness, ) from rag.config import settings llm = ChatOpenAI( model=settings.LLM_MODEL, openai_api_key=settings.OPENROUTER_API_KEY, openai_api_base=settings.LLM_BASE_URL, temperature=0, ) ragas_llm = LangchainLLMWrapper(llm) for metric in [faithfulness, answer_relevancy, context_precision, context_recall]: if hasattr(metric, "llm"): metric.llm = ragas_llm data = { "question": [s.question for s in completed], "answer": [s.answer for s in completed], "contexts": [s.contexts for s in completed], "ground_truth": [s.ground_truth or s.answer for s in completed], } dataset = Dataset.from_dict(data) logger.info("Running RAGAS metrics on %d samples...", len(completed)) ragas_result = evaluate( dataset, metrics=[faithfulness, answer_relevancy, context_precision, context_recall], llm=ragas_llm, raise_exceptions=False, ) metrics_map = { "faithfulness": faithfulness.name, "answer_relevancy": answer_relevancy.name, "context_precision": context_precision.name, "context_recall": context_recall.name, } metrics = {} for key, metric_name in metrics_map.items(): try: scores = ragas_result[metric_name] metrics[key] = _safe_mean(scores) except Exception: metrics[key] = 0.0 metrics["error_rate"] = round(1 - len(completed) / len(samples), 3) metrics["num_samples"] = len(completed) metrics["total_questions"] = len(samples) logger.info("RAGAS results: %s", metrics) except Exception as e: logger.error("RAGAS computation failed: %s", e) metrics = { "faithfulness": 0.0, "answer_relevancy": 0.0, "context_precision": 0.0, "context_recall": 0.0, "error_rate": round(1 - len(completed) / len(samples), 3), "num_samples": len(completed), "total_questions": len(samples), "ragas_error": str(e), } return EvalResult(samples=samples, metrics=metrics) def _safe_mean(values: list[float]) -> float: if not values: return 0.0 import math filtered = [v for v in values if v is not None and not (isinstance(v, float) and math.isnan(v))] if not filtered: return 0.0 return round(sum(filtered) / len(filtered), 4) def _save_results(result: EvalResult, path: str) -> None: output = { "timestamp": result.timestamp, "metrics": result.metrics, "samples": [ { "question": s.question, "ground_truth": s.ground_truth, "answer": s.answer[:300] if s.answer else "", "confidence": s.confidence, "latency_ms": s.latency_ms, "error": s.error, } for s in result.samples ], } with open(path, "w") as f: json.dump(output, f, indent=2) logger.info("Results saved to %s", path) if __name__ == "__main__": test_path = os.environ.get("EVAL_TEST_SET") test_set = load_test_set(test_path) from rag import load_index, index_exists if not index_exists(): logger.error("No index found. Build the index first.") sys.exit(1) index = load_index() result = run_evaluation(test_set, index) print("\n=== EVALUATION RESULTS ===") for name, val in result.metrics.items(): print(f" {name}: {val}") print(f"\nResults saved to evaluation_results.json")