Series 4 — Part 6 of 8

The same AI core — system prompt, LLM, RAG — needs to work across WhatsApp, a website widget, and a mobile app. The channel adapter pattern isolates channel-specific handling (payload format, delivery mechanism, rate limits) from the shared intelligence core.

The Adapter Pattern for Channels

class BaseChannelAdapter:
    def parse_inbound(self, payload: dict) -> InboundMessage:
        raise NotImplementedError

    def send_text(self, recipient_id: str, text: str) -> None:
        raise NotImplementedError

    def send_audio(self, recipient_id: str, audio_bytes: bytes) -> None:
        pass  # not all channels support audio

class WhatsAppAdapter(BaseChannelAdapter):
    def parse_inbound(self, payload: dict) -> InboundMessage:
        entry   = payload['entry'][0]
        change  = entry['changes'][0]
        msg     = change['value']['messages'][0]
        return InboundMessage(
            channel_id = msg['from'],
            text       = msg.get('text', {}).get('body', ''),
            msg_id     = msg['id'],
            msg_type   = msg['type'],
        )

    def send_text(self, recipient_id: str, text: str) -> None:
        meta_api.send_text_message(recipient_id, text)

class WidgetAdapter(BaseChannelAdapter):
    def parse_inbound(self, payload: dict) -> InboundMessage:
        return InboundMessage(
            channel_id = payload['session_id'],
            text       = payload['message'],
            msg_id     = payload['msg_id'],
            msg_type   = 'text',
        )

    def send_text(self, recipient_id: str, text: str) -> None:
        websocket_manager.send(recipient_id, {"type": "text", "content": text})

WhatsApp Webhook → Task Queue → Send

# Webhook handler (FastAPI)
@app.post("/webhook/whatsapp/{client_slug}")
async def whatsapp_webhook(client_slug: str, request: Request):
    if not verify_hmac(request):
        raise HTTPException(status_code=403)

    payload = await request.json()
    adapter = WhatsAppAdapter()
    msg     = adapter.parse_inbound(payload)

    if is_duplicate(msg.msg_id, db):
        return {"status": "duplicate"}

    client = get_client_by_slug(client_slug, db)
    process_message.delay(client.id, msg.channel_id, msg.text, msg.msg_id, "whatsapp")
    return {"status": "accepted"}

Website Widget: JS Embed

The widget is a JS snippet that creates a chat frame. The frame connects via polling (simpler) or WebSocket (lower latency). For most clients, polling every 2 seconds is sufficient and avoids the complexity of persistent WebSocket connections through Nginx.

// Widget JS (served from your CDN)
const chat = new the chatbot platformWidget({
  clientId: "acme",
  endpoint: "https://api.the chatbot platform.com/widget",
  pollingInterval: 2000,
});

What to Watch For

  • Rate limits differ by channel — WhatsApp Business API limits template messages (1,000/day on free tier). Widget has no limit. Rate limiting at the Nginx level per channel prevents one channel from starving another.
  • Message type handling — WhatsApp sends images, audio, documents, locations. Parse the type field before accessing text.body, or your handler will throw a KeyError on every non-text message.
  • Channel-specific session scope — A WhatsApp conversation and a widget conversation from the same company are different sessions. Do not merge context across channels without an explicit identity resolution step.