File size: 9,286 Bytes
698535a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | """Items in the arena's exact format, built with the arena's own session generator and label code.
Every item is what `mta dev` / `mta hidden` would emit (task, label, family, twin_of), plus an
`oracle` block: what happened in the session up to the cut, taken from the structured session the
renderer saw. `targets.py` writes the training summary from that block; nothing in it is used for
grading. Splits use disjoint generator seeds:
train dev-like lengths (12k to 95k tokens), the arena's two cuts plus extra cuts per session
val hidden-like (35k to 95k, full items at least 40k tokens), the arena's two cuts only
test hidden-like, a separate seed, 100 sessions by default like one SLM pool
"""
from __future__ import annotations
import argparse
import random
import re
import time
from pathlib import Path
from typing import Any
from ccslm.common import log, write_json, write_jsonl
SPLITS: dict[str, dict[str, Any]] = {
"train": {"tokens": (12_000, 95_000), "min_tokens": 12_000, "extra_cuts": 2},
"val": {"tokens": (35_000, 95_000), "min_tokens": 40_000, "extra_cuts": 0},
"test": {"tokens": (35_000, 95_000), "min_tokens": 40_000, "extra_cuts": 0},
}
EXTRA_SPAN = (0.25, 0.97)
MAX_NOTE = 240
MAX_ARGS = 200
SUMMARY_LINE = re.compile(r"^=*\s*((?:\d+ (?:passed|failed|errors?|skipped|xfailed|xpassed|warnings?)[, ]*)+.*?)\s*=*$")
def _result_line(text: str) -> str:
for line in reversed(text.strip().splitlines()):
found = SUMMARY_LINE.match(line.strip())
if found:
return found.group(1).strip()
return ""
def oracle_block(session: Any, cut: int, rendered: str) -> dict[str, Any]:
"""What the session shows up to `cut`, restricted to text that is visible in `rendered`."""
from arenas.compress import facts as factsmod
from arenas.compress import observe as obs
def visible(value: str) -> bool:
return bool(value) and factsmod.mentions(rendered, value)
seen = obs.observe(session, cut)
events: list[dict[str, Any]] = []
users: list[str] = []
pending: list[tuple[Any, str]] = []
for step in session.steps[:cut]:
if step.role == "user":
users.append(step.text.strip())
elif step.role == "agent":
note = " ".join(step.text.split())[:MAX_NOTE]
pending = [(call, note if i == 0 else "") for i, call in enumerate(step.calls)]
elif step.role == "tool" and pending:
call, note = pending.pop(0)
path = session.relative(call.path) if call.path else ""
event: dict[str, Any] = {
"kind": call.kind,
"name": call.name,
"path": path if visible(path) else "",
"args": call.args[:MAX_ARGS] if call.args and call.args[:60] in rendered else "",
"note": note if note and note[:60] in rendered else "",
"status": "ok" if step.ok else ("error" if step.ok is False else "done"),
}
if call.kind == "test":
event["passed"] = obs._test_passed(step.text, step.ok)
line = _result_line(step.text)
event["result"] = line if line and line in rendered else ""
errors: list[list[str]] = []
for name, message in obs.ERROR_LINE.findall(step.text):
message = (message or "").strip()[:120]
if not visible(name) or any(e[0] == name for e in errors):
continue
errors.append([name, message if message and message in rendered else ""])
event["errors"] = errors[:3]
events.append(event)
following = obs.next_action(session, cut)
errors_seen: list[list[str]] = []
for name, message in seen.errors:
if not visible(name):
continue
errors_seen = [e for e in errors_seen if e[0] != name]
errors_seen.append([name, message if message and message in rendered else ""])
frames = [[p, f] for p, f in seen.frames if f != "<module>" and visible(f)]
return {
"issue": users[0] if users else "",
"user_messages": users,
"events": events,
"opened": [p for p in seen.opened if visible(p)],
"created": [p for p in seen.created if visible(p)],
"edited": [p for p in seen.edited if visible(p)],
"errors": errors_seen,
"frames": frames,
"symbols": [s for s in seen.symbols if visible(s)],
"tests": list(seen.tests),
"next": {"kind": following[0], "path": following[1]} if following else None,
"goal": obs.goal_of(session),
}
def _extra_cuts(session: Any, rng: random.Random, taken: set[int], count: int) -> list[int]:
from arenas.compress import build
total = len(session.steps)
bounds = [
b
for b in build._tool_boundaries(session)
if EXTRA_SPAN[0] * total <= b <= EXTRA_SPAN[1] * total and b not in taken
]
rng.shuffle(bounds)
return sorted(bounds[:count])
def build_split(
split: str,
seed: str,
sessions: int,
out: Path,
*,
tokens: tuple[int, int] | None = None,
min_tokens: int | None = None,
extra_cuts: int | None = None,
workers: int = 0,
) -> dict[str, Any]:
from arenas.compress import build, synth
from arenas.compress.sources import parse
from arenas.pool import Pool
shape = SPLITS[split]
span = tokens or shape["tokens"]
floor = shape["min_tokens"] if min_tokens is None else min_tokens
extra = shape["extra_cuts"] if extra_cuts is None else extra_cuts
count, _ = build.counter()
rng = random.Random(f"{seed}:cuts")
pool = Pool(arena_id="ctx_compress", seed=0)
skipped = {"unusable": 0, "no_cuts": 0, "thin": 0}
made = 0
started = time.time()
for row in synth.rows(seed, sessions, span, workers=workers):
session = parse(row, "synth")
if not build.usable(session):
skipped["unusable"] += 1
continue
cuts = build.choose_cuts(session, rng)
if cuts is None:
skipped["no_cuts"] += 1
continue
half, late = cuts
stem = f"cc-{split}-{made:05d}"
full = build.make_item(session, late, count, stem, ["slm"], min_tokens=floor)
if full is None:
skipped["thin"] += 1
continue
chosen = [(full, late)]
twin = build.make_item(
session, half, count, f"{stem}-twin", ["slm"], min_tokens=floor // 2, twin_of=stem
)
if twin is not None:
chosen.append((twin, half))
for k, cut in enumerate(_extra_cuts(session, rng, {half, late}, extra)):
more = build.make_item(
session, cut, count, f"{stem}-x{k}", ["slm"], min_tokens=floor // 2
)
if more is not None:
more["extra_cut"] = True
chosen.append((more, cut))
for item, cut in chosen:
item["oracle"] = oracle_block(session, cut, item["task"]["user"])
pool.items.append(item)
made += 1
if made % 25 == 0:
log(f"{split}: {made}/{sessions} sessions, {len(pool.items)} items, {time.time() - started:.0f}s")
build.add_goal_probes(pool, rng)
for item in pool.items:
item["label"].pop("goal", None)
written = write_jsonl(out / "items.jsonl", pool.items)
report = {
"split": split,
"seed": seed,
"sessions": made,
"items": written,
"twins": sum(1 for i in pool.items if i.get("twin_of")),
"extra": sum(1 for i in pool.items if i.get("extra_cut")),
"skipped": skipped,
"input_tokens": build.stats([i["label"]["input_tokens"] for i in pool.items]),
"budget_tokens": build.stats([i["label"]["budget_tokens"] for i in pool.items]),
"seconds": round(time.time() - started, 1),
}
problems = build.check(pool)
report["problems"] = problems[:20]
write_json(out / "report.json", report)
return report
def main() -> None:
parser = argparse.ArgumentParser(description="build ctx_compress items with the arena generator")
parser.add_argument("--split", choices=sorted(SPLITS), required=True)
parser.add_argument("--sessions", type=int, required=True)
parser.add_argument("--seed", default="", help="generator seed string; default ccslm-<split>")
parser.add_argument("--out", required=True)
parser.add_argument("--tokens", default="", help="session size span, e.g. 12000-95000")
parser.add_argument("--min-tokens", type=int, default=None)
parser.add_argument("--extra-cuts", type=int, default=None)
parser.add_argument("--workers", type=int, default=0)
args = parser.parse_args()
span = tuple(int(v) for v in args.tokens.split("-")) if args.tokens else None
report = build_split(
args.split,
args.seed or f"ccslm-{args.split}",
args.sessions,
Path(args.out),
tokens=span, # type: ignore[arg-type]
min_tokens=args.min_tokens,
extra_cuts=args.extra_cuts,
workers=args.workers,
)
log(f"{args.split}: {report['items']} items from {report['sessions']} sessions, {len(report['problems'])} problems")
if __name__ == "__main__":
main()
|