feat(agent): cross-char search_items tool + bump timeouts
Adds an MCP tool wrapping the inventory-service /search/items endpoint with include_all_characters=true, so questions like 'find me a bracelet with Legendary Acid Ward on any unequipped char' resolve in ONE tool call instead of looping get_inventory over 60+ chars (which timed out at 120s). - agent/tools.py: search_items_global wrapper - agent/mcp_overlord.py: register new tool with detailed schema doc - agent/claude_wrapper.py: include in --allowed-tools whitelist; bump timeout 120s -> 240s - nginx/overlord.conf: bump /api/agent/ proxy timeout 180s -> 300s - CLAUDE.md: brief Claude to USE search_items for cross-char searches
This commit is contained in:
parent
d3943e894c
commit
4ae18536be
5 changed files with 82 additions and 8 deletions
|
|
@ -150,11 +150,16 @@ When invoked through the dashboard's chat window (the **🤖 Assistant** button)
|
|||
- `get_live_players` — current online characters with positions/kills/state
|
||||
- `get_recent_rares` — rare item finds in the last N hours
|
||||
- `query_telemetry_db` — read-only SQL on the telemetry DB for ad-hoc analysis
|
||||
- (more tools added over time — call `list_tools` if unsure)
|
||||
- `search_items` — **cross-character** inventory search (use this instead of looping `get_inventory` per character — single call is much faster)
|
||||
- `get_inventory` / `get_inventory_search` — single-character inventory
|
||||
- `get_player_state` / `get_combat_stats` / `get_equipment_cantrips` — per-character lookups
|
||||
- `get_quest_status` / `get_server_health` — global state
|
||||
- `suitbuilder_search` — armor optimization (slow, only on explicit request)
|
||||
|
||||
### Behaviour rules
|
||||
|
||||
1. **Use tools, don't speculate.** If the user asks "how many chars are online" — call `get_live_players`. Don't say "I'd need to check" — just check.
|
||||
1a. **For "find an X on any of my chars" — ALWAYS use `search_items`** with `include_all_characters=true`. Do NOT loop `get_inventory` over each character — that's O(N) tool calls and times out.
|
||||
2. **Be concise.** The user is glancing at a chat window, not reading a report. 2-5 sentences for most answers. Use markdown tables for tabular data.
|
||||
3. **No code unless asked.** This mode is about *operating* the system, not editing it. Don't open files or write code unless the user explicitly asks.
|
||||
4. **Real numbers, real names.** Cite actual character names and counts from tools — never make up sample data.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ CLAUDE_CWD = os.getenv("CLAUDE_CWD", "/home/erik/MosswartOverlord")
|
|||
# Hard cap on how long a single agent turn may take. Claude Code can spin a
|
||||
# while when chaining many tool calls; we don't want to leave a zombie
|
||||
# subprocess if something gets stuck.
|
||||
CLAUDE_TIMEOUT_S = int(os.getenv("CLAUDE_TIMEOUT_S", "120"))
|
||||
CLAUDE_TIMEOUT_S = int(os.getenv("CLAUDE_TIMEOUT_S", "240"))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -82,6 +82,7 @@ async def ask_claude(message: str, session_id: str) -> ClaudeResult:
|
|||
"mcp__overlord__get_player_state",
|
||||
"mcp__overlord__get_inventory",
|
||||
"mcp__overlord__get_inventory_search",
|
||||
"mcp__overlord__search_items",
|
||||
"mcp__overlord__get_combat_stats",
|
||||
"mcp__overlord__get_equipment_cantrips",
|
||||
"mcp__overlord__get_quest_status",
|
||||
|
|
|
|||
|
|
@ -127,10 +127,8 @@ TOOL_DEFS: dict[str, dict[str, Any]] = {
|
|||
},
|
||||
"get_inventory_search": {
|
||||
"description": (
|
||||
"Filtered inventory search for one character. Pass filter query "
|
||||
"params as the `filters` object. Common filters: name (substring), "
|
||||
"armor_level_min, armor_level_max, material, item_set, has_spell. "
|
||||
"Returns matching items in the same shape as get_inventory."
|
||||
"Filtered inventory search for ONE character. Use search_items "
|
||||
"instead when the user wants to find something across ALL chars."
|
||||
),
|
||||
"schema": {
|
||||
"type": "object",
|
||||
|
|
@ -147,6 +145,39 @@ TOOL_DEFS: dict[str, dict[str, Any]] = {
|
|||
str(args["character_name"]), args.get("filters") or {}
|
||||
),
|
||||
},
|
||||
"search_items": {
|
||||
"description": (
|
||||
"CROSS-CHARACTER item search — one query that scans every "
|
||||
"character's inventory. Use this whenever the user asks "
|
||||
"'find me an X on any of my chars'. **Do not** iterate "
|
||||
"get_inventory per character — this single tool call is far "
|
||||
"faster and avoids agent timeouts.\n\n"
|
||||
"Filter keys (pass as `filters` object, all optional):\n"
|
||||
" include_all_characters: true (default if no scope given)\n"
|
||||
" character: 'Name' (single char)\n"
|
||||
" characters: 'A,B,C' (specific list, comma-separated)\n"
|
||||
" text: substring of item name/description\n"
|
||||
" has_spell: 'Legendary Acid Ward' (exact spell name match)\n"
|
||||
" spell_contains: 'Legendary' (substring)\n"
|
||||
" legendary_cantrips: 'Foo,Bar'\n"
|
||||
" equipment_status: 'equipped' | 'unequipped'\n"
|
||||
" equipment_slot: int bitmask (4=chest, 2048=bracelet, 4096=ring)\n"
|
||||
" slot_names: 'Bracelet,Ring'\n"
|
||||
" armor_only / jewelry_only / weapon_only: bool\n"
|
||||
" min_armor / max_armor / min_damage / max_damage: int\n"
|
||||
),
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["filters"],
|
||||
"properties": {
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"description": "Query params dict — see tool description for keys.",
|
||||
},
|
||||
},
|
||||
},
|
||||
"fn": lambda args: T.search_items_global(args.get("filters") or {}),
|
||||
},
|
||||
"get_combat_stats": {
|
||||
"description": (
|
||||
"Lifetime + session combat stats for one character. Includes total "
|
||||
|
|
|
|||
|
|
@ -297,6 +297,40 @@ async def get_inventory_search(
|
|||
return resp.json()
|
||||
|
||||
|
||||
async def search_items_global(filters: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Cross-character item search via the inventory service's /search/items.
|
||||
|
||||
Use this INSTEAD of looping per-character when the user asks "find an X
|
||||
on any of my chars" — one DB query vs. 60+ HTTP roundtrips.
|
||||
|
||||
Common filter keys (passed straight through as query params):
|
||||
include_all_characters: bool (set true to search every char)
|
||||
character: str (single char) | characters: "A,B,C"
|
||||
text: str (name/description substring)
|
||||
has_spell: "Legendary Acid Ward" — exact spell name
|
||||
spell_contains: "Legendary" — substring match
|
||||
legendary_cantrips: "Foo,Bar"
|
||||
equipment_status: "equipped" | "unequipped"
|
||||
equipment_slot: int (bitmask: 4=chest, 2048=bracelet, 4096=ring, ...)
|
||||
slot_names: "Bracelet,Ring"
|
||||
armor_only / jewelry_only / weapon_only: bool
|
||||
min_armor / max_armor / min_damage / max_damage: int
|
||||
...and many more — see /search/items endpoint docs.
|
||||
"""
|
||||
client = await _http()
|
||||
# Default to all-character search if caller didn't scope; otherwise the
|
||||
# endpoint refuses with a 400.
|
||||
params = dict(filters or {})
|
||||
if not any(
|
||||
k in params
|
||||
for k in ("character", "characters", "include_all_characters")
|
||||
):
|
||||
params["include_all_characters"] = True
|
||||
resp = await client.get("/search/items", params=params)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def get_combat_stats(character_name: str) -> dict[str, Any]:
|
||||
"""Lifetime + session combat stats for one character (per-element split,
|
||||
monster encounters, surge counts)."""
|
||||
|
|
|
|||
|
|
@ -64,8 +64,11 @@ server {
|
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_read_timeout 180s;
|
||||
proxy_send_timeout 180s;
|
||||
# Heavy tool calls (cross-char search, suitbuilder) can take a while;
|
||||
# the python wrapper caps each turn at 240s, so 300s gives some
|
||||
# headroom for the round trip.
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
|
||||
# API endpoints (live, trails, history, stats) — short-lived HTTP
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue