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
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user