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()