fix(gateway): extend observe+attribution to location and media handlers

_handle_location_message and _handle_media_message were skipped when the
observe-unmentioned-group-messages feature landed (a9db0e2c7). Both handlers
now:

1. Check _should_observe_unmentioned_group_message on the skipped path and
   call _observe_unmentioned_group_message so group chatter is stored as
   shared session context even when the bot is not addressed.

2. Call _apply_telegram_group_observe_attribution on the triggered path so
   the dispatched event uses the shared (user_id=None) group session instead
   of the per-user session, letting the model see previously observed context.
   For stickers the attribution is applied after _handle_sticker completes
   (which overwrites event.text with the vision description); for all other
   media types it is applied once after caption cleaning.

Four new tests cover the observe and attribution paths for both handlers.
This commit is contained in:
EloquentBrush0x 2026-05-21 09:26:46 +03:00 committed by Teknium
parent 5edb346c75
commit 6c26727bb3
2 changed files with 208 additions and 2 deletions

View file

@ -4783,6 +4783,8 @@ class TelegramAdapter(BasePlatformAdapter):
if not msg:
return
if not self._should_process_message(msg):
if self._should_observe_unmentioned_group_message(msg):
self._observe_unmentioned_group_message(msg, MessageType.LOCATION, update_id=update.update_id)
return
venue = getattr(msg, "venue", None)
@ -4812,6 +4814,7 @@ class TelegramAdapter(BasePlatformAdapter):
event = self._build_message_event(msg, MessageType.LOCATION, update_id=update.update_id)
event.text = "\n".join(parts)
event = self._apply_telegram_group_observe_attribution(event)
await self.handle_message(event)
# ------------------------------------------------------------------
@ -4956,8 +4959,23 @@ class TelegramAdapter(BasePlatformAdapter):
if not update.message:
return
if not self._should_process_message(update.message):
if self._should_observe_unmentioned_group_message(update.message):
_m = update.message
if _m.sticker:
_observe_type = MessageType.STICKER
elif _m.photo:
_observe_type = MessageType.PHOTO
elif _m.video:
_observe_type = MessageType.VIDEO
elif _m.audio:
_observe_type = MessageType.AUDIO
elif _m.voice:
_observe_type = MessageType.VOICE
else:
_observe_type = MessageType.DOCUMENT
self._observe_unmentioned_group_message(_m, _observe_type, update_id=update.update_id)
return
msg = update.message
# Determine media type
@ -4985,9 +5003,14 @@ class TelegramAdapter(BasePlatformAdapter):
# Handle stickers: describe via vision tool with caching
if msg.sticker:
await self._handle_sticker(msg, event)
event = self._apply_telegram_group_observe_attribution(event)
await self.handle_message(event)
return
# Apply observe attribution after caption is set; sticker is handled above
# because _handle_sticker overwrites event.text with its vision description.
event = self._apply_telegram_group_observe_attribution(event)
# Download photo to local image cache so the vision tool can access it
# even after Telegram's ephemeral file URLs expire (~1 hour).
if msg.photo:

View file

@ -700,3 +700,186 @@ def test_config_bridges_telegram_ignored_threads(monkeypatch, tmp_path):
assert config is not None
assert __import__("os").environ["TELEGRAM_IGNORED_THREADS"] == "31,42"
# ---------------------------------------------------------------------------
# Helpers for location / media observe+attribution tests
# ---------------------------------------------------------------------------
def _group_location_message(
*,
chat_id=-100,
from_user_id=111,
from_user_name="Alice Example",
lat=37.7749,
lon=-122.4194,
):
return SimpleNamespace(
message_id=50,
text=None,
caption=None,
entities=[],
caption_entities=[],
message_thread_id=None,
is_topic_message=False,
chat=SimpleNamespace(id=chat_id, type="group", title="Test Group", is_forum=False),
from_user=SimpleNamespace(
id=from_user_id, full_name=from_user_name,
first_name=from_user_name.split()[0],
),
reply_to_message=None,
date=None,
location=SimpleNamespace(latitude=lat, longitude=lon),
venue=None,
sticker=None,
photo=None,
video=None,
audio=None,
voice=None,
document=None,
)
def _group_voice_message(
*,
chat_id=-100,
from_user_id=111,
from_user_name="Alice Example",
caption=None,
):
return SimpleNamespace(
message_id=51,
text=None,
caption=caption,
entities=[],
caption_entities=[],
message_thread_id=None,
is_topic_message=False,
chat=SimpleNamespace(id=chat_id, type="group", title="Test Group", is_forum=False),
from_user=SimpleNamespace(
id=from_user_id, full_name=from_user_name,
first_name=from_user_name.split()[0],
),
reply_to_message=None,
date=None,
location=None,
venue=None,
sticker=None,
photo=None,
video=None,
audio=None,
voice=SimpleNamespace(
get_file=AsyncMock(side_effect=Exception("simulated download failure"))
),
document=None,
)
# ---------------------------------------------------------------------------
# Observe + attribution parity: location messages
# ---------------------------------------------------------------------------
def test_unmentioned_location_message_observed_in_group():
async def _run():
adapter = _make_adapter(
require_mention=True,
allowed_chats=["-100"],
group_allowed_chats=["-100"],
observe_unmentioned_group_messages=True,
)
store = _FakeSessionStore()
adapter._session_store = store
update = SimpleNamespace(
update_id=2001,
message=_group_location_message(),
effective_message=None,
)
await adapter._handle_location_message(update, SimpleNamespace())
adapter._message_handler.assert_not_awaited()
assert len(store.messages) == 1
_, message, _ = store.messages[0]
assert message["observed"] is True
assert store.sources[0].user_id is None
asyncio.run(_run())
def test_triggered_location_message_uses_shared_session_in_observe_mode():
async def _run():
adapter = _make_adapter(
require_mention=False,
group_allowed_chats=["-100"],
observe_unmentioned_group_messages=True,
)
adapter.handle_message = AsyncMock()
update = SimpleNamespace(
update_id=2002,
message=_group_location_message(),
effective_message=None,
)
await adapter._handle_location_message(update, SimpleNamespace())
adapter.handle_message.assert_awaited_once()
event = adapter.handle_message.call_args[0][0]
assert event.source.user_id is None
assert "[Alice Example|111]" in event.text
asyncio.run(_run())
# ---------------------------------------------------------------------------
# Observe + attribution parity: media messages (voice as representative)
# ---------------------------------------------------------------------------
def test_unmentioned_voice_message_observed_in_group():
async def _run():
adapter = _make_adapter(
require_mention=True,
allowed_chats=["-100"],
group_allowed_chats=["-100"],
observe_unmentioned_group_messages=True,
)
store = _FakeSessionStore()
adapter._session_store = store
update = SimpleNamespace(
update_id=3001,
message=_group_voice_message(),
effective_message=None,
)
await adapter._handle_media_message(update, SimpleNamespace())
adapter._message_handler.assert_not_awaited()
assert len(store.messages) == 1
_, message, _ = store.messages[0]
assert message["observed"] is True
assert store.sources[0].user_id is None
asyncio.run(_run())
def test_triggered_voice_message_uses_shared_session_in_observe_mode():
async def _run():
adapter = _make_adapter(
require_mention=False,
group_allowed_chats=["-100"],
observe_unmentioned_group_messages=True,
)
adapter.handle_message = AsyncMock()
update = SimpleNamespace(
update_id=3002,
message=_group_voice_message(caption="check this audio"),
effective_message=None,
)
await adapter._handle_media_message(update, SimpleNamespace())
adapter.handle_message.assert_awaited_once()
event = adapter.handle_message.call_args[0][0]
assert event.source.user_id is None
assert "[Alice Example|111]" in event.text
asyncio.run(_run())