Spaces:
Running
Running
| """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 | |