diff --git a/apps/docs/content/docs/api/index.mdx b/apps/docs/content/docs/api/index.mdx index 9d898d16..df150b30 100644 --- a/apps/docs/content/docs/api/index.mdx +++ b/apps/docs/content/docs/api/index.mdx @@ -24,7 +24,7 @@ import { Chat, root, paragraph, text, Card, Button, emoji } from "chat"; | Export | Description | |--------|-------------| -| [`toAiMessages`](/docs/streaming#toaimessagesmessages-options) | Convert `Message[]` to AI SDK `{ role, content }[]` format | +| [`toAiMessages`](/docs/api/to-ai-messages) | Convert `Message[]` to AI SDK `{ role, content }[]` format | ## Message formats diff --git a/apps/docs/content/docs/api/message.mdx b/apps/docs/content/docs/api/message.mdx index 6572cc66..54676a6c 100644 --- a/apps/docs/content/docs/api/message.mdx +++ b/apps/docs/content/docs/api/message.mdx @@ -186,7 +186,7 @@ Links found in incoming messages are extracted and exposed as `LinkPreview` obje /> -When using [`toAiMessages()`](/docs/streaming#toaimessagesmessages-options), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context. +When using [`toAiMessages()`](/docs/api/to-ai-messages), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context. ### Platform support diff --git a/apps/docs/content/docs/api/to-ai-messages.mdx b/apps/docs/content/docs/api/to-ai-messages.mdx new file mode 100644 index 00000000..c2f2adf0 --- /dev/null +++ b/apps/docs/content/docs/api/to-ai-messages.mdx @@ -0,0 +1,190 @@ +--- +title: toAiMessages +description: Convert Chat SDK messages to AI SDK conversation format. +type: reference +--- + +Convert an array of `Message` objects into the `{ role, content }[]` format expected by AI SDKs. The output is structurally compatible with AI SDK's `ModelMessage[]`. + +```typescript +import { toAiMessages } from "chat"; +``` + +## Usage + +```typescript title="lib/bot.ts" lineNumbers +import { toAiMessages } from "chat"; + +bot.onSubscribedMessage(async (thread, message) => { + const result = await thread.adapter.fetchMessages(thread.id, { limit: 20 }); + const history = await toAiMessages(result.messages); + const response = await agent.stream({ prompt: history }); + await thread.post(response.fullStream); +}); +``` + +## Signature + +```typescript +function toAiMessages( + messages: Message[], + options?: ToAiMessagesOptions +): Promise +``` + +### Parameters + + + +### Options + + AiMessage | null | Promise', + }, + onUnsupportedAttachment: { + description: 'Called when an attachment type is not supported (video, audio).', + type: '(attachment: Attachment, message: Message) => void', + default: 'console.warn', + }, + }} +/> + +### Returns + +`Promise` — an array of messages with `role` and `content` fields, directly assignable to AI SDK's `ModelMessage[]`. + +## Behavior + +- **Role mapping** — `author.isMe === true` maps to `"assistant"`, all others to `"user"` +- **Filtering** — Messages with empty or whitespace-only text are removed +- **Sorting** — Messages are sorted chronologically (oldest first) by `metadata.dateSent` +- **Links** — Link metadata (URL, title, description, site name) is appended to message content. Embedded message links are labeled as `[Embedded message: ...]` +- **Attachments** — Images and text files (JSON, XML, YAML, etc.) are included as multipart content using `fetchData()`. Video and audio attachments trigger `onUnsupportedAttachment` + +## Return types + +```typescript +type AiMessage = AiUserMessage | AiAssistantMessage; + +interface AiUserMessage { + role: "user"; + content: string | AiMessagePart[]; +} + +interface AiAssistantMessage { + role: "assistant"; + content: string; +} +``` + +User messages have multipart `content` when attachments are present: + +```typescript +type AiMessagePart = AiTextPart | AiImagePart | AiFilePart; + +interface AiTextPart { + type: "text"; + text: string; +} + +interface AiImagePart { + type: "image"; + image: DataContent | URL; + mediaType?: string; +} + +interface AiFilePart { + type: "file"; + data: DataContent | URL; + filename?: string; + mediaType: string; +} +``` + +## Examples + +### Multi-user context + +Prefix each user message with their username so the AI model can distinguish speakers: + +```typescript +const history = await toAiMessages(result.messages, { includeNames: true }); +// [{ role: "user", content: "[alice]: Hello" }, +// { role: "assistant", content: "Hi there!" }, +// { role: "user", content: "[bob]: Thanks" }] +``` + +### Transforming messages + +Replace raw user IDs with readable names: + +```typescript +const history = await toAiMessages(result.messages, { + transformMessage: (aiMessage) => { + if (typeof aiMessage.content === "string") { + return { + ...aiMessage, + content: aiMessage.content.replace(/<@U123>/g, "@VercelBot"), + }; + } + return aiMessage; + }, +}); +``` + +### Filtering messages + +Skip messages from a specific user: + +```typescript +const history = await toAiMessages(result.messages, { + transformMessage: (aiMessage, source) => { + if (source.author.userId === "U_NOISY_BOT") return null; + return aiMessage; + }, +}); +``` + +### Handling unsupported attachments + +```typescript +const history = await toAiMessages(result.messages, { + onUnsupportedAttachment: (attachment, message) => { + logger.warn(`Skipped ${attachment.type} attachment in message ${message.id}`); + }, +}); +``` + +## Supported attachment types + +| Type | MIME types | Included as | +|------|-----------|-------------| +| `image` | Any image MIME type | `FilePart` with base64 data | +| `file` | `text/*`, `application/json`, `application/xml`, `application/javascript`, `application/typescript`, `application/yaml`, `application/toml` | `FilePart` with base64 data | +| `video` | Any | Skipped (triggers `onUnsupportedAttachment`) | +| `audio` | Any | Skipped (triggers `onUnsupportedAttachment`) | +| `file` | Other (e.g. `application/pdf`) | Silently skipped | + + +Attachments require `fetchData()` to be available on the attachment object. Attachments without `fetchData()` are silently skipped. + diff --git a/apps/docs/content/docs/handling-events.mdx b/apps/docs/content/docs/handling-events.mdx index 6497184e..a1988975 100644 --- a/apps/docs/content/docs/handling-events.mdx +++ b/apps/docs/content/docs/handling-events.mdx @@ -111,6 +111,8 @@ bot.onSubscribedMessage(async (thread, message) => { }); ``` +See [`toAiMessages`](/docs/api/to-ai-messages) for all options including multi-user name prefixing, message transforms, and attachment handling. + ### Example: Unsubscribe on keyword ```typescript title="lib/bot.ts" lineNumbers diff --git a/apps/docs/content/docs/posting-messages.mdx b/apps/docs/content/docs/posting-messages.mdx index 507dad71..aeccc25e 100644 --- a/apps/docs/content/docs/posting-messages.mdx +++ b/apps/docs/content/docs/posting-messages.mdx @@ -151,6 +151,8 @@ await thread.post(result.fullStream); Both `fullStream` and `textStream` are supported. Use `fullStream` with multi-step agents — it preserves paragraph breaks between steps. Any `AsyncIterable` also works for custom streams. +For multi-turn conversations, use [`toAiMessages()`](/docs/api/to-ai-messages) to convert thread history into the `{ role, content }[]` format expected by AI SDKs. + See the [Streaming](/docs/streaming) page for details on platform behavior and configuration. ## Attachments and files diff --git a/apps/docs/content/docs/streaming.mdx b/apps/docs/content/docs/streaming.mdx index 9377e5a8..d9105dc0 100644 --- a/apps/docs/content/docs/streaming.mdx +++ b/apps/docs/content/docs/streaming.mdx @@ -184,7 +184,7 @@ await thread.stream(textStream, { ## Streaming with conversation history Combine message history with streaming for multi-turn AI conversations. -Use `toAiMessages()` to convert chat messages into the `{ role, content }` format expected by AI SDKs: +Use [`toAiMessages()`](/docs/api/to-ai-messages) to convert chat messages into the `{ role, content }` format expected by AI SDKs: ```typescript title="lib/bot.ts" lineNumbers import { toAiMessages } from "chat"; @@ -200,51 +200,4 @@ bot.onSubscribedMessage(async (thread, message) => { }); ``` -### `toAiMessages(messages, options?)` - -Converts an array of `Message` objects into AI SDK conversation format: - -- Maps `author.isMe` to `"assistant"` role, all others to `"user"` -- Filters out empty messages -- Sorts chronologically (oldest first) -- Appends link metadata (URLs, titles, descriptions) when present -- Labels embedded message links (e.g. shared Slack messages) as `[Embedded message: ...]` - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `includeNames` | `boolean` | `false` | Prefix user messages with `[username]: ` for multi-user context | -| `transformMessage` | `(aiMessage, source) => AiMessage \| Promise \| null` | — | Transform or filter each message after default processing. Return `null` to skip. | -| `onUnsupportedAttachment` | `(attachment, message) => void` | `console.warn` | Called when an attachment type is not supported | - -### Customizing messages with `transformMessage` - -Use `transformMessage` to modify, enrich, or filter messages after default processing: - -```typescript title="lib/bot.ts" lineNumbers -import { toAiMessages } from "chat"; - -const history = await toAiMessages(result.messages, { - transformMessage: (aiMessage, source) => { - // Replace bot user IDs with readable names - if (typeof aiMessage.content === "string") { - return { - ...aiMessage, - content: aiMessage.content.replace(/<@U123>/g, "@VercelBot"), - }; - } - return aiMessage; - }, -}); -``` - -Return `null` to skip a message entirely: - -```typescript -const history = await toAiMessages(result.messages, { - transformMessage: (aiMessage, source) => { - // Skip messages from a specific user - if (source.author.userId === "U_NOISY_BOT") return null; - return aiMessage; - }, -}); -``` +See the [`toAiMessages` API reference](/docs/api/to-ai-messages) for all options including `includeNames`, `transformMessage`, and attachment handling.