1. Introduzione
A2UI consente agli agenti AI di generare interfacce utente interattive e avanzate che vengono visualizzate in modo nativo su web, dispositivi mobili e computer desktop, senza eseguire codice arbitrario. Invece di risposte solo testuali o esecuzione di codice rischiosa, A2UI consente agli agenti di inviare descrizioni di componenti dichiarative che i client visualizzano utilizzando i propri widget nativi. È come se gli agenti parlassero un linguaggio UI universale.
In questo lab pratico, creerai prima un agente di generazione di immagini utilizzando Agent Development Kit (ADK) e Gemini 3.1 Flash Image (noto anche come Nano Banana 2). Poi utilizzerai A2UI per creare un'interfaccia personalizzata che va oltre un tipico chatbot, mostrando come puoi generare dinamicamente interfacce per consentire interazioni più avanzate tra agente e utente.
Cosa imparerai a fare
- Creare un agente utilizzando ADK Python
- Configurare l'agente per lo streaming dei componenti A2UI al frontend
- Creare un frontend personalizzato per visualizzare gli elementi A2UI
Prerequisiti
- Conoscenza di base degli agenti AI
- Conoscenza di base della sintassi di Python
- Conoscenza di base dei concetti di frontend
2. Configurazione
Segui le istruzioni riportate di seguito per inizializzare il progetto Google Cloud necessario per questo codelab. Dopo aver inizializzato il progetto, ti consigliamo di eseguire questo codelab su Cloud Shell, poiché include tutti gli strumenti necessari per eseguirlo immediatamente.
Se preferisci eseguire questo codelab nel tuo ambiente locale, prima di procedere devi installare Python, uv e un editor di codice. Tutte le istruzioni di questo codelab presuppongono che tu lo stia eseguendo in Cloud Shell, se non diversamente indicato.
Configurazione dell'ambiente autonomo
- Accedi alla console Google Cloud e crea un nuovo progetto o riutilizzane uno esistente. Se non hai già un account Gmail o Google Workspace, devi crearne uno.



- Il nome del progetto è il nome visualizzato per i partecipanti a questo progetto. È una stringa di caratteri non utilizzata dalle API di Google. Puoi aggiornarlo in qualsiasi momento.
- L'ID progetto è univoco in tutti i progetti Google Cloud ed è immutabile (non può essere modificato dopo l'impostazione). La console Cloud genera automaticamente una stringa univoca; in genere non ti interessa quale sia. Nella maggior parte dei codelab, dovrai fare riferimento all'ID progetto (in genere identificato come
PROJECT_ID). Se non ti piace l'ID generato, puoi generarne un altro casuale. In alternativa, puoi provare a crearne uno e verificare se è disponibile. Non può essere modificato dopo questo passaggio e rimane per tutta la durata del progetto. - Per tua informazione, esiste un terzo valore, un numero di progetto, utilizzato da alcune API. Scopri di più su tutti e tre questi valori nella documentazione.
- Poi, devi abilitare la fatturazione nella console Cloud per utilizzare le risorse/API Cloud. L'esecuzione di questo codelab non costa molto, se non nulla. Per arrestare le risorse ed evitare addebiti oltre questo tutorial, puoi eliminare le risorse che hai creato o eliminare il progetto. I nuovi utenti Google Cloud possono usufruire del programma di prova senza costi di 300$.
Avvia Cloud Shell
Sebbene Google Cloud possa essere gestito da remoto dal tuo laptop, in questo codelab utilizzerai Google Cloud Shell, un ambiente a riga di comando in esecuzione nel cloud.
Nella console Google Cloud, fai clic sull'icona di Cloud Shell nella barra degli strumenti in alto a destra:

Bastano pochi istanti per eseguire il provisioning e connettersi all'ambiente. Al termine, il risultato dovrebbe essere simile a questo:

