File size: 16,318 Bytes
61b6fb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
"""Decision SFT v3: a balanced mix of public typed-decision data -> common case format (decisions.py).

    python scripts/decisions/prepare_mix.py --publico $DATA_ROOT/decisions/public \
        --out $DATA_ROOT/decisions/mix

Families and caps (questions), so no single source dominates:
  typed      typed-decisions train EN + our ES translation (publico/, prepare_public.py) + the
             independent ES translation of telepatia-ai/typed-decisions-pt-es (Portuguese dropped)
  jev        samatv256/jev-decisions-v1 general-clean-50k, tool selection (validation ids unchanged)
  tasksource tasksource/tasksource-jev-typed-decisions: only rows with license_use == commercial,
             at most --per-source questions from each of its sources, no source that is itself a
             typed-decisions or jev-decisions derivative (would leak the tests)
  n4ze3m     n4ze3m/typed-decisions-synth (MIT): soft targets from its teacher (3 samples, mean)
  helmo      helmo/synthetic-typed-decisions (MIT): score gold given as mean/variance -> discretised
             normal over the levels
Cleaning: options 2..26, no empty instructions, exact duplicate (state, question) pairs dropped,
telepatia train cases whose structure matches a typed-decisions TEST or VALIDATION case dropped (fingerprint of the
non-text leaves, which survive translation).
Outputs: train_<family>.jsonl, val_<family>.jsonl, test_telepatia_es.jsonl, test_tasksource.jsonl,
         STATS.json, SOURCES.md
"""
from __future__ import annotations

import argparse
import ast
import collections
import hashlib
import json
import math
import random
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from prepare_public import jev_cases, val_side, write  # noqa: E402

TELEPATIA = "telepatia-ai/typed-decisions-pt-es"
TASKSOURCE = "tasksource/tasksource-jev-typed-decisions"
N4ZE3M = "n4ze3m/typed-decisions-synth"
HELMO = "helmo/synthetic-typed-decisions"
JEV = "samatv256/jev-decisions-v1"


def js(x):
    return json.loads(x) if isinstance(x, str) else x


def dl(repo, f):
    from huggingface_hub import hf_hub_download
    return hf_hub_download(repo, f, repo_type="dataset")


def jsonl(path):
    return [json.loads(x) for x in open(path, encoding="utf-8") if x.strip()]


def norm(d):
    s = sum(d.values())
    return {k: v / s for k, v in d.items()} if s > 0 else None


def argmax(d):
    return max(d, key=d.get)


def n_questions(cases):
    return sum(len(c["questions"]) for c in cases)


def fingerprint(state):
    """Non-text leaves and key paths of a state: identical across translations of the same case."""
    out = []

    def walk(x, path):
        if isinstance(x, dict):
            for k in sorted(x):
                walk(x[k], path + "/" + str(k))
        elif isinstance(x, list):
            out.append("%s#%d" % (path, len(x)))
            for i, v in enumerate(x):
                walk(v, "%s[%d]" % (path, i))
        elif not isinstance(x, str):
            out.append("%s=%r" % (path, x))
    walk(state, "")
    return hashlib.sha1("|".join(out).encode()).hexdigest()


def parse_state(s):
    if not isinstance(s, str):
        return s
    for f in (json.loads, ast.literal_eval):
        try:
            v = f(s)
            if isinstance(v, (dict, list)):
                return v
        except Exception:  # noqa: BLE001
            pass
    return s


def valid(q):
    if not (q.get("instructions") or "").strip():
        return False
    if q["type"] == "choice":
        return 2 <= len(q.get("criteria") or {}) <= 26
    if q["type"] == "score":
        return 2 <= len(q.get("criteria") or []) <= 26
    return q["type"] == "noul"


