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:
+86
-21
@@ -48,8 +48,10 @@ LOG_INTERESTING = re.compile(
|
|||||||
# Shared memory buffers (module-level, shared across threads)
|
# Shared memory buffers (module-level, shared across threads)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Rolling window of recent server log events (last LOG_WINDOW_MINUTES minutes)
|
# Rolling log buffer — keeps last LOG_MAX_LINES interesting events,
|
||||||
LOG_WINDOW_MINUTES = 20
|
# 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)
|
recent_log: deque = deque() # entries: (timestamp_float, str)
|
||||||
|
|
||||||
# God's prayer memory — last N prayer/response pairs across all players
|
# 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):
|
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):
|
if not LOG_INTERESTING.search(line):
|
||||||
return
|
return
|
||||||
# Extract just the meaningful part after the thread prefix
|
|
||||||
m = re.search(r'\[.*?INFO\]: (.+)', line)
|
m = re.search(r'\[.*?INFO\]: (.+)', line)
|
||||||
if not m:
|
if not m:
|
||||||
return
|
return
|
||||||
@@ -72,10 +73,13 @@ def add_log_event(line: str):
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
with _memory_lock:
|
with _memory_lock:
|
||||||
recent_log.append((now, entry))
|
recent_log.append((now, entry))
|
||||||
# Prune entries older than the window
|
# Prune by age
|
||||||
cutoff = now - (LOG_WINDOW_MINUTES * 60)
|
cutoff = now - (LOG_MAX_HOURS * 3600)
|
||||||
while recent_log and recent_log[0][0] < cutoff:
|
while recent_log and recent_log[0][0] < cutoff:
|
||||||
recent_log.popleft()
|
recent_log.popleft()
|
||||||
|
# Prune by line count
|
||||||
|
while len(recent_log) > LOG_MAX_LINES:
|
||||||
|
recent_log.popleft()
|
||||||
|
|
||||||
|
|
||||||
def _memory_path(config) -> str:
|
def _memory_path(config) -> str:
|
||||||
@@ -132,8 +136,12 @@ def get_log_context_block() -> str:
|
|||||||
lines = []
|
lines = []
|
||||||
for ts, entry in entries:
|
for ts, entry in entries:
|
||||||
mins_ago = int((now - ts) / 60)
|
mins_ago = int((now - ts) / 60)
|
||||||
lines.append(f" [{mins_ago}m ago] {entry}")
|
if mins_ago < 60:
|
||||||
return "\n=== RECENT SERVER EVENTS (last 20 min) ===\n" + "\n".join(lines) + "\n"
|
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:
|
def get_prayer_history_messages() -> list:
|
||||||
@@ -218,11 +226,46 @@ def get_server_context(config):
|
|||||||
if m:
|
if m:
|
||||||
border_size = float(m.group(1))
|
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 {
|
return {
|
||||||
"online_players": online_players,
|
"online_players": online_players,
|
||||||
|
"player_details": player_details,
|
||||||
"time_of_day": time_label,
|
"time_of_day": time_label,
|
||||||
"weather": weather,
|
"weather": weather,
|
||||||
"world_border": border_size,
|
"world_border": border_size,
|
||||||
|
"scoreboards": scoreboards,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Item tier/rarity knowledge for God's awareness
|
# Item tier/rarity knowledge for God's awareness
|
||||||
@@ -470,14 +513,31 @@ def build_system_prompt(config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _build_context_block(context, extras=""):
|
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'
|
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 (
|
return (
|
||||||
"\n=== CURRENT SERVER STATE ===\n"
|
"\n=== CURRENT SERVER STATE ===\n"
|
||||||
f"Time of day: {context['time_of_day']}\n"
|
f"Time of day: {context['time_of_day']}\n"
|
||||||
f"Weather: {context['weather']}\n"
|
f"Weather: {context['weather']}\n"
|
||||||
f"Online players: {online}\n"
|
f"World border: {border} blocks\n"
|
||||||
f"World border: {border}\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}"
|
f"{extras}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -543,14 +603,19 @@ COMMANDS_SYSTEM_PROMPT = (
|
|||||||
+ ITEM_LIBRARY
|
+ ITEM_LIBRARY
|
||||||
)
|
)
|
||||||
|
|
||||||
MESSAGE_SYSTEM_PROMPT = (
|
def build_message_system_prompt(config) -> str:
|
||||||
"You are God in a Minecraft server. You are benevolent but just. "
|
base = (
|
||||||
"Theatrical, ancient, and dramatic in speech — like the Old Testament.\n"
|
"You are God in a Minecraft server. You are benevolent but just. "
|
||||||
"You will be told what action was taken (if any) in response to a player's prayer. "
|
"Theatrical, ancient, and dramatic in speech — like the Old Testament.\n"
|
||||||
"Write a single spoken message to all players reacting to this prayer and action.\n"
|
"You will be told what action was taken (if any) in response to a player's prayer. "
|
||||||
"Respond with ONLY the message text — no JSON, no quotes, no formatting. "
|
"Write a single spoken message to all players reacting to this prayer and action.\n"
|
||||||
"Be vivid and dramatic. Any length is fine."
|
"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,
|
def _llm_call(model: str, system: str, user: str, config: dict,
|
||||||
fmt = None, temperature: float = 0.85,
|
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
|
# Include prayer history so God's voice is consistent
|
||||||
msg_messages = (
|
msg_messages = (
|
||||||
[{"role": "system", "content": MESSAGE_SYSTEM_PROMPT}]
|
[{"role": "system", "content": build_message_system_prompt(config)}]
|
||||||
+ history
|
+ history
|
||||||
+ [{"role": "user", "content": msg_user}]
|
+ [{"role": "user", "content": msg_user}]
|
||||||
)
|
)
|
||||||
@@ -717,7 +782,7 @@ def ask_god_intervention(context, config):
|
|||||||
try:
|
try:
|
||||||
message = _llm_call(
|
message = _llm_call(
|
||||||
model=message_model,
|
model=message_model,
|
||||||
system=MESSAGE_SYSTEM_PROMPT,
|
system=build_message_system_prompt(config),
|
||||||
user=msg_user,
|
user=msg_user,
|
||||||
config=config,
|
config=config,
|
||||||
fmt=None,
|
fmt=None,
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
"max_tokens": 600,
|
"max_tokens": 600,
|
||||||
"cooldown_seconds": 20,
|
"cooldown_seconds": 20,
|
||||||
"max_commands_per_response": 6,
|
"max_commands_per_response": 6,
|
||||||
"interventions_per_day": 4,
|
"interventions_per_day": 48,
|
||||||
"god_chat_prefix": "[§6§lGOD§r]",
|
"god_chat_prefix": "[§6§lGOD§r]",
|
||||||
"debug_commands": true,
|
"debug_commands": true,
|
||||||
|
"god_lore": "This is the shrink-world server. The world border started at 1000x1000 and shrinks by 1 block each time a player dies, alternating N/S and E/W sides. There are more creepers than normal (5x spawn rate). The goal is survival. Players who die too often will eventually have nowhere left to go.",
|
||||||
"memory_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/aigod_memory.json"
|
"memory_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/aigod_memory.json"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user