A2UI Agent Integration — Support Ticket Agent with Schema-Driven Forms¶
Real-world scenario: a customer support agent that collects structured ticket data via a Pydantic-driven form. Field types are reflected automatically — str→TextField, bool→CheckBox, Literal→ChoicePicker, EmailStr→validated email field. The agent can also operate in LLM-guided mode where it designs the UI dynamically based on conversation context.
Patterns: .ui(spec) — attach a declarative surface to an agent .ui(llm_guided=True) — auto-wire T.a2ui() + G.a2ui() for dynamic UI UI.form(PydanticModel) — reflect a Pydantic class into a typed form UI.paths(Model) — typo-proof binding paths with compile-time validation
Tip
What you’ll learn How to work with typed outputs.
Source: 71_a2ui_agent_integration.py
from __future__ import annotations
from typing import Literal, Optional
from pydantic import BaseModel
from adk_fluent import (
A2UIError,
A2UINotInstalled,
Agent,
BuilderError,
G,
T,
UI,
UIBinding,
UISurface,
)
from adk_fluent._ui import _UIAutoSpec # internal marker, used only for isinstance check
# ---------------------------------------------------------------------------
# 1. Declarative surface — old form-dict syntax still works.
# ---------------------------------------------------------------------------
agent = (
Agent("support", "gemini-2.5-flash")
.instruct("Help users.")
.ui(UI.form("ticket", fields={"issue": "longText", "priority": "text"}))
)
assert isinstance(agent._config["_ui_spec"], UISurface)
assert agent._config["_a2ui_auto_tool"] is False # prompt-only
assert agent._config["_a2ui_auto_guard"] is False
assert agent._config["_a2ui_validate"] is True # default
# ---------------------------------------------------------------------------
# 2. Schema-driven form (the wedge headline feature).
# ---------------------------------------------------------------------------
class TicketForm(BaseModel):
title: str
priority: Literal["low", "med", "high"]
description: str | None = None
notify: bool = True
schema_surface = UI.form(TicketForm)
assert isinstance(schema_surface, UISurface)
assert schema_surface.name == "ticketform"
# Five components: title, priority, description, notify + Text("Title") header + Submit
# Just sanity check the root has children
assert schema_surface.root is not None and len(schema_surface.root._children) >= 5
ticket_agent = Agent("ticket", "gemini-2.5-flash").instruct("Collect ticket info.").ui(schema_surface)
assert isinstance(ticket_agent._config["_ui_spec"], UISurface)
# ---------------------------------------------------------------------------
# 3. Reflective binding paths — typo-proof field references.
# ---------------------------------------------------------------------------
paths = UI.paths(TicketForm)
binding = paths.title
assert isinstance(binding, UIBinding)
assert binding.path == "/title"
try:
_ = paths.titel # typo
except AttributeError as exc:
assert "Available fields" in str(exc)
else:
raise AssertionError("UI.paths should reject unknown attributes")
# ---------------------------------------------------------------------------
# 4. LLM-guided mode — auto-wires T.a2ui() + G.a2ui() at build time.
# When 'a2ui-agent' is not installed, .build() raises BuilderError
# (chained from A2UINotInstalled). This is the fail-loud contract.
# ---------------------------------------------------------------------------
creative = Agent("creative", "gemini-2.5-flash").instruct("Build UIs.").ui(llm_guided=True)
spec = creative._config["_ui_spec"]
assert isinstance(spec, _UIAutoSpec)
assert spec._from_flag is True
assert creative._config["_a2ui_auto_tool"] is True
assert creative._config["_a2ui_auto_guard"] is True
try:
import a2ui.adk # type: ignore[import-not-found] # noqa: F401
creative.build() # succeeds when the optional dep is present
except ImportError:
try:
creative.build()
except BuilderError as exc:
assert isinstance(exc.__cause__, A2UINotInstalled)
else:
raise AssertionError("expected BuilderError when a2ui-agent is missing")
# ---------------------------------------------------------------------------
# 5. Mixing flags: declarative surface + log=True.
# ---------------------------------------------------------------------------
logged = Agent("logged", "gemini-2.5-flash").instruct("Show a confirm dialog.").ui(UI.confirm("Delete?"), log=True)
assert logged._config["_a2ui_auto_log"] is True
assert getattr(logged, "_middlewares", None) is None # log middleware applies at build
# Building wires the M.a2ui_log middleware onto the builder.
logged.build()
assert any(getattr(mw, "_hook_name", None) == "after_model_callback" for mw in logged._middlewares)
# ---------------------------------------------------------------------------
# 6. Incompatible combinations raise A2UIError early.
# ---------------------------------------------------------------------------
try:
Agent("bad", "gemini-2.5-flash").ui(UI.surface("x", UI.text("hi")), llm_guided=True)
except A2UIError as exc:
assert "incompatible" in str(exc)
else:
raise AssertionError("declarative surface + llm_guided=True should raise")
try:
Agent("bad", "gemini-2.5-flash").ui()
except A2UIError as exc:
assert "requires a spec or llm_guided=True" in str(exc)
else:
raise AssertionError(".ui() with no args and no flag should raise")
# ---------------------------------------------------------------------------
# 7. surface.validate() — opt-in static checks.
# ---------------------------------------------------------------------------
clean = UI.surface("clean", UI.text("Hi"))
assert clean.validate() is clean # no-op for clean surfaces
# ---------------------------------------------------------------------------
# 8. Cross-namespace integration — manual wiring still composes cleanly.
# ---------------------------------------------------------------------------
guard_chain = G.pii() | G.a2ui() # G.a2ui works without a2ui-agent
assert guard_chain is not None
# ---------------------------------------------------------------------------
# 9. Runnable agent: ticket collector with schema-driven form.
# ---------------------------------------------------------------------------
_ticket_builder = (
Agent("ticket_agent", "gemini-2.5-flash")
.instruct(
"You are a support ticket assistant. Collect the issue details "
"from the user using a structured form. Ask for title, priority "
"(low/med/high), and description. Use the A2UI tools to present "
"an interactive ticket form."
)
.ui(llm_guided=True)
)
try:
root_agent = _ticket_builder.build()
except Exception:
root_agent = None
See also
API reference: Agent