Error correction, fire fix, Gemini pricing, command format examples

- Error correction on both sudo and pray paths
- Broadened RCON error detection: <--[HERE] catches all syntax errors
- Fixed fire fallback matching "firework" as fire intent
- Dynamic Gemini pricing by model name
- Command format RIGHT vs WRONG examples in prompts
- max_tokens 600 for command calls
- Removed template workflow from sudo prompt and context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-03-19 23:09:42 -04:00
parent 4706952c52
commit 618c98cc4e
+123 -16
View File
@@ -1772,10 +1772,13 @@ def _parse_llm_json(content: str) -> dict:
If max_tokens cuts the response mid-string, we attempt to salvage If max_tokens cuts the response mid-string, we attempt to salvage
whatever message and commands were already present. whatever message and commands were already present.
""" """
log.info(f"Raw LLM content: {content[:500]}")
# Strip think blocks before parsing
content = re.sub(r'<think>[\s\S]*?</think>\s*', '', content)
try: try:
return json.loads(content) return json.loads(content)
except json.JSONDecodeError: except json.JSONDecodeError:
log.warning("LLM response truncated — attempting repair") log.warning(f"LLM response truncated — attempting repair. Raw: {content[:300]}")
# Extract message if present, even if truncated # Extract message if present, even if truncated
msg_m = re.search(r'"message"\s*:\s*"((?:[^"\\]|\\.)*)', content) msg_m = re.search(r'"message"\s*:\s*"((?:[^"\\]|\\.)*)', content)
@@ -1853,8 +1856,16 @@ COMMANDS_SYSTEM_PROMPT = (
"decide what server commands to run (if any) as an act of divine judgment.\n\n" "decide what server commands to run (if any) as an act of divine judgment.\n\n"
"Respond ONLY with a valid JSON object, nothing else:\n" "Respond ONLY with a valid JSON object, nothing else:\n"
"{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n" "{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n"
+ (_GOD_SOUL + "\n\n" if _GOD_SOUL else "") "IMPORTANT: Each command is ONE COMPLETE STRING. Example:\n"
+ "SYNTAX RULES:\n" "{\"commands\": [\"give slingshooter08 minecraft:diamond_sword 1\"]}\n"
"WRONG: {\"commands\": [\"give\", \"slingshooter08\", \"minecraft:diamond_sword\"]}\n\n"
"Rules:\n"
"- commands may be empty [] if no action is warranted.\n"
"- Reward humble prayers. Punish hubris or blasphemy. Be unpredictable.\n"
"- Consider the player's inventory and state.\n"
"- Powerful rewards (netherite, enchanted_golden_apple, totem) must be rare.\n"
"- kill is reserved for extreme blasphemy only.\n\n"
"SYNTAX RULES:\n"
"- {player} = the praying player. You may target any other online player by name.\n" "- {player} = the praying player. You may target any other online player by name.\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"
@@ -1879,9 +1890,12 @@ def build_sudo_commands_system_prompt(config=None) -> str:
"Minecraft server commands. You do NOT roleplay.\n\n" "Minecraft server commands. You do NOT roleplay.\n\n"
"Respond ONLY with valid JSON:\n" "Respond ONLY with valid JSON:\n"
"{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n" "{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n"
"IMPORTANT: Each command is ONE COMPLETE STRING. Example:\n"
"{\"commands\": [\"give slingshooter08 minecraft:diamond_sword 1\", \"effect give slingshooter08 minecraft:speed 60 1\"]}\n"
"WRONG: {\"commands\": [\"give\", \"slingshooter08\", \"minecraft:diamond_sword\", \"1\"]}\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" "- Do NOT output 'template' commands. Only output valid Minecraft RCON commands.\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" "- Do not broaden scope to @a/@e unless the user explicitly asks for all players/entities.\n"
@@ -1893,11 +1907,7 @@ def build_sudo_commands_system_prompt(config=None) -> str:
" If quantity is requested, output multiple summon commands.\n" " If quantity is requested, output multiple summon commands.\n"
"- Never use old NBT enchant syntax like {Enchantments:[...]}; use item[enchantments={...}] only.\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, use fill/setblock/clone commands. Do not use template commands.\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"
@@ -2106,11 +2116,21 @@ def _gemini_call(system: str, user: str, config: dict,
text = data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "") text = data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "")
# Estimate cost (Gemini Flash Lite: $0.075/M input, $0.30/M output) # Cost by model (per million tokens)
# flash-lite: $0.075 in, $0.30 out
# 2.5-flash: $0.15 in, $0.60 out
# 2.5-pro: $1.25 in, $10.00 out
usage = data.get("usageMetadata", {}) usage = data.get("usageMetadata", {})
input_tokens = usage.get("promptTokenCount", 500) input_tokens = usage.get("promptTokenCount", 500)
output_tokens = usage.get("candidatesTokenCount", 150) output_tokens = usage.get("candidatesTokenCount", 150)
cost = (input_tokens / 1_000_000) * 0.075 + (output_tokens / 1_000_000) * 0.30 pricing = {
"gemini-2.5-flash-lite": (0.075, 0.30),
"gemini-2.5-flash": (0.15, 0.60),
"gemini-2.0-flash": (0.10, 0.40),
"gemini-2.5-pro": (1.25, 10.00),
}
in_rate, out_rate = pricing.get(model, (0.15, 0.60))
cost = (input_tokens / 1_000_000) * in_rate + (output_tokens / 1_000_000) * out_rate
with _gemini_cost_lock: with _gemini_cost_lock:
prev_dollar = int(_gemini_total_cost) prev_dollar = int(_gemini_total_cost)
@@ -2450,7 +2470,7 @@ def ask_god(player, prayer, context, config):
config=config, config=config,
fmt="json", fmt="json",
temperature=0.3, # low temp for precise structured output temperature=0.3, # low temp for precise structured output
max_tokens=200, max_tokens=600,
) )
cmd_result = _parse_llm_json(cmd_content) cmd_result = _parse_llm_json(cmd_content)
commands = cmd_result.get("commands") or [] commands = cmd_result.get("commands") or []
@@ -2552,7 +2572,7 @@ def ask_god_intervention(context, config):
config=config, config=config,
fmt="json", fmt="json",
temperature=0.3, temperature=0.3,
max_tokens=200, max_tokens=600,
) )
commands = (_parse_llm_json(cmd_content) or {}).get("commands") or [] commands = (_parse_llm_json(cmd_content) or {}).get("commands") or []
log.info(f"Intervention commands: {commands}") log.info(f"Intervention commands: {commands}")
@@ -3242,7 +3262,10 @@ def _is_fire_intent(prompt: str) -> bool:
p = (prompt or "").lower() p = (prompt or "").lower()
if "tnt" in p: if "tnt" in p:
return False return False
return any(k in p for k in ("fire", "ignite", "burn", "flame")) # Exclude firework/firecracker/fire_resistance/campfire etc
if any(k in p for k in ("firework", "firecracker", "fire_resistance", "fire_protection", "campfire", "fire aspect", "fire_aspect")):
return False
return any(k in p for k in ("set fire", "set on fire", "ignite", "burn it", "burn the", "light on fire"))
def _normalize_sudo_command_shape(cmd: str, player: str) -> str: def _normalize_sudo_command_shape(cmd: str, player: str) -> str:
@@ -3458,6 +3481,63 @@ def _expand_tnt_commands_from_prompt(commands: list, prompt: str, player: str, c
log.warning(f"Expanded TNT commands from {len(commands)} to {len(expanded)} (requested={requested}, cap={cap})") log.warning(f"Expanded TNT commands from {len(commands)} to {len(expanded)} (requested={requested}, cap={cap})")
return expanded return expanded
_RCON_ERROR_PATTERNS = [
"Unknown or incomplete command",
"Unknown game mode",
"Unknown item",
"Unknown enchantment",
"Unknown effect",
"Unknown entity",
"Incorrect argument",
"Expected whitespace",
"Expected ",
"Invalid or unknown",
"No such",
"<--[HERE]",
]
def _is_rcon_error(result: str) -> bool:
"""Check if RCON result indicates a command error (not just 'no player found')."""
if not result:
return False
r = result.lower()
# Benign non-errors
if "no player was found" in r or "that position is not loaded" in r:
return False
return any(p.lower() in r for p in _RCON_ERROR_PATTERNS)
_ERROR_CORRECTION_SYSTEM = (
"A Minecraft command failed. Analyze the error and return a corrected command.\n"
"Respond with JSON: {\"corrected\": \"the fixed command\"}\n"
"The command must be a single complete string. Use minecraft: prefix for all items/effects.\n"
"Use 1.21 syntax: enchantments use [enchantments={name:level}] NOT {Enchantments:[...]}.\n"
"If you cannot fix it, return {\"corrected\": \"\"}."
)
def _attempt_error_correction(failed_cmd: str, error_msg: str, config: dict) -> str:
"""Ask the model to fix a failed command. Returns corrected command or empty string."""
try:
command_model = config.get("command_model", config["model"])
content = _llm_call(
model=command_model,
system=_ERROR_CORRECTION_SYSTEM,
user=f"Failed command: {failed_cmd}\nError: {error_msg}",
config=config,
fmt="json",
temperature=0.1,
max_tokens=200,
)
parsed = _parse_llm_json(content)
corrected = parsed.get("corrected") or ""
if corrected:
log.info(f"Error correction: '{failed_cmd}' -> '{corrected}'")
return corrected
except Exception as e:
log.warning(f"Error correction failed: {e}")
return ""
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 []
@@ -3549,6 +3629,20 @@ def execute_response(response, context, config, praying_player=None):
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}")
# Error correction: if RCON returns an error, ask model to fix and retry once
if config.get("error_correction", True) and result and _is_rcon_error(result):
log.info(f"RCON error detected, attempting correction: {result}")
corrected = _attempt_error_correction(resolved, result, config)
if corrected and corrected != resolved:
corrected_resolved, corrected_safe = validate_command(
corrected, context["online_players"], fallback, config
)
if corrected_safe:
log.info(f"Retrying with corrected command: {corrected_resolved}")
retry_result = rcon(corrected_resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"Retry RCON result: {retry_result!r}")
if resolved.startswith("weather "): if resolved.startswith("weather "):
if "thunder" in resolved: config["_weather_state"] = "thunderstorm" if "thunder" in resolved: config["_weather_state"] = "thunderstorm"
elif "rain" in resolved: config["_weather_state"] = "rain" elif "rain" in resolved: config["_weather_state"] = "rain"
@@ -3889,7 +3983,6 @@ 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()
@@ -3906,7 +3999,7 @@ def process_sudo(player, prompt, config):
config=config, config=config,
fmt="json", fmt="json",
temperature=0.1, temperature=0.1,
max_tokens=180, max_tokens=600,
) )
parsed = _parse_llm_json(content) parsed = _parse_llm_json(content)
return parsed.get("commands") or [] return parsed.get("commands") or []
@@ -4052,6 +4145,20 @@ 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}")
_sudo_trace(player, f"[SUDO RES] {str(result or '')[:180]}", config) _sudo_trace(player, f"[SUDO RES] {str(result or '')[:180]}", config)
# Error correction for sudo
if config.get("error_correction", True) and result and _is_rcon_error(result):
log.info(f"SUDO error detected, attempting correction: {result}")
corrected = _attempt_error_correction(resolved, result, config)
if corrected and corrected != resolved:
corrected_resolved, corrected_safe = validate_command(corrected, online, player, config)
if corrected_safe:
log.info(f"SUDO retry: {corrected_resolved}")
_sudo_trace(player, f"[SUDO RETRY] {corrected_resolved}", config)
result = rcon(corrected_resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"SUDO retry result: {result!r}")
resolved = corrected_resolved
executed.append(resolved) executed.append(resolved)
results_seen.append((resolved, str(result or ""))) results_seen.append((resolved, str(result or "")))
time.sleep(0.2) time.sleep(0.2)