Text Generation
PEFT
Safetensors
English
Arabic
lora
vmware
vsphere
it-operations
tool-calling
agent
Instructions to use RabiaaAi/sanad with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use RabiaaAi/sanad with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Download scripts/make_all_data.py from RabiaaAi/sanad: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/RabiaaAi/sanad/resolve/main/scripts/make_all_data.py
- Command line
-
hf download hf://RabiaaAi/sanad/scripts/make_all_data.py
-
curl -L -o make_all_data.py https://huggingface.co/RabiaaAi/sanad/resolve/main/scripts/make_all_data.py
10.7 kB
| """Build every dataset Sanad needs, in one go. | |
| python scripts/make_all_data.py | |
| data/ | |
| operator/ operator_train.jsonl operator_val.jsonl operator_eval.jsonl (120 held-out scenarios) | |
| guardian/ guardian_train.jsonl guardian_val.jsonl guardian_eval.jsonl (252 review packets + expected decision) | |
| verifier/ verifier_train.jsonl verifier_val.jsonl verifier_eval.jsonl (re-checks + expected status) | |
| stats.json counts, label balance, diversity and length | |
| Each agent is trained separately (its own LoRA adapter on the same base model). | |
| Every row: {"messages": [...], "tools": [...], "meta": {"role": ..., ...}} | |
| Names of hosts, VMs, datastores, cluster and vCenter change in every episode. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import random | |
| import sys | |
| from collections import Counter | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| from sanad_sim import FAULTS, Simulator # noqa: E402 | |
| from sanad_sim.checks import check_grounding # noqa: E402 | |
| from sanad_sim.guardian_data import EVAL_MODES, MODE_CYCLE, GuardianBuilder # noqa: E402 | |
| from sanad_sim.teacher import ScriptedTeacher # noqa: E402 | |
| from sanad_sim.verifier_data import CASE_CYCLE, CASES as V_EVAL_CASES, VerifierBuilder # noqa: E402 | |
| FIDS = list(FAULTS) | |
| SEEDS = { # role -> split -> base seed (ranges never overlap) | |
| "operator": {"train": 0, "val": 500_000, "eval": 900_000}, | |
| "guardian": {"train": 1_100_000, "val": 1_500_000, "eval": 1_900_000}, | |
| "verifier": {"train": 2_100_000, "val": 2_500_000, "eval": 2_900_000}, | |
| } | |
| SIZES = {"operator": {"train": 70, "val": 8}, "guardian": {"train": 80, "val": 10}, | |
| "verifier": {"train": 40, "val": 5}} | |
| SPECIAL_SIZES = {"train": {"OK00": 150, "UNK00": 100}, "val": {"OK00": 20, "UNK00": 15}} | |
| P_TOOL_ERROR = 0.15 | |
| # operator | |
| def operator_row(fid, seed, lang, teacher): | |
| rng = random.Random(seed * 7919 + 13) | |
| probe = Simulator(seed).new_episode(fid, lang=lang) | |
| errs = None | |
| if fid not in ("UNK00",) and rng.random() < P_TOOL_ERROR: | |
| errs = [{"tool": probe.inj.evidence[0][0], "call": 1, | |
| "error": rng.choice(["timeout after 30s", "connection reset by vCenter", "503 Service Unavailable"])}] | |
| ep = Simulator(seed).new_episode(fid, lang=lang, tool_errors=errs) | |
| s = teacher.run(ep, rng) | |
| fa = s["first_action"] | |
| if fid == "UNK00": | |
| if check_grounding(s["final_answer"], ep.trace): | |
| return None, "grounding" | |
| else: | |
| chk = ep.code_checks(s["final_answer"], fa and fa["action"], fa and fa["args"]) | |
| if not chk["pass"]: | |
| return None, f"checks {[i['type'] for i in chk['issues']]}" | |
| for action, args in ep.inj.solution: | |
| ep.execute(action, args) | |
| if not ep.resolved(): | |
| return None, "fix did not resolve" | |
| msgs = ep.display(s["messages"]) | |
| # re-check grounding on what the model will actually see (after renaming) | |
| shown = [{"output": json.loads(m["content"])} for m in msgs if m["role"] == "tool"] | |
| shown += [{"tool": "propose_action", "args": tc["function"]["arguments"], "output": {}} | |
| for m in msgs if m["role"] == "assistant" for tc in m.get("tool_calls", []) | |
| if tc["function"]["name"] == "propose_action"] | |
| if fid != "UNK00" and check_grounding(msgs[-1]["content"], shown): | |
| return None, "grounding after rename" | |
| meta = {"role": "operator", "fault_id": fid, "seed": seed, "lang": lang, "tool_error": bool(errs), | |
| "ground_truth": ep.display(ep.ground_truth())} | |
| return {"messages": msgs, "tools": s["tools"], "meta": meta}, None | |
| def build_operator(split, teacher, drops): | |
| rows, base, n = [], SEEDS["operator"][split], SIZES["operator"][split] | |
| plan = [(fid, n) for fid in FIDS] + list(SPECIAL_SIZES[split].items()) | |
| for k, (fid, count) in enumerate(plan): | |
| for i in range(count): | |
| seed = base + k * 1000 + i | |
| row, why = operator_row(fid, seed, "ar" if i % 2 else "en", teacher) | |
| if row: | |
| rows.append(row) | |
| else: | |
| drops[f"operator {fid}: {why}"] += 1 | |
| return rows | |
| # guardian / verifier | |
| FALLBACK_MODES = ["wrong", "policy", "ignored_error", "correct"] | |
| def build_guardian(split, gb, drops): | |
| rows, base, n = [], SEEDS["guardian"][split], SIZES["guardian"][split] | |
| for k, fid in enumerate(FIDS): | |
| for i in range(n): | |
| seed = base + k * 1000 + i | |
| lang = "ar" if i % 2 else "en" | |
| mode = MODE_CYCLE[(i * 7 + k) % len(MODE_CYCLE)] # spread modes evenly in every split | |
| row = gb.build(fid, seed, lang, mode) | |
| for alt in FALLBACK_MODES: | |
| if row: | |
| break | |
| drops[f"guardian {fid}: {mode} -> {alt}"] += 1 | |
| row = gb.build(fid, seed, lang, alt) | |
| if row: | |
| rows.append(row) | |
| return rows | |
| def build_verifier(split, vb): | |
| rows, base, n = [], SEEDS["verifier"][split], SIZES["verifier"][split] | |
| for k, fid in enumerate(FIDS): | |
| for i in range(n): | |
| seed = base + k * 1000 + i | |
| case = CASE_CYCLE[(i * 4 + k) % len(CASE_CYCLE)] | |
| rows.append(vb.build(fid, seed, "ar" if (i + k) % 2 else "en", case)) | |
| return rows | |
| # eval sets | |
| def build_evals(teacher, gb, vb): | |
| ev_op = [] | |
| order = FIDS * 2 + random.Random(7).sample(FIDS, 16) + ["OK00"] * 10 + ["UNK00"] * 10 | |
| for j, fid in enumerate(order): | |
| seed = SEEDS["operator"]["eval"] + j | |
| lang = "ar" if j % 2 else "en" | |
| ep = Simulator(seed).new_episode(fid, lang=lang) | |
| s = teacher.run(Simulator(seed).new_episode(fid, lang=lang), random.Random(seed)) | |
| ev_op.append({"id": f"op-{j:03d}", "fault_id": fid, "seed": seed, "lang": lang, | |
| "messages": ep.display(s["messages"][:2]), "tools": s["tools"], | |
| "ground_truth": ep.display(ep.ground_truth()), | |
| "rebuild": f"Simulator({seed}).new_episode('{fid}', lang='{lang}')"}) | |
| ev_g = [] | |
| for k, fid in enumerate(FIDS): | |
| for i, mode in enumerate(EVAL_MODES): | |
| seed = SEEDS["guardian"]["eval"] + k * 1000 + i | |
| row = gb.build(fid, seed, "ar" if i % 2 else "en", mode) | |
| for alt in FALLBACK_MODES: | |
| if row: | |
| break | |
| row = gb.build(fid, seed, "ar" if i % 2 else "en", alt) | |
| if row: | |
| ev_g.append({"id": f"g-{len(ev_g):03d}", "fault_id": fid, "seed": seed, | |
| "lang": row["meta"]["lang"], "mode": row["meta"]["mode"], | |
| "messages": row["messages"][:2], "tools": row["tools"], | |
| "expected": json.loads(row["messages"][-1]["content"]), "gold_messages": row["messages"], | |
| "meta": row["meta"]}) | |
| ev_v = [] | |
| for k, fid in enumerate(FIDS): | |
| for i, case in enumerate(V_EVAL_CASES): | |
| seed = SEEDS["verifier"]["eval"] + k * 1000 + i | |
| lang = "ar" if (i + k) % 2 else "en" | |
| row = vb.build(fid, seed, lang, case) | |
| if row["meta"]["case"] != case: | |
| continue # this case does not exist for this fault | |
| ev_v.append({"id": f"v-{len(ev_v):03d}", "fault_id": fid, "seed": seed, "lang": lang, "case": case, | |
| "messages": row["messages"][:2], "tools": row["tools"], | |
| "expected": json.loads(row["messages"][-1]["content"]), "gold_messages": row["messages"], | |
| "meta": row["meta"]}) | |
| return ev_op, ev_g, ev_v | |
| # stats | |
| def tok_est(row) -> int: | |
| return int(len(json.dumps(row["messages"], ensure_ascii=False)) / 3.2 | |
| + len(json.dumps(row["tools"])) / 3.6) | |
| def diversity(rows): | |
| users = [r["messages"][1]["content"] for r in rows] | |
| finals = [r["messages"][-1]["content"] for r in rows] | |
| return {"unique_requests_pct": round(100 * len(set(users)) / len(users), 1), | |
| "unique_final_answers_pct": round(100 * len(set(finals)) / len(finals), 1)} | |
| def write(path: Path, rows): | |
| with open(path, "w", encoding="utf-8") as f: | |
| for r in rows: | |
| f.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| def main(): | |
| out = ROOT / "data" | |
| for d in ("operator", "guardian", "verifier"): | |
| (out / d).mkdir(parents=True, exist_ok=True) | |
| teacher, gb, vb = ScriptedTeacher(), GuardianBuilder(), VerifierBuilder() | |
| stats, drops = {}, Counter() | |
| allrows = {} | |
| for split in ("train", "val"): | |
| op = build_operator(split, teacher, drops) | |
| g = build_guardian(split, gb, drops) | |
| v = build_verifier(split, vb) | |
| for name, rows in (("operator", op), ("guardian", g), ("verifier", v)): | |
| write(out / name / f"{name}_{split}.jsonl", rows) | |
| toks = [tok_est(r) for r in rows] | |
| labels = Counter(r["meta"].get("label") or r["meta"]["fault_id"][:1] for r in rows) | |
| stats[f"{name}_{split}"] = { | |
| "rows": len(rows), "arabic_pct": round(100 * sum(r["meta"]["lang"] == "ar" for r in rows) / len(rows), 1), | |
| "labels": dict(labels) if name != "operator" else None, | |
| "tokens_avg": int(sum(toks) / len(toks)), "tokens_max": max(toks), "tokens_total": sum(toks), | |
| **diversity(rows)} | |
| allrows[split] = op + g + v | |
| print(split, {k: v["rows"] for k, v in stats.items() if k.endswith(split)}) | |
| ev_op, ev_g, ev_v = build_evals(teacher, gb, vb) | |
| write(out / "operator" / "operator_eval.jsonl", ev_op) | |
| write(out / "guardian" / "guardian_eval.jsonl", ev_g) | |
| write(out / "verifier" / "verifier_eval.jsonl", ev_v) | |
| stats["eval"] = {"operator": len(ev_op), "guardian": len(ev_g), "verifier": len(ev_v), | |
| "guardian_labels": dict(Counter(e["expected"]["decision"] for e in ev_g)), | |
| "verifier_labels": dict(Counter(e["expected"]["status"] for e in ev_v))} | |
| stats["dropped_or_replaced"] = dict(drops.most_common(25)) | |
| stats["train_total_rows"] = len(allrows["train"]) | |
| stats["train_total_tokens_est"] = sum(tok_est(r) for r in allrows["train"]) | |
| leak = sum("__BADVM__" in json.dumps(r, ensure_ascii=False) for r in allrows["train"]) | |
| stats["placeholder_leaks"] = leak | |
| (out / "stats.json").write_text(json.dumps(stats, indent=2, ensure_ascii=False)) | |
| print(json.dumps({k: stats[k] for k in ("eval", "train_total_rows", "train_total_tokens_est", | |
| "placeholder_leaks")}, ensure_ascii=False)) | |
| if __name__ == "__main__": | |
| main() | |