0.5.0 bake-off results, knowledge lookup tools, training progress chart

Bake-off (0.5.0 vs 0.4.0):
- Overall: 46.8% vs 45.2% (+1.6%), 0 errors vs 2
- Enchantments: +47% (20% → 67%)
- EssentialsX: +60% (0% → 60%)
- Effects: +25% (0% → 25%)
- Regressions: fill_build -67%, world -20%

Knowledge Lookup Tools (4 new):
- plugin.docs_lookup: WorldGuard, WorldEdit, CoreProtect, EssentialsX, LuckPerms docs
- minecraft.changelog_lookup: version history from Minecraft Wiki
- paper.docs_lookup: Paper server-specific documentation
- Wired into gateway model-driven tool loop and exploration self-play

Exploration Self-Play:
- General (vanilla MC) and plugins focus modes
- Wiki-grounded: model researches before acting, validates through RCON
- 2,243 exploration examples generated, 150 kept after quality filtering

Training Progress Chart:
- SVG chart showing training examples and inverse loss across versions
- Added to MODEL_CARD.md for Gitea display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mortdecai
2026-03-21 15:28:09 -04:00
parent da8f557219
commit f5118505b1
10 changed files with 3215 additions and 20 deletions
+356
View File
@@ -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'<script[^>]*>[\s\S]*?</script>', '', html)
text = re.sub(r'<style[^>]*>[\s\S]*?</style>', '', html)
text = re.sub(r'<nav[^>]*>[\s\S]*?</nav>', '', text)
text = re.sub(r'<header[^>]*>[\s\S]*?</header>', '', text)
text = re.sub(r'<footer[^>]*>[\s\S]*?</footer>', '', 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}
+89
View File
@@ -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": (