# ------------------------------------------------------------------ telepatia (typed ES, another translation)
def telepatia(split, test_fps):
    out, dropped = [], 0
    for r in jsonl(dl(TELEPATIA, "data/%s.es.jsonl" % split)):
        state = parse_state(r["state"])
        if split == "train" and isinstance(state, dict) and fingerprint(state) in test_fps:
            dropped += 1
            continue
        qs, gold, soft = {}, {}, {}
        for qid, q in r["questions"].items():
            g = r["gold"][qid]
            if q["type"] == "noul":
                lab = str(g["label"]).lower()
            else:
                lab = str(g["label"])
            if not valid(q):
                continue
            qs[qid], gold[qid] = q, [lab]
            p = g.get("probabilities")
            if p:
                soft[qid] = {(str(k).lower() if q["type"] == "noul" else str(k)): float(v) for k, v in p.items()}
        if qs:
            out.append({"id": "telepatia-" + r["id"], "grupo": "telepatia/" + r["workflow"], "lang": "es",
                        "state": state, "questions": qs, "gold": gold, "suave": soft})
    return out, dropped


# ------------------------------------------------------------------ tasksource
def tasksource_rows(files, per_source, rng, leak_words=("typed-decisions", "typed_decisions", "jev")):
    """Reservoir of at most `per_source` commercial-licence questions per source."""
    import pyarrow.parquet as pq
    cols = ["state", "kind", "id", "options", "target", "question", "source", "license", "license_use", "variant"]
    seen_src = collections.Counter()
    keep = collections.defaultdict(list)
    skipped = collections.Counter()
    for f in files:
        pf = pq.ParquetFile(f)
        for b in pf.iter_batches(columns=cols, batch_size=50000):
            for r in b.to_pylist():
                src = r["source"] or "?"
                if r["license_use"] != "commercial":
                    skipped["licence"] += 1
                    continue
                if any(w in src.lower() for w in leak_words):
                    skipped["leak-source"] += 1
                    continue
                k = r["kind"]
                opts, tgt = r["options"] or [], r["target"] or []
                if k == "noul":
                    ok = len(tgt) == 1
                else:
                    ok = 2 <= len(opts) <= 26 and len(tgt) == len(opts) and sum(tgt) > 0
                if not ok or not (r["question"] or "").strip():
                    skipped["shape"] += 1
                    continue
                seen_src[src] += 1
                n = seen_src[src]
                if len(keep[src]) < per_source:
                    keep[src].append(r)
                else:
                    j = rng.randrange(n)
                    if j < per_source:
                        keep[src][j] = r
    return keep, skipped


def tasksource_case(r):
    k, opts, tgt = r["kind"], r["options"] or [], [float(x) for x in r["target"] or []]
    if k == "noul":
        p = min(max(tgt[0], 0.0), 1.0)
        q = {"type": "noul", "instructions": r["question"]}
        soft = {"true": p, "false": 1 - p}
    elif k == "choice":
        q = {"type": "choice", "instructions": r["question"], "criteria": {"o%d" % i: str(o) for i, o in enumerate(opts)}}
        soft = norm({"o%d" % i: v for i, v in enumerate(tgt)})
    else:
        q = {"type": "score", "instructions": r["question"], "criteria": [str(o) for o in opts]}
        soft = norm({str(i): v for i, v in enumerate(tgt)})
    if soft is None:
        return None
    return {"id": "ts-" + r["id"], "grupo": "tasksource/" + r["source"].split("/")[0], "lang": "en",
            "state": r["state"], "questions": {"q": q}, "gold": {"q": [argmax(soft)]}, "suave": {"q": soft},
            "license": r["license"]}


def tasksource(split, per_source, total, rng):
    from huggingface_hub import HfApi
    files = sorted(s.rfilename for s in HfApi().dataset_info(TASKSOURCE).siblings
                   if s.rfilename.startswith("data/%s-" % split) and s.rfilename.endswith(".parquet"))
    keep, skipped = tasksource_rows([dl(TASKSOURCE, f) for f in files], per_source, rng)
    cases = [c for rows in keep.values() for c in map(tasksource_case, rows) if c is not None]
    rng.shuffle(cases)
    return cases[:total], {"sources": len(keep), "skipped": dict(skipped), "available": len(cases)}


