Skip to main content

chat-sdk-python

PyPI Python Tests License: MIT

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 Card once 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
WhatsApp 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)

Source distribution for chat-sdk 0.4.41
File Size Uploaded
chat_sdk-0.4.41.tar.gz 1.9 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for chat-sdk 0.4.41
File Interpreter ABI Platform
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 log

Release 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
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page