Spaces:
Sleeping
Sleeping
Download scripts/run_canary_eval.py from Nomearod/agentbench: direct link, hf CLI and curl.
- Browser
- Download file 8.01 kB
-
https://huggingface.co/spaces/Nomearod/agentbench/resolve/main/scripts/run_canary_eval.py
- Command line
-
hf download hf://spaces/Nomearod/agentbench/scripts/run_canary_eval.py
-
curl -L -o run_canary_eval.py https://huggingface.co/spaces/Nomearod/agentbench/resolve/main/scripts/run_canary_eval.py
8.01 kB
| """Canary detection runner: run-judges | build-report. | |
| run-judges scores the four dimension judges over a canary set -- each canary is | |
| a golden question plus an injected answer with known planted defects -- and | |
| writes a predictions JSON. build-report joins those predictions with the canary | |
| ground truth and renders docs/_generated/canary_detection.md. | |
| run-judges with real judges spends API budget (one judge call per canary x | |
| dimension); it is the paid path, mirrored on `make calibrate`, and is not run in | |
| tests or CI. build-report is free and offline. Tests drive the run by injecting | |
| MockJudge through the build_judges seam, so no API calls happen. | |
| Examples: | |
| python scripts/run_canary_eval.py run-judges \ | |
| --canaries tests/stats/fixtures/canary/canaries.json \ | |
| --out results/canary/predictions.json | |
| python scripts/run_canary_eval.py build-report \ | |
| --canaries tests/stats/fixtures/canary/canaries.json \ | |
| --predictions tests/stats/fixtures/canary/predictions.json | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import importlib | |
| import json | |
| import sys | |
| from pathlib import Path | |
| from typing import TYPE_CHECKING, Any | |
| import structlog | |
| if TYPE_CHECKING: | |
| from agent_bench.evaluation.judges.base import Judge | |
| # Run as a script (python scripts/run_canary_eval.py) puts scripts/ on | |
| # sys.path[0], not the repo root, so the repo-local stats packages are not | |
| # importable until we add the repo root (same bootstrap as run_epochs.py). | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| from stats_adapters import canary # noqa: E402 | |
| logger = structlog.get_logger() | |
| REPO = Path(__file__).resolve().parents[1] | |
| DIMENSIONS = ("groundedness", "completeness", "relevance", "citation_faithfulness") | |
| DEFAULT_OUT = REPO / "docs/_generated/canary_detection.md" | |
| # The committed report renders from golden-derived canary fixtures with simulated | |
| # verdicts; say so in the artifact so no reader mistakes the simulation for a | |
| # real judge measurement. The committed-report test pins the artifact to this | |
| # exact string, so it is the single source of truth for the demonstration | |
| # provenance. | |
| DEFAULT_PROVENANCE = ( | |
| "golden-derived canary fixtures (tests/stats/fixtures/canary) with real " | |
| "corpus source passages; the judge verdicts are simulated, not a measurement " | |
| "of any real judge. Replace with real run-judges output for a measurement" | |
| ) | |
| def _build_item_and_output(c: dict) -> tuple[Any, Any]: | |
| """Build the (GoldenQuestion, AgentResponse) pair a judge scores from one | |
| canary record. agent_bench imports are deferred so the offline build-report | |
| path imports neither agent_bench nor the embedding stack. | |
| ``source_chunks`` must align one-to-one with ``sources``: the citation judge | |
| maps a "[source: X]" claim to its evidence via ``zip(sources, source_chunks)``, | |
| so a missing or misaligned chunk silently scores the citation against no | |
| evidence. Misalignment is a loud authoring error. | |
| """ | |
| from agent_bench.agents.orchestrator import AgentResponse, SourceReference | |
| from agent_bench.core.types import TokenUsage | |
| from agent_bench.evaluation.harness import GoldenQuestion | |
| sources = c.get("sources", []) | |
| source_chunks = c.get("source_chunks", []) | |
| if len(source_chunks) != len(sources): | |
| raise ValueError( | |
| f"canary {c['id']!r}: source_chunks must align one-to-one with sources " | |
| f"(one retrieved chunk per source, for the citation judge); got " | |
| f"{len(source_chunks)} chunks for {len(sources)} sources" | |
| ) | |
| item = GoldenQuestion( | |
| id=c["id"], | |
| question=c.get("question", ""), | |
| expected_answer_keywords=[], | |
| expected_sources=[], | |
| category=c.get("category", "retrieval"), | |
| difficulty="easy", | |
| requires_calculator=False, | |
| source_snippets=c.get("source_snippets", []), | |
| reference_answer=c.get("reference_answer", ""), | |
| ) | |
| output = AgentResponse( | |
| answer=c.get("answer", ""), | |
| sources=[SourceReference(source=s) for s in sources], | |
| ranked_sources=c.get("ranked_sources", []), | |
| source_chunks=source_chunks, | |
| iterations=1, | |
| usage=TokenUsage(input_tokens=0, output_tokens=0, estimated_cost_usd=0), | |
| latency_ms=0, | |
| ) | |
| return item, output | |
| async def score_canaries(canaries: list[dict], judges: dict[str, Judge]) -> list[dict[str, Any]]: | |
| """Score each canary on each dimension with the supplied judges | |
| ({dimension: Judge}). Returns one prediction dict per canary x dimension: | |
| {item_id, dimension, **ScoreResult}. Judges are injected, so tests pass | |
| MockJudge and no API call happens. | |
| """ | |
| preds: list[dict[str, Any]] = [] | |
| for c in canaries: | |
| item, output = _build_item_and_output(c) | |
| for dim in DIMENSIONS: | |
| result = await judges[dim].score(item, output) | |
| preds.append({"item_id": c["id"], "dimension": dim, **result.model_dump()}) | |
| return preds | |
| def build_judges(provider: str, model_id: str) -> dict[str, Judge]: | |
| """Construct the four real dimension judges (paid path). Seam: tests | |
| monkeypatch this to inject MockJudge. | |
| Reuses the calibration runner's rubric-loading judge factory so the canary | |
| judges are byte-identical to production. It is imported dynamically because | |
| run_calibration is a sibling script, not a library API; a static import | |
| would also pull its module into this script's type-check scope. | |
| """ | |
| from agent_bench.core.config import load_config | |
| make_judge = getattr(importlib.import_module("run_calibration"), "_make_judge") | |
| cfg = load_config() | |
| return {dim: make_judge(provider, model_id, dim, cfg) for dim in DIMENSIONS} | |
| async def cmd_run_judges( | |
| canaries_path: Path, out_path: Path, *, provider: str, model_id: str | |
| ) -> None: | |
| canaries = json.loads(canaries_path.read_text()) | |
| judges = build_judges(provider, model_id) | |
| preds = await score_canaries(canaries, judges) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| out_path.write_text(json.dumps(preds, indent=2) + "\n") | |
| logger.info("canary_run_judges_complete", count=len(preds), path=str(out_path)) | |
| def cmd_build_report( | |
| canaries_path: Path, | |
| predictions_path: Path, | |
| out_path: Path, | |
| *, | |
| provenance: str = DEFAULT_PROVENANCE, | |
| ) -> None: | |
| canaries = json.loads(canaries_path.read_text()) | |
| predictions = json.loads(predictions_path.read_text()) | |
| md = canary.build_report(canaries, predictions, provenance=provenance) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| out_path.write_text(md) | |
| logger.info("canary_build_report_complete", path=str(out_path)) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter | |
| ) | |
| sub = parser.add_subparsers(dest="cmd", required=True) | |
| p_run = sub.add_parser("run-judges", help="PAID: score the dimension judges over a canary set") | |
| p_run.add_argument("--canaries", type=Path, required=True) | |
| p_run.add_argument("--out", type=Path, required=True) | |
| p_run.add_argument("--provider", default="anthropic") | |
| p_run.add_argument("--model-id", default="claude-haiku-4-5") | |
| p_rep = sub.add_parser( | |
| "build-report", help="FREE: render the detection report from predictions" | |
| ) | |
| p_rep.add_argument("--canaries", type=Path, required=True) | |
| p_rep.add_argument("--predictions", type=Path, required=True) | |
| p_rep.add_argument("--out", type=Path, default=DEFAULT_OUT) | |
| p_rep.add_argument("--provenance", default=DEFAULT_PROVENANCE) | |
| args = parser.parse_args() | |
| if args.cmd == "run-judges": | |
| asyncio.run( | |
| cmd_run_judges(args.canaries, args.out, provider=args.provider, model_id=args.model_id) | |
| ) | |
| elif args.cmd == "build-report": | |
| cmd_build_report(args.canaries, args.predictions, args.out, provenance=args.provenance) | |
| if __name__ == "__main__": | |
| main() | |