1. Introducción
A2UI permite que los agentes de IA generen interfaces de usuario interactivas y enriquecidas que se renderizan de forma nativa en la Web, dispositivos móviles y computadoras, sin ejecutar código arbitrario. En lugar de respuestas solo de texto o ejecución de código riesgosa, A2UI permite que los agentes envíen descripciones declarativas de componentes que los clientes renderizan con sus propios widgets nativos. Es como si los agentes hablaran un idioma universal de la IU.
En este lab práctico, primero crearás un agente de generación de imágenes con el Kit de desarrollo de agentes (ADK) y Gemini 3.1 Flash Image (también conocido como Nano Banana 2). Luego, usarás A2UI para crear una interfaz personalizada que vaya más allá de un chatbot típico y mostrarás cómo puedes generar interfaces de forma dinámica para permitir interacciones más enriquecidas entre el agente y el usuario.
Qué aprenderás
- Crea un agente con Python del ADK
- Configura el agente para transmitir componentes de A2UI al frontend
- Crea un frontend personalizado para renderizar elementos de la A2UI
Requisitos previos
- Conocimientos básicos sobre los agentes de IA
- Conocimientos básicos de la sintaxis de Python
- Conocimientos básicos de los conceptos de frontend
2. Configuración
Sigue las instrucciones que se indican a continuación para inicializar el proyecto de Google Cloud necesario para este codelab. Después de inicializar el proyecto, se recomienda que ejecutes este codelab en Cloud Shell, ya que incluye todas las herramientas necesarias para ejecutarlo de inmediato.
Si prefieres ejecutar este codelab en tu entorno local, deberás instalar Python, uv y un editor de código antes de continuar. Todas las instrucciones de este codelab suponen que lo ejecutas en Cloud Shell, a menos que se indique lo contrario.
Configuración del entorno de autoaprendizaje
- Accede a consola de Google Cloud y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.



- El Nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una cadena de caracteres que no se utiliza en las APIs de Google. Puedes actualizarla cuando quieras.
- El ID del proyecto es único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). La consola de Cloud genera automáticamente una cadena única. Por lo general, no importa cuál sea. En la mayoría de los codelabs, deberás hacer referencia al ID de tu proyecto (suele identificarse como
PROJECT_ID). Si no te gusta el ID que se generó, podrías generar otro aleatorio. También puedes probar uno propio y ver si está disponible. No se puede cambiar después de este paso y se usa el mismo durante todo el proyecto. - Recuerda que hay un tercer valor, un número de proyecto, que usan algunas APIs. Obtén más información sobre estos tres valores en la documentación.
- A continuación, deberás habilitar la facturación en la consola de Cloud para usar las APIs o los recursos de Cloud. Ejecutar este codelab no costará mucho, tal vez nada. Para cerrar recursos y evitar que se generen cobros más allá de este instructivo, puedes borrar los recursos que creaste o borrar el proyecto. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de $300.
Inicie Cloud Shell
Si bien Google Cloud y Spanner se pueden operar de manera remota desde tu laptop, en este codelab usarás Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.
En Google Cloud Console, haz clic en el ícono de Cloud Shell en la barra de herramientas en la parte superior derecha:

El aprovisionamiento y la conexión al entorno deberían tomar solo unos minutos. Cuando termine el proceso, debería ver algo como lo siguiente:

Esta máquina virtual está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Todo tu trabajo en este codelab se puede hacer en un navegador. No es necesario que instales nada.
3. Crea un agente nuevo del ADK
- Crea una carpeta para este taller llamada
a2ui_lab:
mkdir -p ~/a2ui_lab && cd ~/a2ui_lab
- Configura el administrador de paquetes uv en esta carpeta y, luego, instala las dependencias:
uv init && uv add google-adk fastapi uvicorn a2ui-agent-sdk
- Habilita la API de AI Platform (para realizar llamadas al modelo de Gemini)
gcloud services enable aiplatform.googleapis.com
- Inicializa el agente del ADK en esta carpeta:
export GOOGLE_CLOUD_PROJECT=`gcloud config get project`
uv run adk create --model gemini-3.5-flash --project $GOOGLE_CLOUD_PROJECT --region global art_creator
Deberías ver un resultado similar al siguiente:
$ uv run adk create --model gemini-3.5-flash --project $GOOGLE_CLOUD_PROJECT --region global art_creator Agent created in ~/a2ui_lab/art_creator: - .env - __init__.py - agent.py ⚠️ WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env. Please ensure .env is added to your .gitignore to avoid committing secrets to version control.
Ten en cuenta que uv run es un comando que ejecuta otros comandos dentro del contexto del repositorio uv actual, que creamos cuando ejecutamos uv init. El comando adk se instaló en este repositorio cuando agregamos la dependencia del paquete google-adk.
En la documentación del ADK, a menudo verás los comandos de adk sin el prefijo uv run, pero cada vez que ejecutes comandos en este taller, siempre anteponle uv run a adk para que se ejecute la utilidad de línea de comandos correcta.
Ahora que se creó la estructura básica del agente, podemos definir el agente de generación de imágenes en agent.py.
- Abre el editor de Cloud Shell con el siguiente comando:
cloudshell workspace ~/a2ui_lab
- Reemplaza el contenido de
art_creator/agent.pycon el siguiente código:
art_creator/agent.py
import os
import time
from google.adk.agents.llm_agent import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
# Load env variables
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
async def generate_image(prompt: str, tool_context: ToolContext) -> dict:
"""Generates a high-quality image based on the user's detailed description prompt.
Args:
prompt: A descriptive text prompt describing the image to generate.
tool_context: Context for the tool execution.
"""
from google.genai import Client
client = Client(
vertexai=True,
project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
location=os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
)
try:
response = client.models.generate_content(
model="gemini-3.1-flash-image",
contents=prompt,
config=types.GenerateContentConfig(
response_modalities=['TEXT', 'IMAGE'],
)
)
image_bytes = None
for part in response.parts or []:
if part.inline_data is not None:
image_bytes = part.inline_data.data
break
if not image_bytes:
return {"status": "failed", "detail": "No image data returned"}
filename = f"image_{int(time.time())}.png"
await tool_context.save_artifact(
filename,
types.Part.from_bytes(data=image_bytes, mime_type='image/png'),
)
return {
"status": "success",
"filename": filename,
"url": f"/api/artifacts/{tool_context.session.id}/{filename}"
}
except Exception as e:
return {"status": "failed", "detail": str(e)}
root_agent = Agent(
name="art_agent",
model="gemini-3.5-flash",
description="A basic art generation agent.",
instruction=(
"You are an art assistant. When the user describes an image they want to generate, "
"use the generate_image tool to create it, then return a text message containing the image's URL."
),
tools=[generate_image],
)
- Ahora puedes probar el agente en la IU de desarrollo del ADK con el comando
uv run adk web:
uv run adk web --port 8080 --allow_origins "*" --reload_agents
Luego, haz clic en el botón Vista previa en la Web y selecciona Vista previa en el puerto 8080. Se abrirá la IU de desarrollo en tu navegador.
Usa la IU de desarrollo del ADK para probar las capacidades del agente con algunas instrucciones, como las siguientes:
- Una chica de anime durmiendo bajo un árbol. Colores pastel 16:9
- Fotografía de una cabaña reflejada en el lago. Finales de la tarde. Sensación nostálgica
Deberías ver que el agente responde con texto y la imagen generada.

4. Crea un frontend simple
Ahora compilaremos una app web dedicada para nuestro agente. Usaremos FastAPI para ejecutar nuestro ejecutor del ADK y entregar una interfaz de chat simple de una sola página.
Primero, detén el servidor de desarrollo del ADK presionando Ctrl + C en la terminal. Luego, crea un archivo llamado main.py en la raíz del espacio de trabajo (~/a2ui_lab/main.py) con el siguiente contenido:
main.py
import os
import logging
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from google.adk.runners import InMemoryRunner
from google.adk.agents.run_config import RunConfig
from google.genai import types
from art_creator.agent import root_agent
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Art Agent - Simple Chat")
class ChatRequest(BaseModel):
prompt: str
session_id: str = "default_session"
static_dir = os.path.join(os.path.dirname(__file__), "static")
os.makedirs(static_dir, exist_ok=True)
runner = InMemoryRunner(agent=root_agent)
runner.auto_create_session = True
@app.get("/api/artifacts/{session_id}/{filename}")
async def get_artifact(session_id: str, filename: str):
user_id = "default_user"
part = await runner.artifact_service.load_artifact(
app_name=runner.app_name,
user_id=user_id,
filename=filename,
session_id=session_id
)
if not part:
raise HTTPException(status_code=404, detail="Artifact not found")
if part.inline_data:
from fastapi.responses import Response
return Response(content=part.inline_data.data, media_type=part.inline_data.mime_type)
raise HTTPException(status_code=400, detail="Unsupported artifact format")
@app.post("/api/chat")
async def chat_endpoint(request: ChatRequest):
if not request.prompt.strip():
raise HTTPException(status_code=400, detail="Prompt cannot be empty")
user_id = "default_user"
content = types.Content(
role="user",
parts=[types.Part.from_text(text=request.prompt)]
)
full_response_text = ""
try:
async for event in runner.run_async(
user_id=user_id,
session_id=request.session_id,
new_message=content,
run_config=RunConfig(save_input_blobs_as_artifacts=True),
):
if event.content and event.content.parts:
if event.author != "user":
for part in event.content.parts:
if part.text:
full_response_text += part.text
elif part.inline_data:
try:
# Process raw binary/custom text parts (A2UI callback packages)
text_data = part.inline_data.data.decode("utf-8")
full_response_text += text_data
except Exception:
pass
except Exception as e:
logger.exception("Error running ADK agent:")
raise HTTPException(status_code=500, detail=str(e))
image_url = None
try:
artifact_keys = await runner.artifact_service.list_artifact_keys(
app_name=runner.app_name,
user_id=user_id,
session_id=request.session_id
)
image_keys = [k for k in artifact_keys if k.startswith("image_") and k.endswith(".png")]
if image_keys:
sorted_keys = sorted(image_keys, reverse=True)
image_url = f"/api/artifacts/{request.session_id}/{sorted_keys[0]}"
except Exception:
pass
return {
"text": full_response_text.strip(),
"image_url": image_url
}
app.mount("/static", StaticFiles(directory=static_dir), name="static")
@app.get("/")
async def read_index():
from fastapi.responses import FileResponse
return FileResponse(os.path.join(static_dir, "index.html"))
A continuación, crea el directorio static para almacenar los archivos de frontend:
mkdir -p static
Ahora agrega el HTML del índice (static/index.html):
static/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Art Agent - Simple Chat</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div id="chat-messages" class="messages">
<div class="message system">
<strong>System:</strong> Welcome to the Art Agent! Describe the image you want to generate.
</div>
</div>
<form id="chat-form" class="input-form">
<input type="text" id="user-input" placeholder="Type image description..." autocomplete="off" required>
<button type="submit">Generate</button>
</form>
</div>
<script src="/static/app.js"></script>
</body>
</html>
Y el CSS de diseño (static/style.css):
static/style.css
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f7f9fa;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
}
.container {
width: 100%;
max-width: 600px;
background: #ffffff;
border: 1px solid #e1e8ed;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
height: 80vh;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
padding: 10px 14px;
border-radius: 6px;
max-width: 85%;
line-height: 1.4;
word-wrap: break-word;
}
.message.user {
background-color: #e8f5fe;
align-self: flex-end;
}
.message.agent {
background-color: #f1f3f4;
align-self: flex-start;
}
.message.system {
background-color: #fff;
border: 1px solid #e1e8ed;
color: #657786;
align-self: center;
font-size: 13px;
text-align: center;
}
.input-form {
display: flex;
border-top: 1px solid #e1e8ed;
padding: 12px;
}
.input-form input {
flex: 1;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
font-size: 14px;
outline: none;
}
.input-form button {
background-color: #1da1f2;
color: white;
border: none;
border-radius: 4px;
padding: 10px 16px;
margin-left: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
}
.input-form button:hover {
background-color: #1a91da;
}
.generated-img {
max-width: 100%;
border-radius: 4px;
margin-top: 8px;
display: block;
}
.image-card {
border: 1px solid #ddd;
border-radius: 6px;
padding: 12px;
background-color: #fff;
margin-top: 8px;
}
.btn-download {
background-color: #1da1f2;
color: white;
border: none;
border-radius: 4px;
padding: 8px 12px;
font-size: 13px;
font-weight: bold;
cursor: pointer;
margin-top: 8px;
width: 100%;
}
Por último, agrega el controlador de JavaScript (static/app.js):
static/app.js
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const chatMessages = document.getElementById('chat-messages');
const sessionId = "session_" + Math.random().toString(36).substring(2, 9);
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = userInput.value.trim();
if (!prompt) return;
userInput.value = '';
appendMessage('user', prompt);
const tempBubble = appendMessage('agent', '...');
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, session_id: sessionId })
});
const data = await response.json();
tempBubble.remove();
const textValue = (data && typeof data === 'object' && data.text) ? data.text : "";
const imageUrl = (data && typeof data === 'object' && data.image_url) ? data.image_url : null;
appendMessage('agent', textValue, imageUrl);
} catch (error) {
tempBubble.remove();
appendMessage('agent', `Error: ${error.message}`);
}
});
function appendMessage(sender, text, imageUrl = null) {
const bubble = document.createElement('div');
bubble.className = `message ${sender}`;
const content = document.createElement('span');
content.innerHTML = `<strong>${sender === 'user' ? 'You' : 'Agent'}:</strong> `;
const textNode = document.createTextNode(text);
content.appendChild(textNode);
bubble.appendChild(content);
// Always format multi-line JSON blocks nicely if the message is from the agent and looks like JSON
if (sender === 'agent' && text && (text.startsWith('{') || text.startsWith('['))) {
bubble.style.fontFamily = 'monospace';
bubble.style.whiteSpace = 'pre-wrap';
bubble.style.fontSize = '12px';
}
if (imageUrl) {
const card = document.createElement('div');
card.className = 'image-card';
const img = document.createElement('img');
img.src = imageUrl;
img.className = 'generated-img';
card.appendChild(img);
const dlBtn = document.createElement('button');
dlBtn.className = 'btn-download';
dlBtn.textContent = 'Download PNG';
dlBtn.onclick = () => {
const link = document.createElement('a');
link.href = imageUrl;
link.download = `generation-${Date.now()}.png`;
link.click();
};
card.appendChild(dlBtn);
bubble.appendChild(card);
}
chatMessages.appendChild(bubble);
chatMessages.scrollTop = chatMessages.scrollHeight;
return bubble;
}
});
Para probar tu app web, inicia el servidor de FastAPI:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
Usa la Vista previa en la Web en el puerto 8080 para acceder al chat personalizado. Ahora puedes hablar directamente con el agente.

5. Configura el agente para que emita mensajes de A2UI
Ahora, actualicemos el agente para que muestre una IU estructurada en lugar de solo texto. Usaremos el a2ui-agent-sdk oficial para compilar una instrucción del sistema que tenga en cuenta la A2UI para el agente.
Cuando se usa el SDK de A2UI, en lugar de definir las instrucciones del agente directamente, usamos la clase A2uiSchemaManager, que estructurará la instrucción del sistema del agente para comprender las capacidades de generación de la interfaz de A2UI, incluido el acceso al catálogo de componentes, el esquema de componentes completo y ejemplos de uso (si están disponibles).
- Primero, detén el servidor de FastAPI con Ctrl+C.
- Modifica
art_creator/agent.pypara integrarA2uiSchemaManagery nuestro nuevo ganchoa2ui_callback:
art_creator/agent.py
import os
import time
from google.adk.agents.llm_agent import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
from a2ui.schema.manager import A2uiSchemaManager
from a2ui.basic_catalog.provider import BasicCatalog
# Load env variables
from dotenv import load_dotenv
load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
async def generate_image(prompt: str, tool_context: ToolContext) -> dict:
"""Generates a high-quality image based on the user's detailed description prompt."""
from google.genai import Client
client = Client(
vertexai=True,
project=os.environ.get("GOOGLE_CLOUD_PROJECT"),
location=os.environ.get("GOOGLE_CLOUD_LOCATION", "global")
)
try:
response = client.models.generate_content(
model="gemini-3.1-flash-image",
contents=prompt,
config=types.GenerateContentConfig(
response_modalities=['TEXT', 'IMAGE'],
)
)
image_bytes = None
for part in response.parts or []:
if part.inline_data is not None:
image_bytes = part.inline_data.data
break
if not image_bytes:
return {"status": "failed", "detail": "No image data returned"}
filename = f"image_{int(time.time())}.png"
await tool_context.save_artifact(
filename,
types.Part.from_bytes(data=image_bytes, mime_type='image/png'),
)
return {
"status": "success",
"filename": filename,
"url": f"/api/artifacts/{tool_context.session.id}/{filename}"
}
except Exception as e:
return {"status": "failed", "detail": str(e)}
schema_manager = A2uiSchemaManager(
version="0.8",
catalogs=[BasicCatalog.get_config("0.8")],
)
instruction = schema_manager.generate_system_prompt(
role_description=(
"You are a specialized Image Creator agent. "
"When given an image description, analyze the prompt and ask the user for any missing details. "
"The image generation prompt should include: "
"Subject, environment, style, lighting, color and mood. "
),
workflow_description=(
"1. if the user greets you, greet the user back explaining your purpose. "
"2. if the user describes an image, DO NOT GENERATE IT IMMEDIATELY: compare with "
" the ideal generation prompt and ask the user for any missing details using "
" rich A2UI UI elements only. NOTE: only run this step once per image, if the user "
" decides to not detail one or more elements it is ok. "
"3. combine the original prompt with the responses in the UI elements and call `generate_image` "
" with the generated prompt. "
" DO NOT INCLUDE EXAMPLES IN THE GENERATED PROMPT, ONLY THE ELEMENTS THE USER ASKED FOR. "
"4. display the resulting image to the user in a card including the image, the prompt and a "
" download button"
),
ui_description=(
"Use Card, Text, Image, Multichoice and Button components to present the options. "
"Always include a single choice selection box for image resolution (1K, 2K or 4K) and one for "
"aspect ratio (1:1, 16:9 or 9:16). "
"When rendering the final output (generated image) always render the generated image using an "
"Image component with the url bound to the image's URL/path returned by the tool. "
"Add a Text component with the prompt that generated the image. "
"Include a Button component labeled 'Download PNG' to allow downloading the image. "
"Do NOT use markdown formatting in text values. Use the usageHint property for heading levels instead. "
"Respond ONLY with the A2UI JSON array. Do NOT include any text "
"outside the JSON. Put all explanations into Text components."
),
include_schema=True,
)
root_agent = Agent(
model="gemini-3.5-flash",
name="art_agent_a2ui",
instruction=instruction,
tools=[generate_image],
)
Observa cómo ahora las instrucciones del agente se generan con la llamada a schema_manager.generate_system_prompt en lugar de estar codificadas en la definición del agente.
Para probar tu app web, inicia el servidor de FastAPI:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
Usa la vista previa web en el puerto 8080 para acceder al chat personalizado. Notarás que ahora el agente emite mensajes JSON en lugar de texto normal. Esta es la representación interna de los elementos de la IU de A2 que renderizaremos en la siguiente sección.

