Back to Copilotkit

Declarative (A2UI)

showcase/shell-docs/src/content/docs/integrations/a2a/generative-ui/declarative-a2ui.mdx

1.57.018.0 KB
Original Source

Build an A2A agent, configure it to use A2UI, use the A2UI composer to generate widgets, and render them in your CopilotKit powered app.

<div> <video src="https://cdn.copilotkit.ai/a2ui/demo.mp4" autoPlay controls loop muted playsInline className="rounded-xl shadow-lg mx-auto mt-auto w-5/6" /> <div className="text-center text-sm text-muted-foreground mt-4"> Demo of the <a href="https://a2ui-editor.ag-ui.com">A2UI Composer</a> - powered by CopilotKit </div> </div>

Getting started

<Steps> <Step> ### Clone the A2A starter template
```bash
git clone https://github.com/copilotkit/with-a2a-a2ui.git
```

The agent and application from the starter template are already configured to use A2UI, but details are included below for completeness.
</Step> <Step> ### Install dependencies
```
pnpm install
```
</Step> <Step> ### Run and connect your agent
```
pnpm dev
```
</Step> <Step> ### Configure your agent to use A2UI </Step> <Step> ### Setting up your agent with components
The starter template is already configured to use A2UI, but lets look at how to add your own components.

```python title="agent/restaurant_finder/prompt_builder.py"
  RESTAURANT_UI_EXAMPLES = """
  ...
  ---BEGIN SINGLE_COLUMN_LIST_EXAMPLE---
  [
    {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }},
    {{ "surfaceUpdate": {{
      "surfaceId": "default",
      "components": [
        {{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "item-list"] }} }} }} }},
        {{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "literalString": "Top Restaurants" }} }} }} }},
        {{ "id": "item-list", "component": {{ "List": {{ "direction": "vertical", "children": {{ "template": {{ "componentId": "item-card-template", "dataBinding": "/items" }} }} }} }} }},
        {{ "id": "item-card-template", "component": {{ "Card": {{ "child": "card-layout" }} }} }},
        {{ "id": "card-layout", "component": {{ "Row": {{ "children": {{ "explicitList": ["template-image", "card-details"] }} }} }} }},
        {{ "id": "template-image", weight: 1, "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }},
        {{ "id": "card-details", weight: 2, "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name", "template-rating", "template-detail", "template-link", "template-book-button"] }} }} }} }},
        {{ "id": "template-name", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "path": "name" }} }} }} }},
        {{ "id": "template-rating", "component": {{ "Text": {{ "text": {{ "path": "rating" }} }} }} }},
        {{ "id": "template-detail", "component": {{ "Text": {{ "text": {{ "path": "detail" }} }} }} }},
        {{ "id": "template-link", "component": {{ "Text": {{ "text": {{ "path": "infoLink" }} }} }} }},
        {{ "id": "template-book-button", "component": {{ "Button": {{ "child": "book-now-text", "primary": true, "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "address" }} }} ] }} }} }} }},
        {{ "id": "book-now-text", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }}
      ]
    }} }},
    {{ "dataModelUpdate": {{
      "surfaceId": "default",
      "path": "/",
      "contents": [
        {{ "key": "items", "valueMap": [
          {{ "key": "item1", "valueMap": [
            {{ "key": "name", "valueString": "The Fancy Place" }},
            {{ "key": "rating", "valueNumber": 4.8 }},
            {{ "key": "detail", "valueString": "Fine dining experience" }},
            {{ "key": "infoLink", "valueString": "https://example.com/fancy" }},
            {{ "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }},
            {{ "key": "address", "valueString": "123 Main St" }}
          ] }},
          {{ "key": "item2", "valueMap": [
            {{ "key": "name", "valueString": "Quick Bites" }},
            {{ "key": "rating", "valueNumber": 4.2 }},
            {{ "key": "detail", "valueString": "Casual and fast" }},
            {{ "key": "infoLink", "valueString": "https://example.com/quick" }},
            {{ "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }},
            {{ "key": "address", "valueString": "456 Oak Ave" }}
          ] }}
        ] }} // Populate this with restaurant data
      ]
    }} }}
  ]
  ---END SINGLE_COLUMN_LIST_EXAMPLE---
  # ... more examples below
  ```

