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:
+123
-16
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user