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:
159
bot.py
159
bot.py
@@ -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 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.
|
- 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.
|
- 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)."""
|
- 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 = [{
|
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 = (
|
ATLASSIAN_NOT_CONNECTED_MSG = (
|
||||||
"Your Atlassian account is not connected. "
|
"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, KeyVerificationMac)
|
||||||
self.client.add_to_device_callback(self.on_key_verification, KeyVerificationCancel)
|
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
|
# Start reminder scheduler
|
||||||
asyncio.create_task(self._reminder_scheduler())
|
asyncio.create_task(self._reminder_scheduler())
|
||||||
|
|
||||||
@@ -1909,10 +1958,14 @@ class Bot:
|
|||||||
is_dm = room.member_count == 2
|
is_dm = room.member_count == 2
|
||||||
if not is_dm:
|
if not is_dm:
|
||||||
bot_display = self.client.user_id.split(":")[0].lstrip("@")
|
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 = (
|
mentioned = (
|
||||||
BOT_USER in body
|
BOT_USER in body
|
||||||
or f"@{bot_display}" in body.lower()
|
or f"@{bot_display}" in body_lower
|
||||||
or bot_display.lower() in body.lower()
|
or bot_display.lower() in body_lower
|
||||||
|
or bot_displayname in body_lower
|
||||||
)
|
)
|
||||||
if not mentioned:
|
if not mentioned:
|
||||||
return
|
return
|
||||||
@@ -2140,10 +2193,14 @@ class Bot:
|
|||||||
# Check if bot was @mentioned in the image body (caption) or skip
|
# Check if bot was @mentioned in the image body (caption) or skip
|
||||||
body = (event.body or "").strip()
|
body = (event.body or "").strip()
|
||||||
bot_display = self.client.user_id.split(":")[0].lstrip("@")
|
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 = (
|
mentioned = (
|
||||||
BOT_USER in body
|
BOT_USER in body
|
||||||
or f"@{bot_display}" in body.lower()
|
or f"@{bot_display}" in body_lower
|
||||||
or bot_display.lower() in body.lower()
|
or bot_display.lower() in body_lower
|
||||||
|
or bot_displayname in body_lower
|
||||||
)
|
)
|
||||||
if not mentioned:
|
if not mentioned:
|
||||||
return
|
return
|
||||||
@@ -2206,10 +2263,14 @@ class Bot:
|
|||||||
if not is_dm:
|
if not is_dm:
|
||||||
body = (event.body or "").strip()
|
body = (event.body or "").strip()
|
||||||
bot_display = self.client.user_id.split(":")[0].lstrip("@")
|
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 = (
|
mentioned = (
|
||||||
BOT_USER in body
|
BOT_USER in body
|
||||||
or f"@{bot_display}" in body.lower()
|
or f"@{bot_display}" in body_lower
|
||||||
or bot_display.lower() in body.lower()
|
or bot_display.lower() in body_lower
|
||||||
|
or bot_displayname in body_lower
|
||||||
)
|
)
|
||||||
if not mentioned:
|
if not mentioned:
|
||||||
return
|
return
|
||||||
@@ -2357,10 +2418,14 @@ class Bot:
|
|||||||
if not is_dm:
|
if not is_dm:
|
||||||
body = (event.body or "").strip()
|
body = (event.body or "").strip()
|
||||||
bot_display = self.client.user_id.split(":")[0].lstrip("@")
|
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 = (
|
mentioned = (
|
||||||
BOT_USER in body
|
BOT_USER in body
|
||||||
or f"@{bot_display}" in body.lower()
|
or f"@{bot_display}" in body_lower
|
||||||
or bot_display.lower() in body.lower()
|
or bot_display.lower() in body_lower
|
||||||
|
or bot_displayname in body_lower
|
||||||
)
|
)
|
||||||
if not mentioned:
|
if not mentioned:
|
||||||
return
|
return
|
||||||
@@ -2468,10 +2533,14 @@ class Bot:
|
|||||||
if not is_dm:
|
if not is_dm:
|
||||||
body = (event.body or "").strip()
|
body = (event.body or "").strip()
|
||||||
bot_display = self.client.user_id.split(":")[0].lstrip("@")
|
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 = (
|
mentioned = (
|
||||||
BOT_USER in body
|
BOT_USER in body
|
||||||
or f"@{bot_display}" in body.lower()
|
or f"@{bot_display}" in body_lower
|
||||||
or bot_display.lower() in body.lower()
|
or bot_display.lower() in body_lower
|
||||||
|
or bot_displayname in body_lower
|
||||||
)
|
)
|
||||||
if not mentioned:
|
if not mentioned:
|
||||||
return
|
return
|
||||||
@@ -2707,6 +2776,74 @@ class Bot:
|
|||||||
if tool_name == "cancel_reminder":
|
if tool_name == "cancel_reminder":
|
||||||
return await self._cancel_reminder(sender, args.get("reminder_id", 0))
|
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
|
# Atlassian tools — need per-user token
|
||||||
token = await self.atlassian.get_token(sender) if sender else None
|
token = await self.atlassian.get_token(sender) if sender else None
|
||||||
if not token:
|
if not token:
|
||||||
|
|||||||
Reference in New Issue
Block a user