Download scripts/export_replays.py from NicolasT08/formal-mas: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/NicolasT08/formal-mas/resolve/main/scripts/export_replays.py
- Command line
-
hf download hf://NicolasT08/formal-mas/scripts/export_replays.py
-
curl -L -o export_replays.py https://huggingface.co/NicolasT08/formal-mas/resolve/main/scripts/export_replays.py
16.1 kB
| """Export recorded timelines from runs/ to demo/replays/*.json for the demo's Replay mode. | |
| python scripts/export_replays.py --candidates # list problems worth replaying (prints only) | |
| python scripts/export_replays.py --dry-run # show what SELECTION would export | |
| python scripts/export_replays.py # write demo/replays/*.json + results.md | |
| Reads runs/ (never writes there). Only the paid `experiment` runs of the `test` split are used | |
| (the ones of docs/results/results.md); for each (config, problem) the last row that is not | |
| `budget_stop` is taken. | |
| What a replay keeps (a whitelist, nothing else leaves runs/): per event only the fields of | |
| KEEP; for Generator calls, the strategy and sketch parsed from the model's JSON (no raw model | |
| output, prompts, cache keys, fingerprints, paths, argv, git or platform data); the problem's | |
| metadata and formal statement from data/eval.jsonl, with the informal text dropped for | |
| PutnamBench rows (MAA permission); the result's status, proof, C2 solution and call/cost totals. | |
| results.md: Tables 1 and 2 of docs/results/results.md, copied verbatim. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import re | |
| import sys | |
| from datetime import datetime | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| RUNS = ROOT / "runs" | |
| OUT = ROOT / "demo" / "replays" | |
| SCHEMA = 1 | |
| MAX_TEXT = 4000 | |
| # (file stem, problem id, config, title, note) — chosen with the team before exporting. | |
| SELECTION: list[tuple[str, str, str, str, str]] = [ | |
| ("1_amc12_2001_p9_C5", "amc12_2001_p9", "C5", "Medium: solved after one retry", | |
| "The first Lean attempt is rejected by the parser; the Critic classifies it as `syntax` and retries, " | |
| "and the second attempt is accepted."), | |
| ("2_imo_1964_p1_2_C5", "imo_1964_p1_2", "C5", "Hard (IMO 1964): solved by C5", | |
| "Two tactic failures, two retries guided by Lean's errors, then a proof the kernel accepts. " | |
| "C4 (no Critic) does not solve it."), | |
| ("3_algebra_absapb_C5", "algebra_absapbon1pabsapbleqsumabsaon1pabsa", "C5", | |
| "Critic: retries and a replan (C4 fails)", | |
| "Missing-lemma errors make the Critic retry with more retrieved lemmas and finally replan; " | |
| "C4, which stops after the first failure, does not solve this problem (see the side-by-side view)."), | |
| ("3b_algebra_absapb_C4", "algebra_absapbon1pabsapbleqsumabsaon1pabsa", "C4", | |
| "Same problem without the Critic (C4)", | |
| "C4 is the full system without the Critic: the first failed Lean attempt ends the problem, " | |
| "with no retry and no replan. In the recorded run its 5 LLM calls were answered from the on-disk LLM " | |
| "cache (the same prompts as C5's first steps), so it took 2 s and $0 (nominal cost $0.0039)."), | |
| ("4_imo_1979_p1_C2", "imo_1979_p1", "C2", "Baseline B claims a proof: not confirmed by Lean", | |
| "Baseline B writes a natural-language proof and judges it correct itself. Formalizing that proof, " | |
| "Lean accepts none of the attempts, and no configuration solved this problem. The claim is " | |
| "*not confirmed by Lean*; that does not make it incorrect (it may be a correct argument that failed " | |
| "to formalize). A self-assessed claim cannot tell the two apart; only the Lean kernel can."), | |
| ] | |
| COMMON = ("event", "level") | |
| KEEP: dict[str, tuple[str, ...]] = { | |
| "problem_start": ("problem_id", "config"), | |
| "prepass": ("targets", "found", "seconds"), | |
| "llm_call": ("role", "model_returned", "latency_s", "cost_usd", "cached", "finish_reason", | |
| "completion_tokens", "reasoning_tokens"), | |
| "plan_proposed": ("subgoals", "closing", "rationale"), | |
| "plan_skeleton": ("stage", "compiles", "errors"), | |
| "plan_refute_check": ("hypotheses_inconsistent", "refuted", "seconds"), | |
| "plan_closing": ("accepted", "reason"), | |
| "plan_closing_auto": ("reason", "failed_reason"), | |
| "plan_fallback": ("reason",), | |
| "plan_syntax_rejected": ("reason", "detail"), | |
| "retrieve": ("subgoal", "expanded", "lemmas"), | |
| "sketch_vote": ("subgoal", "votes", "strategy", "samples", "status"), | |
| "lean_verify": ("status", "sorry", "axioms", "seconds"), | |
| "formalize_failed": ("subgoal", "reason", "detail", "tactics", "errors"), | |
| "formalize_verified": ("subgoal", "tactics"), | |
| "critique": ("subgoal", "stage", "reason", "error_class", "action", "route", "attempts", "replans", | |
| "calls", "missing_identifier", "suggestions"), | |
| "subgoal_solved": ("subgoal", "solved_by", "attempts"), | |
| "assemble": ("accepted", "reason", "errors"), | |
| "pipeline_end": ("status", "solved", "stage", "error_classes", "replans"), | |
| "self_assessment": ("sample", "verdict", "confidence"), | |
| "claim_check_start": (), | |
| "claim_check_attempt": ("attempt", "reason", "error_class"), | |
| "claim_check_end": ("status", "accepted", "attempts", "error_classes"), | |
| "invalid_statement": ("error",), | |
| "llm_retry": (), "llm_json_invalid": (), "llm_json_repaired": (), "llm_length_finish": (), | |
| "problem_end": ("status", "solved", "claimed_solved", "llm", "wall_s"), | |
| } | |
| LLM_SUMMARY = ("llm_calls", "b_max", "cached_calls", "cost_usd") | |
| RESULT_TABLES = ("### Table 1.", "### Table 2.") | |
| __all__ = ["KEEP", "SELECTION", "clean_event", "export_one", "find_row", "results_tables"] | |
| def _ts(s: str) -> datetime: | |
| return datetime.fromisoformat(s.replace("Z", "+00:00")) | |
| def _clip(value): | |
| if isinstance(value, str) and len(value) > MAX_TEXT: | |
| return value[:MAX_TEXT] + " … (truncated)" | |
| if isinstance(value, list): | |
| return [_clip(v) for v in value] | |
| return value | |
| def clean_event(e: dict, t0: datetime) -> dict | None: | |
| """Whitelisted copy of a trace event with `t` = seconds since problem_start; None = dropped.""" | |
| name = e.get("event") | |
| if name not in KEEP: | |
| return None | |
| out = {"t": round((_ts(e["ts"]) - t0).total_seconds(), 3)} if e.get("ts") else {"t": 0.0} | |
| for key in (*COMMON, *KEEP[name]): | |
| if key in e: | |
| out[key] = _clip(e[key]) | |
| if name == "problem_end" and isinstance(out.get("llm"), dict): | |
| out["llm"] = {k: out["llm"][k] for k in LLM_SUMMARY if k in out["llm"]} | |
| if name == "llm_call" and e.get("role") == "generator": | |
| try: | |
| data = json.loads(e.get("content") or "") | |
| except (ValueError, TypeError): | |
| data = None | |
| if isinstance(data, dict) and data.get("sketch"): | |
| out["strategy"], out["sketch"] = data.get("strategy"), _clip(str(data["sketch"])) | |
| return out | |
| def _experiment_runs(config: str) -> list[Path]: | |
| """test-split runs of `config` with profile `experiment` (not experiment_pro), oldest first.""" | |
| out = [] | |
| for d in sorted(RUNS.glob(f"*_{config}_experiment*")): | |
| cfg_path = d / "run_config.json" | |
| if not cfg_path.is_file() or not (d / "results.jsonl").is_file(): | |
| continue | |
| cfg = json.loads(cfg_path.read_text(encoding="utf-8")) | |
| split = (cfg.get("dataset") or {}).get("split") | |
| if cfg.get("profile") == "experiment" and split == "test" and cfg.get("config") == config: | |
| out.append(d) | |
| return out | |
| def find_row(config: str, problem_id: str) -> tuple[Path, dict] | None: | |
| """(run folder, result row): the last row of the problem that is not budget_stop.""" | |
| found = None | |
| for d in _experiment_runs(config): | |
| for line in (d / "results.jsonl").read_text(encoding="utf-8").splitlines(): | |
| if not line.strip(): | |
| continue | |
| row = json.loads(line) | |
| if row.get("problem_id") == problem_id and row.get("status") != "budget_stop": | |
| found = (d, row) | |
| return found | |
| def _problem_events(run_dir: Path, problem_id: str) -> list[dict]: | |
| """Events of the problem's last attempt in the run (from its last problem_start).""" | |
| events = [] | |
| with (run_dir / "trace.jsonl").open(encoding="utf-8") as fh: | |
| for line in fh: | |
| if f'"{problem_id}"' not in line: | |
| continue | |
| e = json.loads(line) | |
| if e.get("problem_id") != problem_id: | |
| continue | |
| if e.get("event") == "problem_start": | |
| events = [] | |
| events.append(e) | |
| return events | |
| def _dataset() -> dict[str, dict]: | |
| path = ROOT / "data" / "eval.jsonl" | |
| return {r["id"]: r for r in map(json.loads, filter(str.strip, path.read_text(encoding="utf-8").splitlines()))} | |
| def _problem(p: dict) -> dict: | |
| withheld = "MAA" in str(p.get("license") or "") | |
| out = {k: p.get(k) for k in ("id", "split", "area", "difficulty", "source", "license")} | |
| out["informal"] = None if withheld else p.get("informal") | |
| if withheld: | |
| out["informal_withheld"] = True | |
| out["statement"] = p.get("statement") | |
| return out | |
| def _result(row: dict, withheld: bool) -> dict: | |
| out = {k: row.get(k) for k in ("status", "solved", "claimed_solved", "stage", "error_classes", "replans", "wall_s")} | |
| out["llm"] = {k: (row.get("llm") or {}).get(k) for k in LLM_SUMMARY} | |
| if row.get("proof") and row.get("solved"): | |
| out["proof"] = row["proof"] | |
| if row.get("config") == "C2" and not withheld: # a C2 solution restates the problem | |
| out["solution"] = _clip(row.get("solution")) | |
| out["final_answer"] = row.get("final_answer") | |
| if row.get("check"): | |
| out["check"] = {"status": row["check"].get("status"), "accepted": row["check"].get("accepted"), | |
| "attempts": len(row["check"].get("attempts") or [])} | |
| return out | |
| def _models(run_dir: Path) -> dict: | |
| cfg = json.loads((run_dir / "run_config.json").read_text(encoding="utf-8")) | |
| roles = ((cfg.get("models") or {}).get("roles") or {}) | |
| return {role: spec.get("model_id") for role, spec in roles.items() if isinstance(spec, dict)} | |
| def export_one(problem_id: str, config: str, title: str, note: str, dataset: dict) -> dict: | |
| hit = find_row(config, problem_id) | |
| if hit is None: | |
| raise SystemExit(f"no {config} experiment/test row for {problem_id}") | |
| run_dir, row = hit | |
| raw = _problem_events(run_dir, problem_id) | |
| if not raw or raw[0].get("event") != "problem_start": | |
| raise SystemExit(f"no trace for {problem_id} in {run_dir.name}") | |
| t0 = _ts(raw[0]["ts"]) | |
| events = [c for c in (clean_event(e, t0) for e in raw) if c is not None] | |
| problem = _problem(dataset[problem_id]) | |
| return {"schema": SCHEMA, "title": title, "note": note, "config": config, "run_id": run_dir.name, | |
| "profile": "experiment", "models": _models(run_dir), "problem": problem, "events": events, | |
| "result": _result(row, bool(problem.get("informal_withheld")))} | |
| def results_tables(text: str) -> str: | |
| """Tables 1 and 2 of docs/results/results.md (each up to the next `### ` heading).""" | |
| parts = ["# Results on the `test` split (n = 30)\n", | |
| "Copied from `docs/results/results.md` (generated by `docs/results/make_tables.py`). " | |
| "C1 = Baseline A (LLM writes the Lean proof, no feedback), C2 = Baseline B (natural language, " | |
| "self-assessed, never verified), C3 = full system without RAG, C4 = without the Critic, " | |
| "C5 = full system.\n"] | |
| for head in RESULT_TABLES: | |
| m = re.search(rf"^{re.escape(head)}.*?(?=^### |^## |\Z)", text, flags=re.M | re.S) | |
| if m: | |
| parts.append(m.group(0).rstrip() + "\n") | |
| return "\n".join(parts) | |
| def _table_rows(text: str, head: str) -> list[list[str]]: | |
| m = re.search(rf"^{re.escape(head)}.*?(?=^### |^## |\Z)", text, flags=re.M | re.S) | |
| if not m: | |
| return [] | |
| rows = [ln.strip().strip("|").split("|") for ln in m.group(0).splitlines() if ln.startswith("|")] | |
| return [[c.strip() for c in r] for r in rows[2:]] # skip the header and the --- line | |
| def _nums(cell: str) -> list[float]: | |
| return [float(x) for x in re.findall(r"[+-]?\d+(?:\.\d+)?", cell)] | |
| def results_data(text: str) -> dict: | |
| """Numbers of Tables 1 and 2 (results.md) for the demo's plain-language results tab.""" | |
| sr, c2 = [], None | |
| for r in _table_rows(text, "### Table 1."): | |
| if r[0].startswith("C2"): | |
| claimed, fpr = _nums(r[4]), _nums(r[5]) | |
| c2 = {"claimed": int(claimed[0]), "n": int(claimed[1]), "claimed_pct": claimed[2], | |
| "not_confirmed": int(fpr[0]), "checked": int(fpr[1]), "not_confirmed_pct": fpr[2], | |
| "not_confirmed_ci": fpr[3:5]} | |
| elif re.fullmatch(r"C\d", r[0]): | |
| solved, ci = _nums(r[1]), _nums(r[3]) | |
| sr.append({"config": r[0], "solved": int(solved[0]), "n": int(solved[1]), "pct": float(r[2]), "ci": ci[:2]}) | |
| tests = [] | |
| for r in _table_rows(text, "### Table 2."): | |
| if re.fullmatch(r"H\d", r[0]): | |
| a, b = re.findall(r"C\d", r[1])[:2] | |
| tests.append({"id": r[0], "a": a, "b": b, "delta_pp": _nums(r[4])[0], "ci": _nums(r[5])[:2], | |
| "p_adj": _nums(r[7])[0], "significant": r[8].lower().startswith("yes")}) | |
| return {"source": "docs/results/results.md (Tables 1 and 2)", "sr": sr, "c2": c2, "tests": tests} | |
| def candidates() -> None: | |
| def rows(config): | |
| out = {} | |
| for d in _experiment_runs(config): | |
| for line in (d / "results.jsonl").read_text(encoding="utf-8").splitlines(): | |
| if line.strip(): | |
| r = json.loads(line) | |
| if r.get("status") != "budget_stop": | |
| out[r["problem_id"]] = r | |
| return out | |
| c = {k: rows(k) for k in ("C1", "C2", "C3", "C4", "C5")} | |
| print("C5 solved and C4 not (the Critic's contribution):") | |
| for pid, r in c["C5"].items(): | |
| if r.get("solved") and not c["C4"].get(pid, {}).get("solved"): | |
| crit = [(x["error_class"], x["action"]) for x in r.get("critiques") or []] | |
| print(f" {pid:45} {r['difficulty']:6} calls={r['llm']['llm_calls']:2} {r['wall_s'] / 60:4.1f} min {crit}") | |
| print("C2 claimed but the Lean check did not verify:") | |
| for pid, r in c["C2"].items(): | |
| if r.get("claimed_solved") and (r.get("check") or {}).get("status") != "verified": | |
| by = [k for k in ("C1", "C3", "C4", "C5") if c[k].get(pid, {}).get("solved")] | |
| print(f" {pid:30} {r['difficulty']:6} solved by {by or 'no configuration'}") | |
| def main(argv: list[str] | None = None) -> int: | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("--candidates", action="store_true", help="list candidate problems and exit") | |
| ap.add_argument("--dry-run", action="store_true", help="print what would be written") | |
| ap.add_argument("--out", type=Path, default=OUT) | |
| args = ap.parse_args(argv) | |
| if args.candidates: | |
| candidates() | |
| return 0 | |
| dataset = _dataset() | |
| if not args.dry_run: | |
| args.out.mkdir(parents=True, exist_ok=True) | |
| for stem, pid, config, title, note in SELECTION: | |
| rep = export_one(pid, config, title, note, dataset) | |
| r = rep["result"] | |
| print(f"{stem:30} {config} {pid:45} {r['status']:15} events={len(rep['events']):3} " | |
| f"calls={r['llm']['llm_calls']} from {rep['run_id']}") | |
| if not args.dry_run: | |
| (args.out / f"{stem}.json").write_text(json.dumps(rep, ensure_ascii=False, indent=1) + "\n", | |
| encoding="utf-8") | |
| tables = results_tables((ROOT / "docs" / "results" / "results.md").read_text(encoding="utf-8")) | |
| data = results_data((ROOT / "docs" / "results" / "results.md").read_text(encoding="utf-8")) | |
| if not (data["sr"] and data["c2"] and data["tests"]): | |
| raise SystemExit("could not read Tables 1 and 2 of docs/results/results.md") | |
| if not args.dry_run: | |
| (args.out / "results.md").write_text(tables, encoding="utf-8") | |
| (args.out / "results.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8") | |
| print(f"results.md: {len(tables)} chars" + (" (dry run, nothing written)" if args.dry_run else f" → {args.out}")) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |