showcase/shell-docs/src/content/docs/integrations/a2a/generative-ui/declarative-a2ui.mdx
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>```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.
```
pnpm install
```
```
pnpm dev
```
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.
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: [],
},
};
```
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.