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
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:
return json.loads(content)
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
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"
"Respond ONLY with a valid JSON object, nothing else:\n"
"{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n"
+ (_GOD_SOUL + "\n\n" if _GOD_SOUL else "")
+ "SYNTAX RULES:\n"
"IMPORTANT: Each command is ONE COMPLETE STRING. Example:\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"
"- For give: syntax is always give <player> minecraft:<item_id> <count>\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"
"Respond ONLY with valid JSON:\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"
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 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"
@@ -1893,11 +1907,7 @@ def build_sudo_commands_system_prompt(config=None) -> str:
" 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"
"- 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"
"- For build requests, use fill/setblock/clone commands. Do not use template commands.\n"
"\n"
"=== TELEPORT SYNTAX ===\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", "")
# 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", {})
input_tokens = usage.get("promptTokenCount", 500)
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:
prev_dollar = int(_gemini_total_cost)
@@ -2450,7 +2470,7 @@ def ask_god(player, prayer, context, config):
config=config,
fmt="json",
temperature=0.3, # low temp for precise structured output
max_tokens=200,
max_tokens=600,
)
cmd_result = _parse_llm_json(cmd_content)
commands = cmd_result.get("commands") or []
@@ -2552,7 +2572,7 @@ def ask_god_intervention(context, config):
config=config,
fmt="json",
temperature=0.3,
max_tokens=200,
max_tokens=600,
)
commands = (_parse_llm_json(cmd_content) or {}).get("commands") or []
log.info(f"Intervention commands: {commands}")
@@ -3242,7 +3262,10 @@ def _is_fire_intent(prompt: str) -> bool:
p = (prompt or "").lower()
if "tnt" in p:
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:
@@ -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})")
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):
message = response.get("message") 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}")
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
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 "thunder" in resolved: config["_weather_state"] = "thunderstorm"
elif "rain" in resolved: config["_weather_state"] = "rain"
@@ -3889,7 +3983,6 @@ def process_sudo(player, prompt, config):
context_hint = (
f"Requesting player: {player}\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 "")
+ f"Natural language request: {prompt}\n"
+ get_sudo_history_block()
@@ -3906,7 +3999,7 @@ def process_sudo(player, prompt, config):
config=config,
fmt="json",
temperature=0.1,
max_tokens=180,
max_tokens=600,
)
parsed = _parse_llm_json(content)
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"])
log.info(f"SUDO result: {result!r}")
_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)
results_seen.append((resolved, str(result or "")))
time.sleep(0.2)