Add retrieval-grounded sudo flow and execution feedback loop
This commit is contained in:
@@ -53,6 +53,10 @@ Gateway hardening currently included:
|
|||||||
- SQLite-backed session persistence across gateway restarts
|
- SQLite-backed session persistence across gateway restarts
|
||||||
- Command sanitization at gateway return time (strips leading `/`, rejects prose/non-command lines)
|
- Command sanitization at gateway return time (strips leading `/`, rejects prose/non-command lines)
|
||||||
- Mode-specific command family filtering and command dedupe/cap
|
- Mode-specific command family filtering and command dedupe/cap
|
||||||
|
- Localized knowledge retrieval for tool mode:
|
||||||
|
- local corpus under `/var/lib/mc-langgraph-gateway/knowledge`
|
||||||
|
- indexed search + document snippet retrieval (`local.search` -> `local.read`)
|
||||||
|
- optional bootstrap download of Minecraft/Paper/WorldEdit docs at startup
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -115,6 +119,8 @@ Info lookup mode via sudo:
|
|||||||
|
|
||||||
Lookup mode is information-only (wiki/web retrieval + optional justification), and does not execute game commands.
|
Lookup mode is information-only (wiki/web retrieval + optional justification), and does not execute game commands.
|
||||||
|
|
||||||
|
For normal `sudo` translation, the gateway now also runs localized retrieval before command generation, so the model can ground command synthesis in local indexed docs rather than relying only on prompt memory.
|
||||||
|
|
||||||
These trigger multi-command `fill/setblock/give` sequences near the player and are optimized for Paper performance.
|
These trigger multi-command `fill/setblock/give` sequences near the player and are optimized for Paper performance.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -130,6 +136,10 @@ Important keys:
|
|||||||
- `sudo_max_commands`: default `12` (higher for build bursts)
|
- `sudo_max_commands`: default `12` (higher for build bursts)
|
||||||
- `interventions_per_day`: default `24`
|
- `interventions_per_day`: default `24`
|
||||||
- `first_login_benevolence_max_commands`: default `12`
|
- `first_login_benevolence_max_commands`: default `12`
|
||||||
|
- `bug_log_path`: default `/var/log/mc_aigod_paper_bug.log`
|
||||||
|
- `bug_log_service_lines`: recent AI action lines attached to each bug entry
|
||||||
|
- `tp_safety_enabled`: auto-wrap risky vertical teleports with fall protection
|
||||||
|
- `tp_safety_vertical_delta`: relative Y threshold for teleport safety wrapper
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+19
@@ -119,7 +119,26 @@ This section captures decisions and context accumulated across conversations wit
|
|||||||
- **Gamemode/effect sudo fix (2026-03-17):** Added `gamemode` to safe prefixes/whitelist and command palette, added syntax normalizer for malformed variants (`gameMode s`, short aliases, missing target) and execute-wrapped gamemode forms; confirmed valid RCON forms are `gamemode <mode> <player>` and `effect give <player> <effect> <duration> <amplifier> [hideParticles]`.
|
- **Gamemode/effect sudo fix (2026-03-17):** Added `gamemode` to safe prefixes/whitelist and command palette, added syntax normalizer for malformed variants (`gameMode s`, short aliases, missing target) and execute-wrapped gamemode forms; confirmed valid RCON forms are `gamemode <mode> <player>` and `effect give <player> <effect> <duration> <amplifier> [hideParticles]`.
|
||||||
- **AI-driven build/template flow (2026-03-17):** `process_sudo` now defaults to non-deterministic build planning (legacy deterministic builder templates disabled unless `sudo_deterministic_build_templates=true`), supports AI-emitted `template ...` meta-commands in sudo execution loop, and includes an AI template planner override for build/make/create prompts when initial translation does not emit template workflow steps.
|
- **AI-driven build/template flow (2026-03-17):** `process_sudo` now defaults to non-deterministic build planning (legacy deterministic builder templates disabled unless `sudo_deterministic_build_templates=true`), supports AI-emitted `template ...` meta-commands in sudo execution loop, and includes an AI template planner override for build/make/create prompts when initial translation does not emit template workflow steps.
|
||||||
- **Failed-execution retry pipeline upgrade (2026-03-17):** sudo now runs a generic retry-repair pass on ineffective results before intent fallback. Added TNT-specific repair for malformed `summon tnt ... <count>` outputs (expands into bounded multiple summon commands) and invulnerability-effect repair to valid protection effects. Added `tnt` to destructive-intent keywords so TNT requests can trigger destructive fallback when needed.
|
- **Failed-execution retry pipeline upgrade (2026-03-17):** sudo now runs a generic retry-repair pass on ineffective results before intent fallback. Added TNT-specific repair for malformed `summon tnt ... <count>` outputs (expands into bounded multiple summon commands) and invulnerability-effect repair to valid protection effects. Added `tnt` to destructive-intent keywords so TNT requests can trigger destructive fallback when needed.
|
||||||
|
- **TNT retry syntax correction (2026-03-17):** retry expansion for malformed TNT count commands initially emitted invalid relative coords with `~+1`/`~+2`; corrected generator to use valid forms (`~1`, `~2`) so repaired TNT summons no longer fail with `Expected double`.
|
||||||
|
- **User preference (2026-03-17):** prioritize context/prompt refinement and better model grounding over adding more deterministic hardcoded action paths.
|
||||||
|
- **Context-first refinement pass (2026-03-17):** added sudo failure memory (`sudo_failures`) and anti-repeat context block (`RECENT FAILED SUDO PATTERNS`) into paper sudo translator input and gateway sudo context payload; prompt rules now explicitly forbid repeated failed shapes, old enchant NBT syntax, and `summon tnt ... <count>` style outputs.
|
||||||
|
- **Prompt safety clarifications (2026-03-17):** updated paper God/system prompts to enforce `effect give ...` syntax, `weather clear|rain|thunder` only, and avoid accidental high vertical teleports in benevolent outputs unless explicitly requested.
|
||||||
|
- **Sudo scope grounding (2026-03-17):** paper sudo prompts now explicitly keep target scope narrow (`me/my` -> requesting player), discouraging broad `@a` outputs unless explicitly requested.
|
||||||
|
- **Unresolved post-update issues (2026-03-17):** (1) AI template planner override can trigger on non-build sudo intents (example: `make me invisible` routed into `template search/pick/build`), and (2) sudo follow-up questions like `did that command ...?` may not return an informational answer unless phrased with explicit lookup prefixes.
|
||||||
|
- **Resolved sudo routing issues (2026-03-17):** tightened build-intent detection so template planner override only triggers for actual structure/template intents (no longer on generic `make me ...` phrasing), and broadened sudo info-mode detection to treat natural question-style prompts (e.g., `did that command ...?`) as lookup queries even without explicit `lookup/search/wiki` prefix.
|
||||||
|
- **Remaining issues after routing fix deploy (2026-03-17):** (1) sudo still outputs old bow enchant NBT (`bow{Enchantments:[...]}`) in some runs and fails with trailing-data parse error, and (2) God prayer outputs still occasionally include unnecessary teleport actions (`tp ~ ~5 ~`) that users report as unhelpful side effects.
|
||||||
|
- **Bugfix pass after in-game retest (2026-03-17):** added command-repair normalizers for (a) old bow enchant NBT to 1.21 component syntax (`minecraft:bow[enchantments={...}]`), and (b) legacy fire fill syntax (`... fire 0 replace air` -> `... minecraft:fire replace air`), plus dynamic sudo command budget scaling for TNT prompts based on requested quantity (capped by `sudo_tnt_max_commands`, default 80).
|
||||||
|
- **Lookup and benevolent TP context refinements (2026-03-17):** lookup mode now has local fallback answering for contextual gameplay questions (e.g., invisibility vs mobs) when retrieval returns no hits, and prompts/gateway guidance now explicitly discourage teleport use in helpful responses unless movement is explicitly requested.
|
||||||
|
- **Post-retest fixes (2026-03-17):** added execute-tail syntax repair so command fixers apply inside `execute ... run ...` payloads (fixes old bow NBT + fill fire variants emitted under execute wrappers), and added TNT quantity expansion from prompt count for summon-heavy intents (bounded by `sudo_tnt_max_commands`, default 80) when model output under-produces summons.
|
||||||
|
- **Retest outcome (2026-03-17 late):** bow repair now works in live runs (`give strongest bow` successfully converted old NBT to `minecraft:bow[enchantments={...}]` and delivered item). Remaining issues: fire-spread requests still often execute as no-op/invalid hybrid fill chains (`execute ... run fill ... fire ...` with mixed legacy args), and TNT intent can still collapse into single-command failure then destructive fallback (large `fill ... air` + few TNT) instead of honoring requested count semantics.
|
||||||
- **God voice update (2026-03-17):** Increased default God persona emphasis on irony, dark humor, and sarcastic one-liners in both command and message system prompts (vanilla + Paper variants) while keeping command strictness unchanged.
|
- **God voice update (2026-03-17):** Increased default God persona emphasis on irony, dark humor, and sarcastic one-liners in both command and message system prompts (vanilla + Paper variants) while keeping command strictness unchanged.
|
||||||
|
- **Bug-log triage (2026-03-17):** `bug_log` entry confirmed an unintended-feeling movement reward in prayer flow (`execute as slingshooter08 run tp slingshooter08 ~ ~10 ~`) during a build-oriented prayer; prioritize pray-path teleport safety guards and intent alignment.
|
||||||
|
- **Bug follow-up (2026-03-17):** second `bug_log` entry reported God feeling "too nice" after greedy follow-up prayer; prompt context updated to bias repeated greedy demands toward corrective responses (rebuke/debuff/symbolic punishment) instead of extra rewards.
|
||||||
|
- **Teleport safety guard (2026-03-17):** added execution-time TP safety wrapper in prayer/intervention path (`execute_response`) to auto-apply `slow_falling` + `resistance` before risky upward teleports, reducing accidental player death from unintended high drops.
|
||||||
|
- **Bug-log signal upgrade (2026-03-17):** `bug_log` now writes filtered raw server log lines (RCON thread noise removed) and includes recent AI action lines from `/var/log/mc_aigod_paper.log` for better root-cause visibility.
|
||||||
|
- **Unaddressed bug triage (2026-03-17):** third `bug_log` entry ("output not in correct format") maps to prayer execution using invalid command `weather storm` (RCON: Incorrect argument). Current validator allows `weather` prefix but lacks argument normalization (`storm` -> `rain`) and no retry/repair pass for pray-path commands.
|
||||||
|
- **Weather normalization fix (2026-03-17):** added explicit prompt/context rule and code normalizer mapping `weather storm|rainstorm|thunderstorm` -> `weather thunder` before validation/execution (paper + vanilla), then redeployed both services.
|
||||||
|
- **New bug cluster observed (2026-03-17):** additional paper `bug_log` entries report failed command outcomes around selector-based targets and legacy enchant syntax in sudo (`minecraft:bow{Enchantments:...}`), indicating retry/repair still needed for 1.21 enchantment conversion and selector reliability feedback.
|
||||||
|
|
||||||
### Infrastructure decisions
|
### Infrastructure decisions
|
||||||
|
|
||||||
|
|||||||
+319
-5
@@ -13,6 +13,7 @@ Execution safety remains in mc_aigod_paper.py.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -20,6 +21,8 @@ import sqlite3
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@@ -76,12 +79,16 @@ class SessionState:
|
|||||||
_sessions: Dict[str, SessionState] = {}
|
_sessions: Dict[str, SessionState] = {}
|
||||||
_sessions_lock = threading.Lock()
|
_sessions_lock = threading.Lock()
|
||||||
|
|
||||||
|
_kb_lock = threading.Lock()
|
||||||
|
_kb_index_cache: Dict[str, Any] = {'loaded_at': 0.0, 'docs': []}
|
||||||
|
_KB_ALLOWED_EXTS = {'.md', '.txt', '.json'}
|
||||||
|
|
||||||
|
|
||||||
COMMAND_PREFIXES_BY_MODE = {
|
COMMAND_PREFIXES_BY_MODE = {
|
||||||
'sudo': [
|
'sudo': [
|
||||||
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ', 'execute ',
|
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ', 'execute ',
|
||||||
'kill ', 'summon ', 'tellraw ', 'worldborder ', 'fill ', 'setblock ',
|
'kill ', 'summon ', 'tellraw ', 'worldborder ', 'fill ', 'setblock ',
|
||||||
'clone ',
|
'clone ', 'gamemode ', 'template ',
|
||||||
],
|
],
|
||||||
'god': [
|
'god': [
|
||||||
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ', 'execute ',
|
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ', 'execute ',
|
||||||
@@ -106,6 +113,21 @@ def load_config() -> Dict[str, Any]:
|
|||||||
'command_model': 'qwen3-coder:30b',
|
'command_model': 'qwen3-coder:30b',
|
||||||
'tool_model': 'qwen2.5:1.5b',
|
'tool_model': 'qwen2.5:1.5b',
|
||||||
'session_ttl_seconds': 21600,
|
'session_ttl_seconds': 21600,
|
||||||
|
'knowledge_base_dir': '/var/lib/mc-langgraph-gateway/knowledge',
|
||||||
|
'knowledge_index_file': '/var/lib/mc-langgraph-gateway/knowledge/index.json',
|
||||||
|
'knowledge_auto_index_on_start': True,
|
||||||
|
'knowledge_bootstrap_on_start': True,
|
||||||
|
'knowledge_bootstrap_urls': [
|
||||||
|
'https://minecraft.wiki/w/Commands/fill',
|
||||||
|
'https://minecraft.wiki/w/Commands/setblock',
|
||||||
|
'https://minecraft.wiki/w/Commands/clone',
|
||||||
|
'https://minecraft.wiki/w/Commands/summon',
|
||||||
|
'https://minecraft.wiki/w/Commands/execute',
|
||||||
|
'https://minecraft.wiki/w/TNT',
|
||||||
|
'https://minecraft.wiki/w/Explosion',
|
||||||
|
'https://minecraft.wiki/w/Tutorial:Worldedit',
|
||||||
|
],
|
||||||
|
'knowledge_max_doc_bytes': 200000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -114,6 +136,228 @@ DB_PATH = CFG.get('session_db_path', '/var/lib/mc-langgraph-gateway/sessions.db'
|
|||||||
_db_lock = threading.Lock()
|
_db_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_root() -> str:
|
||||||
|
root = str(CFG.get('knowledge_base_dir', '/var/lib/mc-langgraph-gateway/knowledge')).strip()
|
||||||
|
return root or '/var/lib/mc-langgraph-gateway/knowledge'
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_index_path() -> str:
|
||||||
|
path = str(CFG.get('knowledge_index_file', '')).strip()
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
return os.path.join(_kb_root(), 'index.json')
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_tokenize(text: str) -> List[str]:
|
||||||
|
toks = re.findall(r'[a-z0-9_]{2,}', (text or '').lower())
|
||||||
|
if not toks:
|
||||||
|
return []
|
||||||
|
out: List[str] = []
|
||||||
|
seen = set()
|
||||||
|
for t in toks:
|
||||||
|
if t in seen:
|
||||||
|
continue
|
||||||
|
seen.add(t)
|
||||||
|
out.append(t)
|
||||||
|
if len(out) >= 300:
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_html_to_text(html: str) -> str:
|
||||||
|
body = re.sub(r'(?is)<script.*?>.*?</script>', ' ', html or '')
|
||||||
|
body = re.sub(r'(?is)<style.*?>.*?</style>', ' ', body)
|
||||||
|
body = re.sub(r'(?is)<[^>]+>', ' ', body)
|
||||||
|
body = re.sub(r'\s+', ' ', body).strip()
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_slug(s: str) -> str:
|
||||||
|
n = re.sub(r'[^a-zA-Z0-9._-]+', '_', (s or '').strip())
|
||||||
|
n = n.strip('._-')
|
||||||
|
return (n[:80] or 'doc').lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_fetch_url(url: str) -> Dict[str, Any]:
|
||||||
|
max_bytes = int(CFG.get('knowledge_max_doc_bytes', 200000))
|
||||||
|
r = requests.get(url, timeout=25)
|
||||||
|
r.raise_for_status()
|
||||||
|
ct = (r.headers.get('content-type') or '').lower()
|
||||||
|
raw = r.content[:max_bytes]
|
||||||
|
if 'html' in ct:
|
||||||
|
text = _kb_html_to_text(raw.decode(errors='replace'))
|
||||||
|
else:
|
||||||
|
text = raw.decode(errors='replace')
|
||||||
|
title = ''
|
||||||
|
m = re.search(r'(?is)<title>(.*?)</title>', r.text if 'html' in ct else '')
|
||||||
|
if m:
|
||||||
|
title = re.sub(r'\s+', ' ', m.group(1)).strip()
|
||||||
|
return {'title': title, 'text': text}
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_ingest_url(url: str) -> Dict[str, Any]:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
host = (parsed.netloc or '').lower()
|
||||||
|
if host not in set(str(h).lower() for h in CFG.get('knowledge_allowed_hosts', [
|
||||||
|
'minecraft.wiki', 'www.minecraft.wiki', 'docs.papermc.io', 'intellectualsites.github.io', 'enginehub.org', 'worldedit.enginehub.org'
|
||||||
|
])):
|
||||||
|
return {'ok': False, 'error': f'host not allowed: {host}'}
|
||||||
|
try:
|
||||||
|
fetched = _kb_fetch_url(url)
|
||||||
|
text = (fetched.get('text') or '').strip()
|
||||||
|
if len(text) < 80:
|
||||||
|
return {'ok': False, 'error': 'document too short'}
|
||||||
|
title = fetched.get('title') or os.path.basename(parsed.path) or host
|
||||||
|
root = Path(_kb_root())
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
digest = hashlib.sha1(url.encode()).hexdigest()[:12]
|
||||||
|
fname = f"{_kb_slug(title)}_{digest}.md"
|
||||||
|
out = root / fname
|
||||||
|
out.write_text(f"# {title}\n\nSource: {url}\n\n{text}\n", encoding='utf-8')
|
||||||
|
return {'ok': True, 'path': str(out), 'source': url, 'title': title}
|
||||||
|
except Exception as e:
|
||||||
|
return {'ok': False, 'error': str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_build_index() -> Dict[str, Any]:
|
||||||
|
root = Path(_kb_root())
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
docs = []
|
||||||
|
for p in root.rglob('*'):
|
||||||
|
if not p.is_file() or p.suffix.lower() not in _KB_ALLOWED_EXTS:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
text = p.read_text(encoding='utf-8', errors='replace')
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
title = p.name
|
||||||
|
m = re.search(r'^#\s+(.+)$', text, re.MULTILINE)
|
||||||
|
if m:
|
||||||
|
title = m.group(1).strip()[:120]
|
||||||
|
snippet = re.sub(r'\s+', ' ', text[:800]).strip()
|
||||||
|
tokens = _kb_tokenize(text)
|
||||||
|
rel = str(p.relative_to(root))
|
||||||
|
doc_id = hashlib.sha1(rel.encode()).hexdigest()[:12]
|
||||||
|
docs.append({
|
||||||
|
'id': doc_id,
|
||||||
|
'path': rel,
|
||||||
|
'title': title,
|
||||||
|
'snippet': snippet[:260],
|
||||||
|
'tokens': tokens,
|
||||||
|
'mtime': p.stat().st_mtime,
|
||||||
|
})
|
||||||
|
|
||||||
|
out = {'generated_at': time.time(), 'docs': docs}
|
||||||
|
idx = Path(_kb_index_path())
|
||||||
|
idx.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
idx.write_text(json.dumps(out, ensure_ascii=True), encoding='utf-8')
|
||||||
|
with _kb_lock:
|
||||||
|
_kb_index_cache['loaded_at'] = time.time()
|
||||||
|
_kb_index_cache['docs'] = docs
|
||||||
|
return {'ok': True, 'count': len(docs), 'path': str(idx)}
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_load_index(force: bool = False) -> List[Dict[str, Any]]:
|
||||||
|
with _kb_lock:
|
||||||
|
if _kb_index_cache.get('docs') and not force:
|
||||||
|
return list(_kb_index_cache['docs'])
|
||||||
|
idx = Path(_kb_index_path())
|
||||||
|
if not idx.exists():
|
||||||
|
_kb_build_index()
|
||||||
|
try:
|
||||||
|
data = json.loads(idx.read_text(encoding='utf-8'))
|
||||||
|
except Exception:
|
||||||
|
_kb_build_index()
|
||||||
|
data = json.loads(idx.read_text(encoding='utf-8'))
|
||||||
|
docs = data.get('docs') or []
|
||||||
|
with _kb_lock:
|
||||||
|
_kb_index_cache['loaded_at'] = time.time()
|
||||||
|
_kb_index_cache['docs'] = docs
|
||||||
|
return docs
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_bootstrap_if_needed() -> None:
|
||||||
|
if not bool(CFG.get('knowledge_bootstrap_on_start', True)):
|
||||||
|
return
|
||||||
|
root = Path(_kb_root())
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
existing = [p for p in root.rglob('*') if p.is_file() and p.suffix.lower() in _KB_ALLOWED_EXTS]
|
||||||
|
if existing:
|
||||||
|
return
|
||||||
|
urls = CFG.get('knowledge_bootstrap_urls', []) or []
|
||||||
|
if not urls:
|
||||||
|
return
|
||||||
|
ok = 0
|
||||||
|
for url in urls:
|
||||||
|
res = _kb_ingest_url(str(url))
|
||||||
|
if res.get('ok'):
|
||||||
|
ok += 1
|
||||||
|
log.info('knowledge bootstrap completed: %d/%d docs ingested', ok, len(urls))
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_search(query: str, limit: int = 5) -> List[Dict[str, Any]]:
|
||||||
|
docs = _kb_load_index()
|
||||||
|
q_tokens = set(_kb_tokenize(query))
|
||||||
|
if not q_tokens:
|
||||||
|
return []
|
||||||
|
scored = []
|
||||||
|
q_lower = query.lower()
|
||||||
|
for d in docs:
|
||||||
|
tokens = set(d.get('tokens') or [])
|
||||||
|
overlap = len(q_tokens.intersection(tokens))
|
||||||
|
if overlap <= 0:
|
||||||
|
continue
|
||||||
|
score = overlap
|
||||||
|
if q_lower in (d.get('title', '').lower()):
|
||||||
|
score += 3
|
||||||
|
if q_lower in (d.get('snippet', '').lower()):
|
||||||
|
score += 1
|
||||||
|
scored.append((score, d))
|
||||||
|
scored.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
out = []
|
||||||
|
for _, d in scored[:max(1, limit)]:
|
||||||
|
out.append({
|
||||||
|
'doc_id': d.get('id'),
|
||||||
|
'title': d.get('title'),
|
||||||
|
'path': d.get('path'),
|
||||||
|
'snippet': d.get('snippet'),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _kb_read(doc_id: str, query: str = '') -> Dict[str, Any]:
|
||||||
|
docs = _kb_load_index()
|
||||||
|
hit = None
|
||||||
|
for d in docs:
|
||||||
|
if d.get('id') == doc_id:
|
||||||
|
hit = d
|
||||||
|
break
|
||||||
|
if not hit:
|
||||||
|
return {'ok': False, 'error': 'doc_id not found', 'results': []}
|
||||||
|
|
||||||
|
full = Path(_kb_root()) / str(hit.get('path'))
|
||||||
|
if not full.exists():
|
||||||
|
return {'ok': False, 'error': 'file missing', 'results': []}
|
||||||
|
text = full.read_text(encoding='utf-8', errors='replace')
|
||||||
|
q = (query or '').strip().lower()
|
||||||
|
if q and q in text.lower():
|
||||||
|
idx = text.lower().find(q)
|
||||||
|
start = max(0, idx - 350)
|
||||||
|
end = min(len(text), idx + 650)
|
||||||
|
excerpt = text[start:end]
|
||||||
|
else:
|
||||||
|
excerpt = text[:1000]
|
||||||
|
return {
|
||||||
|
'ok': True,
|
||||||
|
'results': [{
|
||||||
|
'doc_id': doc_id,
|
||||||
|
'title': hit.get('title'),
|
||||||
|
'path': hit.get('path'),
|
||||||
|
'text': re.sub(r'\s+', ' ', excerpt).strip(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _db_enabled() -> bool:
|
def _db_enabled() -> bool:
|
||||||
return bool(CFG.get('session_persistence_enabled', True))
|
return bool(CFG.get('session_persistence_enabled', True))
|
||||||
|
|
||||||
@@ -407,13 +651,35 @@ def tool_wiki_lookup(query: str) -> Dict[str, Any]:
|
|||||||
return {'ok': False, 'error': str(e), 'results': []}
|
return {'ok': False, 'error': str(e), 'results': []}
|
||||||
|
|
||||||
|
|
||||||
def _tool_router(user_text: str, max_steps: int) -> List[Dict[str, Any]]:
|
def tool_local_search(query: str) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
rows = _kb_search(query, limit=5)
|
||||||
|
return {'ok': True, 'results': rows}
|
||||||
|
except Exception as e:
|
||||||
|
return {'ok': False, 'error': str(e), 'results': []}
|
||||||
|
|
||||||
|
|
||||||
|
def tool_local_read(doc_id: str, query: str = '') -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return _kb_read(doc_id, query)
|
||||||
|
except Exception as e:
|
||||||
|
return {'ok': False, 'error': str(e), 'results': []}
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_router(user_text: str, max_steps: int, mode: str, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""Very small bounded heuristic tool planner."""
|
"""Very small bounded heuristic tool planner."""
|
||||||
text = user_text.lower()
|
text = user_text.lower()
|
||||||
calls: List[Dict[str, Any]] = []
|
calls: List[Dict[str, Any]] = []
|
||||||
if max_steps <= 0:
|
if max_steps <= 0:
|
||||||
return calls
|
return calls
|
||||||
|
|
||||||
|
if mode == 'sudo':
|
||||||
|
q = user_text
|
||||||
|
req = str((context or {}).get('request') or '').strip()
|
||||||
|
if req:
|
||||||
|
q = req
|
||||||
|
calls.append({'tool': 'local.search', 'query': q})
|
||||||
|
|
||||||
if any(k in text for k in ['wiki', 'minecraft', 'item id', 'recipe', 'craft']):
|
if any(k in text for k in ['wiki', 'minecraft', 'item id', 'recipe', 'craft']):
|
||||||
calls.append({'tool': 'minecraft.wiki_lookup', 'query': user_text})
|
calls.append({'tool': 'minecraft.wiki_lookup', 'query': user_text})
|
||||||
|
|
||||||
@@ -433,7 +699,15 @@ def _commands_prompt(mode: str) -> str:
|
|||||||
'You are a Minecraft command translator. Return ONLY JSON: {"commands": ["..."]}.\n'
|
'You are a Minecraft command translator. Return ONLY JSON: {"commands": ["..."]}.\n'
|
||||||
f'Allowed command prefixes: {allowed}.\n'
|
f'Allowed command prefixes: {allowed}.\n'
|
||||||
'Output must be command strings only, no prose, no markdown, no labels, no leading slash.\n'
|
'Output must be command strings only, no prose, no markdown, no labels, no leading slash.\n'
|
||||||
'If unsafe/unknown, return empty commands.'
|
'Use TOOL results as your source of truth. Do not invent command syntax not supported by retrieved context.\n'
|
||||||
|
'Read context.sudo_failures and avoid repeating those exact failing patterns.\n'
|
||||||
|
'Never use old enchantment NBT {Enchantments:[...]} syntax; use item[enchantments={...}] format.\n'
|
||||||
|
'For TNT, never append a count to summon; use multiple summon commands instead.\n'
|
||||||
|
'Keep target scope narrow: if request is about "me/my", do not use @a unless explicitly requested.\n'
|
||||||
|
'You may output template workflow meta-commands: template search <query>, template pick <n> [name], template build <name>.\n'
|
||||||
|
'For build/make/create requests, prefer the template workflow instead of raw block-by-block commands.\n'
|
||||||
|
'If request is ambiguous or unsupported, choose a closest valid in-game workaround and keep scope bounded.\n'
|
||||||
|
'If still unsafe/unknown, return empty commands.'
|
||||||
)
|
)
|
||||||
|
|
||||||
if mode == 'god_system':
|
if mode == 'god_system':
|
||||||
@@ -441,6 +715,7 @@ def _commands_prompt(mode: str) -> str:
|
|||||||
'You are Minecraft divine system automation. Return ONLY JSON: {"commands": ["..."]}.\n'
|
'You are Minecraft divine system automation. Return ONLY JSON: {"commands": ["..."]}.\n'
|
||||||
f'Allowed command prefixes: {allowed}.\n'
|
f'Allowed command prefixes: {allowed}.\n'
|
||||||
'Output must be command strings only, no prose, no markdown, no labels, no leading slash.\n'
|
'Output must be command strings only, no prose, no markdown, no labels, no leading slash.\n'
|
||||||
|
'Use valid 1.21 syntax: effect give <player> ..., and weather is clear/rain/thunder only.\n'
|
||||||
'This mode is for intervention/first-login events. Prefer benevolent or thematic world actions.\n'
|
'This mode is for intervention/first-login events. Prefer benevolent or thematic world actions.\n'
|
||||||
'If you include kill commands, keep it to at most one player.'
|
'If you include kill commands, keep it to at most one player.'
|
||||||
)
|
)
|
||||||
@@ -449,6 +724,9 @@ def _commands_prompt(mode: str) -> str:
|
|||||||
'You are Minecraft God command planner. Return ONLY JSON: {"commands": ["..."]}.\n'
|
'You are Minecraft God command planner. Return ONLY JSON: {"commands": ["..."]}.\n'
|
||||||
f'Allowed command prefixes: {allowed}.\n'
|
f'Allowed command prefixes: {allowed}.\n'
|
||||||
'Output must be command strings only, no prose, no markdown, no labels, no leading slash.\n'
|
'Output must be command strings only, no prose, no markdown, no labels, no leading slash.\n'
|
||||||
|
'Use valid 1.21 syntax: effect give <player> ..., and weather is clear/rain/thunder only.\n'
|
||||||
|
'Avoid accidental lethal vertical teleports in benevolent responses unless explicitly requested.\n'
|
||||||
|
'Do not use tp in helpful responses unless user explicitly asks for movement.\n'
|
||||||
'Balance benevolence and judgment based on context.\n'
|
'Balance benevolence and judgment based on context.\n'
|
||||||
'Use valid Minecraft command syntax only.'
|
'Use valid Minecraft command syntax only.'
|
||||||
)
|
)
|
||||||
@@ -524,25 +802,53 @@ def run_pipeline(session: SessionState, req: MessageRequest) -> MessageResponse:
|
|||||||
user_blob = f"message: {user_text}\ncontext: {context_json}"
|
user_blob = f"message: {user_text}\ncontext: {context_json}"
|
||||||
|
|
||||||
session.messages.append({'role': req.role, 'content': user_blob})
|
session.messages.append({'role': req.role, 'content': user_blob})
|
||||||
|
|
||||||
|
# Feedback-only messages update session state without running LLM/tools.
|
||||||
|
if bool((req.context or {}).get('feedback_only', False)):
|
||||||
|
session.messages.append({
|
||||||
|
'role': 'assistant',
|
||||||
|
'content': json.dumps({'message': '', 'commands': []}, ensure_ascii=True)
|
||||||
|
})
|
||||||
|
_db_upsert_session(session)
|
||||||
|
return MessageResponse(message=None, commands=[], tool_trace=[])
|
||||||
|
|
||||||
_db_upsert_session(session)
|
_db_upsert_session(session)
|
||||||
|
|
||||||
tool_trace: List[Dict[str, Any]] = []
|
tool_trace: List[Dict[str, Any]] = []
|
||||||
tool_results_block = ''
|
tool_results_block = ''
|
||||||
|
|
||||||
if req.allow_tools:
|
if req.allow_tools:
|
||||||
calls = _tool_router(user_text, max(0, min(req.max_tool_steps, 6)))
|
calls = _tool_router(
|
||||||
|
user_text,
|
||||||
|
max(0, min(req.max_tool_steps, 6)),
|
||||||
|
session.mode,
|
||||||
|
req.context or {},
|
||||||
|
)
|
||||||
for c in calls:
|
for c in calls:
|
||||||
tool = c['tool']
|
tool = c['tool']
|
||||||
q = c['query']
|
q = c.get('query', '')
|
||||||
if tool == 'web.search':
|
if tool == 'web.search':
|
||||||
out = tool_web_search(q)
|
out = tool_web_search(q)
|
||||||
elif tool == 'minecraft.wiki_lookup':
|
elif tool == 'minecraft.wiki_lookup':
|
||||||
out = tool_wiki_lookup(q)
|
out = tool_wiki_lookup(q)
|
||||||
|
elif tool == 'local.search':
|
||||||
|
out = tool_local_search(q)
|
||||||
|
elif tool == 'local.read':
|
||||||
|
out = tool_local_read(str(c.get('doc_id', '')), q)
|
||||||
else:
|
else:
|
||||||
out = {'ok': False, 'error': 'unknown tool', 'results': []}
|
out = {'ok': False, 'error': 'unknown tool', 'results': []}
|
||||||
tool_trace.append({'tool': tool, 'input': q, 'ok': out.get('ok', False), 'results_count': len(out.get('results', []))})
|
tool_trace.append({'tool': tool, 'input': q, 'ok': out.get('ok', False), 'results_count': len(out.get('results', []))})
|
||||||
tool_results_block += f"\nTOOL {tool} query={q}\nRESULT={json.dumps(out, ensure_ascii=True)[:3000]}\n"
|
tool_results_block += f"\nTOOL {tool} query={q}\nRESULT={json.dumps(out, ensure_ascii=True)[:3000]}\n"
|
||||||
|
|
||||||
|
# localized retrieval hop: after index search, fetch one top document excerpt
|
||||||
|
if tool == 'local.search' and out.get('ok') and out.get('results') and len(tool_trace) < max(0, min(req.max_tool_steps, 6)):
|
||||||
|
top = out['results'][0]
|
||||||
|
doc_id = str(top.get('doc_id', ''))
|
||||||
|
if doc_id:
|
||||||
|
read_out = tool_local_read(doc_id, q)
|
||||||
|
tool_trace.append({'tool': 'local.read', 'input': doc_id, 'ok': read_out.get('ok', False), 'results_count': len(read_out.get('results', []))})
|
||||||
|
tool_results_block += f"\nTOOL local.read doc_id={doc_id}\nRESULT={json.dumps(read_out, ensure_ascii=True)[:3000]}\n"
|
||||||
|
|
||||||
# Commands call
|
# Commands call
|
||||||
cmd_messages = [
|
cmd_messages = [
|
||||||
{'role': 'system', 'content': _commands_prompt(session.mode)},
|
{'role': 'system', 'content': _commands_prompt(session.mode)},
|
||||||
@@ -618,4 +924,12 @@ def close_session(session_id: str):
|
|||||||
return {'closed': existed}
|
return {'closed': existed}
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
_kb_bootstrap_if_needed()
|
||||||
|
if bool(CFG.get('knowledge_auto_index_on_start', True)):
|
||||||
|
meta = _kb_build_index()
|
||||||
|
log.info('knowledge index ready: %s docs=%s', meta.get('path'), meta.get('count'))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning('knowledge bootstrap/index failed: %s', e)
|
||||||
|
|
||||||
_db_init()
|
_db_init()
|
||||||
|
|||||||
@@ -41,6 +41,10 @@
|
|||||||
"bug_log_path": "/var/log/mc_aigod_paper_bug.log",
|
"bug_log_path": "/var/log/mc_aigod_paper_bug.log",
|
||||||
"bug_log_event_lines": 40,
|
"bug_log_event_lines": 40,
|
||||||
"bug_log_raw_lines": 120,
|
"bug_log_raw_lines": 120,
|
||||||
|
"bug_log_service_lines": 30,
|
||||||
|
"tp_safety_enabled": true,
|
||||||
|
"tp_safety_vertical_delta": 8,
|
||||||
|
"tp_safety_absolute_y": 120,
|
||||||
"sudo_build_max_commands": 6,
|
"sudo_build_max_commands": 6,
|
||||||
"sudo_deterministic_build_templates": false,
|
"sudo_deterministic_build_templates": false,
|
||||||
"tp_border_guard_enabled": true,
|
"tp_border_guard_enabled": true,
|
||||||
|
|||||||
+329
-9
@@ -76,6 +76,10 @@ first_login_seen = set()
|
|||||||
SUDO_HISTORY_SIZE = 10
|
SUDO_HISTORY_SIZE = 10
|
||||||
sudo_history: deque = deque() # entries: (ts, player, prompt, translated_cmds, executed_cmds)
|
sudo_history: deque = deque() # entries: (ts, player, prompt, translated_cmds, executed_cmds)
|
||||||
|
|
||||||
|
# Sudo failure memory — last N failed command/result pairs, for anti-repeat context.
|
||||||
|
SUDO_FAILURE_SIZE = 20
|
||||||
|
sudo_failures: deque = deque() # entries: (ts, player, command, error)
|
||||||
|
|
||||||
_memory_lock = threading.Lock()
|
_memory_lock = threading.Lock()
|
||||||
|
|
||||||
# Gateway client session mapping (player+mode -> session_id)
|
# Gateway client session mapping (player+mode -> session_id)
|
||||||
@@ -980,19 +984,71 @@ def get_last_sudo_executed_command(player: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def add_sudo_failure(player: str, command: str, error: str):
|
||||||
|
"""Record a failed sudo command/result so future prompts can avoid repeats."""
|
||||||
|
c = (command or "").strip()[:220]
|
||||||
|
e = re.sub(r'\s+', ' ', (error or "")).strip()[:220]
|
||||||
|
if not c or not e:
|
||||||
|
return
|
||||||
|
with _memory_lock:
|
||||||
|
sudo_failures.append((time.time(), player, c, e))
|
||||||
|
while len(sudo_failures) > SUDO_FAILURE_SIZE:
|
||||||
|
sudo_failures.popleft()
|
||||||
|
|
||||||
|
|
||||||
|
def get_sudo_failures_block(player: str = "") -> str:
|
||||||
|
"""Return recent failed sudo commands as anti-pattern context."""
|
||||||
|
with _memory_lock:
|
||||||
|
entries = list(sudo_failures)
|
||||||
|
if player:
|
||||||
|
entries = [e for e in entries if e[1] == player]
|
||||||
|
if not entries:
|
||||||
|
return ""
|
||||||
|
now = time.time()
|
||||||
|
lines = []
|
||||||
|
for ts, p, cmd, err in entries[-8:]:
|
||||||
|
mins = int((now - ts) / 60)
|
||||||
|
lines.append(f" [{mins}m ago] {p}: {cmd} -> {err}")
|
||||||
|
return "\n=== RECENT FAILED SUDO PATTERNS ===\n" + "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def _bug_log_path(config) -> str:
|
def _bug_log_path(config) -> str:
|
||||||
return config.get("bug_log_path", "/var/log/mc_aigod_paper_bug.log")
|
return config.get("bug_log_path", "/var/log/mc_aigod_paper_bug.log")
|
||||||
|
|
||||||
|
|
||||||
def _read_recent_raw_log_lines(log_path: str, max_lines: int) -> list:
|
def _read_recent_raw_log_lines(log_path: str, max_lines: int) -> list:
|
||||||
lines = deque(maxlen=max_lines)
|
lines = deque(maxlen=max_lines * 4)
|
||||||
|
noise = re.compile(r'RCON (?:Listener|Client)|Thread RCON Client')
|
||||||
try:
|
try:
|
||||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
lines.append(line.rstrip('\n'))
|
clean = line.rstrip('\n')
|
||||||
|
if noise.search(clean):
|
||||||
|
continue
|
||||||
|
lines.append(clean)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [f"<unable to read raw server log: {e}>"]
|
return [f"<unable to read raw server log: {e}>"]
|
||||||
return list(lines)
|
return list(lines)[-max_lines:]
|
||||||
|
|
||||||
|
|
||||||
|
def _service_log_path(config) -> str:
|
||||||
|
return config.get("service_log_path", "/var/log/mc_aigod_paper.log")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_recent_service_action_lines(path: str, max_lines: int) -> list:
|
||||||
|
lines = deque(maxlen=max_lines * 8)
|
||||||
|
keep = re.compile(
|
||||||
|
r'Prayer from|SUDO from|Executing RCON:|SUDO execute:|RCON result:|SUDO result:|BUG_LOG from'
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
for line in f:
|
||||||
|
clean = line.rstrip('\n')
|
||||||
|
if keep.search(clean):
|
||||||
|
lines.append(clean)
|
||||||
|
except Exception as e:
|
||||||
|
return [f"<unable to read service log: {e}>"]
|
||||||
|
return list(lines)[-max_lines:]
|
||||||
|
|
||||||
|
|
||||||
def _format_recent_event_lines(max_events: int) -> list:
|
def _format_recent_event_lines(max_events: int) -> list:
|
||||||
@@ -1012,9 +1068,11 @@ def process_bug_log(player: str, description: str, config):
|
|||||||
|
|
||||||
event_count = int(config.get("bug_log_event_lines", 40))
|
event_count = int(config.get("bug_log_event_lines", 40))
|
||||||
raw_count = int(config.get("bug_log_raw_lines", 120))
|
raw_count = int(config.get("bug_log_raw_lines", 120))
|
||||||
|
service_count = int(config.get("bug_log_service_lines", 30))
|
||||||
|
|
||||||
recent_events = _format_recent_event_lines(event_count)
|
recent_events = _format_recent_event_lines(event_count)
|
||||||
raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count)
|
raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count)
|
||||||
|
service_lines = _read_recent_service_action_lines(_service_log_path(config), service_count)
|
||||||
bug_path = _bug_log_path(config)
|
bug_path = _bug_log_path(config)
|
||||||
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
|
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
|
||||||
@@ -1033,6 +1091,11 @@ def process_bug_log(player: str, description: str, config):
|
|||||||
bf.write("\n".join(recent_events) + "\n")
|
bf.write("\n".join(recent_events) + "\n")
|
||||||
else:
|
else:
|
||||||
bf.write("(no recent in-memory events captured)\n")
|
bf.write("(no recent in-memory events captured)\n")
|
||||||
|
bf.write("\n-- RECENT AI ACTIONS (SERVICE LOG) --\n")
|
||||||
|
if service_lines:
|
||||||
|
bf.write("\n".join(service_lines) + "\n")
|
||||||
|
else:
|
||||||
|
bf.write("(no AI service lines available)\n")
|
||||||
bf.write("\n-- RECENT RAW SERVER LOG LINES --\n")
|
bf.write("\n-- RECENT RAW SERVER LOG LINES --\n")
|
||||||
if raw_lines:
|
if raw_lines:
|
||||||
bf.write("\n".join(raw_lines) + "\n")
|
bf.write("\n".join(raw_lines) + "\n")
|
||||||
@@ -1382,6 +1445,8 @@ MOVEMENT:
|
|||||||
NEVER use: tp <player> minecraft:the_nether (this is wrong syntax)
|
NEVER use: tp <player> minecraft:the_nether (this is wrong syntax)
|
||||||
|
|
||||||
WORLD/ENVIRONMENT (affects all players):
|
WORLD/ENVIRONMENT (affects all players):
|
||||||
|
SYNTAX: weather <clear|rain|thunder> [duration]
|
||||||
|
NOTE: 'storm' is invalid; if intent says storm/rainstorm/thunderstorm use thunder.
|
||||||
time set day
|
time set day
|
||||||
time set night
|
time set night
|
||||||
weather clear 6000
|
weather clear 6000
|
||||||
@@ -1443,9 +1508,12 @@ def build_system_prompt(config):
|
|||||||
"- Do not ask a player to gather materials they clearly don't have access to or that are rare relative to their current situation.\n"
|
"- Do not ask a player to gather materials they clearly don't have access to or that are rare relative to their current situation.\n"
|
||||||
"- For give commands: use any valid Minecraft 1.21 item ID following the Item Naming Rules. Do not guess item IDs — consult the naming rules and common IDs list.\n"
|
"- For give commands: use any valid Minecraft 1.21 item ID following the Item Naming Rules. Do not guess item IDs — consult the naming rules and common IDs list.\n"
|
||||||
"- For all other commands: only use forms shown in the Command Palette. Do not invent new command types.\n"
|
"- For all other commands: only use forms shown in the Command Palette. Do not invent new command types.\n"
|
||||||
|
"- Weather values are only clear, rain, or thunder. If you mean storm/rainstorm/thunderstorm, output thunder.\n"
|
||||||
"- Reward humble, genuine prayers. Punish hubris, blasphemy, or naked greed.\n"
|
"- Reward humble, genuine prayers. Punish hubris, blasphemy, or naked greed.\n"
|
||||||
|
"- Repeated greedy demands after recent gifts should usually receive correction (rebuke, debuff, or symbolic punishment), not more rewards.\n"
|
||||||
"- Powerful rewards (netherite, enchanted_golden_apple, totem) must be rare.\n"
|
"- Powerful rewards (netherite, enchanted_golden_apple, totem) must be rare.\n"
|
||||||
"- kill {target} is reserved for extreme blasphemy only.\n"
|
"- kill {target} is reserved for extreme blasphemy only.\n"
|
||||||
|
"- Avoid lethal accidents from vertical teleports. If using a high upward teleport as punishment or spectacle, pair it with slow_falling or resistance unless explicit execution is intended.\n"
|
||||||
"- When angered, chain commands: thunder + lightning + debuffs = divine wrath.\n\n"
|
"- When angered, chain commands: thunder + lightning + debuffs = divine wrath.\n\n"
|
||||||
"=== COMMAND PALETTE ===\n"
|
"=== COMMAND PALETTE ===\n"
|
||||||
f"{COMMAND_PALETTE}\n"
|
f"{COMMAND_PALETTE}\n"
|
||||||
@@ -1566,6 +1634,10 @@ COMMANDS_SYSTEM_PROMPT = (
|
|||||||
"- kill is reserved for extreme blasphemy only.\n"
|
"- kill is reserved for extreme blasphemy only.\n"
|
||||||
"- For give: syntax is always give <player> minecraft:<item_id> <count>\n"
|
"- For give: syntax is always give <player> minecraft:<item_id> <count>\n"
|
||||||
"- Count comes LAST. Namespace prefix minecraft: is REQUIRED.\n"
|
"- Count comes LAST. Namespace prefix minecraft: is REQUIRED.\n"
|
||||||
|
"- For effects: use 'effect give <player> minecraft:<effect> <seconds> <amplifier>'.\n"
|
||||||
|
"- For weather use only clear/rain/thunder (NOT storm).\n"
|
||||||
|
"- Avoid accidental lethal movement in benevolent responses; do not launch players high unless explicitly asked.\n"
|
||||||
|
"- Do not use tp in helpful/benevolent responses unless the player explicitly requests movement/teleportation.\n"
|
||||||
"- Beds: white_bed not bed. Logs: oak_log not log. Wool: white_wool not wool.\n"
|
"- Beds: white_bed not bed. Logs: oak_log not log. Wool: white_wool not wool.\n"
|
||||||
"- Chain commands for dramatic effect: thunder + lightning + blindness = wrath.\n\n"
|
"- Chain commands for dramatic effect: thunder + lightning + blindness = wrath.\n\n"
|
||||||
+ "=== COMMAND PALETTE ===\n"
|
+ "=== COMMAND PALETTE ===\n"
|
||||||
@@ -1588,8 +1660,14 @@ def build_sudo_commands_system_prompt(config=None) -> str:
|
|||||||
"- You may also output meta-commands starting with 'template ' for schematic workflows (not RCON).\n"
|
"- You may also output meta-commands starting with 'template ' for schematic workflows (not RCON).\n"
|
||||||
"- If the request cannot be mapped safely, return commands: [].\n"
|
"- If the request cannot be mapped safely, return commands: [].\n"
|
||||||
"- If player says 'me' or 'my', target the requesting player.\n"
|
"- If player says 'me' or 'my', target the requesting player.\n"
|
||||||
|
"- Do not broaden scope to @a/@e unless the user explicitly asks for all players/entities.\n"
|
||||||
"- You will receive LAST 10 SUDO ACTIONS. Use them for continuity and corrections when the player says previous output was wrong.\n"
|
"- You will receive LAST 10 SUDO ACTIONS. Use them for continuity and corrections when the player says previous output was wrong.\n"
|
||||||
|
"- You will also receive RECENT FAILED SUDO PATTERNS. Do not repeat those broken shapes.\n"
|
||||||
"- For give syntax: give <player> minecraft:<item_id> <count> (count LAST, namespace required)\n"
|
"- For give syntax: give <player> minecraft:<item_id> <count> (count LAST, namespace required)\n"
|
||||||
|
"- For effect syntax: effect give <player> minecraft:<effect> <seconds> <amplifier> [hideParticles]\n"
|
||||||
|
"- For summon tnt: summon minecraft:tnt <x> <y> <z> (NO trailing count number).\n"
|
||||||
|
" If quantity is requested, output multiple summon commands.\n"
|
||||||
|
"- Never use old NBT enchant syntax like {Enchantments:[...]}; use item[enchantments={...}] only.\n"
|
||||||
"- Return commands only. No commentary.\n"
|
"- Return commands only. No commentary.\n"
|
||||||
"- For build requests, prefer template workflow in one response when possible:\n"
|
"- For build requests, prefer template workflow in one response when possible:\n"
|
||||||
" template search <query>\n"
|
" template search <query>\n"
|
||||||
@@ -1683,6 +1761,7 @@ def build_message_system_prompt(config) -> str:
|
|||||||
"Write a single spoken message to all players reacting to this prayer and action.\n"
|
"Write a single spoken message to all players reacting to this prayer and action.\n"
|
||||||
"Respond with ONLY the message text — no JSON, no quotes, no formatting. "
|
"Respond with ONLY the message text — no JSON, no quotes, no formatting. "
|
||||||
"Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n"
|
"Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n"
|
||||||
|
"For punishments, prefer fear and humiliation over accidental instant death unless destruction is explicitly intended.\n"
|
||||||
)
|
)
|
||||||
lore = config.get("god_lore", "")
|
lore = config.get("god_lore", "")
|
||||||
if lore:
|
if lore:
|
||||||
@@ -2070,6 +2149,53 @@ def _tp_inside_worldborder(cmd: str, config) -> bool:
|
|||||||
|
|
||||||
return abs(x - cx) <= limit and abs(z - cz) <= limit
|
return abs(x - cx) <= limit and abs(z - cz) <= limit
|
||||||
|
|
||||||
|
|
||||||
|
def _tp_safety_enabled(config) -> bool:
|
||||||
|
return bool(config.get("tp_safety_enabled", True))
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_tp_target_y(cmd: str):
|
||||||
|
m = re.search(r'\btp\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', cmd)
|
||||||
|
if not m:
|
||||||
|
return None, None
|
||||||
|
return m.group(1), m.group(3)
|
||||||
|
|
||||||
|
|
||||||
|
def _needs_vertical_tp_safety(resolved_cmd: str, config) -> tuple:
|
||||||
|
target, ytok = _extract_tp_target_y(resolved_cmd)
|
||||||
|
if not target or not ytok:
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
risky_delta = float(config.get("tp_safety_vertical_delta", 8.0))
|
||||||
|
risky_abs_y = float(config.get("tp_safety_absolute_y", 120.0))
|
||||||
|
|
||||||
|
if ytok.startswith("~"):
|
||||||
|
offs = ytok[1:].strip()
|
||||||
|
dy = float(offs) if offs else 0.0
|
||||||
|
if dy >= risky_delta:
|
||||||
|
return True, target
|
||||||
|
return False, target
|
||||||
|
|
||||||
|
try:
|
||||||
|
abs_y = float(ytok)
|
||||||
|
if abs_y >= risky_abs_y:
|
||||||
|
return True, target
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False, target
|
||||||
|
|
||||||
|
|
||||||
|
def _tp_safety_prefix_commands(resolved_cmd: str, config) -> list:
|
||||||
|
if not _tp_safety_enabled(config):
|
||||||
|
return []
|
||||||
|
needs, target = _needs_vertical_tp_safety(resolved_cmd, config)
|
||||||
|
if not needs or not target:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
f"effect give {target} minecraft:slow_falling 20 0",
|
||||||
|
f"effect give {target} minecraft:resistance 8 1",
|
||||||
|
]
|
||||||
|
|
||||||
def fix_give_command(cmd: str) -> str:
|
def fix_give_command(cmd: str) -> str:
|
||||||
"""
|
"""
|
||||||
Correct common LLM give command mistakes:
|
Correct common LLM give command mistakes:
|
||||||
@@ -2167,6 +2293,80 @@ def fix_gamemode_command(cmd: str, fallback_player: str) -> str:
|
|||||||
log.warning(f"Fixed gamemode syntax: '{cmd}' -> '{fixed}'")
|
log.warning(f"Fixed gamemode syntax: '{cmd}' -> '{fixed}'")
|
||||||
return fixed
|
return fixed
|
||||||
|
|
||||||
|
|
||||||
|
def fix_weather_command(cmd: str) -> str:
|
||||||
|
"""Normalize weather synonyms to valid Minecraft literals."""
|
||||||
|
raw = (cmd or "").strip()
|
||||||
|
fixed = re.sub(r'\bweather\s+(storm|rainstorm|thunderstorm)\b', 'weather thunder', raw, flags=re.IGNORECASE)
|
||||||
|
if fixed != raw:
|
||||||
|
log.warning(f"Fixed weather syntax: '{cmd}' -> '{fixed}'")
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
|
||||||
|
def fix_fill_fire_command(cmd: str) -> str:
|
||||||
|
"""Fix legacy fill syntax like `... fire 0 replace air` for 1.21."""
|
||||||
|
raw = (cmd or "").strip()
|
||||||
|
m = re.match(r'^(fill\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+)(minecraft:)?(fire|soul_fire)\s+0\s+replace\s+air$', raw, flags=re.IGNORECASE)
|
||||||
|
if not m:
|
||||||
|
return raw
|
||||||
|
prefix, ns, block = m.groups()
|
||||||
|
block_id = f"minecraft:{block.lower()}" if not ns else f"{ns.lower()}{block.lower()}"
|
||||||
|
fixed = f"{prefix}{block_id} replace air"
|
||||||
|
log.warning(f"Fixed fill fire syntax: '{cmd}' -> '{fixed}'")
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
|
||||||
|
def fix_bow_enchant_syntax(cmd: str) -> str:
|
||||||
|
"""Rewrite old bow Enchantments NBT to 1.21 component format."""
|
||||||
|
raw = (cmd or "").strip()
|
||||||
|
if "Enchantments:[" not in raw or "bow{" not in raw:
|
||||||
|
return raw
|
||||||
|
m = re.match(r'^(give\s+\S+\s+)(minecraft:)?bow\{Enchantments:\[(.+)\]\}\s+(\d+)$', raw)
|
||||||
|
if not m:
|
||||||
|
return raw
|
||||||
|
pre, _, body, count = m.groups()
|
||||||
|
ench = {}
|
||||||
|
for eid, lvl in re.findall(r'id:?\"?([a-z_]+)\"?,\s*lvl:([0-9]+)s?', body):
|
||||||
|
ench[eid] = lvl
|
||||||
|
if not ench:
|
||||||
|
return raw
|
||||||
|
ench_part = ",".join(f"{k}:{v}" for k, v in ench.items())
|
||||||
|
fixed = f"{pre}minecraft:bow[enchantments={{{ench_part}}}] {count}"
|
||||||
|
log.warning(f"Fixed bow enchant syntax: '{cmd}' -> '{fixed}'")
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
|
||||||
|
def _repair_execute_tail(cmd: str, fallback_player: str) -> str:
|
||||||
|
"""Apply syntax repairers to `execute ... run <tail>` payloads."""
|
||||||
|
raw = (cmd or "").strip()
|
||||||
|
tail = raw
|
||||||
|
stack = []
|
||||||
|
# Peel execute wrappers up to a small depth.
|
||||||
|
for _ in range(4):
|
||||||
|
if not tail.startswith("execute "):
|
||||||
|
break
|
||||||
|
marker = " run "
|
||||||
|
idx = tail.find(marker)
|
||||||
|
if idx < 0:
|
||||||
|
break
|
||||||
|
stack.append(tail[: idx + len(marker)])
|
||||||
|
tail = tail[idx + len(marker):].strip()
|
||||||
|
|
||||||
|
fixed_tail = tail
|
||||||
|
fixed_tail = fix_give_command(fixed_tail)
|
||||||
|
fixed_tail = fix_effect_command(fixed_tail)
|
||||||
|
fixed_tail = fix_gamemode_command(fixed_tail, fallback_player)
|
||||||
|
fixed_tail = fix_weather_command(fixed_tail)
|
||||||
|
fixed_tail = fix_fill_fire_command(fixed_tail)
|
||||||
|
fixed_tail = fix_bow_enchant_syntax(fixed_tail)
|
||||||
|
|
||||||
|
if stack:
|
||||||
|
rebuilt = "".join(stack) + fixed_tail
|
||||||
|
if rebuilt != raw:
|
||||||
|
log.warning(f"Fixed execute-tail syntax: '{raw}' -> '{rebuilt}'")
|
||||||
|
return rebuilt
|
||||||
|
return raw
|
||||||
|
|
||||||
def validate_command(cmd, online_players, fallback_player, config=None):
|
def validate_command(cmd, online_players, fallback_player, config=None):
|
||||||
"""Replace placeholders, auto-fix common give syntax errors, check safe prefix."""
|
"""Replace placeholders, auto-fix common give syntax errors, check safe prefix."""
|
||||||
resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player)
|
resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player)
|
||||||
@@ -2176,6 +2376,10 @@ def validate_command(cmd, online_players, fallback_player, config=None):
|
|||||||
resolved = fix_give_command(resolved)
|
resolved = fix_give_command(resolved)
|
||||||
resolved = fix_effect_command(resolved)
|
resolved = fix_effect_command(resolved)
|
||||||
resolved = fix_gamemode_command(resolved, fallback_player)
|
resolved = fix_gamemode_command(resolved, fallback_player)
|
||||||
|
resolved = fix_weather_command(resolved)
|
||||||
|
resolved = fix_fill_fire_command(resolved)
|
||||||
|
resolved = fix_bow_enchant_syntax(resolved)
|
||||||
|
resolved = _repair_execute_tail(resolved, fallback_player)
|
||||||
caps = get_server_capabilities(config) if config else SERVER_CAPABILITIES[DEFAULT_SERVER_TYPE]
|
caps = get_server_capabilities(config) if config else SERVER_CAPABILITIES[DEFAULT_SERVER_TYPE]
|
||||||
prefixes = caps["safe_prefixes"]
|
prefixes = caps["safe_prefixes"]
|
||||||
if not any(resolved.startswith(p) for p in prefixes):
|
if not any(resolved.startswith(p) for p in prefixes):
|
||||||
@@ -2344,9 +2548,9 @@ def _repair_failed_sudo_commands(player: str, results_seen: list, config) -> lis
|
|||||||
xx = x if x.startswith("~") else str(int(float(x)) + dx)
|
xx = x if x.startswith("~") else str(int(float(x)) + dx)
|
||||||
zz = z if z.startswith("~") else str(int(float(z)) + dz)
|
zz = z if z.startswith("~") else str(int(float(z)) + dz)
|
||||||
if x.startswith("~") and dx != 0:
|
if x.startswith("~") and dx != 0:
|
||||||
xx = f"~{dx:+d}"
|
xx = f"~{dx}" if dx < 0 else f"~{dx}"
|
||||||
if z.startswith("~") and dz != 0:
|
if z.startswith("~") and dz != 0:
|
||||||
zz = f"~{dz:+d}"
|
zz = f"~{dz}" if dz < 0 else f"~{dz}"
|
||||||
out.append(f"{prefix}summon minecraft:tnt {xx} {y} {zz}")
|
out.append(f"{prefix}summon minecraft:tnt {xx} {y} {zz}")
|
||||||
if len(out) >= max_retry:
|
if len(out) >= max_retry:
|
||||||
return out
|
return out
|
||||||
@@ -2363,6 +2567,53 @@ def _repair_failed_sudo_commands(player: str, results_seen: list, config) -> lis
|
|||||||
|
|
||||||
return out[:max_retry]
|
return out[:max_retry]
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_tnt_commands_from_prompt(commands: list, prompt: str, player: str, config) -> list:
|
||||||
|
"""If user asked for many TNT and model returned too few summons, expand boundedly."""
|
||||||
|
p = (prompt or "").lower()
|
||||||
|
if "tnt" not in p:
|
||||||
|
return commands
|
||||||
|
nums = [int(n) for n in re.findall(r'\b(\d{1,3})\b', p)]
|
||||||
|
if not nums:
|
||||||
|
return commands
|
||||||
|
requested = max(nums)
|
||||||
|
cap = int(config.get("sudo_tnt_max_commands", 80))
|
||||||
|
target = max(1, min(requested, cap))
|
||||||
|
if len(commands) >= target:
|
||||||
|
return commands
|
||||||
|
|
||||||
|
summons = [c for c in commands if "summon" in c and "tnt" in c]
|
||||||
|
if not summons:
|
||||||
|
return commands
|
||||||
|
|
||||||
|
base = summons[0]
|
||||||
|
prefix = ""
|
||||||
|
body = base
|
||||||
|
m_pref = re.match(rf'^(execute\s+at\s+{re.escape(player)}\s+run\s+)(.+)$', base)
|
||||||
|
if m_pref:
|
||||||
|
prefix = m_pref.group(1)
|
||||||
|
body = m_pref.group(2)
|
||||||
|
|
||||||
|
m = re.match(r'^summon\s+(?:minecraft:)?tnt\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+\{.*\})?$', body)
|
||||||
|
if not m:
|
||||||
|
return commands
|
||||||
|
x, y, z = m.groups()
|
||||||
|
|
||||||
|
expanded = []
|
||||||
|
for i in range(target):
|
||||||
|
dx = (i % 9) - 4
|
||||||
|
dz = (i // 9) - 4
|
||||||
|
xx = x
|
||||||
|
zz = z
|
||||||
|
if x.startswith("~"):
|
||||||
|
xx = "~" if dx == 0 else f"~{dx}"
|
||||||
|
if z.startswith("~"):
|
||||||
|
zz = "~" if dz == 0 else f"~{dz}"
|
||||||
|
expanded.append(f"{prefix}summon minecraft:tnt {xx} {y} {zz}")
|
||||||
|
|
||||||
|
log.warning(f"Expanded TNT commands from {len(commands)} to {len(expanded)} (requested={requested}, cap={cap})")
|
||||||
|
return expanded
|
||||||
|
|
||||||
def execute_response(response, context, config, praying_player=None):
|
def execute_response(response, context, config, praying_player=None):
|
||||||
message = response.get("message") or ""
|
message = response.get("message") or ""
|
||||||
commands = response.get("commands") or []
|
commands = response.get("commands") or []
|
||||||
@@ -2421,6 +2672,17 @@ def execute_response(response, context, config, praying_player=None):
|
|||||||
resolved, is_safe = validate_command(cmd, context["online_players"], fallback, config)
|
resolved, is_safe = validate_command(cmd, context["online_players"], fallback, config)
|
||||||
if not is_safe:
|
if not is_safe:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
safety_prefix = _tp_safety_prefix_commands(resolved, config)
|
||||||
|
for scmd in safety_prefix:
|
||||||
|
sresolved, safe_ok = validate_command(scmd, context["online_players"], fallback, config)
|
||||||
|
if not safe_ok:
|
||||||
|
continue
|
||||||
|
log.info(f"Executing RCON: {sresolved}")
|
||||||
|
sresult = rcon(sresolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
||||||
|
log.info(f"RCON result: {sresult!r}")
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
log.info(f"Executing RCON: {resolved}")
|
log.info(f"Executing RCON: {resolved}")
|
||||||
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
||||||
log.info(f"RCON result: {result!r}")
|
log.info(f"RCON result: {result!r}")
|
||||||
@@ -2587,10 +2849,33 @@ def process_sudo(player, prompt, config):
|
|||||||
if process_sudo_template_command(player, prompt, config):
|
if process_sudo_template_command(player, prompt, config):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _looks_like_lookup_question(text: str) -> bool:
|
||||||
|
t = (text or "").strip().lower()
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
if t.endswith("?"):
|
||||||
|
return True
|
||||||
|
q_starts = (
|
||||||
|
"what ", "why ", "how ", "did ", "does ", "is ", "are ",
|
||||||
|
"can ", "could ", "should ", "would ", "where ", "when ",
|
||||||
|
)
|
||||||
|
if t.startswith(q_starts):
|
||||||
|
return True
|
||||||
|
if re.search(r'\b(that command|last command|did that|does that)\b', t):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _local_lookup_fallback_answer(query: str, ref_cmd: str) -> str:
|
||||||
|
q = (query or "").lower()
|
||||||
|
rc = (ref_cmd or "").lower()
|
||||||
|
if "invisible" in q and "mob" in q and "invisibility" in rc:
|
||||||
|
return "Invisibility greatly reduces mob detection, but it does not make you perfectly undetectable at close range or while making noise/actions."
|
||||||
|
return ""
|
||||||
|
|
||||||
# Deterministic lookup mode: information only, no command execution.
|
# Deterministic lookup mode: information only, no command execution.
|
||||||
low = prompt.lower().strip()
|
low = prompt.lower().strip()
|
||||||
lookup_prefixes = ("lookup ", "search ", "wiki ", "explain ", "what is ", "how do i ", "how to ")
|
lookup_prefixes = ("lookup ", "search ", "wiki ", "explain ", "what is ", "how do i ", "how to ")
|
||||||
if low.startswith(lookup_prefixes):
|
if low.startswith(lookup_prefixes) or _looks_like_lookup_question(prompt):
|
||||||
query = prompt.strip()
|
query = prompt.strip()
|
||||||
if low.startswith("lookup "):
|
if low.startswith("lookup "):
|
||||||
query = prompt[len("lookup "):].strip()
|
query = prompt[len("lookup "):].strip()
|
||||||
@@ -2614,6 +2899,7 @@ def process_sudo(player, prompt, config):
|
|||||||
|
|
||||||
wiki_rows = _info_lookup_wiki(lookup_query)
|
wiki_rows = _info_lookup_wiki(lookup_query)
|
||||||
web_rows = _info_lookup_web(lookup_query)
|
web_rows = _info_lookup_web(lookup_query)
|
||||||
|
gateway_msg = ""
|
||||||
|
|
||||||
if wiki_rows:
|
if wiki_rows:
|
||||||
_send_private(player, "minecraft.wiki:", config, "dark_aqua")
|
_send_private(player, "minecraft.wiki:", config, "dark_aqua")
|
||||||
@@ -2647,6 +2933,7 @@ def process_sudo(player, prompt, config):
|
|||||||
msg = (out.get("message") or "").strip()
|
msg = (out.get("message") or "").strip()
|
||||||
trace = out.get("tool_trace") or []
|
trace = out.get("tool_trace") or []
|
||||||
if msg:
|
if msg:
|
||||||
|
gateway_msg = msg
|
||||||
_send_private(player, "justification:", config, "dark_aqua")
|
_send_private(player, "justification:", config, "dark_aqua")
|
||||||
for ln in re.split(r"\n+", msg)[:3]:
|
for ln in re.split(r"\n+", msg)[:3]:
|
||||||
ln = ln.strip()
|
ln = ln.strip()
|
||||||
@@ -2659,9 +2946,14 @@ def process_sudo(player, prompt, config):
|
|||||||
q = str(t.get("input", ""))[:80]
|
q = str(t.get("input", ""))[:80]
|
||||||
_send_private(player, f"- {tool}: {q}", config, "dark_gray")
|
_send_private(player, f"- {tool}: {q}", config, "dark_gray")
|
||||||
|
|
||||||
if not wiki_rows and not web_rows:
|
if not wiki_rows and not web_rows and not gateway_msg:
|
||||||
_send_private(player, "No lookup results found.", config, "yellow")
|
fb = _local_lookup_fallback_answer(lookup_query, last_cmd)
|
||||||
|
if fb:
|
||||||
|
_send_private(player, f"- {fb}", config, "gray")
|
||||||
|
else:
|
||||||
|
_send_private(player, "No lookup results found.", config, "yellow")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
log.warning(f"SUDO lookup failed for query={lookup_query!r}: {e}")
|
||||||
_send_private(player, f"[SUDO-LOOKUP] Failed: {e}", config, "red")
|
_send_private(player, f"[SUDO-LOOKUP] Failed: {e}", config, "red")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2717,6 +3009,7 @@ def process_sudo(player, prompt, config):
|
|||||||
+ (positions_block + "\n" if positions_block else "")
|
+ (positions_block + "\n" if positions_block else "")
|
||||||
+ f"Natural language request: {prompt}\n"
|
+ f"Natural language request: {prompt}\n"
|
||||||
+ get_sudo_history_block()
|
+ get_sudo_history_block()
|
||||||
|
+ get_sudo_failures_block(player)
|
||||||
)
|
)
|
||||||
|
|
||||||
command_model = config.get("command_model", config["model"])
|
command_model = config.get("command_model", config["model"])
|
||||||
@@ -2771,6 +3064,7 @@ def process_sudo(player, prompt, config):
|
|||||||
"request": prompt,
|
"request": prompt,
|
||||||
"online_players": online,
|
"online_players": online,
|
||||||
"sudo_history": get_sudo_history_block(),
|
"sudo_history": get_sudo_history_block(),
|
||||||
|
"sudo_failures": get_sudo_failures_block(player),
|
||||||
"mode": "sudo",
|
"mode": "sudo",
|
||||||
},
|
},
|
||||||
config=config,
|
config=config,
|
||||||
@@ -2792,7 +3086,22 @@ def process_sudo(player, prompt, config):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
build_intent = any(prompt.lower().strip().startswith(x) for x in ("build ", "make ", "create "))
|
def _is_build_intent(text: str) -> bool:
|
||||||
|
t = (text or "").strip().lower()
|
||||||
|
if t.startswith("build ") or t.startswith("create "):
|
||||||
|
return True
|
||||||
|
if "schem" in t or "schematic" in t or "template" in t:
|
||||||
|
return True
|
||||||
|
if t.startswith("make "):
|
||||||
|
build_nouns = (
|
||||||
|
"house", "base", "tower", "wall", "castle", "bridge", "barn",
|
||||||
|
"bar", "shop", "village", "room", "road", "farm", "portal",
|
||||||
|
"structure", "schem", "schematic", "template", "statue", "arena",
|
||||||
|
)
|
||||||
|
return any(n in t for n in build_nouns)
|
||||||
|
return False
|
||||||
|
|
||||||
|
build_intent = _is_build_intent(prompt)
|
||||||
has_template_cmd = any(isinstance(c, str) and c.lower().startswith("template ") for c in commands)
|
has_template_cmd = any(isinstance(c, str) and c.lower().startswith("template ") for c in commands)
|
||||||
if build_intent and not has_template_cmd:
|
if build_intent and not has_template_cmd:
|
||||||
try:
|
try:
|
||||||
@@ -2807,11 +3116,18 @@ def process_sudo(player, prompt, config):
|
|||||||
low_prompt = prompt.lower().strip()
|
low_prompt = prompt.lower().strip()
|
||||||
if any(low_prompt.startswith(x) for x in ("build ", "make ", "create ")):
|
if any(low_prompt.startswith(x) for x in ("build ", "make ", "create ")):
|
||||||
max_cmds = max(max_cmds, int(config.get("sudo_build_max_commands", 6)))
|
max_cmds = max(max_cmds, int(config.get("sudo_build_max_commands", 6)))
|
||||||
|
if "tnt" in low_prompt:
|
||||||
|
nums = re.findall(r'\b(\d{1,3})\b', low_prompt)
|
||||||
|
if nums:
|
||||||
|
requested = max(int(n) for n in nums)
|
||||||
|
cap = int(config.get("sudo_tnt_max_commands", 80))
|
||||||
|
max_cmds = max(max_cmds, min(requested, cap))
|
||||||
commands = [
|
commands = [
|
||||||
_normalize_sudo_command_shape(c, player)
|
_normalize_sudo_command_shape(c, player)
|
||||||
for c in commands[:max_cmds]
|
for c in commands[:max_cmds]
|
||||||
]
|
]
|
||||||
commands = [c for c in commands if c]
|
commands = [c for c in commands if c]
|
||||||
|
commands = _expand_tnt_commands_from_prompt(commands, prompt, player, config)
|
||||||
|
|
||||||
if not commands:
|
if not commands:
|
||||||
add_sudo_history(player, prompt, [], [])
|
add_sudo_history(player, prompt, [], [])
|
||||||
@@ -2895,6 +3211,10 @@ def process_sudo(player, prompt, config):
|
|||||||
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
|
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
|
||||||
ineffective = (len(executed) == 0) or (effective_hits == 0)
|
ineffective = (len(executed) == 0) or (effective_hits == 0)
|
||||||
|
|
||||||
|
for cmd, res in results_seen:
|
||||||
|
if not _sudo_result_is_effective(res):
|
||||||
|
add_sudo_failure(player, cmd, res)
|
||||||
|
|
||||||
_report_sudo_feedback(player, prompt, commands, results_seen, ineffective, config)
|
_report_sudo_feedback(player, prompt, commands, results_seen, ineffective, config)
|
||||||
|
|
||||||
add_sudo_history(player, prompt, commands, executed)
|
add_sudo_history(player, prompt, commands, executed)
|
||||||
|
|||||||
@@ -5,5 +5,28 @@
|
|||||||
"tool_model": "qwen2.5:1.5b",
|
"tool_model": "qwen2.5:1.5b",
|
||||||
"session_ttl_seconds": 21600,
|
"session_ttl_seconds": 21600,
|
||||||
"session_persistence_enabled": true,
|
"session_persistence_enabled": true,
|
||||||
"session_db_path": "/var/lib/mc-langgraph-gateway/sessions.db"
|
"session_db_path": "/var/lib/mc-langgraph-gateway/sessions.db",
|
||||||
|
"knowledge_base_dir": "/var/lib/mc-langgraph-gateway/knowledge",
|
||||||
|
"knowledge_index_file": "/var/lib/mc-langgraph-gateway/knowledge/index.json",
|
||||||
|
"knowledge_auto_index_on_start": true,
|
||||||
|
"knowledge_bootstrap_on_start": true,
|
||||||
|
"knowledge_max_doc_bytes": 200000,
|
||||||
|
"knowledge_allowed_hosts": [
|
||||||
|
"minecraft.wiki",
|
||||||
|
"www.minecraft.wiki",
|
||||||
|
"docs.papermc.io",
|
||||||
|
"intellectualsites.github.io",
|
||||||
|
"enginehub.org",
|
||||||
|
"worldedit.enginehub.org"
|
||||||
|
],
|
||||||
|
"knowledge_bootstrap_urls": [
|
||||||
|
"https://minecraft.wiki/w/Commands/fill",
|
||||||
|
"https://minecraft.wiki/w/Commands/setblock",
|
||||||
|
"https://minecraft.wiki/w/Commands/clone",
|
||||||
|
"https://minecraft.wiki/w/Commands/summon",
|
||||||
|
"https://minecraft.wiki/w/Commands/execute",
|
||||||
|
"https://minecraft.wiki/w/TNT",
|
||||||
|
"https://minecraft.wiki/w/Explosion",
|
||||||
|
"https://minecraft.wiki/w/Tutorial:Worldedit"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user