Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# agentchat

a minimal sdk to connect your agents to slack, teams, imessage, whatsapp.
a minimal sdk to connect your agents to slack, discord, teams, imessage, whatsapp.

use any SDK (Claude Agents, OpenAI Agents, Langchain DeepAgents, Pydantic Agents) and instantly connect it to all supported channels.
use any SDK (Claude Agents, OpenAI Agents, Langchain DeepAgents, Pydantic Agents) and instantly connect it to all supported channels.

| Connect | Setup and echo example |
| --- | --- |
| Slack | [Slack guide](docs/slack.md) · [echo](examples/slack_echo.py) |
| Discord | [Discord guide](docs/discord.md) · [echo](examples/discord_echo.py) |
| Microsoft Teams | [Teams guide](docs/teams.md) · [echo](examples/teams_echo.py) |
| iMessage | [BlueBubbles guide](docs/imessage.md) · [echo](examples/imessage_echo.py) |
| WhatsApp | [Cloud API guide](docs/whatsapp.md) · [echo](examples/whatsapp_echo.py) |

The four new adapters support receiving messages and confirmed text replies.
See [shared usage, capabilities, and delivery limits](docs/providers.md) for
multiple providers or an existing agent runner.

## Quickstart

Expand Down
7 changes: 7 additions & 0 deletions docs/adding-a-channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ deduplication and failure propagation. Add a harness fixture for your provider i
assertions against it. Each harness adapts native event injection and outbound
transport inspection; the contract tests remain provider-independent.

The new gateway/HTTP adapters also run the parametrized suite in
[`test_providers.py`](../tests/contracts/test_providers.py), covering receipts,
dispatch/deduplication, absent capabilities, queue limits, and real lifecycle
cleanup. Its fixtures use helpers from `tests/support/<provider>.py`. Add a new
provider there when using the bounded background-dispatch helper; its errors
are logged asynchronously instead of escaping the event callback.

Also cover provider-specific workspace checks, DM/thread routing, subscription
rules, malformed events, pagination, socket acknowledgement, queue limits and
shutdown where applicable. Capability tests should exercise unsupported features,
Expand Down
10 changes: 10 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ channels/
Every Slack-specific module lives below `slack/`. There are no sibling Slack
compatibility modules or provider aliases in the shared namespace.

Discord, Teams, iMessage, and WhatsApp use the same package boundary with
`adapter.py`, normalization in `events.py`, and text validation in `rendering.py`.
Discord and BlueBubbles have small REST transport modules too. The provider-neutral
private helper `agentchat/_delivery.py` shares bounded background dispatch and
cleanup. Provider authentication, acknowledgement, and overflow policy stay in
each adapter. See the [provider guides](providers.md) for supported operations.

## Composition

The generic adapter boundary remains `Channel`. Within Slack, replace event
Expand Down Expand Up @@ -141,3 +148,6 @@ Root-level message and context imports are unchanged. Importing the core does
not import Slack, aiohttp or OpenAI Agents. Installation defaults are unchanged:
Slack dependencies remain installed with the base package, while OpenAI Agents
still uses the `openai` extra.

Discord, Teams, and iMessage use separate optional extras. WhatsApp uses aiohttp
from the base install. Requesting one provider does not import the others.
74 changes: 74 additions & 0 deletions docs/discord.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Discord

Connect a Discord bot through its outbound gateway. No public webhook, model API
key, or privileged Message Content intent is required for DMs and explicit mentions.

## Get the first reply

