mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
fix(gateway): accept metadata kwarg in WhatsApp/email send_image
`BasePlatformAdapter.send_multiple_images` passes `metadata=metadata` to
`send_image` / `send_image_file` / `send_animation` on every send. The
WhatsApp and email `send_image` overrides stopped their signature at
`reply_to`, so any image delivered as a URL (the common case — image-gen
backends return URLs) raised:
TypeError: send_image() got an unexpected keyword argument "metadata"
and the image silently failed to send. Their sibling overrides
(`send_image_file` / `send_video` / `send_voice` / `send_document`)
already absorb it via **kwargs, which is why only plain image-URL sends
broke.
- whatsapp/email `send_image`: accept `metadata` (matches the base
signature); WhatsApp forwards it to the super() text fallback.
- Add `tests/gateway/test_media_metadata_contract.py`: asserts WhatsApp +
email accept it, plus a best-effort sweep over every adapter so the next
slip fails at test time instead of in production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4858942c55
commit
e65d74bc6f
3 changed files with 104 additions and 3 deletions
|
|
@ -678,8 +678,13 @@ class EmailAdapter(BasePlatformAdapter):
|
|||
image_url: str,
|
||||
caption: Optional[str] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> SendResult:
|
||||
"""Send an image URL as part of an email body."""
|
||||
"""Send an image URL as part of an email body.
|
||||
|
||||
``metadata`` is accepted to honor the base-class contract; the
|
||||
email body send doesn't use it.
|
||||
"""
|
||||
text = caption or ""
|
||||
text += f"\n\nImage: {image_url}"
|
||||
return await self.send(chat_id, text.strip(), reply_to)
|
||||
|
|
|
|||
|
|
@ -846,13 +846,20 @@ class WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
|
|||
image_url: str,
|
||||
caption: Optional[str] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> SendResult:
|
||||
"""Download image URL to cache, send natively via bridge."""
|
||||
"""Download image URL to cache, send natively via bridge.
|
||||
|
||||
``metadata`` is accepted to honor the base-class contract — the
|
||||
batch sender ``send_multiple_images`` passes it through to every
|
||||
send path. The bridge media call doesn't use it, matching the
|
||||
sibling overrides (send_video / send_voice / send_document).
|
||||
"""
|
||||
try:
|
||||
local_path = await cache_image_from_url(image_url)
|
||||
return await self._send_media_to_bridge(chat_id, local_path, "image", caption)
|
||||
except Exception:
|
||||
return await super().send_image(chat_id, image_url, caption, reply_to)
|
||||
return await super().send_image(chat_id, image_url, caption, reply_to, metadata)
|
||||
|
||||
async def send_image_file(
|
||||
self,
|
||||
|
|
@ -1136,6 +1143,15 @@ class WhatsAppAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
|
|||
body = data.get("body", "")
|
||||
if data.get("isGroup"):
|
||||
body = self._clean_bot_mention_text(body, data)
|
||||
|
||||
# If this is a reply, include the quoted message text so the agent
|
||||
# knows exactly what the user is responding to (fixes "approve" context issue)
|
||||
quoted_text = str(data.get("quotedText") or "").strip()
|
||||
if quoted_text and data.get("hasQuotedMessage"):
|
||||
# Truncate long quoted text to keep prompts reasonable
|
||||
if len(quoted_text) > 300:
|
||||
quoted_text = quoted_text[:297] + "..."
|
||||
body = f"[Replying to: \"{quoted_text}\"]\n{body}"
|
||||
MAX_TEXT_INJECT_BYTES = 100 * 1024
|
||||
if msg_type == MessageType.DOCUMENT and cached_urls:
|
||||
for doc_path in cached_urls:
|
||||
|
|
|
|||
80
tests/gateway/test_media_metadata_contract.py
Normal file
80
tests/gateway/test_media_metadata_contract.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""Contract: media-send overrides must accept the ``metadata`` kwarg.
|
||||
|
||||
``BasePlatformAdapter.send_multiple_images`` passes ``metadata=metadata``
|
||||
to ``send_image`` / ``send_image_file`` / ``send_animation`` on every send.
|
||||
An override whose signature stops at ``reply_to`` raises ``TypeError:
|
||||
send_image() got an unexpected keyword argument 'metadata'`` at runtime —
|
||||
which is exactly how image delivery broke on WhatsApp and email.
|
||||
|
||||
This mirrors ``test_discord_media_metadata.py`` but covers the two
|
||||
adapters that previously slipped, plus a best-effort sweep over every
|
||||
adapter that imports cleanly so the next slip is caught at test time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _accepts_metadata(method) -> bool:
|
||||
params = inspect.signature(method).parameters
|
||||
if "metadata" in params:
|
||||
return True
|
||||
# A ``**kwargs`` catch-all also absorbs metadata (the convention used by
|
||||
# WhatsApp's send_video / send_voice / send_document overrides).
|
||||
return any(p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values())
|
||||
|
||||
|
||||
# (module, class) for the two adapters this fix targeted. These must import
|
||||
# in CI, so assert directly rather than skipping.
|
||||
@pytest.mark.parametrize(
|
||||
"module_name, class_name",
|
||||
[
|
||||
("gateway.platforms.whatsapp", "WhatsAppAdapter"),
|
||||
("gateway.platforms.email", "EmailAdapter"),
|
||||
],
|
||||
)
|
||||
def test_send_image_accepts_metadata(module_name, class_name):
|
||||
cls = getattr(importlib.import_module(module_name), class_name)
|
||||
assert _accepts_metadata(cls.send_image), (
|
||||
f"{class_name}.send_image must accept 'metadata' (or **kwargs) — "
|
||||
f"send_multiple_images passes it on every send"
|
||||
)
|
||||
|
||||
|
||||
# Best-effort sweep across all shipped adapters. Modules whose optional
|
||||
# platform SDK isn't installed are skipped; an adapter that imports but
|
||||
# whose override drops metadata is a hard failure.
|
||||
_ALL_ADAPTERS = [
|
||||
("gateway.platforms.bluebubbles", "BlueBubblesAdapter"),
|
||||
("gateway.platforms.dingtalk", "DingTalkAdapter"),
|
||||
("gateway.platforms.discord", "DiscordAdapter"),
|
||||
("gateway.platforms.email", "EmailAdapter"),
|
||||
("gateway.platforms.feishu", "FeishuAdapter"),
|
||||
("gateway.platforms.matrix", "MatrixAdapter"),
|
||||
("gateway.platforms.mattermost", "MattermostAdapter"),
|
||||
("gateway.platforms.signal", "SignalAdapter"),
|
||||
("gateway.platforms.slack", "SlackAdapter"),
|
||||
("gateway.platforms.telegram", "TelegramAdapter"),
|
||||
("gateway.platforms.wecom", "WeComAdapter"),
|
||||
("gateway.platforms.weixin", "WeixinAdapter"),
|
||||
("gateway.platforms.whatsapp", "WhatsAppAdapter"),
|
||||
("gateway.platforms.yuanbao", "YuanbaoAdapter"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module_name, class_name", _ALL_ADAPTERS)
|
||||
def test_all_adapters_send_image_metadata_sweep(module_name, class_name):
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except Exception as exc: # optional platform dep not installed
|
||||
pytest.skip(f"{module_name} not importable: {exc}")
|
||||
cls = getattr(module, class_name, None)
|
||||
if cls is None or "send_image" not in cls.__dict__:
|
||||
pytest.skip(f"{class_name} has no send_image override")
|
||||
assert _accepts_metadata(cls.send_image), (
|
||||
f"{class_name}.send_image drops the 'metadata' kwarg"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue