#!/usr/bin/env python3 """ Build a crafting dependency tree from minecraft-data and generate training examples. Creates: 1. knowledge/mc-data/recipe_tree.json — full item→recipe→ingredients tree 2. data/raw/recipe_training.jsonl — training examples about crafting chains """ import json from pathlib import Path from collections import defaultdict ROOT = Path(__file__).resolve().parent.parent KNOWLEDGE = ROOT / "knowledge" / "mc-data" # Load data items_raw = json.load(open(KNOWLEDGE / "items.json")) recipes_raw = json.load(open(KNOWLEDGE / "recipes.json")) blocks_raw = json.load(open(KNOWLEDGE / "blocks.json")) # Build ID→name maps id_to_name = {} name_to_id = {} for item in items_raw: iid = item["id"] name = item["name"] id_to_name[iid] = name name_to_id[name] = iid # Build recipe tree: item_name → list of recipes with ingredient names recipe_tree = {} for item_id_str, recipe_list in recipes_raw.items(): item_id = int(item_id_str) item_name = id_to_name.get(item_id, f"unknown_{item_id}") parsed_recipes = [] for recipe in recipe_list: ingredients = set() # Shaped recipes (inShape) if "inShape" in recipe: for row in recipe["inShape"]: if row is None: continue for cell in row: if cell is not None: if isinstance(cell, int): ingredients.add(id_to_name.get(cell, f"unknown_{cell}")) elif isinstance(cell, list): # Multiple options for this slot for opt in cell: if opt is not None: ingredients.add(id_to_name.get(opt, f"unknown_{opt}")) # Shapeless recipes (ingredients list) if "ingredients" in recipe: for ing in recipe["ingredients"]: if ing is not None: if isinstance(ing, int): ingredients.add(id_to_name.get(ing, f"unknown_{ing}")) elif isinstance(ing, list): for opt in ing: if opt is not None: ingredients.add(id_to_name.get(opt, f"unknown_{opt}")) result_count = recipe.get("result", {}).get("count", 1) if isinstance(recipe.get("result"), dict) else 1 if ingredients: parsed_recipes.append({ "ingredients": sorted(ingredients), "count": result_count, "shaped": "inShape" in recipe, }) if parsed_recipes: recipe_tree[item_name] = parsed_recipes # Build dependency chains (what do you need from scratch?) def get_full_chain(item_name, visited=None): """Recursively get all raw materials needed to craft an item.""" if visited is None: visited = set() if item_name in visited: return {"item": item_name, "raw": True} # circular dependency visited.add(item_name) if item_name not in recipe_tree: return {"item": item_name, "raw": True} # raw material (mined/found) recipe = recipe_tree[item_name][0] # use first recipe deps = [] for ing in recipe["ingredients"]: deps.append(get_full_chain(ing, visited.copy())) return { "item": item_name, "raw": False, "ingredients": recipe["ingredients"], "count": recipe["count"], "deps": deps, } def get_raw_materials(item_name, visited=None): """Get flat list of raw materials needed.""" if visited is None: visited = set() if item_name in visited: return [] visited.add(item_name) if item_name not in recipe_tree: return [item_name] raw = [] recipe = recipe_tree[item_name][0] for ing in recipe["ingredients"]: raw.extend(get_raw_materials(ing, visited.copy())) return raw # Save recipe tree with open(KNOWLEDGE / "recipe_tree.json", "w") as f: json.dump(recipe_tree, f, indent=2) print(f"Recipe tree: {len(recipe_tree)} craftable items") # Generate training examples examples = [] # Important crafting chains that players commonly ask about KEY_ITEMS = [ "crafting_table", "furnace", "chest", "torch", "stick", "wooden_pickaxe", "stone_pickaxe", "iron_pickaxe", "diamond_pickaxe", "wooden_sword", "stone_sword", "iron_sword", "diamond_sword", "iron_helmet", "iron_chestplate", "iron_leggings", "iron_boots", "diamond_helmet", "diamond_chestplate", "diamond_leggings", "diamond_boots", "shield", "bow", "arrow", "bed", "bucket", "compass", "clock", "enchanting_table", "anvil", "brewing_stand", "beacon", "glass", "glass_pane", "bookshelf", "ladder", "fence", "door", "rail", "powered_rail", "hopper", "piston", "sticky_piston", "observer", "dispenser", "dropper", "repeater", "comparator", "oak_planks", "oak_slab", "oak_stairs", "oak_door", "oak_fence", "iron_ingot", "gold_ingot", "netherite_ingot", "bread", "cake", "cookie", "golden_apple", "tnt", "firework_rocket", "map", "lantern", "campfire", ] for item_name in KEY_ITEMS: if item_name not in recipe_tree: continue recipe = recipe_tree[item_name][0] ingredients = recipe["ingredients"] count = recipe["count"] raw_mats = list(set(get_raw_materials(item_name))) # "How do I craft X" ing_str = ", ".join(ingredients) examples.append({ "id": f"recipe-{item_name}-craft", "source": "manual", "category": "info", "input": { "user_message": f"sudo how do I craft {item_name.replace('_', ' ')}", "server_context": {"server_type": "paper", "version": "1.21.x"}, }, "output": { "reasoning": f"Crafting recipe for {item_name} in 1.21.", "commands": [], "message": f"To craft {item_name.replace('_', ' ')} you need: {ing_str}. Makes {count}. Requires a crafting table." if recipe["shaped"] and len(ingredients) > 2 else f"To craft {item_name.replace('_', ' ')} you need: {ing_str}. Makes {count}.", "safety_flags": [], }, "metadata": {"difficulty": "easy", "validated": True, "extracted_from": "minecraft-data recipes", "risk_level": 3}, }) # "Give me everything I need to make X" give_cmds = [] for mat in sorted(set(ingredients)): give_cmds.append(f"give slingshooter08 minecraft:{mat} 64") # Also give crafting table if shaped recipe needs it if recipe["shaped"] and len(ingredients) > 2 and "crafting_table" not in ingredients: give_cmds.insert(0, "give slingshooter08 minecraft:crafting_table 1") examples.append({ "id": f"recipe-{item_name}-materials", "source": "manual", "category": "command_gen", "input": { "user_message": f"sudo give me everything I need to craft {item_name.replace('_', ' ')}", "server_context": {"server_type": "paper", "version": "1.21.x", "online_players": ["slingshooter08"]}, }, "output": { "reasoning": f"Player needs materials to craft {item_name}. Ingredients: {ing_str}. Giving materials + crafting table if needed.", "commands": give_cmds, "safety_flags": [], }, "metadata": {"difficulty": "medium", "validated": True, "extracted_from": "minecraft-data recipes", "risk_level": 3}, }) # "Give me everything I need to make X from scratch" (raw materials) if raw_mats != ingredients and len(raw_mats) > 0: raw_cmds = [] for mat in sorted(set(raw_mats)): raw_cmds.append(f"give slingshooter08 minecraft:{mat} 64") raw_cmds.insert(0, "give slingshooter08 minecraft:crafting_table 1") raw_str = ", ".join(sorted(set(raw_mats))) examples.append({ "id": f"recipe-{item_name}-scratch", "source": "manual", "category": "command_gen", "input": { "user_message": f"sudo give me everything I need to make {item_name.replace('_', ' ')} from scratch", "server_context": {"server_type": "paper", "version": "1.21.x", "online_players": ["slingshooter08"]}, }, "output": { "reasoning": f"Player needs raw materials to craft {item_name} from scratch. Full chain: {raw_str}.", "commands": raw_cmds, "safety_flags": [], }, "metadata": {"difficulty": "hard", "validated": True, "extracted_from": "minecraft-data recipes", "risk_level": 4}, }) # Smelting knowledge (furnace recipes) SMELTING = { "glass": {"input": "sand", "fuel": "coal"}, "iron_ingot": {"input": "raw_iron", "fuel": "coal"}, "gold_ingot": {"input": "raw_gold", "fuel": "coal"}, "stone": {"input": "cobblestone", "fuel": "coal"}, "smooth_stone": {"input": "stone", "fuel": "coal"}, "brick": {"input": "clay_ball", "fuel": "coal"}, "cooked_beef": {"input": "beef", "fuel": "coal"}, "cooked_porkchop": {"input": "porkchop", "fuel": "coal"}, "cooked_chicken": {"input": "chicken", "fuel": "coal"}, "dried_kelp": {"input": "kelp", "fuel": "coal"}, "charcoal": {"input": "oak_log", "fuel": "oak_log"}, } for result, recipe in SMELTING.items(): examples.append({ "id": f"smelt-{result}", "source": "manual", "category": "info", "input": { "user_message": f"sudo how do I make {result.replace('_', ' ')}", "server_context": {"server_type": "paper", "version": "1.21.x"}, }, "output": { "reasoning": f"Smelting recipe: {recipe['input']} + fuel in a furnace = {result}.", "commands": [], "message": f"Smelt {recipe['input'].replace('_', ' ')} in a furnace with {recipe['fuel'].replace('_', ' ')} as fuel to get {result.replace('_', ' ')}.", "safety_flags": [], }, "metadata": {"difficulty": "easy", "validated": True, "extracted_from": "smelting knowledge", "risk_level": 3}, }) # Version awareness examples.append({ "id": "recipe-version-note", "source": "manual", "category": "info", "input": { "user_message": "sudo are there any new items or recipes", "server_context": {"server_type": "paper", "version": "1.21.x"}, }, "output": { "reasoning": "The model should acknowledge it's trained on a specific version and recipes may change.", "commands": [], "message": f"I know {len(recipe_tree)} craftable items from Minecraft 1.21.x. New items and recipes are added with each update. If something doesn't work, it may have been added or changed in a newer version.", "safety_flags": [], }, "metadata": {"difficulty": "easy", "validated": True, "extracted_from": "version awareness", "risk_level": 3}, }) # Save training examples output = ROOT / "data" / "raw" / "recipe_training.jsonl" with open(output, "w") as f: for ex in examples: f.write(json.dumps(ex, ensure_ascii=False) + "\n") print(f"Training examples: {len(examples)}") print(f" Crafting info: {sum(1 for e in examples if 'craft' in e['id'])}") print(f" Material giving: {sum(1 for e in examples if 'materials' in e['id'])}") print(f" From scratch: {sum(1 for e in examples if 'scratch' in e['id'])}") print(f" Smelting: {sum(1 for e in examples if 'smelt' in e['id'])}") print(f"Saved to {output}")