diff --git a/mc_aigod_paper.json b/mc_aigod_paper.json index 2ae9ea6..6f955a8 100644 --- a/mc_aigod_paper.json +++ b/mc_aigod_paper.json @@ -37,7 +37,9 @@ "debug_commands": true, "sudo_enabled": true, "sudo_user": "slingshooter08", + "sudo_allow_all_players": true, "sudo_max_commands": 12, + "training_audit_path": "/var/log/mc_training_audit.jsonl", "bug_log_path": "/var/log/mc_aigod_paper_bug.log", "bug_log_event_lines": 40, "bug_log_raw_lines": 120, diff --git a/mc_aigod_paper.py b/mc_aigod_paper.py index 95f7d10..a020b9f 100644 --- a/mc_aigod_paper.py +++ b/mc_aigod_paper.py @@ -139,6 +139,12 @@ def _send_private(player: str, text: str, config, color: str = "gray", italic: b ) +def _sudo_trace(player: str, text: str, config, color: str = "dark_gray"): + if not bool(config.get("sudo_trace_commands", True)): + return + _send_private(player, text, config, color=color) + + def _template_path(config) -> str: return config.get("template_dir", DEFAULT_TEMPLATE_DIR) @@ -1057,7 +1063,8 @@ def _service_log_path(config) -> str: 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' + r'Prayer from|SUDO from|Executing RCON:|SUDO execute:|RCON result:|SUDO result:|BUG_LOG from|' + r'Gateway god_system|God intervenes unprompted|Blocked tp in unprompted intervention' ) try: with open(path, 'r', encoding='utf-8', errors='replace') as f: @@ -1070,6 +1077,32 @@ def _read_recent_service_action_lines(path: str, max_lines: int) -> list: return list(lines)[-max_lines:] +def _read_recent_intervention_lines(path: str, max_lines: int) -> list: + """Return focused history of unprompted intervention cycles.""" + out = deque(maxlen=max_lines * 6) + start = re.compile(r'Gateway god_system|God intervenes unprompted') + detail = re.compile( + r'Blocked tp in unprompted intervention|Executing RCON:|RCON result:|Next divine intervention' + ) + window = 0 + try: + with open(path, 'r', encoding='utf-8', errors='replace') as f: + for line in f: + clean = line.rstrip('\n') + if start.search(clean): + out.append(clean) + window = 14 + continue + if window > 0 and detail.search(clean): + out.append(clean) + window -= 1 + elif window > 0: + window -= 1 + except Exception as e: + return [f""] + return list(out)[-max_lines:] + + def _format_recent_event_lines(max_events: int) -> list: with _memory_lock: entries = list(recent_log)[-max_events:] @@ -1088,10 +1121,12 @@ 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)) + intervention_count = int(config.get("bug_log_intervention_lines", 80)) 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) + intervention_lines = _read_recent_intervention_lines(_service_log_path(config), intervention_count) bug_path = _bug_log_path(config) timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC') @@ -1115,6 +1150,11 @@ def process_bug_log(player: str, description: str, config): bf.write("\n".join(service_lines) + "\n") else: bf.write("(no AI service lines available)\n") + bf.write("\n-- RECENT INTERVENTION CYCLES (SERVICE LOG) --\n") + if intervention_lines: + bf.write("\n".join(intervention_lines) + "\n") + else: + bf.write("(no intervention lines available)\n") bf.write("\n-- RECENT RAW SERVER LOG LINES --\n") if raw_lines: bf.write("\n".join(raw_lines) + "\n") @@ -1122,6 +1162,10 @@ def process_bug_log(player: str, description: str, config): bf.write("(no raw lines available)\n") log.info(f"BUG_LOG recorded by {player}: {desc} -> {bug_path}") + + # Also write structured feedback to the training audit log + write_training_feedback(player, desc, config) + rcon( f'tellraw {player} {{"text":"[BUG_LOG] Logged. Thank you.","color":"green"}}', config["rcon_host"], config["rcon_port"], config["rcon_password"] @@ -1133,6 +1177,142 @@ def process_bug_log(player: str, description: str, config): config["rcon_host"], config["rcon_port"], config["rcon_password"] ) +# --------------------------------------------------------------------------- +# Training Audit Log — structured JSONL for dataset expansion +# --------------------------------------------------------------------------- + +_audit_lock = threading.Lock() + +def _training_audit_path(config) -> str: + return config.get("training_audit_path", "/var/log/mc_training_audit.jsonl") + + +def write_training_audit(player: str, mode: str, user_message: str, + commands_generated: list, commands_executed: list, + message: str, context: dict, config: dict, + rcon_results: list = None): + """ + Write a structured training example to the audit JSONL. + Every pray/sudo interaction becomes a candidate training pair. + """ + audit_path = _training_audit_path(config) + server_ctx = { + "server_type": config.get("server_type", "paper"), + "version": "1.21.x", + "online_players": context.get("online_players", []), + } + # Add player position if available + try: + pos = get_player_xyz(player, config) + if pos: + server_ctx["player_position"] = {"x": pos[0], "y": pos[1], "z": pos[2]} + except Exception: + pass + + admin_user = config.get("sudo_user", "slingshooter08") + + entry = { + "timestamp": datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'), + "source": "live_playtest", + "category": _infer_category(mode, user_message, commands_executed), + "mode": mode, + "player": player, + "player_is_admin": player == admin_user, + "input": { + "user_message": user_message, + "server_context": server_ctx, + }, + "output": { + "commands_generated": commands_generated or [], + "commands_executed": commands_executed or [], + "message": message or "", + }, + "rcon_results": rcon_results or [], + "needs_review": True, + } + + try: + parent = os.path.dirname(audit_path) + if parent: + os.makedirs(parent, exist_ok=True) + with _audit_lock: + with open(audit_path, 'a', encoding='utf-8') as f: + f.write(json.dumps(entry, ensure_ascii=False) + '\n') + except Exception as e: + log.error(f"Training audit write failed: {e}") + + +def write_training_feedback(player: str, description: str, config: dict): + """ + Write a bug_log feedback entry that links to the player's last interaction. + Feedback from non-admin players is tagged as unverified — they may have + wrong expectations about what should have happened. + """ + audit_path = _training_audit_path(config) + admin_user = config.get("sudo_user", "slingshooter08") + is_admin = player == admin_user + + # Pull the last sudo/prayer context for this player + last_sudo = get_last_sudo_feedback(player) + last_prayer = None + with _memory_lock: + for mem in reversed(prayer_memory): + if mem.get("player") == player: + last_prayer = mem + break + + entry = { + "timestamp": datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'), + "source": "player_feedback", + "type": "bug_report", + "player": player, + "player_is_admin": is_admin, + "trust_level": "verified" if is_admin else "unverified", + "description": description, + "last_sudo_context": { + "prompt": last_sudo.get("prompt", ""), + "results": last_sudo.get("results", []), + "ineffective": last_sudo.get("ineffective", False), + } if last_sudo else None, + "last_prayer_context": { + "prayer": last_prayer.get("prayer", ""), + "god_message": last_prayer.get("god_message", ""), + } if last_prayer else None, + "needs_review": True, + "reviewer_notes": "" if is_admin else "UNVERIFIED: player may not understand expected behavior", + } + + try: + parent = os.path.dirname(audit_path) + if parent: + os.makedirs(parent, exist_ok=True) + with _audit_lock: + with open(audit_path, 'a', encoding='utf-8') as f: + f.write(json.dumps(entry, ensure_ascii=False) + '\n') + except Exception as e: + log.error(f"Training feedback write failed: {e}") + + +def _infer_category(mode: str, user_message: str, commands_executed: list) -> str: + """Infer dataset category from the interaction.""" + low = (user_message or "").lower() + if mode == "god_system": + return "negative" + if not commands_executed: + # No commands = either info query, safety refusal, or empty response + q_words = ("what ", "how ", "why ", "explain ", "wiki ", "lookup ") + if any(low.startswith(w) for w in q_words) or low.endswith("?"): + return "info" + return "safety" + if mode == "god": + return "command_gen" + # sudo mode + troubleshoot_words = ("lag", "can't", "broken", "not working", "won't", "error", "crash", "stuck") + if any(w in low for w in troubleshoot_words): + return "troubleshoot" + return "command_gen" + + # --------------------------------------------------------------------------- # RCON # --------------------------------------------------------------------------- @@ -2271,10 +2451,12 @@ def fix_effect_command(cmd: str) -> str: - effect -> effect give """ - m = re.match(r'^effect\s+(\w+)\s+(minecraft:\w+)\s+(\d+)\s+(\d+)$', cmd) + m = re.match(r'^effect\s+(\S+)\s+(minecraft:\w+)\s+(\d+)\s+(\d+)(?:\s+(true|false))?$', cmd) if m: - player, eff, dur, amp = m.groups() + player, eff, dur, amp, hide = m.groups() fixed = f"effect give {player} {eff} {dur} {amp}" + if hide in ("true", "false"): + fixed += f" {hide}" log.warning(f"Fixed malformed effect: '{cmd}' -> '{fixed}'") return fixed return cmd @@ -2425,6 +2607,21 @@ def validate_command(cmd, online_players, fallback_player, config=None): log.warning(f"Command blocked (unknown prefix for server_type={config.get('server_type', DEFAULT_SERVER_TYPE) if config else DEFAULT_SERVER_TYPE}): {resolved}") return resolved, False + if resolved.startswith("tellraw "): + m = re.match(r'^tellraw\s+\S+\s+(.+)$', resolved) + if not m: + log.warning(f"Command blocked (malformed tellraw): {resolved}") + return resolved, False + payload = m.group(1).strip() + if not (payload.startswith("{") or payload.startswith("[")): + log.warning(f"Command blocked (tellraw payload not json): {resolved}") + return resolved, False + try: + json.loads(payload) + except Exception: + log.warning(f"Command blocked (invalid tellraw json): {resolved}") + return resolved, False + # Prevent execute-wrapper bypass (e.g. execute ... run gameMode s) if resolved.startswith("execute "): tail = resolved @@ -2459,6 +2656,8 @@ def _is_destructive_intent(prompt: str) -> bool: 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")) @@ -2734,6 +2933,11 @@ def execute_response(response, context, config, praying_player=None): if not is_safe: continue + # Prevent unsolicited teleporting in unprompted interventions. + if praying_player is None and re.search(r'\btp\b', resolved): + log.warning(f"Blocked tp in unprompted intervention: {resolved}") + 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) @@ -2892,7 +3096,8 @@ def process_sudo(player, prompt, config): return sudo_user = config.get("sudo_user", "slingshooter08") - if player != sudo_user: + allow_all = config.get("sudo_allow_all_players", False) + if player != sudo_user and not allow_all: # Keep this private and quiet rcon( f'tellraw {player} {{"text":"[SUDO] Unauthorized.","color":"red"}}', @@ -2941,6 +3146,19 @@ def process_sudo(player, prompt, config): # Deterministic lookup mode: information only, no command execution. low = prompt.lower().strip() + + # Deterministic status-check shortcut for common follow-up wording. + if re.search(r'\bdid that command do what i asked\b', low): + fb = get_last_sudo_feedback(player) + if not fb: + _send_private(player, "[SUDO-LOOKUP] No recent sudo execution context to evaluate.", config, "yellow") + return + if bool(fb.get("ineffective", False)): + _send_private(player, "[SUDO-LOOKUP] Likely no — recent execution looked ineffective.", config, "yellow") + else: + _send_private(player, "[SUDO-LOOKUP] Likely yes — recent execution looked successful.", config, "green") + return + lookup_prefixes = ("lookup ", "search ", "wiki ", "explain ", "what is ", "how do i ", "how to ") if low.startswith(lookup_prefixes) or _looks_like_lookup_question(prompt): query = prompt.strip() @@ -3130,14 +3348,15 @@ def process_sudo(player, prompt, config): text=prompt, context_payload={ "request": prompt, + "player": player, "online_players": online, "sudo_history": get_sudo_history_block(), "sudo_failures": get_sudo_failures_block(player), "mode": "sudo", }, config=config, - allow_tools=bool(config.get("gateway_allow_tools_sudo", False)), - max_tool_steps=int(config.get("gateway_max_tool_steps", 2)), + allow_tools=bool(config.get("gateway_allow_tools_sudo", True)), + max_tool_steps=int(config.get("gateway_max_tool_steps", 4)), ) log.info(f"Gateway sudo tool_trace={out.get('tool_trace', [])}") commands = out.get("commands") or [] @@ -3231,8 +3450,10 @@ def process_sudo(player, prompt, config): if not is_safe: continue log.info(f"SUDO execute: {resolved}") + _sudo_trace(player, f"[SUDO TRY] {resolved}", 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) executed.append(resolved) results_seen.append((resolved, str(result or ""))) time.sleep(0.2) @@ -3251,8 +3472,10 @@ def process_sudo(player, prompt, config): if not is_safe: continue log.info(f"SUDO retry execute: {resolved}") + _sudo_trace(player, f"[SUDO RETRY] {resolved}", config, color="yellow") result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"]) log.info(f"SUDO retry result: {result!r}") + _sudo_trace(player, f"[SUDO RETRY RES] {str(result or '')[:180]}", config, color="yellow") executed.append(resolved) results_seen.append((resolved, str(result or ""))) time.sleep(0.12) @@ -3270,8 +3493,10 @@ def process_sudo(player, prompt, config): if not is_safe: continue log.info(f"SUDO fallback execute: {resolved}") + _sudo_trace(player, f"[SUDO FALLBACK] {resolved}", config, color="red") result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"]) log.info(f"SUDO fallback result: {result!r}") + _sudo_trace(player, f"[SUDO FALLBACK RES] {str(result or '')[:180]}", config, color="red") executed.append(resolved) results_seen.append((resolved, str(result or ""))) time.sleep(0.15) @@ -3287,8 +3512,10 @@ def process_sudo(player, prompt, config): if not is_safe: continue log.info(f"SUDO fire fallback execute: {resolved}") + _sudo_trace(player, f"[SUDO FIRE FALLBACK] {resolved}", config, color="gold") result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"]) log.info(f"SUDO fire fallback result: {result!r}") + _sudo_trace(player, f"[SUDO FIRE RES] {str(result or '')[:180]}", config, color="gold") executed.append(resolved) results_seen.append((resolved, str(result or ""))) time.sleep(0.15) @@ -3304,6 +3531,19 @@ def process_sudo(player, prompt, config): add_sudo_history(player, prompt, commands, executed) + # Training audit: log the full sudo interaction + write_training_audit( + player=player, + mode="sudo", + user_message=f"sudo {prompt}", + commands_generated=commands, + commands_executed=executed, + message="", + context={"online_players": online}, + config=config, + rcon_results=[(cmd, res) for cmd, res in results_seen], + ) + def get_player_xyz(player: str, config): """Return integer xyz for a player using RCON entity data.""" @@ -3511,6 +3751,18 @@ def process_prayer(player, prayer, config, cooldowns): if god_msg: add_prayer_memory(player, prayer, god_msg, config) + # Training audit: log the full interaction as a candidate training pair + write_training_audit( + player=player, + mode="god", + user_message=f"pray {prayer}", + commands_generated=response.get("commands") or [], + commands_executed=response.get("commands") or [], # prayer path doesn't track executed separately + message=god_msg, + context=context, + config=config, + ) + # --------------------------------------------------------------------------- # Divine intervention timer # ---------------------------------------------------------------------------