Files
OpenJarvis/scripts/orchestrator/format_eval_sample.py
T
Andrew Park e6c47e9532 orchestrator: track the SFT training pipeline and fix ToolOrchestra tracing
The orchestrator SFT pipeline was living entirely in the working tree and
never got committed. One script in that lineage (clean_sft_data.py) has
already been lost with no way to recover it, so track the rest before the
same thing happens again:

  run_sft_fsdp.py      full-parameter FSDP trainer (launched via accelerate)
  sft_tokenize.py      conversation -> tokens + assistant-only masking
  render_sft_data.py   record -> chat-template rendering
  make_splits.py       train / holdout / overfit splits
  chat_orchestrator.py REPL for eyeballing a trained checkpoint's routing
  format_eval_sample.py, upload_to_braintrust.py
  scripts/train/       fsdp4.yaml, fsdp8.yaml, eval sbatch

toolorchestra/tracing.py was also untracked despite being imported by
unified.py and rollout.py, which left the branch broken for a fresh
checkout. Add it along with the rest of the ToolOrchestra package split.

Also here: mmlu_pro / supergpqa scorers, expert pricing for the OpenRouter
Qwen builds (estimates, flagged in-file), an expanded calculator/web_search
tool surface, and the reject-sampling clean gate that run_sft_fsdp reads
via --require-clean.

GRPO is left at its upstream state -- the RL stage hasn't been run yet, so
it stays out of this change. Paths in the sbatch are env-driven rather than
hardcoded to one cluster home.
2026-07-11 12:23:47 -07:00

240 lines
8.9 KiB
Python

