22-tool architecture: log.query, user.ask, journal system deployed
New tools implemented and deployed to dev gateway: - log.query: focused event queries (chat/deaths/joins/actions), replaces 200-line dump - user.ask: risk-scaled clarifying questions, async with tellraw - journal.read/write: per-player files, cross-mode (God+Sudo share) All wired into langgraph_gateway.py _execute_tool and model-driven tool loop. Tool schemas updated (22 total). Deployed to CT 644 dev server. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
|||||||
|
"""
|
||||||
|
Log Query — focused queries against the server event log.
|
||||||
|
|
||||||
|
Replaces the 200-line log dump with specific, targeted queries.
|
||||||
|
Reads from the existing recent_log buffer in mc_aigod.
|
||||||
|
|
||||||
|
Query types:
|
||||||
|
chat — recent chat messages (optionally filtered by player)
|
||||||
|
deaths — recent death events
|
||||||
|
joins — recent join/leave events
|
||||||
|
actions — recent commands/interactions
|
||||||
|
all — recent events of any type
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from agent.tools.log_query import handle_log_query
|
||||||
|
|
||||||
|
result = handle_log_query(recent_log_buffer, {
|
||||||
|
"type": "chat",
|
||||||
|
"player": "TheBigBoss",
|
||||||
|
"limit": 5,
|
||||||
|
})
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
# Patterns for classifying log events
|
||||||
|
CHAT_PATTERN = re.compile(r'<(\w+)>\s*(.+)')
|
||||||
|
DEATH_PATTERNS = [
|
||||||
|
re.compile(r'(\w+) (fell from a high place|hit the ground too hard|was slain by \w+|was shot by \w+|drowned|tried to swim in lava|burned to death|went up in flames|blew up|was blown up by \w+|suffocated|starved to death|was killed by \w+|was pricked to death|withered away|fell out of the world)'),
|
||||||
|
]
|
||||||
|
JOIN_PATTERN = re.compile(r'(\w+) joined the game')
|
||||||
|
LEAVE_PATTERN = re.compile(r'(\w+) left the game')
|
||||||
|
ADVANCEMENT_PATTERN = re.compile(r'(\w+) has made the advancement \[(.+?)\]')
|
||||||
|
COMMAND_PATTERN = re.compile(r'(\w+) issued server command: /(.+)')
|
||||||
|
|
||||||
|
|
||||||
|
def classify_event(text: str) -> tuple:
|
||||||
|
"""Classify a log line into (type, player, detail)."""
|
||||||
|
# Strip color codes and log prefix
|
||||||
|
clean = re.sub(r'\xa7.', '', text)
|
||||||
|
# Strip timestamp/thread prefix
|
||||||
|
m = re.search(r'INFO\]: (.+)$', clean)
|
||||||
|
if m:
|
||||||
|
clean = m.group(1).strip()
|
||||||
|
|
||||||
|
# Chat
|
||||||
|
cm = CHAT_PATTERN.match(clean)
|
||||||
|
if cm:
|
||||||
|
return ("chat", cm.group(1), cm.group(2))
|
||||||
|
|
||||||
|
# Deaths
|
||||||
|
for dp in DEATH_PATTERNS:
|
||||||
|
dm = dp.search(clean)
|
||||||
|
if dm:
|
||||||
|
return ("death", dm.group(1), dm.group(0))
|
||||||
|
|
||||||
|
# Joins
|
||||||
|
jm = JOIN_PATTERN.search(clean)
|
||||||
|
if jm:
|
||||||
|
return ("join", jm.group(1), f"{jm.group(1)} joined")
|
||||||
|
|
||||||
|
# Leaves
|
||||||
|
lm = LEAVE_PATTERN.search(clean)
|
||||||
|
if lm:
|
||||||
|
return ("leave", lm.group(1), f"{lm.group(1)} left")
|
||||||
|
|
||||||
|
# Advancements
|
||||||
|
am = ADVANCEMENT_PATTERN.search(clean)
|
||||||
|
if am:
|
||||||
|
return ("advancement", am.group(1), f"{am.group(1)} earned [{am.group(2)}]")
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
com = COMMAND_PATTERN.search(clean)
|
||||||
|
if com:
|
||||||
|
return ("command", com.group(1), f"{com.group(1)}: /{com.group(2)}")
|
||||||
|
|
||||||
|
return ("other", "", clean)
|
||||||
|
|
||||||
|
|
||||||
|
def query_log(recent_log: list, query_type: str = "all",
|
||||||
|
player: str = None, limit: int = 5) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Query the log buffer for specific events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recent_log: list of (timestamp_float, log_line_str) tuples
|
||||||
|
query_type: chat, deaths, joins, actions, all
|
||||||
|
player: optional player name filter
|
||||||
|
limit: max results (default 5)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{ok, results: [{type, player, detail, age_seconds}], count}
|
||||||
|
"""
|
||||||
|
type_map = {
|
||||||
|
"chat": {"chat"},
|
||||||
|
"deaths": {"death"},
|
||||||
|
"joins": {"join", "leave"},
|
||||||
|
"actions": {"command", "advancement"},
|
||||||
|
"all": {"chat", "death", "join", "leave", "command", "advancement", "other"},
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed_types = type_map.get(query_type, type_map["all"])
|
||||||
|
results = []
|
||||||
|
|
||||||
|
import time
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
# Iterate newest first
|
||||||
|
for entry in reversed(list(recent_log)):
|
||||||
|
if isinstance(entry, tuple) and len(entry) == 2:
|
||||||
|
ts, line = entry
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_type, event_player, detail = classify_event(line)
|
||||||
|
|
||||||
|
if event_type not in allowed_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if player and event_player.lower() != player.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
age = int(now - ts)
|
||||||
|
age_str = f"{age}s ago" if age < 60 else f"{age//60}m ago" if age < 3600 else f"{age//3600}h ago"
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"type": event_type,
|
||||||
|
"player": event_player,
|
||||||
|
"detail": detail,
|
||||||
|
"age": age_str,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(results) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"results": results,
|
||||||
|
"count": len(results),
|
||||||
|
"query": {"type": query_type, "player": player, "limit": limit},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_log_query(recent_log, arguments: dict) -> Dict[str, Any]:
|
||||||
|
"""Tool handler for log.query calls."""
|
||||||
|
return query_log(
|
||||||
|
recent_log=recent_log,
|
||||||
|
query_type=arguments.get("type", "all"),
|
||||||
|
player=arguments.get("player"),
|
||||||
|
limit=int(arguments.get("limit", 5)),
|
||||||
|
)
|
||||||
@@ -608,6 +608,88 @@ TOOL_SCHEMAS: List[Dict[str, Any]] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
# ── Log query tool ────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"name": "log.query",
|
||||||
|
"description": (
|
||||||
|
"Query recent server events. Use instead of reading raw logs. "
|
||||||
|
"Types: chat (recent messages), deaths (who died and how), "
|
||||||
|
"joins (who joined/left), actions (commands, advancements), all. "
|
||||||
|
"Filter by player name. Returns newest first."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["chat", "deaths", "joins", "actions", "all"],
|
||||||
|
"description": "Event type to query."
|
||||||
|
},
|
||||||
|
"player": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by player name (optional)."
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Max results (default 5)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": False
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"results": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"type": "string"},
|
||||||
|
"player": {"type": "string"},
|
||||||
|
"detail": {"type": "string"},
|
||||||
|
"age": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"count": {"type": "integer"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
# ── User ask tool ─────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
"name": "user.ask",
|
||||||
|
"description": (
|
||||||
|
"Ask the player a clarifying question in-game via chat. "
|
||||||
|
"Use ONLY when the request is ambiguous AND high-risk (affects other players, "
|
||||||
|
"destructive, permanent). For low-risk ambiguity, just make a creative choice. "
|
||||||
|
"BEFORE asking: try to resolve ambiguity using journal.read, world.server_state, "
|
||||||
|
"log.query, and world.nearby_entities. Only ask if context doesn't resolve it."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"player": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Player to ask."
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The clarifying question. Be specific about the options."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["player", "question"],
|
||||||
|
"additionalProperties": False
|
||||||
|
},
|
||||||
|
"returns": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"response": {"type": "string", "description": "The player's answer (filled by gateway)."}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
User Ask — clarifying questions sent to the player in-game.
|
||||||
|
|
||||||
|
The model sends a question via tellraw and the gateway stores the pending
|
||||||
|
question state. The player's next chat message gets routed back as the
|
||||||
|
tool result.
|
||||||
|
|
||||||
|
Risk-scaled: model should exhaust journal/state/log queries before asking.
|
||||||
|
Low risk = just act creatively. High risk = ask first.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
1. Model emits: <tool_call>{"name": "user.ask", "arguments": {"question": "..."}}</tool_call>
|
||||||
|
2. Gateway sends tellraw to the player
|
||||||
|
3. Gateway stores pending_question in session state
|
||||||
|
4. Player's next chat message becomes the tool result
|
||||||
|
5. Model continues with the answer
|
||||||
|
|
||||||
|
For training: simulate the ask/answer flow with synthetic responses.
|
||||||
|
For production: gateway handles the async wait.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from agent.tools.user_ask import handle_user_ask, format_ask_tellraw
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
def format_ask_tellraw(player: str, question: str, prefix: str = "[MORTDECAI]") -> str:
|
||||||
|
"""Format a clarifying question as a tellraw command."""
|
||||||
|
safe_q = question.replace('"', '\\"').replace("\\", "\\\\")
|
||||||
|
return (
|
||||||
|
f'tellraw {player} ['
|
||||||
|
f'{{"text":"{prefix} ","color":"gold","bold":true}},'
|
||||||
|
f'{{"text":"{safe_q}","color":"yellow","italic":true}}'
|
||||||
|
f']'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_user_ask(config: dict, arguments: dict, rcon_fn=None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Send a clarifying question to the player.
|
||||||
|
|
||||||
|
In production: sends tellraw and returns a pending state.
|
||||||
|
The gateway is responsible for waiting for the player's response
|
||||||
|
and feeding it back as the tool result.
|
||||||
|
|
||||||
|
In training: the response is simulated in the training data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: server config
|
||||||
|
arguments: {"player": str, "question": str}
|
||||||
|
rcon_fn: function to execute RCON commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{"ok": True, "status": "pending", "question": question}
|
||||||
|
In production, the gateway replaces this with the actual player response.
|
||||||
|
"""
|
||||||
|
player = arguments.get("player", "")
|
||||||
|
question = arguments.get("question", "")
|
||||||
|
|
||||||
|
if not player or not question:
|
||||||
|
return {"ok": False, "error": "player and question required"}
|
||||||
|
|
||||||
|
# Send the question in-game
|
||||||
|
if rcon_fn:
|
||||||
|
prefix = config.get("god_chat_prefix", "[MORTDECAI]")
|
||||||
|
cmd = format_ask_tellraw(player, question, prefix)
|
||||||
|
try:
|
||||||
|
rcon_fn(cmd)
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": f"Failed to send question: {e}"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"status": "pending",
|
||||||
|
"player": player,
|
||||||
|
"question": question,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user