Ines-1 / training /code /prepare_public.py
Endikavi's picture
Ines-1 RC1 (private staging; release commit b7f5644)
61b6fb9 verified
Raw History Blame Contribute Delete
9.77 kB
"""Public decision datasets -> common case format (see decisions.py), plus a Spanish version.
python scripts/decisions/prepare_public.py --out $DATA_ROOT/decisions/public \
--translator URL/v1/chat/completions --translator-model NAME [--jev-sample 10000]
typed-decisions (LocalLLaMA, Apache-2.0), pinned to c76749ec (the repo has moved since):
train 1,200 cases / 6,000 questions, test 400 / 2,000; choice, score and noul. Gold carries a
distribution (a ~4B teacher sampled 3 times) -> used as the soft target.
Validation = 10 % of train by id hash (same ids on the EN and ES sides).
jev-decisions-v1 (samatv256, CC-BY-4.0: keep attribution), config general-clean-50k:
agent tool selection, choice only, hard target. English only (translating would break tool
names and schemas). Up to 26 options; the long system policy is dropped, the user goal and
the last turns are kept. Random sample of --jev-sample, 5 % of it held out for validation.
Spanish: typed-decisions train and test translated by an LLM behind an OpenAI-compatible endpoint
(--translator, --translator-model; thinking disabled, temperature 0.3, so a rerun does not reproduce the
released translation byte for byte): only text values; keys, option ids and structure unchanged, checked,
else retried/dropped. The translator actually used is recorded in training/TRANSLATION_PROVENANCE.md.
Outputs (JSONL): typed_{train,val,test}_{en,es}.jsonl, jev_{train,val}.jsonl, SOURCES.md
"""
from __future__ import annotations
import argparse
import hashlib
import json
import random
import re
import sys
import threading
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
TYPED = ("LocalLLaMA/typed-decisions", "c76749ec58bd8c3d2ea706b31c333a9059c38f90")
JEV = ("samatv256/jev-decisions-v1", None)
def val_side(key, frac=0.1):
return int(hashlib.sha256(str(key).encode()).hexdigest(), 16) % 1000 < frac * 1000
def js(x):
return json.loads(x) if isinstance(x, str) else x
def typed_cases(parquet, split):
import pyarrow.parquet as pq
out = []
for r in pq.read_table(parquet).to_pylist():
qs, gold = js(r["questions"]), js(r["gold"])
g, soft = {}, {}
for qid, q in qs.items():
gq = gold[qid]
lab = gq["label"]
if q["type"] == "noul":
lab = str(lab).lower()
g[qid] = [str(lab)]
probs = gq.get("probabilities")
if probs:
soft[qid] = {str(k).lower() if q["type"] == "noul" else str(k): float(v) for k, v in probs.items()}
out.append({"id": r["id"], "grupo": "typed/" + r["workflow"], "lang": "en", "state": js(r["state"]),
"questions": qs, "gold": g, "suave": soft, "split": split})
return out
def jev_cases(files, sample, rng):
import pyarrow.parquet as pq
rows = []
for f in files:
rows += pq.read_table(f, columns=["id", "source", "state", "question", "answer_options", "target_index"]).to_pylist()
rng.shuffle(rows)
out = []
for r in rows:
opts = r["answer_options"] or []
if not 2 <= len(opts) <= 26 or r["target_index"] is None or r["target_index"] >= len(opts):
continue
st = r["state"] or {}
hist = st.get("history") or []
turns = []
for h in hist[-6:]:
txt = h.get("content") or h.get("text") or json.dumps({k: v for k, v in h.items() if k != "role"}, ensure_ascii=False)
turns.append("%s: %s" % (h.get("role", "?"), str(txt)[:600]))
state = {"user_goal": st.get("user_goal") or "", "recent_history": "\n".join(turns)}
crit, keys = {}, []
for i, o in enumerate(opts):
k = "%s_%d" % (re.sub(r"[^a-z0-9]+", "_", str(o.get("label") or "opt").lower()).strip("_")[:40], i)
crit[k] = "%s: %s" % (o.get("label"), (o.get("description") or "")[:300])
keys.append(k)
out.append({"id": "jev-" + str(r["id"]), "grupo": "jev/" + str(r["source"]).split("/")[-1][:40], "lang": "en",
"state": state, "questions": {"q": {"type": "choice", "instructions": r["question"], "criteria": crit}},
"gold": {"q": [keys[r["target_index"]]]}})
if len(out) >= sample:
break
return out
# ------------------------------------------------------------------ translation
def same_shape(a, b):
"""Same keys everywhere, same non-string leaves (numbers/bools/None), strings may change."""
if isinstance(a, dict):
return isinstance(b, dict) and set(a) == set(b) and all(same_shape(a[k], b[k]) for k in a)
if isinstance(a, list):
return isinstance(b, list) and len(a) == len(b) and all(same_shape(x, y) for x, y in zip(a, b))
if isinstance(a, str):
return isinstance(b, str)
return a == b
def translate_case(url, model, case, tries=3):
# noul questions may come without criteria (literal true/false): only what exists is sent
payload = {"state": case["state"],
"questions": {qid: {k: q[k] for k in ("instructions", "criteria") if q.get(k) is not None}
for qid, q in case["questions"].items()}}
prompt = ("Traduce al español TODOS los textos de este JSON (valores de tipo texto), con redacción natural "
"de España y parafraseando si suena mejor. NO cambies ninguna clave, ni los números, ni los "
"booleanos, ni la estructura, ni los identificadores técnicos (códigos, ids, emails, rutas). "
"Devuelve SOLO el JSON traducido.\n\n" + json.dumps(payload, ensure_ascii=False))
body = {"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": 4000,
"temperature": 0.3, "chat_template_kwargs": {"enable_thinking": False}}
for _ in range(tries):
try:
req = urllib.request.Request(url, json.dumps(body).encode(), {"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=300) as r:
txt = json.loads(r.read())["choices"][0]["message"]["content"] or ""
m = re.search(r"\{.*\}", txt, re.S)
tr = json.loads(m.group(0)) if m else None
if tr is not None and same_shape(payload, tr):
es = dict(case, lang="es", id=case["id"] + "-es", state=tr["state"], questions={
qid: dict(q, **tr["questions"][qid]) for qid, q in case["questions"].items()})
return es
except Exception:
pass
return None
def write(path, cases):
with open(path, "w", encoding="utf-8") as f:
for c in cases:
f.write(json.dumps(c, ensure_ascii=False) + "\n")
print(" %-26s %6d cases, %6d questions" % (Path(path).name, len(cases), sum(len(c["questions"]) for c in cases)))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", type=Path, required=True)
ap.add_argument("--translator", required=True, help="OpenAI-compatible chat completions URL")
ap.add_argument("--translator-model", required=True,
help="model name sent to --translator (historical value: see TRANSLATION_PROVENANCE.md)")
ap.add_argument("--threads", type=int, default=16)
ap.add_argument("--jev-sample", type=int, default=10000)
ap.add_argument("--seed", type=int, default=13)
a = ap.parse_args()
a.out.mkdir(parents=True, exist_ok=True)
rng = random.Random(a.seed)
from huggingface_hub import hf_hub_download, list_repo_files
tr_p = hf_hub_download(TYPED[0], "all/train-00000-of-00001.parquet", repo_type="dataset", revision=TYPED[1])
te_p = hf_hub_download(TYPED[0], "all/test-00000-of-00001.parquet", repo_type="dataset", revision=TYPED[1])
train_all, test = typed_cases(tr_p, "train"), typed_cases(te_p, "test")
train = [c for c in train_all if not val_side(c["id"])]
val = [c for c in train_all if val_side(c["id"])]
print("typed-decisions @%s:" % TYPED[1][:8])
write(a.out / "typed_train_en.jsonl", train)
write(a.out / "typed_val_en.jsonl", val)
write(a.out / "typed_test_en.jsonl", test)
jev_files = [f for f in list_repo_files(JEV[0], repo_type="dataset") if f.startswith("clean50k/data/") and f.endswith(".parquet")]
local = [hf_hub_download(JEV[0], f, repo_type="dataset") for f in sorted(jev_files)]
jev = jev_cases(local, a.jev_sample, rng)
print("jev-decisions-v1 (general-clean-50k):")
write(a.out / "jev_train.jsonl", [c for c in jev if not val_side(c["id"], 0.05)])
write(a.out / "jev_val.jsonl", [c for c in jev if val_side(c["id"], 0.05)])
print("translating typed-decisions to Spanish with %s ..." % a.translator, flush=True)
for name, cases in (("train", train), ("val", val), ("test", test)):
done, lock = [], threading.Lock()
with ThreadPoolExecutor(a.threads) as ex:
for es in ex.map(lambda c: translate_case(a.translator, a.translator_model, c), cases):
if es is not None:
with lock:
done.append(es)
print(" %s: %d of %d translated with identical structure" % (name, len(done), len(cases)), flush=True)
write(a.out / ("typed_%s_es.jsonl" % name), done)
(a.out / "SOURCES.md").write_text(
"# Sources\n\n"
"- LocalLLaMA/typed-decisions @%s (Apache-2.0). Spanish files: machine translation by %s.\n"
"- samatv256/jev-decisions-v1, config general-clean-50k (CC-BY-4.0, see its SOURCE_LICENSES.md;\n"
" upstream NVIDIA Nemotron agentic datasets). Sample of %d, reformatted.\n" % (TYPED[1], a.translator_model, a.jev_sample))
if __name__ == "__main__":
sys.exit(main())