The widgets are injected into the agent's prompt, in this case using the `RESTAURANT_UI_EXAMPLES` variable.
Widgets are defined for the agent using examples of the json arrays they should output, wrapped in a comment block.
- A comment indicating the start of the example
- beginRendering: The start of the widget's rendering
- surfaceUpdate: An example of the json structure for the widget
- dataModelUpdate: an example of the data that will be used to populate the widget
- A comment indicating the end of the example

In the example repo, all of the widgets are defined in a single variable int the prompt_builder.py file, but you can structure them however you like,
they simply need to be be injected into the agent's prompt in a clearly delineated way.
</Step> <Step> ### Generating components with the A2UI Composer If you want an easy way to generate components, you can use the A2UI Composer. Go to https://a2ui-composer.ag-ui.com/ to create your own components. The composer will generate the json spec for you, all you have to do is copy and paste it into your agent's prompt. <a href="https://a2ui-editor.ag-ui.com/gallery" target="_blank" rel="noopener noreferrer"> <Image src="/images/a2ui-composer.png" alt="Agentic Backend to Agentic Application" width={4096} height={2304} className="mx-auto w-1/2" /> </a> </Step> <Step> ### Configuring your application to render A2UI AG-UI handles communicating with your a2a agent, and passes the a2ui messages back and forth as `ActivityMessage` objects. In order to render them in your frontend, you need to configure activity message rendering.
Copilotkit provides a renderer for A2UI messages, all you need to do is instantiate it with a theme and pass it to your CopilotKitProvider.

```tsx title="app/page.tsx"
"use client";


// Disable static optimization for this page
export const dynamic = "force-dynamic";

const A2UIMessageRenderer = createA2UIMessageRenderer({ theme });

export default function Home() {
  return (
    <CopilotKitProvider
      runtimeUrl="/api/copilotkit"
      showDevConsole="auto"
      renderActivityMessages={[A2UIMessageRenderer]}
    >
      <main
        className="flex min-h-screen flex-1 flex-col overflow-hidden"
        style={{ minHeight: "100dvh" }}
      >
        <Chat />
      </main>
    </CopilotKitProvider>
  );
}

function Chat() {
  return (
    <div className="flex flex-1 flex-col overflow-hidden">
      <CopilotChat style={{ flex: 1, minHeight: "100%" }} />
    </div>
  );
}
```