# ------------------------------------------------------------------ n4ze3m (teacher soft targets)
def n4ze3m(split):
    out = []
    for r in jsonl(dl(N4ZE3M, "data/%s.jsonl" % split)):
        qs_all, gold_all, teacher = js(r["questions"]), js(r["gold"]), js(r.get("teacher") or "{}") or {}
        qs, gold, soft = {}, {}, {}
        for qid, q in qs_all.items():
            if not valid(q) or qid not in gold_all:
                continue
            g, t = gold_all[qid], teacher.get(qid) or {}
            if q["type"] == "noul":
                lab = "true" if g in (True, "true", 1) else "false"
                s = {"true": float(t["noul"]), "false": 1 - float(t["noul"])} if "noul" in t else None
            else:
                lab = str(g)
                s = {str(k): float(v) for k, v in (t.get("probabilities") or {}).items()} or None
            qs[qid], gold[qid] = q, [lab]
            if s and norm(s):
                soft[qid] = norm(s)
        if qs:
            out.append({"id": "n4-" + r["state_id"], "grupo": "n4ze3m/" + str(r.get("domain", "?"))[:40], "lang": "en",
                        "state": parse_state(r["state"]) if r.get("state_is_json") else r["state"],
                        "questions": qs, "gold": gold, "suave": soft})
    return out


# ------------------------------------------------------------------ helmo
def helmo():
    out = []
    for i, r in enumerate(jsonl(dl(HELMO, "synthetic_train.jsonl"))):
        qs_all, gold_all = js(r["questions"]), js(r["gold"])
        qs, gold, soft = {}, {}, {}
        for qid, q in qs_all.items():
            if q["type"] == "noul" and not q.get("criteria"):
                q = {k: v for k, v in q.items() if k != "criteria"}
            if not valid(q) or qid not in gold_all:
                continue
            g = gold_all[qid]
            if q["type"] == "noul":
                p = float(g["noul"])
                s = {"true": p, "false": 1 - p}
            elif q["type"] == "choice":
                s = norm({str(k): float(v) for k, v in g["probabilities"].items() if str(k) in q["criteria"]})
            else:
                n = len(q["criteria"])
                mu, var = float(g["mean"]), max(float(g.get("variance") or 0.0), 0.05)
                s = norm({str(j): math.exp(-((j - mu) ** 2) / (2 * var)) for j in range(n)})
            if not s:
                continue
            qs[qid], gold[qid], soft[qid] = q, [argmax(s)], s
        if qs:
            out.append({"id": "helmo-%d" % i, "grupo": "helmo/" + str(r.get("topic", "?"))[:40], "lang": "en",
                        "state": r["state"], "questions": qs, "gold": gold, "suave": soft})
    return out