1. In the [Discord Developer Portal](https://discord.com/developers/applications),
create an application. Under **Bot**, create/reset its token and store it as
`DISCORD_BOT_TOKEN`. Use the bot token, not the client secret.
2. Under **OAuth2 → URL Generator**, select the `bot` scope and the permissions
**View Channels**, **Send Messages**, **Read Message History**, and **Send
Messages in Threads**. Open the generated URL and install the bot in a test
server you manage. Channel overrides must grant these permissions too.
3. From this repository, install and run:

```bash
python -m pip install -e '.[discord]'
export DISCORD_BOT_TOKEN='your-bot-token'
python examples/discord_echo.py
```

4. Send `hello` to the bot in a DM, or `@YourBot hello` in the test channel. The
bot should reply `hello` in the same destination. Stop with Ctrl+C.

The example echoes each accepted input. Replace its handler with your runner
after authorizing the sender. See [shared usage](providers.md) to run several
adapters or bind an existing runner directly.

## Routing and replies

- Each DM channel, guild channel, and native Discord thread has its own shared
conversation. Native threads set `message.thread_id` to the thread channel ID.
Use separate native threads when tasks need separate history.
- Guild messages require an explicit bot mention or a resolved reply to this bot.
When replying, leave Discord's reply mention enabled so the default gateway
intents receive the message content. Unmentioned thread messages are ignored;
this adapter does not automatically subscribe to thread follow-ups.
- Messages from bots, webhooks, and system events are ignored. Only this bot's
mention is removed from input text. Attachments become metadata references;
their contents are not fetched.
- Replies use a Discord message reference in the original channel/thread.
If the original message was deleted, the reply still posts there. Outbound
`@everyone`, role/user mentions, and reply-author pings are disabled.
- A reply must fit 2,000 UTF-16 code units. Oversized replies raise `ValueError`
before sending. Split longer output into separate `context.reply()` calls.

`Discord(bot_token=..., max_pending_events=64, shutdown_timeout=20)` also accepts
an owned `discord.Client` through `client=` and an owned `DiscordRESTClient`
through `send_client=`. The adapter installs the client's `on_message` callback
and closes both clients on shutdown. If supplying custom intents, retain guild,
guild-message, and DM-message events. Reading unmentioned guild content requires
enabling Message Content in both the portal and your custom client; this does not
change the adapter's mention filter.

The gateway reconnects through discord.py. Replies use a separate, single-attempt
HTTP request, including on HTTP 429 or 5xx; errors propagate. The application
owns rate-limit scheduling and recovery of uncertain sends.

## Troubleshooting and maintenance

| Symptom | Check |
| --- | --- |
| Login fails | Reset the bot token, update the environment, and restart. |
| Bot receives DMs but misses guild messages | Mention the bot and check channel permissions; the default does not read arbitrary channel content. |
| Replies fail with HTTP 403 | Grant Send Messages/Read Message History, plus Send Messages in Threads for a thread. |
| HTTP 429 | Reduce/pause outgoing sends and apply the platform's rate-limit policy in your host. |
| A thread gets no reply | The bot must have access; check archived/locked/private thread permissions. |

Local tests exercise real discord.py message parsing and a local HTTP send server.
A live bot-token round trip remains the final deployment check.

References: [discord.py gateway/intents](https://discordpy.readthedocs.io/en/stable/intents.html),
[Discord message API](https://docs.discord.com/developers/resources/message).
80 changes: 80 additions & 0 deletions docs/imessage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# iMessage through BlueBubbles

Run AgentChat anywhere that can reach your BlueBubbles server. BlueBubbles itself
must run on a Mac signed into Messages. Replies are sent as that Mac's Apple
account, so use a dedicated account/Mac for an agent that should receive every
incoming conversation. A separate bot identity is not created.

## Get the first reply

1. Follow the [BlueBubbles server setup](https://docs.bluebubbles.app/server/installation-guides)
on the Mac. Grant its required Full Disk Access and Messages automation
permissions, and confirm the Mac can send and receive iMessages manually.
Set the BlueBubbles server password and enable its HTTP API/socket service.
This adapter's basic text replies use AppleScript and do not require the
BlueBubbles Private API or disabling System Integrity Protection.
2. Use the server's root URL, such as `http://127.0.0.1:1234` when AgentChat runs
on the same Mac. Use HTTPS for a remote server. A reverse proxy must forward
`/api/v1/` and `/socket.io/`, including WebSocket upgrades. No incoming public
webhook or Firebase configuration is needed by this adapter.
3. From this repository:

```bash
python -m pip install -e '.[imessage]'
export BLUEBUBBLES_URL='http://127.0.0.1:1234'
export BLUEBUBBLES_PASSWORD='your-server-password'
export IMESSAGE_INSTANCE_ID='agent-mac'
python examples/imessage_echo.py
```

The stable instance ID keeps message/history identities unchanged if you move
the server URL. If omitted, it is derived from the URL.
4. From a different Apple account, send `hello` to the Mac's iMessage account.
Expect `hello` back. Sending from the same Apple account is ignored as an
outgoing/self message. Stop with Ctrl+C.

For a test account that should only handle one conversation, restrict it before
starting: `export IMESSAGE_ALLOWED_CHATS='iMessage;-;+15551234567'`. Obtain the
actual chat GUID from BlueBubbles' chat API; do not infer it from a phone number.
The GUID above is an example. Comma-separate multiple GUIDs. Unset this variable
to receive all iMessage chats visible to the account. Programmatic
`allowed_chat_guids=[]` disables all conversations.

## Routing and server behavior

`IMessage(server_url=..., password=..., instance_id=...)` validates REST access,
then connects to authenticated Socket.IO `new-message` events. One chat GUID is
one conversation, including group chats; each message preserves its human sender.
Only iMessage chats are handled. SMS, outgoing messages, tapbacks/reactions,
service messages, and group-management events are ignored.

Replies post to the same existing chat using `/api/v1/message/text`. The returned
message GUID is the receipt; `tempGuid` is only a correlation value, not an
idempotency guarantee. Native inline reply threads are treated as part of the
chat, and outgoing replies appear in its main timeline. There is no automatic
history backfill or replay of events missed while offline. Socket reconnection
recovers future traffic, not missed messages.

Attachment-only messages retain file references, but downloads, uploads, rich
replies, and working indicators are not implemented. See [shared capabilities](providers.md).

BlueBubbles authenticates using a password in request query parameters. The
adapter disables redirects, suppresses credential-bearing Socket.IO diagnostics,
and sanitizes REST errors. Use TLS remotely and exclude query strings from your
proxy access logs. Updating the server password requires updating the environment
and restarting the adapter.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| REST HTTP 401 | BlueBubbles server password matches `BLUEBUBBLES_PASSWORD`. |
| REST succeeds but the socket disconnects | `/socket.io/` is forwarded, the password is current, and the server is running. |
| Incoming messages appear but sends fail | Messages is signed in; BlueBubbles can automate Messages and send through AppleScript. |
| No inbound event | Test from another Apple account; check the chat allowlist and that the Mac actually received the message. |
| Works only while the Mac is awake | Keep the BlueBubbles host running and reachable. |

Local tests run real Socket.IO and REST round trips against a simulated server.
They do not exercise macOS Messages. The live echo above is the final check.

Reference: [BlueBubbles REST API and events](https://docs.bluebubbles.app/server/developer-guides/rest-api-and-webhooks).
135 changes: 135 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Run messaging adapters

Choose the setup guide for the account you want to connect. Each guide ends with
a runnable echo example; no agent framework or model key is needed for that check.

| Provider | Install from checkout | Connection | Setup |
| --- | --- | --- | --- |
| Slack | `pip install -e .` | Socket Mode | [Slack](slack.md) |
| Discord | `pip install -e '.[discord]'` | Outbound gateway | [Discord](discord.md) |
| Teams | `pip install -e '.[teams]'` | Authenticated HTTPS endpoint | [Teams](teams.md) |
| iMessage | `pip install -e '.[imessage]'` | BlueBubbles Socket.IO + REST | [iMessage](imessage.md) |
| WhatsApp | `pip install -e '.[whatsapp]'` | Signed HTTPS webhook + Cloud API | [WhatsApp](whatsapp.md) |

Use each provider's own package for imports, following the current Slack guide.
The `whatsapp` extra needs
no additional dependency because aiohttp is already installed. Provider packages
and agent-framework integrations load only when imported. Python 3.11+ is required.

## Connect your runner

After completing the provider setups, construct just the channels you need:

```python
import asyncio

from agentchat import AgentChat
from agentchat.channels.discord import Discord
from agentchat.channels.imessage import IMessage
from agentchat.channels.teams import Teams
from agentchat.channels.whatsapp import WhatsApp


async def main():
app = AgentChat(channels=[
Discord.from_env(), Teams.from_env(), IMessage.from_env(), WhatsApp.from_env(),
])

@app.on_message
async def respond(context):
# Authorize context.sender before fetching context or invoking agent tools.
return context.message.text or "Received your attachment."

try:
await app.run()
finally:
await app.close()


asyncio.run(main())
```

Install several extras together, for example `pip install -e '.[discord,teams,imessage,whatsapp]'`.
The namespaces in `Message.id` and `conversation_id` keep provider/account histories
separate. Replacing the echo handler with an agent does not change transport setup.

If your host already owns deduplication, ordering, and history, bind it directly:

```python
channel = Discord.from_env()


async def receive(channel, message):
# Authorize the sender, claim the ID, and serialize this conversation in your host.
await channel.reply(message, "Received.")


async def main():
channel.bind(receive)
try:
await channel.run()
finally:
await channel.close()
```

Call `asyncio.run(main())` in your executable. Retain the full source `Message`
when handing work to a worker: its metadata contains provider-specific reply routing.
Resolve that source from your trusted store rather than accepting destinations
or account IDs from an agent or browser input.

## Capabilities

These four new adapters implement the minimal `Channel` contract: receive,
lifecycle, and confirmed text replies. They normalize sender identity and
incoming attachment references. They do not download those attachments.

History uses the application's local `State`; default `MemoryState` is lost on
restart. Native history, working indicators, rich replies, file transfers, and
cross-surface mirroring remain optional capabilities. On the new adapters,
`set_status()` returns `False`; unsupported rich/file/mirror operations raise
`TypeError`. Slack retains its existing capabilities. Applications choose text
or link fallbacks explicitly rather than passing Slack Block Kit to another provider.

Long responses raise a validation error at the provider's documented limit;
there is no silent truncation or automatic multi-message send. If you split an
answer, track each `context.reply()` independently because earlier chunks may
have succeeded before a later send fails.

## Delivery and shutdown

Each adapter admits at most `max_pending_events=64` in-process handler tasks.
`AgentChat` deduplicates accepted message IDs and serializes handlers by
conversation using the configured `State`.

- Teams returns 202 after admission; WhatsApp returns 200. A full HTTP receiver
returns 503 before admission so the provider can apply its retry policy.
WhatsApp batches are admitted atomically.
- Discord and BlueBubbles callbacks cannot ask the gateway to replay an event.
At capacity, the adapter logs a warning and drops the excess event. Reconnects
do not provide durable catch-up. Size the limit/host for the expected workload.
- `close()` stops admission, drains handlers for up to `shutdown_timeout=20`
seconds, then cancels unfinished handlers and releases clients/listeners.
`run()` cleans up on cancellation or connection/startup failure. Instances are
intended for a single run; create a new instance after closing one.

Accepted work is not a durable queue. A crash after webhook acknowledgement can
lose a pending handler; a failed handler is logged and not automatically replayed.
Use application-owned durable ingestion/state/outboxes for stronger guarantees.
Persisting only a claim before work is durable can suppress a provider retry.

Replies make a single send attempt and return a normalized assistant message only
after obtaining a provider receipt. The core appends that confirmed result to
history. A receipt means provider acceptance, not that a human received/read it.
Timeouts may occur after a provider accepts a message; avoid blindly retrying
uncertain writes. Rate-limit scheduling and delivery reconciliation belong to the
host. Injected clients must preserve these receipt/retry semantics.

## Validate a deployment

The test suite covers provider event fixtures, Microsoft/Discord SDK behavior,
authentication rejection, local HTTP and Socket.IO round trips, import isolation,
receipts, failure handling, and cleanup. It uses no live platform credentials.
Run the guide's live echo check for each configured account before enabling your
agent, including a channel/thread check where relevant. Successful local tests
do not verify provider installation, tenant permissions, phone registration, or
macOS automation permissions.
Loading
Loading