diff --git a/mc_aigod_paper.py b/mc_aigod_paper.py
index 9a8a435..df56ea6 100644
--- a/mc_aigod_paper.py
+++ b/mc_aigod_paper.py
@@ -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'[\s\S]*?\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 minecraft: \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 \n"
- " template pick [name]\n"
- " template build \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 \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 ; template pick [name] ; template build \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)