Generated UI

An agent can answer with UI rather than text: a weekend plan with a map of the places, a tab for each day and a form to change the pace. The agent writes the UI into its reply, the router carries it to Stream Chat beside the reply's words, and your app shows it in place of them. What the person does in it comes back to the agent with their next message.

The router knows no UI format. The model writes the UI in a format your app has a renderer for, such as A2UI or OpenUI, and the router passes it on as it is.

How the agent writes UI

The model writes the UI in a fenced block of its reply. The block's info string is ui:<format>, or ui:<format>@<version> to name the format's version, such as ui:a2ui@1.0. OpenUI's own fence, openui-lang, is read as the openui format, since that's the fence OpenUI's prompts ask models for:

Here's a weekend in Ohrid, with a map and a plan for each day.

```openui-lang
root = Card([title, map])
title = CardHeader("A weekend in Ohrid")
```

The first such block is the reply's UI, and any later one is left out of its text. Only the agent writes UI: a block in a person's message stays part of their words.

The model writes UI when the agent's instructions ask it to: add the format's own instructions, with the components of your app it may place, to the agent's instructions.md. stream-chat-react writes those instructions from the same components your app shows. The capabilities() of the app's AIUIHost hold, for each format it reads, the fence the model writes in and the instructions for the model:

// Each format the host reads, such as A2UI and OpenUI, with its `fence` and `instructions`.
const { renderers } = await ui.capabilities();
console.log(JSON.stringify(renderers, null, 2));

The page can't change the agent, so print them while you build the app and add them to instructions.md, or send them to your backend and have it add them. Write them again when your components change. You can also write them on your backend or at build time, from the same component definitions:

import { a2uiInstructions } from "stream-chat-react/a2ui";
import { openUIInstructions } from "stream-chat-react/openui";

const a2ui = await a2uiInstructions({ components: [placeCard] });
const openui = await openUIInstructions([placeCard]);

See Tell the agent what the app shows in the React guide.

What reaches Stream Chat

As the reply streams, the router moves the block out of its text into the message's custom data, as custom.ai_ui, and keeps the words around it as the message's text:

{
  "v": 1,
  "format": "openui",
  "status": "streaming",
  "content": "root = Card([title, map])\ntitle = CardHeader(\"A weekend in Ohrid\")"
}
Field Description
v The version of the field, 1.
format The format from the block's info string, such as a2ui or openui.
version The format's version, such as 1.0, when the info string names one.
status streaming while the agent is still writing the block, then complete.
content The block as the model wrote it, such as an OpenUI program or A2UI messages.

The text is what people read wherever the UI can't show: in a push notification, in an external thread and in an app without a renderer for the format. So have the model write a sentence saying what the UI shows before the block.

Stream Chat holds at most 50 KB of custom data on a message. When the UI would take the message's custom data over 48 KB as JSON, the router leaves it out, and the reply keeps only its words. A UI that was showing while the reply streamed is taken off again, rather than left cut off midway.

When the conversation goes on, the model reads its earlier replies with their UI, written back as the blocks it wrote.

Show it in your app

AIConversation from stream-chat-react shows a reply's UI in place of its text, brings it in as the agent writes it, and sends what the person does in it back to the agent. Its renderers of A2UI and OpenUI are in stream-chat-react/a2ui and stream-chat-react/openui, from version 14.16.0. Each is built on its format's official packages, which your app installs for the formats your agent writes, at the versions the React guide's Installation names.

Keep an AIUIHost with the renderers of the formats your agent writes and your components for as long as the conversation shows, so each reply's UI keeps its state, such as what the person typed into a form. Its send asks the session, with what the person did as uiAction:

AgentConversation.tsx
import type { Session } from "@stream-io/calypso";
import { useMemo } from "react";
import type { LocalMessage } from "stream-chat";
import {
  AIConversation,
  AIUIHost,
  aiUIActionJSON,
  fromStreamMessage,
} from "stream-chat-react/ai-components";
import { createA2UIRenderer } from "stream-chat-react/a2ui";
import { createOpenUIRenderer } from "stream-chat-react/openui";
import "stream-chat-react/css/index.css";
import "stream-chat-react/css/ai-components.css";
import "@openuidev/react-ui/components.css";

import { placeCard } from "./placeCard";

// `messages` are the messages of the channel session.chat() opened.
export const AgentConversation = ({
  session,
  messages,
}: {
  session: Session;
  messages: LocalMessage[];
}) => {
  const ui = useMemo(
    () =>
      new AIUIHost({
        components: [placeCard],
        renderers: [createA2UIRenderer(), createOpenUIRenderer()],
        // What the person did, in their words, with the action for the agent.
        send: (action) =>
          session.responses.create(action.label, {
            uiAction: aiUIActionJSON(action),
          }),
      }),
    [session],
  );

  return (
    <AIConversation
      messages={messages.map((message) => fromStreamMessage(message))}
      ui={ui}
    />
  );
};

A reply shows its UI when one of the host's renderers reads its format and version, the first one listed asked first. Otherwise, it shows its text.

A component is a view of your app the agent can place, described once with the parameters the agent fills in and the events it reports. Every renderer places it by its name, so it works whichever format the agent writes:

placeCard.tsx
import {
  AIUIParameterType,
  defineAIUIComponent,
  requiredAIUIParameter,
} from "stream-chat-react/ai-components";

export const placeCard = defineAIUIComponent<{ title: string }>(
  {
    name: "place_card",
    description: "A photo and a short summary of a place.",
    parameters: [
      requiredAIUIParameter(
        "title",
        AIUIParameterType.string,
        "The place's Wikipedia title.",
      ),
    ],
    events: [
      {
        name: "ask_about",
        description: "The person asks about the place.",
        parameters: [],
      },
    ],
  },
  ({ args, context }) => (
    <PlaceCard
      title={args.title}
      onClick={() =>
        context.emit("ask_about", `Tell me more about ${args.title}`)
      }
    />
  ),
);

A component can load what it shows in the browser, such as a place's photo, which then never reaches the agent. The React generative UI guide covers the rest: the renderers' options, theming, running your app's tools from a reply's UI and handling errors. The iOS and Android guides do the same in the apps.

What the person does

When the person does something in a reply's UI that's for the agent, such as tapping a place or submitting a form, the host calls its send with an AIUIAction. Ask the session with it, as AgentConversation does above: action.label is the question, in the person's words, such as "Tell me more about St. Naum", and aiUIActionJSON(action) is what they did, passed as uiAction to responses.create. See Send what the person did in a reply's UI.

The React guide sends the action to the channel as a message. The agent doesn't answer a message sent to the session's channel, so ask the session instead: the router writes the person's message into the channel itself, with the action on it.

The action names the reply it came from (message_id, the id of the reply's Stream Chat message), the UI's format and the action (name), with what it carries (payload), in at most 8 KB as JSON. The router keeps it on the person's message as custom.ai_ui_action, and tells the model what they did after their words, with the opening words of the reply it was in. The model reads the action as data, not as instructions.

Examples

The Swift assistant sample plans a trip as interactive UI. Its settings choose how the agent answers: in Markdown, or with UI in A2UI or OpenUI. Ask "Plan a weekend in Ohrid" and the reply is a map of the places, the person's own calendar for those days, a tab for each day and a form to change the plan. The map, the place cards and the calendar are the app's own views, described once in UIComponents.json and placed by both formats. The agent learns them from a2ui/instructions.md or openui/instructions.md, which are generated from that file with the renderers' own code. When the settings change, the sample's backend rewrites the agent's instructions: its instructions.md, followed by the chosen format's.

The React assistant sample shows the same replies in the browser, with the page's own components for the map, the place cards and the calendar. Tapping a place or sending the form asks the agent in the person's words, with what they did as its uiAction, and the agent answers with a new plan.

The Android assistant sample shows them in a Compose app. Its Settings screen chooses how the agent answers: in Markdown, or with UI in A2UI or OpenUI.