ragas_eval.py

script

← Back to skill

Content hash: d08888d164bf8e190d7170ecfd3203289e672ee6be0cc941d23394cdc9273335
#!/usr/bin/env python3
"""RAG evaluation script using RAGAS metrics.

Measures: faithfulness, answer relevancy, context precision, context recall.
Run this against your RAG pipeline's output to quantify where it fails.

Usage: python ragas_eval.py <eval_samples.jsonl>

Requires: pip install ragas
Set OPENAI_API_KEY in env for LLM-as-judge evaluations.
"""
from __future__ import annotations

import json
import sys
from dataclasses import dataclass


@dataclass
class EvalSample:
    question: str
    answer: str
    contexts: list[str]
    reference: str = ""


def load_samples(path: str) -> list[EvalSample]:
    """Load eval set from JSONL: {question, answer, contexts, [reference]}."""
    samples = []
    with open(path) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            obj = json.loads(line)
            samples.append(EvalSample(
                question=obj["question"],
                answer=obj["answer"],
                contexts=obj.get("contexts", []),
                reference=obj.get("reference", ""),
            ))
    return samples


def evaluate_ragas(samples: list[EvalSample]) -> dict[str, float]:
    """Run RAGAS metrics. Requires OPENAI_API_KEY."""
    try:
        from ragas import EvaluationDataset, SingleTurnSample, evaluate
        from ragas.metrics import (
            faithfulness, answer_relevancy,
            context_precision, context_recall,
        )
    except ImportError:
        print("Install ragas: pip install ragas")
        sys.exit(1)

    ragas_samples = [
        SingleTurnSample(
            user_input=s.question,
            response=s.answer,
            retrieved_contexts=s.contexts,
            reference=s.reference,
        )
        for s in samples
    ]
    dataset = EvaluationDataset(samples=ragas_samples)
    metrics = [faithfulness, answer_relevancy, context_recall, context_precision]
    result = evaluate(dataset=dataset, metrics=metrics)

    return {name: round(float(val), 4) for name, val in result.items()}


def main() -> None:
    if len(sys.argv) < 2:
        print("Usage: python ragas_eval.py <eval_samples.jsonl>")
        print("\nJSONL format per line:")
        print('  {"question":"...","answer":"...","contexts":[...],"reference":"..."}')
        sys.exit(1)

    samples = load_samples(sys.argv[1])
    print(f"Loaded {len(samples)} samples")

    if len(samples) == 0:
        print("No samples to evaluate.")
        return

    scores = evaluate_ragas(samples)

    print("\n=== RAGAS Metrics ===")
    for metric, value in scores.items():
        bar = "#" * int(value * 20)
        print(f"  {metric:25s}: {value:.4f}  {bar}")

    # Diagnostic hints
    print("\n=== Diagnostic ===")
    issues = []
    faithfulness_val = scores.get("faithfulness", 0)
    context_recall_val = scores.get("context_recall", 0)
    answer_rel = scores.get("answer_relevancy", 0)
    context_prec = scores.get("context_precision", 0)

    if faithfulness_val < 0.7:
        issues.append("Low faithfulness -> generator/prompt problem: model fabricating")
    if answer_rel < 0.7:
        issues.append("Low answer relevance -> model padding or evading the question")
    if context_recall_val < 0.7:
        issues.append("Low context recall -> retriever missing needed chunks")
    if context_prec < 0.7:
        issues.append("Low context precision -> too much irrelevant retrieved text")

    if issues:
        for msg in issues:
            print(f"  ! {msg}")
    else:
        print("  All metrics above 0.7 -- RAG pipeline healthy")

    # Threshold check (exit code for CI)
    threshold = 0.6
    failing = [m for m, v in scores.items() if v < threshold]
    if failing:
        print(f"\nFAIL: {failing} below threshold {threshold}")
        sys.exit(1)


if __name__ == "__main__":
    main()