A2UI Agent Integration: Wiring UI to Agents (the wedge devex)¶
Demonstrates the ergonomic Agent.ui() overload introduced in the A2UI
devex wedge:
.ui(spec)— declarative surface (prompt-only, no tool wiring).ui(llm_guided=True)— auto-wiresT.a2ui()+G.a2ui()for you.ui(spec, log=True)— also auto-wiresM.a2ui_log().ui(spec, validate=False)— skipsurface.validate()at build time
Plus the schema-driven helpers:
UI.form(MyPydanticModel)— generate a typed form from a BaseModelUI.paths(MyPydanticModel)— typed two-way binding proxy
Tip
What you’ll learn How to attach tools to an agent using the fluent API.
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.agent # 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
print("All A2UI agent integration assertions passed!")
See also
API reference: Agent