Add paper bug_log flow and more ironic God persona
This commit is contained in:
+17
@@ -17,6 +17,14 @@ This document links the two Minecraft AI God projects together, describes their
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Memory Discipline
|
||||||
|
|
||||||
|
- Update `SESSION.md` immediately when a durable fact, decision, or fix is discovered.
|
||||||
|
- Before every final reply, run a memory check and append any missing durable notes.
|
||||||
|
- End every reply with one line: `Session memory: updated` or `Session memory: no new durable facts.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## The Two Projects
|
## The Two Projects
|
||||||
|
|
||||||
### Sister repo — minecraft-ai-god (Vanilla)
|
### Sister repo — minecraft-ai-god (Vanilla)
|
||||||
@@ -103,6 +111,15 @@ This section captures decisions and context accumulated across conversations wit
|
|||||||
- **LLM backend:** Ollama at `192.168.0.179:11434` for the vanilla service. This fork uses `192.168.0.141:11434` (steel141, local GPU) for better throughput on heavier models (gemma3:12b message model, qwen3-coder:30b command model).
|
- **LLM backend:** Ollama at `192.168.0.179:11434` for the vanilla service. This fork uses `192.168.0.141:11434` (steel141, local GPU) for better throughput on heavier models (gemma3:12b message model, qwen3-coder:30b command model).
|
||||||
- **Session gateway (this fork only):** The LangGraph-style FastAPI sidecar adds multi-turn memory without requiring a full LangGraph install. Safety enforcement stays in `mc_aigod_paper.py`, not the gateway.
|
- **Session gateway (this fork only):** The LangGraph-style FastAPI sidecar adds multi-turn memory without requiring a full LangGraph install. Safety enforcement stays in `mc_aigod_paper.py`, not the gateway.
|
||||||
- **Build templates:** Deterministic templates are tried first for `sudo build/make/create` requests. LLM translation is the fallback. This reduces surprises for structure generation tasks.
|
- **Build templates:** Deterministic templates are tried first for `sudo build/make/create` requests. LLM translation is the fallback. This reduces surprises for structure generation tasks.
|
||||||
|
- **Localized retrieval for sudo reasoning (2026-03-17):** `langgraph_gateway.py` now supports a local knowledge corpus (`/var/lib/mc-langgraph-gateway/knowledge`) with index/search/read tools (`local.search`, `local.read`), optional startup bootstrap from Minecraft/Paper docs URLs, and automatic search->read retrieval hop in sudo tool mode before command generation.
|
||||||
|
- **Post-deploy behavior check (2026-03-17):** retrieval is active (`tool_trace` shows `local.search` + `local.read`) for `sudo destroy my surroundings`, but generated command remained `execute as <player> run fill ~-5 ~-5 ~-5 ~5 ~5 ~5 air` and produced no useful visible outcome in gameplay (empty/weak RCON feedback). Retrieval alone did not guarantee effective action selection.
|
||||||
|
- **Sudo execution hardening follow-up (2026-03-17):** added command-shape normalization for sudo (`execute as <player> run ...` -> `execute at <player> run ...`), blocked execute-wrapper bypass in validator (unsafe inner tails like `gameMode` inside `execute ... run`), and added adaptive destructive fallback for failed `destroy/nuke` intents (absolute `fill ... air` around player + TNT summons / TNT kit fallback when position unavailable).
|
||||||
|
- **Sudo effectiveness feedback loop (2026-03-17):** sudo now scores execution effectiveness from RCON results for all intents, reports translated commands + outcomes + ineffective flag back to gateway session memory (`mode=sudo_feedback`, `feedback_only=true`), and gateway stores this feedback without running extra LLM/tool calls.
|
||||||
|
- **Lookup contextual memory fix (2026-03-17):** `sudo lookup/search/wiki` now resolves pronoun references (e.g., "that command", "do it again") against the player's most recent executed sudo command, includes that command in lookup context payloads, and passes sudo history into gateway lookup context to improve follow-up reasoning.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
### Infrastructure decisions
|
### Infrastructure decisions
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -570,7 +570,7 @@ POTIONS: potion (requires component syntax for type — prefer effect give i
|
|||||||
def build_system_prompt(config):
|
def build_system_prompt(config):
|
||||||
return (
|
return (
|
||||||
f"You are God in a Minecraft server called {config['server_name']}.\n"
|
f"You are God in a Minecraft server called {config['server_name']}.\n"
|
||||||
"You are benevolent but just. Theatrical, ancient, and dramatic in speech.\n"
|
"You are benevolent but just. Theatrical, ancient, dramatic, and darkly funny in speech.\n"
|
||||||
"You answer every prayer with a message. You pass judgement on players when they pray.\n\n"
|
"You answer every prayer with a message. You pass judgement on players when they pray.\n\n"
|
||||||
"Respond ONLY with a valid JSON object — no markdown, no explanation, nothing else:\n"
|
"Respond ONLY with a valid JSON object — no markdown, no explanation, nothing else:\n"
|
||||||
'{\n'
|
'{\n'
|
||||||
@@ -578,7 +578,7 @@ def build_system_prompt(config):
|
|||||||
' "commands": ["command1", "command2"]\n'
|
' "commands": ["command1", "command2"]\n'
|
||||||
'}\n\n'
|
'}\n\n'
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
"- message: always present and non-empty. Speak as God. Be dramatic and biblical. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n"
|
"- message: always present and non-empty. Speak as God. Be dramatic, biblical, and ironic with dry humor. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n"
|
||||||
"- commands: list of server commands WITHOUT a leading slash. May be empty [] ONLY if you are deliberately granting nothing.\n"
|
"- commands: list of server commands WITHOUT a leading slash. May be empty [] ONLY if you are deliberately granting nothing.\n"
|
||||||
"- CRITICAL: If your message says you will give something, grant something, or fulfil a request, you MUST include the corresponding command in the commands array. Words alone do nothing. The commands array is the ONLY way anything happens in the world. If commands is empty, nothing happens regardless of what your message says.\n"
|
"- CRITICAL: If your message says you will give something, grant something, or fulfil a request, you MUST include the corresponding command in the commands array. Words alone do nothing. The commands array is the ONLY way anything happens in the world. If commands is empty, nothing happens regardless of what your message says.\n"
|
||||||
"- {player} is the player who prayed. You may also use any other online player's literal username as a target.\n"
|
"- {player} is the player who prayed. You may also use any other online player's literal username as a target.\n"
|
||||||
@@ -723,11 +723,11 @@ FIRST_LOGIN_BENEVOLENCE_PROMPT = (
|
|||||||
def build_message_system_prompt(config) -> str:
|
def build_message_system_prompt(config) -> str:
|
||||||
base = (
|
base = (
|
||||||
"You are God in a Minecraft server. You are benevolent but just. "
|
"You are God in a Minecraft server. You are benevolent but just. "
|
||||||
"Theatrical, ancient, and dramatic in speech — like the Old Testament.\n"
|
"Theatrical, ancient, dramatic, and laced with dry irony — like the Old Testament with a sharper wit.\n"
|
||||||
"You will be told what action was taken (if any) in response to a player's prayer. "
|
"You will be told what action was taken (if any) in response to a player's prayer. "
|
||||||
"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. Any length is fine.\n"
|
"Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n"
|
||||||
)
|
)
|
||||||
lore = config.get("god_lore", "")
|
lore = config.get("god_lore", "")
|
||||||
if lore:
|
if lore:
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
"sudo_enabled": true,
|
"sudo_enabled": true,
|
||||||
"sudo_user": "slingshooter08",
|
"sudo_user": "slingshooter08",
|
||||||
"sudo_max_commands": 12,
|
"sudo_max_commands": 12,
|
||||||
|
"bug_log_path": "/var/log/mc_aigod_paper_bug.log",
|
||||||
|
"bug_log_event_lines": 40,
|
||||||
|
"bug_log_raw_lines": 120,
|
||||||
|
"sudo_build_max_commands": 6,
|
||||||
|
"sudo_deterministic_build_templates": false,
|
||||||
"tp_border_guard_enabled": true,
|
"tp_border_guard_enabled": true,
|
||||||
"worldborder_center_x": 0,
|
"worldborder_center_x": 0,
|
||||||
"worldborder_center_z": 0,
|
"worldborder_center_z": 0,
|
||||||
|
|||||||
+481
-34
@@ -37,6 +37,10 @@ SUDO_PATTERNS = [
|
|||||||
re.compile(r'\[.*?\]: <(\w+)> [Ss]udo (.+)'),
|
re.compile(r'\[.*?\]: <(\w+)> [Ss]udo (.+)'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
BUG_LOG_PATTERNS = [
|
||||||
|
re.compile(r'\[.*?\]: <(\w+)> [Bb]ug[_ ]?[Ll]og(?:\s+(.+))?\s*$'),
|
||||||
|
]
|
||||||
|
|
||||||
JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game')
|
JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game')
|
||||||
LEAVE_PATTERN = re.compile(r'\[.*?\]: (\w+) left the game')
|
LEAVE_PATTERN = re.compile(r'\[.*?\]: (\w+) left the game')
|
||||||
|
|
||||||
@@ -963,6 +967,90 @@ def get_sudo_history_block() -> str:
|
|||||||
)
|
)
|
||||||
return "\n=== LAST 10 SUDO ACTIONS ===\n" + "\n".join(lines) + "\n"
|
return "\n=== LAST 10 SUDO ACTIONS ===\n" + "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_sudo_executed_command(player: str) -> str:
|
||||||
|
"""Return the most recent executed sudo command for a player, if any."""
|
||||||
|
with _memory_lock:
|
||||||
|
entries = list(sudo_history)
|
||||||
|
for _, p, _, _, executed in reversed(entries):
|
||||||
|
if p != player:
|
||||||
|
continue
|
||||||
|
if executed:
|
||||||
|
return str(executed[-1])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _bug_log_path(config) -> str:
|
||||||
|
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:
|
||||||
|
lines = deque(maxlen=max_lines)
|
||||||
|
try:
|
||||||
|
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
for line in f:
|
||||||
|
lines.append(line.rstrip('\n'))
|
||||||
|
except Exception as e:
|
||||||
|
return [f"<unable to read raw server log: {e}>"]
|
||||||
|
return list(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_recent_event_lines(max_events: int) -> list:
|
||||||
|
with _memory_lock:
|
||||||
|
entries = list(recent_log)[-max_events:]
|
||||||
|
out = []
|
||||||
|
for ts, entry in entries:
|
||||||
|
stamp = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
out.append(f"[{stamp}] {entry}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def process_bug_log(player: str, description: str, config):
|
||||||
|
desc = (description or "").strip()
|
||||||
|
if not desc:
|
||||||
|
desc = "(no description provided)"
|
||||||
|
|
||||||
|
event_count = int(config.get("bug_log_event_lines", 40))
|
||||||
|
raw_count = int(config.get("bug_log_raw_lines", 120))
|
||||||
|
|
||||||
|
recent_events = _format_recent_event_lines(event_count)
|
||||||
|
raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count)
|
||||||
|
bug_path = _bug_log_path(config)
|
||||||
|
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||||
|
|
||||||
|
try:
|
||||||
|
parent = os.path.dirname(bug_path)
|
||||||
|
if parent:
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
|
||||||
|
with open(bug_path, 'a', encoding='utf-8') as bf:
|
||||||
|
bf.write("\n" + "=" * 80 + "\n")
|
||||||
|
bf.write(f"BUG LOG ENTRY: {timestamp}\n")
|
||||||
|
bf.write(f"Player: {player}\n")
|
||||||
|
bf.write(f"Description: {desc}\n")
|
||||||
|
bf.write("\n-- RECENT INTERESTING EVENTS --\n")
|
||||||
|
if recent_events:
|
||||||
|
bf.write("\n".join(recent_events) + "\n")
|
||||||
|
else:
|
||||||
|
bf.write("(no recent in-memory events captured)\n")
|
||||||
|
bf.write("\n-- RECENT RAW SERVER LOG LINES --\n")
|
||||||
|
if raw_lines:
|
||||||
|
bf.write("\n".join(raw_lines) + "\n")
|
||||||
|
else:
|
||||||
|
bf.write("(no raw lines available)\n")
|
||||||
|
|
||||||
|
log.info(f"BUG_LOG recorded by {player}: {desc} -> {bug_path}")
|
||||||
|
rcon(
|
||||||
|
f'tellraw {player} {{"text":"[BUG_LOG] Logged. Thank you.","color":"green"}}',
|
||||||
|
config["rcon_host"], config["rcon_port"], config["rcon_password"]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"BUG_LOG write failed for {player}: {e}", exc_info=True)
|
||||||
|
rcon(
|
||||||
|
f'tellraw {player} {{"text":"[BUG_LOG] Failed to write bug log.","color":"red"}}',
|
||||||
|
config["rcon_host"], config["rcon_port"], config["rcon_password"]
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# RCON
|
# RCON
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1225,18 +1313,18 @@ SERVER_CAPABILITIES = {
|
|||||||
"vanilla": {
|
"vanilla": {
|
||||||
"safe_prefixes": [
|
"safe_prefixes": [
|
||||||
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ',
|
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ',
|
||||||
'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ',
|
'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ', 'gamemode ',
|
||||||
],
|
],
|
||||||
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder",
|
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder, gamemode",
|
||||||
"template_build": False,
|
"template_build": False,
|
||||||
},
|
},
|
||||||
"paper": {
|
"paper": {
|
||||||
"safe_prefixes": [
|
"safe_prefixes": [
|
||||||
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ',
|
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ',
|
||||||
'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ',
|
'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ',
|
||||||
'fill ', 'setblock ', 'clone ',
|
'fill ', 'setblock ', 'clone ', 'gamemode ',
|
||||||
],
|
],
|
||||||
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder, fill, setblock, clone",
|
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder, fill, setblock, clone, gamemode",
|
||||||
"template_build": True,
|
"template_build": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1260,6 +1348,7 @@ GIVE (any item, based on player need — see Item Naming Rules below):
|
|||||||
xp add {target} <amount> levels
|
xp add {target} <amount> levels
|
||||||
|
|
||||||
EFFECTS (replace {target} with any online player's username):
|
EFFECTS (replace {target} with any online player's username):
|
||||||
|
SYNTAX: effect give <player> minecraft:<effect> <seconds> <amplifier>
|
||||||
effect give {target} minecraft:regeneration 120 2
|
effect give {target} minecraft:regeneration 120 2
|
||||||
effect give {target} minecraft:strength 300 1
|
effect give {target} minecraft:strength 300 1
|
||||||
effect give {target} minecraft:speed 300 2
|
effect give {target} minecraft:speed 300 2
|
||||||
@@ -1275,6 +1364,12 @@ EFFECTS (replace {target} with any online player's username):
|
|||||||
effect give {target} minecraft:levitation 5 3
|
effect give {target} minecraft:levitation 5 3
|
||||||
effect clear {target}
|
effect clear {target}
|
||||||
|
|
||||||
|
GAMEMODE:
|
||||||
|
gamemode survival {target}
|
||||||
|
gamemode creative {target}
|
||||||
|
gamemode adventure {target}
|
||||||
|
gamemode spectator {target}
|
||||||
|
|
||||||
MOVEMENT:
|
MOVEMENT:
|
||||||
tp {target} 0 64 0
|
tp {target} 0 64 0
|
||||||
tp {target} <x> <y> <z>
|
tp {target} <x> <y> <z>
|
||||||
@@ -1329,7 +1424,7 @@ POTIONS: potion (requires component syntax for type — prefer effect give i
|
|||||||
def build_system_prompt(config):
|
def build_system_prompt(config):
|
||||||
return (
|
return (
|
||||||
f"You are God in a Minecraft server called {config['server_name']}.\n"
|
f"You are God in a Minecraft server called {config['server_name']}.\n"
|
||||||
"You are benevolent but just. Theatrical, ancient, and dramatic in speech.\n"
|
"You are benevolent but just. Theatrical, ancient, dramatic, and darkly funny in speech.\n"
|
||||||
"You answer every prayer with a message. You pass judgement on players when they pray.\n\n"
|
"You answer every prayer with a message. You pass judgement on players when they pray.\n\n"
|
||||||
"Respond ONLY with a valid JSON object — no markdown, no explanation, nothing else:\n"
|
"Respond ONLY with a valid JSON object — no markdown, no explanation, nothing else:\n"
|
||||||
'{\n'
|
'{\n'
|
||||||
@@ -1337,7 +1432,7 @@ def build_system_prompt(config):
|
|||||||
' "commands": ["command1", "command2"]\n'
|
' "commands": ["command1", "command2"]\n'
|
||||||
'}\n\n'
|
'}\n\n'
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
"- message: always present and non-empty. Speak as God. Be dramatic and biblical. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n"
|
"- message: always present and non-empty. Speak as God. Be dramatic, biblical, and ironic with dry humor. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n"
|
||||||
"- commands: list of server commands WITHOUT a leading slash. May be empty [] ONLY if you are deliberately granting nothing.\n"
|
"- commands: list of server commands WITHOUT a leading slash. May be empty [] ONLY if you are deliberately granting nothing.\n"
|
||||||
"- CRITICAL: If your message says you will give something, grant something, or fulfil a request, you MUST include the corresponding command in the commands array. Words alone do nothing. The commands array is the ONLY way anything happens in the world. If commands is empty, nothing happens regardless of what your message says.\n"
|
"- CRITICAL: If your message says you will give something, grant something, or fulfil a request, you MUST include the corresponding command in the commands array. Words alone do nothing. The commands array is the ONLY way anything happens in the world. If commands is empty, nothing happens regardless of what your message says.\n"
|
||||||
"- {player} is the player who prayed. You may also use any other online player's literal username as a target.\n"
|
"- {player} is the player who prayed. You may also use any other online player's literal username as a target.\n"
|
||||||
@@ -1490,11 +1585,17 @@ def build_sudo_commands_system_prompt(config=None) -> str:
|
|||||||
"{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n"
|
"{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n"
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
f"- Use commands from this whitelist only: {whitelist}.\n"
|
f"- Use commands from this whitelist only: {whitelist}.\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"
|
||||||
"- 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"
|
||||||
"- 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"
|
||||||
"- Return commands only. No commentary.\n"
|
"- Return commands only. No commentary.\n"
|
||||||
|
"- For build requests, prefer template workflow in one response when possible:\n"
|
||||||
|
" template search <query>\n"
|
||||||
|
" template pick <n> [name]\n"
|
||||||
|
" template build <name or filename>\n"
|
||||||
|
"- Keep template workflow concise (2-4 commands max).\n"
|
||||||
"\n"
|
"\n"
|
||||||
"=== TELEPORT SYNTAX ===\n"
|
"=== TELEPORT SYNTAX ===\n"
|
||||||
"Same dimension: tp <player> <x> <y> <z>\n"
|
"Same dimension: tp <player> <x> <y> <z>\n"
|
||||||
@@ -1577,11 +1678,11 @@ SUDO_COMMANDS_SYSTEM_PROMPT = build_sudo_commands_system_prompt()
|
|||||||
def build_message_system_prompt(config) -> str:
|
def build_message_system_prompt(config) -> str:
|
||||||
base = (
|
base = (
|
||||||
"You are God in a Minecraft server. You are benevolent but just. "
|
"You are God in a Minecraft server. You are benevolent but just. "
|
||||||
"Theatrical, ancient, and dramatic in speech — like the Old Testament.\n"
|
"Theatrical, ancient, dramatic, and laced with dry irony — like the Old Testament with a sharper wit.\n"
|
||||||
"You will be told what action was taken (if any) in response to a player's prayer. "
|
"You will be told what action was taken (if any) in response to a player's prayer. "
|
||||||
"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. Any length is fine.\n"
|
"Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n"
|
||||||
)
|
)
|
||||||
lore = config.get("god_lore", "")
|
lore = config.get("god_lore", "")
|
||||||
if lore:
|
if lore:
|
||||||
@@ -2033,6 +2134,39 @@ def fix_effect_command(cmd: str) -> str:
|
|||||||
return fixed
|
return fixed
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
def fix_gamemode_command(cmd: str, fallback_player: str) -> str:
|
||||||
|
"""
|
||||||
|
Normalize common gamemode variants:
|
||||||
|
- gameMode -> gamemode
|
||||||
|
- short aliases (s/c/a/sp) -> survival/creative/adventure/spectator
|
||||||
|
- add target player when omitted
|
||||||
|
"""
|
||||||
|
raw = (cmd or "").strip()
|
||||||
|
m = re.match(r'^(?:gameMode|gamemode)\s+(\S+)(?:\s+(\w+))?$', raw, flags=re.IGNORECASE)
|
||||||
|
if not m:
|
||||||
|
return cmd
|
||||||
|
mode, target = m.group(1).lower(), (m.group(2) or "").strip()
|
||||||
|
alias = {
|
||||||
|
"s": "survival",
|
||||||
|
"0": "survival",
|
||||||
|
"c": "creative",
|
||||||
|
"1": "creative",
|
||||||
|
"a": "adventure",
|
||||||
|
"2": "adventure",
|
||||||
|
"sp": "spectator",
|
||||||
|
"3": "spectator",
|
||||||
|
}
|
||||||
|
mode = alias.get(mode, mode)
|
||||||
|
if mode not in ("survival", "creative", "adventure", "spectator"):
|
||||||
|
return cmd
|
||||||
|
if not target:
|
||||||
|
target = fallback_player
|
||||||
|
fixed = f"gamemode {mode} {target}"
|
||||||
|
if fixed != raw:
|
||||||
|
log.warning(f"Fixed gamemode syntax: '{cmd}' -> '{fixed}'")
|
||||||
|
return fixed
|
||||||
|
|
||||||
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)
|
||||||
@@ -2041,16 +2175,194 @@ def validate_command(cmd, online_players, fallback_player, config=None):
|
|||||||
resolved = resolved[1:]
|
resolved = resolved[1:]
|
||||||
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)
|
||||||
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):
|
||||||
log.warning(f"Command blocked (unknown prefix for server_type={config.get('server_type', DEFAULT_SERVER_TYPE) if config else DEFAULT_SERVER_TYPE}): {resolved}")
|
log.warning(f"Command blocked (unknown prefix for server_type={config.get('server_type', DEFAULT_SERVER_TYPE) if config else DEFAULT_SERVER_TYPE}): {resolved}")
|
||||||
return resolved, False
|
return resolved, False
|
||||||
|
|
||||||
|
# Prevent execute-wrapper bypass (e.g. execute ... run gameMode s)
|
||||||
|
if resolved.startswith("execute "):
|
||||||
|
tail = resolved
|
||||||
|
for _ in range(4):
|
||||||
|
if not tail.startswith("execute "):
|
||||||
|
break
|
||||||
|
marker = " run "
|
||||||
|
idx = tail.find(marker)
|
||||||
|
if idx < 0:
|
||||||
|
break
|
||||||
|
tail = tail[idx + len(marker):].strip()
|
||||||
|
if tail and not tail.startswith("execute "):
|
||||||
|
inner_prefixes = [p for p in prefixes if p != "execute "]
|
||||||
|
if not any(tail.startswith(p) for p in inner_prefixes):
|
||||||
|
log.warning(f"Command blocked (unsafe execute tail): {resolved}")
|
||||||
|
return resolved, False
|
||||||
|
|
||||||
if config and _extract_tp_abs_xyz(resolved) and not _tp_inside_worldborder(resolved, config):
|
if config and _extract_tp_abs_xyz(resolved) and not _tp_inside_worldborder(resolved, config):
|
||||||
log.warning(f"Command blocked (tp outside worldborder): {resolved}")
|
log.warning(f"Command blocked (tp outside worldborder): {resolved}")
|
||||||
return resolved, False
|
return resolved, False
|
||||||
return resolved, True
|
return resolved, True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_destructive_intent(prompt: str) -> bool:
|
||||||
|
p = (prompt or "").lower()
|
||||||
|
keys = (
|
||||||
|
"destroy", "destruction", "nuke", "blow up", "blowup", "explode",
|
||||||
|
"erase", "delete", "annihilate", "wreck", "flatten", "ruin", "tnt",
|
||||||
|
)
|
||||||
|
return any(k in p for k in keys)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_sudo_command_shape(cmd: str, player: str) -> str:
|
||||||
|
c = (cmd or "").strip()
|
||||||
|
if not c:
|
||||||
|
return c
|
||||||
|
|
||||||
|
# Collapse execute-wrapped gamemode into direct targeted form.
|
||||||
|
gm = re.match(rf'^execute\s+as\s+{re.escape(player)}\s+run\s+(?:gameMode|gamemode)\s+(\S+)(?:\s+(\w+))?$', c, flags=re.IGNORECASE)
|
||||||
|
if gm:
|
||||||
|
mode = gm.group(1)
|
||||||
|
target = gm.group(2) or player
|
||||||
|
fixed = f"gamemode {mode} {target}"
|
||||||
|
log.warning(f"Normalized execute-wrapped gamemode: '{c}' -> '{fixed}'")
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
# Prefer position-context execution; this yields more reliable behavior than nested execute chains.
|
||||||
|
m = re.match(rf'^execute\s+as\s+{re.escape(player)}\s+run\s+execute\s+positioned\s+~\s+~\s+~\s+run\s+(.+)$', c)
|
||||||
|
if m:
|
||||||
|
fixed = f"execute at {player} run {m.group(1).strip()}"
|
||||||
|
log.warning(f"Normalized nested execute: '{c}' -> '{fixed}'")
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
m = re.match(rf'^execute\s+as\s+{re.escape(player)}\s+run\s+(.+)$', c)
|
||||||
|
if m:
|
||||||
|
fixed = f"execute at {player} run {m.group(1).strip()}"
|
||||||
|
log.warning(f"Normalized execute anchor: '{c}' -> '{fixed}'")
|
||||||
|
return fixed
|
||||||
|
|
||||||
|
# Drop known no-op clone shape (source to same destination).
|
||||||
|
if re.match(r'^clone\s+~\S*\s+~\S*\s+~\S*\s+~\S*\s+~\S*\s+~\S*\s+~\s+~\s+~$', c):
|
||||||
|
log.warning(f"Dropped likely no-op clone: '{c}'")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def _build_destructive_fallback(player: str, config) -> list:
|
||||||
|
pos = get_player_xyz(player, config)
|
||||||
|
if not pos:
|
||||||
|
return [f"give {player} minecraft:tnt 64", f"give {player} minecraft:flint_and_steel 1"]
|
||||||
|
|
||||||
|
x, y, z = pos
|
||||||
|
radius = int(config.get("sudo_destroy_radius", 12))
|
||||||
|
depth = int(config.get("sudo_destroy_depth", 18))
|
||||||
|
height = int(config.get("sudo_destroy_height", 6))
|
||||||
|
y1 = max(-64, y - depth)
|
||||||
|
y2 = min(319, y + height)
|
||||||
|
return [
|
||||||
|
f"fill {x-radius} {y1} {z-radius} {x+radius} {y2} {z+radius} minecraft:air",
|
||||||
|
f"summon minecraft:tnt {x} {min(319, y+2)} {z}",
|
||||||
|
f"summon minecraft:tnt {x+3} {min(319, y+2)} {z}",
|
||||||
|
f"summon minecraft:tnt {x-3} {min(319, y+2)} {z}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _sudo_result_is_effective(result: str) -> bool:
|
||||||
|
r = (result or "").strip().lower()
|
||||||
|
if not r:
|
||||||
|
return False
|
||||||
|
bad_markers = (
|
||||||
|
"unknown", "incorrect argument", "expected", "syntax error",
|
||||||
|
"no entity was found", "cannot", "failed", "error",
|
||||||
|
)
|
||||||
|
if any(m in r for m in bad_markers):
|
||||||
|
return False
|
||||||
|
good_markers = (
|
||||||
|
"successfully", "summoned", "set the", "set block", "filled",
|
||||||
|
"teleported", "gave", "applied effect", "killed",
|
||||||
|
)
|
||||||
|
return any(m in r for m in good_markers)
|
||||||
|
|
||||||
|
|
||||||
|
def _report_sudo_feedback(player: str, prompt: str, translated: list, results_seen: list, ineffective: bool, config):
|
||||||
|
if not _gateway_enabled(config):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
summary_rows = []
|
||||||
|
for cmd, res in results_seen[:8]:
|
||||||
|
summary_rows.append({"command": cmd, "result": (res or "")[:220]})
|
||||||
|
_gateway_send(
|
||||||
|
player=player,
|
||||||
|
mode="sudo",
|
||||||
|
text=f"execution feedback for sudo request: {prompt}",
|
||||||
|
context_payload={
|
||||||
|
"mode": "sudo_feedback",
|
||||||
|
"feedback_only": True,
|
||||||
|
"request": prompt,
|
||||||
|
"translated_commands": translated[:8],
|
||||||
|
"execution_results": summary_rows,
|
||||||
|
"ineffective": bool(ineffective),
|
||||||
|
},
|
||||||
|
config=config,
|
||||||
|
allow_tools=False,
|
||||||
|
max_tool_steps=0,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Could not report sudo feedback to gateway: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _repair_failed_sudo_commands(player: str, results_seen: list, config) -> list:
|
||||||
|
"""Build bounded retry commands from observed command/result failures."""
|
||||||
|
out = []
|
||||||
|
max_retry = int(config.get("sudo_retry_max_commands", 10))
|
||||||
|
|
||||||
|
for cmd, result in results_seen:
|
||||||
|
c = (cmd or "").strip()
|
||||||
|
r = (result or "").lower()
|
||||||
|
if not c or not r:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fix malformed TNT count usage:
|
||||||
|
# execute at <p> run summon tnt ~ ~1 ~ 80 -> multiple summon commands.
|
||||||
|
if "expected compound tag" in r and "summon" in c and "tnt" in c:
|
||||||
|
prefix = ""
|
||||||
|
body = c
|
||||||
|
m_pref = re.match(rf'^(execute\s+at\s+{re.escape(player)}\s+run\s+)(.+)$', c)
|
||||||
|
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+(\d+))?$', body)
|
||||||
|
if m:
|
||||||
|
x, y, z, cnt = m.groups()
|
||||||
|
count = int(cnt) if cnt and cnt.isdigit() else 1
|
||||||
|
count = max(1, min(count, int(config.get("sudo_tnt_retry_cap", 24))))
|
||||||
|
for i in range(count):
|
||||||
|
dx = (i % 6) - 3
|
||||||
|
dz = (i // 6) - 2
|
||||||
|
xx = x if x.startswith("~") else str(int(float(x)) + dx)
|
||||||
|
zz = z if z.startswith("~") else str(int(float(z)) + dz)
|
||||||
|
if x.startswith("~") and dx != 0:
|
||||||
|
xx = f"~{dx:+d}"
|
||||||
|
if z.startswith("~") and dz != 0:
|
||||||
|
zz = f"~{dz:+d}"
|
||||||
|
out.append(f"{prefix}summon minecraft:tnt {xx} {y} {zz}")
|
||||||
|
if len(out) >= max_retry:
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Fix nonexistent invulnerability effect to a real durable protection set.
|
||||||
|
if "mob_effect" in r and "invulnerability" in r:
|
||||||
|
out.extend([
|
||||||
|
f"effect give {player} minecraft:resistance 1200 4 true",
|
||||||
|
f"effect give {player} minecraft:regeneration 1200 2 true",
|
||||||
|
f"effect give {player} minecraft:absorption 1200 4 true",
|
||||||
|
])
|
||||||
|
if len(out) >= max_retry:
|
||||||
|
return out[:max_retry]
|
||||||
|
|
||||||
|
return out[:max_retry]
|
||||||
|
|
||||||
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 []
|
||||||
@@ -2289,11 +2601,19 @@ def process_sudo(player, prompt, config):
|
|||||||
if not query:
|
if not query:
|
||||||
_send_private(player, "[SUDO-LOOKUP] Usage: sudo lookup <question>", config, "yellow")
|
_send_private(player, "[SUDO-LOOKUP] Usage: sudo lookup <question>", config, "yellow")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Resolve contextual references like "that command" using sudo memory.
|
||||||
|
last_cmd = get_last_sudo_executed_command(player)
|
||||||
|
lookup_query = query
|
||||||
|
if last_cmd and re.search(r'\b(that|it|same|again|last command)\b', query.lower()):
|
||||||
|
lookup_query = f"{query} (reference command: {last_cmd})"
|
||||||
try:
|
try:
|
||||||
_send_private(player, "[SUDO-LOOKUP] Result:", config, "aqua")
|
_send_private(player, "[SUDO-LOOKUP] Result:", config, "aqua")
|
||||||
|
if last_cmd:
|
||||||
|
_send_private(player, f"context command: {last_cmd}", config, "dark_gray")
|
||||||
|
|
||||||
wiki_rows = _info_lookup_wiki(query)
|
wiki_rows = _info_lookup_wiki(lookup_query)
|
||||||
web_rows = _info_lookup_web(query)
|
web_rows = _info_lookup_web(lookup_query)
|
||||||
|
|
||||||
if wiki_rows:
|
if wiki_rows:
|
||||||
_send_private(player, "minecraft.wiki:", config, "dark_aqua")
|
_send_private(player, "minecraft.wiki:", config, "dark_aqua")
|
||||||
@@ -2314,7 +2634,9 @@ def process_sudo(player, prompt, config):
|
|||||||
context_payload={
|
context_payload={
|
||||||
"mode": "sudo_lookup",
|
"mode": "sudo_lookup",
|
||||||
"lookup_only": True,
|
"lookup_only": True,
|
||||||
"query": query,
|
"query": lookup_query,
|
||||||
|
"reference_command": last_cmd,
|
||||||
|
"sudo_history": get_sudo_history_block(),
|
||||||
"wiki_hits": wiki_rows,
|
"wiki_hits": wiki_rows,
|
||||||
"web_hits": web_rows,
|
"web_hits": web_rows,
|
||||||
},
|
},
|
||||||
@@ -2345,27 +2667,28 @@ def process_sudo(player, prompt, config):
|
|||||||
|
|
||||||
online = players_online(config)
|
online = players_online(config)
|
||||||
|
|
||||||
# Deterministic builder templates (Paper-optimized) for sudo build requests.
|
# Legacy deterministic builder templates are optional; default is AI-driven build planning.
|
||||||
templated = build_template_commands(player, prompt, config)
|
if bool(config.get("sudo_deterministic_build_templates", False)):
|
||||||
if templated is not None:
|
templated = build_template_commands(player, prompt, config)
|
||||||
commands = templated[: int(config.get("sudo_max_commands", 12))]
|
if templated is not None:
|
||||||
safe_preview = " | ".join(commands).replace("\\", "\\\\").replace('"', '\\"')
|
commands = templated[: int(config.get("sudo_max_commands", 12))]
|
||||||
rcon(
|
safe_preview = " | ".join(commands).replace("\\", "\\\\").replace('"', '\\"')
|
||||||
f'tellraw {player} {{"text":"[SUDO-BUILD] {safe_preview}","color":"dark_aqua","italic":true}}',
|
rcon(
|
||||||
config["rcon_host"], config["rcon_port"], config["rcon_password"]
|
f'tellraw {player} {{"text":"[SUDO-BUILD] {safe_preview}","color":"dark_aqua","italic":true}}',
|
||||||
)
|
config["rcon_host"], config["rcon_port"], config["rcon_password"]
|
||||||
executed = []
|
)
|
||||||
for cmd in commands:
|
executed = []
|
||||||
resolved, is_safe = validate_command(cmd, online, player, config)
|
for cmd in commands:
|
||||||
if not is_safe:
|
resolved, is_safe = validate_command(cmd, online, player, config)
|
||||||
continue
|
if not is_safe:
|
||||||
log.info(f"SUDO-BUILD execute: {resolved}")
|
continue
|
||||||
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
log.info(f"SUDO-BUILD execute: {resolved}")
|
||||||
log.info(f"SUDO-BUILD result: {result!r}")
|
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
||||||
executed.append(resolved)
|
log.info(f"SUDO-BUILD result: {result!r}")
|
||||||
time.sleep(0.15)
|
executed.append(resolved)
|
||||||
add_sudo_history(player, prompt, commands, executed)
|
time.sleep(0.15)
|
||||||
return
|
add_sudo_history(player, prompt, commands, executed)
|
||||||
|
return
|
||||||
|
|
||||||
# Collect positions for all online players.
|
# Collect positions for all online players.
|
||||||
position_lines = []
|
position_lines = []
|
||||||
@@ -2390,6 +2713,7 @@ def process_sudo(player, prompt, config):
|
|||||||
context_hint = (
|
context_hint = (
|
||||||
f"Requesting player: {player}\n"
|
f"Requesting player: {player}\n"
|
||||||
f"Online players: {', '.join(online) or 'none'}\n"
|
f"Online players: {', '.join(online) or 'none'}\n"
|
||||||
|
+ "Template workflow commands available: template search <query> ; template pick <n> [name] ; template build <name|filename>\n"
|
||||||
+ (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()
|
||||||
@@ -2410,6 +2734,32 @@ def process_sudo(player, prompt, config):
|
|||||||
parsed = _parse_llm_json(content)
|
parsed = _parse_llm_json(content)
|
||||||
return parsed.get("commands") or []
|
return parsed.get("commands") or []
|
||||||
|
|
||||||
|
def _ai_template_plan() -> list:
|
||||||
|
planner_system = (
|
||||||
|
"You are a Minecraft template workflow planner. Return ONLY JSON: {\"commands\": [\"...\"]}.\n"
|
||||||
|
"Output only template workflow meta-commands (not RCON):\n"
|
||||||
|
"- template search <query>\n"
|
||||||
|
"- template pick <n> [name]\n"
|
||||||
|
"- template build <name|filename>\n"
|
||||||
|
"Keep to 2-4 commands and preserve user intent."
|
||||||
|
)
|
||||||
|
content = _llm_call(
|
||||||
|
model=command_model,
|
||||||
|
system=planner_system,
|
||||||
|
user=(
|
||||||
|
f"Player: {player}\n"
|
||||||
|
f"Request: {prompt}\n"
|
||||||
|
f"Recent sudo history:\n{get_sudo_history_block()}"
|
||||||
|
),
|
||||||
|
config=config,
|
||||||
|
fmt="json",
|
||||||
|
temperature=0.2,
|
||||||
|
max_tokens=140,
|
||||||
|
)
|
||||||
|
parsed = _parse_llm_json(content)
|
||||||
|
cmds = parsed.get("commands") or []
|
||||||
|
return [c for c in cmds if isinstance(c, str) and c.lower().startswith("template ")]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if _gateway_enabled(config):
|
if _gateway_enabled(config):
|
||||||
try:
|
try:
|
||||||
@@ -2418,6 +2768,7 @@ def process_sudo(player, prompt, config):
|
|||||||
mode="sudo",
|
mode="sudo",
|
||||||
text=prompt,
|
text=prompt,
|
||||||
context_payload={
|
context_payload={
|
||||||
|
"request": prompt,
|
||||||
"online_players": online,
|
"online_players": online,
|
||||||
"sudo_history": get_sudo_history_block(),
|
"sudo_history": get_sudo_history_block(),
|
||||||
"mode": "sudo",
|
"mode": "sudo",
|
||||||
@@ -2441,8 +2792,26 @@ def process_sudo(player, prompt, config):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
max_cmds = config.get("sudo_max_commands", 3)
|
build_intent = any(prompt.lower().strip().startswith(x) for x in ("build ", "make ", "create "))
|
||||||
commands = commands[:max_cmds]
|
has_template_cmd = any(isinstance(c, str) and c.lower().startswith("template ") for c in commands)
|
||||||
|
if build_intent and not has_template_cmd:
|
||||||
|
try:
|
||||||
|
planned = _ai_template_plan()
|
||||||
|
if planned:
|
||||||
|
log.info(f"SUDO template planner override: {planned}")
|
||||||
|
commands = planned
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"SUDO template planner failed: {e}")
|
||||||
|
|
||||||
|
max_cmds = int(config.get("sudo_max_commands", 3))
|
||||||
|
low_prompt = prompt.lower().strip()
|
||||||
|
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)))
|
||||||
|
commands = [
|
||||||
|
_normalize_sudo_command_shape(c, player)
|
||||||
|
for c in commands[:max_cmds]
|
||||||
|
]
|
||||||
|
commands = [c for c in commands if c]
|
||||||
|
|
||||||
if not commands:
|
if not commands:
|
||||||
add_sudo_history(player, prompt, [], [])
|
add_sudo_history(player, prompt, [], [])
|
||||||
@@ -2460,7 +2829,20 @@ def process_sudo(player, prompt, config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
executed = []
|
executed = []
|
||||||
|
results_seen = []
|
||||||
for cmd in commands:
|
for cmd in commands:
|
||||||
|
if cmd.lower().startswith("template "):
|
||||||
|
log.info(f"SUDO template action: {cmd}")
|
||||||
|
try:
|
||||||
|
process_sudo_template_command(player, cmd, config)
|
||||||
|
executed.append(cmd)
|
||||||
|
results_seen.append((cmd, "template action executed"))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"SUDO template action failed: {e}")
|
||||||
|
results_seen.append((cmd, f"template action failed: {e}"))
|
||||||
|
time.sleep(0.15)
|
||||||
|
continue
|
||||||
|
|
||||||
resolved, is_safe = validate_command(cmd, online, player, config)
|
resolved, is_safe = validate_command(cmd, online, player, config)
|
||||||
if not is_safe:
|
if not is_safe:
|
||||||
continue
|
continue
|
||||||
@@ -2468,8 +2850,53 @@ def process_sudo(player, prompt, config):
|
|||||||
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"SUDO result: {result!r}")
|
log.info(f"SUDO result: {result!r}")
|
||||||
executed.append(resolved)
|
executed.append(resolved)
|
||||||
|
results_seen.append((resolved, str(result or "")))
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
|
||||||
|
ineffective = (len(executed) == 0) or (effective_hits == 0)
|
||||||
|
|
||||||
|
# Generic failed-execution repair pass.
|
||||||
|
if ineffective:
|
||||||
|
retry_cmds = _repair_failed_sudo_commands(player, results_seen, config)
|
||||||
|
if retry_cmds:
|
||||||
|
log.warning(f"SUDO retry pipeline engaged: {retry_cmds}")
|
||||||
|
_send_private(player, "[SUDO] Initial command failed; retrying with repaired syntax.", config, "yellow")
|
||||||
|
for cmd in retry_cmds:
|
||||||
|
resolved, is_safe = validate_command(cmd, online, player, config)
|
||||||
|
if not is_safe:
|
||||||
|
continue
|
||||||
|
log.info(f"SUDO retry execute: {resolved}")
|
||||||
|
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
||||||
|
log.info(f"SUDO retry result: {result!r}")
|
||||||
|
executed.append(resolved)
|
||||||
|
results_seen.append((resolved, str(result or "")))
|
||||||
|
time.sleep(0.12)
|
||||||
|
|
||||||
|
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
|
||||||
|
ineffective = (len(executed) == 0) or (effective_hits == 0)
|
||||||
|
|
||||||
|
# Adaptive fallback for destructive intent when output appears ineffective.
|
||||||
|
if _is_destructive_intent(prompt) and ineffective:
|
||||||
|
fallback_cmds = _build_destructive_fallback(player, config)
|
||||||
|
log.warning(f"SUDO destructive fallback engaged for prompt={prompt!r}: {fallback_cmds}")
|
||||||
|
_send_private(player, "[SUDO] Initial plan was weak; applying destructive fallback.", config, "yellow")
|
||||||
|
for cmd in fallback_cmds[:max_cmds]:
|
||||||
|
resolved, is_safe = validate_command(cmd, online, player, config)
|
||||||
|
if not is_safe:
|
||||||
|
continue
|
||||||
|
log.info(f"SUDO fallback execute: {resolved}")
|
||||||
|
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
||||||
|
log.info(f"SUDO fallback result: {result!r}")
|
||||||
|
executed.append(resolved)
|
||||||
|
results_seen.append((resolved, str(result or "")))
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
|
||||||
|
ineffective = (len(executed) == 0) or (effective_hits == 0)
|
||||||
|
|
||||||
|
_report_sudo_feedback(player, prompt, commands, results_seen, ineffective, config)
|
||||||
|
|
||||||
add_sudo_history(player, prompt, commands, executed)
|
add_sudo_history(player, prompt, commands, executed)
|
||||||
|
|
||||||
|
|
||||||
@@ -2586,6 +3013,7 @@ BIBLE_LINES = [
|
|||||||
("God watches over this server.", "yellow", False),
|
("God watches over this server.", "yellow", False),
|
||||||
("Speak to him by typing in chat:", "white", False),
|
("Speak to him by typing in chat:", "white", False),
|
||||||
(" pray <your message>", "green", True),
|
(" pray <your message>", "green", True),
|
||||||
|
(" bug_log <optional description>", "aqua", True),
|
||||||
("", "white", False),
|
("", "white", False),
|
||||||
("God is benevolent, but just.", "yellow", False),
|
("God is benevolent, but just.", "yellow", False),
|
||||||
("He hears every prayer — but answers as he sees fit.", "white", False),
|
("He hears every prayer — but answers as he sees fit.", "white", False),
|
||||||
@@ -2595,6 +3023,7 @@ BIBLE_LINES = [
|
|||||||
(" pray Lord, bless my journey through the mines.", "gray", False),
|
(" pray Lord, bless my journey through the mines.", "gray", False),
|
||||||
(" pray Smite my enemy, for they have wronged me.", "gray", False),
|
(" pray Smite my enemy, for they have wronged me.", "gray", False),
|
||||||
(" pray Forgive me, I have sinned against thy creations.", "gray", False),
|
(" pray Forgive me, I have sinned against thy creations.", "gray", False),
|
||||||
|
(" bug_log creeper explosion desynced and killed me", "dark_aqua", False),
|
||||||
("", "white", False),
|
("", "white", False),
|
||||||
("Thou may only pray once every 20 seconds.", "red", False),
|
("Thou may only pray once every 20 seconds.", "red", False),
|
||||||
("Type \"bible\" in chat to see this again.", "gray", False),
|
("Type \"bible\" in chat to see this again.", "gray", False),
|
||||||
@@ -2783,6 +3212,24 @@ def main():
|
|||||||
# Feed every line into the rolling log buffer
|
# Feed every line into the rolling log buffer
|
||||||
add_log_event(line)
|
add_log_event(line)
|
||||||
|
|
||||||
|
# bug logging trigger
|
||||||
|
matched = False
|
||||||
|
for pat in BUG_LOG_PATTERNS:
|
||||||
|
m = pat.search(line)
|
||||||
|
if m:
|
||||||
|
player = m.group(1)
|
||||||
|
description = (m.group(2) or "").strip()
|
||||||
|
log.info(f"BUG_LOG from {player}: {description or '(no description)'}")
|
||||||
|
try:
|
||||||
|
process_bug_log(player, description, config)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error processing bug_log: {e}", exc_info=True)
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched:
|
||||||
|
continue
|
||||||
|
|
||||||
# sudo translator
|
# sudo translator
|
||||||
matched = False
|
matched = False
|
||||||
for pat in SUDO_PATTERNS:
|
for pat in SUDO_PATTERNS:
|
||||||
|
|||||||
Reference in New Issue
Block a user