chat-sdk-python
Multi-platform async chat SDK for Python. Port of Vercel Chat (MIT, © Vercel, Inc. — see NOTICE).
Status: 0.4.41 — synced to Vercel Chat 4.41.1 (
UPSTREAM_PARITY = "4.41.1"). See CHANGELOG.md.
Why chat-sdk?
- Write once, deploy to 9 platforms. One handler runs on Slack, Discord, Teams, Telegram, WhatsApp, Messenger, Google Chat, GitHub, and Linear.
- Built-in concurrency primitives. Deduplication, thread locking, and message queuing are handled for you.
- Cross-platform cards. Author a
Cardonce and it renders as Block Kit (Slack), Adaptive Cards (Teams), embeds (Discord), and more. - Not a replacement for platform SDKs. chat-sdk is built on top of them. You can always drop down to the native SDK when you need to.
Install
pip install chat-sdk # core only
pip install chat-sdk[slack] # + Slack adapter
pip install chat-sdk[all] # all adapters + state backends
Quick Start
from chat_sdk import Chat, Card, Button, Actions, MemoryStateAdapter
from chat_sdk.adapters.slack import create_slack_adapter
chat = Chat(
adapters={"slack": create_slack_adapter()},
state=MemoryStateAdapter(),
user_name="my-bot",
)
@chat.on_mention
async def handle_mention(thread, message):
await thread.post(
Card(title="Hello!", children=[
Actions([Button(id="hi", label="Say Hi")])
])
)
Adapters
| Platform | Install Extra | Status |
|---|---|---|
| Slack | chat-sdk[slack] |
Alpha |
| Discord | chat-sdk[discord] |
Alpha |
| Teams | chat-sdk[teams] |
Alpha |
| Telegram | chat-sdk[telegram] |
Alpha |
chat-sdk[whatsapp] |
Alpha | |
| Messenger (Meta) | chat-sdk[messenger] |
Alpha |
| Google Chat | chat-sdk[google-chat] |
Alpha |
| GitHub | chat-sdk[github] |
Alpha |
| Linear | chat-sdk[linear] |
Alpha |
AI / LLM Integration
Expose chat actions to an LLM agent as tools (chat/ai parity, vercel/chat#492):
from chat_sdk.ai import create_chat_tools, to_ai_messages
tools = create_chat_tools(chat, preset="messenger", require_approval=True)
# {"postMessage": ChatTool(description=..., input_schema={...}, execute=..., needs_approval=True, name="postMessage"), ...}
Each ChatTool is SDK-agnostic: name is the tool id (same as its dict key),
input_schema is a JSON-Schema dict you can hand to any agent runtime
(Anthropic tool use, OpenAI tools, pydantic-ai, ...), execute is the async
implementation, and needs_approval flags write tools for human-in-the-loop
gating. Presets: reader, messenger, moderator.
to_ai_messages(thread) converts thread history into model-ready messages.
Runnable demo: examples/ai_tools_example.py.
State Backends
| Backend | Install Extra |
|---|---|
| In-Memory | Built-in |
| Redis | chat-sdk[redis] |
| PostgreSQL | chat-sdk[postgres] |
PostgreSQL state
from chat_sdk.state import create_postgres_state
# Explicit url, an existing asyncpg pool (pool=...), or neither to use
# POSTGRES_URL / DATABASE_URL.
state = create_postgres_state(url="postgres://...", key_prefix="chat-sdk")
By default connect() creates the tables and indexes it needs
(CREATE ... IF NOT EXISTS). All rows are namespaced by key_prefix.
Migration-owned schema
Pass auto_create_schema=False (to create_postgres_state or
PostgresStateAdapter, with a url, an env var or a pool) when your
migrations own the schema and the runtime role has no DDL rights:
state = create_postgres_state(auto_create_schema=False) # url from POSTGRES_URL / DATABASE_URL
Table and index names are unqualified and resolve through the connection's
search_path. To keep them in a dedicated schema, set the runtime role's
default (ALTER ROLE chat_runtime SET search_path TO chat_state;), which
survives transaction-mode poolers, or pass
server_settings={"search_path": "chat_state"} to asyncpg.create_pool on a
direct connection.
Run the migration as the schema owner, with the same search_path as the
runtime connection. The statements are exported, in execution order, as
chat_sdk.state.POSTGRES_SCHEMA_STATEMENTS:
from chat_sdk.state import POSTGRES_SCHEMA_STATEMENTS
for statement in POSTGRES_SCHEMA_STATEMENTS:
await conn.execute(statement)
The same migration in SQL (the complete adapter schema; bigserial also
creates the list and queue sequences):
CREATE TABLE IF NOT EXISTS chat_state_subscriptions (
key_prefix text NOT NULL,
thread_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (key_prefix, thread_id)
);
CREATE TABLE IF NOT EXISTS chat_state_locks (
key_prefix text NOT NULL,
thread_id text NOT NULL,
token text NOT NULL,
expires_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (key_prefix, thread_id)
);
CREATE TABLE IF NOT EXISTS chat_state_cache (
key_prefix text NOT NULL,
cache_key text NOT NULL,
value text NOT NULL,
expires_at timestamptz,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (key_prefix, cache_key)
);
CREATE INDEX IF NOT EXISTS chat_state_locks_expires_idx
ON chat_state_locks (expires_at);
CREATE INDEX IF NOT EXISTS chat_state_cache_expires_idx
ON chat_state_cache (expires_at);
CREATE TABLE IF NOT EXISTS chat_state_lists (
key_prefix text NOT NULL,
list_key text NOT NULL,
seq bigserial NOT NULL,
value text NOT NULL,
expires_at timestamptz,
PRIMARY KEY (key_prefix, list_key, seq)
);
CREATE INDEX IF NOT EXISTS chat_state_lists_expires_idx
ON chat_state_lists (expires_at);
CREATE TABLE IF NOT EXISTS chat_state_queues (
key_prefix text NOT NULL,
thread_id text NOT NULL,
seq bigserial NOT NULL,
value text NOT NULL,
expires_at timestamptz NOT NULL,
PRIMARY KEY (key_prefix, thread_id, seq)
);
CREATE INDEX IF NOT EXISTS chat_state_queues_expires_idx
ON chat_state_queues (expires_at);
Grant the runtime role access to the schema, the five tables and both sequences, for example:
GRANT USAGE ON SCHEMA chat_state TO chat_runtime;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA chat_state TO chat_runtime;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA chat_state TO chat_runtime;
The role also needs database CONNECT; it needs neither schema CREATE nor
table ownership. These grants cover existing objects only.
With auto_create_schema=False, connect() runs SELECT 1 and then one
read-only query, and never issues DDL. The query checks that all five tables
exist and that the current role holds what the adapter uses: SELECT,
INSERT and DELETE on every table, UPDATE on the locks, cache and lists
tables, and nextval on the list and queue sequences (USAGE or UPDATE;
identity seq columns need no sequence grant). Column-level SELECT /
INSERT / UPDATE grants on the columns the adapter uses are accepted too.
If anything is missing, connect() raises chat_sdk.StateSchemaError
("PostgreSQL state schema is not ready: ...") naming the problem, so a wrong
search_path or a forgotten grant fails at startup instead of on the first
message. A missing table is reported as the first relation PostgreSQL cannot
resolve; missing grants are all named together. Your migrations also own
future adapter schema changes; the CHANGELOG lists them. A pool you pass in
stays open after disconnect(); a pool the adapter created from a URL is
closed, and is also closed as soon as a connect() attempt fails, so
retrying does not pile up open connections.
Compared to Alternatives
| Feature | chat-sdk | Raw platform SDKs | BotFramework SDK |
|---|---|---|---|
| Multi-platform from one codebase | 9 platforms | 1 per SDK | Teams + limited |
| Async-native (Python 3.12+) | Yes | Varies | No |
| Cross-platform cards | Card model | Platform-specific | Adaptive Cards only |
| Thread locking / dedup | Built-in | DIY | DIY |
| State abstraction (mem/redis/pg) | Built-in | DIY | DIY |
| Drop down to native SDK | Yes | N/A | Partially |
Documentation
| Document | Description |
|---|---|
| Architecture | Module dependency graph, adapter protocol, card system, concurrency strategies, state backends, markdown pipeline, streaming pipeline |
| Upstream Sync | How to keep the Python port in sync with the Vercel Chat TS SDK, translation patterns, known footguns |
| Security | Webhook verification per platform, SSRF protections, crypto details, known limitations, production audit checklist |
| Testing | Test categories, how to run tests, how to add adapter tests, coverage gaps, dispatch key validation |
| Design Decisions | Rationale for key architectural choices (hand-rolled parser, PascalCase builders, global singleton, zero deps) |
| Contributing | Dev setup, code quality, PR expectations |
| Changelog | Release history |
Development
uv sync --group dev
uv run pytest tests/
uv run ruff check src/
License
MIT
Metadata
Release files for chat-sdk 0.4.41
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| chat_sdk-0.4.41.tar.gz | 1.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| chat_sdk-0.4.41-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.7 MB
Release files / chat_sdk-0.4.41.tar.gz
| Download URL | chat_sdk-0.4.41.tar.gz |
|---|---|
| Size | 1.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
410f01fbc1b670008b45426958cb050f29933d6df0de66c738ff76a2c129a51b
|
|
BLAKE2b-256 checksum How to use checksums |
f6840ab57c16496061ff20844c941ccf9566a88336bf7efd63780563a5e7af7e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / chat_sdk-0.4.41-py3-none-any.whl
| Download URL | chat_sdk-0.4.41-py3-none-any.whl |
|---|---|
| Size | 770.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d1c01f77e7c741153d38f5effd7e466c3216a2b87fbfd5b6481282746cb3ce44
|
|
BLAKE2b-256 checksum How to use checksums |
77ba03eb889d7ac4098ea1dd3ce5816967f38337a913191b9a4f26df5a0c3f34
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log