Praneshrajan15's picture
Deploy DataForge playground API
13fe504 verified
Raw
History Blame Contribute Delete
7.31 kB
"""CLI subcommand: ``dataforge bench``."""
from __future__ import annotations
import json
from collections.abc import Callable
from pathlib import Path
from typing import Annotated, Any
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
_console = Console(stderr=True)
run_agent_comparison: Callable[..., Any] | None = None
def _parse_csv_list(raw_value: str) -> list[str]:
"""Parse a comma-separated CLI option into a list of strings."""
values = [item.strip() for item in raw_value.split(",")]
return [value for value in values if value]
def _parse_seed_list(raw_value: str | None) -> list[int] | None:
"""Parse an optional comma-separated seed list."""
if raw_value is None:
return None
seeds = [item.strip() for item in raw_value.split(",") if item.strip()]
return [int(seed) for seed in seeds]
def _runner() -> Callable[..., Any]:
"""Load the benchmark runner lazily so core CLI imports stay lightweight."""
global run_agent_comparison
if run_agent_comparison is None:
from dataforge.bench.runner import run_agent_comparison as loaded_runner
run_agent_comparison = loaded_runner
return run_agent_comparison
def _render_summary(output: Any) -> None:
"""Render the aggregate F1/quota summary table."""
table = Table(title="DataForge Benchmark Summary")
table.add_column("Method")
table.add_column("Dataset")
table.add_column("Status")
table.add_column("F1")
table.add_column("Avg Steps")
table.add_column("Quota")
for aggregate in output.aggregates:
table.add_row(
aggregate.method,
aggregate.dataset,
aggregate.status,
"Skipped" if aggregate.f1_mean is None else f"{aggregate.f1_mean:.4f}",
"Skipped" if aggregate.avg_steps_mean is None else f"{aggregate.avg_steps_mean:.2f}",
"Skipped"
if aggregate.quota_units_mean is None
else f"{aggregate.quota_units_mean:.4f}",
)
Console().print(table)
if any(aggregate.status == "skipped" for aggregate in output.aggregates):
Console().print(
Panel(
"Some LLM baselines were skipped. Set DATAFORGE_LLM_PROVIDER=groq and "
"GROQ_API_KEY to enable them.",
title="Benchmark Warning",
style="yellow",
)
)
def _render_coverage_matrix(output: Any) -> None:
"""Render the per-error-class recall matrix (honest coverage view)."""
from dataforge.bench.error_classes import BENCH_ERROR_CLASSES, class_coverage_matrix
matrix = class_coverage_matrix(list(output.records))
if not matrix:
return
table = Table(title="Per-Error-Class Detection Recall (coverage)")
table.add_column("Method/Dataset")
for error_class in BENCH_ERROR_CLASSES:
table.add_column(error_class)
for (method, dataset), scores in sorted(matrix.items()):
row = [f"{method}/{dataset}"]
for error_class in BENCH_ERROR_CLASSES:
score = scores.get(error_class)
if score is None or score.support == 0:
row.append("-")
else:
row.append(f"{score.detection_recall:.2f}/{score.recall:.2f} (n={score.support})")
table.add_row(*row)
Console().print(table)
Console().print(
Panel(
"Each cell shows detection_recall/correction_recall per error class on "
"the full RAHA datasets ('-' = no ground-truth cells of that class). "
"Detection credits flagging the error; correction credits producing the "
"exact right value. The honest split: a class can be well-detected yet "
"not auto-correctable (no derivable value), which is reported, not hidden.",
title="Coverage",
style="cyan",
)
)
def bench(
methods: Annotated[
str,
typer.Option(
"--methods",
help="Comma-separated benchmark methods.",
),
] = "heuristic,llm_zeroshot",
datasets: Annotated[
str,
typer.Option(
"--datasets",
help="Comma-separated benchmark datasets.",
),
] = "hospital",
seeds: Annotated[
int,
typer.Option("--seeds", help="Number of seeds per method/dataset pair."),
] = 3,
seed_list: Annotated[
str | None,
typer.Option(
"--seed-list",
help="Explicit comma-separated seed list. Overrides --seeds for reproducibility.",
),
] = None,
really_run_big_bench: Annotated[
bool,
typer.Option(
"--really-run-big-bench",
help="Override the free-tier benchmark quota guard when estimated calls exceed 500.",
),
] = False,
output_json: Annotated[
Path,
typer.Option(
"--output-json",
help="Where to write eval/results/agent_comparison.json.",
),
] = Path("eval/results/agent_comparison.json"),
cache_root: Annotated[
Path | None,
typer.Option(
"--cache-root",
help="Benchmark dataset cache root. Defaults to the user DataForge cache.",
),
] = None,
verify_dataset_hashes: Annotated[
bool,
typer.Option(
"--verify-dataset-hashes/--no-verify-dataset-hashes",
help="Verify cached benchmark bytes against pinned upstream hashes.",
),
] = True,
json_output: Annotated[
bool,
typer.Option("--json", help="Print benchmark results as JSON."),
] = False,
quick: Annotated[
bool,
typer.Option(
"--quick",
help="Offline coverage check: run random,heuristic on all datasets (1 seed) "
"and print the per-error-class coverage matrix. No API keys required.",
),
] = False,
) -> None:
"""Run real-world benchmark methods across cached benchmark datasets."""
if quick:
methods = "random,heuristic"
datasets = datasets if datasets != "hospital" else "hospital,flights,beers"
seeds = 1
seed_list = None
try:
output = _runner()(
methods=_parse_csv_list(methods),
datasets=_parse_csv_list(datasets),
seeds=seeds,
seed_list=_parse_seed_list(seed_list),
output_json=output_json,
really_run_big_bench=really_run_big_bench,
cache_root=cache_root,
verify_dataset_hashes=verify_dataset_hashes,
)
except Exception as exc:
_console.print(
Panel(
f"[bold red]{exc}[/bold red]",
title="Benchmark Error",
style="red",
)
)
raise typer.Exit(code=2) from exc
if json_output:
typer.echo(json.dumps(output.model_dump(mode="json"), indent=2, sort_keys=True))
return
_render_summary(output)
if quick:
_render_coverage_matrix(output)