Skip to content

feat(ai): add create_chat_tools tool factory (PR 2 of 3)#122

Merged
patrick-chinchill merged 5 commits into
claude/port-chat-ai-module-movefrom
claude/port-chat-ai-tools
May 30, 2026
Merged

feat(ai): add create_chat_tools tool factory (PR 2 of 3)#122
patrick-chinchill merged 5 commits into
claude/port-chat-ai-module-movefrom
claude/port-chat-ai-tools

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

PR 2 of 3 — chat/ai subpath port (vercel/chat#492)

Stacked on top of #116 (claude/port-chat-ai-module-move), which did the structural module-to-package move only. This PR adds the create_chat_tools tool factory in the new chat_sdk.ai package. PR 3 will wire these tools into the existing handler / consumer paths.

GitHub will automatically retarget this PR to main once #116 merges.

Refs tracking issue #98 (overall 4.29 sync) and design issue #109 (chat-ai port design).

What's ported from vercel/chat#492

  • packages/chat/src/ai/tools.ts (createChatTools orchestrator)
  • packages/chat/src/ai/types.ts (ChatBinding, ToolOptions, ToolOverrides)
  • packages/chat/src/ai/tools/channels.tsget_channel_info
  • packages/chat/src/ai/tools/messages.tspost_message, post_channel_message, send_direct_message, edit_message, delete_message
  • packages/chat/src/ai/tools/reactions.tsadd_reaction, remove_reaction
  • packages/chat/src/ai/tools/threads.tsfetch_messages, fetch_channel_messages, fetch_thread, list_threads, get_thread_participants, subscribe_thread, unsubscribe_thread, start_typing
  • packages/chat/src/ai/tools/users.tsget_user
  • Tests mirroring packages/chat/src/ai/index.test.ts

Public API

from chat_sdk.ai import create_chat_tools

tools = create_chat_tools(
    chat=chat,
    preset="messenger",                 # "reader" | "messenger" | "moderator" | list
    require_approval=True,              # bool or per-tool dict
    overrides={"postMessage": {"description": "..."}},
)
# tools is dict[str, ChatTool] keyed by camelCase upstream tool names.

Each tool is a ChatTool dataclass:

@dataclass
class ChatTool:
    description: str
    input_schema: dict[str, Any]          # JSON-Schema shape
    execute: Callable[[dict], Awaitable]  # async
    needs_approval: bool | None = None
    extras: dict[str, Any] = field(default_factory=dict)

Individual factories (post_message, add_reaction, ...) are also exported so callers can cherry-pick — matching upstream's per-symbol exports.

Python idiom notes / divergences

  • Upstream depends on the Vercel AI SDK (ai) tool() helper and zod schemas. Python has no canonical agent runtime; rather than add a third-party schema validator as a hard dependency, each tool factory returns a plain ChatTool dataclass holding a JSON-Schema-shaped input_schema dict. Consumers that bind these into an actual agent runtime translate that dict to their schema layer. No new runtime deps added.
  • Tool keys use upstream's camelCase names (postMessage, fetchChannelMessages, ...) so consumers that mix this with the JS SDK across language boundaries see the same tool ids.
  • The protected-field set covers both snake_case and camelCase names (input_schema + inputSchema, etc.) so callers porting upstream overrides dicts get the same protection.
  • ToolOverrides is a plain dict[str, Any] rather than a TypedDict; the upstream type is a Partial<Pick<Tool, ...>> over fields from the Vercel AI SDK package that don't have direct Python equivalents.

Out of scope (PR 3)

  • Wiring create_chat_tools into existing handlers / consumer code paths.
  • Docs site updates (the upstream PR shipped apps/docs/content/docs/ai/*).
  • Example app integration.

Files

  • src/chat_sdk/ai/tools.py (new)
  • src/chat_sdk/ai/__init__.py (re-exports added)
  • tests/test_ai_tools.py (new, 38 tests)

Validation

uv run ruff check src/ tests/                  -> All checks passed
uv run ruff format --check src/ tests/         -> 198 files already formatted
uv run python scripts/audit_test_quality.py    -> 0 hard failures
uv run pytest tests/ -q                        -> 4081 passed, 3 skipped

Baseline (from #116) was 4043 passed, 3 skipped — this PR adds exactly the 38 new tests.

Load-bearing verification: each test was confirmed to fail when its corresponding production code is reverted (spot-checked by breaking _resolve_approval and watching TestRequireApproval::test_per_tool_approval_overrides fail).

🤖 Generated with Claude Code via claude-code-sdk-python


Generated by Claude Code

…#492)

Port of `packages/chat/src/ai/tools.ts` (plus the supporting
`tools/{channels,messages,reactions,threads,users}.ts` and `types.ts`)
introduced by vercel/chat#492. Builds on top of #116, which moved
`chat_sdk.ai` from a single module to a package so `tools.py` has a home.

`create_chat_tools(chat, preset=, require_approval=, overrides=)` returns
a mapping of tool-name -> `ChatTool` dataclass holding a description,
JSON-Schema-shaped `input_schema`, an async `execute` callable, and the
`needs_approval` flag. Three presets (`reader` / `messenger` /
`moderator`) and per-tool overrides match the upstream API surface
verbatim. Individual factories (`post_message`, `add_reaction`, ...) are
also exported so callers can cherry-pick.

PR 3 will wire these tools into the existing handler paths; this PR
adds the surface only.

Validation:
  uv run ruff check src/ tests/                  -> clean
  uv run ruff format --check src/ tests/         -> clean
  uv run python scripts/audit_test_quality.py    -> 0 hard failures
  uv run pytest tests/ -q                        -> 4081 passed, 3 skipped

Refs #98, #109.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 08a3527c-0e84-497a-ad17-e93f274f36bc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request ports the TypeScript Chat SDK tool factory to Python, introducing the ChatTool class and the create_chat_tools orchestrator to expose chat operations to AI agents, supported by a comprehensive test suite. The review feedback suggests improving error handling by catching ChatNotImplementedError when invoking optional adapter methods (such as fetch_channel_messages and list_threads) and raising a clean ChatError. Additionally, it recommends a more robust type check for the preset parameter by checking if it is a string rather than a list.

Comment thread src/chat_sdk/ai/tools.py Outdated
from typing import Any, Literal

from chat_sdk.chat import Chat
from chat_sdk.errors import ChatError
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import ChatNotImplementedError from chat_sdk.errors so that we can catch it when calling optional adapter methods directly.

Suggested change
from chat_sdk.errors import ChatError
from chat_sdk.errors import ChatError, ChatNotImplementedError

Comment thread src/chat_sdk/ai/tools.py Outdated
Comment on lines +691 to +694
cursor = args.get("cursor")
direction = args.get("direction", "backward")
result = await fetch_method(
channel_id,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since fetch_channel_messages is an optional adapter method defined on BaseAdapter (which raises ChatNotImplementedError by default), calling it directly will raise ChatNotImplementedError instead of the expected ChatError if the adapter does not implement it. Wrap the call in a try...except ChatNotImplementedError block to raise ChatError with a clean message, matching the pattern used in Chat.get_user. Ensure that the await statement is placed inside the try block, and always accompany the fix with a regression test that verifies the exception is correctly logged and propagated.

        try:
            result = await fetch_method(
                channel_id,
                FetchOptions(limit=limit, cursor=cursor, direction=direction),
            )
        except ChatNotImplementedError as exc:
            raise ChatError(f'Adapter "{adapter_name}" does not support fetching channel messages') from exc
References
  1. When handling exceptions for asynchronous operations, ensure that the await statement is placed inside the try block so that exceptions are caught during execution, and always accompany the fix with a regression test that verifies the exception is correctly logged and propagated.

Comment thread src/chat_sdk/ai/tools.py Outdated

limit = args.get("limit", 20)
cursor = args.get("cursor")
result = await list_method(channel_id, ListThreadsOptions(limit=limit, cursor=cursor))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since list_threads is an optional adapter method defined on BaseAdapter (which raises ChatNotImplementedError by default), calling it directly will raise ChatNotImplementedError instead of the expected ChatError if the adapter does not implement it. Wrap the call in a try...except ChatNotImplementedError block to raise ChatError with a clean message, matching the pattern used in Chat.get_user. Ensure that the await statement is placed inside the try block, and always accompany the fix with a regression test that verifies the exception is correctly logged and propagated.

        try:
            result = await list_method(channel_id, ListThreadsOptions(limit=limit, cursor=cursor))
        except ChatNotImplementedError as exc:
            raise ChatError(f'Adapter "{adapter_name}" does not support listing threads') from exc
References
  1. When handling exceptions for asynchronous operations, ensure that the await statement is placed inside the try block so that exceptions are caught during execution, and always accompany the fix with a regression test that verifies the exception is correctly logged and propagated.

Comment thread src/chat_sdk/ai/tools.py Outdated


def _resolve_preset_tools(preset: ChatToolPreset | list[ChatToolPreset]) -> set[str]:
presets: list[str] = list(preset) if isinstance(preset, list) else [preset]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using isinstance(preset, list) to check if preset is a collection of presets is fragile. If a user passes a tuple or set of presets, it will fail or behave unexpectedly because it will treat the entire collection as a single preset name. Checking isinstance(preset, str) to distinguish a single string from any other iterable collection is much more robust and idiomatic in Python.

presets: list[str] = [preset] if isinstance(preset, str) else list(preset)

claude and others added 2 commits May 29, 2026 20:59
…emini review)

Four small follow-ups to PR 2 of the chat-ai port from Gemini's review:

- Import `ChatNotImplementedError` alongside `ChatError`.
- Wrap `fetch_channel_messages` invocation in `try/except
  ChatNotImplementedError` and re-raise as `ChatError` (preserves cause).
  The early `getattr(adapter, "fetch_channel_messages", None) is None`
  branch only catches missing attributes; `BaseAdapter` exposes the
  attribute as a stub that itself raises `ChatNotImplementedError`, so
  without this wrap the tool surfaces a different exception type than
  callers expect. Matches the `Chat.get_user` pattern.
- Same wrap for `list_threads`.
- `_resolve_preset_tools` now distinguishes a single `str` preset from any
  iterable of presets via `isinstance(preset, str)` rather than
  `isinstance(preset, list)`, so tuples/sets/Sequences work correctly.

Two new regression tests (`test_fetch_channel_messages_wraps_not_implemented`,
`test_list_threads_wraps_not_implemented`) drive an `AsyncMock` whose
`side_effect` is `ChatNotImplementedError`, assert the surfaced exception is
`ChatError` with the expected message, and pin the cause chain
(`__cause__ is ChatNotImplementedError`).

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
@patrick-chinchill patrick-chinchill marked this pull request as ready for review May 30, 2026 00:11
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a20605432

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/chat_sdk/ai/tools.py Outdated
limit = args.get("limit", 20)
cursor = args.get("cursor")
try:
result = await list_method(channel_id, ListThreadsOptions(limit=limit, cursor=cursor))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass list thread options as a keyword

When the tool is used with the built-in MockAdapter (a common downstream test adapter), listThreads raises TypeError before returning anything because MockAdapter.list_threads is defined as list_threads(self, channel_id, **kwargs), so this second positional argument is rejected. Passing the options as options=... (or otherwise accommodating keyword-style adapters) keeps the production adapters working while making the new tool usable with the SDK’s own mock adapter.

Useful? React with 👍 / 👎.

claude added 2 commits May 30, 2026 00:19
`MockAdapter.list_threads` is declared as
`list_threads(self, channel_id, **kwargs)`, so the tool's positional
`list_method(channel_id, ListThreadsOptions(...))` raised `TypeError` for
any consumer wiring `create_chat_tools` into MockAdapter (the SDK's own
mock). Production adapters accept the kwarg form just as readily as the
positional form, so the change is universally safe.

The existing `test_list_threads_projects_summaries` used
`AsyncMock(return_value=...)` which masked the TypeError by replacing the
real `MockAdapter.list_threads` entirely. New
`test_list_threads_uses_keyword_options` exercises the real MockAdapter
and asserts the default empty result — fails on the old positional call
with `TypeError`, passes after.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
Copy link
Copy Markdown
Collaborator Author

@codex review


Generated by Claude Code

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@patrick-chinchill patrick-chinchill merged commit bf66edb into claude/port-chat-ai-module-move May 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants