#!/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()