feat(whatsapp): add WhatsApp Business Cloud API adapter
Add an official, production-grade WhatsApp integration via Meta's Business Cloud API as a complement to the existing Baileys bridge. No bridge subprocess, no QR codes, no account-ban risk — at the cost of a Meta Business account and a public HTTPS webhook URL. Setup is fully wizard-driven: 'hermes whatsapp-cloud' walks through every credential with paste-time validation (catches the #1 trap of pasting a phone number into the Phone Number ID field), generates a verify token, and ends with copy-paste instructions for the cloudflared / Meta-dashboard / Business Manager pieces that can't be automated. The wizard also points users at Meta's Business Manager for setting the bot's display name and profile picture. Feature set: - Inbound: text, images (with native-vision routing), voice notes (STT), documents (small text inlined, larger cached), reply context. - Outbound: text with WhatsApp-flavored markdown conversion, images, videos, documents, opus voice notes via ffmpeg with MP3 fallback. - Native interactive buttons for clarify, dangerous-command approval, and slash-command confirmation flows — matches the Telegram / Discord UX, graceful degrades to plain text. - Read receipts (blue double-checkmarks) and typing indicator, using Meta's combined endpoint so they fire in a single API call. - Webhook security: X-Hub-Signature-256 HMAC verification (raw body, constant-time), wamid deduplication, group-shaped-message refusal (groups deferred to v2 — Baileys still covers them). - Full integration with the gateway's session, cron, display-tier, prompt-hint, and auth-allowlist systems. Cloud and Baileys can run side-by-side against different phone numbers. Also wires STT (speech-to-text) through Nous's managed audio gateway for Nous subscribers — previously the default stt.provider=local required a separate faster-whisper install. New subscribers now get voice-note transcription out of the box. Docs: 418-line user guide at website/docs/user-guide/messaging/ whatsapp-cloud.md, sidebar entry, environment-variables reference, ADDING_A_PLATFORM.md updated with the optional interactive-UX contract for future adapter authors. Tests: 100 dedicated tests for the adapter, 32 for the setup wizard, 20 for the Nous subscription STT wiring, plus regression coverage across display_config, prompt_builder, and the cron scheduler. Known limitations (deferred until clear demand signal): - Group chats — use the Baileys bridge if you need them. - Message templates for 24-hour-window outside-conversation sends — reactive chat is unaffected; cron / delegate_task with gaps > 24h will fail with a clear error. The agent's system prompt warns the model about this so it knows to mention it when scheduling delayed messages.
This commit is contained in:
parent
a7cd254c29
commit
984e6cb5b8
26 changed files with 6368 additions and 287 deletions
|
|
@ -442,6 +442,7 @@ class TestBuildNousSubscriptionPrompt:
|
|||
"web": NousFeatureState("web", "Web tools", True, True, True, True, False, True, "firecrawl"),
|
||||
"image_gen": NousFeatureState("image_gen", "Image generation", True, True, True, True, False, True, "Nous Subscription"),
|
||||
"tts": NousFeatureState("tts", "OpenAI TTS", True, True, True, True, False, True, "OpenAI TTS"),
|
||||
"stt": NousFeatureState("stt", "Speech-to-text", True, True, True, True, False, True, "OpenAI Whisper"),
|
||||
"browser": NousFeatureState("browser", "Browser automation", True, True, True, True, False, True, "Browser Use"),
|
||||
"modal": NousFeatureState("modal", "Modal execution", False, True, False, False, False, True, "local"),
|
||||
},
|
||||
|
|
@ -452,7 +453,7 @@ class TestBuildNousSubscriptionPrompt:
|
|||
|
||||
assert "Browser Use" in prompt
|
||||
assert "Modal execution is optional" in prompt
|
||||
assert "do not ask the user for Firecrawl, FAL, OpenAI TTS, or Browser-Use API keys" in prompt
|
||||
assert "do not ask the user for Firecrawl, FAL, OpenAI TTS, OpenAI Whisper, or Browser-Use API keys" in prompt
|
||||
|
||||
def test_non_subscriber_prompt_includes_relevant_upgrade_guidance(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True)
|
||||
|
|
@ -466,6 +467,7 @@ class TestBuildNousSubscriptionPrompt:
|
|||
"web": NousFeatureState("web", "Web tools", True, False, False, False, False, True, ""),
|
||||
"image_gen": NousFeatureState("image_gen", "Image generation", True, False, False, False, False, True, ""),
|
||||
"tts": NousFeatureState("tts", "OpenAI TTS", True, False, False, False, False, True, ""),
|
||||
"stt": NousFeatureState("stt", "Speech-to-text", True, False, False, False, False, True, ""),
|
||||
"browser": NousFeatureState("browser", "Browser automation", True, False, False, False, False, True, ""),
|
||||
"modal": NousFeatureState("modal", "Modal execution", False, False, False, False, False, True, ""),
|
||||
},
|
||||
|
|
@ -784,6 +786,7 @@ class TestPromptBuilderConstants:
|
|||
|
||||
def test_platform_hints_known_platforms(self):
|
||||
assert "whatsapp" in PLATFORM_HINTS
|
||||
assert "whatsapp_cloud" in PLATFORM_HINTS
|
||||
assert "telegram" in PLATFORM_HINTS
|
||||
assert "discord" in PLATFORM_HINTS
|
||||
assert "cron" in PLATFORM_HINTS
|
||||
|
|
@ -791,6 +794,22 @@ class TestPromptBuilderConstants:
|
|||
assert "api_server" in PLATFORM_HINTS
|
||||
assert "webui" in PLATFORM_HINTS
|
||||
|
||||
def test_whatsapp_cloud_hint_mentions_24h_window(self):
|
||||
"""The Cloud API's 24-hour conversation window is a hard rule the
|
||||
agent should know about. Phase 5 (template fallback) was deferred,
|
||||
so the model needs to know free-form replies outside the window
|
||||
will fail with Graph error 131047 — otherwise it'll cheerfully
|
||||
try to schedule delayed messages that silently break."""
|
||||
hint = PLATFORM_HINTS["whatsapp_cloud"]
|
||||
assert "24-hour" in hint or "24h" in hint or "24 hour" in hint
|
||||
assert "131047" in hint
|
||||
|
||||
def test_whatsapp_cloud_hint_advertises_media(self):
|
||||
"""Cloud adapter supports the same MEDIA:/path/ convention as
|
||||
Baileys for outbound attachments."""
|
||||
hint = PLATFORM_HINTS["whatsapp_cloud"]
|
||||
assert "MEDIA:" in hint
|
||||
|
||||
def test_cli_hint_does_not_suggest_media_tags(self):
|
||||
# Regression: MEDIA:/path tags are intercepted only by messaging
|
||||
# gateway platforms. On the CLI they render as literal text and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue