Add equip/inventory commands, report results without AI

New commands:
- "Doug, equip your sword" → equips item, reports "Equipped sword."
- "Doug, what do you have?" → lists actual inventory items
- "Doug, check your inventory" → same

Key change: command results now report DIRECTLY to chat without
going through AI. No more hallucinated responses about items
Doug doesn't have. Commands execute → report real result.

AI is ONLY used for conversation, not for task responses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
roberts 2026-03-30 17:18:41 -05:00
parent 5a42c2b881
commit e6d4c8d377
2 changed files with 113 additions and 3 deletions

View file

@ -592,6 +592,12 @@ class DougBrain(QObject):
self._tasks.complete()
return
# Callbacks that need special response handling
if task.callback in ("on_equip_result", "on_inventory_report"):
# Execute then report result directly to chat (no AI)
self._execute_action_with_report(task)
return
# Skip placeholder actions
if task.action == "status":
self._tasks.complete()
@ -648,6 +654,48 @@ class DougBrain(QObject):
self._ws.send_request(task.action, task.params, on_response)
def _execute_action_with_report(self, task: Task):
"""Execute an action and report the result directly to chat (no AI)."""
self._state = BrainState.EXECUTING_TASK
self._action_sent_time = time.time()
if task.description and task.priority >= Priority.LOW:
log.info(f"[{task.priority.name}] {task.description}")
def on_response(resp: ResponseMessage):
self._state = BrainState.IDLE
if task.callback == "on_equip_result":
if resp.status == "success":
item = task.params.get("name", "item").replace("_", " ")
self._ws.send_request("send_chat", {"message": f"Equipped {item}."})
self._tasks.complete()
else:
error = resp.error or "I don't have that item."
self._ws.send_request("send_chat", {"message": error})
self._tasks.cancel()
elif task.callback == "on_inventory_report":
if resp.status == "success":
items = resp.data.get("items", [])
if items:
# List items concisely
item_strs = [f"{i['count']}x {i['name'].replace('_',' ')}" for i in items[:8]]
msg = "I have: " + ", ".join(item_strs)
if len(items) > 8:
msg += f" and {len(items) - 8} more items"
self._ws.send_request("send_chat", {"message": msg})
else:
self._ws.send_request("send_chat", {"message": "My inventory is empty."})
self._tasks.complete()
else:
self._ws.send_request("send_chat", {"message": "Can't check inventory right now."})
self._tasks.cancel()
else:
self._tasks.complete()
self._ws.send_request(task.action, task.params, on_response)
# ── Helpers ──
def _handle_idle_chat(self, task: Task):

View file

@ -69,7 +69,19 @@ class CommandParser:
r"(?:give|hand|pass|toss)\s+(?:me|us)\s+(?:a\s+|an\s+|some\s+)?(.+)",
]
# Social commands
EQUIP_PATTERNS = [
r"(?:equip|hold|use|grab|wield|switch to|pull out|get out)\s+(?:your\s+|the\s+|a\s+|my\s+)?(.+)",
r"(?:put on|wear)\s+(?:your\s+|the\s+|a\s+)?(.+)",
]
INVENTORY_PATTERNS = [
r"what(?:'s| is| do you have)\s+in\s+(?:your\s+)?(?:inventory|bag|pocket|backpack)",
r"what\s+(?:do you have|are you carrying|items do you have)",
r"show\s+(?:me\s+)?(?:your\s+)?(?:inventory|items|stuff)",
r"check\s+(?:your\s+)?(?:inventory|items|stuff)",
]
# Social/combat commands
ATTACK_PATTERNS = [
r"(?:attack|fight|kill|hit)\s+(?:that|this|the)?\s*(.+)",
]
@ -115,6 +127,12 @@ class CommandParser:
cmd = self._try_give(msg, sender)
if cmd: return cmd
cmd = self._try_equip(msg, sender)
if cmd: return cmd
cmd = self._try_inventory(msg, sender)
if cmd: return cmd
cmd = self._try_attack(msg, sender)
if cmd: return cmd
@ -236,6 +254,30 @@ class CommandParser:
)
return None
def _try_equip(self, msg: str, sender: str) -> Optional[ParsedCommand]:
for pattern in self.EQUIP_PATTERNS:
match = re.search(pattern, msg, re.IGNORECASE)
if match:
raw_item = match.group(1).strip() if match.lastindex else ""
# Clean up item name
filler = {"your", "the", "a", "my", "that"}
words = [w for w in raw_item.split() if w.lower().rstrip(".,!?") not in filler]
if not words:
return None
item = "_".join(w.lower().rstrip(".,!?") for w in words[:3])
return ParsedCommand(
action="equip",
target=item,
raw_message=msg,
)
return None
def _try_inventory(self, msg: str, sender: str) -> Optional[ParsedCommand]:
for pattern in self.INVENTORY_PATTERNS:
if re.search(pattern, msg, re.IGNORECASE):
return ParsedCommand(action="check_inventory", raw_message=msg)
return None
def _try_attack(self, msg: str, sender: str) -> Optional[ParsedCommand]:
for pattern in self.ATTACK_PATTERNS:
match = re.search(pattern, msg, re.IGNORECASE)
@ -348,9 +390,29 @@ def command_to_task(cmd: ParsedCommand, behaviors) -> Optional[Task]:
callback="on_look_around_report",
)
elif cmd.action == "equip":
return Task(
name=f"equip_{cmd.target}",
priority=Priority.HIGH,
action="equip_item",
params={"name": cmd.target, "destination": "hand"},
description=f"Equipping {cmd.target.replace('_', ' ')}",
timeout=10,
callback="on_equip_result",
)
elif cmd.action == "check_inventory":
return Task(
name="check_inventory",
priority=Priority.HIGH,
action="get_inventory",
params={},
description="Checking inventory",
timeout=10,
callback="on_inventory_report",
)
elif cmd.action == "go_to":
# For named destinations, we'd need a memory system
# For now, try to interpret as a player name
return Task(
name=f"go_to_{cmd.target}",
priority=Priority.HIGH,