fix: match display name ('Claude') in group room mention check

Bot user is @ai:agiliton.eu but display name is 'Claude'.
Element renders mentions using display name, so the old check
for 'ai' in message body never matched '@Claude: ...' messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-03-20 15:05:09 +00:00
parent 0988f636d0
commit b69980d57f

159
bot.py
View File

@@ -118,6 +118,7 @@ IMPORTANT RULES — FOLLOW THESE STRICTLY:
- If a user's Atlassian account is not connected, tell them to connect it at https://matrixhost.eu/settings and provide the link.
- If user memories are provided, use them to personalize responses. Address users by name if known.
- When asked to translate, provide ONLY the translation with no explanation.
- You can scan Popular Investors (PIs) on eToro using the pi_scan tool. Use it when users ask about PI activity, trading signals, or want to run a scan. The scan takes 3-8 minutes and detects allocation changes.
- You can set reminders and scheduled messages. When users ask to be reminded of something, use the schedule_message tool. Parse natural language times like "in 2 hours", "tomorrow at 9am", "every Monday" into ISO 8601 datetime with Europe/Berlin timezone (unless user specifies otherwise)."""
IMAGE_GEN_TOOLS = [{
@@ -389,7 +390,47 @@ SCHEDULER_TOOLS = [
},
]
ALL_TOOLS = IMAGE_GEN_TOOLS + WEB_SEARCH_TOOLS + ATLASSIAN_TOOLS + ROOM_TOOLS + SCHEDULER_TOOLS
PIRADAR_URL = os.environ.get("PIRADAR_URL", "http://127.0.0.1:8095")
PIRADAR_API_KEY = os.environ.get("PIRADAR_API_KEY", "")
PI_SCAN_TOOLS = [
{
"type": "function",
"function": {
"name": "pi_scan",
"description": "Run a PI (Popular Investor) scan on eToro. Fetches top PI rankings, compares portfolio allocations to previous snapshots, and detects symbols where PIs increased their allocation. Takes 3-8 minutes. Use when user asks to scan PIs, check PI activity, or run a trading scan.",
"parameters": {
"type": "object",
"properties": {},
},
},
},
{
"type": "function",
"function": {
"name": "pi_scan_status",
"description": "Get the status and metadata of the last PI scan (timestamp, PI count, signals, health score). Use when user asks about scan status or last scan results.",
"parameters": {
"type": "object",
"properties": {},
},
},
},
{
"type": "function",
"function": {
"name": "pi_scan_signals",
"description": "Get the signals (allocation increases) from the most recent PI scan. Returns symbol, direction, PI count, weighted score, and details. Use when user asks for latest signals or PI consensus.",
"parameters": {
"type": "object",
"properties": {},
},
},
},
]
ALL_TOOLS = IMAGE_GEN_TOOLS + WEB_SEARCH_TOOLS + ATLASSIAN_TOOLS + ROOM_TOOLS + SCHEDULER_TOOLS + PI_SCAN_TOOLS
ATLASSIAN_NOT_CONNECTED_MSG = (
"Your Atlassian account is not connected. "
@@ -1276,6 +1317,14 @@ class Bot:
self.client.add_to_device_callback(self.on_key_verification, KeyVerificationMac)
self.client.add_to_device_callback(self.on_key_verification, KeyVerificationCancel)
# Cache display name for mention matching in group rooms
try:
dn_resp = await self.client.get_displayname(BOT_USER)
self._display_name = getattr(dn_resp, 'displayname', '') or ''
logger.info('Bot display name: %s', self._display_name)
except Exception:
self._display_name = ''
# Start reminder scheduler
asyncio.create_task(self._reminder_scheduler())
@@ -1909,10 +1958,14 @@ class Bot:
is_dm = room.member_count == 2
if not is_dm:
bot_display = self.client.user_id.split(":")[0].lstrip("@")
# Also match display name (e.g. 'Claude') since Element uses it in mentions
bot_displayname = (getattr(self, '_display_name', '') or bot_display).lower()
body_lower = body.lower()
mentioned = (
BOT_USER in body
or f"@{bot_display}" in body.lower()
or bot_display.lower() in body.lower()
or f"@{bot_display}" in body_lower
or bot_display.lower() in body_lower
or bot_displayname in body_lower
)
if not mentioned:
return
@@ -2140,10 +2193,14 @@ class Bot:
# Check if bot was @mentioned in the image body (caption) or skip
body = (event.body or "").strip()
bot_display = self.client.user_id.split(":")[0].lstrip("@")
# Also match display name (e.g. 'Claude') since Element uses it in mentions
bot_displayname = (getattr(self, '_display_name', '') or bot_display).lower()
body_lower = body.lower()
mentioned = (
BOT_USER in body
or f"@{bot_display}" in body.lower()
or bot_display.lower() in body.lower()
or f"@{bot_display}" in body_lower
or bot_display.lower() in body_lower
or bot_displayname in body_lower
)
if not mentioned:
return
@@ -2206,10 +2263,14 @@ class Bot:
if not is_dm:
body = (event.body or "").strip()
bot_display = self.client.user_id.split(":")[0].lstrip("@")
# Also match display name (e.g. 'Claude') since Element uses it in mentions
bot_displayname = (getattr(self, '_display_name', '') or bot_display).lower()
body_lower = body.lower()
mentioned = (
BOT_USER in body
or f"@{bot_display}" in body.lower()
or bot_display.lower() in body.lower()
or f"@{bot_display}" in body_lower
or bot_display.lower() in body_lower
or bot_displayname in body_lower
)
if not mentioned:
return
@@ -2357,10 +2418,14 @@ class Bot:
if not is_dm:
body = (event.body or "").strip()
bot_display = self.client.user_id.split(":")[0].lstrip("@")
# Also match display name (e.g. 'Claude') since Element uses it in mentions
bot_displayname = (getattr(self, '_display_name', '') or bot_display).lower()
body_lower = body.lower()
mentioned = (
BOT_USER in body
or f"@{bot_display}" in body.lower()
or bot_display.lower() in body.lower()
or f"@{bot_display}" in body_lower
or bot_display.lower() in body_lower
or bot_displayname in body_lower
)
if not mentioned:
return
@@ -2468,10 +2533,14 @@ class Bot:
if not is_dm:
body = (event.body or "").strip()
bot_display = self.client.user_id.split(":")[0].lstrip("@")
# Also match display name (e.g. 'Claude') since Element uses it in mentions
bot_displayname = (getattr(self, '_display_name', '') or bot_display).lower()
body_lower = body.lower()
mentioned = (
BOT_USER in body
or f"@{bot_display}" in body.lower()
or bot_display.lower() in body.lower()
or f"@{bot_display}" in body_lower
or bot_display.lower() in body_lower
or bot_displayname in body_lower
)
if not mentioned:
return
@@ -2707,6 +2776,74 @@ class Bot:
if tool_name == "cancel_reminder":
return await self._cancel_reminder(sender, args.get("reminder_id", 0))
# PI Scan tools — no auth needed
if tool_name == "pi_scan":
try:
headers = {}
if PIRADAR_API_KEY:
headers["Authorization"] = f"Bearer {PIRADAR_API_KEY}"
async with httpx.AsyncClient(timeout=600.0) as hc:
resp = await hc.post(f"{PIRADAR_URL}/scan", headers=headers)
if resp.status_code == 200:
data = resp.json()
meta = data.get("scan_meta", {})
signals = data.get("signals", [])
if "error" in data:
return f"PI Scan error: {data['error']}"
prev_ts = meta.get("previous_timestamp", "unknown")
result = f"PI Scan complete ({meta.get('duration_s', '?')}s)\n"
result += f"Time range: {prev_ts or 'first scan'}{meta.get('timestamp', '?')}\n"
result += f"PIs: {meta.get('fetched', '?')}/{meta.get('pi_count', '?')} | Health: {meta.get('health_score', '?')}%\n"
if meta.get("seeded", 0) > 0:
result += f"Seeded {meta['seeded']} new PI snapshots (signals from next scan)\n"
if not signals:
result += "No signals detected."
else:
result += f"\n{len(signals)} signals (allocation increases >= {meta.get('compared', '?')} PIs compared):\n\n"
result += "Symbol | Direction | PIs | Score | Details\n"
result += "-------|-----------|-----|-------|--------\n"
for sig in signals[:25]:
d = sig.get("details", "")[:80]
result += f"{sig['symbol']} | {sig['direction']} | {sig['pi_count']} | {sig['weighted_score']} | {d}\n"
if len(signals) > 25:
result += f"... and {len(signals) - 25} more signals"
return result
return f"PI Scan failed: HTTP {resp.status_code}"
except Exception as e:
return f"PI Scan error: {e}"
if tool_name == "pi_scan_status":
try:
async with httpx.AsyncClient(timeout=10.0) as hc:
resp = await hc.get(f"{PIRADAR_URL}/status")
if resp.status_code == 200:
meta = resp.json()
return f"Last scan: {meta.get('timestamp', '?')}\nDuration: {meta.get('duration_s', '?')}s | PIs: {meta.get('fetched', '?')}/{meta.get('pi_count', '?')} | Health: {meta.get('health_score', '?')}% | Signals: {meta.get('signals', '?')} | Seeded: {meta.get('seeded', 0)} | Compared: {meta.get('compared', 0)}"
return "No scan data yet."
except Exception as e:
return f"Status check failed: {e}"
if tool_name == "pi_scan_signals":
try:
async with httpx.AsyncClient(timeout=10.0) as hc:
resp = await hc.get(f"{PIRADAR_URL}/signals")
if resp.status_code == 200:
data = resp.json()
signals = data.get("signals", [])
if not signals:
return "No signals from last scan."
result = f"Last signals ({data.get('timestamp', '?')}, {len(signals)} signals):\n\n"
result += "Symbol | PIs | Score | Details\n"
result += "-------|-----|-------|--------\n"
for sig in signals[:25]:
d = sig.get("details", "")[:80]
result += f"{sig['symbol']} | {sig['pi_count']} | {sig['weighted_score']} | {d}\n"
return result
return "No signal data."
except Exception as e:
return f"Signals fetch failed: {e}"
# Atlassian tools — need per-user token
token = await self.atlassian.get_token(sender) if sender else None
if not token: