docs(ai): document the AI chat API - #572
Open
Devon-White wants to merge 19 commits into
Open
Conversation
Adds the AI chat endpoint to the REST reference and the guide and SDK reference pages around it (closes #569). The endpoint is JSON-RPC over one path, so it follows the Calling API's RPC-over-HTTP pattern: a single POST whose request body is a union of per-method models discriminated on `method`, with one titled example per method. New `ai-api` family, since `/api/ai` is a prefix no existing family owns. - specs/signalwire-rest/ai-api: six methods, their results, the JSON-RPC error envelope, and the front door's non-JSON-RPC failures (401, and a 502 when the chat service is unreachable). Requires the `chat` scope. - /ai/chat: how-to for both paths — direct from your server, and through a ChatGateway for browsers — plus the operational behavior that bites (errors under HTTP 200, keepalive whitespace, one turn at a time, idle timeouts, microseconds vs seconds, greeting costs a turn). - Python `ai_chat` reference: AIChatClient and ChatGateway. Webhooks are deliberately not modeled here. A chat conversation delivers the same SWAIG and post-prompt payloads a voice session does, with different values, so the guide links the canonical pages instead of forking near-identical models.
Contributor
Webhooks moved to their own top-level section with an AI channel, so the chat guide's links move with them, and the post-prompt payload now has a page to point at instead of prose. Widens the two AI webhook fields chat actually changes: conversation_type names chat alongside voice, and call_id notes that a chat conversation carries its conversation id there. The Bedrock variants stay voice-only. The AI SWAIG payload has no conversation_type field to widen — see the follow-up in the PR. Also brings /ai/chat in line with two guide rules that landed with the reorg: it uses the AI cluster's shared cast (Ada, dispatcher for Bayview Taxi) rather than a second unrelated example, and it ends with a way to confirm the result — the expected transcript, plus the four failures that actually happen and what each one means.
…e/docs into Devon/ai-chat-api-docs
Three things in the issue had no home anywhere in the docs: - The structured tool reply. A chat tool can answer with an object — `tool_result` for what the model learns, `tool_prompt` for what it should say — instead of a `response` string. Documented as chat-only on purpose: `FunctionResult.set_tool_response` builds it, but the voice engine reads `response` with cJSON_GetObjectCstr (actions.c:2230), which returns null for an object, so a call takes the string form only. That is why this goes in the chat guide rather than widening the AI SWAIG webhook page. - `swaig_post_conversation`, which puts the transcript in every tool webhook. Canonically owned by the SWML params reference, so this is a pointer, not a second treatment. - Where to get an agent in the first place. Before you start now sends a reader with no agent to the tool-calling guide. Also corrects the key-rotation claim: revoking a publishable key is a server-side redeploy, but the page still needs the new key, so serve it rather than hard-coding it.
…load The voice engine has always sent it: get_call_info() stamps conversation_type on every AI webhook body (ai_utils.c:198-218), and the SWAIG function webhook goes through it (actions.c:2040-2070), which is why the Bedrock payload documents the field and this one did not. Documented as `voice` | `chat`, so it covers a call and a text conversation without a second model.
Regenerated openapi.yaml rather than resolving it by hand — the conflict
was entirely in generated output, and the regenerated file now differs
from main only by this branch's own additions.
The webhook-operations refactor landed three things this branch had to
answer to:
- Voice AI *does* accept the `{tool_result, tool_prompt}` object form
(actions.c:2404-2425 parses it). An earlier commit here claimed the
opposite on the strength of a truncated grep. The guide no longer says
a call takes the string form only, and no longer restates the reply
contract the AI SWAIG webhook page now owns.
- `post_prompt_data` now documents `parsed`, `raw` and `substituted`, so
the guide's "not parsed JSON" trap is gone rather than left to rot.
- The new shared-contract rule applies to the transcript: `chat_log`
entries are the same contract as the AI webhooks' `call_log` entries,
so this drops its own ChatLogMessage and references
Webhooks.AI.AICallLogEntry. `call_timeline` stays open, with a comment
saying why. Widened that shared model's `role` doc too, since
`system-log` reaches both pathways (conversation.c:151).
Two places named their values in prose while typing the field loosely: - The JSON-RPC error code was `int32` with a markdown table on the operation. It is now `ChatErrorCode`, a 14-member enum, so the valid codes appear in the schema and a generated client can switch on them. - `conversation_type` on the AI SWAIG and post-prompt payloads was `string` with "`voice` for a call, `chat` for a text conversation" in the description. Both are now `"voice" | "chat"`, matching how `action` and `reason` are already typed in the same directory. The code meanings stay in the enum's own description on purpose: the emitter drops per-enum-value docs, and unlike a self-describing string enum such as CallResponseStatus, `-32002` says nothing on its own. The per-member @doc entries are kept for anyone reading the source, with a comment explaining why they are duplicated.
The fourteen codes were duplicated in the enum's own description, which
made the spec the de facto reference for them — and left a reader who
starts at "Error codes" unable to find them at all.
They now live in one place, in an AI chat section on that page, alongside
the two things that make these errors different from every other endpoint's:
they arrive under HTTP 200, and the code is numeric. The enum keeps the
closed value set and points there, and the operation and guide point there
too.
Heading is "AI chat errors" rather than naming JSON-RPC, because
`fern check` does not validate anchor fragments and no page in the repo
links an anchor derived from a hyphenated acronym — so the slug for
"JSON-RPC" would have been a guess. Plain words match the verified
pattern ("Error response body" -> #error-response-body).
They were a markdown table in a section of their own, which read as a sidebar next to the page's actual reference. They now sit in that reference as ParamField entries like every other code, under an "AI chat" subsection, so each code gets its own anchor and the page has one place where a reader looks a code up. The subsection keeps the lead-in the codes need and the table could not carry: these are numeric rather than string codes, they arrive in an `error` object under HTTP 200, and a 502 carries no code at all. Placed at the end rather than merged into the alphabetized list, because numeric codes have no place in that ordering and they are not drawn from the same set — a `code` from a REST `errors[]` array is never one of these.
The security scheme already renders how Basic auth works on every endpoint page — project_id as username, api_token as password, base64 — so the operation was restating it. The one fact the scheme does not cover is that identity keys are rejected in `params` rather than ignored, which is a property of the request body. That sentence stays on the request union, where the body contract lives.
The overview spliced its explanation onto the claim with a colon, and seven descriptions used an em-dash where a period does the work. Both read as filler once you notice them. No facts changed. The overview now states what the endpoint is, then how to call it, then defines the three terms in separate sentences; every em-dash in these files is gone rather than merely thinned.
Moves the endpoint out of a section of its own and into Messaging, next to the other text-conversation surfaces. The URL is unchanged: both sections are skip-slug, so it stays at /docs/apis/rest/ai-chat/chat-methods and the three pages linking it need no edit.
A three-page module doesn't earn a section beside Core, Agents, Relay and REST Client, and Agents already spans the whole framework — CLI, MCP gateway, LiveWire, search — so this belongs inside it. Nested groups there need no yml entry, so the folder entry is gone and the pages nest automatically. Slugs move to /reference/python/agents/ai-chat/* to match siblings like agents/skills and agents/prefabs, and index.mdx drops the sidebar-title and position it was carrying: those belong on a section landing (overview.mdx), not on a nested topic's index.
Three pages became seventeen. AIChatClient and ChatGateway are now sibling class folders under Agents, each with an index and one page per method, which is how agent-base, function-result and swaig-function are already organized. Dropped the ai-chat category. Two classes from one module do not need a grouping folder, and its index was a module overview standing between the reader and the classes. Its content moved onto the class pages: install and credentials onto AIChatClient, the wire and the caps onto ChatGateway. Class indexes keep the constructor, properties, exceptions, return types and a CardGroup of methods. Every method page follows the sibling format: intro, Parameters, Returns, Example, plus Raises where a method has failure modes worth naming. The guide's single "ai_chat module" card is now two cards, one per class.
Hidden rather than removed, so the page stays reachable by direct URL while it is out of the sidebar, search, and llms.txt. `hidden: true` goes on the nav entry, not in frontmatter: Fern reads `hidden` from docs.yml and `noindex` from frontmatter, and the two do different things. An MDX comment at the top of the page records that, along with the plan to fold this content into the AI overview. Leaving the nav entry in place rather than dropping it keeps the page inside yarn fern-md-check, which only validates what the nav references.
Chat reads as messaging, so the endpoint now sits in a labeled AI Chat subsection rather than as a bare entry beside `messages`, matching how WhatsApp and Campaign Registry are grouped. The subsection is skip-slug on purpose. Nested sections add a path segment — Campaign Registry gives campaign-registry/brands/... — and without it the endpoint would land at /ai-chat/ai-chat/chat-methods. Verified by fern check: the three pages linking chat-methods still resolve, so the URL is unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #569.
Documents the AI chat API: the JSON-RPC endpoint, the guide around it, and the Python
ai_chatmodule that speaks it.The endpoint lives in the spec, not MDX
POST /api/ai/chatis JSON-RPC — one path, one HTTP method, six RPC methods selected by a body field — so it follows the pattern the Calling API already uses forPOST /api/calling/calls: a single operation whose request body is a union of per-method models, discriminated onmethod, with one titled@opExampleper method.New
specs/signalwire-rest/ai-api/family, because/api/aiis a route prefix no existing family owns. It covers all six methods and their results, the JSON-RPC error envelope with the error-code table, and the two failures that are not JSON-RPC at all: a401, and a502when the front door cannot reach the chat service. The operation documents the requiredchatscope via the sharedtokenPermissionsalias.Reference page:
/docs/apis/rest/ai-chat/chat-methodsPages
/ai/chat/server-sdks/reference/python/ai-chat.../ai-chat/ai-chat-clientAIChatClient.../ai-chat/chat-gatewayChatGatewayWebhooks: reused, not forked
A chat conversation delivers the same SWAIG and post-prompt payloads a voice session does, with different values — not different shapes — so no new webhook models were added. Following the reorg in #571/#575, the guide links the AI channel's canonical pages (
/docs/apis/rest/webhooks/ai-swaig-tool-webhook,/docs/apis/rest/webhooks/ai-post-prompt-callback).This branch widens the two fields chat actually changes, both of which said "Always
voice":AiPostPromptCallbackPayload.conversation_typenow nameschatalongsidevoicecall_idon both AI payloads notes that a chat conversation carries its conversation id thereAiSwaigToolWebhookPayload.conversation_idnotes that a chat turn always has one and that it matchescall_idThe Bedrock variants stay voice-only.
Notable decisions
Error codes are documented as code plus meaning, without internal constant names. The names never appear on the wire (only
codeand a coarsemessagedo), and one of them would name a model provider. The SDK's exception class names are customer-facing and are documented on the client page instead.Python-only SDK reference. The TypeScript SDK has no
ai_chatmodule, so that variant gets nothing.Three SDK pages rather than one per symbol. The house pattern is one page per method (
function-resulthas 50). For a two-class module this lands as three pages with every member documented inline; splitting per method is easy to do later if the module grows.Verification
yarn build:specs— green; source and regeneratedopenapi.yamlcommitted togetheryarn fern-check— only the pre-existing FDR-unreachable error, which predates this branchyarn fern-md-check— all 2810 MDX files validsignalwire-pythonai_chatsourcesignalwire-agent-chat'srpc_doc.mdand the prime-rails front door (route,chatscope, 30-second proxy timeout, 502 body)No live calls were made against the service, so production reachability is untested — the guide's "Confirm it works" section is written from the documented behavior, not from an observed run.
Please review
title: Chaton/ai/chat— unambiguous inside the AI tab, but it sits alongside Programmable Chat at/chatin search results andllms.txt. Thedescriptiondisambiguates.Merge of main (2026-08-14)
Main's webhook-operations refactor (#579) conflicted only in generated output, so
openapi.yamlwas regenerated rather than hand-merged; it now differs from main solely by this branch's own additions. Three alignment changes came out of reviewing that refactor:A correction. An earlier commit here claimed voice AI accepts only a
responsestring, and documented the{tool_result, tool_prompt}object form as chat-only. That was wrong —actions.c:2404-2425parses the object form foraiagents, and my evidence for the opposite was a truncatedgrep. The reply contract is the same on both channels, main's page documents it, and the guide now points there instead of restating it.A stale trap removed.
AIPostPromptDatanow documentsparsed,rawandsubstituted, so the guide's "post_prompt_data is not parsed JSON" caveat is gone rather than left to contradict the reference.The new shared-contract rule applied.
chat_logentries are the same contract as the AI webhooks'call_logentries, so this branch dropped its ownChatLogMessageand referencesWebhooks.AI.AICallLogEntry.call_timelinestaysRecord<unknown>[]with a comment explaining that a chat timeline entry is not verifiable from a public source and the voice model names atsfield this pathway has not been confirmed to use. Widened that shared model'sroledoc as part of the reuse, sincesystem-logreaches both pathways (conversation.c:151).