Expand world context + two-call LLM split

- Replace time-based log window with line-based (200 lines, 3hr cap)
- Add per-player position and death count to server context
- Add server scoreboards (total deaths, shrink enabled, border parity)
- Add god_lore config key injected into message system prompt
- Two-call split: qwen3-coder:30b (commands) + gemma3:12b (messages)
- interventions_per_day bumped to 48 (avg every 30min)
- cooldown_seconds reduced to 20
This commit is contained in:
2026-03-15 19:42:31 -04:00
parent 8ee8be9cc0
commit c94fa1872a
2 changed files with 88 additions and 22 deletions
+86 -21
View File
@@ -48,8 +48,10 @@ LOG_INTERESTING = re.compile(
# Shared memory buffers (module-level, shared across threads)
# ---------------------------------------------------------------------------
# Rolling window of recent server log events (last LOG_WINDOW_MINUTES minutes)
LOG_WINDOW_MINUTES = 20
# Rolling log buffer — keeps last LOG_MAX_LINES interesting events,
# pruning anything older than LOG_MAX_HOURS. Whichever limit hits first.
LOG_MAX_LINES = 200
LOG_MAX_HOURS = 3
recent_log: deque = deque() # entries: (timestamp_float, str)
# God's prayer memory — last N prayer/response pairs across all players
@@ -61,10 +63,9 @@ _memory_lock = threading.Lock()
def add_log_event(line: str):
"""Add a meaningful log line to the rolling buffer, pruning old entries."""
"""Add a meaningful log line to the rolling buffer."""
if not LOG_INTERESTING.search(line):
return
# Extract just the meaningful part after the thread prefix
m = re.search(r'\[.*?INFO\]: (.+)', line)
if not m:
return
@@ -72,10 +73,13 @@ def add_log_event(line: str):
now = time.time()
with _memory_lock:
recent_log.append((now, entry))
# Prune entries older than the window
cutoff = now - (LOG_WINDOW_MINUTES * 60)
# Prune by age
cutoff = now - (LOG_MAX_HOURS * 3600)
while recent_log and recent_log[0][0] < cutoff:
recent_log.popleft()
# Prune by line count
while len(recent_log) > LOG_MAX_LINES:
recent_log.popleft()
def _memory_path(config) -> str:
@@ -132,8 +136,12 @@ def get_log_context_block() -> str:
lines = []
for ts, entry in entries:
mins_ago = int((now - ts) / 60)
lines.append(f" [{mins_ago}m ago] {entry}")
return "\n=== RECENT SERVER EVENTS (last 20 min) ===\n" + "\n".join(lines) + "\n"
if mins_ago < 60:
time_label = f"{mins_ago}m ago"
else:
time_label = f"{mins_ago // 60}h {mins_ago % 60}m ago"
lines.append(f" [{time_label}] {entry}")
return f"\n=== RECENT SERVER EVENTS (last {len(lines)} events, up to {LOG_MAX_HOURS}h) ===\n" + "\n".join(lines) + "\n"
def get_prayer_history_messages() -> list:
@@ -218,11 +226,46 @@ def get_server_context(config):
if m:
border_size = float(m.group(1))
# Per-player: position, death count
player_details = {}
for player in online_players:
details = {}
# Position
pos_raw = q(f"data get entity {player} Pos")
pos_m = re.findall(r'(-?[\d.]+)d', pos_raw)
if pos_m and len(pos_m) >= 3:
x, y, z = float(pos_m[0]), float(pos_m[1]), float(pos_m[2])
dist = int((x**2 + z**2) ** 0.5)
details["pos"] = f"x={int(x)}, y={int(y)}, z={int(z)} ({dist} blocks from spawn)"
# Deaths
deaths_raw = q(f"scoreboard players get {player} player_deaths")
deaths_m = re.search(r'has\s+(\d+)', deaths_raw)
if deaths_m:
details["deaths"] = int(deaths_m.group(1))
player_details[player] = details
# Server-wide scoreboards
total_deaths_raw = q("scoreboard players get $deaths_total Total Deaths")
shrink_enabled_raw = q("scoreboard players get $shrink_enabled shrink_enabled")
border_parity_raw = q("scoreboard players get $border_parity border_parity")
total_deaths_m = re.search(r'has\s+(\d+)', total_deaths_raw)
shrink_enabled_m = re.search(r'has\s+(\d+)', shrink_enabled_raw)
border_parity_m = re.search(r'has\s+(\d+)', border_parity_raw)
scoreboards = {
"total_deaths": total_deaths_m.group(1) if total_deaths_m else "unknown",
"shrink_enabled": shrink_enabled_m.group(1) if shrink_enabled_m else "unknown",
"border_parity": ("N/S" if border_parity_m and border_parity_m.group(1) == "0" else "E/W") if border_parity_m else "unknown",
}
return {
"online_players": online_players,
"player_details": player_details,
"time_of_day": time_label,
"weather": weather,
"world_border": border_size,
"scoreboards": scoreboards,
}
# Item tier/rarity knowledge for God's awareness
@@ -470,14 +513,31 @@ def build_system_prompt(config):
)
def _build_context_block(context, extras=""):
online = ', '.join(context['online_players']) or 'none'
border = str(context['world_border']) if context['world_border'] else 'N/A'
scoreboards = context.get("scoreboards", {})
shrink = scoreboards.get("shrink_enabled", "unknown")
parity = scoreboards.get("border_parity", "unknown")
total_deaths = scoreboards.get("total_deaths", "unknown")
# Per-player summary
player_details = context.get("player_details", {})
player_lines = []
for player in context['online_players']:
d = player_details.get(player, {})
pos = d.get("pos", "unknown")
deaths = d.get("deaths", "?")
player_lines.append(f" {player}: pos={pos}, deaths={deaths}")
players_block = "\n".join(player_lines) if player_lines else " none"
return (
"\n=== CURRENT SERVER STATE ===\n"
f"Time of day: {context['time_of_day']}\n"
f"Weather: {context['weather']}\n"
f"Online players: {online}\n"
f"World border: {border}\n"
f"World border: {border} blocks\n"
f"Border shrinking: {'yes' if shrink == '1' else 'no' if shrink == '0' else shrink}\n"
f"Next shrink direction: {parity}\n"
f"Total deaths (all players, all time): {total_deaths}\n"
f"Online players:\n{players_block}\n"
f"{extras}"
)
@@ -543,14 +603,19 @@ COMMANDS_SYSTEM_PROMPT = (
+ ITEM_LIBRARY
)
MESSAGE_SYSTEM_PROMPT = (
"You are God in a Minecraft server. You are benevolent but just. "
"Theatrical, ancient, and dramatic in speech — like the Old Testament.\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."
)
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"
"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"
)
lore = config.get("god_lore", "")
if lore:
base += f"\n=== SERVER LORE ===\n{lore}\n"
return base
def _llm_call(model: str, system: str, user: str, config: dict,
fmt = None, temperature: float = 0.85,
@@ -637,7 +702,7 @@ def ask_god(player, prayer, context, config):
# Include prayer history so God's voice is consistent
msg_messages = (
[{"role": "system", "content": MESSAGE_SYSTEM_PROMPT}]
[{"role": "system", "content": build_message_system_prompt(config)}]
+ history
+ [{"role": "user", "content": msg_user}]
)
@@ -717,7 +782,7 @@ def ask_god_intervention(context, config):
try:
message = _llm_call(
model=message_model,
system=MESSAGE_SYSTEM_PROMPT,
system=build_message_system_prompt(config),
user=msg_user,
config=config,
fmt=None,