Spaces:
Sleeping
Sleeping
Vineetiitg commited on
Commit ·
e7ea02e
1
Parent(s): b9537ed
test: add golden RAG evaluation benchmark
Browse files- app/tests/eval_rag.py +101 -0
- datasets/golden_qa.csv +3 -0
- reports/eval_report.md +9 -0
app/tests/eval_rag.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
from datetime import datetime, timezone
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from time import perf_counter
|
| 5 |
+
|
| 6 |
+
from app.graph.workflow import compile_workflow
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
DATASET_PATH = Path("datasets/golden_qa.csv")
|
| 10 |
+
REPORT_PATH = Path("reports/eval_report.md")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_golden_questions(path: Path = DATASET_PATH) -> list[dict]:
|
| 14 |
+
with path.open(newline="", encoding="utf-8") as handle:
|
| 15 |
+
return list(csv.DictReader(handle))
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def token_overlap(expected: str, actual: str) -> float:
|
| 19 |
+
expected_tokens = set(expected.lower().split())
|
| 20 |
+
actual_tokens = set(actual.lower().split())
|
| 21 |
+
if not expected_tokens:
|
| 22 |
+
return 0.0
|
| 23 |
+
return round(len(expected_tokens & actual_tokens) / len(expected_tokens), 3)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def source_hit(expected_sources: str, sources: list[dict]) -> bool:
|
| 27 |
+
expected = {source.strip() for source in expected_sources.split("|") if source.strip()}
|
| 28 |
+
actual = {source.get("source") for source in sources}
|
| 29 |
+
return bool(expected & actual)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def run_local_evaluation() -> dict:
|
| 33 |
+
agent = compile_workflow()
|
| 34 |
+
rows = load_golden_questions()
|
| 35 |
+
results = []
|
| 36 |
+
|
| 37 |
+
for row in rows:
|
| 38 |
+
started = perf_counter()
|
| 39 |
+
output_state = agent.invoke({"question": row["question"], "run_count": 0})
|
| 40 |
+
latency_ms = round((perf_counter() - started) * 1000, 2)
|
| 41 |
+
answer = output_state.get("generation", "")
|
| 42 |
+
sources = output_state.get("sources", [])
|
| 43 |
+
results.append(
|
| 44 |
+
{
|
| 45 |
+
"question": row["question"],
|
| 46 |
+
"answer": answer,
|
| 47 |
+
"latency_ms": latency_ms,
|
| 48 |
+
"answer_overlap": token_overlap(row["expected_answer"], answer),
|
| 49 |
+
"source_hit": source_hit(row["expected_sources"], sources),
|
| 50 |
+
"retrieved_contexts": len(output_state.get("documents", [])),
|
| 51 |
+
}
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
average_overlap = round(sum(result["answer_overlap"] for result in results) / max(len(results), 1), 3)
|
| 55 |
+
source_hit_rate = round(sum(1 for result in results if result["source_hit"]) / max(len(results), 1), 3)
|
| 56 |
+
average_latency_ms = round(sum(result["latency_ms"] for result in results) / max(len(results), 1), 2)
|
| 57 |
+
summary = {
|
| 58 |
+
"questions": len(results),
|
| 59 |
+
"answer_overlap": average_overlap,
|
| 60 |
+
"source_hit_rate": source_hit_rate,
|
| 61 |
+
"average_latency_ms": average_latency_ms,
|
| 62 |
+
"results": results,
|
| 63 |
+
}
|
| 64 |
+
write_report(summary)
|
| 65 |
+
return summary
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def write_report(summary: dict) -> None:
|
| 69 |
+
REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 70 |
+
lines = [
|
| 71 |
+
"# RAG Evaluation Report",
|
| 72 |
+
"",
|
| 73 |
+
f"Generated: {datetime.now(timezone.utc).isoformat()}",
|
| 74 |
+
"",
|
| 75 |
+
"| Metric | Value |",
|
| 76 |
+
"| --- | ---: |",
|
| 77 |
+
f"| Questions | {summary['questions']} |",
|
| 78 |
+
f"| Answer overlap | {summary['answer_overlap']} |",
|
| 79 |
+
f"| Source hit rate | {summary['source_hit_rate']} |",
|
| 80 |
+
f"| Average latency ms | {summary['average_latency_ms']} |",
|
| 81 |
+
"",
|
| 82 |
+
"## Question Results",
|
| 83 |
+
"",
|
| 84 |
+
]
|
| 85 |
+
for result in summary["results"]:
|
| 86 |
+
lines.extend(
|
| 87 |
+
[
|
| 88 |
+
f"### {result['question']}",
|
| 89 |
+
"",
|
| 90 |
+
f"- Answer overlap: {result['answer_overlap']}",
|
| 91 |
+
f"- Source hit: {result['source_hit']}",
|
| 92 |
+
f"- Retrieved contexts: {result['retrieved_contexts']}",
|
| 93 |
+
f"- Latency ms: {result['latency_ms']}",
|
| 94 |
+
"",
|
| 95 |
+
]
|
| 96 |
+
)
|
| 97 |
+
REPORT_PATH.write_text("\n".join(lines), encoding="utf-8")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
print(run_local_evaluation())
|
datasets/golden_qa.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
question,expected_answer,expected_sources
|
| 2 |
+
What is Error Code 404?,Error Code 404 indicates that the requested server resource was not found and the router configuration should be checked.,sample_error.txt
|
| 3 |
+
What does this support copilot do?,The support copilot answers support questions using indexed documentation and retrieved context.,product_guide.txt
|
reports/eval_report.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RAG Evaluation Report
|
| 2 |
+
|
| 3 |
+
Run the local benchmark with:
|
| 4 |
+
|
| 5 |
+
```powershell
|
| 6 |
+
.\.venv\Scripts\python.exe -m app.tests.eval_rag
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
This report is regenerated from `datasets/golden_qa.csv` and captures answer overlap, source hit rate, retrieved context count, and latency.
|