6. Crea un frontend personalizado para el agente
En esta etapa, nuestro cliente obtiene una lista de mensajes de A2UI limpios (beginRendering, surfaceUpdate y dataModelUpdate). Ahora compilaremos un motor de renderización personalizado del cliente en JavaScript simple para ver estos componentes en acción.
Aquí se muestra el archivo static/app.js completo con la lógica de análisis y renderización de A2UI:
static/app.js
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const userInput = document.getElementById('user-input');
const chatMessages = document.getElementById('chat-messages');
const sessionId = "session_" + Math.random().toString(36).substring(2, 9);
async function sendChat(prompt, showInUi = true) {
if (!prompt) return;
if (showInUi) {
appendMessage('user', prompt);
}
const tempBubble = appendMessage('agent', '...');
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, session_id: sessionId })
});
const data = await response.json();
tempBubble.remove();
let text = data.text || "";
let a2uiMessages = [];
// 1. Extract <a2ui-json> blocks
const a2uiRegex = /<a2ui-json>(.*?)<\/a2ui-json>/gs;
let match;
while ((match = a2uiRegex.exec(text)) !== null) {
try {
const jsonStr = match[1].trim();
const parsed = JSON.parse(jsonStr);
const parsedList = Array.isArray(parsed) ? parsed : [parsed];
for (const msg of parsedList) {
if (msg && typeof msg === 'object') {
a2uiMessages.push(msg);
}
}
} catch (e) {
console.error("Error parsing <a2ui-json> block:", e);
}
}
// 2. Extract <a2a_datapart_json> blocks (for robust history/callback parsing)
const a2aRegex = /<a2a_datapart_json>(.*?)<\/a2a_datapart_json>/gs;
while ((match = a2aRegex.exec(text)) !== null) {
try {
const jsonStr = match[1].trim();
const parsed = JSON.parse(jsonStr);
const dataMsg = (parsed && parsed.kind === 'data') ? parsed.data : parsed;
if (dataMsg && typeof dataMsg === 'object') {
a2uiMessages.push(dataMsg);
}
} catch (e) {
console.error("Error parsing <a2a_datapart_json> block:", e);
}
}
// 3. Clean XML and A2UI JSON tags from displayed conversational text
const cleanText = text.replace(/<(a2ui-json|a2a_datapart_json)>.*?<\/\1>/gs, '').trim();
const imageUrl = (data && typeof data === 'object' && data.image_url) ? data.image_url : null;
appendMessage('agent', cleanText, imageUrl, a2uiMessages);
} catch (error) {
tempBubble.remove();
appendMessage('agent', `Error: ${error.message}`);
}
}
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = userInput.value.trim();
if (!prompt) return;
userInput.value = '';
await sendChat(prompt, true);
});
function renderA2UI(a2uiMessages) {
try {
let rootId = null;
const components = {};
const dataModel = {};
for (const msg of a2uiMessages) {
if (msg.beginRendering) {
rootId = msg.beginRendering.root;
} else if (msg.surfaceUpdate) {
for (const item of msg.surfaceUpdate.components) {
components[item.id] = item.component;
}
} else if (msg.dataModelUpdate) {
for (const item of msg.dataModelUpdate.contents) {
const val = item.valueString !== undefined ? item.valueString :
item.valueBool !== undefined ? item.valueBool :
item.valueInt !== undefined ? item.valueInt :
item.valueDouble !== undefined ? item.valueDouble : item.valueString;
dataModel[item.key] = val;
}
}
}
if (!rootId || Object.keys(components).length === 0) {
return null;
}
function resolveValue(valObj) {
if (!valObj) return '';
let val = '';
if (typeof valObj === 'string') val = valObj;
else if (valObj.literalString !== undefined) val = valObj.literalString;
else if (valObj.path !== undefined) val = dataModel[valObj.path] || '';
else val = JSON.stringify(valObj);
// Dynamically replace any wrong session IDs in artifact URLs with the active sessionId
if (typeof val === 'string' && val.includes('/api/artifacts/')) {
val = val.replace(/\/api\/artifacts\/session_[a-z0-9]+/g, `/api/artifacts/${sessionId}`);
}
return val;
}
function buildElement(id) {
try {
const compDesc = components[id];
if (!compDesc) return null;
const type = Object.keys(compDesc)[0];
const props = compDesc[type];
const el = document.createElement('div');
el.className = `a2ui-component a2ui-${type.toLowerCase()}`;
el.style.margin = '4px 0';
if (type === 'Column') {
el.style.display = 'flex';
el.style.flexDirection = 'column';
el.style.gap = '8px';
const children = props.children?.explicitList || [];
for (const childId of children) {
const childEl = buildElement(childId);
if (childEl) el.appendChild(childEl);
}
} else if (type === 'Row') {
el.style.display = 'flex';
el.style.flexDirection = 'row';
el.style.gap = '8px';
el.style.alignItems = 'center';
const children = props.children?.explicitList || [];
for (const childId of children) {
const childEl = buildElement(childId);
if (childEl) el.appendChild(childEl);
}
} else if (type === 'Card') {
el.style.border = '1px solid #ddd';
el.style.borderRadius = '6px';
el.style.padding = '12px';
el.style.backgroundColor = '#fdfdfd';
el.style.marginTop = '8px';
if (props.child) {
const childEl = buildElement(props.child);
if (childEl) el.appendChild(childEl);
}
} else if (type === 'Text') {
const textVal = resolveValue(props.text);
const tag = props.usageHint === 'h1' ? 'h3' :
props.usageHint === 'h2' ? 'h4' : 'p';
const textEl = document.createElement(tag);
textEl.textContent = textVal;
textEl.style.margin = '0 0 4px 0';
el.appendChild(textEl);
} else if (type === 'Image') {
const srcVal = resolveValue(props.url) || resolveValue(props.src);
const imgEl = document.createElement('img');
imgEl.src = srcVal;
imgEl.style.maxWidth = '100%';
imgEl.style.borderRadius = '4px';
imgEl.style.display = 'block';
imgEl.style.marginTop = '6px';
imgEl.className = 'generated-img';
el.appendChild(imgEl);
} else if (type === 'Divider') {
const hrEl = document.createElement('hr');
hrEl.style.border = '0';
hrEl.style.borderTop = '1px solid #eee';
hrEl.style.margin = '12px 0';
el.appendChild(hrEl);
} else if (type === 'MultipleChoice') {
const labelVal = resolveValue(props.label);
const options = props.options?.explicitList || (Array.isArray(props.options) ? props.options : []);
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '4px';
container.style.margin = '8px 0';
if (labelVal) {
const labelEl = document.createElement('label');
labelEl.textContent = labelVal;
labelEl.style.fontSize = '12px';
labelEl.style.fontWeight = 'bold';
container.appendChild(labelEl);
}
const selectEl = document.createElement('select');
selectEl.className = 'a2ui-select';
selectEl.name = id;
for (const option of options) {
const optEl = document.createElement('option');
optEl.value = option.value !== undefined ? option.value : (option.id !== undefined ? option.id : '');
optEl.textContent = resolveValue(option.label);
selectEl.appendChild(optEl);
}
container.appendChild(selectEl);
el.appendChild(container);
} else if (type === 'Button') {
let labelVal = props.label ? resolveValue(props.label) : '';
if (!labelVal && props.child) {
const childComp = components[props.child];
if (childComp && childComp.Text) {
labelVal = resolveValue(childComp.Text.text);
}
}
const btnEl = document.createElement('button');
btnEl.className = 'btn-download';
btnEl.textContent = labelVal || 'Submit';
btnEl.addEventListener('click', (e) => {
e.preventDefault();
const action = props.action;
let downloadUrl = null;
if (action && (action.name === 'download_file' || action.name === 'download') && action.context) {
const urlContext = action.context.find(ctx => ctx.key === 'url');
if (urlContext) {
downloadUrl = resolveValue(urlContext.value);
}
}
const isDownload = downloadUrl || (labelVal && labelVal.toLowerCase().includes('download'));
if (isDownload) {
const finalUrl = downloadUrl || (el.closest('.message')?.querySelector('img')?.src);
if (finalUrl) {
const link = document.createElement('a');
link.href = finalUrl;
link.download = `generation-${Date.now()}.png`;
link.click();
}
} else {
const bubbleEl = el.closest('.message');
const selects = bubbleEl.querySelectorAll('.a2ui-select');
let answers = [];
selects.forEach(sel => {
let labelText = sel.previousSibling ? sel.previousSibling.textContent : sel.name;
const selectedText = sel.options[sel.selectedIndex]?.textContent || sel.value;
answers.push(`- ${labelText}: ${selectedText}`);
});
if (answers.length > 0) {
const responseText = `Selected options:\n` + answers.join('\n');
sendChat(responseText, false);
} else {
sendChat(labelVal || 'Submit', false);
}
}
});
el.appendChild(btnEl);
}
return el;
} catch (err) {
console.error('Error building component:', id, err);
return null;
}
}
return buildElement(rootId);
} catch (err) {
console.error('Error in renderA2UI:', err);
return null;
}
}
function appendMessage(sender, text, imageUrl = null, a2ui = null) {
const bubble = document.createElement('div');
bubble.className = `message ${sender}`;
const textSpan = document.createElement('span');
textSpan.innerHTML = `<strong>${sender === 'user' ? 'You' : 'Agent'}:</strong> `;
bubble.appendChild(textSpan);
if (text) {
const textContent = document.createTextNode(text);
textSpan.appendChild(textContent);
}
if (sender === 'agent' && a2ui && a2ui.length > 0) {
const a2uiEl = renderA2UI(a2ui);
if (a2uiEl) {
bubble.appendChild(a2uiEl);
}
}
if (imageUrl) {
const imgContainer = document.createElement('div');
imgContainer.style.marginTop = '8px';
const img = document.createElement('img');
img.src = imageUrl;
img.style.maxWidth = '100%';
img.style.borderRadius = '4px';
img.className = 'generated-img';
imgContainer.appendChild(img);
bubble.appendChild(imgContainer);
}
chatMessages.appendChild(bubble);
chatMessages.scrollTop = chatMessages.scrollHeight;
return bubble;
}
});
Vuelve a iniciar el servidor de la app de FastAPI:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
Y habla con tu agente de Art Creator de A2UI completamente dinámico.

7. ¡Felicitaciones!
Creaste un agente del ADK que genera elementos de la IU de forma dinámica con A2UI. Para continuar tu recorrido de aprendizaje, puedes explorar las diversas integraciones de frameworks o la documentación en las referencias que se indican a continuación.
Compila un frontend de producción
En este taller, usamos un frontend personalizado de JS puro con fines didácticos, pero para la producción, compilarías un frontend con uno de los renderizadores oficiales de A2UI:
Plataforma | Procesador | Instalar |
Web (React) | @a2ui/react | npm install @a2ui/react |
Web (Lit) | @a2ui/lit | npm install @a2ui/lit |
Web (Angular) | @a2ui/angular | npm install @a2ui/angular |
Dispositivos móviles y computadoras | SDK de GenUI de Flutter |