oev 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
oev/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """OEV: a small System One decision model.
2
+
3
+ State + typed questions in, calibrated probability distributions out,
4
+ one forward pass.
5
+ """
6
+
7
+ __version__ = "0.1.0"
oev/benchmark.py ADDED
@@ -0,0 +1,70 @@
1
+ import argparse
2
+ import torch
3
+ import torch.nn.functional as F
4
+ from torch.utils.data import DataLoader
5
+ from oev.dataset import OEVDataset, collate
6
+ from oev.model import HFBackboneOEV
7
+ from oev.evaluate import load_model, ece
8
+ from oev.calibrate import fit_temperature_from_logits
9
+ from oev.tokenizer_hf import HFTokenPacker
10
+
11
+
12
+ def stack_ragged(rows):
13
+ kmax = max(r.numel() for r in rows)
14
+ out = torch.full((len(rows), kmax), -1e4)
15
+ for i, r in enumerate(rows):
16
+ out[i, : r.numel()] = r
17
+ return out
18
+
19
+
20
+ def evaluate_benchmark(checkpoint, data_dir, calibrate=True, batch_size=64):
21
+ device = "cuda" if torch.cuda.is_available() else "cpu"
22
+ model = load_model(checkpoint, device)
23
+ if isinstance(model, HFBackboneOEV):
24
+ packer = HFTokenPacker(model.cfg["backbone"])
25
+ max_len = model.cfg["max_len"]
26
+ else:
27
+ packer = None
28
+ max_len = model.cfg.max_len
29
+ dl = DataLoader(OEVDataset(f"{data_dir}/test.jsonl", max_len, packer=packer), batch_size=batch_size, collate_fn=collate)
30
+ confs, corr = [], []
31
+ val_logits, val_labels = [], []
32
+ if calibrate:
33
+ vdl = DataLoader(OEVDataset(f"{data_dir}/valid.jsonl", max_len, packer=packer), batch_size=batch_size, collate_fn=collate)
34
+ with torch.no_grad():
35
+ for batch in vdl:
36
+ logits = model(batch["ids"].to(device), batch["pad_mask"].to(device), batch["anchor_pos"].to(device)) + batch["logits_mask"].to(device)
37
+ keep = batch["anchor_valid"].sum(dim=1)
38
+ for i in range(logits.size(0)):
39
+ row = logits[i][: keep[i]]
40
+ if row.numel() >= 2:
41
+ val_logits.append(row)
42
+ val_labels.append(batch["labels"][i])
43
+ temperature = fit_temperature_from_logits(stack_ragged(val_logits), torch.stack(val_labels))
44
+ else:
45
+ temperature = 1.0
46
+ with torch.no_grad():
47
+ for batch in dl:
48
+ logits = model(batch["ids"].to(device), batch["pad_mask"].to(device), batch["anchor_pos"].to(device)) + batch["logits_mask"].to(device)
49
+ probs = F.softmax(logits / temperature, dim=-1)
50
+ conf, pred = probs.max(dim=-1)
51
+ for i in range(len(batch["labels"])):
52
+ confs.append(conf[i].item())
53
+ corr.append(int(pred[i].item() == batch["labels"][i].item()))
54
+ return {
55
+ "accuracy": sum(corr) / len(corr),
56
+ "n": len(corr),
57
+ "ece": ece(confs, corr),
58
+ "temperature": temperature,
59
+ }
60
+
61
+
62
+ if __name__ == "__main__":
63
+ p = argparse.ArgumentParser()
64
+ p.add_argument("--checkpoint", required=True)
65
+ p.add_argument("--data-dir", required=True)
66
+ p.add_argument("--no-calibrate", action="store_true")
67
+ args = p.parse_args()
68
+ r = evaluate_benchmark(args.checkpoint, args.data_dir, calibrate=not args.no_calibrate)
69
+ for k, v in r.items():
70
+ print(f"{k}: {v:.4f}" if isinstance(v, float) else f"{k}: {v}")
oev/benchmark_ext.py ADDED
@@ -0,0 +1,213 @@
1
+ """Extended benchmark metrics for typed-decisions: soft accuracy, Brier score,
2
+ score MAE, and per-workflow / per-primitive breakdowns.
3
+
4
+ Mirrors the metric set published in Laya's typed-decisions table so results
5
+ are directly comparable column-for-column.
6
+
7
+ Usage:
8
+ python -m oev.benchmark_ext --checkpoint CKPT --data-dir data/typed
9
+ Ensembles:
10
+ python -m oev.benchmark_ext --ckpts a.pt,b.pt,c.pt --data-dir data/typed
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+
16
+ import torch
17
+ import torch.nn.functional as F
18
+
19
+ from oev.evaluate import load_model
20
+ from oev.tokenizer_hf import HFTokenPacker
21
+
22
+
23
+ def _predict_probs(model, packer, state, question, device, gamma=1.0):
24
+ pq = {
25
+ "name": question["name"],
26
+ "type": question["type"],
27
+ "instructions": question.get("instructions", question["type"]),
28
+ "options": question["options"],
29
+ "answer": question["answer"],
30
+ }
31
+ ids, anchors, label = packer.pack(state, pq, model.cfg["max_len"])
32
+ tids = torch.tensor([ids], device=device)
33
+ pmask = torch.zeros(1, len(ids), dtype=torch.bool, device=device)
34
+ apos = torch.tensor([anchors], device=device)
35
+ with torch.no_grad():
36
+ logits = model(tids, pmask, apos)
37
+ probs = torch.softmax(logits[0].float(), dim=-1)
38
+ if gamma != 1.0:
39
+ probs = probs.clamp_min(1e-9) ** gamma
40
+ probs = probs / probs.sum()
41
+ return probs, label, question["type"], question.get("target")
42
+
43
+
44
+ def ece_metric(confs, corrs, n_bins=10):
45
+ """Expected Calibration Error: weighted |confidence - accuracy| over bins."""
46
+ if not confs:
47
+ return 0.0
48
+ bins = [[] for _ in range(n_bins)]
49
+ for c, o in zip(confs, corrs):
50
+ b = min(int(c * n_bins), n_bins - 1)
51
+ bins[b].append((c, o))
52
+ e = 0.0
53
+ for b in bins:
54
+ if not b:
55
+ continue
56
+ acc = sum(o for _, o in b) / len(b)
57
+ conf = sum(c for c, _ in b) / len(b)
58
+ e += (len(b) / len(confs)) * abs(conf - acc)
59
+ return e
60
+
61
+
62
+ def evaluate_metrics(checkpoints, data_dir, device="cuda", gamma=1.0, latency=False):
63
+ if device == "cuda" and not torch.cuda.is_available():
64
+ device = "cpu"
65
+ models = [load_model(c, device) for c in checkpoints]
66
+ packers = [HFTokenPacker(m.cfg["backbone"]) for m in models]
67
+
68
+ rows = [json.loads(l) for l in open(f"{data_dir}/test.jsonl", encoding="utf-8")]
69
+
70
+ n = correct = 0
71
+ soft_acc_sum = 0.0
72
+ brier_sum = 0.0
73
+ score_mae_sum = 0.0
74
+ score_n = 0
75
+ confs = []
76
+ corrs = []
77
+ by_type = {}
78
+ by_domain = {}
79
+
80
+ with torch.no_grad():
81
+ for r in rows:
82
+ for q in r["questions"]:
83
+ probs_sum = None
84
+ label = None
85
+ target = None
86
+ qtype = None
87
+ for m, p in zip(models, packers):
88
+ probs, label, qtype, target = _predict_probs(m, p, r["state"], q, device, gamma=gamma)
89
+ probs_sum = probs if probs_sum is None else probs_sum + probs
90
+ probs = probs_sum / len(models)
91
+
92
+ n += 1
93
+ hit = int(probs.argmax().item() == label)
94
+ correct += hit
95
+ confs.append(probs.max().item())
96
+ corrs.append(hit)
97
+
98
+ # soft accuracy: the probability mass the model put on the gold answer
99
+ soft_acc_sum += probs[label].item()
100
+
101
+ # Brier score against the gold one-hot (lower is better)
102
+ oh = torch.zeros_like(probs)
103
+ oh[label] = 1.0
104
+ brier_sum += ((probs - oh) ** 2).sum().item()
105
+
106
+ # score MAE: |expected level - gold level| for score questions
107
+ if qtype == "score":
108
+ levels = torch.arange(probs.numel(), dtype=torch.float32, device=probs.device)
109
+ ev = (probs * levels).sum().item()
110
+ score_mae_sum += abs(ev - label)
111
+ score_n += 1
112
+
113
+ agg = by_type.setdefault(qtype, [0, 0, 0.0])
114
+ agg[0] += hit
115
+ agg[1] += 1
116
+ agg[2] += probs[label].item()
117
+
118
+ agg = by_domain.setdefault(r.get("domain", "typed"), [0, 0, 0.0])
119
+ agg[0] += hit
120
+ agg[1] += 1
121
+ agg[2] += probs[label].item()
122
+
123
+ result = {
124
+ "accuracy": correct / n,
125
+ "soft_acc": soft_acc_sum / n,
126
+ "brier": brier_sum / n,
127
+ "score_mae": (score_mae_sum / score_n) if score_n else None,
128
+ "ece": ece_metric(confs, corrs),
129
+ "n": n,
130
+ }
131
+ print(f"accuracy : {result['accuracy']:.4f}")
132
+ print(f"soft acc : {result['soft_acc']:.4f}")
133
+ print(f"brier : {result['brier']:.4f}")
134
+ if result["score_mae"] is not None:
135
+ print(f"score MAE: {result['score_mae']:.4f}")
136
+ print(f"ece : {result['ece']:.4f}")
137
+ print(f"n : {result['n']}")
138
+
139
+ print("\nper primitive:")
140
+ for t, (c, tot, s) in sorted(by_type.items()):
141
+ print(f" {t:<7} acc {c / tot:.4f} soft acc {s / tot:.4f} (n={tot})")
142
+ print("per workflow:")
143
+ for d, (c, tot, s) in sorted(by_domain.items()):
144
+ print(f" {d:<28} acc {c / tot:.4f} soft acc {s / tot:.4f} (n={tot})")
145
+
146
+ if latency:
147
+ measure_latency(models[0], packers[0], rows, device)
148
+ return result
149
+
150
+
151
+ def measure_latency(model, packer, rows, device, n_single=50, n_batch=200, batch_size=32):
152
+ """Latency protocol: p50 single-question and batched per-question throughput.
153
+ Model must already be on `device` and warmed up by at least a few calls."""
154
+ import time
155
+
156
+ qs = [(r["state"], q) for r in rows for q in r["questions"]]
157
+
158
+ def run_one(s, q):
159
+ pq = {"name": q["name"], "type": q["type"], "instructions": q.get("instructions", q["type"]), "options": q["options"], "answer": q["answer"]}
160
+ ids, an, _ = packer.pack(s, pq, model.cfg["max_len"])
161
+ t0 = time.perf_counter()
162
+ with torch.no_grad():
163
+ model(torch.tensor([ids], device=device), torch.zeros(1, len(ids), dtype=torch.bool, device=device), torch.tensor([an], device=device))
164
+ if device == "cuda":
165
+ torch.cuda.synchronize()
166
+ return (time.perf_counter() - t0) * 1000
167
+
168
+ for s, q in qs[:3]: # warmup
169
+ run_one(s, q)
170
+ times = sorted(run_one(s, q) for s, q in qs[:n_single])
171
+ print(f"\nlatency single-question p50: {times[len(times) // 2]:.1f} ms (n={n_single}, warmup 3)")
172
+
173
+ # batched throughput: pack n_batch questions, pad to common length, one forward per batch
174
+ items = qs[:n_batch]
175
+ t0 = time.perf_counter()
176
+ done = 0
177
+ for i in range(0, len(items), batch_size):
178
+ chunk = items[i : i + batch_size]
179
+ packed = []
180
+ for s, q in chunk:
181
+ pq = {"name": q["name"], "type": q["type"], "instructions": q.get("instructions", q["type"]), "options": q["options"], "answer": q["answer"]}
182
+ packed.append(packer.pack(s, pq, model.cfg["max_len"]))
183
+ L = max(len(p[0]) for p in packed)
184
+ A = max(len(p[1]) for p in packed)
185
+ B = len(packed)
186
+ ids = torch.zeros(B, L, dtype=torch.long, device=device)
187
+ pmask = torch.ones(B, L, dtype=torch.bool, device=device)
188
+ apos = torch.zeros(B, A, dtype=torch.long, device=device)
189
+ for j, (pid, pan, _) in enumerate(packed):
190
+ ids[j, : len(pid)] = torch.tensor(pid, device=device)
191
+ pmask[j, : len(pid)] = False
192
+ apos[j, : len(pan)] = torch.tensor(pan, device=device)
193
+ with torch.no_grad():
194
+ model(ids, pmask, apos)
195
+ done += B
196
+ if device == "cuda":
197
+ torch.cuda.synchronize()
198
+ total_ms = (time.perf_counter() - t0) * 1000
199
+ print(f"latency batched: {total_ms / done:.1f} ms/question (n={done}, batch={batch_size})")
200
+ print(f"throughput: {done / (total_ms / 1000):.0f} questions/sec")
201
+
202
+
203
+ if __name__ == "__main__":
204
+ p = argparse.ArgumentParser()
205
+ p.add_argument("--checkpoint", default=None)
206
+ p.add_argument("--ckpts", default=None, help="comma-separated checkpoints (ensemble)")
207
+ p.add_argument("--data-dir", default="data/typed")
208
+ p.add_argument("--sharpen", type=float, default=1.0, help="confidence exponent gamma; >1 sharpens distributions")
209
+ p.add_argument("--latency", action="store_true", help="also measure single p50 and batched throughput")
210
+ args = p.parse_args()
211
+ ckpts = ([c.strip() for c in args.ckpts.split(",") if c.strip()]
212
+ if args.ckpts else [args.checkpoint])
213
+ evaluate_metrics(ckpts, args.data_dir, gamma=args.sharpen, latency=args.latency)
oev/calibrate.py ADDED
@@ -0,0 +1,22 @@
1
+ import torch
2
+
3
+
4
+ def apply_temperature(logits, temperature):
5
+ return logits / temperature
6
+
7
+
8
+ def fit_temperature_from_logits(logits, labels, max_iter=200, lr=0.05):
9
+ logits = logits.detach().float().cpu()
10
+ labels = labels.detach().cpu()
11
+ log_t = torch.zeros(1, requires_grad=True)
12
+ opt = torch.optim.LBFGS([log_t], max_iter=max_iter, lr=lr)
13
+ labels = labels.long()
14
+
15
+ def closure():
16
+ opt.zero_grad()
17
+ loss = torch.nn.functional.cross_entropy(logits / log_t.exp(), labels)
18
+ loss.backward()
19
+ return loss
20
+
21
+ opt.step(closure)
22
+ return float(log_t.exp().item())
oev/convert.py ADDED
@@ -0,0 +1,61 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ AG_LABELS = ["world", "sports", "business", "sci/tech"]
5
+ EMOTION_LABELS = ["sadness", "joy", "love", "anger", "fear", "surprise"]
6
+
7
+
8
+ def convert_rows(rows, labels, domain, qname, instructions):
9
+ for i, r in enumerate(rows):
10
+ yield {
11
+ "id": f"{domain}-{i:06d}",
12
+ "domain": domain,
13
+ "state": r["text"],
14
+ "questions": [
15
+ {
16
+ "name": qname,
17
+ "type": "choice",
18
+ "options": list(labels),
19
+ "answer": labels[r["label"]],
20
+ "instructions": instructions,
21
+ }
22
+ ],
23
+ }
24
+
25
+
26
+ def download_ag_news():
27
+ from datasets import load_dataset
28
+
29
+ return load_dataset("fancyzhx/ag_news")
30
+
31
+
32
+ def download_emotion():
33
+ from datasets import load_dataset
34
+
35
+ return load_dataset("dair-ai/emotion")
36
+
37
+
38
+ def build_domain(hf_split_rows, labels, domain, qname, instructions):
39
+ return list(convert_rows(hf_split_rows, labels, domain, qname, instructions))
40
+
41
+
42
+ def write_jsonl(rows, path):
43
+ p = Path(path)
44
+ p.parent.mkdir(parents=True, exist_ok=True)
45
+ with open(p, "w", encoding="utf-8") as f:
46
+ for r in rows:
47
+ f.write(json.dumps(r) + "\n")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ ag = download_ag_news()
52
+ em = download_emotion()
53
+ ag_train = build_domain(ag["train"], AG_LABELS, "ag_news", "topic", "Which topic does this news article belong to?")
54
+ write_jsonl(ag_train[:-2000], "data/ag_news/train.jsonl")
55
+ write_jsonl(ag_train[-2000:], "data/ag_news/valid.jsonl")
56
+ write_jsonl(build_domain(ag["test"], AG_LABELS, "ag_news", "topic", "Which topic does this news article belong to?"), "data/ag_news/test.jsonl")
57
+ em_train = build_domain(em["train"], EMOTION_LABELS, "emotion", "emotion", "Which emotion does this text express?")
58
+ write_jsonl(em_train[:-1000], "data/emotion/train.jsonl")
59
+ write_jsonl(em_train[-1000:], "data/emotion/valid.jsonl")
60
+ write_jsonl(build_domain(em["test"], EMOTION_LABELS, "emotion", "emotion", "Which emotion does this text express?"), "data/emotion/test.jsonl")
61
+ print("ag_news train/valid/test written; emotion train/valid/test written")
@@ -0,0 +1,88 @@
1
+ """Convert PolyAI/banking77 into OEV format.
2
+
3
+ Banking77: 13,083 customer-service queries, 77 fine-grained intents.
4
+ This is the high-cardinality test: 77 options in ONE choice question.
5
+ Laya's published number is 0.425 (head token budget squeezes each label
6
+ to 3-4 tokens); Jev publishes 0.870. Our anchor mechanism gives every
7
+ option full tokens, so this is our most winnable remaining benchmark row.
8
+
9
+ One choice question per state, all 77 intents as options with their
10
+ human-readable intent names as option text.
11
+
12
+ Usage:
13
+ python -m oev.convert_banking77
14
+ Writes:
15
+ data/banking77/train.jsonl (~10k cases, 1 question each)
16
+ data/banking77/valid.jsonl (1,000 cases)
17
+ data/banking77/test.jsonl (3,000 cases)
18
+ """
19
+
20
+ import json
21
+ from pathlib import Path
22
+
23
+
24
+ def download_banking77():
25
+ from datasets import load_dataset
26
+
27
+ try:
28
+ ds = load_dataset("PolyAI/banking77")
29
+ except RuntimeError:
30
+ # new datasets versions dropped script support; use the auto-converted parquet branch
31
+ ds = load_dataset("PolyAI/banking77", revision="refs/convert/parquet")
32
+ return ds["train"], ds["test"]
33
+
34
+
35
+ def intent_names():
36
+ """Official BANKING77 intent names in the dataset's label-id order,
37
+ fetched from the dataset's schema so it can never drift from the ids."""
38
+ import json
39
+ import urllib.request
40
+
41
+ url = "https://huggingface.co/datasets/PolyAI/banking77/resolve/main/dataset_infos.json"
42
+ data = json.load(urllib.request.urlopen(url, timeout=30))
43
+ names = list(data.values())[0]["features"]["label"]["names"]
44
+ assert len(names) == 77, f"expected 77 official intents, got {len(names)}"
45
+ return names
46
+
47
+
48
+ def write_jsonl(rows, path):
49
+ p = Path(path)
50
+ p.parent.mkdir(parents=True, exist_ok=True)
51
+ with open(p, "w", encoding="utf-8") as f:
52
+ for r in rows:
53
+ f.write(json.dumps(r) + "\n")
54
+
55
+
56
+ def convert(rows, names):
57
+ """One case per query: a single choice question with ALL 77 intents."""
58
+ n = len(names)
59
+ for r in rows:
60
+ label = int(r["label"])
61
+ if not (0 <= label < n):
62
+ continue
63
+ yield {
64
+ "id": f"b77-{r.get('id', label)}",
65
+ "domain": "banking77",
66
+ "state": r["text"],
67
+ "questions": [{
68
+ "name": "intent",
69
+ "type": "choice",
70
+ "instructions": "Which banking intent does this customer query belong to? Choose the single closest match.",
71
+ "options": names,
72
+ "answer": names[label],
73
+ }],
74
+ }
75
+
76
+
77
+ if __name__ == "__main__":
78
+ train, test = download_banking77()
79
+ names = intent_names()
80
+ assert len(names) == 77, f"expected 77 intents, got {len(names)}"
81
+
82
+ train_rows = list(convert(train, names))
83
+ # carve a validation split off the train pool
84
+ write_jsonl(train_rows[:-1000], "data/banking77/train.jsonl")
85
+ write_jsonl(train_rows[-1000:], "data/banking77/valid.jsonl")
86
+ test_rows = list(convert(test, names))
87
+ write_jsonl(test_rows, "data/banking77/test.jsonl")
88
+ print(f"banking77 train/valid/test written: {len(train_rows) - 1000}/{1000}/{len(test_rows)} cases (77 options each)")
oev/convert_typed.py ADDED
@@ -0,0 +1,124 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ NOUL_OPTIONS = ["no", "yes"]
5
+
6
+
7
+ def render_state(raw):
8
+ try:
9
+ obj = json.loads(raw) if isinstance(raw, str) else raw
10
+ except (json.JSONDecodeError, TypeError):
11
+ return str(raw)
12
+ if isinstance(obj, str):
13
+ return obj
14
+ if isinstance(obj, dict):
15
+ return "\n".join(f"{k}: {v}" for k, v in obj.items())
16
+ return str(obj)
17
+
18
+
19
+ def score_levels(gold_q, criteria):
20
+ probs = gold_q.get("probabilities", {})
21
+ int_levels = [int(k) for k in probs if str(k).lstrip("-").isdigit()]
22
+ n = len(criteria) if isinstance(criteria, list) else 4
23
+ label = int(gold_q["label"])
24
+ return max(n, label + 1, (max(int_levels) + 1 if int_levels else 0))
25
+
26
+
27
+ def soft_target(gold_q, options):
28
+ probs = gold_q.get("probabilities", {})
29
+ total = 0.0
30
+ target = []
31
+ for o in options:
32
+ p = probs.get(o)
33
+ if p is None and o == "no":
34
+ p = probs.get("false")
35
+ elif p is None and o == "yes":
36
+ p = probs.get("true")
37
+ p = float(p) if p is not None else 0.0
38
+ target.append(p)
39
+ total += p
40
+ if total <= 0.0:
41
+ return None
42
+ return [p / total for p in target]
43
+
44
+
45
+ def _enrich(option, desc):
46
+ d = (desc or "").strip()
47
+ return f"{option}: {d}" if d else option
48
+
49
+
50
+ def convert_question(qname, q, gold_q):
51
+ t = q["type"]
52
+ crit = q.get("criteria", {})
53
+ base = {"name": qname, "type": t, "instructions": q.get("instructions", t)}
54
+ if t == "choice":
55
+ keys = list(crit.keys())
56
+ if str(gold_q["label"]) not in keys:
57
+ return None
58
+ target = soft_target(gold_q, keys)
59
+ options = [_enrich(k, crit[k]) for k in keys]
60
+ answer = options[keys.index(str(gold_q["label"]))]
61
+ elif t == "noul":
62
+ options = NOUL_OPTIONS
63
+ target = soft_target(gold_q, options)
64
+ answer = "yes" if str(gold_q["label"]).lower() == "true" else "no"
65
+ elif t == "score":
66
+ n = score_levels(gold_q, crit)
67
+ raw = [str(i) for i in range(n)]
68
+ target = soft_target(gold_q, raw)
69
+ descs = crit if isinstance(crit, list) else []
70
+ options = [_enrich(str(i), descs[i] if i < len(descs) else "") for i in range(n)]
71
+ label = int(gold_q["label"])
72
+ if label >= n:
73
+ return None
74
+ answer = options[label]
75
+ else:
76
+ return None
77
+ cq = {**base, "options": options, "answer": answer}
78
+ if target is not None:
79
+ cq["target"] = target
80
+ return cq
81
+
82
+
83
+ def convert_rows_typed(rows):
84
+ for row in rows:
85
+ questions = json.loads(row["questions"]) if isinstance(row["questions"], str) else row["questions"]
86
+ gold = json.loads(row["gold"]) if isinstance(row["gold"], str) else row["gold"]
87
+ qds = []
88
+ for qname, q in questions.items():
89
+ if qname not in gold:
90
+ continue
91
+ cq = convert_question(qname, q, gold[qname])
92
+ if cq is not None:
93
+ qds.append(cq)
94
+ if not qds:
95
+ continue
96
+ yield {
97
+ "id": f"typed-{row['id']}",
98
+ "domain": row.get("workflow", "typed"),
99
+ "state": render_state(row["state"]),
100
+ "questions": qds,
101
+ }
102
+
103
+
104
+ def download_typed():
105
+ from datasets import load_dataset
106
+
107
+ return load_dataset("LocalLLaMA/typed-decisions", "all")
108
+
109
+
110
+ def write_jsonl(rows, path):
111
+ p = Path(path)
112
+ p.parent.mkdir(parents=True, exist_ok=True)
113
+ with open(p, "w", encoding="utf-8") as f:
114
+ for r in rows:
115
+ f.write(json.dumps(r) + "\n")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ ds = download_typed()
120
+ train_rows = list(convert_rows_typed(ds["train"]))
121
+ write_jsonl(train_rows[:-200], "data/typed/train.jsonl")
122
+ write_jsonl(train_rows[-200:], "data/typed/valid.jsonl")
123
+ write_jsonl(list(convert_rows_typed(ds["test"])), "data/typed/test.jsonl")
124
+ print(f"typed train/valid/test written: {len(train_rows) - 200}/{200}/{len(ds['test'])} cases")