""" 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}