237 lines
12 KiB
Markdown
237 lines
12 KiB
Markdown
# @copilotkit/channels-whatsapp
|
||
|
||
The **WhatsApp `PlatformAdapter`** for [`@copilotkit/channels`](../channels). It connects a
|
||
WhatsApp Business number to any AG-UI agent: ingress via the Meta Cloud API webhook,
|
||
egress as text or interactive messages rendered from the `@copilotkit/channels-ui` JSX
|
||
vocabulary, opaque-id interactions, and HITL.
|
||
|
||
You write your UI as JSX once (`@copilotkit/channels-ui`) and drive the bot with
|
||
`@copilotkit/channels`; this package is the only one that talks to the WhatsApp Cloud API.
|
||
|
||
## Install
|
||
|
||
```sh
|
||
pnpm add @copilotkit/channels @copilotkit/channels-whatsapp
|
||
```
|
||
|
||
## Quickstart
|
||
|
||
```ts
|
||
import { createBot } from "@copilotkit/channels";
|
||
import {
|
||
whatsapp,
|
||
defaultWhatsAppContext,
|
||
} from "@copilotkit/channels-whatsapp";
|
||
|
||
const bot = createBot({
|
||
adapters: [
|
||
whatsapp({
|
||
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
|
||
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
|
||
appSecret: process.env.WHATSAPP_APP_SECRET!,
|
||
verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
|
||
port: 3000,
|
||
}),
|
||
],
|
||
agent: makeAgent(process.env.AGENT_URL!),
|
||
tools: [...appTools],
|
||
context: [...defaultWhatsAppContext, ...appContext],
|
||
});
|
||
|
||
// Every inbound text is for the bot — there is no @-mention concept on WhatsApp.
|
||
bot.onMessage(async ({ thread }) => {
|
||
await thread.runAgent();
|
||
});
|
||
|
||
await bot.start();
|
||
console.log("[whatsapp-bot] listening for webhooks");
|
||
```
|
||
|
||
`whatsapp(opts)` returns a `WhatsAppAdapter`. It starts an HTTP server on `port`
|
||
(default 3000) that handles the Meta webhook: a `GET /webhook` verification
|
||
handshake and signed `POST /webhook` event delivery. You must expose this port
|
||
publicly (e.g. via ngrok) and register the URL + `verifyToken` in the Meta app
|
||
configuration. See [`examples/whatsapp`](../../examples/whatsapp) for a complete
|
||
setup walkthrough.
|
||
|
||
### Required env
|
||
|
||
| Var | Purpose |
|
||
| -------------------------- | ----------------------------------------------------------- |
|
||
| `WHATSAPP_ACCESS_TOKEN` | Cloud API access token (Bearer), from Meta App → API setup. |
|
||
| `WHATSAPP_PHONE_NUMBER_ID` | Business phone-number id that sends messages. |
|
||
| `WHATSAPP_APP_SECRET` | App secret for `X-Hub-Signature-256` webhook validation. |
|
||
| `WHATSAPP_VERIFY_TOKEN` | Token echoed during the GET verification handshake. |
|
||
|
||
## Capabilities
|
||
|
||
| Capability | Supported | Notes |
|
||
| ------------------- | --------- | -------------------------------------------------------------- |
|
||
| `supportsStreaming` | false | WhatsApp messages are immutable; there is no edit-message API. |
|
||
| `supportsModals` | false | No modal surface in the Cloud API. |
|
||
| `supportsTyping` | false | No typing-indicator API for business accounts. |
|
||
| `supportsReactions` | false | No reaction API for business-sent messages. |
|
||
|
||
Because messages are immutable, `thread.stream(...)` buffers the full iterable
|
||
and sends it as a single message — there is no token-by-token streaming. Calls to
|
||
`update` and `delete` are also no-ops (they post a new message instead, or silently
|
||
drop). The `defaultWhatsAppContext` entry tells the agent about this constraint so
|
||
it doesn't promise to "update this message."
|
||
|
||
## `WhatsAppAdapterOptions` reference
|
||
|
||
| Option | Type | Default | Description |
|
||
| --------------------- | --------------------- | ------------------------------ | ------------------------------------------------------------------- |
|
||
| `accessToken` | `string` | required | Cloud API access token (Bearer). |
|
||
| `phoneNumberId` | `string` | required | Business phone-number id that sends messages. |
|
||
| `appSecret` | `string` | required | App secret for `X-Hub-Signature-256` webhook validation. |
|
||
| `verifyToken` | `string` | required | Token echoed during the GET verification handshake. |
|
||
| `port` | `number` | `3000` | HTTP server port. |
|
||
| `path` | `string` | `"/webhook"` | Webhook path. |
|
||
| `apiVersion` | `string` | `"v21.0"` | Graph API version. |
|
||
| `graphBaseUrl` | `string` | `"https://graph.facebook.com"` | Graph API base origin. Overridable for tests. |
|
||
| `interruptEventNames` | `ReadonlySet<string>` | `undefined` | Custom AG-UI event names treated as interrupts by the run renderer. |
|
||
| `commandPrefix` | `string` | `"/"` | Prefix for leading-keyword command matching. |
|
||
| `historyStore` | `HistoryStore` | `new InMemoryHistoryStore()` | Pluggable conversation-history persistence. |
|
||
| `files` | `FileDeliveryConfig` | `{}` | Inbound media handling configuration. |
|
||
|
||
## JSX → WhatsApp rendering
|
||
|
||
`renderWhatsAppMessage(ir)` lowers the `@copilotkit/channels-ui` IR to Cloud API
|
||
payloads. The strategy:
|
||
|
||
- **0 actions** → plain `text` message (markdown converted to WhatsApp formatting).
|
||
- **1–3 button actions** → interactive `button` message (reply buttons).
|
||
- **4–10 actions** → interactive `list` message (list picker).
|
||
- **>10 actions** → numbered text menu (degraded fallback).
|
||
|
||
Image nodes always emit their own `image` payload. Markdown is translated to
|
||
WhatsApp formatting: `**bold**`, `_italic_`, `~~strikethrough~~`, `` `code` ``, and
|
||
code blocks. Headings, tables, and clickable Markdown links are not supported on
|
||
WhatsApp — links render as plain text.
|
||
|
||
### Per-element budget
|
||
|
||
WhatsApp caps interactive elements. Limits live in `WA_LIMITS`:
|
||
|
||
| Limit | Value | Element |
|
||
| ------------------- | ----- | ------------------------------------------------ |
|
||
| `bodyText` | 4096 | text message body chars |
|
||
| `replyButtons` | 3 | reply buttons in an interactive button message |
|
||
| `buttonTitle` | 20 | reply-button title chars |
|
||
| `interactiveBody` | 1024 | interactive message body chars |
|
||
| `interactiveHeader` | 60 | interactive header chars |
|
||
| `interactiveFooter` | 60 | interactive footer chars |
|
||
| `listRows` | 10 | total rows across all sections in a list message |
|
||
| `rowTitle` | 24 | list-row title chars |
|
||
| `rowDescription` | 72 | list-row description chars |
|
||
| `listButton` | 20 | list open-button label chars |
|
||
| `controlId` | 256 | interactive control id chars |
|
||
|
||
## Persistence
|
||
|
||
### ActionStore (interaction rehydration)
|
||
|
||
The engine's `ActionStore` (from `@copilotkit/channels`) stores the minted opaque ids
|
||
that power `Button` / `Select` click handlers. By default it is in-memory: after a
|
||
process restart, clicks on old interactive messages are acknowledged but ignored.
|
||
For persistent interactions, pass a durable `ActionStore` to
|
||
`createBot({ actionStore })`.
|
||
|
||
### HistoryStore (conversation memory)
|
||
|
||
Unlike Slack, WhatsApp exposes no readable message history. The adapter maintains
|
||
its own `HistoryStore` and replays it into `agent.messages` on every turn. The
|
||
default is `InMemoryHistoryStore` (up to 100 messages per conversation, drops
|
||
oldest). Swap a durable backend by implementing the `HistoryStore` interface:
|
||
|
||
```ts
|
||
interface HistoryStore {
|
||
append(conversationKey: string, message: StoredMessage): Promise<void>;
|
||
read(conversationKey: string): Promise<StoredMessage[]>;
|
||
}
|
||
```
|
||
|
||
Pass it as `historyStore` in the adapter options:
|
||
|
||
```ts
|
||
whatsapp({
|
||
// ...
|
||
historyStore: new MyRedisHistoryStore(),
|
||
});
|
||
```
|
||
|
||
Without a durable `HistoryStore`, conversation history is lost on process restart.
|
||
|
||
## Commands
|
||
|
||
Commands are matched by a leading keyword in the message text (default prefix `/`).
|
||
Register handlers with `bot.onCommand`:
|
||
|
||
```ts
|
||
bot.onCommand("status", async ({ thread, text }) => {
|
||
await thread.runAgent({ prompt: `Status check: ${text}` });
|
||
});
|
||
```
|
||
|
||
Unlike Slack, WhatsApp has no native slash-command surface — commands are plain
|
||
text messages that start with the prefix. They are NOT pre-filtered by the adapter
|
||
(the engine matches them), and command messages are not persisted to the
|
||
`HistoryStore` at ingress. Sent commands need to be serialized into the agent prompt
|
||
explicitly if the agent needs to see them as history.
|
||
|
||
## Built-ins
|
||
|
||
- `defaultWhatsAppTools` — empty in v1 (WhatsApp exposes no user directory, so
|
||
there is no `lookup_user` equivalent). Spread into `tools` for future
|
||
compatibility.
|
||
- `defaultWhatsAppContext` — two context entries: WhatsApp formatting rules
|
||
(bold/italic/code, no headings or clickable links) and delivery constraints (no
|
||
streaming, no message editing). Spread into `context`.
|
||
- `whatsAppFormattingContext` / `whatsAppDeliveryContext` — the individual entries
|
||
if you need to compose them selectively.
|
||
|
||
## Tool context
|
||
|
||
Tools receive the single shared `BotToolContext` from `@copilotkit/channels`
|
||
(`{ thread, message?, user?, signal?, platform }`) and reach WhatsApp power through
|
||
capability-gated `thread` methods this adapter backs:
|
||
|
||
- `thread.getMessages()` — the current conversation's message history (from
|
||
`HistoryStore`), each a `ThreadMessage` (`{ user?, text, ts?, isBot? }`).
|
||
- `thread.postFile({ bytes, filename, title?, altText? })` — upload and send a
|
||
file (image → `image` payload; other → `document` payload via the media-upload
|
||
API).
|
||
|
||
Note: `thread.lookupUser(query)` is a no-op on WhatsApp — the Cloud API exposes no
|
||
user directory. It always returns `undefined`.
|
||
|
||
## Running the demo
|
||
|
||
This package is the **library**. A runnable end-to-end demo wiring everything
|
||
against a real WhatsApp number lives in
|
||
[`examples/whatsapp`](../../examples/whatsapp).
|
||
|
||
## What's NOT in v1
|
||
|
||
- No message editing or streaming (WhatsApp messages are immutable)
|
||
- No proactive messaging outside the 24-hour customer-service window — the adapter
|
||
does not implement template-message sending; the bot can only reply within the
|
||
24-hour window opened by an inbound user message
|
||
- No user directory (`lookupUser` always returns `undefined`)
|
||
- No OAuth / multi-number install (single access token only)
|
||
- Durable `ActionStore` and `HistoryStore` are in-memory by default; actions and
|
||
history expire on restart unless you provide durable implementations
|
||
|
||
## Exports
|
||
|
||
`whatsapp`, `WhatsAppAdapter`; `WhatsAppAdapterOptions`, `ReplyTarget`,
|
||
`WhatsAppMessageRef` (types); `WhatsAppConversationStore`;
|
||
`InMemoryHistoryStore`, `HistoryStore`, `StoredMessage` (types);
|
||
`renderWhatsAppMessage`, `WhatsAppOutbound` (type); `WA_LIMITS`, `truncateText`,
|
||
`clampArray`; `markdownToWhatsApp`; `decodeInteraction`, `conversationKeyOf`;
|
||
`createRunRenderer`; `WhatsAppClient`, `DownloadedMedia` (type);
|
||
`buildFileContentParts`, `AgentContentPart`, `FileDeliveryConfig` (types);
|
||
`defaultWhatsAppTools`; `defaultWhatsAppContext`, `whatsAppFormattingContext`,
|
||
`whatsAppDeliveryContext`.
|