#!/usr/bin/env python
"""Render graded orchestrator-eval JSONL records into clean, readable .txt files.
Companion to ``format_sft_sample.py`` (same banner/indent visual style), but
for the *scored* eval outputs written by ``eval_orchestrator.py``:
results/orch-eval-<model>-<tranche>-<suite>/<benchmark>_orchestrator.jsonl
Each record has: record_id, benchmark, model, model_answer (the model's
FINAL_ANSWER string), is_correct, score, latency_seconds, cost_usd, error, and
scoring_metadata. The full step-by-step routing trace is NOT saved — only the
final answer + gold + score — so this renderer cannot show intermediate steps.
The QUESTION text is not in the result JSONL; it's loaded from the benchmark
dataset by ``record_id`` (via ``openjarvis.evals.cli._build_dataset``). If the
dataset can't be loaded / the id doesn't match, we render everything else and
show "(question unavailable)".
Gold answer lives in ``scoring_metadata`` (shape varies by scorer):
* MCQ (mmlu-pro, supergpqa): ``reference_letter``.
* LLM-judge (gaia): parsed from ``raw_judge_output`` (`gold target "..."`),
plus the judge's ``reasoning``.
* anything else: dumped verbatim under SCORING (raw).
Usage:
.venv/bin/python scripts/orchestrator/format_eval_sample.py \
--input results/orch-eval-gemma-2k-full/gaia_orchestrator.jsonl --n 2
... --input <file> --lines 1,5,42 # specific 1-indexed lines
... --input <file> --all # one .txt per record
... --input <file> --all --only-wrong # only incorrect samples
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
WIDTH = 80
# Friendly aliases -> the registry keys _build_dataset expects (mirrors
# eval_orchestrator.BENCHMARK_ALIASES for the ones that write .jsonl here).
BENCHMARK_ALIASES = {
"mmlu_pro": "mmlu-pro",
"mmlu-pro": "mmlu-pro",
"gaia": "gaia",
"taubench": "taubench",
"supergpqa": "supergpqa",
"terminalbench_v2_1": "terminalbench-v2.1",
"terminalbench-v2.1": "terminalbench-v2.1",
}
def _banner(label: str) -> str:
"""Full-width headline: ``━━━ LABEL ━━━━━━…``."""
prefix = f"━━━ {label} "
fill = "━" * max(3, WIDTH - len(prefix))
return f"\n{prefix}{fill}\n"
def _indent(text: str, pad: str = " ") -> str:
text = (text or "").rstrip()
return "\n".join(pad + ln if ln.strip() else ln for ln in text.splitlines())
def _normalize_benchmark(name: str) -> str:
key = (name or "").strip()
return BENCHMARK_ALIASES.get(key, BENCHMARK_ALIASES.get(key.lower(), key))
# ---------------------------------------------------------------------------
# Question map: record_id -> problem text, loaded from the benchmark dataset.
# ---------------------------------------------------------------------------
def build_question_map(benchmark: str, n: int, seed: int = 42) -> dict:
"""Best-effort {record_id: problem}. Returns {} if the dataset can't load."""
try:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset(_normalize_benchmark(benchmark))
ds.load(max_samples=n, seed=seed)
return {r.record_id: r.problem for r in ds.iter_records()}
except Exception as exc: # noqa: BLE001 - never fail the render over this
print(f" [warn] could not load dataset for {benchmark!r}: "
f"{type(exc).__name__}: {exc}")
return {}
# ---------------------------------------------------------------------------
# Gold / judge parsing out of scoring_metadata.
# ---------------------------------------------------------------------------
_GOLD_TARGET_RE = re.compile(r'gold target\s*"([^"]*)"', re.IGNORECASE)
_JUDGE_REASONING_RE = re.compile(
r"reasoning:\s*(.*?)(?:\n\s*correct:|\Z)", re.IGNORECASE | re.DOTALL
)
def parse_scoring(meta: dict) -> dict:
"""Return {gold, judge, kind, clean}. ``clean`` False => dump raw block.
kind is one of 'mcq' | 'judge' | 'unknown'.
"""
if not isinstance(meta, dict):
return {"gold": None, "judge": None, "kind": "unknown", "clean": False}
# MCQ scorer (mmlu-pro / supergpqa).
if "reference_letter" in meta:
return {
"gold": meta.get("reference_letter"),
"judge": None,
"kind": "mcq",
"clean": True,
}
# LLM-judge scorer (gaia).
raw = meta.get("raw_judge_output")
if raw:
gold_m = _GOLD_TARGET_RE.search(raw)
reason_m = _JUDGE_REASONING_RE.search(raw)
return {
"gold": gold_m.group(1) if gold_m else None,
"judge": (reason_m.group(1).strip() if reason_m else raw.strip()),
"kind": "judge",
"clean": True,
}
# Unknown shape (e.g. {"reason": "no_choice_letter_extracted"}).
return {"gold": None, "judge": None, "kind": "unknown", "clean": False}
# ---------------------------------------------------------------------------
# Render one record.
# ---------------------------------------------------------------------------
def format_record(rec: dict, question: str | None) -> str:
parsed = parse_scoring(rec.get("scoring_metadata"))
is_correct = rec.get("is_correct")
verdict = "CORRECT" if is_correct else ("INCORRECT" if is_correct is False else "UNSCORED")
def _fmt(v, fmt):
try:
return fmt.format(v)
except (ValueError, TypeError):
return str(v)
top = " · ".join([
str(rec.get("record_id", "?")),
str(rec.get("benchmark", "?")),
verdict,
f"score {_fmt(rec.get('score'), '{:.3f}')}",
f"lat {_fmt(rec.get('latency_seconds'), '{:.1f}')}s",
f"cost ${_fmt(rec.get('cost_usd'), '{:.4f}')}",
])
parts = [top]
err = rec.get("error")
if err:
parts.append(f" error: {err}")
parts.append(_banner("QUESTION"))
parts.append(_indent(question if question else "(question unavailable)"))
parts.append(_banner("MODEL ANSWER"))
parts.append(_indent(str(rec.get("model_answer") or "(empty)")))
parts.append(_banner("GOLD"))
gold = parsed["gold"]
parts.append(_indent(str(gold) if gold not in (None, "") else "(gold unavailable — see SCORING below)"))
if parsed["kind"] == "judge" and parsed["judge"]:
parts.append(_banner("JUDGE"))
parts.append(_indent(parsed["judge"]))
if not parsed["clean"]:
parts.append(_banner("SCORING (raw)"))
parts.append(_indent(json.dumps(rec.get("scoring_metadata"), indent=2, default=str)))
return "\n".join(parts).rstrip() + "\n"
def main(argv=None):
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("--input", required=True, help="Graded *_orchestrator.jsonl file.")
p.add_argument("--out-dir", default="results/formatted", help="Where the .txt files go.")
p.add_argument("--seed", type=int, default=42, help="Subset seed used at eval time.")
p.add_argument("--only-wrong", action="store_true",
help="Render only incorrect/unscored samples.")
g = p.add_mutually_exclusive_group()
g.add_argument("--n", type=int, default=1, help="Format the first N records.")
g.add_argument("--lines", help="Comma-separated 1-indexed line numbers.")
g.add_argument("--all", action="store_true", help="Format every record.")
args = p.parse_args(argv)
src = Path(args.input)
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
records = [json.loads(l) for l in src.open() if l.strip()]
if not records:
print("no records in input")
return 1
# Pick indices first (so we only load the dataset once, sized to the file).
if args.lines:
idxs = [int(x) - 1 for x in args.lines.split(",") if x.strip()]
elif args.all:
idxs = list(range(len(records)))
else:
idxs = list(range(min(args.n, len(records))))
idxs = [i for i in idxs if 0 <= i < len(records)]
if args.only_wrong:
idxs = [i for i in idxs if not records[i].get("is_correct")]
benchmark = _normalize_benchmark(records[0].get("benchmark", src.stem.split("_")[0]))
# Load the whole subset so any selected id resolves (the eval-time subset is
# the first len(records) of seed=42).
qmap = build_question_map(benchmark, n=len(records), seed=args.seed)
written = []
for i in idxs:
rec = records[i]
rid = str(rec.get("record_id", i))
bench = rec.get("benchmark", benchmark)
fname = re.sub(r"[^A-Za-z0-9_.-]", "_", f"{bench}__{rid}")[:120] + ".txt"
out = out_dir / fname
out.write_text(format_record(rec, qmap.get(rid)))
written.append(out)
for w in written:
print(w)
print(f"\nwrote {len(written)} file(s) to {out_dir}/"
+ (" (dataset unavailable — questions omitted)" if not qmap else ""))
return 0
if __name__ == "__main__":
raise SystemExit(main())