Add retrieval-grounded sudo flow and execution feedback loop
This commit is contained in:
+329
-9
@@ -76,6 +76,10 @@ first_login_seen = set()
|
||||
SUDO_HISTORY_SIZE = 10
|
||||
sudo_history: deque = deque() # entries: (ts, player, prompt, translated_cmds, executed_cmds)
|
||||
|
||||
# Sudo failure memory — last N failed command/result pairs, for anti-repeat context.
|
||||
SUDO_FAILURE_SIZE = 20
|
||||
sudo_failures: deque = deque() # entries: (ts, player, command, error)
|
||||
|
||||
_memory_lock = threading.Lock()
|
||||
|
||||
# Gateway client session mapping (player+mode -> session_id)
|
||||
@@ -980,19 +984,71 @@ def get_last_sudo_executed_command(player: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def add_sudo_failure(player: str, command: str, error: str):
|
||||
"""Record a failed sudo command/result so future prompts can avoid repeats."""
|
||||
c = (command or "").strip()[:220]
|
||||
e = re.sub(r'\s+', ' ', (error or "")).strip()[:220]
|
||||
if not c or not e:
|
||||
return
|
||||
with _memory_lock:
|
||||
sudo_failures.append((time.time(), player, c, e))
|
||||
while len(sudo_failures) > SUDO_FAILURE_SIZE:
|
||||
sudo_failures.popleft()
|
||||
|
||||
|
||||
def get_sudo_failures_block(player: str = "") -> str:
|
||||
"""Return recent failed sudo commands as anti-pattern context."""
|
||||
with _memory_lock:
|
||||
entries = list(sudo_failures)
|
||||
if player:
|
||||
entries = [e for e in entries if e[1] == player]
|
||||
if not entries:
|
||||
return ""
|
||||
now = time.time()
|
||||
lines = []
|
||||
for ts, p, cmd, err in entries[-8:]:
|
||||
mins = int((now - ts) / 60)
|
||||
lines.append(f" [{mins}m ago] {p}: {cmd} -> {err}")
|
||||
return "\n=== RECENT FAILED SUDO PATTERNS ===\n" + "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _bug_log_path(config) -> str:
|
||||
return config.get("bug_log_path", "/var/log/mc_aigod_paper_bug.log")
|
||||
|
||||
|
||||
def _read_recent_raw_log_lines(log_path: str, max_lines: int) -> list:
|
||||
lines = deque(maxlen=max_lines)
|
||||
lines = deque(maxlen=max_lines * 4)
|
||||
noise = re.compile(r'RCON (?:Listener|Client)|Thread RCON Client')
|
||||
try:
|
||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
for line in f:
|
||||
lines.append(line.rstrip('\n'))
|
||||
clean = line.rstrip('\n')
|
||||
if noise.search(clean):
|
||||
continue
|
||||
lines.append(clean)
|
||||
except Exception as e:
|
||||
return [f"<unable to read raw server log: {e}>"]
|
||||
return list(lines)
|
||||
return list(lines)[-max_lines:]
|
||||
|
||||
|
||||
def _service_log_path(config) -> str:
|
||||
return config.get("service_log_path", "/var/log/mc_aigod_paper.log")
|
||||
|
||||
|
||||
def _read_recent_service_action_lines(path: str, max_lines: int) -> list:
|
||||
lines = deque(maxlen=max_lines * 8)
|
||||
keep = re.compile(
|
||||
r'Prayer from|SUDO from|Executing RCON:|SUDO execute:|RCON result:|SUDO result:|BUG_LOG from'
|
||||
)
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
for line in f:
|
||||
clean = line.rstrip('\n')
|
||||
if keep.search(clean):
|
||||
lines.append(clean)
|
||||
except Exception as e:
|
||||
return [f"<unable to read service log: {e}>"]
|
||||
return list(lines)[-max_lines:]
|
||||
|
||||
|
||||
def _format_recent_event_lines(max_events: int) -> list:
|
||||
@@ -1012,9 +1068,11 @@ def process_bug_log(player: str, description: str, config):
|
||||
|
||||
event_count = int(config.get("bug_log_event_lines", 40))
|
||||
raw_count = int(config.get("bug_log_raw_lines", 120))
|
||||
service_count = int(config.get("bug_log_service_lines", 30))
|
||||
|
||||
recent_events = _format_recent_event_lines(event_count)
|
||||
raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count)
|
||||
service_lines = _read_recent_service_action_lines(_service_log_path(config), service_count)
|
||||
bug_path = _bug_log_path(config)
|
||||
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
|
||||
@@ -1033,6 +1091,11 @@ def process_bug_log(player: str, description: str, config):
|
||||
bf.write("\n".join(recent_events) + "\n")
|
||||
else:
|
||||
bf.write("(no recent in-memory events captured)\n")
|
||||
bf.write("\n-- RECENT AI ACTIONS (SERVICE LOG) --\n")
|
||||
if service_lines:
|
||||
bf.write("\n".join(service_lines) + "\n")
|
||||
else:
|
||||
bf.write("(no AI service lines available)\n")
|
||||
bf.write("\n-- RECENT RAW SERVER LOG LINES --\n")
|
||||
if raw_lines:
|
||||
bf.write("\n".join(raw_lines) + "\n")
|
||||
@@ -1382,6 +1445,8 @@ MOVEMENT:
|
||||
NEVER use: tp <player> minecraft:the_nether (this is wrong syntax)
|
||||
|
||||
WORLD/ENVIRONMENT (affects all players):
|
||||
SYNTAX: weather <clear|rain|thunder> [duration]
|
||||
NOTE: 'storm' is invalid; if intent says storm/rainstorm/thunderstorm use thunder.
|
||||
time set day
|
||||
time set night
|
||||
weather clear 6000
|
||||
@@ -1443,9 +1508,12 @@ def build_system_prompt(config):
|
||||
"- Do not ask a player to gather materials they clearly don't have access to or that are rare relative to their current situation.\n"
|
||||
"- For give commands: use any valid Minecraft 1.21 item ID following the Item Naming Rules. Do not guess item IDs — consult the naming rules and common IDs list.\n"
|
||||
"- For all other commands: only use forms shown in the Command Palette. Do not invent new command types.\n"
|
||||
"- Weather values are only clear, rain, or thunder. If you mean storm/rainstorm/thunderstorm, output thunder.\n"
|
||||
"- Reward humble, genuine prayers. Punish hubris, blasphemy, or naked greed.\n"
|
||||
"- Repeated greedy demands after recent gifts should usually receive correction (rebuke, debuff, or symbolic punishment), not more rewards.\n"
|
||||
"- Powerful rewards (netherite, enchanted_golden_apple, totem) must be rare.\n"
|
||||
"- kill {target} is reserved for extreme blasphemy only.\n"
|
||||
"- Avoid lethal accidents from vertical teleports. If using a high upward teleport as punishment or spectacle, pair it with slow_falling or resistance unless explicit execution is intended.\n"
|
||||
"- When angered, chain commands: thunder + lightning + debuffs = divine wrath.\n\n"
|
||||
"=== COMMAND PALETTE ===\n"
|
||||
f"{COMMAND_PALETTE}\n"
|
||||
@@ -1566,6 +1634,10 @@ COMMANDS_SYSTEM_PROMPT = (
|
||||
"- kill is reserved for extreme blasphemy only.\n"
|
||||
"- For give: syntax is always give <player> minecraft:<item_id> <count>\n"
|
||||
"- Count comes LAST. Namespace prefix minecraft: is REQUIRED.\n"
|
||||
"- For effects: use 'effect give <player> minecraft:<effect> <seconds> <amplifier>'.\n"
|
||||
"- For weather use only clear/rain/thunder (NOT storm).\n"
|
||||
"- Avoid accidental lethal movement in benevolent responses; do not launch players high unless explicitly asked.\n"
|
||||
"- Do not use tp in helpful/benevolent responses unless the player explicitly requests movement/teleportation.\n"
|
||||
"- Beds: white_bed not bed. Logs: oak_log not log. Wool: white_wool not wool.\n"
|
||||
"- Chain commands for dramatic effect: thunder + lightning + blindness = wrath.\n\n"
|
||||
+ "=== COMMAND PALETTE ===\n"
|
||||
@@ -1588,8 +1660,14 @@ def build_sudo_commands_system_prompt(config=None) -> str:
|
||||
"- You may also output meta-commands starting with 'template ' for schematic workflows (not RCON).\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"
|
||||
"- You will receive LAST 10 SUDO ACTIONS. Use them for continuity and corrections when the player says previous output was wrong.\n"
|
||||
"- You will also receive RECENT FAILED SUDO PATTERNS. Do not repeat those broken shapes.\n"
|
||||
"- For give syntax: give <player> minecraft:<item_id> <count> (count LAST, namespace required)\n"
|
||||
"- For effect syntax: effect give <player> minecraft:<effect> <seconds> <amplifier> [hideParticles]\n"
|
||||
"- For summon tnt: summon minecraft:tnt <x> <y> <z> (NO trailing count number).\n"
|
||||
" 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"
|
||||
@@ -1683,6 +1761,7 @@ def build_message_system_prompt(config) -> str:
|
||||
"Write a single spoken message to all players reacting to this prayer and action.\n"
|
||||
"Respond with ONLY the message text — no JSON, no quotes, no formatting. "
|
||||
"Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n"
|
||||
"For punishments, prefer fear and humiliation over accidental instant death unless destruction is explicitly intended.\n"
|
||||
)
|
||||
lore = config.get("god_lore", "")
|
||||
if lore:
|
||||
@@ -2070,6 +2149,53 @@ def _tp_inside_worldborder(cmd: str, config) -> bool:
|
||||
|
||||
return abs(x - cx) <= limit and abs(z - cz) <= limit
|
||||
|
||||
|
||||
def _tp_safety_enabled(config) -> bool:
|
||||
return bool(config.get("tp_safety_enabled", True))
|
||||
|
||||
|
||||
def _extract_tp_target_y(cmd: str):
|
||||
m = re.search(r'\btp\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', cmd)
|
||||
if not m:
|
||||
return None, None
|
||||
return m.group(1), m.group(3)
|
||||
|
||||
|
||||
def _needs_vertical_tp_safety(resolved_cmd: str, config) -> tuple:
|
||||
target, ytok = _extract_tp_target_y(resolved_cmd)
|
||||
if not target or not ytok:
|
||||
return False, ""
|
||||
|
||||
risky_delta = float(config.get("tp_safety_vertical_delta", 8.0))
|
||||
risky_abs_y = float(config.get("tp_safety_absolute_y", 120.0))
|
||||
|
||||
if ytok.startswith("~"):
|
||||
offs = ytok[1:].strip()
|
||||
dy = float(offs) if offs else 0.0
|
||||
if dy >= risky_delta:
|
||||
return True, target
|
||||
return False, target
|
||||
|
||||
try:
|
||||
abs_y = float(ytok)
|
||||
if abs_y >= risky_abs_y:
|
||||
return True, target
|
||||
except Exception:
|
||||
pass
|
||||
return False, target
|
||||
|
||||
|
||||
def _tp_safety_prefix_commands(resolved_cmd: str, config) -> list:
|
||||
if not _tp_safety_enabled(config):
|
||||
return []
|
||||
needs, target = _needs_vertical_tp_safety(resolved_cmd, config)
|
||||
if not needs or not target:
|
||||
return []
|
||||
return [
|
||||
f"effect give {target} minecraft:slow_falling 20 0",
|
||||
f"effect give {target} minecraft:resistance 8 1",
|
||||
]
|
||||
|
||||
def fix_give_command(cmd: str) -> str:
|
||||
"""
|
||||
Correct common LLM give command mistakes:
|
||||
@@ -2167,6 +2293,80 @@ def fix_gamemode_command(cmd: str, fallback_player: str) -> str:
|
||||
log.warning(f"Fixed gamemode syntax: '{cmd}' -> '{fixed}'")
|
||||
return fixed
|
||||
|
||||
|
||||
def fix_weather_command(cmd: str) -> str:
|
||||
"""Normalize weather synonyms to valid Minecraft literals."""
|
||||
raw = (cmd or "").strip()
|
||||
fixed = re.sub(r'\bweather\s+(storm|rainstorm|thunderstorm)\b', 'weather thunder', raw, flags=re.IGNORECASE)
|
||||
if fixed != raw:
|
||||
log.warning(f"Fixed weather syntax: '{cmd}' -> '{fixed}'")
|
||||
return fixed
|
||||
|
||||
|
||||
def fix_fill_fire_command(cmd: str) -> str:
|
||||
"""Fix legacy fill syntax like `... fire 0 replace air` for 1.21."""
|
||||
raw = (cmd or "").strip()
|
||||
m = re.match(r'^(fill\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+)(minecraft:)?(fire|soul_fire)\s+0\s+replace\s+air$', raw, flags=re.IGNORECASE)
|
||||
if not m:
|
||||
return raw
|
||||
prefix, ns, block = m.groups()
|
||||
block_id = f"minecraft:{block.lower()}" if not ns else f"{ns.lower()}{block.lower()}"
|
||||
fixed = f"{prefix}{block_id} replace air"
|
||||
log.warning(f"Fixed fill fire syntax: '{cmd}' -> '{fixed}'")
|
||||
return fixed
|
||||
|
||||
|
||||
def fix_bow_enchant_syntax(cmd: str) -> str:
|
||||
"""Rewrite old bow Enchantments NBT to 1.21 component format."""
|
||||
raw = (cmd or "").strip()
|
||||
if "Enchantments:[" not in raw or "bow{" not in raw:
|
||||
return raw
|
||||
m = re.match(r'^(give\s+\S+\s+)(minecraft:)?bow\{Enchantments:\[(.+)\]\}\s+(\d+)$', raw)
|
||||
if not m:
|
||||
return raw
|
||||
pre, _, body, count = m.groups()
|
||||
ench = {}
|
||||
for eid, lvl in re.findall(r'id:?\"?([a-z_]+)\"?,\s*lvl:([0-9]+)s?', body):
|
||||
ench[eid] = lvl
|
||||
if not ench:
|
||||
return raw
|
||||
ench_part = ",".join(f"{k}:{v}" for k, v in ench.items())
|
||||
fixed = f"{pre}minecraft:bow[enchantments={{{ench_part}}}] {count}"
|
||||
log.warning(f"Fixed bow enchant syntax: '{cmd}' -> '{fixed}'")
|
||||
return fixed
|
||||
|
||||
|
||||
def _repair_execute_tail(cmd: str, fallback_player: str) -> str:
|
||||
"""Apply syntax repairers to `execute ... run <tail>` payloads."""
|
||||
raw = (cmd or "").strip()
|
||||
tail = raw
|
||||
stack = []
|
||||
# Peel execute wrappers up to a small depth.
|
||||
for _ in range(4):
|
||||
if not tail.startswith("execute "):
|
||||
break
|
||||
marker = " run "
|
||||
idx = tail.find(marker)
|
||||
if idx < 0:
|
||||
break
|
||||
stack.append(tail[: idx + len(marker)])
|
||||
tail = tail[idx + len(marker):].strip()
|
||||
|
||||
fixed_tail = tail
|
||||
fixed_tail = fix_give_command(fixed_tail)
|
||||
fixed_tail = fix_effect_command(fixed_tail)
|
||||
fixed_tail = fix_gamemode_command(fixed_tail, fallback_player)
|
||||
fixed_tail = fix_weather_command(fixed_tail)
|
||||
fixed_tail = fix_fill_fire_command(fixed_tail)
|
||||
fixed_tail = fix_bow_enchant_syntax(fixed_tail)
|
||||
|
||||
if stack:
|
||||
rebuilt = "".join(stack) + fixed_tail
|
||||
if rebuilt != raw:
|
||||
log.warning(f"Fixed execute-tail syntax: '{raw}' -> '{rebuilt}'")
|
||||
return rebuilt
|
||||
return raw
|
||||
|
||||
def validate_command(cmd, online_players, fallback_player, config=None):
|
||||
"""Replace placeholders, auto-fix common give syntax errors, check safe prefix."""
|
||||
resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player)
|
||||
@@ -2176,6 +2376,10 @@ def validate_command(cmd, online_players, fallback_player, config=None):
|
||||
resolved = fix_give_command(resolved)
|
||||
resolved = fix_effect_command(resolved)
|
||||
resolved = fix_gamemode_command(resolved, fallback_player)
|
||||
resolved = fix_weather_command(resolved)
|
||||
resolved = fix_fill_fire_command(resolved)
|
||||
resolved = fix_bow_enchant_syntax(resolved)
|
||||
resolved = _repair_execute_tail(resolved, fallback_player)
|
||||
caps = get_server_capabilities(config) if config else SERVER_CAPABILITIES[DEFAULT_SERVER_TYPE]
|
||||
prefixes = caps["safe_prefixes"]
|
||||
if not any(resolved.startswith(p) for p in prefixes):
|
||||
@@ -2344,9 +2548,9 @@ def _repair_failed_sudo_commands(player: str, results_seen: list, config) -> lis
|
||||
xx = x if x.startswith("~") else str(int(float(x)) + dx)
|
||||
zz = z if z.startswith("~") else str(int(float(z)) + dz)
|
||||
if x.startswith("~") and dx != 0:
|
||||
xx = f"~{dx:+d}"
|
||||
xx = f"~{dx}" if dx < 0 else f"~{dx}"
|
||||
if z.startswith("~") and dz != 0:
|
||||
zz = f"~{dz:+d}"
|
||||
zz = f"~{dz}" if dz < 0 else f"~{dz}"
|
||||
out.append(f"{prefix}summon minecraft:tnt {xx} {y} {zz}")
|
||||
if len(out) >= max_retry:
|
||||
return out
|
||||
@@ -2363,6 +2567,53 @@ def _repair_failed_sudo_commands(player: str, results_seen: list, config) -> lis
|
||||
|
||||
return out[:max_retry]
|
||||
|
||||
|
||||
def _expand_tnt_commands_from_prompt(commands: list, prompt: str, player: str, config) -> list:
|
||||
"""If user asked for many TNT and model returned too few summons, expand boundedly."""
|
||||
p = (prompt or "").lower()
|
||||
if "tnt" not in p:
|
||||
return commands
|
||||
nums = [int(n) for n in re.findall(r'\b(\d{1,3})\b', p)]
|
||||
if not nums:
|
||||
return commands
|
||||
requested = max(nums)
|
||||
cap = int(config.get("sudo_tnt_max_commands", 80))
|
||||
target = max(1, min(requested, cap))
|
||||
if len(commands) >= target:
|
||||
return commands
|
||||
|
||||
summons = [c for c in commands if "summon" in c and "tnt" in c]
|
||||
if not summons:
|
||||
return commands
|
||||
|
||||
base = summons[0]
|
||||
prefix = ""
|
||||
body = base
|
||||
m_pref = re.match(rf'^(execute\s+at\s+{re.escape(player)}\s+run\s+)(.+)$', base)
|
||||
if m_pref:
|
||||
prefix = m_pref.group(1)
|
||||
body = m_pref.group(2)
|
||||
|
||||
m = re.match(r'^summon\s+(?:minecraft:)?tnt\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+\{.*\})?$', body)
|
||||
if not m:
|
||||
return commands
|
||||
x, y, z = m.groups()
|
||||
|
||||
expanded = []
|
||||
for i in range(target):
|
||||
dx = (i % 9) - 4
|
||||
dz = (i // 9) - 4
|
||||
xx = x
|
||||
zz = z
|
||||
if x.startswith("~"):
|
||||
xx = "~" if dx == 0 else f"~{dx}"
|
||||
if z.startswith("~"):
|
||||
zz = "~" if dz == 0 else f"~{dz}"
|
||||
expanded.append(f"{prefix}summon minecraft:tnt {xx} {y} {zz}")
|
||||
|
||||
log.warning(f"Expanded TNT commands from {len(commands)} to {len(expanded)} (requested={requested}, cap={cap})")
|
||||
return expanded
|
||||
|
||||
def execute_response(response, context, config, praying_player=None):
|
||||
message = response.get("message") or ""
|
||||
commands = response.get("commands") or []
|
||||
@@ -2421,6 +2672,17 @@ def execute_response(response, context, config, praying_player=None):
|
||||
resolved, is_safe = validate_command(cmd, context["online_players"], fallback, config)
|
||||
if not is_safe:
|
||||
continue
|
||||
|
||||
safety_prefix = _tp_safety_prefix_commands(resolved, config)
|
||||
for scmd in safety_prefix:
|
||||
sresolved, safe_ok = validate_command(scmd, context["online_players"], fallback, config)
|
||||
if not safe_ok:
|
||||
continue
|
||||
log.info(f"Executing RCON: {sresolved}")
|
||||
sresult = rcon(sresolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
|
||||
log.info(f"RCON result: {sresult!r}")
|
||||
time.sleep(0.15)
|
||||
|
||||
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}")
|
||||
@@ -2587,10 +2849,33 @@ def process_sudo(player, prompt, config):
|
||||
if process_sudo_template_command(player, prompt, config):
|
||||
return
|
||||
|
||||
def _looks_like_lookup_question(text: str) -> bool:
|
||||
t = (text or "").strip().lower()
|
||||
if not t:
|
||||
return False
|
||||
if t.endswith("?"):
|
||||
return True
|
||||
q_starts = (
|
||||
"what ", "why ", "how ", "did ", "does ", "is ", "are ",
|
||||
"can ", "could ", "should ", "would ", "where ", "when ",
|
||||
)
|
||||
if t.startswith(q_starts):
|
||||
return True
|
||||
if re.search(r'\b(that command|last command|did that|does that)\b', t):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _local_lookup_fallback_answer(query: str, ref_cmd: str) -> str:
|
||||
q = (query or "").lower()
|
||||
rc = (ref_cmd or "").lower()
|
||||
if "invisible" in q and "mob" in q and "invisibility" in rc:
|
||||
return "Invisibility greatly reduces mob detection, but it does not make you perfectly undetectable at close range or while making noise/actions."
|
||||
return ""
|
||||
|
||||
# Deterministic lookup mode: information only, no command execution.
|
||||
low = prompt.lower().strip()
|
||||
lookup_prefixes = ("lookup ", "search ", "wiki ", "explain ", "what is ", "how do i ", "how to ")
|
||||
if low.startswith(lookup_prefixes):
|
||||
if low.startswith(lookup_prefixes) or _looks_like_lookup_question(prompt):
|
||||
query = prompt.strip()
|
||||
if low.startswith("lookup "):
|
||||
query = prompt[len("lookup "):].strip()
|
||||
@@ -2614,6 +2899,7 @@ def process_sudo(player, prompt, config):
|
||||
|
||||
wiki_rows = _info_lookup_wiki(lookup_query)
|
||||
web_rows = _info_lookup_web(lookup_query)
|
||||
gateway_msg = ""
|
||||
|
||||
if wiki_rows:
|
||||
_send_private(player, "minecraft.wiki:", config, "dark_aqua")
|
||||
@@ -2647,6 +2933,7 @@ def process_sudo(player, prompt, config):
|
||||
msg = (out.get("message") or "").strip()
|
||||
trace = out.get("tool_trace") or []
|
||||
if msg:
|
||||
gateway_msg = msg
|
||||
_send_private(player, "justification:", config, "dark_aqua")
|
||||
for ln in re.split(r"\n+", msg)[:3]:
|
||||
ln = ln.strip()
|
||||
@@ -2659,9 +2946,14 @@ def process_sudo(player, prompt, config):
|
||||
q = str(t.get("input", ""))[:80]
|
||||
_send_private(player, f"- {tool}: {q}", config, "dark_gray")
|
||||
|
||||
if not wiki_rows and not web_rows:
|
||||
_send_private(player, "No lookup results found.", config, "yellow")
|
||||
if not wiki_rows and not web_rows and not gateway_msg:
|
||||
fb = _local_lookup_fallback_answer(lookup_query, last_cmd)
|
||||
if fb:
|
||||
_send_private(player, f"- {fb}", config, "gray")
|
||||
else:
|
||||
_send_private(player, "No lookup results found.", config, "yellow")
|
||||
except Exception as e:
|
||||
log.warning(f"SUDO lookup failed for query={lookup_query!r}: {e}")
|
||||
_send_private(player, f"[SUDO-LOOKUP] Failed: {e}", config, "red")
|
||||
return
|
||||
|
||||
@@ -2717,6 +3009,7 @@ def process_sudo(player, prompt, config):
|
||||
+ (positions_block + "\n" if positions_block else "")
|
||||
+ f"Natural language request: {prompt}\n"
|
||||
+ get_sudo_history_block()
|
||||
+ get_sudo_failures_block(player)
|
||||
)
|
||||
|
||||
command_model = config.get("command_model", config["model"])
|
||||
@@ -2771,6 +3064,7 @@ def process_sudo(player, prompt, config):
|
||||
"request": prompt,
|
||||
"online_players": online,
|
||||
"sudo_history": get_sudo_history_block(),
|
||||
"sudo_failures": get_sudo_failures_block(player),
|
||||
"mode": "sudo",
|
||||
},
|
||||
config=config,
|
||||
@@ -2792,7 +3086,22 @@ def process_sudo(player, prompt, config):
|
||||
)
|
||||
return
|
||||
|
||||
build_intent = any(prompt.lower().strip().startswith(x) for x in ("build ", "make ", "create "))
|
||||
def _is_build_intent(text: str) -> bool:
|
||||
t = (text or "").strip().lower()
|
||||
if t.startswith("build ") or t.startswith("create "):
|
||||
return True
|
||||
if "schem" in t or "schematic" in t or "template" in t:
|
||||
return True
|
||||
if t.startswith("make "):
|
||||
build_nouns = (
|
||||
"house", "base", "tower", "wall", "castle", "bridge", "barn",
|
||||
"bar", "shop", "village", "room", "road", "farm", "portal",
|
||||
"structure", "schem", "schematic", "template", "statue", "arena",
|
||||
)
|
||||
return any(n in t for n in build_nouns)
|
||||
return False
|
||||
|
||||
build_intent = _is_build_intent(prompt)
|
||||
has_template_cmd = any(isinstance(c, str) and c.lower().startswith("template ") for c in commands)
|
||||
if build_intent and not has_template_cmd:
|
||||
try:
|
||||
@@ -2807,11 +3116,18 @@ def process_sudo(player, prompt, config):
|
||||
low_prompt = prompt.lower().strip()
|
||||
if any(low_prompt.startswith(x) for x in ("build ", "make ", "create ")):
|
||||
max_cmds = max(max_cmds, int(config.get("sudo_build_max_commands", 6)))
|
||||
if "tnt" in low_prompt:
|
||||
nums = re.findall(r'\b(\d{1,3})\b', low_prompt)
|
||||
if nums:
|
||||
requested = max(int(n) for n in nums)
|
||||
cap = int(config.get("sudo_tnt_max_commands", 80))
|
||||
max_cmds = max(max_cmds, min(requested, cap))
|
||||
commands = [
|
||||
_normalize_sudo_command_shape(c, player)
|
||||
for c in commands[:max_cmds]
|
||||
]
|
||||
commands = [c for c in commands if c]
|
||||
commands = _expand_tnt_commands_from_prompt(commands, prompt, player, config)
|
||||
|
||||
if not commands:
|
||||
add_sudo_history(player, prompt, [], [])
|
||||
@@ -2895,6 +3211,10 @@ def process_sudo(player, prompt, config):
|
||||
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
|
||||
ineffective = (len(executed) == 0) or (effective_hits == 0)
|
||||
|
||||
for cmd, res in results_seen:
|
||||
if not _sudo_result_is_effective(res):
|
||||
add_sudo_failure(player, cmd, res)
|
||||
|
||||
_report_sudo_feedback(player, prompt, commands, results_seen, ineffective, config)
|
||||
|
||||
add_sudo_history(player, prompt, commands, executed)
|
||||
|
||||
Reference in New Issue
Block a user