Files
hermes-agent/plugins/platforms/telegram/telegram_context.py
Teknium bc31e04d83 fix(telegram): strip our handle only as sole addressee; keep the identity line session-stable
Two corrections on top of the mention-preservation pick:

- Stripping our own trigger handle from every group message was replaced by never stripping
  it in groups, which broke the pre-existing contract that `@bot 2` answers a pending clarify
  prompt and `@bot ok` approves (the intercepts match the exact stripped text). Strip only when
  no other participant is named; when other bots/users are mentioned the text is kept verbatim
  so `@research_bot , @ops_bot …` no longer arrives as `, @ops_bot …`.
- The addressing block carried a per-message fact ("explicitly mentions you: yes/no") inside
  `channel_prompt`, which is part of the cached-agent signature: a mention turn followed by a
  reply turn rebuilt the AIAgent (prompt-cache miss) every time the shape flipped. Keep only
  the username line, byte-identical for the life of the session.

Tests rewritten to pin both contracts (sole-addressee stripping; identical channel_prompt and
agent signature across a mention turn and a reply turn).
2026-09-06 05:41:10 -07:00

54 lines
2.4 KiB
Python

"""Telegram group addressing helpers, kept out of the adapter facade.
The identity line lives in ``channel_prompt`` and therefore in the cached-agent signature: it
must be stable for the life of a session (username only — never a per-message fact).
"""
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from telegram import Message
from plugins.platforms.telegram.adapter import TelegramAdapter
def mentions_other_participants(adapter: "TelegramAdapter", message: "Message") -> bool:
"""True when a ``mention``/``text_mention`` entity names someone other than this bot."""
own = adapter._current_bot_username()
bot_id = getattr(adapter._bot, "id", None) if adapter._bot else None
for source_text, entities in adapter._entity_sources(message):
for entity in entities:
entity_type = adapter._entity_type(entity)
if entity_type == "mention":
handle = (adapter._entity_span(source_text, entity) or "").strip().lstrip("@").lower()
if handle and handle != own:
return True
elif entity_type == "text_mention":
user = getattr(entity, "user", None)
if user is not None and getattr(user, "id", None) != bot_id:
return True
return False
def group_trigger_text(adapter: "TelegramAdapter", message: "Message", text: Optional[str]) -> Optional[str]:
"""Strip our own handle only when we are the sole addressee. With other participants named,
``@research_bot , @ops_bot are you both listening?`` must not reach us as ``, @ops_bot …``."""
if adapter._is_group_chat(message) and mentions_other_participants(adapter, message):
return text
return adapter._clean_bot_trigger_text(text)
def group_identity_prompt(
adapter: "TelegramAdapter", message: "Message", channel_prompt: Optional[str],
) -> Optional[str]:
"""Session-stable identity line so the model can read retained @mentions as itself or not."""
if not adapter._is_group_chat(message) or not getattr(adapter, "_bot", None):
return channel_prompt
username = adapter._current_bot_username()
if not username:
return channel_prompt
identity = (
f"Your Telegram bot username in this group: @{username}. "
"Mentions of other bots are not requests for you to relay the message."
)
return f"{channel_prompt}\n\n{identity}" if channel_prompt else identity