#!/usr/bin/env python3 # -*- coding: utf-8 -*- # /// script # dependencies = ["datasets","huggingface_hub","hf_xet","tqdm"] # /// import argparse, json, os, random, hashlib from pathlib import Path from datasets import load_dataset from huggingface_hub import HfApi, upload_folder, hf_hub_download from tqdm import tqdm def jd(obj): return json.dumps(obj, ensure_ascii=False, sort_keys=False, default=str) def clean(s, max_chars=12000): if s is None: return "" if not isinstance(s, str): s = jd(s) s = s.replace("\r\n", "\n").replace("\r", "\n").strip() if len(s) > max_chars: s = s[:max_chars].rstrip() + "\n...[TRUNCATED]" return s def compact(obj, max_chars=12000): s = jd(obj) if len(s) > max_chars: s = s[:max_chars].rstrip() + "...[TRUNCATED]" return s def role_of(m): r = str(m.get("role", "")).lower().strip() if r in ("human", "user"): return "user" if r in ("assistant", "gpt", "model"): return "assistant" if r in ("tool", "function", "observation"): return "tool" if r in ("system", "developer"): return "system" return r or "user" def content_of(m): for k in ("content", "text", "value", "message"): if k in m and m[k] is not None: return clean(m[k]) return "" def normalize_tool_call(c): if isinstance(c, str): try: c = json.loads(c) except Exception: return {"raw": c} if not isinstance(c, dict): return {"raw": c} fn = c.get("function") if isinstance(c.get("function"), dict) else c name = fn.get("name") or c.get("name") or c.get("tool_name") or c.get("tool") args = fn.get("arguments", c.get("arguments", c.get("args", c.get("parameters", {})))) if isinstance(args, str): try: args = json.loads(args) except Exception: pass out = {} if c.get("id"): out["id"] = c.get("id") if name: out["name"] = str(name) out["arguments"] = args if args is not None else {} return out def extract_tool_calls(m): calls = [] if isinstance(m.get("tool_calls"), list): calls.extend([normalize_tool_call(c) for c in m["tool_calls"]]) if m.get("function_call"): calls.append(normalize_tool_call(m["function_call"])) if m.get("tool_call"): calls.append(normalize_tool_call(m["tool_call"])) return [c for c in calls if c] def used_tool_names(conversations): names = set() for m in conversations if isinstance(conversations, list) else []: if not isinstance(m, dict): continue for c in extract_tool_calls(m): if c.get("name"): names.add(str(c["name"])) for k in ("name", "tool_name"): if m.get(k): names.add(str(m[k])) return names def summarize_tool(t, max_desc=220): if not isinstance(t, dict): return None fn = t.get("function") if isinstance(t.get("function"), dict) else t name = fn.get("name") or t.get("name") if not name: return None desc = clean(fn.get("description", ""), max_desc) params = fn.get("parameters", {}) or fn.get("arguments", {}) required, props = [], [] if isinstance(params, dict): required = params.get("required") or [] properties = params.get("properties") or params.get("arguments") or {} if isinstance(properties, dict): props = list(properties.keys())[:20] line = "- " + str(name) if desc: line += ": " + desc if required: line += " | required: " + ", ".join(map(str, required[:10])) elif props: line += " | fields: " + ", ".join(map(str, props[:16])) return line def build_system(tools, conversations, max_tools=20, max_chars=5000): used = used_tool_names(conversations) lines = ["You can call tools when needed.", "Use only the available tool names and copy arguments exactly.", "", "Available tools:"] selected = [] if isinstance(tools, list): for t in tools: line = summarize_tool(t) if not line: continue name = line[2:].split(":", 1)[0].split(" | ", 1)[0].strip() if used and name not in used: continue selected.append(line) if not selected: for t in tools[:max_tools]: line = summarize_tool(t) if line: selected.append(line) lines.extend(selected[:max_tools] if selected else ["- no_tool: No tool available"]) return clean("\n".join(lines), max_chars) def tool_call_block(calls): if len(calls) == 1: return "TOOL_CALL:\n" + compact(calls[0]) return "TOOL_CALLS:\n" + compact(calls) def tool_result_block(m): payload = {} for k in ("name", "tool_name", "tool_call_id", "id"): if m.get(k): payload[k] = m[k] c = content_of(m) if c: payload["content"] = c else: for k in ("result", "observation", "output", "data"): if m.get(k) is not None: payload[k] = m[k] break return "TOOL_RESULT:\n" + compact(payload or m) def normalize_row(row, source): conversations = row.get("conversations") or row.get("messages") tools = row.get("tools") or [] if not isinstance(conversations, list) or not conversations: return None out = [{"role": "system", "content": build_system(tools, conversations)}] saw_user = False saw_assistant = False saw_tool_call = False for m in conversations: if not isinstance(m, dict): continue r = role_of(m) if r == "system": c = content_of(m) if c: out.append({"role": "system", "content": c}) continue if r == "tool": out.append({"role": "user", "content": tool_result_block(m)}) saw_user = True continue if r == "assistant": content = content_of(m) calls = extract_tool_calls(m) if calls: saw_tool_call = True block = tool_call_block(calls) content = (content + "\n\n" + block).strip() if content else block if content: out.append({"role": "assistant", "content": content}) saw_assistant = True continue c = content_of(m) if c: out.append({"role": "user", "content": c}) saw_user = True merged = [] for m in out: if merged and merged[-1]["role"] == m["role"]: merged[-1]["content"] = clean(merged[-1]["content"] + "\n\n" + m["content"]) else: merged.append(m) if not saw_user or not saw_assistant: return None return {"messages": merged, "source": source, "category": "toolmind_tool_call" if saw_tool_call else "toolmind_no_tool_call"} def stable_key(obj): return hashlib.sha256(jd(obj.get("messages", [])).encode("utf-8")).hexdigest() def try_reuse(out_repo_id, out_dir): out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) try: train = hf_hub_download(repo_id=out_repo_id, filename="train.jsonl", repo_type="dataset") val = hf_hub_download(repo_id=out_repo_id, filename="validation.jsonl", repo_type="dataset") (out / "train.jsonl").write_bytes(Path(train).read_bytes()) (out / "validation.jsonl").write_bytes(Path(val).read_bytes()) print("REUSED", out_repo_id, flush=True) return True except Exception as e: print("NO REUSE:", repr(e)[:250], flush=True) return False def convert(a): out = Path(a.out_dir) out.mkdir(parents=True, exist_ok=True) if a.reuse and try_reuse(a.out_repo_id, a.out_dir): return out print("Loading", a.dataset, a.split, flush=True) ds = load_dataset(a.dataset, split=a.split) total = len(ds) limit = total if a.max_rows <= 0 else min(a.max_rows, total) print("Rows", total, "limit", limit, flush=True) rows, seen, counts = [], set(), {} for i in tqdm(range(limit), desc="convert"): obj = normalize_row(dict(ds[i]), f"{a.dataset}:{a.split}:{i}") if not obj: continue k = stable_key(obj) if k in seen: continue seen.add(k) rows.append(obj) counts[obj["category"]] = counts.get(obj["category"], 0) + 1 random.Random(a.seed).shuffle(rows) val_n = min(a.val_size, max(1, len(rows)//100)) val = rows[:val_n] train = rows[val_n:] with (out/"train.jsonl").open("w", encoding="utf-8") as f: for r in train: f.write(jd(r)+"\n") with (out/"validation.jsonl").open("w", encoding="utf-8") as f: for r in val: f.write(jd(r)+"\n") (out/"README.md").write_text("---\nlicense: apache-2.0\nlanguage:\n- en\ntask_categories:\n- text-generation\n---\n\n# ToolMind converted to Scugnizz format\n\n" + json.dumps(counts, ensure_ascii=False, indent=2), encoding="utf-8") print("DONE", out, "TRAIN", len(train), "VAL", len(val), flush=True) print(json.dumps(counts, ensure_ascii=False, indent=2), flush=True) if rows[:1]: print("SAMPLE", json.dumps(rows[0], ensure_ascii=False)[:2000], flush=True) return out def upload_dataset(folder, repo_id, private=False): token = os.environ.get("HF_TOKEN") or os.environ.get("UV_SCRIPT_HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN") api = HfApi(token=token) api.create_repo(repo_id, repo_type="dataset", private=private, exist_ok=True) upload_folder(repo_id=repo_id, repo_type="dataset", folder_path=str(folder), commit_message="Convert ToolMind to Scugnizz format", token=token) print("UPLOADED", repo_id, flush=True) def main(): p = argparse.ArgumentParser() p.add_argument("--dataset", default="mlx-community/ToolMind") p.add_argument("--split", default="graph_syn_datasets") p.add_argument("--max-rows", type=int, default=50000) p.add_argument("--val-size", type=int, default=1000) p.add_argument("--seed", type=int, default=20260709) p.add_argument("--out-dir", default="data/toolmind-scugnizz-converted") p.add_argument("--upload", action="store_true") p.add_argument("--out-repo-id", default="ProjectScugnizz/toolmind-scugnizz-converted") p.add_argument("--private", action="store_true") p.add_argument("--reuse", action="store_true") a = p.parse_args() folder = convert(a) if a.upload: upload_dataset(folder, a.out_repo_id, a.private) if __name__ == "__main__": main()