f5118505b1
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>
357 lines
13 KiB
Python
357 lines
13 KiB
Python
"""
|
|
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}
|