Add bug_log intake and sharpen divine voice

This commit is contained in:
Claude Code
2026-03-17 14:53:54 -04:00
parent e73fa81b8c
commit 29c43e2d30
4 changed files with 167 additions and 13 deletions
+143 -13
View File
@@ -6,7 +6,7 @@ validates targets, and executes commands via RCON.
Config: /etc/mc_aigod.json
"""
import json, random, re, socket, struct, threading, time, logging
import json, os, random, re, socket, struct, threading, time, logging
from collections import deque
from datetime import datetime
import requests
@@ -35,6 +35,10 @@ SUDO_PATTERNS = [
re.compile(r'\[.*?\]: <(\w+)> [Ss]udo (.+)'),
]
BUG_LOG_PATTERNS = [
re.compile(r'\[.*?\]: <(\w+)> [Bb]ug[_ ]?[Ll]og(?:\s+(.+))?\s*$'),
]
JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game')
LEAVE_PATTERN = re.compile(r'\[.*?\]: (\w+) left the game')
@@ -244,6 +248,78 @@ def get_sudo_history_block() -> str:
)
return "\n=== LAST 10 SUDO ACTIONS ===\n" + "\n".join(lines) + "\n"
def _bug_log_path(config) -> str:
return config.get("bug_log_path", "/var/log/mc_aigod_bug.log")
def _read_recent_raw_log_lines(log_path: str, max_lines: int) -> list:
lines = deque(maxlen=max_lines)
try:
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
lines.append(line.rstrip('\n'))
except Exception as e:
return [f"<unable to read raw server log: {e}>"]
return list(lines)
def _format_recent_event_lines(max_events: int) -> list:
with _memory_lock:
entries = list(recent_log)[-max_events:]
out = []
for ts, entry in entries:
stamp = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
out.append(f"[{stamp}] {entry}")
return out
def process_bug_log(player: str, description: str, config):
desc = (description or "").strip()
if not desc:
desc = "(no description provided)"
event_count = int(config.get("bug_log_event_lines", 40))
raw_count = int(config.get("bug_log_raw_lines", 120))
recent_events = _format_recent_event_lines(event_count)
raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count)
bug_path = _bug_log_path(config)
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
try:
parent = os.path.dirname(bug_path)
if parent:
os.makedirs(parent, exist_ok=True)
with open(bug_path, 'a', encoding='utf-8') as bf:
bf.write("\n" + "=" * 80 + "\n")
bf.write(f"BUG LOG ENTRY: {timestamp}\n")
bf.write(f"Player: {player}\n")
bf.write(f"Description: {desc}\n")
bf.write("\n-- RECENT INTERESTING EVENTS --\n")
if recent_events:
bf.write("\n".join(recent_events) + "\n")
else:
bf.write("(no recent in-memory events captured)\n")
bf.write("\n-- RECENT RAW SERVER LOG LINES --\n")
if raw_lines:
bf.write("\n".join(raw_lines) + "\n")
else:
bf.write("(no raw lines available)\n")
log.info(f"BUG_LOG recorded by {player}: {desc} -> {bug_path}")
rcon(
f'tellraw {player} {{"text":"[BUG_LOG] Logged. Thank you.","color":"green"}}',
config["rcon_host"], config["rcon_port"], config["rcon_password"]
)
except Exception as e:
log.error(f"BUG_LOG write failed for {player}: {e}", exc_info=True)
rcon(
f'tellraw {player} {{"text":"[BUG_LOG] Failed to write bug log.","color":"red"}}',
config["rcon_host"], config["rcon_port"], config["rcon_password"]
)
# ---------------------------------------------------------------------------
# RCON
# ---------------------------------------------------------------------------
@@ -610,7 +686,7 @@ POTIONS: potion (requires component syntax for type — prefer effect give i
def build_system_prompt(config):
return (
f"You are God in a Minecraft server called {config['server_name']}.\n"
"You are benevolent but just. Theatrical, ancient, and dramatic in speech.\n"
"You are benevolent but just. Theatrical, ancient, dramatic, and darkly funny in speech.\n"
"You answer every prayer with a message. You pass judgement on players when they pray.\n\n"
"Respond ONLY with a valid JSON object — no markdown, no explanation, nothing else:\n"
'{\n'
@@ -618,7 +694,7 @@ def build_system_prompt(config):
' "commands": ["command1", "command2"]\n'
'}\n\n'
"Rules:\n"
"- message: always present and non-empty. Speak as God. Be dramatic and biblical. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n"
"- message: always present and non-empty. Speak as God. Be dramatic, biblical, and ironic with dry humor. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n"
"- commands: list of server commands WITHOUT a leading slash. May be empty [] ONLY if you are deliberately granting nothing.\n"
"- CRITICAL: If your message says you will give something, grant something, or fulfil a request, you MUST include the corresponding command in the commands array. Words alone do nothing. The commands array is the ONLY way anything happens in the world. If commands is empty, nothing happens regardless of what your message says.\n"
"- {player} is the player who prayed. You may also use any other online player's literal username as a target.\n"
@@ -859,11 +935,11 @@ FIRST_LOGIN_BENEVOLENCE_PROMPT = (
def build_message_system_prompt(config) -> str:
base = (
"You are God in a Minecraft server. You are benevolent but just. "
"Theatrical, ancient, and dramatic in speech — like the Old Testament.\n"
"Theatrical, ancient, dramatic, and laced with dry irony — like the Old Testament with a sharper wit.\n"
"You will be told what action was taken (if any) in response to a player's prayer. "
"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. Any length is fine.\n"
"Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n"
)
lore = config.get("god_lore", "")
if lore:
@@ -1504,6 +1580,7 @@ BIBLE_LINES = [
("God watches over this server.", "yellow", False),
("Speak to him by typing in chat:", "white", False),
(" pray <your message>", "green", True),
(" bug_log <optional description>", "aqua", True),
("", "white", False),
("God is benevolent, but just.", "yellow", False),
("He hears every prayer — but answers as he sees fit.", "white", False),
@@ -1513,6 +1590,7 @@ BIBLE_LINES = [
(" pray Lord, bless my journey through the mines.", "gray", False),
(" pray Smite my enemy, for they have wronged me.", "gray", False),
(" pray Forgive me, I have sinned against thy creations.", "gray", False),
(" bug_log creeper explosion desynced and killed me", "dark_aqua", False),
("", "white", False),
("Thou may only pray once every 20 seconds.", "red", False),
("Type \"bible\" in chat to see this again.", "gray", False),
@@ -1647,14 +1725,48 @@ def divine_intervention_loop(config):
# ---------------------------------------------------------------------------
def tail_log(log_path):
with open(log_path, 'r') as f:
f.seek(0, 2)
while True:
line = f.readline()
if line:
yield line
else:
time.sleep(0.2)
"""
Follow a log file across truncation/recreation.
MCSManager/server restarts can replace latest.log with a new inode.
A plain open()+readline() tail will then silently watch the old file forever.
"""
f = None
inode = None
while True:
try:
st = os.stat(log_path)
except FileNotFoundError:
# Server may not have created logs yet.
time.sleep(0.5)
continue
# Reopen when file first appears or inode changes (log rotation/recreate).
if f is None or inode != st.st_ino:
if f:
try:
f.close()
except Exception:
pass
f = open(log_path, 'r')
f.seek(0, 2)
inode = st.st_ino
log.info(f"Now following log file: {log_path} (inode={inode})")
# If file was truncated, jump to end again.
try:
if f.tell() > st.st_size:
f.seek(0, 2)
log.info("Log file truncated; seeking to end.")
except Exception:
pass
line = f.readline()
if line:
yield line
else:
time.sleep(0.2)
# ---------------------------------------------------------------------------
# Main
@@ -1681,6 +1793,24 @@ def main():
# Feed every line into the rolling log buffer
add_log_event(line)
# bug logging trigger
matched = False
for pat in BUG_LOG_PATTERNS:
m = pat.search(line)
if m:
player = m.group(1)
description = (m.group(2) or "").strip()
log.info(f"BUG_LOG from {player}: {description or '(no description)'}")
try:
process_bug_log(player, description, config)
except Exception as e:
log.error(f"Error processing bug_log: {e}", exc_info=True)
matched = True
break
if matched:
continue
# sudo translator
matched = False
for pat in SUDO_PATTERNS: