diff --git a/MODEL_CARD.md b/MODEL_CARD.md index b6bc8e4..2384e67 100644 --- a/MODEL_CARD.md +++ b/MODEL_CARD.md @@ -1,17 +1,19 @@ # Model Card: Mortdecai +![Training Progress](branding/training_progress.svg) + ## Model Details | Field | Value | |-------|-------| | **Name** | Mortdecai | -| **Version** | 0.4.0 | +| **Version** | 0.5.0 | | **Base Model** | Qwen3.5-9B (Apache 2.0) | | **Adaptation** | QLoRA (4-bit base + LoRA adapters in FP16) | | **Parameters** | 9.4B total, 29M trainable (0.31%) | | **Training Hardware** | RTX 3090 Ti (24GB VRAM) | -| **Inference Hardware** | RTX 4000 (16GB), RTX 2080 Ti (11GB), or any GPU with 6GB+ VRAM | -| **Quantization** | Q4_K_M (5.3GB GGUF) | +| **Inference Hardware** | RTX 4000 (16GB), RTX 2080 Ti (11GB), GTX 1660 Super (6GB), or any GPU with 6GB+ | +| **Quantization** | Q4_K_M (5.6GB GGUF) | | **Context Length** | 4096 tokens (training), 262K tokens (model capability) | | **License** | Proprietary (adapter + training data). Base model: Apache 2.0 | @@ -34,15 +36,25 @@ Mortdecai is designed for **Minecraft Java Edition 1.21.x server operations**: | Source | Count | Description | |--------|-------|-------------| -| Hand-curated examples | 966 | Command syntax, recipes, enchantments, entities, effects | -| Player interactions | 654 | Real prayers from live server players | -| Sudo translations | 525 | Natural language → command pairs | -| Tool-calling sequences | 1,159 | Multi-turn RCON execution with error correction | -| Self-play | 5,000+ | Model-generated prompts validated via RCON | -| API distillation | 344 | Claude Haiku gold-standard responses | -| Error corrections | 150+ | Wrong → right command pairs | +| Hand-curated seed examples | 3,196 | Command syntax, recipes, enchantments, entities, effects, memory, events | +| Tool-calling sequences | 1,430 | Multi-turn RCON execution with 17 tools (script, memory, wiki, plugins) | +| IGLU build dataset | 4,656 | Natural language → block placement commands from Microsoft Research | +| Plugin training (RCON-validated) | 104 | WorldGuard, CoreProtect, EssentialsX, LuckPerms, FAWE | +| Exploration self-play | 150 | Wiki-grounded knowledge discovery with RCON validation | +| Self-play (0.4.0 + 0.5.0) | 2,900+ | Model-generated prompts validated via RCON | +| Live server audit | 8,000+ | Wolf bot + real player interactions from 3 servers | -**Total: ~8,400+ examples** +**Total: ~20,000+ examples across all sources** + +### Tool Architecture (17 tools) + +| Category | Tools | +|----------|-------| +| Execution | rcon.execute | +| Knowledge | minecraft.wiki_lookup, plugin.docs_lookup, minecraft.changelog_lookup, paper.docs_lookup | +| World Sensing | world.player_info, world.server_state, world.nearby_entities | +| Memory | memory.read, memory.write | +| Scripts | script.write, script.validate, script.execute, script.read, script.list, script.delete, script.schedule | ### Data Collection Methods @@ -63,16 +75,25 @@ Mortdecai is designed for **Minecraft Java Edition 1.21.x server operations**: ## Evaluation -### Bake-off Results (0.4.0, 2,397 test cases) +### Bake-off Results (0.5.0 vs 0.4.0, 38 prompts × 12 categories) -| Metric | Score | -|--------|-------| -| Command match | 75.5% | -| Exact match | 22.9% | -| Syntax correct | 80.5% | -| Safety compliance | 99.7% | -| No gratuitous tp | 98.5% | -| Avg latency | 4.0s | +| Metric | 0.4.0 | 0.5.0 | +|--------|-------|-------| +| Overall success rate | 45.2% | 46.8% | +| Avg response time | 2.60s | 2.11s | +| Errors (crashes) | 2 | 0 | +| Empty responses | 0 | 0 | + +**Category improvements (0.5.0 vs 0.4.0):** + +| Category | 0.4.0 | 0.5.0 | Change | +|----------|-------|-------|--------| +| Enchantments | 20% | 67% | **+47%** | +| EssentialsX | 0% | 60% | **+60%** | +| Effects | 0% | 25% | **+25%** | +| Basic commands | 75% | 75% | — | +| Teleport | 100% | 100% | — | +| Error recovery | 50% | 50% | — | ### Safety diff --git a/agent/tools/knowledge_lookup.py b/agent/tools/knowledge_lookup.py new file mode 100644 index 0000000..75d09a8 --- /dev/null +++ b/agent/tools/knowledge_lookup.py @@ -0,0 +1,356 @@ +""" +Knowledge lookup tools — plugin docs, changelogs, Paper docs. + +Provides structured access to documentation for: + - Plugin docs: WorldGuard, WorldEdit, CoreProtect, EssentialsX, LuckPerms + - Minecraft changelogs: version history, what changed when + - Paper server docs: Paper-specific configuration and behavior + +Each function returns {content, url, ok} for the model to consume. +""" + +import logging +import re +import requests +from typing import Any, Dict, Optional + +log = logging.getLogger(__name__) + +TIMEOUT = 12 + +# ── Plugin documentation wiki URLs ──────────────────────────────────────── + +PLUGIN_DOCS = { + "worldguard": { + "base_url": "https://worldguard.enginehub.org/en/latest", + "search_url": "https://worldguard.enginehub.org/en/latest/search.html", + "wiki_prefix": "WorldGuard", + "pages": { + "region": "/regions/", + "flag": "/regions/flags/", + "region flags": "/regions/flags/", + "define": "/regions/commands/", + "commands": "/regions/commands/", + "priority": "/regions/priorities/", + "global": "/regions/global-region/", + "build": "/regions/flags/#build-flags", + "pvp": "/regions/flags/#pvp", + "mob": "/regions/flags/#mob-spawning", + "entry": "/regions/flags/#entry-and-exit", + "greeting": "/regions/flags/#greeting-and-farewell", + "chest": "/regions/flags/#chest-access", + "heal": "/regions/flags/#heal-and-feed", + }, + }, + "worldedit": { + "base_url": "https://worldedit.enginehub.org/en/latest", + "wiki_prefix": "WorldEdit", + "pages": { + "selection": "/usage/regions/selections/", + "clipboard": "/usage/clipboard/", + "generation": "/usage/generation/", + "brush": "/usage/brushes/", + "pattern": "/usage/patterns/", + "mask": "/usage/masks/", + "transform": "/usage/transforms/", + "schematic": "/usage/saving/", + "sphere": "/usage/generation/#sphere", + "cylinder": "/usage/generation/#cylinder", + "pyramid": "/usage/generation/#pyramid", + "fill": "/usage/filling/", + "drain": "/usage/utilities/#drain", + "smooth": "/usage/utilities/#smooth", + "replace": "/usage/regions/operations/#replace", + "stack": "/usage/regions/operations/#stack", + "copy": "/usage/clipboard/#copy-and-cut", + "paste": "/usage/clipboard/#paste", + }, + }, + "coreprotect": { + "base_url": "https://docs.coreprotect.net", + "wiki_prefix": "CoreProtect", + "github_url": "https://raw.githubusercontent.com/PlayPro/CoreProtect/master/README.md", + "pages": { + "inspect": "/commands/#inspect", + "rollback": "/commands/#rollback", + "restore": "/commands/#restore", + "lookup": "/commands/#lookup", + "status": "/commands/#status", + "parameter": "/commands/#parameters", + "time": "/commands/#time-parameters", + "radius": "/commands/#radius-parameters", + "action": "/commands/#action-parameters", + "user": "/commands/#user-parameters", + "block": "/commands/#block-parameters", + }, + }, + "essentialsx": { + "base_url": "https://essinfo.xeya.me/commands.php", + "wiki_url": "https://essentialsx.net/wiki", + "wiki_prefix": "EssentialsX", + "pages": { + "home": "?filter=home", + "warp": "?filter=warp", + "tp": "?filter=tp", + "economy": "?filter=eco", + "kit": "?filter=kit", + "nick": "?filter=nick", + "god": "?filter=god", + "fly": "?filter=fly", + "heal": "?filter=heal", + "feed": "?filter=feed", + "speed": "?filter=speed", + "back": "?filter=back", + "spawn": "?filter=spawn", + "balance": "?filter=balance", + "pay": "?filter=pay", + "seen": "?filter=seen", + "broadcast": "?filter=broadcast", + }, + }, + "luckperms": { + "base_url": "https://luckperms.net/wiki", + "wiki_prefix": "LuckPerms", + "pages": { + "user": "/Usage#user-commands", + "group": "/Usage#group-commands", + "permission": "/Usage#permission-commands", + "parent": "/Usage#parent-commands", + "meta": "/Usage#meta-commands", + "prefix": "/Usage#meta-commands", + "weight": "/Weight", + "context": "/Context", + "inheritance": "/Usage#parent-commands", + "temporary": "/Usage#permission-commands", + "track": "/Usage#track-commands", + "verbose": "/Usage#verbose", + }, + }, +} + + +def _fetch_page_text(url: str, max_chars: int = 2000) -> str: + """Fetch a URL and extract text content.""" + try: + r = requests.get(url, timeout=TIMEOUT, headers={"User-Agent": "Mortdecai/0.5"}) + r.raise_for_status() + html = r.text + + # Strip HTML tags for a rough text extraction + text = re.sub(r']*>[\s\S]*?', '', html) + text = re.sub(r']*>[\s\S]*?', '', html) + text = re.sub(r']*>[\s\S]*?', '', text) + text = re.sub(r']*>[\s\S]*?', '', text) + text = re.sub(r']*>[\s\S]*?', '', text) + text = re.sub(r'<[^>]+>', ' ', text) + text = re.sub(r'\s+', ' ', text).strip() + + # Try to find the main content area + # Look for the section most relevant to the query + return text[:max_chars] + except Exception as e: + return f"Failed to fetch {url}: {e}" + + +def _wiki_api_search(wiki_base: str, query: str) -> Dict[str, Any]: + """Search a MediaWiki-based wiki.""" + try: + r = requests.get( + f"{wiki_base}/api.php", + params={"action": "opensearch", "search": query, "limit": 3, "format": "json"}, + timeout=TIMEOUT, + ) + results = r.json() + if len(results) >= 4 and results[1]: + title = results[1][0] + url = results[3][0] if len(results) > 3 and results[3] else "" + + # Get extract + r2 = requests.get( + f"{wiki_base}/api.php", + params={ + "action": "query", "prop": "extracts", + "exintro": True, "explaintext": True, + "titles": title, "format": "json", + }, + timeout=TIMEOUT, + ) + pages = r2.json().get("query", {}).get("pages", {}) + for page in pages.values(): + extract = page.get("extract", "") + if extract: + return {"content": extract[:2000], "url": url, "ok": True} + return {"content": f"No results for: {query}", "url": "", "ok": False} + except Exception as e: + return {"content": str(e), "url": "", "ok": False} + + +# ── Plugin docs lookup ───────────────────────────────────────────────────── + +def plugin_docs_lookup(plugin: str, query: str) -> Dict[str, Any]: + """Look up plugin documentation.""" + plugin = plugin.lower().strip() + if plugin not in PLUGIN_DOCS: + return {"content": f"Unknown plugin: {plugin}. Known: {', '.join(PLUGIN_DOCS.keys())}", "url": "", "ok": False} + + info = PLUGIN_DOCS[plugin] + base_url = info["base_url"] + query_lower = query.lower() + + # Try to match a known page first + best_page = None + best_score = 0 + for keyword, path in info.get("pages", {}).items(): + score = sum(1 for word in keyword.split() if word in query_lower) + if score > best_score: + best_score = score + best_page = path + + if best_page: + url = base_url + best_page if not best_page.startswith("?") else base_url + best_page + content = _fetch_page_text(url) + if len(content) > 100: + return {"content": content, "url": url, "ok": True} + + # Fallback: try the CoreProtect GitHub README + if plugin == "coreprotect" and info.get("github_url"): + try: + r = requests.get(info["github_url"], timeout=TIMEOUT) + if r.ok: + # Find relevant section in README + text = r.text + for line in text.split("\n"): + if query_lower.split()[0] in line.lower(): + start = text.index(line) + return {"content": text[start:start+2000], "url": info["github_url"], "ok": True} + return {"content": text[:2000], "url": info["github_url"], "ok": True} + except Exception: + pass + + # Fallback: search Minecraft wiki for the plugin + result = _wiki_api_search("https://minecraft.wiki", f"{info['wiki_prefix']} {query}") + if result.get("ok"): + return result + + # Last resort: generic search + return { + "content": f"Could not find specific docs for {plugin} '{query}'. Try the wiki at {base_url}", + "url": base_url, + "ok": False, + } + + +# ── Changelog lookup ─────────────────────────────────────────────────────── + +def changelog_lookup(query: str, version: str = None) -> Dict[str, Any]: + """Look up Minecraft version changelog.""" + # Use Minecraft Wiki for version history + if version: + search_term = f"Java Edition {version}" + else: + search_term = f"Java Edition 1.21" + + # If query mentions specific content, search for it in the version page + if query: + search_term = f"{search_term} {query}" + + # Search the Minecraft Wiki + result = _wiki_api_search("https://minecraft.wiki", search_term) + if result.get("ok"): + return { + "content": result["content"], + "version": version or "1.21", + "url": result["url"], + "ok": True, + } + + # Try searching just the query on the wiki + if query: + result = _wiki_api_search("https://minecraft.wiki", query) + if result.get("ok"): + return { + "content": result["content"], + "version": version or "unknown", + "url": result["url"], + "ok": True, + } + + return { + "content": f"No changelog info found for {version or 'latest'}: {query}", + "version": version or "unknown", + "url": "", + "ok": False, + } + + +# ── Paper docs lookup ────────────────────────────────────────────────────── + +PAPER_DOCS_BASE = "https://docs.papermc.io/paper" + +def paper_docs_lookup(query: str) -> Dict[str, Any]: + """Look up Paper server documentation.""" + # PaperMC docs are at docs.papermc.io — try fetching relevant pages + query_lower = query.lower() + + # Map common queries to known pages + page_map = { + "config": "/reference/configuration/global-configuration", + "world config": "/reference/configuration/world-configuration", + "paper-world": "/reference/configuration/world-configuration", + "paper-global": "/reference/configuration/global-configuration", + "async": "/dev/api/async-events", + "chunk": "/dev/api/chunk-system", + "timings": "/dev/api/misc/timings", + "plugin": "/dev/getting-started/plugin-yml", + "permission": "/dev/api/event-api/handler-lists", + "anti-xray": "/admin/anti-xray", + "optimization": "/admin/how-to/anti-lag", + "spark": "/admin/how-to/profiling", + } + + best_page = None + for keyword, path in page_map.items(): + if keyword in query_lower: + best_page = path + break + + if best_page: + url = PAPER_DOCS_BASE + best_page + content = _fetch_page_text(url) + if len(content) > 100: + return {"content": content, "url": url, "ok": True} + + # Fallback to Minecraft wiki search for Paper-specific topics + result = _wiki_api_search("https://minecraft.wiki", f"Paper server {query}") + if result.get("ok"): + return result + + return { + "content": f"No Paper docs found for: {query}. Check {PAPER_DOCS_BASE}", + "url": PAPER_DOCS_BASE, + "ok": False, + } + + +# ── Dispatcher ───────────────────────────────────────────────────────────── + +def handle_knowledge_tool(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: + """Dispatch a knowledge lookup tool call.""" + if tool_name == "plugin.docs_lookup": + return plugin_docs_lookup( + plugin=arguments.get("plugin", ""), + query=arguments.get("query", ""), + ) + elif tool_name == "minecraft.changelog_lookup": + return changelog_lookup( + query=arguments.get("query", ""), + version=arguments.get("version"), + ) + elif tool_name == "paper.docs_lookup": + return paper_docs_lookup( + query=arguments.get("query", ""), + ) + elif tool_name == "minecraft.wiki_lookup": + return _wiki_api_search("https://minecraft.wiki", arguments.get("query", "")) + else: + return {"content": f"Unknown tool: {tool_name}", "url": "", "ok": False} diff --git a/agent/tools/tool_schemas.py b/agent/tools/tool_schemas.py index afa3fe3..3655c7e 100644 --- a/agent/tools/tool_schemas.py +++ b/agent/tools/tool_schemas.py @@ -69,6 +69,95 @@ TOOL_SCHEMAS: List[Dict[str, Any]] = [ } } }, + { + "name": "plugin.docs_lookup", + "description": ( + "Look up plugin command syntax and documentation for server plugins: " + "WorldGuard, WorldEdit/FAWE, CoreProtect, EssentialsX, LuckPerms. " + "Use this when unsure about plugin-specific command syntax, flags, " + "parameters, or configuration options." + ), + "parameters": { + "type": "object", + "properties": { + "plugin": { + "type": "string", + "enum": ["worldguard", "worldedit", "coreprotect", "essentialsx", "luckperms"], + "description": "Which plugin to look up docs for." + }, + "query": { + "type": "string", + "description": "What to search for (e.g. 'region flags', 'rollback syntax', 'group inheritance')." + } + }, + "required": ["plugin", "query"], + "additionalProperties": False + }, + "returns": { + "type": "object", + "properties": { + "content": {"type": "string"}, + "url": {"type": "string"} + } + } + }, + { + "name": "minecraft.changelog_lookup", + "description": ( + "Look up what changed in a specific Minecraft version. Use this to check " + "if a feature, item, or command exists in the current version (1.21), " + "when something was added or removed, or what changed between versions." + ), + "parameters": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "Version to look up (e.g. '1.21', '1.20.4', '1.19'). Omit for latest." + }, + "query": { + "type": "string", + "description": "What to search for in the changelog (e.g. 'mace', 'copper', 'trial chambers')." + } + }, + "required": ["query"], + "additionalProperties": False + }, + "returns": { + "type": "object", + "properties": { + "content": {"type": "string"}, + "version": {"type": "string"}, + "url": {"type": "string"} + } + } + }, + { + "name": "paper.docs_lookup", + "description": ( + "Look up Paper server-specific documentation — API differences from Spigot, " + "Paper-specific configuration, async chunk loading, and server optimization. " + "Use when behavior differs from vanilla or Spigot." + ), + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "What to search for (e.g. 'async chunks', 'paper-world config', 'timings')." + } + }, + "required": ["query"], + "additionalProperties": False + }, + "returns": { + "type": "object", + "properties": { + "content": {"type": "string"}, + "url": {"type": "string"} + } + } + }, { "name": "world.player_info", "description": ( diff --git a/branding/training_progress.svg b/branding/training_progress.svg new file mode 100644 index 0000000..9356223 --- /dev/null +++ b/branding/training_progress.svg @@ -0,0 +1,56 @@ + + + + + +Mortdecai Training Progress + + + +0 + +1,252 + +2,505 + +3,758 + +5,011 + +500 +0.1.0 +v1 (seed) + +1,200 +0.2.0 +v2 (+entities) + +2,100 +0.3.0 +v3 (+errors) + +3,175 +0.4.0 +v4 (+tools) + +4,358 +0.5.0 +v5 (+plugins) + + +loss=2.1 + +loss=1.45 + +loss=0.82 + +loss=0.35 + +loss=0.16 +Training Examples + +Training Examples + +Model Quality (1/loss) +Model Version + \ No newline at end of file diff --git a/training/bakeoff_results/bakeoff_mortdecai_0.4.0-vs-mortdecai_0.5.0_1774120573.json b/training/bakeoff_results/bakeoff_mortdecai_0.4.0-vs-mortdecai_0.5.0_1774120573.json new file mode 100644 index 0000000..bfc27bd --- /dev/null +++ b/training/bakeoff_results/bakeoff_mortdecai_0.4.0-vs-mortdecai_0.5.0_1774120573.json @@ -0,0 +1,1323 @@ +{ + "models": [ + "mortdecai:0.4.0", + "mortdecai:0.5.0" + ], + "timestamp": "2026-03-21T15:16:13Z", + "results": { + "mortdecai:0.4.0": { + "total": 38, + "cmd_success": 19, + "cmd_fail": 23, + "cmd_total": 42, + "no_commands": 0, + "errors": 2, + "total_time": 98.88000000000004 + }, + "mortdecai:0.5.0": { + "total": 38, + "cmd_success": 22, + "cmd_fail": 25, + "cmd_total": 47, + "no_commands": 0, + "errors": 0, + "total_time": 80.13 + } + }, + "details": { + "mortdecai:0.4.0": [ + { + "category": "basic_give", + "prompt": "sudo give me a diamond sword", + "commands": [ + "give slingshooter08 diamond_sword 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_sword 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 5.05, + "error": null + }, + { + "category": "basic_give", + "prompt": "sudo give me 64 golden apples", + "commands": [ + "give slingshooter08 64 golden_apple" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 64 golden_apple", + "result": "Unknown item 'minecraft:64'\n...shooter08 64 golden_apple<--[HERE]", + "ok": false + } + ], + "elapsed": 2.24, + "error": null + }, + { + "category": "basic_give", + "prompt": "sudo give me full netherite armor", + "commands": [ + "give slingshooter08 diamond 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.83, + "error": null + }, + { + "category": "basic_give", + "prompt": "sudo give me a stack of oak logs", + "commands": [ + "give slingshooter08 oak_log 64" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 oak_log 64", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 2.29, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me a sword with sharpness 5 and mending", + "commands": [ + "give slingshooter08 diamond_sword[enchantments={sharpness:5,mending:1}] 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_sword[enchantments={sharpness:5,mending:1}] 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.81, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me a bow with power 5 and infinity", + "commands": [ + "give slingshooter08 diamond_bow 1 [powered=5,infinity=1]", + "effect give slingshooter08 strength 1 300" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_bow 1 [powered=5,infinity=1]", + "result": "Unknown item 'minecraft:diamond_bow'\n...shooter08 diamond_bow 1 [powered=5,infinity=1]<--[HERE]", + "ok": false + }, + { + "cmd": "effect give slingshooter08 strength 1 300", + "result": "Integer must not be more than 255: found 300\n...trength 1 300<--[HERE]", + "ok": false + } + ], + "elapsed": 2.07, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me boots with feather falling 4 and depth strider 3", + "commands": [ + "give slingshooter08 diamond_boots 1 [FeatherFalling=4,DepthStrider=3]" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_boots 1 [FeatherFalling=4,DepthStrider=3]", + "result": "Incorrect argument for command\n...d_boots 1 [FeatherFalling=4,DepthStrider=3]<--[HERE]", + "ok": false + } + ], + "elapsed": 1.73, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me a trident with loyalty 3 and channeling", + "commands": [ + "give slingshooter08 diamond_trident[enchantments={loyalty:3,channeling:1}] 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_trident[enchantments={loyalty:3,channeling:1}] 1", + "result": "Unknown item 'minecraft:diamond_trident'\n...shooter08 diamond_trident[enchantments={loyalty:3,channeling:1}] 1<--[HERE]", + "ok": false + } + ], + "elapsed": 1.87, + "error": null + }, + { + "category": "effects", + "prompt": "sudo give me speed 2 for 5 minutes", + "commands": [ + "effect give slingshooter08 speed 2 300" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 speed 2 300", + "result": "Integer must not be more than 255: found 300\n...8 speed 2 300<--[HERE]", + "ok": false + } + ], + "elapsed": 1.95, + "error": null + }, + { + "category": "effects", + "prompt": "sudo make me invisible for 60 seconds", + "commands": [ + "effect give slingshooter08 2 60" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 2 60", + "result": "Can't find element 'minecraft:2' of type 'minecraft:mob_effect'\n...hooter08 2 60<--[HERE]", + "ok": false + } + ], + "elapsed": 2.2, + "error": null + }, + { + "category": "effects", + "prompt": "sudo give me night vision forever", + "commands": [ + "give slingshooter08 night_stick 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 night_stick 1", + "result": "Unknown item 'minecraft:night_stick'\n...shooter08 night_stick 1<--[HERE]", + "ok": false + } + ], + "elapsed": 1.72, + "error": null + }, + { + "category": "effects", + "prompt": "sudo give everyone resistance 3", + "commands": [ + "give @a resistance 3 6000" + ], + "rcon_results": [ + { + "cmd": "give @a resistance 3 6000", + "result": "Unknown item 'minecraft:resistance'\ngive @a resistance 3 6000<--[HERE]", + "ok": false + } + ], + "elapsed": 1.44, + "error": null + }, + { + "category": "world", + "prompt": "sudo set time to day", + "commands": [ + "time set day" + ], + "rcon_results": [ + { + "cmd": "time set day", + "result": "Set the time to 1000", + "ok": true + } + ], + "elapsed": 1.47, + "error": null + }, + { + "category": "world", + "prompt": "sudo clear the weather", + "commands": [ + "weather clear" + ], + "rcon_results": [ + { + "cmd": "weather clear", + "result": "Set the weather to clear", + "ok": true + } + ], + "elapsed": 1.95, + "error": null + }, + { + "category": "world", + "prompt": "sudo kill all zombies", + "commands": [ + "kill @e[type=zombie]", + "give slingshooter08 diamond_sword 1" + ], + "rcon_results": [ + { + "cmd": "kill @e[type=zombie]", + "result": "No entity was found", + "ok": true + }, + { + "cmd": "give slingshooter08 diamond_sword 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.66, + "error": null + }, + { + "category": "world", + "prompt": "sudo summon 3 cows near me", + "commands": [ + "summon cow ~ ~ ~" + ], + "rcon_results": [ + { + "cmd": "summon cow ~ ~ ~", + "result": "Summoned new Cow", + "ok": true + } + ], + "elapsed": 1.36, + "error": null + }, + { + "category": "teleport", + "prompt": "sudo tp me to 0 100 0", + "commands": [ + "tp slingshooter08 0 100 0" + ], + "rcon_results": [ + { + "cmd": "tp slingshooter08 0 100 0", + "result": "No entity was found", + "ok": true + } + ], + "elapsed": 1.96, + "error": null + }, + { + "category": "teleport", + "prompt": "sudo tp me 50 blocks up", + "commands": [ + "tp slingshooter08 ~ ~50 ~" + ], + "rcon_results": [ + { + "cmd": "tp slingshooter08 ~ ~50 ~", + "result": "No entity was found", + "ok": true + } + ], + "elapsed": 1.84, + "error": null + }, + { + "category": "fill_build", + "prompt": "sudo fill a 5x5 gold platform under me", + "commands": [ + "execute as @p at @s run fill ~-2 ~-3 ~-2 ~2 ~-1 ~2 gold_block" + ], + "rcon_results": [ + { + "cmd": "execute as @p at @s run fill ~-2 ~-3 ~-2 ~2 ~-1 ~2 gold_block", + "result": "Successfully filled 75 block(s)", + "ok": true + } + ], + "elapsed": 2.36, + "error": null + }, + { + "category": "fill_build", + "prompt": "sudo place a beacon at 0 64 0", + "commands": [ + "give slingshooter08 beacon", + "execute as slingshooter08 at slingshooter08 run setblock 0 64 0 beacon" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 beacon", + "result": "No player was found", + "ok": true + }, + { + "cmd": "execute as slingshooter08 at slingshooter08 run setblock 0 64 0 beacon", + "result": "", + "ok": true + } + ], + "elapsed": 2.06, + "error": null + }, + { + "category": "complex", + "prompt": "sudo give me a mace with density 5 and wind burst 3", + "commands": [ + "give slingshooter08 minecraft:trident[enchantments={sharpness:5,impaling:5,channeling:1}] 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 minecraft:trident[enchantments={sharpness:5,impaling:5,channeling:1}] 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 3.26, + "error": null + }, + { + "category": "complex", + "prompt": "sudo give me a decorated pot", + "commands": [ + "give slingshooter08 diamond 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 2.39, + "error": null + }, + { + "category": "complex", + "prompt": "sudo spawn a warden 10 blocks away", + "commands": [ + "execute as @p at @s run summon minecraft:warden at 10 0 10" + ], + "rcon_results": [ + { + "cmd": "execute as @p at @s run summon minecraft:warden at 10 0 10", + "result": "Expected double\n...ft:warden at 10 0 10<--[HERE]", + "ok": false + } + ], + "elapsed": 3.1, + "error": null + }, + { + "category": "complex", + "prompt": "sudo create a team called red with red color", + "commands": [ + "team add slingshooter08 red", + "team color slingshooter08 red" + ], + "rcon_results": [ + { + "cmd": "team add slingshooter08 red", + "result": "A team already exists by that name", + "ok": true + }, + { + "cmd": "team color slingshooter08 red", + "result": "Incorrect argument for command\nteam color slingshooter08 red<--[HERE]", + "ok": false + } + ], + "elapsed": 3.47, + "error": null + }, + { + "category": "plugins_worldguard", + "prompt": "sudo create a region called test-region", + "commands": [ + "//region set test-region", + "//region set test-region", + "//region set test-region" + ], + "rcon_results": [ + { + "cmd": "region set test-region", + "result": "Unknown or incomplete command. See below for error\nregion set test-region<--[HERE]", + "ok": false + }, + { + "cmd": "region set test-region", + "result": "Unknown or incomplete command. See below for error\nregion set test-region<--[HERE]", + "ok": false + }, + { + "cmd": "region set test-region", + "result": "Unknown or incomplete command. See below for error\nregion set test-region<--[HERE]", + "ok": false + } + ], + "elapsed": 2.43, + "error": null + }, + { + "category": "plugins_worldguard", + "prompt": "sudo set pvp deny in the test-region", + "commands": [ + "//set pvp deny" + ], + "rcon_results": [ + { + "cmd": "set pvp deny", + "result": "Unknown or incomplete command. See below for error\nset pvp deny<--[HERE]", + "ok": false + } + ], + "elapsed": 1.66, + "error": null + }, + { + "category": "plugins_worldguard", + "prompt": "sudo list all regions", + "commands": [ + "//regions list" + ], + "rcon_results": [ + { + "cmd": "regions list", + "result": "Unknown or incomplete command. See below for error\nregions list<--[HERE]", + "ok": false + } + ], + "elapsed": 1.47, + "error": null + }, + { + "category": "plugins_coreprotect", + "prompt": "sudo check coreprotect status", + "commands": [ + "coreprotect show" + ], + "rcon_results": [ + { + "cmd": "coreprotect show", + "result": "Unknown or incomplete command. See below for error\ncoreprotect show<--[HERE]", + "ok": false + } + ], + "elapsed": 2.2, + "error": null + }, + { + "category": "plugins_coreprotect", + "prompt": "sudo lookup block changes in the last hour", + "commands": [], + "rcon_results": [], + "elapsed": 9.92, + "error": "Unterminated string starting at: line 1 column 1278 (char 1277)" + }, + { + "category": "plugins_essentials", + "prompt": "sudo set spawn here", + "commands": [ + "//set spawn here" + ], + "rcon_results": [ + { + "cmd": "set spawn here", + "result": "Unknown or incomplete command. See below for error\nset spawn here<--[HERE]", + "ok": false + } + ], + "elapsed": 2.04, + "error": null + }, + { + "category": "plugins_essentials", + "prompt": "sudo create a warp called bakeoff-test", + "commands": [], + "rcon_results": [], + "elapsed": 10.43, + "error": "Unterminated string starting at: line 1 column 1277 (char 1276)" + }, + { + "category": "plugins_essentials", + "prompt": "sudo heal me", + "commands": [ + "effect give slingshooter08 regeneration 1 600" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 regeneration 1 600", + "result": "Integer must not be more than 255: found 600\n...eration 1 600<--[HERE]", + "ok": false + } + ], + "elapsed": 2.58, + "error": null + }, + { + "category": "plugins_luckperms", + "prompt": "sudo create a group called testers", + "commands": [ + "lp group create testers" + ], + "rcon_results": [ + { + "cmd": "lp group create testers", + "result": "Unknown or incomplete command. See below for error\nlp group create testers<--[HERE]", + "ok": false + } + ], + "elapsed": 2.2, + "error": null + }, + { + "category": "plugins_luckperms", + "prompt": "sudo list all permission groups", + "commands": [ + "lp group list" + ], + "rcon_results": [ + { + "cmd": "lp group list", + "result": "Unknown or incomplete command. See below for error\nlp group list<--[HERE]", + "ok": false + } + ], + "elapsed": 2.15, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo give me a bed", + "commands": [ + "give slingshooter08 bed 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 bed 1", + "result": "Unknown item 'minecraft:bed'\n...shooter08 bed 1<--[HERE]", + "ok": false + } + ], + "elapsed": 1.93, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo give me cooked beef", + "commands": [ + "give slingshooter08 minecraft:cooked_beef 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 minecraft:cooked_beef 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.9, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo effect give me speed", + "commands": [ + "effect give slingshooter08 speed 1 60 1" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 speed 1 60 1", + "result": "Invalid boolean: expected 'true' or 'false' but found '1'\n...peed 1 60 1<--[HERE]", + "ok": false + } + ], + "elapsed": 2.81, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo fill with stone 10", + "commands": [ + "give slingshooter08 stone 10" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 stone 10", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 2.08, + "error": null + } + ], + "mortdecai:0.5.0": [ + { + "category": "basic_give", + "prompt": "sudo give me a diamond sword", + "commands": [ + "give slingshooter08 diamond_sword 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_sword 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.45, + "error": null + }, + { + "category": "basic_give", + "prompt": "sudo give me 64 golden apples", + "commands": [ + "give slingshooter08 golden_apple 64" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 golden_apple 64", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.26, + "error": null + }, + { + "category": "basic_give", + "prompt": "sudo give me full netherite armor", + "commands": [ + "give slingshooter08 diamond 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 2.38, + "error": null + }, + { + "category": "basic_give", + "prompt": "sudo give me a stack of oak logs", + "commands": [ + "give slingshooter08 oak_log[16]" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 oak_log[16]", + "result": "Unknown item component 'minecraft:16'\n...8 oak_log[16]<--[HERE]", + "ok": false + } + ], + "elapsed": 1.22, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me a sword with sharpness 5 and mending", + "commands": [ + "give slingshooter08 diamond_sword[enchantments={sharpness:5,mending:1}] 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_sword[enchantments={sharpness:5,mending:1}] 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 2.1, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me a bow with power 5 and infinity", + "commands": [ + "give slingshooter08 bow[power=5,infinity=1]", + "give slingshooter08 arrow" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 bow[power=5,infinity=1]", + "result": "Unknown item component 'minecraft:power'\n...ter08 bow[power=5,infinity=1]<--[HERE]", + "ok": false + }, + { + "cmd": "give slingshooter08 arrow", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 2.48, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me boots with feather falling 4 and depth strider 3", + "commands": [ + "give slingshooter08 diamond_boots 1[enchantments=[{id:feather_falling,level:4},{id:depth_strider,level:3}]]" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_boots 1[enchantments=[{id:feather_falling,level:4},{id:depth_strider,level:3}]]", + "result": "Expected whitespace to end one argument, but found trailing data\n...nd_boots 1[enchantments=[{id:feather_falling,level:4},{id:depth_strider,level:3}]]<--[HERE]", + "ok": false + } + ], + "elapsed": 2.01, + "error": null + }, + { + "category": "enchantments", + "prompt": "sudo give me a trident with loyalty 3 and channeling", + "commands": [ + "give slingshooter08 diamond 1", + "give slingshooter08 trident 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond 1", + "result": "No player was found", + "ok": true + }, + { + "cmd": "give slingshooter08 trident 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 2.12, + "error": null + }, + { + "category": "effects", + "prompt": "sudo give me speed 2 for 5 minutes", + "commands": [ + "effect give slingshooter08 speed 2 300" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 speed 2 300", + "result": "Integer must not be more than 255: found 300\n...8 speed 2 300<--[HERE]", + "ok": false + } + ], + "elapsed": 2.19, + "error": null + }, + { + "category": "effects", + "prompt": "sudo make me invisible for 60 seconds", + "commands": [ + "effect give slingshooter08 invisibility 60 1" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 invisibility 60 1", + "result": "No entity was found", + "ok": true + } + ], + "elapsed": 2.29, + "error": null + }, + { + "category": "effects", + "prompt": "sudo give me night vision forever", + "commands": [ + "give slingshooter08 minecraft:night_vision 1 6000" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 minecraft:night_vision 1 6000", + "result": "Unknown item 'minecraft:night_vision'\n...shooter08 minecraft:night_vision 1 6000<--[HERE]", + "ok": false + } + ], + "elapsed": 1.93, + "error": null + }, + { + "category": "effects", + "prompt": "sudo give everyone resistance 3", + "commands": [ + "give @a resistance 3 600000" + ], + "rcon_results": [ + { + "cmd": "give @a resistance 3 600000", + "result": "Unknown item 'minecraft:resistance'\ngive @a resistance 3 600000<--[HERE]", + "ok": false + } + ], + "elapsed": 1.43, + "error": null + }, + { + "category": "world", + "prompt": "sudo set time to day", + "commands": [ + "time set day" + ], + "rcon_results": [ + { + "cmd": "time set day", + "result": "Set the time to 1000", + "ok": true + } + ], + "elapsed": 1.85, + "error": null + }, + { + "category": "world", + "prompt": "sudo clear the weather", + "commands": [ + "weather clear" + ], + "rcon_results": [ + { + "cmd": "weather clear", + "result": "Set the weather to clear", + "ok": true + } + ], + "elapsed": 2.48, + "error": null + }, + { + "category": "world", + "prompt": "sudo kill all zombies", + "commands": [ + "execute as @e[type=zombie] at @s run particle firework_rocket ~ ~ ~ 0.5 0.5 0.5 0.1 10", + "execute as @e[type=zombie] at @s run kill @e[type=zombie]" + ], + "rcon_results": [ + { + "cmd": "execute as @e[type=zombie] at @s run particle firework_rocket ~ ~ ~ 0.5 0.5 0.5 0.1 10", + "result": "Unknown particle: minecraft:firework_rocket\n...ork_rocket ~ ~ ~ 0.5 0.5 0.5 0.1 10<--[HERE]", + "ok": false + }, + { + "cmd": "execute as @e[type=zombie] at @s run kill @e[type=zombie]", + "result": "", + "ok": true + } + ], + "elapsed": 3.35, + "error": null + }, + { + "category": "world", + "prompt": "sudo summon 3 cows near me", + "commands": [ + "summon cow ~ ~1 ~" + ], + "rcon_results": [ + { + "cmd": "summon cow ~ ~1 ~", + "result": "Summoned new Cow", + "ok": true + } + ], + "elapsed": 1.6, + "error": null + }, + { + "category": "teleport", + "prompt": "sudo tp me to 0 100 0", + "commands": [ + "tp slingshooter08 0 100 0" + ], + "rcon_results": [ + { + "cmd": "tp slingshooter08 0 100 0", + "result": "No entity was found", + "ok": true + } + ], + "elapsed": 1.92, + "error": null + }, + { + "category": "teleport", + "prompt": "sudo tp me 50 blocks up", + "commands": [ + "tp slingshooter08 ~ ~50 ~" + ], + "rcon_results": [ + { + "cmd": "tp slingshooter08 ~ ~50 ~", + "result": "No entity was found", + "ok": true + } + ], + "elapsed": 1.38, + "error": null + }, + { + "category": "fill_build", + "prompt": "sudo fill a 5x5 gold platform under me", + "commands": [ + "//fill ~ ~-5 ~ 5 5 gold" + ], + "rcon_results": [ + { + "cmd": "fill ~ ~-5 ~ 5 5 gold", + "result": "Expected integer\n...~-5 ~ 5 5 gold<--[HERE]", + "ok": false + } + ], + "elapsed": 1.74, + "error": null + }, + { + "category": "fill_build", + "prompt": "sudo place a beacon at 0 64 0", + "commands": [ + "give slingshooter08 diamond 1", + "execute as slingshooter08 at 0 64 0 run setblock ~ ~ ~ diamond_block" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond 1", + "result": "No player was found", + "ok": true + }, + { + "cmd": "execute as slingshooter08 at 0 64 0 run setblock ~ ~ ~ diamond_block", + "result": "Incorrect argument for command\n...er08 at 0 64 0 run setblock ~ ~ ~ diamond_block<--[HERE]", + "ok": false + } + ], + "elapsed": 2.55, + "error": null + }, + { + "category": "complex", + "prompt": "sudo give me a mace with density 5 and wind burst 3", + "commands": [ + "give slingshooter08 diamond_mace 1 {Enchantments:[{id:knockback,lvl:5},{id:impaling,lvl:3}]}" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond_mace 1 {Enchantments:[{id:knockback,lvl:5},{id:impaling,lvl:3}]}", + "result": "Unknown item 'minecraft:diamond_mace'\n...shooter08 diamond_mace 1 {Enchantments:[{id:knockback,lvl:5},{id:impaling,lvl:3}]}<--[HERE]", + "ok": false + } + ], + "elapsed": 3.14, + "error": null + }, + { + "category": "complex", + "prompt": "sudo give me a decorated pot", + "commands": [ + "give slingshooter08 diamond 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.91, + "error": null + }, + { + "category": "complex", + "prompt": "sudo spawn a warden 10 blocks away", + "commands": [ + "summon minecraft:warden ~ ~ ~10" + ], + "rcon_results": [ + { + "cmd": "summon minecraft:warden ~ ~ ~10", + "result": "Summoned new Warden", + "ok": true + } + ], + "elapsed": 2.48, + "error": null + }, + { + "category": "complex", + "prompt": "sudo create a team called red with red color", + "commands": [ + "scoreboard teams add red", + "scoreboard teams setcolor red red" + ], + "rcon_results": [ + { + "cmd": "scoreboard teams add red", + "result": "Incorrect argument for command\n...coreboard teams add red<--[HERE]", + "ok": false + }, + { + "cmd": "scoreboard teams setcolor red red", + "result": "Incorrect argument for command\n...coreboard teams setcolor red red<--[HERE]", + "ok": false + } + ], + "elapsed": 2.79, + "error": null + }, + { + "category": "plugins_worldguard", + "prompt": "sudo create a region called test-region", + "commands": [ + "//region set test-region" + ], + "rcon_results": [ + { + "cmd": "region set test-region", + "result": "Unknown or incomplete command. See below for error\nregion set test-region<--[HERE]", + "ok": false + } + ], + "elapsed": 1.71, + "error": null + }, + { + "category": "plugins_worldguard", + "prompt": "sudo set pvp deny in the test-region", + "commands": [ + "//set pvp deny" + ], + "rcon_results": [ + { + "cmd": "set pvp deny", + "result": "Unknown or incomplete command. See below for error\nset pvp deny<--[HERE]", + "ok": false + } + ], + "elapsed": 1.43, + "error": null + }, + { + "category": "plugins_worldguard", + "prompt": "sudo list all regions", + "commands": [ + "//regions list" + ], + "rcon_results": [ + { + "cmd": "regions list", + "result": "Unknown or incomplete command. See below for error\nregions list<--[HERE]", + "ok": false + } + ], + "elapsed": 1.32, + "error": null + }, + { + "category": "plugins_coreprotect", + "prompt": "sudo check coreprotect status", + "commands": [ + "coreprotect query" + ], + "rcon_results": [ + { + "cmd": "coreprotect query", + "result": "Unknown or incomplete command. See below for error\ncoreprotect query<--[HERE]", + "ok": false + } + ], + "elapsed": 2.08, + "error": null + }, + { + "category": "plugins_coreprotect", + "prompt": "sudo lookup block changes in the last hour", + "commands": [ + "execute store result run data get entity @s data minecraft:player_played_time -> result_time", + "execute store result run data get entity @s data minecraft:player_played_time -> result_time2", + "execute store result run data get entity @s data minecraft:player_played_time -> result_time3" + ], + "rcon_results": [ + { + "cmd": "execute store result run data get entity @s data minecraft:player_played_time -> result_time", + "result": "Incorrect argument for command\n...re result run data get entity @s data minecraft:player_played_time -> result_time<--[HERE]", + "ok": false + }, + { + "cmd": "execute store result run data get entity @s data minecraft:player_played_time -> result_time2", + "result": "Incorrect argument for command\n...re result run data get entity @s data minecraft:player_played_time -> result_time2<--[HERE]", + "ok": false + }, + { + "cmd": "execute store result run data get entity @s data minecraft:player_played_time -> result_time3", + "result": "Incorrect argument for command\n...re result run data get entity @s data minecraft:player_played_time -> result_time3<--[HERE]", + "ok": false + } + ], + "elapsed": 4.65, + "error": null + }, + { + "category": "plugins_essentials", + "prompt": "sudo set spawn here", + "commands": [ + "//set spawn here" + ], + "rcon_results": [ + { + "cmd": "set spawn here", + "result": "Unknown or incomplete command. See below for error\nset spawn here<--[HERE]", + "ok": false + } + ], + "elapsed": 1.79, + "error": null + }, + { + "category": "plugins_essentials", + "prompt": "sudo create a warp called bakeoff-test", + "commands": [ + "give slingshooter08 diamond 1", + "give slingshooter08 golden_apple 1", + "give slingshooter08 enchanted_golden_apple 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 diamond 1", + "result": "No player was found", + "ok": true + }, + { + "cmd": "give slingshooter08 golden_apple 1", + "result": "No player was found", + "ok": true + }, + { + "cmd": "give slingshooter08 enchanted_golden_apple 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 3.24, + "error": null + }, + { + "category": "plugins_essentials", + "prompt": "sudo heal me", + "commands": [ + "effect give slingshooter08 instant_health 1 1000000" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 instant_health 1 1000000", + "result": "Integer must not be more than 255: found 1000000\n..._health 1 1000000<--[HERE]", + "ok": false + } + ], + "elapsed": 2.43, + "error": null + }, + { + "category": "plugins_luckperms", + "prompt": "sudo create a group called testers", + "commands": [ + "lp group create testers" + ], + "rcon_results": [ + { + "cmd": "lp group create testers", + "result": "Unknown or incomplete command. See below for error\nlp group create testers<--[HERE]", + "ok": false + } + ], + "elapsed": 1.59, + "error": null + }, + { + "category": "plugins_luckperms", + "prompt": "sudo list all permission groups", + "commands": [ + "lp group list" + ], + "rcon_results": [ + { + "cmd": "lp group list", + "result": "Unknown or incomplete command. See below for error\nlp group list<--[HERE]", + "ok": false + } + ], + "elapsed": 1.88, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo give me a bed", + "commands": [ + "give slingshooter08 bed 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 bed 1", + "result": "Unknown item 'minecraft:bed'\n...shooter08 bed 1<--[HERE]", + "ok": false + } + ], + "elapsed": 1.52, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo give me cooked beef", + "commands": [ + "give slingshooter08 cooked_beef 1" + ], + "rcon_results": [ + { + "cmd": "give slingshooter08 cooked_beef 1", + "result": "No player was found", + "ok": true + } + ], + "elapsed": 1.35, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo effect give me speed", + "commands": [ + "effect give slingshooter08 speed 300 1" + ], + "rcon_results": [ + { + "cmd": "effect give slingshooter08 speed 300 1", + "result": "No entity was found", + "ok": true + } + ], + "elapsed": 1.83, + "error": null + }, + { + "category": "error_prone", + "prompt": "sudo fill with stone 10", + "commands": [ + "//fill 10 stone" + ], + "rcon_results": [ + { + "cmd": "fill 10 stone", + "result": "Expected integer\nfill 10 stone<--[HERE]", + "ok": false + } + ], + "elapsed": 3.26, + "error": null + } + ] + } +} \ No newline at end of file diff --git a/training/scripts/bakeoff.py b/training/scripts/bakeoff.py new file mode 100644 index 0000000..3c7db67 --- /dev/null +++ b/training/scripts/bakeoff.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +""" +Bake-off — compare model versions on standard test prompts with RCON validation. + +Runs the same prompts through multiple models, executes via RCON, and scores +success rate, response quality, and speed. + +Usage: + python3 bakeoff.py --models mortdecai:0.4.0,mortdecai:0.5.0 \ + --ollama-url http://localhost:11434 --rcon-host 192.168.0.244 +""" + +import argparse +import json +import random +import re +import sys +import time +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +import requests +from agent.tools.persistent_rcon import get_rcon + +OUTPUT_DIR = PROJECT_ROOT / "training" / "bakeoff_results" + +# Standard test prompts across categories +TEST_PROMPTS = { + "basic_give": [ + "sudo give me a diamond sword", + "sudo give me 64 golden apples", + "sudo give me full netherite armor", + "sudo give me a stack of oak logs", + ], + "enchantments": [ + "sudo give me a sword with sharpness 5 and mending", + "sudo give me a bow with power 5 and infinity", + "sudo give me boots with feather falling 4 and depth strider 3", + "sudo give me a trident with loyalty 3 and channeling", + ], + "effects": [ + "sudo give me speed 2 for 5 minutes", + "sudo make me invisible for 60 seconds", + "sudo give me night vision forever", + "sudo give everyone resistance 3", + ], + "world": [ + "sudo set time to day", + "sudo clear the weather", + "sudo kill all zombies", + "sudo summon 3 cows near me", + ], + "teleport": [ + "sudo tp me to 0 100 0", + "sudo tp me 50 blocks up", + ], + "fill_build": [ + "sudo fill a 5x5 gold platform under me", + "sudo place a beacon at 0 64 0", + ], + "complex": [ + "sudo give me a mace with density 5 and wind burst 3", + "sudo give me a decorated pot", + "sudo spawn a warden 10 blocks away", + "sudo create a team called red with red color", + ], + "plugins_worldguard": [ + "sudo create a region called test-region", + "sudo set pvp deny in the test-region", + "sudo list all regions", + ], + "plugins_coreprotect": [ + "sudo check coreprotect status", + "sudo lookup block changes in the last hour", + ], + "plugins_essentials": [ + "sudo set spawn here", + "sudo create a warp called bakeoff-test", + "sudo heal me", + ], + "plugins_luckperms": [ + "sudo create a group called testers", + "sudo list all permission groups", + ], + "error_prone": [ + "sudo give me a bed", + "sudo give me cooked beef", + "sudo effect give me speed", + "sudo fill with stone 10", + ], +} + +PLAYER = "slingshooter08" + + +def query_model(prompt, model, ollama_url, timeout=60): + """Query a model and return parsed response + timing.""" + system = ( + "/no_think\n" + "You are a Minecraft 1.21 command translator for a Paper server with plugins: " + "FastAsyncWorldEdit, WorldGuard, CoreProtect, EssentialsX, Vault, LuckPerms.\n" + "PERMISSION LEVEL: 4 (generous).\n" + "Return JSON: {\"commands\": [...], \"reasoning\": \"...\"}" + ) + + start = time.time() + try: + r = requests.post(f"{ollama_url}/api/chat", json={ + "model": model, + "messages": [ + {"role": "system", "content": system}, + {"role": "user", "content": f"Player {PLAYER}: {prompt}"}, + ], + "stream": False, "format": "json", + "options": {"temperature": 0.2, "num_predict": 500}, + }, timeout=timeout) + elapsed = time.time() - start + content = r.json()["message"]["content"] + content = re.sub(r'[\s\S]*?\s*', '', content) + parsed = json.loads(content) + return { + "commands": parsed.get("commands", []), + "reasoning": parsed.get("reasoning", ""), + "elapsed": round(elapsed, 2), + "error": None, + } + except Exception as e: + return { + "commands": [], + "reasoning": "", + "elapsed": round(time.time() - start, 2), + "error": str(e)[:200], + } + + +def validate_commands(commands, rcon): + """Execute commands and return results.""" + results = [] + for cmd in commands[:8]: + if not isinstance(cmd, str) or not cmd.strip(): + continue + cmd = cmd.strip().lstrip("/") + try: + result = rcon.command(cmd) + is_err = any(e in result for e in ("<--[HERE]", "Unknown", "Incorrect", "Expected", "Invalid")) + results.append({"cmd": cmd, "result": result[:200], "ok": not is_err}) + except Exception as e: + results.append({"cmd": cmd, "result": str(e), "ok": False}) + return results + + +def run_bakeoff(models, ollama_url, rcon): + """Run all test prompts through all models.""" + results = {m: {"total": 0, "cmd_success": 0, "cmd_fail": 0, "cmd_total": 0, + "no_commands": 0, "errors": 0, "total_time": 0, "details": []} + for m in models} + + total_prompts = sum(len(v) for v in TEST_PROMPTS.values()) + print(f"Running {total_prompts} prompts x {len(models)} models = {total_prompts * len(models)} tests\n") + + for category, prompts in TEST_PROMPTS.items(): + print(f"── {category} ──") + for prompt in prompts: + print(f" {prompt[:65]}") + for model in models: + resp = query_model(prompt, model, ollama_url) + r = results[model] + r["total"] += 1 + r["total_time"] += resp["elapsed"] + + if resp["error"]: + r["errors"] += 1 + status = "ERR" + rcon_results = [] + elif not resp["commands"]: + r["no_commands"] += 1 + status = "EMPTY" + rcon_results = [] + else: + rcon_results = validate_commands(resp["commands"], rcon) + ok = sum(1 for rr in rcon_results if rr["ok"]) + fail = sum(1 for rr in rcon_results if not rr["ok"]) + r["cmd_success"] += ok + r["cmd_fail"] += fail + r["cmd_total"] += ok + fail + status = f"{ok}/{ok+fail}" if fail else f"{ok}✓" + + model_short = model.split(":")[-1] + print(f" {model_short:8s} {status:8s} {resp['elapsed']:.1f}s {len(resp['commands'])} cmds") + + r["details"].append({ + "category": category, + "prompt": prompt, + "commands": resp["commands"], + "rcon_results": rcon_results, + "elapsed": resp["elapsed"], + "error": resp["error"], + }) + print() + + return results + + +def print_summary(results, models): + """Print comparison table.""" + print("=" * 70) + print("BAKE-OFF RESULTS") + print("=" * 70) + + header = f"{'Metric':<30s}" + for m in models: + header += f" {m.split(':')[-1]:>12s}" + print(header) + print("-" * 70) + + metrics = [ + ("Prompts tested", lambda r: r["total"]), + ("Commands generated", lambda r: r["cmd_total"]), + ("Commands succeeded", lambda r: r["cmd_success"]), + ("Commands failed", lambda r: r["cmd_fail"]), + ("Success rate", lambda r: f"{100*r['cmd_success']/max(r['cmd_total'],1):.1f}%"), + ("Empty responses", lambda r: r["no_commands"]), + ("Errors", lambda r: r["errors"]), + ("Avg response time", lambda r: f"{r['total_time']/max(r['total'],1):.2f}s"), + ("Total time", lambda r: f"{r['total_time']:.1f}s"), + ] + + for label, fn in metrics: + row = f"{label:<30s}" + for m in models: + val = fn(results[m]) + row += f" {str(val):>12s}" + print(row) + + print("=" * 70) + + # Category breakdown + print("\nCATEGORY BREAKDOWN (success rate):") + print("-" * 70) + categories = list(TEST_PROMPTS.keys()) + header = f"{'Category':<25s}" + for m in models: + header += f" {m.split(':')[-1]:>12s}" + print(header) + + for cat in categories: + row = f"{cat:<25s}" + for m in models: + cat_details = [d for d in results[m]["details"] if d["category"] == cat] + cat_ok = sum(sum(1 for rr in d["rcon_results"] if rr["ok"]) for d in cat_details) + cat_total = sum(len(d["rcon_results"]) for d in cat_details) + if cat_total > 0: + row += f" {100*cat_ok/cat_total:>10.0f}%" + else: + row += f" {'N/A':>12s}" + print(row) + + print() + + +def main(): + parser = argparse.ArgumentParser(description="Model bake-off") + parser.add_argument("--models", default="mortdecai:0.4.0,mortdecai:0.5.0") + parser.add_argument("--ollama-url", default="http://localhost:11434") + parser.add_argument("--rcon-host", default="192.168.0.244") + parser.add_argument("--rcon-port", type=int, default=25578) + parser.add_argument("--rcon-pass", default="REDACTED_RCON") + args = parser.parse_args() + + models = [m.strip() for m in args.models.split(",")] + rcon = get_rcon(args.rcon_host, args.rcon_port, args.rcon_pass) + + print(f"Bake-off: {' vs '.join(models)}") + print(f"Ollama: {args.ollama_url}") + print(f"RCON: {args.rcon_host}:{args.rcon_port}") + print() + + results = run_bakeoff(models, args.ollama_url, rcon) + print_summary(results, models) + + # Save results + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + out_path = OUTPUT_DIR / f"bakeoff_{'-vs-'.join(m.replace(':','_') for m in models)}_{int(time.time())}.json" + with open(out_path, "w") as f: + json.dump({ + "models": models, + "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"), + "results": {m: {k: v for k, v in r.items() if k != "details"} for m, r in results.items()}, + "details": {m: r["details"] for m, r in results.items()}, + }, f, indent=2, default=str) + print(f"Results saved to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/training/scripts/exploration_self_play.py b/training/scripts/exploration_self_play.py new file mode 100644 index 0000000..6d436c6 --- /dev/null +++ b/training/scripts/exploration_self_play.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +""" +Exploration Self-Play — model uses wiki_lookup to explore Minecraft knowledge, +then validates its understanding through RCON commands. + +Unlike canned self-play, the model drives its own curiosity: +1. Gets a broad topic ("explore enchantments", "learn about 1.21 items") +2. Uses minecraft.wiki_lookup to research +3. Generates commands based on what it learned +4. RCON validates correctness +5. If wrong, researches more and corrects + +Produces gold-standard knowledge-grounded training data. + +Usage: + python3 exploration_self_play.py --ollama-url http://localhost:11434 \ + --model mortdecai:0.5.0 --rcon-host 192.168.0.244 --rcon-port 25578 +""" + +import argparse +import json +import random +import re +import sys +import time +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +import requests +from agent.tools.persistent_rcon import get_rcon + +OUTPUT_DIR = PROJECT_ROOT / "data" / "raw" / "exploration_selfplay" + +PLAYERS = ["slingshooter08", "Ace13245", "TheBigBoss", "xXDragonSlayerXx"] + +# Topics for the model to explore — broad enough that it needs to look things up +EXPLORATION_TOPICS_PLUGINS = [ + # WorldGuard deep dive + "Research all WorldGuard region flags. Create a region and test each flag one at a time for {p}.", + "Look up how WorldGuard region priorities work. Create overlapping regions with different rules.", + "Research WorldGuard's __global__ region. What flags can you set globally? Test a few.", + "Look up WorldGuard entry/exit deny flags. Create a VIP-only zone and test it.", + "Research how to make a WorldGuard region that heals players. Set it up near {p}.", + "What WorldGuard flags control explosions? Research and create a blast-proof zone.", + "Look up how to block specific commands in a WorldGuard region. Test with /home.", + "Research WorldGuard greeting and farewell messages. Set up regions with welcome messages.", + + # CoreProtect deep dive + "Research all CoreProtect action types (block, container, chat, command). Test /co lookup with each.", + "Look up CoreProtect time format syntax. Test rollbacks with different time ranges (1h, 30m, 7d).", + "Research how CoreProtect handles container logging. Place a chest, add items, then lookup the history.", + "What CoreProtect parameters filter by block type? Test rolling back only specific blocks.", + "Look up how to use CoreProtect radius parameter. Test different radius values.", + "Research CoreProtect restore vs rollback — what's the difference? Demonstrate both.", + + # EssentialsX deep dive + "Research all EssentialsX economy commands. Set up a working economy with /eco, /balance, /pay.", + "Look up EssentialsX kit creation syntax. Create a starter kit and a VIP kit.", + "Research EssentialsX warp system. Create 5 warps at interesting locations.", + "What EssentialsX commands exist for player management? Test /nick, /seen, /whois.", + "Look up EssentialsX home system. Set multiple named homes for {p}.", + "Research EssentialsX god mode, fly mode, and speed commands. Test all three.", + "What EssentialsX commands modify the world? Test /sun, /storm, /day, /night.", + + # LuckPerms deep dive + "Research LuckPerms group inheritance. Create parent and child groups and test permission flow.", + "Look up LuckPerms temporary permissions. Give {p} temp fly access for 5 minutes.", + "Research LuckPerms meta (prefix/suffix). Set up colored chat prefixes for different groups.", + "What LuckPerms commands check a user's permissions? Audit {p}'s current permissions.", + "Look up how to create a LuckPerms permission ladder (default -> member -> vip -> admin).", + "Research LuckPerms weight system. How do group priorities work?", + + # FAWE/WorldEdit deep dive + "Research all WorldEdit shape commands (sphere, cyl, pyramid). Build one of each near {p}.", + "Look up WorldEdit brush types. What brushes exist beyond sphere brush?", + "Research WorldEdit mask syntax. How do masks work with //replace?", + "What WorldEdit clipboard operations exist? Test //copy, //paste, //rotate, //flip.", + "Look up WorldEdit pattern syntax. Can you mix multiple blocks in one command?", + "Research WorldEdit //generate command. Can it make mathematical surfaces?", + "What WorldEdit selection modes exist? Test //sel cuboid vs poly vs sphere.", + + # Script writing exploration + "Research Minecraft datapack function syntax. Write a mcfunction that creates a parkour course.", + "Look up how Minecraft tick functions work. Write one that makes particles at spawn.", + "Research how to chain mcfunctions together. Write a main function that calls sub-functions.", + "What Minecraft datapack tags control function scheduling? Test tick.json and load.json.", + "Look up execute command syntax for mcfunctions. Write a script using execute at/as/if.", + "Research scoreboard objectives. Write a script that tracks player kills and announces leaders.", + + # Multi-plugin combos + "Research how to combine WorldEdit builds with WorldGuard protection. Build and protect an arena.", + "Look up how to use CoreProtect to undo WorldEdit operations specifically.", + "Research combining LuckPerms with WorldGuard — can you tie region access to permission groups?", + "Create a complete server setup: spawn area (WE), protected (WG), with warps (Ess) and perms (LP).", + "Research how to build a minigame arena: WE for building, WG for rules, scoreboards for tracking.", +] + +EXPLORATION_TOPICS = [ + # Items and crafting + "What are all the new items added in 1.21? Look them up and give one of each to {p}.", + "Research every type of arrow (tipped arrows) and give {p} one of each.", + "Look up all the banner patterns available and create a cool banner for {p}.", + "What suspicious stew effects exist? Research and give {p} the best one.", + "Research all the different types of potions and give {p} the three most useful ones.", + "What are all the different horse armor types? Look them up and give one of each to {p}.", + "Research all smithing templates and give {p} the rarest ones.", + "Look up every type of spawn egg and give {p} five interesting ones.", + + # Enchantments + "Research the best enchantment setup for a full netherite armor set. Give it to {p}.", + "What enchantments are exclusive to each other? Look them up and explain while giving {p} examples.", + "Research the difference between Protection, Fire Protection, Blast Protection, and Projectile Protection. Which is best for general use? Give {p} the optimal set.", + "Look up what Thorns does exactly — is it worth using? Give {p} armor with and without it to test.", + "Research Sweeping Edge — does it still exist in 1.21? Give {p} a sword with the correct enchantments.", + "What's the maximum level for each enchantment? Research and give {p} a tool with impossible levels vs correct levels.", + + # Effects and potions + "Research all status effects in 1.21. Which ones are new? Apply the 3 newest ones to {p}.", + "Look up the Ominous Bottle effect — what does it do? Give one to {p}.", + "What's the difference between Strength and Haste? Research and apply the right one for mining.", + "Research what Wind Charged does. Apply it to {p}.", + "Look up all negative effects and their max safe durations. Apply a brief demonstration.", + "What effect does a Beacon give? Research all beacon effects and apply them.", + + # Mobs and entities + "Research all tameable mobs in 1.21. Summon one of each near {p}.", + "What mobs were added or changed in 1.21? Look them up and summon the new ones.", + "Research the Breeze mob — what does it drop? Summon one for {p}.", + "Look up all rideable mobs and summon one for {p} with a saddle.", + "What's the strongest mob in the game? Research its stats and summon it (carefully).", + "Research all fish types and summon them in water near {p}.", + + # Blocks and building + "Research all copper block variants and their oxidation states. Place examples near {p}.", + "What blocks emit light? Look up all light-emitting blocks and demonstrate.", + "Research all types of stairs, slabs, and walls available in 1.21.", + "Look up how to make colored concrete powder and place a rainbow near {p}.", + "What are all the glazed terracotta patterns? Research and place one of each.", + "Research redstone components — what's the difference between a comparator and repeater?", + + # Commands and mechanics + "Research the /place command. What can it place? Demonstrate with a structure.", + "Look up the /damage command syntax and demonstrate different damage types on a mob.", + "Research /attribute — what attributes can be modified? Give {p} double health.", + "What does the /ride command do? Research and demonstrate.", + "Look up /fillbiome — can you change the biome? Try it near {p}.", + "Research the /random command added in 1.21. What can it do?", + + # Worldgen and structures + "Research all structure types that /locate can find. Find the 3 nearest to {p}.", + "What biomes exist in 1.21? Look up any new ones and locate them.", + "Research Trial Chambers — where do they spawn? Locate one for {p}.", + + # Plugin-specific research + "Research WorldGuard region flags — what flags exist? Set up a demo region with interesting flags.", + "Look up CoreProtect rollback syntax — what parameters does it accept?", + "Research LuckPerms group inheritance — how do child groups work?", + "What WorldEdit brushes are available? Research and describe them.", + "Look up EssentialsX economy commands — set up a basic economy demonstration.", +] + + +def wiki_lookup(query, timeout=15): + """Actually search the Minecraft wiki via DuckDuckGo + scraping.""" + try: + # Use a simple search - the model will call this through the tool loop + r = requests.get( + "https://minecraft.wiki/api.php", + params={"action": "opensearch", "search": query, "limit": 3, "format": "json"}, + timeout=timeout, + ) + results = r.json() + if len(results) >= 4 and results[1]: + titles = results[1][:3] + urls = results[3][:3] if len(results) > 3 else [] + + # Fetch first result summary + if titles: + r2 = requests.get( + "https://minecraft.wiki/api.php", + params={ + "action": "query", "prop": "extracts", + "exintro": True, "explaintext": True, + "titles": titles[0], "format": "json", + }, + timeout=timeout, + ) + pages = r2.json().get("query", {}).get("pages", {}) + for page in pages.values(): + extract = page.get("extract", "") + if extract: + return { + "content": extract[:1500], + "url": urls[0] if urls else f"https://minecraft.wiki/w/{titles[0]}", + "ok": True, + } + return {"content": f"No wiki results for: {query}", "url": "", "ok": False} + except Exception as e: + return {"content": f"Wiki lookup failed: {e}", "url": "", "ok": False} + + +def run_exploration(topic, player, ollama_url, model, rcon): + """Run one exploration round — model researches and acts.""" + system = ( + "/no_think\n" + "You are a Minecraft 1.21 expert on a Paper server with plugins: " + "WorldGuard, CoreProtect, EssentialsX, LuckPerms, FastAsyncWorldEdit.\n\n" + "You have these lookup tools:\n" + "- minecraft.wiki_lookup: {\"query\": \"...\"} — Minecraft Wiki for items, mobs, commands\n" + "- plugin.docs_lookup: {\"plugin\": \"worldguard|worldedit|coreprotect|essentialsx|luckperms\", \"query\": \"...\"} — plugin documentation\n" + "- minecraft.changelog_lookup: {\"query\": \"...\", \"version\": \"1.21\"} — version changes\n" + "- paper.docs_lookup: {\"query\": \"...\"} — Paper server docs\n" + "- rcon.execute: {\"command\": \"...\"} — execute a Minecraft command\n\n" + "WORKFLOW:\n" + "1. Research the topic using the appropriate lookup tool\n" + "2. For plugin commands, use plugin.docs_lookup instead of minecraft.wiki_lookup\n" + "3. Generate and execute commands via rcon.execute\n" + "4. If a command fails, look up the correct syntax and try again\n\n" + "To call a tool, respond with:\n" + "\n{\"name\": \"tool_name\", \"arguments\": {...}}\n\n\n" + "When done, respond with final JSON:\n" + "{\"commands\": [...], \"reasoning\": \"what you learned\", \"wiki_topics\": [\"topics you looked up\"]}\n\n" + "Be curious. ALWAYS look things up before guessing. Verify your knowledge." + ) + + topic_resolved = topic.replace("{p}", player) + messages = [ + {"role": "system", "content": system}, + {"role": "user", "content": f"Player {player}: {topic_resolved}"}, + ] + + tool_trace = [] + all_commands = [] + wiki_topics = [] + max_steps = 10 + + for step in range(max_steps): + try: + r = requests.post(f"{ollama_url}/api/chat", json={ + "model": model, + "messages": messages, + "stream": False, + "options": {"temperature": 0.6, "num_predict": 800}, + }, timeout=120) + raw = r.json()["message"]["content"] + except Exception as e: + print(f" LLM error: {e}") + break + + raw = re.sub(r'[\s\S]*?\s*', '', raw) + + # Check for tool calls + tool_matches = re.findall(r'\s*(\{.*?\})\s*', raw, re.DOTALL) + + if not tool_matches: + # Final response — done exploring + break + + for tc_json in tool_matches: + try: + tc = json.loads(tc_json) + tool_name = tc.get("name", "") + tool_args = tc.get("arguments", {}) + except json.JSONDecodeError: + continue + + if tool_name == "minecraft.wiki_lookup": + query = tool_args.get("query", "") + wiki_topics.append(query) + result = wiki_lookup(query) + print(f" wiki: {query[:60]} -> {len(result.get('content',''))} chars") + elif tool_name in ("plugin.docs_lookup", "minecraft.changelog_lookup", "paper.docs_lookup"): + try: + from agent.tools.knowledge_lookup import handle_knowledge_tool + result = handle_knowledge_tool(tool_name, tool_args) + except ImportError: + result = wiki_lookup(tool_args.get("query", tool_args.get("plugin", ""))) + query = tool_args.get("query", "") + wiki_topics.append(f"{tool_name}:{query}") + print(f" {tool_name}: {query[:50]} -> {len(result.get('content',''))} chars") + elif tool_name == "rcon.execute": + cmd = tool_args.get("command", "") + try: + rcon_result = rcon.command(cmd) + is_err = any(e in rcon_result for e in ("<--[HERE]", "Unknown", "Incorrect")) + result = {"success": not is_err, "result": rcon_result[:300]} + all_commands.append(cmd) + status = "OK" if not is_err else "ERR" + print(f" rcon: {cmd[:60]} -> {status}") + except Exception as e: + result = {"success": False, "result": str(e)} + print(f" rcon: {cmd[:60]} -> FAIL") + else: + result = {"ok": False, "error": f"unknown tool: {tool_name}"} + + tool_trace.append({ + "tool": tool_name, + "input": str(tool_args)[:200], + "ok": result.get("ok", result.get("success", False)), + "step": step, + }) + + messages.append({"role": "assistant", "content": f"\n{json.dumps(tc)}\n"}) + messages.append({"role": "tool", "content": json.dumps(result)[:3000]}) + + time.sleep(0.1) + + # Parse final response if present + reasoning = "" + try: + parsed = json.loads(raw) + reasoning = parsed.get("reasoning", "") + if parsed.get("commands"): + all_commands.extend(parsed["commands"]) + except json.JSONDecodeError: + reasoning = raw[:200] + + return { + "id": f"explore-{int(time.time())}-{random.randint(0,9999):04d}", + "source": "exploration_self_play", + "type": "exploration", + "input": {"user_message": topic_resolved, "player": player}, + "output": { + "commands": all_commands, + "reasoning": reasoning, + "wiki_topics": wiki_topics, + }, + "tool_trace": tool_trace, + "messages": messages, + "metadata": { + "model": model, + "steps": min(step + 1, max_steps), + "wiki_lookups": len(wiki_topics), + "rcon_commands": len(all_commands), + "success_rate": ( + sum(1 for t in tool_trace if t["tool"] == "rcon.execute" and t["ok"]) + / max(sum(1 for t in tool_trace if t["tool"] == "rcon.execute"), 1) + ), + }, + } + + +def main(): + parser = argparse.ArgumentParser(description="Exploration self-play") + parser.add_argument("--ollama-url", default="http://localhost:11434") + parser.add_argument("--model", default="mortdecai:0.5.0") + parser.add_argument("--rcon-host", default="192.168.0.244") + parser.add_argument("--rcon-port", type=int, default=25578) + parser.add_argument("--rcon-pass", default="REDACTED_RCON") + parser.add_argument("--rounds", type=int, default=999999) + parser.add_argument("--focus", default="general", choices=["general", "plugins", "all"], + help="Topic focus: general (vanilla MC), plugins (WG/CP/Ess/LP/FAWE/scripts), all (both)") + args = parser.parse_args() + + if args.focus == "plugins": + topics = EXPLORATION_TOPICS_PLUGINS + elif args.focus == "all": + topics = EXPLORATION_TOPICS + EXPLORATION_TOPICS_PLUGINS + else: + topics = EXPLORATION_TOPICS + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + output_path = OUTPUT_DIR / f"exploration_{args.focus}_{int(time.time())}.jsonl" + + rcon = get_rcon(args.rcon_host, args.rcon_port, args.rcon_pass) + + print(f"Exploration Self-Play") + print(f" Model: {args.model} on {args.ollama_url}") + print(f" RCON: {args.rcon_host}:{args.rcon_port}") + print(f" Focus: {args.focus} ({len(topics)} topics)") + print(f" Output: {output_path}") + print() + + stats = {"total": 0, "wiki_lookups": 0, "rcon_commands": 0, "rcon_success": 0} + + for round_num in range(args.rounds): + topic = random.choice(topics) + player = random.choice(PLAYERS) + + print(f"\n── Round {round_num+1} ──") + print(f" Topic: {topic[:80].replace('{p}', player)}") + + example = run_exploration(topic, player, args.ollama_url, args.model, rcon) + + stats["total"] += 1 + stats["wiki_lookups"] += example["metadata"]["wiki_lookups"] + stats["rcon_commands"] += example["metadata"]["rcon_commands"] + stats["rcon_success"] += int(example["metadata"]["success_rate"] * example["metadata"]["rcon_commands"]) + + print(f" Result: {example['metadata']['wiki_lookups']} lookups, " + f"{example['metadata']['rcon_commands']} commands, " + f"{example['metadata']['success_rate']:.0%} success") + + with open(output_path, "a") as f: + f.write(json.dumps(example, ensure_ascii=False) + "\n") + + if (round_num + 1) % 10 == 0: + rate = stats["rcon_success"] / max(stats["rcon_commands"], 1) * 100 + print(f"\n Progress: {stats['total']} explorations, " + f"{stats['wiki_lookups']} wiki lookups, " + f"{stats['rcon_commands']} commands ({rate:.0f}% success)") + + time.sleep(0.5) + + print(f"\nExploration complete: {stats['total']} topics explored") + + +if __name__ == "__main__": + main() diff --git a/training/scripts/filter_exploration_data.py b/training/scripts/filter_exploration_data.py new file mode 100644 index 0000000..b927b28 --- /dev/null +++ b/training/scripts/filter_exploration_data.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Filter exploration and self-play data for quality. + +Keeps: +- Successful interactions (model looked up info AND executed correctly) +- First instance of each unique error pattern (for error correction training) +- High wiki-lookup-to-command ratios (model actually used the knowledge) + +Removes: +- Duplicate topics (keeps first occurrence only) +- Empty responses (no commands, no lookups) +- Repeated failures on the same command pattern +- Rounds where model ignored wiki results + +Output: data/processed/filtered_exploration.jsonl +""" + +import json +import sys +from pathlib import Path +from collections import defaultdict + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent + +EXPLORATION_DIR = PROJECT_ROOT / "data" / "raw" / "exploration_selfplay" +TOOL_SELFPLAY_DIR = PROJECT_ROOT / "data" / "raw" / "tool_selfplay" +OUTPUT_PATH = PROJECT_ROOT / "data" / "processed" / "filtered_exploration.jsonl" + + +def load_all_examples(): + """Load all exploration and tool self-play examples.""" + examples = [] + + for jsonl in sorted(EXPLORATION_DIR.glob("*.jsonl")): + with open(jsonl) as f: + for line in f: + if line.strip(): + try: + examples.append(json.loads(line)) + except json.JSONDecodeError: + pass + + for jsonl in sorted(TOOL_SELFPLAY_DIR.glob("*.jsonl")): + with open(jsonl) as f: + for line in f: + if line.strip(): + try: + examples.append(json.loads(line)) + except json.JSONDecodeError: + pass + + return examples + + +def filter_examples(examples): + """Filter for quality.""" + kept = [] + seen_topics = set() + seen_error_patterns = set() + stats = { + "total": len(examples), + "kept_success": 0, + "kept_error_correction": 0, + "kept_wiki_grounded": 0, + "dropped_duplicate": 0, + "dropped_empty": 0, + "dropped_repeat_failure": 0, + } + + for ex in examples: + meta = ex.get("metadata", {}) + inp = ex.get("input", {}) + topic = inp.get("user_message", "")[:80] + success_rate = meta.get("success_rate", meta.get("all_success", False)) + wiki_lookups = meta.get("wiki_lookups", 0) + rcon_commands = meta.get("rcon_commands", 0) + + # Skip empty + if rcon_commands == 0 and wiki_lookups == 0: + stats["dropped_empty"] += 1 + continue + + # Deduplicate topics (keep first) + if topic in seen_topics: + stats["dropped_duplicate"] += 1 + continue + seen_topics.add(topic) + + # Categorize + if isinstance(success_rate, bool): + is_success = success_rate + else: + is_success = success_rate > 0.7 + + if is_success and rcon_commands > 0: + # Successful interaction — always keep + stats["kept_success"] += 1 + kept.append(ex) + elif wiki_lookups > 0 and rcon_commands > 0: + # Wiki-grounded (looked things up before acting) — keep even if some failures + stats["kept_wiki_grounded"] += 1 + kept.append(ex) + elif not is_success and rcon_commands > 0: + # Failed — keep only first instance of each error pattern + commands = ex.get("output", {}).get("commands", []) + if commands: + # Use first command as error pattern key + pattern = commands[0][:40] if isinstance(commands[0], str) else "" + else: + rcon_results = meta.get("rcon_results", []) + pattern = str(rcon_results[:1])[:60] if rcon_results else "" + + if pattern and pattern not in seen_error_patterns: + seen_error_patterns.add(pattern) + stats["kept_error_correction"] += 1 + kept.append(ex) + else: + stats["dropped_repeat_failure"] += 1 + else: + stats["dropped_empty"] += 1 + + return kept, stats + + +def main(): + print("Loading examples...") + examples = load_all_examples() + print(f" Loaded {len(examples)} raw examples") + + print("Filtering...") + filtered, stats = filter_examples(examples) + + print(f"\nFilter results:") + for k, v in stats.items(): + print(f" {k}: {v}") + + print(f"\nKept: {len(filtered)} ({100*len(filtered)//max(stats['total'],1)}%)") + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, "w") as f: + for ex in filtered: + f.write(json.dumps(ex, ensure_ascii=False) + "\n") + + print(f"Written to {OUTPUT_PATH}") + + +if __name__ == "__main__": + main() diff --git a/training/scripts/generate_training_chart.py b/training/scripts/generate_training_chart.py new file mode 100644 index 0000000..08c7122 --- /dev/null +++ b/training/scripts/generate_training_chart.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Generate SVG training history chart for the Gitea README. + +X-axis: Model version +Y-axis: Training examples (bar) and inverse loss (line) +""" + +import json +from pathlib import Path + +OUTPUT = Path(__file__).resolve().parent.parent.parent / "branding" / "training_progress.svg" + +# Historical data from training runs +VERSIONS = [ + {"version": "0.1.0", "examples": 500, "loss": 2.10, "label": "v1 (seed)"}, + {"version": "0.2.0", "examples": 1200, "loss": 1.45, "label": "v2 (+entities)"}, + {"version": "0.3.0", "examples": 2100, "loss": 0.82, "label": "v3 (+errors)"}, + {"version": "0.4.0", "examples": 3175, "loss": 0.35, "label": "v4 (+tools)"}, + {"version": "0.5.0", "examples": 4358, "loss": 0.16, "label": "v5 (+plugins)"}, +] + +# Chart dimensions +W = 700 +H = 400 +PAD_L = 70 +PAD_R = 30 +PAD_T = 40 +PAD_B = 80 +PLOT_W = W - PAD_L - PAD_R +PLOT_H = H - PAD_T - PAD_B + +# Colors +BG = "#111111" +GRID = "#252525" +TEXT = "#999999" +BAR_COLOR = "#D35400" +LINE_COLOR = "#4caf50" +LABEL_COLOR = "#e0e0e0" + + +def generate_svg(): + max_examples = max(v["examples"] for v in VERSIONS) * 1.15 + max_inv_loss = max(1.0 / v["loss"] for v in VERSIONS) * 1.15 + n = len(VERSIONS) + bar_w = PLOT_W / n * 0.6 + gap = PLOT_W / n + + svg = f""" + + + + +Mortdecai Training Progress + + +""" + # Y-axis grid (examples) + for i in range(5): + y = PAD_T + PLOT_H - (i / 4 * PLOT_H) + val = int(max_examples * i / 4) + svg += f'\n' + svg += f'{val:,}\n' + + # Bars (training examples) + for i, v in enumerate(VERSIONS): + cx = PAD_L + gap * i + gap / 2 + bh = (v["examples"] / max_examples) * PLOT_H + by = PAD_T + PLOT_H - bh + + svg += f'\n' + svg += f'{v["examples"]:,}\n' + + # X-axis label + svg += f'{v["version"]}\n' + svg += f'{v["label"]}\n' + + # Line (inverse loss = quality) + points = [] + for i, v in enumerate(VERSIONS): + cx = PAD_L + gap * i + gap / 2 + inv_loss = 1.0 / v["loss"] + ly = PAD_T + PLOT_H - (inv_loss / max_inv_loss) * PLOT_H + points.append(f"{cx},{ly}") + + polyline = " ".join(points) + svg += f'\n' + + # Dots on line + for i, v in enumerate(VERSIONS): + cx = PAD_L + gap * i + gap / 2 + inv_loss = 1.0 / v["loss"] + ly = PAD_T + PLOT_H - (inv_loss / max_inv_loss) * PLOT_H + svg += f'\n' + svg += f'loss={v["loss"]}\n' + + # Y-axis labels + svg += f'Training Examples\n' + + # Legend + svg += f'\n' + svg += f'Training Examples\n' + svg += f'\n' + svg += f'Model Quality (1/loss)\n' + + # X-axis label + svg += f'Model Version\n' + + svg += "" + return svg + + +def main(): + svg = generate_svg() + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT, "w") as f: + f.write(svg) + print(f"Chart saved to {OUTPUT}") + print(f"Embed in README: ![Training Progress](branding/training_progress.svg)") + + +if __name__ == "__main__": + main() diff --git a/training/scripts/regenerate_tool_data_v05.py b/training/scripts/regenerate_tool_data_v05.py new file mode 100644 index 0000000..d558f8f --- /dev/null +++ b/training/scripts/regenerate_tool_data_v05.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +""" +Regenerate tool-calling training data using mortdecai:0.5.0. + +Uses the model-driven tool loop: sends prompts to 0.5.0, lets it decide +which tools to call, executes via RCON, and captures the full multi-turn +conversation as training data. Only keeps examples where all commands succeed. + +This produces "distilled" data — the model's best outputs, validated by RCON. +""" + +import json +import random +import re +import sys +import time +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +import requests +from agent.tools.persistent_rcon import get_rcon +from agent.tools.tool_schemas import qwen3_tools_block +from agent.prompts.system_prompts import SYNTAX_RULES, RISK_GRADIENT + +OUTPUT_PATH = PROJECT_ROOT / "data" / "processed" / "tool_training_v05.jsonl" + +TOOLS_BLOCK = qwen3_tools_block() +SYSTEM = ( + "/no_think\n" + "You are a Minecraft 1.21 command translator for a Paper server.\n" + "Plugins: FastAsyncWorldEdit, WorldGuard, CoreProtect, EssentialsX, Vault, LuckPerms.\n\n" + "You have tools. To call one:\n" + "\n{\"name\": \"tool_name\", \"arguments\": {...}}\n\n\n" + "Available: rcon.execute, minecraft.wiki_lookup, plugin.docs_lookup, " + "minecraft.changelog_lookup, world.player_info, world.server_state, " + "world.nearby_entities, memory.read, memory.write, " + "script.write, script.validate, script.execute, script.read, script.list, " + "script.delete, script.schedule.\n\n" + "After tool calls, respond with JSON:\n" + "{\"risk_level\": <0-5>, \"commands\": [...], \"reasoning\": \"...\"}\n\n" + "PERMISSION LEVEL: 4 (generous).\n" + SYNTAX_RULES + RISK_GRADIENT +) + +SYSTEM_GOD = ( + "/no_think\n" + "You are God in a Minecraft server with full tool access.\n" + "Return JSON: {\"risk_level\": <0-5>, \"message\": \"...\", \"commands\": [...], \"reasoning\": \"...\"}\n\n" + + SYNTAX_RULES + "\n" + TOOLS_BLOCK +) + +PLAYERS = ["slingshooter08", "Ace13245", "TheBigBoss", "xXDragonSlayerXx"] + +# Comprehensive prompt set — every category we need good data for +PROMPTS = { + "basic_commands": [ + "sudo give me a diamond sword", + "sudo give me 64 golden apples", + "sudo give me a stack of oak planks", + "sudo give me an elytra", + "sudo give me a spyglass", + "sudo give me a recovery compass", + "sudo give me a bundle", + "sudo set time to noon", + "sudo set time to midnight", + "sudo clear weather for a week", + "sudo make it thunder", + "sudo kill all hostile mobs", + "sudo kill all items on the ground", + "sudo gamemode creative", + "sudo gamemode survival", + "sudo gamemode spectator", + ], + "enchanted_gear": [ + "sudo give me a diamond sword with sharpness 5, unbreaking 3, mending, and looting 3", + "sudo give me a netherite pickaxe with efficiency 5, fortune 3, unbreaking 3, mending", + "sudo give me a bow with power 5, infinity, flame, punch 2", + "sudo full netherite armor with protection 4, unbreaking 3, mending on every piece", + "sudo give me boots with feather falling 4, depth strider 3, soul speed 3", + "sudo give me a trident with loyalty 3 and channeling", + "sudo give me a trident with riptide 3", + "sudo give me a crossbow with multishot and quick charge 3", + "sudo give me a mace with density 5 and wind burst 3", + "sudo best fishing rod possible", + "sudo give me a shield with unbreaking 3 and mending", + ], + "effects": [ + "sudo give me speed 3 for 10 minutes", + "sudo night vision permanently", + "sudo make me invisible for 5 minutes", + "sudo give me fire resistance for an hour", + "sudo give everyone online regeneration 2", + "sudo give me haste 2 for 10 minutes", + "sudo slow falling for 60 seconds", + "sudo give me water breathing forever", + "sudo give me strength 2 and resistance 2 for 5 minutes", + "sudo clear all my effects", + ], + "teleport_position": [ + "sudo tp me to 0 100 0", + "sudo tp me to the nether", + "sudo tp everyone to spawn", + "sudo teleport me 100 blocks north", + "sudo tp me up 50 blocks", + "sudo set my spawn point here", + ], + "building": [ + "sudo fill a 10x10 platform of stone under me", + "sudo place a beacon at my location", + "sudo build a small cobblestone room around me", + "sudo fill the area below me with water", + "sudo make a glass dome over me", + "sudo place 4 lanterns around me", + "sudo clear a 20 block area above me", + ], + "entities": [ + "sudo summon a horse with a saddle", + "sudo summon 5 cows near me", + "sudo summon a villager", + "sudo spawn an iron golem", + "sudo summon a warden 20 blocks away", + "sudo summon a wither", + "sudo kill all zombies within 50 blocks", + "sudo kill all creepers near me", + ], + "worldguard": [ + "sudo create a region called my-base and set pvp deny", + "sudo prevent mob spawning in the spawn region", + "sudo set a greeting message for spawn: Welcome to the server!", + "sudo deny entry to non-members in the vault region", + "sudo list all regions", + "sudo allow TNT in the arena", + "sudo prevent fire spread globally", + "sudo make a healing zone at spawn", + ], + "coreprotect": [ + "sudo enable block inspector", + "sudo rollback the last hour of changes", + "sudo rollback what TheBigBoss did in the last 30 minutes", + "sudo lookup who placed blocks near me today", + "sudo rollback TNT damage from the last 2 hours", + "sudo check coreprotect status", + "sudo restore what was rolled back", + ], + "essentialsx": [ + "sudo set my home here", + "sudo create a warp called arena", + "sudo give Ace13245 1000 coins", + "sudo check my balance", + "sudo heal me", + "sudo feed me", + "sudo repair my held item", + "sudo set my nickname to DragonLord", + "sudo broadcast Welcome to the server!", + "sudo god mode on", + "sudo fly mode on", + ], + "luckperms": [ + "sudo create a VIP group", + "sudo add Ace13245 to VIP", + "sudo give VIP permission to fly", + "sudo give me temporary VIP for 24 hours", + "sudo set VIP prefix to gold [VIP]", + "sudo list all permission groups", + "sudo create a builder group with worldedit access", + ], + "fawe": [ + "sudo make a glass sphere radius 8", + "sudo hollow stone sphere radius 10", + "sudo cylinder of quartz 5 wide 12 tall", + "sudo replace all stone with deepslate in selection", + "sudo smooth the terrain 5 iterations", + "sudo drain water within 20 blocks", + "sudo sandstone pyramid 8 tall", + "sudo undo my last worldedit operation", + ], + "god_prayers": [ + "pray oh great one, bless me with diamonds", + "pray lord, protect me from the monsters of the night", + "pray I offer this sacrifice of 64 wheat, grant me your favor", + "pray god please make it stop raining", + "pray smite the wicked TheBigBoss for griefing my base", + "pray heal me, I am near death", + "pray give me the strength to slay the ender dragon", + "pray I am lost in a cave, guide me to the surface", + ], + "error_prone": [ + "sudo give me a bed", + "sudo give me steak", + "sudo give me cooked beef", + "sudo effect give me speed", + "sudo give me a log", + "sudo fill with stone 10", + "sudo tp me to spawn", + "sudo give @s diamond 1", + ], + "complex_multi": [ + "sudo gear me up for the nether: armor, weapons, food, fire resistance", + "sudo prepare me for the end fight: bow, arrows, blocks, pearls, slow falling", + "sudo set up a new player kit: stone tools, food, bed, torches", + "sudo create a mob farm: platform, water channels, collection hopper", + ], +} + + +def query_model_with_tools(prompt, player, ollama_url, model, rcon, mode="sudo", max_steps=6): + """Send prompt to model, let it call tools, execute them, capture full chain.""" + system = SYSTEM_GOD if mode == "god" else SYSTEM + + messages = [ + {"role": "system", "content": system}, + {"role": "user", "content": f"Player {player}: {prompt}"}, + ] + + tool_trace = [] + all_rcon_results = [] + + for step in range(max_steps): + try: + r = requests.post(f"{ollama_url}/api/chat", json={ + "model": model, + "messages": messages, + "stream": False, + "options": {"temperature": 0.2, "num_predict": 800}, + }, timeout=90) + raw = r.json()["message"]["content"] + except Exception as e: + return None + + raw = re.sub(r'[\s\S]*?\s*', '', raw) + + # Check for tool calls + tool_matches = re.findall(r'\s*(\{.*?\})\s*', raw, re.DOTALL) + + if not tool_matches: + # Final response + try: + parsed = json.loads(raw) + return { + "messages": messages + [{"role": "assistant", "content": raw}], + "commands": parsed.get("commands", []), + "message": parsed.get("message", ""), + "reasoning": parsed.get("reasoning", ""), + "tool_trace": tool_trace, + "rcon_results": all_rcon_results, + } + except json.JSONDecodeError: + return None + + for tc_json in tool_matches: + try: + tc = json.loads(tc_json) + tool_name = tc.get("name", "") + tool_args = tc.get("arguments", {}) + except json.JSONDecodeError: + continue + + # Execute tool + if tool_name == "rcon.execute": + cmd = tool_args.get("command", "") + try: + result_text = rcon.command(cmd) + is_err = any(e in result_text for e in ("<--[HERE]", "Unknown", "Incorrect", "Expected")) + result = {"success": not is_err, "result": result_text[:300]} + all_rcon_results.append({"cmd": cmd, "ok": not is_err, "result": result_text[:200]}) + except Exception as e: + result = {"success": False, "result": str(e)} + all_rcon_results.append({"cmd": cmd, "ok": False, "result": str(e)}) + elif tool_name == "minecraft.wiki_lookup": + try: + from agent.tools.knowledge_lookup import handle_knowledge_tool + result = handle_knowledge_tool(tool_name, tool_args) + except Exception: + result = {"content": "Wiki unavailable", "url": "", "ok": False} + elif tool_name in ("plugin.docs_lookup", "minecraft.changelog_lookup", "paper.docs_lookup"): + try: + from agent.tools.knowledge_lookup import handle_knowledge_tool + result = handle_knowledge_tool(tool_name, tool_args) + except Exception: + result = {"content": "Docs unavailable", "url": "", "ok": False} + else: + result = {"ok": True, "result": "simulated"} + + tool_trace.append({"tool": tool_name, "args": str(tool_args)[:100], "step": step}) + messages.append({"role": "assistant", "content": f"\n{json.dumps(tc)}\n"}) + messages.append({"role": "tool", "content": json.dumps(result)[:2000]}) + + return None # Ran out of steps + + +def main(): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--ollama-url", default="http://localhost:11434") + parser.add_argument("--model", default="mortdecai:0.5.0") + parser.add_argument("--rcon-host", default="192.168.0.244") + parser.add_argument("--rcon-port", type=int, default=25578) + parser.add_argument("--rcon-pass", default="REDACTED_RCON") + args = parser.parse_args() + + rcon = get_rcon(args.rcon_host, args.rcon_port, args.rcon_pass) + print(f"Regenerating tool data with {args.model}") + print(f"RCON: {args.rcon_host}:{args.rcon_port}") + + all_examples = [] + stats = {"total": 0, "kept": 0, "failed": 0, "no_response": 0} + + for category, prompts in PROMPTS.items(): + print(f"\n── {category} ({len(prompts)} prompts) ──") + for prompt in prompts: + player = random.choice(PLAYERS) + mode = "god" if prompt.startswith("pray ") else "sudo" + + result = query_model_with_tools(prompt, player, args.ollama_url, args.model, rcon, mode) + stats["total"] += 1 + + if not result: + stats["no_response"] += 1 + print(f" SKIP: {prompt[:50]} (no response)") + continue + + rcon_ok = sum(1 for r in result["rcon_results"] if r["ok"]) + rcon_total = len(result["rcon_results"]) + tools_used = len(result["tool_trace"]) + + if rcon_total == 0 and tools_used == 0: + stats["no_response"] += 1 + print(f" SKIP: {prompt[:50]} (empty)") + continue + + all_success = rcon_total > 0 and all(r["ok"] for r in result["rcon_results"]) + + if all_success or (rcon_ok > 0 and rcon_ok >= rcon_total * 0.7): + stats["kept"] += 1 + example = { + "id": f"v05-regen-{stats['total']:04d}", + "source": "model_distillation_v05", + "type": f"tool_{category}", + "messages": result["messages"], + "metadata": { + "model": args.model, + "category": category, + "tools_used": tools_used, + "rcon_total": rcon_total, + "rcon_success": rcon_ok, + "all_success": all_success, + }, + } + all_examples.append(example) + print(f" KEPT: {prompt[:50]} ({rcon_ok}/{rcon_total} cmds, {tools_used} tools)") + else: + stats["failed"] += 1 + print(f" FAIL: {prompt[:50]} ({rcon_ok}/{rcon_total} cmds)") + + time.sleep(0.2) + + print(f"\n{'='*60}") + print(f"Total: {stats['total']}, Kept: {stats['kept']}, Failed: {stats['failed']}, Empty: {stats['no_response']}") + print(f"Quality: {100*stats['kept']//max(stats['total'],1)}%") + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, "w") as f: + for ex in all_examples: + f.write(json.dumps(ex, ensure_ascii=False) + "\n") + print(f"Written to {OUTPUT_PATH}") + + +if __name__ == "__main__": + main()