mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
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:
parent
5edb346c75
commit
6c26727bb3
2 changed files with 208 additions and 2 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue