Skip to content

docs(ai): document the AI chat API - #572

Open
Devon-White wants to merge 19 commits into
mainfrom
Devon/ai-chat-api-docs
Open

docs(ai): document the AI chat API#572
Devon-White wants to merge 19 commits into
mainfrom
Devon/ai-chat-api-docs

Conversation

@Devon-White

@Devon-White Devon-White commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #569.

Documents the AI chat API: the JSON-RPC endpoint, the guide around it, and the Python ai_chat module that speaks it.

The endpoint lives in the spec, not MDX

POST /api/ai/chat is 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 for POST /api/calling/calls: a single operation whose request body is a union of per-method models, discriminated on method, with one titled @opExample per method.

New specs/signalwire-rest/ai-api/ family, because /api/ai is 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: a 401, and a 502 when the front door cannot reach the chat service. The operation documents the required chat scope via the shared tokenPermissions alias.

Reference page: /docs/apis/rest/ai-chat/chat-methods

Pages

Page Type
/ai/chat How-to: both connection paths, prerequisites, the behavior that bites, and how to confirm a working setup
/server-sdks/reference/python/ai-chat Module overview
.../ai-chat/ai-chat-client AIChatClient
.../ai-chat/chat-gateway ChatGateway

Webhooks: 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_type now names chat alongside voice
  • call_id on both AI payloads notes that a chat conversation carries its conversation id there
  • AiSwaigToolWebhookPayload.conversation_id notes that a chat turn always has one and that it matches call_id

The 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 code and a coarse message do), 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_chat module, so that variant gets nothing.

Three SDK pages rather than one per symbol. The house pattern is one page per method (function-result has 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 regenerated openapi.yaml committed together
  • yarn fern-check — only the pre-existing FDR-unreachable error, which predates this branch
  • yarn fern-md-check — all 2810 MDX files valid
  • Every Python and JS example parse-checked; every signature, kwarg, default, and return attribute verified against signalwire-python ai_chat source
  • Wire facts cross-checked against signalwire-agent-chat's rpc_doc.md and the prime-rails front door (route, chat scope, 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: Chat on /ai/chat — unambiguous inside the AI tab, but it sits alongside Programmable Chat at /chat in search results and llms.txt. The description disambiguates.
  • Placement of the guide as a top-level entry in the AI tab rather than under Guides.

Merge of main (2026-08-14)

Main's webhook-operations refactor (#579) conflicted only in generated output, so openapi.yaml was 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 response string, and documented the {tool_result, tool_prompt} object form as chat-only. That was wrong — actions.c:2404-2425 parses the object form for ai agents, and my evidence for the opposite was a truncated grep. 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. AIPostPromptData now documents parsed, raw and substituted, 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_log entries are the same contract as the AI webhooks' call_log entries, so this branch dropped its own ChatLogMessage and references Webhooks.AI.AICallLogEntry. call_timeline stays Record<unknown>[] with a comment explaining that a chat timeline entry is not verifiable from a public source and the voice model names a ts field this pathway has not been confirmed to use. Widened that shared model's role doc as part of the reuse, since system-log reaches both pathways (conversation.c:151).

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.
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Devon-White and others added 17 commits August 13, 2026 12:06
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.
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.
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.

Document new AI chat API

1 participant