Spaces:
Sleeping
Sleeping
Download scripts/run_epochs.py from Nomearod/agentbench: direct link, hf CLI and curl.
- Browser
- Download file 13.7 kB
-
https://huggingface.co/spaces/Nomearod/agentbench/resolve/main/scripts/run_epochs.py
- Command line
-
hf download hf://spaces/Nomearod/agentbench/scripts/run_epochs.py
-
curl -L -o run_epochs.py https://huggingface.co/spaces/Nomearod/agentbench/resolve/main/scripts/run_epochs.py
13.7 kB
| """Repeat harness runs k times per configuration with injected provenance. | |
| This script can make real, PAID API calls. The free path is narrow: a *custom* | |
| entry run with a mock config (provider.default: mock). It is the only path | |
| exercised in CI. langchain entries always build a real ChatOpenAI/ChatAnthropic | |
| from their --provider regardless of any config, so a mock config does NOT make | |
| them free; and a custom entry without a mock config uses its real provider. | |
| run_config_epochs therefore refuses any paid run unless allow_paid is set | |
| (--allow-paid on the CLI), so invoking this script directly cannot silently | |
| spend money -- the Makefile epochs target is not the only guard (guardrail 2). | |
| Provenance is injected by post-processing each entry point's --output JSON into | |
| an envelope file (design spec section 6); harness internals are never edited. | |
| """ | |
| import argparse | |
| import datetime | |
| import hashlib | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| import yaml | |
| # Repo root on path so the preflight can import agent_bench when this file is | |
| # run as a script (python scripts/run_epochs.py puts scripts/ on sys.path[0], | |
| # not the repo root). The heavy work still happens in subprocesses. | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" | |
| PROVIDER_KEY_ENV = {"openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY"} | |
| # Provider -> model recorded in langchain config_id (audit #7) AND imported by | |
| # run_langchain_eval to build the client, so the recorded provenance and the | |
| # model that actually bills are the same constant. Do not duplicate it. | |
| MODEL_DEFAULTS = {"openai": "gpt-4o-mini", "anthropic": "claude-haiku-4-5-20251001"} | |
| def _is_mock_config(path: Path | None) -> bool: | |
| """True only if the config actually sets provider.default: mock. | |
| The free-path guard must verify config CONTENT, not just that --mock-config | |
| was passed: a real config handed to --mock-config would otherwise be | |
| classified free and bill silently (paid-path audit finding #6). | |
| """ | |
| if path is None: | |
| return False | |
| data = yaml.safe_load(Path(path).read_text()) or {} | |
| return bool(data.get("provider", {}).get("default") == "mock") | |
| def new_ulid() -> str: | |
| ts = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp() * 1000) | |
| time_part = "" | |
| for _ in range(10): | |
| time_part = CROCKFORD[ts % 32] + time_part | |
| ts //= 32 | |
| rand = int.from_bytes(os.urandom(10), "big") | |
| rand_part = "" | |
| for _ in range(16): | |
| rand_part = CROCKFORD[rand % 32] + rand_part | |
| rand //= 32 | |
| return time_part + rand_part | |
| def write_envelope( | |
| raw_output: Path, | |
| dest_dir: Path, | |
| run_id: str, | |
| config_id: str, | |
| epoch: int, | |
| code_version: str, | |
| dataset_version: str, | |
| timestamp: str, | |
| ) -> Path: | |
| dest_dir.mkdir(parents=True, exist_ok=True) | |
| envelope = { | |
| "run_id": run_id, | |
| "timestamp": timestamp, | |
| "config_id": config_id, | |
| "code_version": code_version, | |
| "dataset_version": dataset_version, | |
| "epoch": epoch, | |
| "results": json.loads(raw_output.read_text()), | |
| } | |
| out = dest_dir / f"{config_id.split('+')[0]}_e{epoch}.json" | |
| out.write_text(json.dumps(envelope, indent=1)) | |
| return out | |
| def _config_hash(path: Path | None) -> str: | |
| if path is None: | |
| return "00000000" | |
| return hashlib.sha256(path.read_bytes()).hexdigest()[:8] | |
| def _dataset_version(golden_path: Path) -> str: | |
| return "sha-" + hashlib.sha256(golden_path.read_bytes()).hexdigest()[:8] | |
| def _config_id(name: str, spec: dict, mock_config: Path | None) -> str: | |
| if spec["entry"] == "langchain": | |
| # Encode the real model so langchain-openai/-anthropic carry meaningful | |
| # provenance instead of the constant +00000000 sentinel (audit #7); the | |
| # model is what actually bills. config_id is free-form, schema untouched. | |
| return f"{name}+{MODEL_DEFAULTS[spec['provider']]}" | |
| return f"{name}+{_config_hash(mock_config or spec.get('config'))}" | |
| def _code_version() -> str: | |
| sha = subprocess.run( | |
| ["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, check=True | |
| ).stdout.strip() | |
| dirty = subprocess.run( | |
| ["git", "status", "--porcelain"], capture_output=True, text=True, check=True | |
| ).stdout.strip() | |
| return sha + ("-dirty" if dirty else "") | |
| # name -> (entry, config yaml or None, provider flag or None, corpus, golden path) | |
| REGISTRY: dict[str, dict] = { | |
| "custom-openai": { | |
| "entry": "custom", | |
| "config": Path("configs/default.yaml"), | |
| "corpus": "fastapi", | |
| "golden": Path("agent_bench/evaluation/datasets/tech_docs_golden.json"), | |
| }, | |
| "custom-anthropic": { | |
| "entry": "custom", | |
| "config": Path("configs/anthropic.yaml"), | |
| "corpus": "fastapi", | |
| "golden": Path("agent_bench/evaluation/datasets/tech_docs_golden.json"), | |
| }, | |
| "langchain-openai": { | |
| "entry": "langchain", | |
| "provider": "openai", | |
| "golden": Path("agent_bench/evaluation/datasets/tech_docs_golden.json"), | |
| }, | |
| "langchain-anthropic": { | |
| "entry": "langchain", | |
| "provider": "anthropic", | |
| "golden": Path("agent_bench/evaluation/datasets/tech_docs_golden.json"), | |
| }, | |
| # --- k8s corpus (campaign executed 2026-06-22, paid) -------------------- | |
| # The full 25-question k8s campaign has run: K=5 per config, one run_id each. | |
| # Envelopes live under results/epochs/ (gitignored, force-added like the | |
| # fastapi run); tidy rows in results/long/k8s/; the report's k8s section | |
| # regenerates with `make evaluate-stats`. langchain has no --corpus flag (see | |
| # _entry_cmd), so k8s is custom-only and the framework-equivalence (TOST) | |
| # section is empty by construction, not a regression. Re-run with: | |
| # make epochs K=5 CONFIGS=custom-openai-k8s,custom-anthropic-k8s CONFIRM_PAID=1 | |
| "custom-openai-k8s": { | |
| "entry": "custom", | |
| "config": Path("configs/default.yaml"), | |
| "corpus": "k8s", | |
| "golden": Path("agent_bench/evaluation/datasets/k8s_golden.json"), | |
| }, | |
| "custom-anthropic-k8s": { | |
| "entry": "custom", | |
| "config": Path("configs/anthropic.yaml"), | |
| "corpus": "k8s", | |
| "golden": Path("agent_bench/evaluation/datasets/k8s_golden.json"), | |
| }, | |
| } | |
| def _entry_cmd(spec: dict, raw_out: Path, mock_config: Path | None) -> list[str]: | |
| config = mock_config or spec.get("config") | |
| if spec["entry"] == "custom": | |
| cmd = [ | |
| sys.executable, | |
| "scripts/evaluate.py", | |
| "--mode", | |
| "deterministic", | |
| "--output", | |
| str(raw_out), | |
| ] | |
| if config: | |
| cmd += ["--config", str(config)] | |
| if spec.get("corpus"): | |
| cmd += ["--corpus", spec["corpus"]] | |
| return cmd | |
| cmd = [ | |
| sys.executable, | |
| "scripts/run_langchain_eval.py", | |
| "--provider", | |
| spec["provider"], | |
| "--output", | |
| str(raw_out), | |
| ] | |
| if config: | |
| cmd += ["--config", str(config)] | |
| return cmd | |
| def run_config_epochs( | |
| name: str, | |
| k: int, | |
| dest_root: Path, | |
| mock_config: Path | None = None, | |
| golden_override: Path | None = None, | |
| allow_paid: bool = False, | |
| ) -> list[Path]: | |
| spec = REGISTRY[name] | |
| # Guardrail 2 (no silent paid calls): the only free path is a custom entry | |
| # with a mock config. langchain entries always use a real LLM; a custom | |
| # entry without a mock config uses its real provider. Refuse otherwise so | |
| # direct script invocation cannot bill around the Makefile's CONFIRM_PAID. | |
| if mock_config is not None and not _is_mock_config(mock_config): | |
| raise SystemExit( | |
| f"refusing: --mock-config {mock_config} does not set provider.default: mock, " | |
| "so it would make real (paid) API calls. Pass an actual mock config, " | |
| "or --allow-paid to confirm you intend to spend money." | |
| ) | |
| is_free = spec["entry"] == "custom" and mock_config is not None | |
| if not is_free and not allow_paid: | |
| raise SystemExit( | |
| f"refusing: config {name!r} would make real (paid) API calls " | |
| f"(entry={spec['entry']}, mock_config={'set' if mock_config else 'none'}); " | |
| "pass --allow-paid to confirm you intend to spend money" | |
| ) | |
| golden = golden_override or spec["golden"] | |
| config_id = _config_id(name, spec, mock_config) | |
| run_id = new_ulid() | |
| written = [] | |
| for epoch in range(1, k + 1): | |
| # Raw harness output lives UNDER the run dir, not a sibling results/epochs/raw/, | |
| # so results/epochs/ contains only run_id dirs. The WP5 convert loop globs | |
| # results/epochs/*/ and would otherwise feed raw EvalResult lists (no envelope | |
| # wrapper) to convert_envelopes and crash (audit #4). | |
| raw_out = dest_root / run_id / "raw" / f"{name}_e{epoch}.json" | |
| raw_out.parent.mkdir(parents=True, exist_ok=True) | |
| subprocess.run(_entry_cmd(spec, raw_out, mock_config), check=True) | |
| written.append( | |
| write_envelope( | |
| raw_output=raw_out, | |
| dest_dir=dest_root / run_id, | |
| run_id=run_id, | |
| config_id=config_id, | |
| epoch=epoch, | |
| code_version=_code_version(), | |
| dataset_version=_dataset_version(golden), | |
| timestamp=datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), | |
| ) | |
| ) | |
| return written | |
| def _preflight(names: list[str], mock_config: Path | None) -> None: | |
| """Validate every requested config BEFORE any subprocess runs, so a broken | |
| config fails at zero cost instead of after earlier configs have already | |
| billed (paid-path audit finding #2). Checks the registry name, that a custom | |
| entry's corpus resolves in its config, that the golden file and corpus store | |
| exist, and that the API key for any paying entry is present in os.environ | |
| (load_dotenv has already run in main()). | |
| """ | |
| from agent_bench.core.config import load_config | |
| problems: list[str] = [] | |
| for name in names: | |
| spec = REGISTRY.get(name) | |
| if spec is None: | |
| problems.append(f"{name}: unknown config (known: {sorted(REGISTRY)})") | |
| continue | |
| if not Path(spec["golden"]).exists(): | |
| problems.append(f"{name}: golden dataset missing: {spec['golden']}") | |
| if spec["entry"] == "custom": | |
| cfg_path = mock_config or spec.get("config") | |
| try: | |
| cfg = load_config(Path(cfg_path) if cfg_path else None) | |
| except Exception as exc: | |
| problems.append(f"{name}: config {cfg_path} failed to load ({exc})") | |
| continue | |
| provider = cfg.provider.default | |
| corpus = spec.get("corpus") | |
| if corpus and corpus not in cfg.corpora: | |
| problems.append( | |
| f"{name}: corpus {corpus!r} not in {cfg_path} corpora " | |
| f"{sorted(cfg.corpora)}; evaluate.py would exit 1 mid-run" | |
| ) | |
| elif corpus and not Path(cfg.corpora[corpus].store_path).exists(): | |
| problems.append( | |
| f"{name}: corpus {corpus!r} store {cfg.corpora[corpus].store_path} " | |
| "missing; build it (make ingest) before the campaign" | |
| ) | |
| else: | |
| provider = spec["provider"] | |
| free = spec["entry"] == "custom" and _is_mock_config(mock_config) | |
| if not free and provider != "mock": | |
| env = PROVIDER_KEY_ENV.get(provider) | |
| if env and not os.environ.get(env): | |
| problems.append(f"{name}: {env} not set; a real {provider} run needs it") | |
| if problems: | |
| raise SystemExit( | |
| "preflight failed (no API calls made); fix before spending:\n - " | |
| + "\n - ".join(problems) | |
| ) | |
| def main() -> None: | |
| from dotenv import load_dotenv | |
| # Load the gitignored .env so the provider key reads (os.environ, in the | |
| # subprocesses) and the preflight key check below see the keys (audit #3). | |
| load_dotenv() | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--k", type=int, required=True) | |
| parser.add_argument("--configs", required=True, help="comma-separated registry names") | |
| parser.add_argument("--dest", default="results/epochs") | |
| parser.add_argument( | |
| "--mock-config", default=None, help="config YAML forcing provider mock (free)" | |
| ) | |
| parser.add_argument("--golden", default=None, help="override golden path (tests only)") | |
| parser.add_argument( | |
| "--allow-paid", | |
| action="store_true", | |
| help="confirm real (paid) API calls for non-mock runs (langchain or no mock config)", | |
| ) | |
| parser.add_argument( | |
| "--dry-run", | |
| action="store_true", | |
| help="run preflight only: validate every config, make no API calls, spend nothing", | |
| ) | |
| args = parser.parse_args() | |
| names = args.configs.split(",") | |
| mock_config = Path(args.mock_config) if args.mock_config else None | |
| _preflight(names, mock_config=mock_config) | |
| if args.dry_run: | |
| print(f"preflight OK for {names}; --dry-run, no API calls made") | |
| return | |
| for name in names: | |
| files = run_config_epochs( | |
| name, | |
| args.k, | |
| Path(args.dest), | |
| mock_config=mock_config, | |
| golden_override=Path(args.golden) if args.golden else None, | |
| allow_paid=args.allow_paid, | |
| ) | |
| print(f"{name}: wrote {len(files)} epoch envelopes") | |
| if __name__ == "__main__": | |
| main() | |