```tsx title="app/theme.ts"

/** Elements */

const a = {
  "typography-f-sf": true,
  "typography-fs-n": true,
  "typography-w-500": true,
  "layout-as-n": true,
  "layout-dis-iflx": true,
  "layout-al-c": true,
};

const audio = {
  "layout-w-100": true,
};

const body = {
  "typography-f-s": true,
  "typography-fs-n": true,
  "typography-w-400": true,
  "layout-mt-0": true,
  "layout-mb-2": true,
  "typography-sz-bm": true,
  "color-c-n10": true,
};

const button = {
  "typography-f-sf": true,
  "typography-fs-n": true,
  "typography-w-500": true,
  "layout-pt-3": true,
  "layout-pb-3": true,
  "layout-pl-5": true,
  "layout-pr-5": true,
  "layout-mb-1": true,
  "border-br-16": true,
  "border-bw-0": true,
  "border-c-n70": true,
  "border-bs-s": true,
  "color-bgc-s30": true,
  "color-c-n100": true,
  "behavior-ho-80": true,
};

const heading = {
  "typography-f-sf": true,
  "typography-fs-n": true,
  "typography-w-500": true,
  "layout-mt-0": true,
  "layout-mb-2": true,
  "color-c-n10": true,
};

const h1 = {
  ...heading,
  "typography-sz-tl": true,
};

const h2 = {
  ...heading,
  "typography-sz-tm": true,
};

const h3 = {
  ...heading,
  "typography-sz-ts": true,
};

const iframe = {
  "behavior-sw-n": true,
};

const input = {
  "typography-f-sf": true,
  "typography-fs-n": true,
  "typography-w-400": true,
  "layout-pl-4": true,
  "layout-pr-4": true,
  "layout-pt-2": true,
  "layout-pb-2": true,
  "border-br-6": true,
  "border-bw-1": true,
  "color-bc-s70": true,
  "border-bs-s": true,
  "layout-as-n": true,
  "color-c-n10": true,
};

const p = {
  "typography-f-s": true,
  "typography-fs-n": true,
  "typography-w-400": true,
  "layout-m-0": true,
  "typography-sz-bm": true,
  "layout-as-n": true,
  "color-c-n10": true,
};

const orderedList = {
  "typography-f-s": true,
  "typography-fs-n": true,
  "typography-w-400": true,
  "layout-m-0": true,
  "typography-sz-bm": true,
  "layout-as-n": true,
};

const unorderedList = {
  "typography-f-s": true,
  "typography-fs-n": true,
  "typography-w-400": true,
  "layout-m-0": true,
  "typography-sz-bm": true,
  "layout-as-n": true,
};

const listItem = {
  "typography-f-s": true,
  "typography-fs-n": true,
  "typography-w-400": true,
  "layout-m-0": true,
  "typography-sz-bm": true,
  "layout-as-n": true,
};

const pre = {
  "typography-f-c": true,
  "typography-fs-n": true,
  "typography-w-400": true,
  "typography-sz-bm": true,
  "typography-ws-p": true,
  "layout-as-n": true,
};

const textarea = {
  ...input,
  "layout-r-none": true,
  "layout-fs-c": true,
};

const video = {
  "layout-el-cv": true,
};

const aLight = v0_8.Styles.merge(a, { "color-c-n5": true });
const inputLight = v0_8.Styles.merge(input, { "color-c-n5": true });
const textareaLight = v0_8.Styles.merge(textarea, { "color-c-n5": true });
const buttonLight = v0_8.Styles.merge(button, { "color-c-n100": true });
const h1Light = v0_8.Styles.merge(h1, { "color-c-n5": true });
const h2Light = v0_8.Styles.merge(h2, { "color-c-n5": true });
const h3Light = v0_8.Styles.merge(h3, { "color-c-n5": true });
const bodyLight = v0_8.Styles.merge(body, { "color-c-n5": true });
const pLight = v0_8.Styles.merge(p, { "color-c-n35": true });
const preLight = v0_8.Styles.merge(pre, { "color-c-n35": true });
const orderedListLight = v0_8.Styles.merge(orderedList, {
  "color-c-n35": true,
});
const unorderedListLight = v0_8.Styles.merge(unorderedList, {
  "color-c-n35": true,
});
const listItemLight = v0_8.Styles.merge(listItem, {
  "color-c-n35": true,
});

export const theme: v0_8.Types.Theme = {
  additionalStyles: {
    Button: {
      "--n-35": "var(--n-100)",
    },
  },
  components: {
    AudioPlayer: {},
    Button: {
      "layout-pt-2": true,
      "layout-pb-2": true,
      "layout-pl-3": true,
      "layout-pr-3": true,
      "border-br-12": true,
      "border-bw-0": true,
      "border-bs-s": true,
      "color-bgc-p30": true,
      "color-c-n100": true,
      "behavior-ho-70": true,
    },
    Card: { "border-br-9": true, "color-bgc-p100": true, "layout-p-4": true },
    CheckBox: {
      element: {
        "layout-m-0": true,
        "layout-mr-2": true,
        "layout-p-2": true,
        "border-br-12": true,
        "border-bw-1": true,
        "border-bs-s": true,
        "color-bgc-p100": true,
        "color-bc-p60": true,
        "color-c-n30": true,
        "color-c-p30": true,
      },
      label: {
        "color-c-p30": true,
        "typography-f-sf": true,
        "typography-v-r": true,
        "typography-w-400": true,
        "layout-flx-1": true,
        "typography-sz-ll": true,
      },
      container: {
        "layout-dsp-iflex": true,
        "layout-al-c": true,
      },
    },
    Column: {
      "layout-g-2": true,
    },
    DateTimeInput: {
      container: {
        "typography-sz-bm": true,
        "layout-w-100": true,
        "layout-g-2": true,
        "layout-dsp-flexhor": true,
        "layout-al-c": true,
      },
      label: {
        "layout-flx-0": true,
      },
      element: {
        "layout-pt-2": true,
        "layout-pb-2": true,
        "layout-pl-3": true,
        "layout-pr-3": true,
        "border-br-12": true,
        "border-bw-1": true,
        "border-bs-s": true,
        "color-bgc-p100": true,
        "color-bc-p60": true,
        "color-c-n30": true,
        "color-c-p30": true,
      },
    },
    Divider: {},
    Image: {
      all: {
        "border-br-5": true,
        "layout-el-cv": true,
        "layout-w-100": true,
        "layout-h-100": true,
      },
      avatar: {},
      header: {},
      icon: {},
      largeFeature: {},
      mediumFeature: {},
      smallFeature: {},
    },
    Icon: {},
    List: {
      "layout-g-4": true,
      "layout-p-2": true,
    },
    Modal: {
      backdrop: { "color-bbgc-p60_20": true },
      element: {
        "border-br-2": true,
        "color-bgc-p100": true,
        "layout-p-4": true,
        "border-bw-1": true,
        "border-bs-s": true,
        "color-bc-p80": true,
      },
    },
    MultipleChoice: {
      container: {},
      label: {},
      element: {},
    },
    Row: {
      "layout-g-4": true,
    },
    Slider: {
      container: {},
      label: {},
      element: {},
    },
    Tabs: {
      container: {},
      controls: { all: {}, selected: {} },
      element: {},
    },
    Text: {
      all: {
        "layout-w-100": true,
        "layout-g-2": true,
        "color-c-p30": true,
      },
      h1: {
        "typography-f-sf": true,
        "typography-v-r": true,
        "typography-w-400": true,
        "layout-m-0": true,
        "layout-p-0": true,
        "typography-sz-tl": true,
      },
      h2: {
        "typography-f-sf": true,
        "typography-v-r": true,
        "typography-w-400": true,
        "layout-m-0": true,
        "layout-p-0": true,
        "typography-sz-tm": true,
      },
      h3: {
        "typography-f-sf": true,
        "typography-v-r": true,
        "typography-w-400": true,
        "layout-m-0": true,
        "layout-p-0": true,
        "typography-sz-ts": true,
      },
      h4: {
        "typography-f-sf": true,
        "typography-v-r": true,
        "typography-w-400": true,
        "layout-m-0": true,
        "layout-p-0": true,
        "typography-sz-bl": true,
      },
      h5: {
        "typography-f-sf": true,
        "typography-v-r": true,
        "typography-w-400": true,
        "layout-m-0": true,
        "layout-p-0": true,
        "typography-sz-bm": true,
      },
      body: {},
      caption: {},
    },
    TextField: {
      container: {
        "typography-sz-bm": true,
        "layout-w-100": true,
        "layout-g-2": true,
        "layout-dsp-flexhor": true,
        "layout-al-c": true,
      },
      label: {
        "layout-flx-0": true,
      },
      element: {
        "typography-sz-bm": true,
        "layout-pt-2": true,
        "layout-pb-2": true,
        "layout-pl-3": true,
        "layout-pr-3": true,
        "border-br-12": true,
        "border-bw-1": true,
        "border-bs-s": true,
        "color-bgc-p100": true,
        "color-bc-p60": true,
        "color-c-n30": true,
        "color-c-p30": true,
      },
    },
    Video: {
      "border-br-5": true,
      "layout-el-cv": true,
    },
  },
  elements: {
    a: aLight,
    audio,
    body: bodyLight,
    button: buttonLight,
    h1: h1Light,
    h2: h2Light,
    h3: h3Light,
    iframe,
    input: inputLight,
    p: pLight,
    pre: preLight,
    textarea: textareaLight,
    video,
  },
  markdown: {
    p: [...Object.keys(pLight)],
    h1: [...Object.keys(h1Light)],
    h2: [...Object.keys(h2Light)],
    h3: [...Object.keys(h3Light)],
    h4: [],
    h5: [],
    h6: [],
    ul: [...Object.keys(unorderedListLight)],
    ol: [...Object.keys(orderedListLight)],
    li: [...Object.keys(listItemLight)],
    a: [...Object.keys(aLight)],
    strong: [],
    em: [],
  },
};
```
</Step> <Step> ### Give it a try!
That's it! When your agent generates an A2UI message, it will be rendered in your frontend.
A2UI actions (like button clicks) are automatically sent back to your agent via AG-UI.
</Step> </Steps>