fix(gateway): include replied-to media attachments (#46107)

This commit is contained in:
Teknium 2026-06-14 04:51:50 -07:00 committed by GitHub
parent a27d7e68cc
commit efbe1635dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 151 additions and 1 deletions

View File

@ -5560,6 +5560,52 @@ class TelegramAdapter(BasePlatformAdapter):
event.text = self._append_observed_note(event.text, cached.context_note()) event.text = self._append_observed_note(event.text, cached.context_note())
logger.info("[Telegram] Cached observed group %s at %s", cached.kind, cached.path) logger.info("[Telegram] Cached observed group %s at %s", cached.kind, cached.path)
async def _cache_replied_media(self, msg: Any, event: MessageEvent) -> None:
"""Cache media from the message this turn replies to, if any."""
from gateway.platforms.base import cache_media_bytes
reply_msg = getattr(msg, "reply_to_message", None)
if reply_msg is None:
return
source, filename, mime, kind = self._observed_media_source(reply_msg)
if source is None:
return
max_bytes = getattr(self, "_max_doc_bytes", 20 * 1024 * 1024)
file_size = getattr(source, "file_size", None)
try:
size = int(file_size or 0)
except (TypeError, ValueError):
size = 0
if not (0 < size <= max_bytes):
return
try:
file_obj = await source.get_file()
data = bytes(await file_obj.download_as_bytearray())
if not filename:
filename = os.path.basename(getattr(file_obj, "file_path", "") or "")
cached = cache_media_bytes(data, filename=filename, mime_type=mime, default_kind=kind)
except Exception as exc:
logger.warning("[Telegram] Failed to cache replied-to media: %s", exc, exc_info=True)
return
if cached is None:
return
event.media_urls.append(cached.path)
event.media_types.append(cached.media_type)
if len(event.media_urls) == 1:
if cached.kind == "image":
event.message_type = MessageType.PHOTO
elif cached.kind == "video":
event.message_type = MessageType.VIDEO
event.text = self._append_observed_note(
event.text,
f"[Replied-to {cached.kind} '{cached.display_name}' saved at: {cached.path}]",
)
logger.info("[Telegram] Cached replied-to %s at %s", cached.kind, cached.path)
def _observed_media_source(self, msg: Message): def _observed_media_source(self, msg: Message):
"""Return (telegram_file_source, filename, mime, default_kind) or Nones.""" """Return (telegram_file_source, filename, mime, default_kind) or Nones."""
if msg.photo: if msg.photo:
@ -5749,6 +5795,7 @@ class TelegramAdapter(BasePlatformAdapter):
event = self._build_message_event(msg, MessageType.TEXT, update_id=update.update_id) event = self._build_message_event(msg, MessageType.TEXT, update_id=update.update_id)
event.text = self._clean_bot_trigger_text(event.text) event.text = self._clean_bot_trigger_text(event.text)
await self._cache_replied_media(msg, event)
event = self._apply_telegram_group_observe_attribution(event) event = self._apply_telegram_group_observe_attribution(event)
self._enqueue_text_event(event) self._enqueue_text_event(event)
@ -5763,6 +5810,7 @@ class TelegramAdapter(BasePlatformAdapter):
event = self._build_message_event(msg, MessageType.COMMAND, update_id=update.update_id) event = self._build_message_event(msg, MessageType.COMMAND, update_id=update.update_id)
event.text = self._clean_bot_trigger_text(event.text) event.text = self._clean_bot_trigger_text(event.text)
await self._cache_replied_media(msg, event)
event = self._apply_telegram_group_observe_attribution(event) event = self._apply_telegram_group_observe_attribution(event)
await self.handle_message(event) await self.handle_message(event)

View File

@ -4973,7 +4973,13 @@ class DiscordAdapter(BasePlatformAdapter):
auto_threaded_channel = thread auto_threaded_channel = thread
self._threads.mark(thread_id) self._threads.mark(thread_id)
all_attachments = list(message.attachments) + snapshot_attachments referenced_attachments = []
reference = getattr(message, "reference", None)
resolved_reference = getattr(reference, "resolved", None) if reference else None
if resolved_reference is not None:
referenced_attachments = list(getattr(resolved_reference, "attachments", []) or [])
all_attachments = list(message.attachments) + snapshot_attachments + referenced_attachments
# Determine message type # Determine message type
msg_type = MessageType.TEXT msg_type = MessageType.TEXT

View File

@ -5,6 +5,7 @@ Covers the fix for slash commands not being recognized when sent via
""" """
import asyncio import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import pytest import pytest
@ -104,3 +105,51 @@ class TestAutoThreadingPreservesCommand:
response = get_response_text(discord_adapter) response = get_response_text(discord_adapter)
assert response is not None assert response is not None
assert "/new" in response assert "/new" in response
class TestRepliedToMediaDispatch:
async def test_reply_to_image_message_caches_referenced_attachment(
self, discord_adapter, bot_user, monkeypatch
):
"""A text reply to an image-bearing Discord message should give the agent that image."""
cached_path = "/tmp/replied-discord-image.png"
async def fake_cache_image_from_url(url, *, ext=".jpg"):
assert url == "https://cdn.discordapp.com/attachments/image.png"
assert ext == ".png"
return cached_path
monkeypatch.setattr(
"plugins.platforms.discord.adapter.cache_image_from_url",
fake_cache_image_from_url,
)
discord_adapter.handle_message = AsyncMock()
attachment = SimpleNamespace(
content_type="image/png",
filename="image.png",
url="https://cdn.discordapp.com/attachments/image.png",
size=1234,
)
referenced_message = SimpleNamespace(
id=12345,
content="",
attachments=[attachment],
)
msg = make_discord_message(
content=f"<@{BOT_USER_ID}> what's in this image?",
mentions=[bot_user],
)
msg.type = 19
msg.reference = SimpleNamespace(message_id=12345, resolved=referenced_message)
await discord_adapter._handle_message(msg)
discord_adapter.handle_message.assert_awaited_once()
await_args = discord_adapter.handle_message.await_args
assert await_args is not None
event = await_args.args[0]
assert event.reply_to_message_id == "12345"
assert event.media_urls == [cached_path]
assert event.media_types == ["image/png"]
assert event.message_type.value == "photo"

View File

@ -1007,6 +1007,53 @@ def test_triggered_voice_message_uses_shared_session_in_observe_mode():
asyncio.run(_run()) asyncio.run(_run())
# ---------------------------------------------------------------------------
# Replied-to media caching
# ---------------------------------------------------------------------------
def test_text_reply_to_photo_caches_referenced_media(monkeypatch, tmp_path):
async def _run():
adapter = _make_adapter(require_mention=False)
adapter.handle_message = AsyncMock()
cached_path = tmp_path / "reply_photo.png"
monkeypatch.setattr(
"gateway.platforms.base.cache_image_from_bytes",
lambda _data, ext=".jpg": str(cached_path),
)
file_obj = SimpleNamespace(
file_path="photos/replied.png",
download_as_bytearray=AsyncMock(return_value=bytearray(b"\x89PNG\r\n\x1a\n reply")),
)
photo = SimpleNamespace(file_size=1234, get_file=AsyncMock(return_value=file_obj))
replied = SimpleNamespace(
message_id=51,
text=None,
caption=None,
photo=[photo],
video=None,
audio=None,
voice=None,
document=None,
)
msg = _group_message("what's in this image?", reply_to_bot=False)
msg.reply_to_message = replied
update = SimpleNamespace(update_id=3010, message=msg, effective_message=msg)
await adapter._handle_text_message(update, SimpleNamespace())
await asyncio.sleep(0.05)
adapter.handle_message.assert_awaited_once()
await_args = adapter.handle_message.await_args
assert await_args is not None
event = await_args.args[0]
assert event.reply_to_message_id == "51"
assert event.media_urls == [str(cached_path)]
assert event.media_types == ["image/png"]
assert event.message_type == MessageType.PHOTO
asyncio.run(_run())
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Observed-media caching (unmentioned group attachments) # Observed-media caching (unmentioned group attachments)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------