Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

289 lines
10 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Calibrate KiCad task difficulty by running each task N times per model.
Usage:
python scripts/calibrate_tasks.py [--attempts N] [--parallel P] [--tasks-dir DIR]
Outputs:
scripts/tasks/calibration.json — per-task pass rates and difficulty tiers
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import time
from pathlib import Path
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
SCRIPT_DIR = Path(__file__).parent
TASKS_DIR = SCRIPT_DIR / "tasks"
OUTPUT_FILE = TASKS_DIR / "calibration.json"
MODELS = [
("claude", "anthropic/claude-opus-4-6"),
("openai", "openai/computer-use-preview"),
]
DEFAULT_ATTEMPTS = 5
DEFAULT_PARALLEL = 6
DEFAULT_MAX_STEPS = 150
# Difficulty tiers (from TBench spec, based on worst-model accuracy)
# Frontier: best model ≤ 20%
# Advanced Plus: worst model ≤ 20% (and not Frontier)
# Advanced: 20% < worst ≤ 60%
# Core: 60% < worst ≤ 80%
# Easy: worst > 80%
def _tier(claude_rate: float, openai_rate: float) -> str:
best = max(claude_rate, openai_rate)
worst = min(claude_rate, openai_rate)
if best <= 0.20:
return "frontier"
if worst <= 0.20:
return "advanced_plus"
if worst <= 0.60:
return "advanced"
if worst <= 0.80:
return "core"
return "easy"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _load_env() -> dict[str, str]:
env_file = SCRIPT_DIR.parent / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("=")
key, val = k.strip(), v.strip()
os.environ[key] = val # export into current process too
return os.environ.copy()
def _cb() -> str:
return str(SCRIPT_DIR.parent / ".venv" / "bin" / "cb")
def _run_dataset(model_name: str, model_id: str, parallel: int,
max_steps: int, tasks_dir: Path,
task_ids: list[str], attempts: int) -> str:
"""Build a temp dir with `attempts` copies of each task, run as one dataset."""
import tempfile, shutil
tmp = Path(tempfile.mkdtemp(prefix="cb_calib_"))
try:
for task_id in task_ids:
for i in range(attempts):
dst = tmp / f"{task_id}_{i}"
shutil.copytree(tasks_dir / task_id, dst)
cmd = [
_cb(), "run", "dataset", str(tmp),
"--agent", "cua-agent",
"--model", model_id,
"--max-parallel", str(parallel),
"--max-steps", str(max_steps),
]
print(f" [{model_name}] Starting {len(task_ids)} tasks × {attempts} attempts "
f"(parallel={parallel}): {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(SCRIPT_DIR.parent))
for line in result.stdout.splitlines():
if "Run ID:" in line:
return line.split("Run ID:")[-1].strip().split()[0]
raise RuntimeError(f"Could not parse run ID:\n{result.stdout}\n{result.stderr}")
finally:
shutil.rmtree(tmp, ignore_errors=True)
def _wait_for_run(run_id: str, poll_interval: int = 30) -> None:
"""Poll cb run list until all sessions for this run are in a terminal state."""
cb = _cb()
print(f" Waiting for run {run_id}...", end="", flush=True)
while True:
result = subprocess.run(
[cb, "run", "list"],
capture_output=True, text=True,
cwd=str(SCRIPT_DIR.parent),
)
lines = [l for l in result.stdout.splitlines() if run_id[:8] in l]
if lines:
terminal = [l for l in lines if any(
s in l for s in ("completed", "failed", "error", "done", "0.", "1.")
)]
if len(terminal) == len(lines):
print(" done.")
return
print(".", end="", flush=True)
time.sleep(poll_interval)
def _get_run_output_dir(run_id: str) -> Path | None:
"""Find the output directory for a run from cb run info."""
cb = _cb()
result = subprocess.run(
[cb, "run", "info", run_id],
capture_output=True, text=True,
cwd=str(SCRIPT_DIR.parent),
)
for line in result.stdout.splitlines():
if "Output:" in line or "output" in line.lower():
parts = line.split(":", 1)
if len(parts) == 2:
p = Path(parts[1].strip())
if p.exists():
return p
# Fallback: search default location
default = Path.home() / ".local" / "share" / "cua-bench" / "runs" / run_id
if default.exists():
return default
return None
def _extract_scores(run_output_dir: Path) -> dict[str, float]:
"""Extract per-task scores from a run output directory.
Returns {task_id: score} where score is 0.01.0.
"""
scores: dict[str, float] = {}
if not run_output_dir or not run_output_dir.exists():
return scores
try:
from datasets import load_from_disk
except ImportError:
# Fallback: parse run.log for "Evaluation result:"
for log in run_output_dir.rglob("run.log"):
task_id = log.parent.name.split("_")[0]
for line in log.read_text().splitlines():
if "Evaluation result:" in line:
try:
scores[task_id] = float(line.split(":")[-1].strip())
except ValueError:
pass
return scores
for trace_dir in run_output_dir.rglob("task_*_trace"):
session_dir = trace_dir.parent
task_id = session_dir.name.split("_")[0]
try:
ds = load_from_disk(str(trace_dir))
for row in ds:
if row.get("event_name") == "evaluate":
data = json.loads(row["data_json"])
scores[task_id] = float(data.get("result", 0.0))
break
except Exception:
# Fallback to run.log
log = session_dir / "run.log"
if log.exists():
for line in log.read_text().splitlines():
if "Evaluation result:" in line:
try:
scores[task_id] = float(line.split(":")[-1].strip())
except ValueError:
pass
return scores
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Calibrate KiCad task difficulty")
parser.add_argument("--attempts", type=int, default=DEFAULT_ATTEMPTS,
help=f"Number of attempts per model (default: {DEFAULT_ATTEMPTS})")
parser.add_argument("--parallel", type=int, default=DEFAULT_PARALLEL,
help=f"Max parallel tasks per run (default: {DEFAULT_PARALLEL})")
parser.add_argument("--max-steps", type=int, default=DEFAULT_MAX_STEPS,
help=f"Max steps per task (default: {DEFAULT_MAX_STEPS})")
parser.add_argument("--tasks-dir", type=Path, default=TASKS_DIR)
parser.add_argument("--output", type=Path, default=OUTPUT_FILE)
parser.add_argument("--task-filter", type=str, default=None,
help="Comma-separated task IDs to run (e.g. '55f2eefb,d1c655da')")
args = parser.parse_args()
# Load .env into os.environ so cb and any subprocesses pick it up
_load_env()
# Discover task IDs (optionally filtered)
all_task_ids = sorted(
p.name for p in args.tasks_dir.iterdir()
if p.is_dir() and (p / "main.py").exists()
)
if args.task_filter:
patterns = [p.strip() for p in args.task_filter.split(",")]
task_ids = [t for t in all_task_ids if any(t.startswith(p) or t == p for p in patterns)]
else:
task_ids = all_task_ids
print(f"Found {len(task_ids)} tasks: {task_ids}")
# Accumulate scores: {task_id: {model_name: [score, ...]}}
all_scores: dict[str, dict[str, list[float]]] = {
tid: {m: [] for m, _ in MODELS} for tid in task_ids
}
for model_name, model_id in MODELS:
print(f"\n=== Model: {model_name} ({model_id}) ===")
for attempt in range(args.attempts):
run_id = _run_dataset(
model_name, model_id, attempt,
args.parallel, args.max_steps,
args.tasks_dir, task_ids,
)
_wait_for_run(run_id)
out_dir = _get_run_output_dir(run_id)
scores = _extract_scores(out_dir)
print(f" Attempt {attempt+1} scores: {scores}")
for task_id in task_ids:
score = scores.get(task_id, 0.0)
all_scores[task_id][model_name].append(score)
# Compute pass rates and tiers
results: dict[str, dict] = {}
for task_id in task_ids:
task_scores = all_scores[task_id]
rates = {
m: (sum(task_scores[m]) / len(task_scores[m]) if task_scores[m] else 0.0)
for m, _ in MODELS
}
tier = _tier(rates["claude"], rates["openai"])
results[task_id] = {
"pass_rates": rates,
"attempts": args.attempts,
"difficulty": tier,
"raw_scores": task_scores,
}
print(f" {task_id}: claude={rates['claude']:.2f} openai={rates['openai']:.2f}{tier}")
# Summary
tier_counts: dict[str, int] = {}
for r in results.values():
tier_counts[r["difficulty"]] = tier_counts.get(r["difficulty"], 0) + 1
print(f"\nDifficulty distribution: {tier_counts}")
# Write manifest
manifest = {
"version": "1.0",
"models": {m: mid for m, mid in MODELS},
"attempts_per_model": args.attempts,
"tasks": results,
}
args.output.write_text(json.dumps(manifest, indent=2))
print(f"\nWrote {args.output}")
if __name__ == "__main__":
main()