Files
Mortdecai/training/scripts/generate_script_training.py
Mortdecai da8f557219 GPU scheduler, 14-tool architecture, plugin deployment, event dispatcher
GPU Scheduler (gpu.sethpc.xyz):
- Live dashboard with 4 GPUs, training monitor, loss sparklines
- Preset-based job scheduler with 3 triggers (time, finish_training, cost)
- Model selection per GPU, pipeline configuration
- Tool self-play and training pipeline types
- Behind Google OAuth, live-refresh without page reload

Tool Architecture (14 tools):
- 3 new tools: world.nearby_entities, memory.read, memory.write
- 7 script.* tools: write, validate, execute, read, list, delete, schedule
- ScriptManager: full mcfunction datapack CRUD with RCON validation
- Training data: 1,430 tool examples (up from 1,159)

Plugin Deployment (paper-ai-25567):
- WorldGuard 7.0.12, CoreProtect CE 23.1, EssentialsX 2.21.2, Vault 1.7.3
- Fresh greenfield world reset
- 104 RCON-validated plugin training examples

Event Dispatcher:
- Watches server log for deaths, joins, advancements, PvP kills
- Configurable trigger probability and cooldowns per event type
- Deployed to dev server, fires god_system prompts on events
- 21 event-response training examples

Training Infrastructure:
- train_lora.py: --save-steps 50, --resume from checkpoint
- run_training.sh: stops Ollama, activates conda, restarts after
- Passwordless sudo for ollama services on steel141
- Dev server added to MCSManager with autoStart

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 03:14:45 -04:00

394 lines
19 KiB
Python

#!/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": <int 0-5>, "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"<tool_call>\n{json.dumps({'name': name, 'arguments': args})}\n</tool_call>"}
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()