Questa macchina virtuale include tutti gli strumenti per sviluppatori di cui avrai bisogno. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Tutto il lavoro in questo codelab può essere svolto all'interno di un browser. Non devi installare nulla.
3. Crea un nuovo agente ADK
- Crea una cartella per questo workshop denominata
a2ui_lab:
mkdir -p ~/a2ui_lab && cd ~/a2ui_lab
- Configura il gestore di pacchetti uv in questa cartella e installa le dipendenze:
uv init && uv add google-adk fastapi uvicorn a2ui-agent-sdk
- Abilita l'API AI Platform (per effettuare chiamate al modello Gemini)
gcloud services enable aiplatform.googleapis.com
- Inizializza l'agente ADK in questa cartella:
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
Dovresti visualizzare un output simile al seguente:
$ 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.
Tieni presente che uv run è un comando che esegue i comandi nel contesto del repository uv corrente, che abbiamo creato quando abbiamo eseguito uv init. Il comando adk è stato installato in questo repository quando abbiamo aggiunto la dipendenza del pacchetto google-adk.
Nella documentazione di ADK vedrai spesso i comandi adk senza il prefisso uv run, ma ogni volta che esegui i comandi in questo workshop, anteponi sempre adk a uv run in modo che venga eseguita l'utilità della riga di comando corretta.
Ora che la struttura di base dell'agente è stata creata, possiamo definire l'agente di generazione di immagini in agent.py.
- Apri l'editor di Cloud Shell con il seguente comando:
cloudshell workspace ~/a2ui_lab
- Sostituisci i contenuti di
art_creator/agent.pycon il codice riportato di seguito:
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],
)
- Ora puoi testare l'agente nell'UI di sviluppo di ADK utilizzando il comando
uv run adk web:
uv run adk web --port 8080 --allow_origins "*" --reload_agents
Poi fai clic sul pulsante Anteprima web e seleziona Anteprima sulla porta 8080. Verrà aperta l'UI di sviluppo nel browser.
Utilizza l'UI di sviluppo di ADK per testare le funzionalità dell'agente fornendo alcuni prompt, ad esempio:
- Ragazza anime che dorme sotto un albero. Colori pastello. 16:9
- Fotografia di una baita riflessa nel lago. Tardo pomeriggio. Sensazione nostalgica.
Dovresti vedere l'agente rispondere con testo e l'immagine generata.

4. Crea un frontend semplice
Ora creeremo un'app web dedicata per il nostro agente. Utilizzeremo FastAPI per eseguire il runner ADK e pubblicare una semplice interfaccia di chat a pagina singola.
Innanzitutto, arresta il server di sviluppo ADK digitando Ctrl+C nel terminale. Poi crea un file denominato main.py nella radice dell'area di lavoro (~/a2ui_lab/main.py) con i seguenti contenuti:
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"))
Poi crea la directory static per archiviare i file del frontend:
mkdir -p static
Ora aggiungi l'HTML dell'indice (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>
E il CSS di stile (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%;
}
Infine, aggiungi il controller 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;
}
});
Testa l'app web avviando il server FastAPI:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
Utilizza l'anteprima web sulla porta 8080 per accedere alla chat personalizzata. Ora puoi parlare direttamente con l'agente.

5. Configura l'agente per l'emissione di messaggi A2UI
Ora aggiorniamo l'agente in modo che restituisca un'UI strutturata anziché solo testo. Utilizzeremo l'SDK a2ui-agent-sdk ufficiale per creare un prompt di sistema con riconoscimento A2UI per l'agente.
Quando utilizziamo l'SDK A2UI, anziché definire direttamente le istruzioni dell'agente, utilizziamo la classe A2uiSchemaManager che strutturerà il prompt di sistema dell'agente per comprendere le funzionalità di generazione dell'interfaccia di A2UI, incluso l'accesso al catalogo dei componenti, allo schema completo dei componenti e agli esempi di utilizzo (se disponibili).
- Innanzitutto, arresta il server FastAPI con Ctrl+C.
- Modifica
art_creator/agent.pyper integrareA2uiSchemaManagere il nostro nuovo hooka2ui_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],
)
Nota che ora le istruzioni dell'agente vengono generate dalla chiamata schema_manager.generate_system_prompt anziché essere codificate nella definizione dell'agente.
Testa l'app web avviando il server FastAPI:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
Utilizza l'anteprima web sulla porta 8080 per accedere alla chat personalizzata. Noterai che ora l'agente emette messaggi JSON anziché testo normale. Questa è la rappresentazione interna degli elementi A2UI che visualizzeremo nella sezione successiva.

6. Crea un frontend personalizzato per l'agente
A questo punto, il nostro client riceve un elenco di messaggi A2UI puliti (beginRendering, surfaceUpdate e dataModelUpdate). Ora creeremo un motore di rendering lato client personalizzato in JavaScript semplice per vedere questi componenti in azione.
Ecco il file static/app.js completo con la logica di analisi e rendering 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;
}
});
Riavvia il server dell'app FastAPI:
uv run python -m uvicorn main:app --port 8080 --host 0.0.0.0
E parla con il tuo agente di creazione di opere d'arte A2UI completamente dinamico.

7. Complimenti!
Hai creato un agente ADK che genera dinamicamente elementi UI utilizzando A2UI. Puoi continuare il tuo percorso di apprendimento esplorando le diverse integrazioni del framework o la documentazione nei riferimenti riportati di seguito.
Crea un frontend di produzione
In questo workshop abbiamo utilizzato un frontend JS puro personalizzato per scopi didattici, ma per la produzione creeresti un frontend utilizzando uno dei renderer A2UI ufficiali:
Piattaforma | Renderer | Installa |
Web (React) | @a2ui/react | npm install @a2ui/react |
Web (Lit) | @a2ui/lit | npm install @a2ui/lit |
Web (Angular) | @a2ui/angular | npm install @a2ui/angular |
Dispositivi mobili/computer desktop | SDK Flutter GenUI |