#!/usr/bin/env python3 """ Generate training data for the script.* tool family. Teaches the model to: 1. Write mcfunction scripts for complex builds and mechanics 2. Validate scripts before writing (catch errors, fix them) 3. Execute scripts at player positions 4. Read/list/delete scripts for management 5. Schedule tick/load functions for persistent effects 6. Chain: validate → fix errors → write → execute ~60 training examples covering the full script lifecycle. """ import json import random import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(PROJECT_ROOT)) from agent.tools.tool_schemas import qwen3_tools_block from agent.prompts.system_prompts import SYNTAX_RULES, RISK_GRADIENT OUTPUT_PATH = PROJECT_ROOT / "data" / "raw" / "script_tool_training.jsonl" TOOLS_BLOCK = qwen3_tools_block() SYSTEM = ( "You are a Minecraft 1.21 command translator with script writing abilities. " "You can write mcfunction scripts for complex builds, mechanics, and automations.\n\n" "PERMISSION LEVEL: 4 (generous). Only refuse level 0-1 actions.\n\n" "You have access to tools including a full script environment. For simple tasks " "(1-3 commands), use rcon.execute directly. For complex tasks (4+ commands, builds, " "mechanics), write a mcfunction script.\n\n" "SCRIPT WORKFLOW: validate → fix errors → write → execute.\n" "Always validate before writing. Fix any errors the validator catches.\n\n" "After all tool calls resolve, respond with JSON:\n" '{"risk_level": , "commands": ["cmd1", ...], "reasoning": "why"}\n\n' + SYNTAX_RULES + RISK_GRADIENT + "\n" + TOOLS_BLOCK ) PLAYERS = ["slingshooter08", "Ace13245", "TheBigBoss", "xXDragonSlayerXx"] def sys_msg(): return {"role": "system", "content": SYSTEM} def user_msg(text): return {"role": "user", "content": text} def tool_call(name, args): return {"role": "assistant", "content": f"\n{json.dumps({'name': name, 'arguments': args})}\n"} def tool_result(data): return {"role": "tool", "content": json.dumps(data)} def final_response(resp): return {"role": "assistant", "content": json.dumps(resp)} def generate_all(): examples = [] idx = 0 # ── Simple script writes (validate → write → execute) ── BUILDS = [ ("sudo build me a wooden house", "wooden_house", "Simple wooden house with door and roof", [ "fill ~-4 ~ ~-4 ~4 ~4 ~4 minecraft:oak_planks hollow", "fill ~-4 ~ ~-4 ~4 ~ ~4 minecraft:oak_planks", "fill ~-3 ~1 ~-4 ~3 ~3 ~-4 minecraft:air", "setblock ~0 ~1 ~-4 minecraft:oak_door[facing=north,half=lower]", "setblock ~0 ~2 ~-4 minecraft:oak_door[facing=north,half=upper]", "fill ~-3 ~1 ~4 ~-3 ~2 ~4 minecraft:glass_pane", "fill ~3 ~1 ~4 ~3 ~2 ~4 minecraft:glass_pane", "setblock ~0 ~3 ~0 minecraft:lantern[hanging=true]", ]), ("sudo create a pvp arena", "pvp_arena", "PvP arena with walls and starting positions", [ "fill ~-15 ~-1 ~-15 ~15 ~-1 ~15 minecraft:smooth_stone", "fill ~-15 ~ ~-15 ~15 ~5 ~-15 minecraft:stone_bricks", "fill ~-15 ~ ~15 ~15 ~5 ~15 minecraft:stone_bricks", "fill ~-15 ~ ~-15 ~-15 ~5 ~15 minecraft:stone_bricks", "fill ~15 ~ ~-15 ~15 ~5 ~15 minecraft:stone_bricks", "fill ~-14 ~ ~-14 ~14 ~4 ~14 minecraft:air", "setblock ~-10 ~ ~0 minecraft:red_concrete", "setblock ~10 ~ ~0 minecraft:blue_concrete", "fill ~-1 ~-1 ~-1 ~1 ~-1 ~1 minecraft:glowstone", ]), ("sudo make a nether portal room", "portal_room", "Decorated nether portal room", [ "fill ~-5 ~ ~-5 ~5 ~6 ~5 minecraft:blackstone hollow", "fill ~-4 ~ ~-4 ~4 ~5 ~4 minecraft:air", "fill ~-1 ~1 ~0 ~1 ~5 ~0 minecraft:obsidian", "fill ~0 ~1 ~0 ~0 ~4 ~0 minecraft:air", "setblock ~0 ~1 ~0 minecraft:nether_portal[axis=x]", "setblock ~0 ~2 ~0 minecraft:nether_portal[axis=x]", "setblock ~0 ~3 ~0 minecraft:nether_portal[axis=x]", "fill ~-4 ~-1 ~-4 ~4 ~-1 ~4 minecraft:polished_blackstone_bricks", "setblock ~-3 ~1 ~-3 minecraft:soul_lantern", "setblock ~3 ~1 ~-3 minecraft:soul_lantern", "setblock ~-3 ~1 ~3 minecraft:soul_lantern", "setblock ~3 ~1 ~3 minecraft:soul_lantern", ]), ("sudo build a watchtower", "watchtower", "Tall watchtower with ladder access", [ "fill ~-2 ~ ~-2 ~2 ~10 ~2 minecraft:cobblestone hollow", "fill ~-1 ~ ~-1 ~1 ~9 ~1 minecraft:air", "fill ~-2 ~10 ~-2 ~2 ~10 ~2 minecraft:oak_planks", "fill ~-2 ~11 ~-2 ~2 ~11 ~2 minecraft:oak_fence", "fill ~-1 ~11 ~-1 ~1 ~11 ~1 minecraft:air", "fill ~2 ~1 ~0 ~2 ~9 ~0 minecraft:ladder[facing=west]", "setblock ~0 ~12 ~0 minecraft:lantern", ]), ("sudo build a farm plot with water", "farm_plot", "9x9 farm with central water and tilled soil", [ "fill ~-4 ~-1 ~-4 ~4 ~-1 ~4 minecraft:farmland", "setblock ~0 ~-1 ~0 minecraft:water", "fill ~-4 ~ ~-4 ~4 ~ ~4 minecraft:air", "fill ~-5 ~-1 ~-5 ~5 ~-1 ~-5 minecraft:oak_fence", "fill ~-5 ~-1 ~5 ~5 ~-1 ~5 minecraft:oak_fence", "fill ~-5 ~-1 ~-5 ~-5 ~-1 ~5 minecraft:oak_fence", "fill ~5 ~-1 ~-5 ~5 ~-1 ~5 minecraft:oak_fence", "setblock ~-5 ~-1 ~0 minecraft:oak_fence_gate[facing=east]", ]), ("sudo make a mob grinder platform", "mob_grinder", "Mob spawning platform with water channels", [ "fill ~-8 ~ ~-8 ~8 ~ ~8 minecraft:cobblestone", "fill ~-7 ~1 ~-7 ~7 ~3 ~7 minecraft:air", "fill ~-8 ~1 ~-8 ~8 ~1 ~-8 minecraft:cobblestone", "fill ~-8 ~1 ~8 ~8 ~1 ~8 minecraft:cobblestone", "fill ~-8 ~1 ~-8 ~-8 ~1 ~8 minecraft:cobblestone", "fill ~8 ~1 ~-8 ~8 ~1 ~8 minecraft:cobblestone", "setblock ~0 ~-1 ~0 minecraft:air", "setblock ~0 ~-2 ~0 minecraft:hopper", "fill ~-8 ~ ~0 ~-1 ~ ~0 minecraft:water", "fill ~1 ~ ~0 ~8 ~ ~0 minecraft:water", ]), ("sudo create a trophy room", "trophy_room", "Display room with item frames and lighting", [ "fill ~-6 ~ ~-6 ~6 ~4 ~6 minecraft:smooth_quartz hollow", "fill ~-5 ~ ~-5 ~5 ~3 ~5 minecraft:air", "fill ~-6 ~-1 ~-6 ~6 ~-1 ~6 minecraft:polished_deepslate", "fill ~-5 ~2 ~-6 ~5 ~2 ~-6 minecraft:item_frame[facing=south]", "fill ~-5 ~2 ~6 ~5 ~2 ~6 minecraft:item_frame[facing=north]", "setblock ~-4 ~3 ~0 minecraft:lantern[hanging=true]", "setblock ~0 ~3 ~0 minecraft:lantern[hanging=true]", "setblock ~4 ~3 ~0 minecraft:lantern[hanging=true]", ]), ] for req, name, desc, cmds in BUILDS: player = random.choice(PLAYERS) msgs = [sys_msg(), user_msg(f"Player {player}: {req}")] # Validate first msgs.append(tool_call("script.validate", {"commands": cmds})) msgs.append(tool_result({"valid": True, "total": len(cmds), "passed": len(cmds), "errors": []})) # Write msgs.append(tool_call("script.write", {"name": name, "commands": cmds, "description": desc})) msgs.append(tool_result({"ok": True, "path": f"mortdecai:{name}", "lines": len(cmds)})) # Execute at player msgs.append(tool_call("script.execute", {"name": name, "as_player": player})) msgs.append(tool_result({"ok": True, "result": f"Executed {len(cmds)} commands from function mortdecai:{name}"})) resp = {"risk_level": 3, "commands": [f"function mortdecai:{name}"], "reasoning": f"Wrote and executed '{name}' script ({len(cmds)} commands) at {player}'s position."} msgs.append(final_response(resp)) examples.append({"id": f"script-build-{idx:03d}", "source": "script_training", "type": "script_write_execute", "messages": msgs}) idx += 1 # ── Validate-fix-write cycle (model catches and fixes errors) ── ERROR_SCENARIOS = [ ("sudo build a glass bridge", "glass_bridge", [ "fill ~0 ~-1 ~0 ~0 ~-1 ~20 minecraft:glass", "fill ~-1 ~ ~0 ~-1 ~ ~20 minecraft:glass_pane", "fill ~1 ~ ~0 ~1 ~ ~20 minecraft:glass_paine", # typo ], 2, "glass_paine", "Unknown block type: minecraft:glass_paine", [ "fill ~0 ~-1 ~0 ~0 ~-1 ~20 minecraft:glass", "fill ~-1 ~ ~0 ~-1 ~ ~20 minecraft:glass_pane", "fill ~1 ~ ~0 ~1 ~ ~20 minecraft:glass_pane", ]), ("sudo create an enchanting setup", "enchanting_room", [ "setblock ~0 ~ ~0 minecraft:enchanting_table", "fill ~-2 ~ ~-2 ~2 ~1 ~2 minecraft:bookshelf", "fill ~-1 ~ ~-1 ~1 ~1 ~1 minecraft:air", "setblock ~0 ~2 ~0 minecraft:lantern", "effect give @s minecraft:luck 600", # missing amplifier — not an error but let's say ], 4, "minecraft:luck", "Unknown effect: minecraft:luck", [ "setblock ~0 ~ ~0 minecraft:enchanting_table", "fill ~-2 ~ ~-2 ~2 ~1 ~2 minecraft:bookshelf", "fill ~-1 ~ ~-1 ~1 ~1 ~1 minecraft:air", "setblock ~0 ~2 ~0 minecraft:lantern", ]), ("sudo make a redstone door", "redstone_door", [ "setblock ~2 ~1 ~0 minecraft:sticky_pistons[facing=west]", # wrong ID "setblock ~2 ~ ~0 minecraft:redstone_block", ], 0, "minecraft:sticky_pistons", "Unknown block: minecraft:sticky_pistons", [ "setblock ~2 ~1 ~0 minecraft:sticky_piston[facing=west]", "setblock ~2 ~ ~0 minecraft:redstone_block", ]), ] for req, name, bad_cmds, err_line, err_cmd_fragment, err_msg, fixed_cmds in ERROR_SCENARIOS: player = random.choice(PLAYERS) msgs = [sys_msg(), user_msg(f"Player {player}: {req}")] # First validation fails msgs.append(tool_call("script.validate", {"commands": bad_cmds})) msgs.append(tool_result({ "valid": False, "total": len(bad_cmds), "passed": len(bad_cmds) - 1, "errors": [{"line": err_line + 1, "command": bad_cmds[err_line], "error": err_msg}] })) # Fix and re-validate msgs.append(tool_call("script.validate", {"commands": fixed_cmds})) msgs.append(tool_result({"valid": True, "total": len(fixed_cmds), "passed": len(fixed_cmds), "errors": []})) # Write fixed version msgs.append(tool_call("script.write", {"name": name, "commands": fixed_cmds})) msgs.append(tool_result({"ok": True, "path": f"mortdecai:{name}", "lines": len(fixed_cmds)})) # Execute msgs.append(tool_call("script.execute", {"name": name, "as_player": player})) msgs.append(tool_result({"ok": True, "result": "Executed commands"})) resp = {"risk_level": 3, "commands": [f"function mortdecai:{name}"], "reasoning": f"Validation caught an error. Fixed and wrote '{name}' script."} msgs.append(final_response(resp)) examples.append({"id": f"script-fix-{idx:03d}", "source": "script_training", "type": "script_validate_fix", "messages": msgs}) idx += 1 # ── Script management (list, read, delete) ── MGMT_SCENARIOS = [ ("sudo show me all my scripts", "list", None), ("sudo what's in the pvp_arena script?", "read", "pvp_arena"), ("sudo delete the old test script", "delete", "test_script"), ("sudo what scripts do I have?", "list", None), ("sudo show me the watchtower script", "read", "watchtower"), ("sudo remove the mob_grinder script", "delete", "mob_grinder"), ] for req, action, target in MGMT_SCENARIOS: player = random.choice(PLAYERS) msgs = [sys_msg(), user_msg(f"Player {player}: {req}")] if action == "list": msgs.append(tool_call("script.list", {})) msgs.append(tool_result({"scripts": [ {"name": "wooden_house", "lines": 8, "scheduled": "none"}, {"name": "pvp_arena", "lines": 9, "scheduled": "none"}, {"name": "portal_room", "lines": 12, "scheduled": "none"}, {"name": "ambient_particles", "lines": 3, "scheduled": "tick"}, ]})) resp = {"risk_level": 4, "commands": [], "reasoning": "Listed all scripts in the datapack."} elif action == "read": msgs.append(tool_call("script.read", {"name": target})) msgs.append(tool_result({"ok": True, "commands": ["fill ~-15 ~-1 ~-15 ~15 ~-1 ~15 minecraft:smooth_stone", "# ... more commands"], "lines": 9})) resp = {"risk_level": 4, "commands": [], "reasoning": f"Read the '{target}' script contents."} elif action == "delete": msgs.append(tool_call("script.delete", {"name": target})) msgs.append(tool_result({"ok": True})) resp = {"risk_level": 3, "commands": [], "reasoning": f"Deleted the '{target}' script."} msgs.append(final_response(resp)) examples.append({"id": f"script-mgmt-{idx:03d}", "source": "script_training", "type": "script_management", "messages": msgs}) idx += 1 # ── Tick/load scheduling ── SCHEDULE_SCENARIOS = [ ("sudo make ambient particles around spawn forever", "ambient_particles", "tick", [ "execute at @a run particle minecraft:cherry_leaves ~ ~2 ~ 3 1 3 0.02 5", ]), ("sudo set up scoreboards when the server starts", "init_scoreboards", "load", [ "scoreboard objectives add deaths deathCount Deaths", "scoreboard objectives add kills playerKillCount Kills", "scoreboard objectives setdisplay sidebar kills", ]), ("sudo make it rain XP orbs on everyone every tick", "xp_rain", "tick", [ "execute at @a run summon minecraft:experience_orb ~ ~3 ~ {Value:1}", ]), ("sudo initialize gamerules on server load", "init_gamerules", "load", [ "gamerule keepInventory true", "gamerule doDaylightCycle true", "gamerule announceAdvancements true", "gamerule playersSleepingPercentage 50", ]), ] for req, name, sched_type, cmds in SCHEDULE_SCENARIOS: player = random.choice(PLAYERS) msgs = [sys_msg(), user_msg(f"Player {player}: {req}")] # Validate msgs.append(tool_call("script.validate", {"commands": cmds})) msgs.append(tool_result({"valid": True, "total": len(cmds), "passed": len(cmds), "errors": []})) # Write msgs.append(tool_call("script.write", {"name": name, "commands": cmds, "description": req})) msgs.append(tool_result({"ok": True, "path": f"mortdecai:{name}", "lines": len(cmds)})) # Schedule msgs.append(tool_call("script.schedule", {"name": name, "type": sched_type})) msgs.append(tool_result({"ok": True})) resp = {"risk_level": 3 if sched_type == "load" else 2, "commands": [f"function mortdecai:{name}"], "reasoning": f"Wrote '{name}' and scheduled it to run on {sched_type}."} msgs.append(final_response(resp)) examples.append({"id": f"script-sched-{idx:03d}", "source": "script_training", "type": "script_schedule", "messages": msgs}) idx += 1 # ── Direct rcon for simple tasks (model should NOT use scripts for 1-2 commands) ── SIMPLE = [ ("sudo give me a diamond", ["give {p} minecraft:diamond 1"], "Simple give — no script needed."), ("sudo set time to day", ["time set day"], "Single command, no script needed."), ("sudo kill nearby zombies", ["kill @e[type=minecraft:zombie,distance=..30]"], "One command, direct execution is better."), ] for req, cmds, reasoning in SIMPLE: player = random.choice(PLAYERS) msgs = [sys_msg(), user_msg(f"Player {player}: {req}")] for cmd in cmds: resolved = cmd.replace("{p}", player) msgs.append(tool_call("rcon.execute", {"command": resolved})) msgs.append(tool_result({"success": True, "result": "Command executed successfully"})) resp = {"risk_level": 3, "commands": [c.replace("{p}", player) for c in cmds], "reasoning": reasoning} msgs.append(final_response(resp)) examples.append({"id": f"script-simple-{idx:03d}", "source": "script_training", "type": "direct_rcon", "messages": msgs}) idx += 1 # ── Edit cycle: read → modify → validate → write ── EDIT_SCENARIOS = [ ("sudo add lighting to the pvp arena", "pvp_arena", ["fill ~-15 ~-1 ~-15 ~15 ~-1 ~15 minecraft:smooth_stone", "fill ~-15 ~ ~-15 ~15 ~5 ~-15 minecraft:stone_bricks"], ["fill ~-15 ~-1 ~-15 ~15 ~-1 ~15 minecraft:smooth_stone", "fill ~-15 ~ ~-15 ~15 ~5 ~-15 minecraft:stone_bricks", "setblock ~-10 ~4 ~0 minecraft:lantern[hanging=true]", "setblock ~0 ~4 ~0 minecraft:lantern[hanging=true]", "setblock ~10 ~4 ~0 minecraft:lantern[hanging=true]"], "Read existing arena script, added hanging lanterns for lighting."), ("sudo add a roof to the wooden house", "wooden_house", ["fill ~-4 ~ ~-4 ~4 ~4 ~4 minecraft:oak_planks hollow"], ["fill ~-4 ~ ~-4 ~4 ~4 ~4 minecraft:oak_planks hollow", "fill ~-5 ~4 ~-5 ~5 ~4 ~5 minecraft:oak_slab", "fill ~-4 ~5 ~-4 ~4 ~5 ~4 minecraft:oak_slab"], "Read existing house script, added oak slab roof."), ] for req, name, old_cmds, new_cmds, reasoning in EDIT_SCENARIOS: player = random.choice(PLAYERS) msgs = [sys_msg(), user_msg(f"Player {player}: {req}")] # Read existing msgs.append(tool_call("script.read", {"name": name})) msgs.append(tool_result({"ok": True, "commands": old_cmds, "lines": len(old_cmds)})) # Validate new version msgs.append(tool_call("script.validate", {"commands": new_cmds})) msgs.append(tool_result({"valid": True, "total": len(new_cmds), "passed": len(new_cmds), "errors": []})) # Overwrite msgs.append(tool_call("script.write", {"name": name, "commands": new_cmds})) msgs.append(tool_result({"ok": True, "path": f"mortdecai:{name}", "lines": len(new_cmds)})) # Execute msgs.append(tool_call("script.execute", {"name": name, "as_player": player})) msgs.append(tool_result({"ok": True, "result": "Executed commands"})) resp = {"risk_level": 3, "commands": [f"function mortdecai:{name}"], "reasoning": reasoning} msgs.append(final_response(resp)) examples.append({"id": f"script-edit-{idx:03d}", "source": "script_training", "type": "script_edit", "messages": msgs}) idx += 1 return examples def main(): print("Generating script tool training data...") examples = generate_all() counts = {} for ex in examples: t = ex["type"] counts[t] = counts.get(t, 0) + 1 print(f"\nGenerated {len(examples)} examples:") for t, c in sorted(counts.items()): print(f" {t}: {c}") with open(OUTPUT_PATH, "w") as f: for ex in examples: f.write(json.dumps(ex, ensure_ascii=False) + "\n") print(f"\nWritten to {OUTPUT_PATH}") if __name__ == "__main__": main()