| """ |
| Run Karthik's V1 eval delivery against the V2.5 fast backend. |
| |
| This is an experiment harness, not a clinical validation. It uses a fresh |
| session for each single-turn eval query, then separately scores the risky / |
| ambiguous cases. The output is intended for iteration and presentation metrics. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| import json |
| from pathlib import Path |
| import sys |
| import time |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT / "demo")) |
|
|
| import app |
|
|
|
|
| DEFAULT_DELIVERY = ROOT / "Data_Karthik" / "empathrag_eval_delivery_v1" |
|
|
|
|
| def read_csv(path: Path) -> list[dict]: |
| with path.open("r", encoding="utf-8-sig", newline="") as handle: |
| return list(csv.DictReader(handle)) |
|
|
|
|
| def split_semicolon(value: str) -> list[str]: |
| return [item.strip() for item in value.split(";") if item.strip() and item.strip().lower() != "none"] |
|
|
|
|
| def source_org_hit(expected_source_names: list[str], retrieved_sources: list[dict]) -> bool: |
| if not expected_source_names: |
| return True |
| actual = [str(source.get("source_name", "")) for source in retrieved_sources] |
| return any(any(expected in source or source in expected for source in actual) for expected in expected_source_names) |
|
|
|
|
| def score_eval_queries(delivery_dir: Path) -> dict: |
| rows = read_csv(delivery_dir / "eval_queries.csv") |
| cases = [] |
| intercept_correct = 0 |
| source_hits = 0 |
| latencies = [] |
|
|
| for row in rows: |
| pipeline = app.FastDemoPipeline(app.CURATED_DB_PATH, "curated_support", 5) |
| t0 = time.perf_counter() |
| result = pipeline.run(row["query_text"]) |
| latency_ms = round((time.perf_counter() - t0) * 1000, 2) |
| latencies.append(latency_ms) |
|
|
| expected_intercept = row["should_intercept"].strip().lower() == "yes" |
| actual_intercept = bool(result.get("crisis")) |
| intercept_match = expected_intercept == actual_intercept |
| intercept_correct += int(intercept_match) |
|
|
| expected_sources = split_semicolon(row.get("expected_source_names", "")) |
| hit = source_org_hit(expected_sources, result.get("retrieved_sources", [])) |
| source_hits += int(hit) |
|
|
| cases.append( |
| { |
| "query_id": row["query_id"], |
| "scenario_category": row["scenario_category"], |
| "risk_category": row["risk_category"], |
| "query_text": row["query_text"], |
| "expected_intercept": expected_intercept, |
| "actual_intercept": actual_intercept, |
| "intercept_match": intercept_match, |
| "expected_source_names": expected_sources, |
| "actual_source_names": [source.get("source_name", "") for source in result.get("retrieved_sources", [])], |
| "source_org_hit": hit, |
| "route": result.get("route_label", ""), |
| "safety_tier": result.get("safety_tier", ""), |
| "output_guard": result.get("output_guard", {}), |
| "latency_ms": latency_ms, |
| } |
| ) |
|
|
| total = len(rows) |
| return { |
| "summary": { |
| "rows": total, |
| "intercept_accuracy": intercept_correct / total if total else None, |
| "source_org_hit_rate": source_hits / total if total else None, |
| "average_latency_ms": round(sum(latencies) / len(latencies), 2) if latencies else None, |
| }, |
| "cases": cases, |
| } |
|
|
|
|
| def score_risky_cases(delivery_dir: Path) -> dict: |
| rows = read_csv(delivery_dir / "risky_or_ambiguous_cases.csv") |
| cases = [] |
| intercept_correct = 0 |
|
|
| for row in rows: |
| pipeline = app.FastDemoPipeline(app.CURATED_DB_PATH, "curated_support", 5) |
| result = pipeline.run(row["query_text"]) |
| expected_intercept = row["should_intercept"].strip().lower() == "yes" |
| actual_intercept = bool(result.get("crisis")) |
| intercept_match = expected_intercept == actual_intercept |
| intercept_correct += int(intercept_match) |
| cases.append( |
| { |
| "case_id": row["case_id"], |
| "correct_risk_category": row["correct_risk_category"], |
| "query_text": row["query_text"], |
| "expected_intercept": expected_intercept, |
| "actual_intercept": actual_intercept, |
| "intercept_match": intercept_match, |
| "route": result.get("route_label", ""), |
| "safety_tier": result.get("safety_tier", ""), |
| "expected_handling": row["expected_handling"], |
| } |
| ) |
|
|
| total = len(rows) |
| return { |
| "summary": { |
| "rows": total, |
| "intercept_accuracy": intercept_correct / total if total else None, |
| }, |
| "cases": cases, |
| } |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--delivery-dir", type=Path, default=DEFAULT_DELIVERY) |
| parser.add_argument("--output", type=Path, default=ROOT / "eval" / "karthik_eval_results.json") |
| args = parser.parse_args() |
|
|
| result = { |
| "delivery_dir": str(args.delivery_dir), |
| "eval_queries": score_eval_queries(args.delivery_dir), |
| "risky_or_ambiguous_cases": score_risky_cases(args.delivery_dir), |
| } |
| args.output.write_text(json.dumps(result, indent=2), encoding="utf-8") |
| print(json.dumps( |
| { |
| "eval_queries": result["eval_queries"]["summary"], |
| "risky_or_ambiguous_cases": result["risky_or_ambiguous_cases"]["summary"], |
| }, |
| indent=2, |
| )) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|