Build with Google's new A2UI Spec: Agent User Interfaces with A2UI + AG-UI
TL;DR
This guide explains how to build full-stack A2UI agents using Google's A2UI spec, A2A protocol, AG-UI, and CopilotKit. It covers backend configuration with A2UI components and frontend integration for dynamic, AI-generated interfaces.
Key Takeaways
- •A2UI is an open-source UI toolkit enabling AI agents to generate dynamic, interactive interfaces on-the-fly instead of static text responses.
- •Building an A2UI agent involves configuring the backend with A2A protocol, defining A2UI response components, and integrating with AG-UI and CopilotKit for the frontend.
- •Prerequisites include React/Next.js knowledge, Python, AG-UI protocol, Google ADK, Gemini API key, and CopilotKit for full-stack development.
- •The process includes defining A2UI schemas, using A2UI Composer for component generation, and setting up agent prompts for UI responses.
Tags
TL;DR
In this guide, you will learn how to build full-stack agent-to-user interface (A2UI) agents using the agent-to-agent (A2A) protocol, the AG-UI protocol, and CopilotKit.
Before we jump in, I would also like to cover how A2UI & AG-UI fit together. You can read about it here
For this build, we'll cover:
What is A2UI?
Building A2UI + A2A agent backend
Building A2UI + A2A agent frontend using AG-UI and CopilotKit
Here is a preview of what we will be building:
What is A2UI?
A2UI is a new open-source UI Toolkit to facilitate LLM-generated UIs that allows AI agents to create and present dynamic, interactive user interfaces (UIs) on the fly, rather than relying on a fixed, pre-built interface.
A2UI is built with the A2A protocol and allows an A2A agent to send interactive components instead of just text, using a high-level framework-agnostic format that can be rendered natively on any surface
In simpler terms, instead of the AI agent only responding with text in a chat window, it can respond by generating a complete UI component like:
An interactive chart or graph.
A fully populated form or table.
Specific buttons (like "Approve" or "Reject") relevant only to the task at hand.
You can learn more about A2UI here.
Now that you have learned what the A2UI protocol is, let us see how to use it together with A2A, AG-UI, and CopilotKit to build full-stack A2UI AI agents.
Prerequisites
To fully understand this tutorial, you need to have a basic understanding of React or Next.js
We'll also make use of the following:
Python - a popular programming language for building AI agents with AI agent frameworks; make sure it is installed on your computer.
AG-UI Protocol - The Agent User Interaction Protocol (AG-UI), developed by CopilotKit, is an open-source, lightweight, event-based protocol that facilitates rich, real-time interactions between the frontend and your AI agent backend.
Google ADK - an open-source framework designed by Google to simplify the process of building complex and production-ready AI agents.
Gemini API Key - an API key to enable you to perform various tasks using the Gemini models for ADK agents.
CopilotKit - an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.
Building A2UI + A2A agent backend
In this section, you will learn how to configure your agent to use A2UI using the A2A protocol in the backend.
Let’s get started.
To get started, clone the A2A starter template using the command below:
git clone https://github.com/copilotkit/with-a2a-a2ui.git
After that, create an environment file with your API key:
echo "GEMINI_API_KEY=your_api_key_here" > .env
Then run the command below to install the dependencies:
pnpm install
Finally, run and connect your agent using the following command:
pnpm dev
Let us now see how to configure your agent to use A2UI using the A2A protocol.
Step 1: Define your agent A2UI Response Components
To define your agent A2UI response components, first, define a schema that contains the complete JSON Schema that defines a valid A2UI message, as shown in the agent/restaurant_finder/prompt_builder.py file.
A2UI_SCHEMA = r'''
{
"title": "A2UI Message Schema",
"description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to construct and update user interfaces dynamically. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.",
"type": "object",
"properties": {
"beginRendering": {
"type": "object",
"description": "Signals the client to begin rendering a surface with a root component and specific styles.",
"properties": {
"surfaceId": {
"type": "string",
"description": "The unique identifier for the UI surface to be rendered."
},
"root": {
"type": "string",
"description": "The ID of the root component to render."
},
"styles": {
"type": "object",
"description": "Styling information for the UI.",
"properties": {
"font": {
"type": "string",
"description": "The primary font for the UI."
},
"primaryColor": {
"type": "string",
"description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').",
"pattern": "^#[0-9a-fA-F]{6}$"
}
}
}
},
"required": ["root", "surfaceId"]
},
// ...
}
Then define A2UI response components that your agent uses to format its responses, as shown in the agent/restaurant_finder/prompt_builder.py file.
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---
// ...
After that, define a function that constructs the full prompt with instructions, A2UI response components and A2UI message JSON Schema, as shown in the agent/restaurant_finder/prompt_builder.py file.
def get_ui_prompt(base_url: str, examples: str) -> str:
"""
Constructs the full prompt with UI instructions, rules, examples, and schema.
Args:
base_url: The base URL for resolving static assets like logos.
examples: A string containing the specific UI examples for the agent's task.
Returns:
A formatted string to be used as the system prompt for the LLM.
"""
# The f-string substitution for base_url happens here, at runtime.
formatted_examples = examples.format(base_url=base_url)
return f"""
You are a helpful restaurant finding assistant. Your final output MUST be a A2UI UI JSON response.
To generate the response, you MUST follow these rules:
1. Your response MUST be in two parts, separated by the delimiter: `---a2ui_JSON---`.
2. The first part is your conversational text response.
3. The second part is a single, raw JSON object that is a list of A2UI messages.
4. The JSON part MUST validate against the A2UI JSON SCHEMA provided below.
--- UI TEMPLATE RULES ---
- If the query is for a list of restaurants, use the restaurant data you have already received from the `get_restaurants` tool to populate the `dataModelUpdate.contents` array (e.g., as a `valueMap` for the "items" key).
- If the number of restaurants is 5 or fewer, you MUST use the `SINGLE_COLUMN_LIST_EXAMPLE` template.
- If the number of restaurants is more than 5, you MUST use the `TWO_COLUMN_LIST_EXAMPLE` template.
- If the query is to book a restaurant (e.g., "USER_WANTS_TO_BOOK..."), you MUST use the `BOOKING_FORM_EXAMPLE` template.
- If the query is a booking submission (e.g., "User submitted a booking..."), you MUST use the `CONFIRMATION_EXAMPLE` template.
{formatted_examples}
---BEGIN A2UI JSON SCHEMA---
{A2UI_SCHEMA}
---END A2UI JSON SCHEMA---
"""
Step 2: Generating A2UI components with A2UI Composer
If you want an easy way to generate components, you can use the A2UI Composer, which enables you to generate A2UI components.
To create a component using the A2UI composer, go to https://a2ui-editor.ag-ui.com/ and describe your A2UI widget, as shown below.
Then click the create button, and the composer will generate your A2UI widget, as shown below.
After that, copy the generated JSON spec and paste it into your agent's prompt as we discussed in step one above.
Step 3: Configure your A2UI agent
Once you have set up your A2A components and assembled your agent prompt, configure your agent, as shown in the agent/restaurant_finder/agent.py file.
// ...
# Local imports from sibling modules
from prompt_builder import (
A2UI_SCHEMA, # JSON Schema for A2UI validation
RESTAURANT_UI_EXAMPLES, # Example A2UI responses for few-shot learning
get_text_prompt, # Prompt for text-only mode
get_ui_prompt, # Prompt for UI mode (with schema and examples)
)
from tools import get_restaurants # Tool function for fetching restaurant data
logger = logging.getLogger(__name__)
AGENT_INSTRUCTION = """
You are a helpful restaurant finding assistant. Your goal is....
"""
class RestaurantAgent:
"""
An agent that finds restaurants based on user criteria.
"""
# Supported content types for A2A protocol
SUPPORTED_CONTENT_TYPES = ["text", "text/plain"]
def __init__(self, base_url: str, use_ui: bool = False):
"""
Initialize the RestaurantAgent.
Args:
base_url: The server's base URL (used for resolving image URLs)
use_ui: If True, generate A2UI JSON responses; if False, generate text
"""
self.base_url = base_url
self.use_ui 