def dedupe(cases, seen):
    out = []
    for c in cases:
        st = c["state"] if isinstance(c["state"], str) else json.dumps(c["state"], ensure_ascii=False, sort_keys=True)
        qs = {}
        for qid, q in c["questions"].items():
            h = hashlib.sha1((st + "\x00" + json.dumps(q, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()
            if h not in seen:
                seen.add(h)
                qs[qid] = q
        if qs:
            out.append(dict(c, questions=qs, gold={k: c["gold"][k] for k in qs},
                            suave={k: v for k, v in (c.get("suave") or {}).items() if k in qs}))
    return out


def cap(cases, n_q, rng):
    cases = list(cases)
    rng.shuffle(cases)
    out, k = [], 0
    for c in cases:
        if k >= n_q:
            break
        out.append(c)
        k += len(c["questions"])
    return out


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--publico", type=Path, required=True, help="output of prepare_public.py")
    ap.add_argument("--out", type=Path, required=True)
    ap.add_argument("--per-source", type=int, default=80)
    ap.add_argument("--cap", type=int, default=18000, help="max train questions per non-typed family")
    ap.add_argument("--cap-tasksource", type=int, default=25000)
    ap.add_argument("--seed", type=int, default=17)
    a = ap.parse_args()
    a.out.mkdir(parents=True, exist_ok=True)
    rng = random.Random(a.seed)
    stats = {}
    seen = set()

    # tests first: their (state, question) pairs are reserved before any train data is deduped
    typed_test = jsonl(a.publico / "typed_test_en.jsonl") + jsonl(a.publico / "typed_test_es.jsonl")
    # telepatia's train is typed-decisions train translated, and that train holds our validation cases
    # (115 of 1,200): both test and validation states are excluded from it
    typed_val = jsonl(a.publico / "typed_val_en.jsonl")
    test_fps = {fingerprint(c["state"]) for c in typed_test + typed_val if isinstance(c["state"], dict)}
    tele_test, _ = telepatia("test", set())
    ts_test, ts_test_info = tasksource("test", 10, 3000, rng)
    for name, cases in (("test_telepatia_es", tele_test), ("test_tasksource", ts_test)):
        cases = dedupe(cases, seen)
        write(a.out / (name + ".jsonl"), cases)
        stats[name] = n_questions(cases)
    dedupe(typed_test, seen)

    # validation: one file per family (train_decisions weighs files equally)
    ts_val, _ = tasksource("validation", 6, 1500, rng)
    n4_val = n4ze3m("validation")
    he_all = helmo()
    he_val = [c for c in he_all if val_side(c["id"], 0.05)]
    vals = {"typed_en": jsonl(a.publico / "typed_val_en.jsonl"), "typed_es": jsonl(a.publico / "typed_val_es.jsonl"),
            "jev": jsonl(a.publico / "jev_val.jsonl"), "tasksource": ts_val, "n4ze3m": cap(n4_val, 1500, rng),
            "helmo": cap(he_val, 1000, rng)}
    for k, v in vals.items():
        v = dedupe(v, seen)
        write(a.out / ("val_%s.jsonl" % k), v)
        stats["val_" + k] = n_questions(v)

    # train
    tele_train, dropped = telepatia("train", test_fps)
    stats["telepatia_train_dropped_test_val_overlap"] = dropped
    from huggingface_hub import list_repo_files
    jev_files = sorted(f for f in list_repo_files(JEV, repo_type="dataset") if f.startswith("clean50k/data/") and f.endswith(".parquet"))
    jev_all = jev_cases([dl(JEV, f) for f in jev_files], 60000, random.Random(a.seed))
    jev_ids_val = {c["id"] for c in vals["jev"]}
    jev_train = [c for c in jev_all if not val_side(c["id"], 0.05) and c["id"] not in jev_ids_val]
    ts_train, ts_info = tasksource("train", a.per_source, 10 ** 9, rng)
    stats["tasksource_info"] = ts_info
    trains = {
        "typed_en": jsonl(a.publico / "typed_train_en.jsonl"),
        "typed_es": jsonl(a.publico / "typed_train_es.jsonl"),
        "typed_es_telepatia": tele_train,
        "jev": cap(jev_train, a.cap, rng),
        "tasksource": cap(ts_train, a.cap_tasksource, rng),
        "n4ze3m": cap(n4ze3m("train"), a.cap, rng),
        "helmo": cap([c for c in he_all if not val_side(c["id"], 0.05)], a.cap, rng),
    }
    types = collections.Counter()
    for k, v in trains.items():
        v = dedupe(v, seen)
        write(a.out / ("train_%s.jsonl" % k), v)
        stats["train_" + k] = n_questions(v)
        types.update(q["type"] for c in v for q in c["questions"].values())
    stats["train_types"] = dict(types)
    stats["train_total"] = sum(v for k, v in stats.items() if k.startswith("train_") and isinstance(v, int))
    lic = collections.Counter(c.get("license") for c in jsonl(a.out / "train_tasksource.jsonl"))
    stats["tasksource_train_licenses"] = dict(lic.most_common())
    (a.out / "STATS.json").write_text(json.dumps(stats, indent=2) + "\n")
    print(json.dumps(stats, indent=2))
    (a.out / "SOURCES.md").write_text(
        "# Sources (decision SFT v3 mix)\n\n"
        "- LocalLLaMA/typed-decisions @c76749ec (Apache-2.0) + our Spanish machine translation (apodex-mini).\n"
        "- telepatia-ai/typed-decisions-pt-es (Apache-2.0), Spanish files only.\n"
        "- samatv256/jev-decisions-v1, general-clean-50k (CC-BY-4.0, see its SOURCE_LICENSES.md).\n"
        "- tasksource/tasksource-jev-typed-decisions: rows with license_use == commercial only; per-row licences\n"
        "  kept in the `license` field (counts in STATS.json).\n"
        "- n4ze3m/typed-decisions-synth (MIT); helmo/synthetic-typed-decisions (MIT).\n")


if __name__ == "__main__":
    sys.exit(main())