GYOM15
Deploy the RAG comparison app
45d0949
Raw
History Blame Contribute Delete
4.67 kB
"""RAGAS evaluation and the stack benchmark loop.
The heavy imports (ragas, datasets) are done **on demand**: importing this
module stays lightweight even without the `[eval]` extra installed. It is only
required when RAGAS is actually called.
"""
def evaluate_rag(
questions: list[str],
answers: list[str],
contexts: list[list[str]],
ground_truths: list[str],
) -> dict:
"""RAGAS metrics (faithfulness, answer_relevancy, context_precision, context_recall + per_question).
OpenAI judge by default -> requires OPENAI_API_KEY. Raises ValueError if the lists differ in length.
"""
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
answer_relevancy,
context_precision,
context_recall,
faithfulness,
)
n = len(questions)
if not (n == len(answers) == len(contexts) == len(ground_truths)):
raise ValueError(
f"All input lists must have the same length. Got "
f"questions={len(questions)}, answers={len(answers)}, "
f"contexts={len(contexts)}, ground_truths={len(ground_truths)}."
)
eval_dataset = Dataset.from_dict({
"question": questions,
"answer": answers,
"contexts": contexts,
"ground_truth": ground_truths,
})
metrics = [faithfulness, answer_relevancy, context_precision, context_recall]
result = evaluate(eval_dataset, metrics=metrics)
summary = {
"faithfulness": float(result["faithfulness"]),
"answer_relevancy": float(result["answer_relevancy"]),
"context_precision": float(result["context_precision"]),
"context_recall": float(result["context_recall"]),
}
if hasattr(result, "to_pandas"):
summary["per_question"] = result.to_pandas().to_dict(orient="records")
else:
summary["per_question"] = []
return summary
_RAGAS_METRICS = ("faithfulness", "answer_relevancy", "context_precision", "context_recall")
def _mean_quality(per_question: list[dict], indices: list[int]) -> dict:
"""Average of the RAGAS metrics over a subset of questions (by index)."""
summary = {}
for metric in _RAGAS_METRICS:
values = [
float(per_question[i][metric])
for i in indices
if isinstance(per_question[i].get(metric), (int, float))
and per_question[i][metric] == per_question[i][metric] # discards NaN
]
if values:
summary[metric] = round(sum(values) / len(values), 4)
return summary
def evaluate_stacks(
stacks: dict,
questions: list[str],
ground_truths: list[str],
k: int = 5,
types: list[str] | None = None,
) -> dict[str, dict]:
"""Evaluate each stack (generation + latencies + RAGAS), overall and by `types` if provided.
Returns {stack_name: metrics}; if RAGAS fails (missing key...), only the latencies.
"""
n = len(questions)
results: dict[str, dict] = {}
for name, rag in stacks.items():
answers, contexts, latencies = [], [], []
retrieval = generation = total = 0.0
for question in questions:
r = rag.query(question, k=k)
answers.append(r["answer"])
contexts.append([c["text"] for c in r["contexts"]])
latencies.append(r["latency_ms"])
retrieval += r["retrieval_ms"]
generation += r["generation_ms"]
total += r["latency_ms"]
metrics: dict = {}
per_question: list[dict] = []
try:
full = evaluate_rag(questions, answers, contexts, ground_truths)
per_question = full.pop("per_question", []) or []
metrics = full
except Exception:
pass # RAGAS optional: without a judge/key, we keep just the latencies
metrics["avg_retrieval_ms"] = round(retrieval / n, 2)
metrics["avg_generation_ms"] = round(generation / n, 2)
metrics["avg_latency_ms"] = round(total / n, 2)
if types:
valid = per_question if len(per_question) == n else []
by_type: dict[str, dict] = {}
for t in dict.fromkeys(types): # unique categories, order preserved
idx = [i for i, tt in enumerate(types) if tt == t]
entry = {
"n": len(idx),
"avg_latency_ms": round(sum(latencies[i] for i in idx) / len(idx), 2),
}
if valid:
entry.update(_mean_quality(valid, idx))
by_type[t] = entry
metrics["by_type"] = by_type
results[name] = metrics
return results