Skip to content

Commit 1a338ec

Browse files
committed
Add agent development guide to leverage the agent SDK
It also updates the method name from `get_effective_catalog` to `get_selected_catalog` to remove any confusion. The `fix` method is renamed to `validate_and_fix` to be more clear.
1 parent e78a161 commit 1a338ec

13 files changed

Lines changed: 261 additions & 27 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# A2A Agent Development Guide
2+
3+
This guide explains how to build AI agents that generate A2UI interfaces using the `a2ui_agent` SDK. The SDK simplifies schema management, prompt engineering, and message validation for A2A (Agent-to-Agent/Agent-to-Client) communication.
4+
5+
## Core Concepts
6+
7+
The `a2ui_agent` SDK revolves around three main classes:
8+
9+
* **`CustomCatalogConfig`**: Defines the metadata for a component catalog (name, schema path, examples path).
10+
* **`A2uiCatalog`**: Represents a processed catalog, providing methods for validation and LLM instruction rendering.
11+
* **`A2uiSchemaManager`**: The central coordinator that loads catalogs, manages versioning, and generates system prompts.
12+
13+
## Generating A2UI Messages
14+
15+
### Step 1: Set up the Schema Manager
16+
17+
The first step in any A2UI-enabled agent is initializing the `A2uiSchemaManager`.
18+
19+
```python
20+
from a2ui.inference.schema.manager import A2uiSchemaManager, CustomCatalogConfig
21+
22+
schema_manager = A2uiSchemaManager(
23+
version="0.8",
24+
basic_examples_path="path/to/basic/examples",
25+
custom_catalogs=[
26+
CustomCatalogConfig(
27+
name="my_custom_catalog",
28+
catalog_path="path/to/catalog.json",
29+
examples_path="path/to/examples"
30+
)
31+
]
32+
)
33+
```
34+
35+
Notes:
36+
- The `custom_catalogs` parameter is optional. If not provided, the schema manager will use the basic catalog maintained by the A2UI team.
37+
- The provided custom catalog must be freestanding, i.e. it should not reference any external schemas or components, except for the common types.
38+
- If you have a modular catalog that references other catalogs, refer to [Freestanding Catalogs](../../../docs/catalogs.md#freestanding-catalogs) for more information.
39+
40+
### Step 2: Generate System Prompt
41+
42+
Use the `generate_system_prompt` method to assemble the LLM's system instructions. This method takes your high-level descriptions (role, workflow, UI goals) and automatically injects the relevant A2UI JSON Schema and few-shot examples from your catalog configuration.
43+
44+
```python
45+
instruction = schema_manager.generate_system_prompt(
46+
role_description="You are a helpful assistant...",
47+
workflow_description="Analyze the request and return UI...",
48+
ui_description="Use the following components...",
49+
include_schema=True, # Injects the raw JSON schema
50+
include_examples=True, # Injects few-shot examples
51+
allowed_components=["Heading", "Text", "Button"] # Optional: prune schema to save tokens
52+
)
53+
```
54+
55+
### Step 3: Build an LLM Agent with the System Prompt
56+
57+
Configure your `LlmAgent` using the generated system instructions. This agent serves as the core logic for interpreting user queries and deciding when to generate rich UI responses.
58+
59+
```python
60+
from google.adk.agents.llm_agent import LlmAgent
61+
from google.adk.models.lite_llm import LiteLlm
62+
63+
agent = LlmAgent(
64+
model=LiteLlm(model=LITELLM_MODEL),
65+
name="Your agent name",
66+
description="Your agent description.",
67+
instruction=instruction,
68+
tools=[Your tools],
69+
)
70+
```
71+
72+
### Step 4: Process Request and Stream UI
73+
74+
The final step is to build an executor (or a custom streaming handler) that manages the runtime lifecycle of a request: running the LLM, validating the generated JSON, and streaming parts to the client.
75+
76+
#### 4a. Build an Agent Executor
77+
Build an agent executor that uses the agent to process requests.
78+
79+
```python
80+
from a2a.server.agent_execution import AgentExecutor
81+
82+
class MyAgentExecutor(AgentExecutor):
83+
def __init__(self, agent: LlmAgent, ...):
84+
self.agent = agent
85+
...
86+
87+
agent_executor = MyAgentExecutor(
88+
agent=agent,
89+
...
90+
)
91+
```
92+
93+
#### 4b. Parse, Validate, and Fix LLM Output
94+
To ensure reliability, always validate the LLM's JSON output before returning it. The SDK's `A2uiCatalog` provides a validator that checks the payload against the A2UI schema. If the payload is invalid, the validator will attempt to fix it.
95+
96+
```python
97+
# Get the catalog for the current request
98+
selected_catalog = schema_manager.get_selected_catalog()
99+
100+
try:
101+
# Parse the LLM's JSON part
102+
parsed_json = json.loads(json_string)
103+
104+
# Validate and fix against the schema
105+
selected_catalog.payload_fixer.validate_and_fix(parsed_json)
106+
except Exception as e:
107+
# Handle validation errors (e.g., log error or retry with correction prompt)
108+
print(f"Validation failed: {e}")
109+
```
110+
111+
#### 4c. Stream the A2UI Payload
112+
After parsing and validating the A2UI JSON payloads, wrap them in an A2A DataPart and stream them to the client.
113+
114+
To ensure the A2UI Renderers on the frontend recognize the data, add `{"mimeType": "application/json+a2ui"}` to the DataPart's metadata.
115+
116+
**Recommendation:** Use the [create_a2ui_datapart](src/a2ui/extension/a2ui_extension.py#L37-L54) helper method to convert A2UI JSON payloads into an A2A DataPart.
117+
118+
## Use Cases
119+
120+
### 1. Simple Agents with Static Schemas
121+
122+
For agents with a fixed set of UI capabilities, simply use the `schema_manager` to generate the system instruction.
123+
124+
**Example Samples:** [contact_lookup](../../../samples/agent/adk/contact_lookup), [restaurant_finder](../../../samples/agent/adk/restaurant_finder)
125+
126+
```python
127+
# Generate system prompt
128+
instruction = schema_manager.generate_system_prompt(
129+
role_description="You are a helpful assistant...",
130+
workflow_description="Analyze the request and return UI...",
131+
ui_description="Use the following components...",
132+
include_schema=True,
133+
include_examples=True,
134+
)
135+
136+
# Use with your LLM framework (e.g., ADK)
137+
agent = LlmAgent(instruction=instruction, ...)
138+
```
139+
140+
### 2. Dynamic Schemas (Context-Aware)
141+
142+
Some agents may need to attach different catalogs or examples depending on the user's request, client capabilities, or conversational context. This is common for dashboard-style agents that support multiple distinct visualization types (e.g., Charts vs. Maps).
143+
144+
**Example Sample:** [rizzcharts](../../../samples/agent/adk/rizzcharts)
145+
146+
#### 2a. Injecting Catalogs into Session State
147+
In a dynamic scenario, you don't provide a static catalog to the agent. Instead, you resolve the selected catalog at runtime (e.g., during session preparation) and store it in the session state.
148+
149+
```python
150+
# In your AgentExecutor subclass
151+
async def _prepare_session(self, context, run_request, runner):
152+
session = await super()._prepare_session(context, run_request, runner)
153+
154+
# 1. Determine client capabilities from metadata
155+
capabilities = context.message.metadata.get("a2ui_client_capabilities")
156+
157+
# 2. Get selected catalog and load examples
158+
a2ui_catalog = self.schema_manager.get_selected_catalog(
159+
client_ui_capabilities=capabilities
160+
)
161+
examples = self.schema_manager.load_examples(a2ui_catalog, validate=True)
162+
163+
# 3. Store in session state for tool access
164+
await runner.session_service.append_event(
165+
session,
166+
Event(
167+
actions=EventActions(
168+
state_delta={
169+
"system:a2ui_enabled": True,
170+
"system:a2ui_catalog": a2ui_catalog,
171+
"system:a2ui_examples": examples,
172+
}
173+
),
174+
),
175+
)
176+
return session
177+
```
178+
179+
#### 2b. Accessing Catalogs via Providers
180+
The `SendA2uiToClientToolset` can use **Providers**—callables that retrieve the catalog and examples from the current context state at runtime.
181+
182+
```python
183+
# Providers that read from context state
184+
def get_a2ui_catalog(ctx: ReadonlyContext):
185+
return ctx.state.get("system:a2ui_catalog")
186+
187+
def get_a2ui_examples(ctx: ReadonlyContext):
188+
return ctx.state.get("system:a2ui_examples")
189+
190+
# Initialize the toolset with providers
191+
ui_toolset = SendA2uiToClientToolset(
192+
a2ui_enabled=True,
193+
a2ui_catalog=get_a2ui_catalog,
194+
a2ui_examples=get_a2ui_examples,
195+
)
196+
```
197+
198+
#### 2c. Runtime Validation
199+
200+
When the LLM calls the UI tool, the toolset uses the dynamic catalog to:
201+
1. **Generate Instructions**: Automatically inject the specific schema and examples into the LLM's system prompt for that turn.
202+
2. **Validate and Fix Payloads**: Automatically validate and fix the LLM's generated JSON against the specific `A2uiCatalog` object's validator and auto-fixer.
203+
204+
### 3. Orchestration and Delegation
205+
206+
Orchestrator agents delegate work to sub-agents. They often need to propagate UI capabilities and handle cross-agent UI state.
207+
208+
**Example Sample:** [orchestrator](../../../samples/agent/adk/orchestrator)
209+
210+
The orchestrator inspects sub-agent capabilities and aggregates their supported catalog IDs into its own `AgentCard`.
211+
212+
```python
213+
# Aggregating capabilities from sub-agents
214+
supported_catalog_ids = set()
215+
for subagent in subagents:
216+
# ... fetch subagent_card ...
217+
for extension in subagent_card.capabilities.extensions:
218+
if extension.uri == A2UI_EXTENSION_URI:
219+
supported_catalog_ids.update(extension.params.get("supportedCatalogIds") or [])
220+
221+
# Creating the orchestrator's AgentCard
222+
agent_card = AgentCard(
223+
capabilities=AgentCapabilities(
224+
extensions=[
225+
get_a2ui_agent_extension(supported_catalog_ids=list(supported_catalog_ids))
226+
]
227+
)
228+
)
229+
```
230+
231+
232+

a2a_agents/python/a2ui_agent/src/a2ui/extension/send_a2ui_to_client_toolset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ async def run_async(
267267
)
268268

269269
a2ui_catalog = await self._resolve_a2ui_catalog(tool_context)
270-
a2ui_json_payload = a2ui_catalog.payload_fixer.fix(a2ui_json)
270+
a2ui_json_payload = a2ui_catalog.payload_fixer.validate_and_fix(a2ui_json)
271271

272272
logger.info(
273273
f"Validated call to tool {self.TOOL_NAME} with {self.A2UI_JSON_ARG_NAME}"

a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,12 +288,12 @@ def _determine_catalog(
288288
f" {list(self._supported_catalogs.keys())}"
289289
)
290290

291-
def get_effective_catalog(
291+
def get_selected_catalog(
292292
self,
293293
client_ui_capabilities: Optional[dict[str, Any]] = None,
294294
allowed_components: List[str] = [],
295295
) -> A2uiCatalog:
296-
"""Gets the effective catalog after selection and component pruning."""
296+
"""Gets the selected catalog after selection and component pruning."""
297297
catalog = self._determine_catalog(client_ui_capabilities)
298298
pruned_catalog = catalog.with_pruned_components(allowed_components)
299299
return pruned_catalog
@@ -324,7 +324,7 @@ def generate_system_prompt(
324324
if ui_description:
325325
parts.append(f"## UI Description:\n{ui_description}")
326326

327-
final_catalog = self.get_effective_catalog(
327+
final_catalog = self.get_selected_catalog(
328328
client_ui_capabilities, allowed_components
329329
)
330330

a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/payload_fixer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ def _parse(self, payload: str) -> List[Dict[str, Any]]:
5959
logger.error(f"Failed to parse JSON: {e}")
6060
raise ValueError(f"Failed to parse JSON: {e}")
6161

62-
def fix(self, payload: str) -> List[Dict[str, Any]]:
63-
"""Applies autofixes to a raw JSON string and returns the parsed payload.
62+
def validate_and_fix(self, payload: str) -> List[Dict[str, Any]]:
63+
"""Validates and applies autofixes to a raw JSON string and returns the parsed payload.
6464
6565
Args:
6666
payload: The raw JSON string from the LLM.

a2a_agents/python/a2ui_agent/tests/extension/test_send_a2ui_to_client_toolset.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ async def test_send_tool_run_async_valid():
193193
tool_context_mock.actions = MagicMock(skip_summarization=False)
194194

195195
valid_a2ui = [{"type": "Text", "text": "Hello"}]
196-
catalog_mock.payload_fixer.fix.return_value = valid_a2ui
196+
catalog_mock.payload_fixer.validate_and_fix.return_value = valid_a2ui
197197
args = {
198198
SendA2uiToClientToolset._SendA2uiJsonToClientTool.A2UI_JSON_ARG_NAME: json.dumps(
199199
valid_a2ui
@@ -218,7 +218,7 @@ async def test_send_tool_run_async_valid_list():
218218
tool_context_mock.actions = MagicMock(skip_summarization=False)
219219

220220
valid_a2ui = [{"type": "Text", "text": "Hello"}]
221-
catalog_mock.payload_fixer.fix.return_value = valid_a2ui
221+
catalog_mock.payload_fixer.validate_and_fix.return_value = valid_a2ui
222222
args = {
223223
SendA2uiToClientToolset._SendA2uiJsonToClientTool.A2UI_JSON_ARG_NAME: json.dumps(
224224
valid_a2ui
@@ -250,7 +250,9 @@ async def test_send_tool_run_async_missing_arg():
250250
@pytest.mark.asyncio
251251
async def test_send_tool_run_async_invalid_json():
252252
catalog_mock = MagicMock(spec=A2uiCatalog)
253-
catalog_mock.payload_fixer.fix.side_effect = Exception("Failed to parse JSON")
253+
catalog_mock.payload_fixer.validate_and_fix.side_effect = Exception(
254+
"Failed to parse JSON"
255+
)
254256
tool = SendA2uiToClientToolset._SendA2uiJsonToClientTool(catalog_mock, "examples")
255257
args = {
256258
SendA2uiToClientToolset._SendA2uiJsonToClientTool.A2UI_JSON_ARG_NAME: "{invalid"
@@ -263,7 +265,7 @@ async def test_send_tool_run_async_invalid_json():
263265
@pytest.mark.asyncio
264266
async def test_send_tool_run_async_schema_validation_fail():
265267
catalog_mock = MagicMock(spec=A2uiCatalog)
266-
catalog_mock.payload_fixer.fix.side_effect = Exception(
268+
catalog_mock.payload_fixer.validate_and_fix.side_effect = Exception(
267269
"'text' is a required property"
268270
)
269271
tool = SendA2uiToClientToolset._SendA2uiJsonToClientTool(catalog_mock, "examples")

a2a_agents/python/a2ui_agent/tests/inference/test_payload_fixer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_fix_payload_success_first_time():
6868
fixer = A2uiPayloadFixer(catalog_mock)
6969

7070
valid_json = '[{"type": "Text", "text": "Hello"}]'
71-
result = fixer.fix(valid_json)
71+
result = fixer.validate_and_fix(valid_json)
7272

7373
assert result == [{"type": "Text", "text": "Hello"}]
7474
catalog_mock.validator.validate.assert_called_once()
@@ -90,7 +90,7 @@ def side_effect(instance):
9090
fixer = A2uiPayloadFixer(catalog_mock)
9191

9292
malformed_json = '[{"type": "Text", "text": "Hello"},]'
93-
result = fixer.fix(malformed_json)
93+
result = fixer.validate_and_fix(malformed_json)
9494

9595
assert result == [{"type": "Text", "text": "Hello"}]
9696
assert "Initial A2UI payload validation failed" in caplog.text

a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def verify():
2323
print('Verifying A2uiSchemaManager...')
2424
try:
2525
manager = A2uiSchemaManager('0.8', schema_modifiers=[remove_strict_validation])
26-
catalog = manager.get_effective_catalog()
26+
catalog = manager.get_selected_catalog()
2727
catalog_components = catalog.catalog_schema[CATALOG_COMPONENTS_KEY]
2828
print(f'Successfully loaded 0.8: {len(catalog_components)} components')
2929
print(f'Components found: {list(catalog_components.keys())[:5]}...')
@@ -384,7 +384,7 @@ def verify():
384384

385385
try:
386386
manager = A2uiSchemaManager('0.9', schema_modifiers=[remove_strict_validation])
387-
catalog = manager.get_effective_catalog()
387+
catalog = manager.get_selected_catalog()
388388
catalog_components = catalog.catalog_schema[CATALOG_COMPONENTS_KEY]
389389
print(f'Successfully loaded 0.9: {len(catalog_components)} components')
390390
print(f'Components found: {list(catalog_components.keys())}...')

samples/agent/adk/contact_lookup/agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
142142
current_query_text = query
143143

144144
# Ensure catalog schema was loaded
145-
effective_catalog = self._schema_manager.get_effective_catalog()
146-
if self.use_ui and not effective_catalog.catalog_schema:
145+
selected_catalog = self._schema_manager.get_selected_catalog()
146+
if self.use_ui and not selected_catalog.catalog_schema:
147147
logger.error(
148148
"--- ContactAgent.stream: A2UI_SCHEMA is not loaded. "
149149
"Cannot perform UI validation. ---"
@@ -245,7 +245,7 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
245245
logger.info(
246246
"--- ContactAgent.stream: Validating against A2UI_SCHEMA... ---"
247247
)
248-
effective_catalog.validator.validate(parsed_json_data)
248+
selected_catalog.validator.validate(parsed_json_data)
249249
# --- End New Validation Steps ---
250250

251251
logger.info(

samples/agent/adk/contact_multiple_surfaces/agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
151151
current_query_text = query
152152

153153
# Ensure schema was loaded
154-
effective_catalog = self.schema_manager.get_effective_catalog()
155-
if self.use_ui and not effective_catalog.catalog_schema:
154+
selected_catalog = self.schema_manager.get_selected_catalog()
155+
if self.use_ui and not selected_catalog.catalog_schema:
156156
logger.error(
157157
"--- ContactAgent.stream: A2UI_SCHEMA is not loaded. "
158158
"Cannot perform UI validation. ---"
@@ -323,7 +323,7 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]:
323323
# logger.info(
324324
# "--- ContactAgent.stream: Validating against A2UI_SCHEMA... ---"
325325
# )
326-
# effective_catalog.validator.validate(parsed_json_data)
326+
# selected_catalog.validator.validate(parsed_json_data)
327327
# --- End New Validation Steps ---
328328

329329
logger.info(

samples/agent/adk/contact_multiple_surfaces/agent_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ async def execute(
9292
client_ui_capabilities = part.root.data["metadata"][
9393
"a2uiClientCapabilities"
9494
]
95-
catalog = agent.schema_manager.get_effective_catalog(
95+
catalog = agent.schema_manager.get_selected_catalog(
9696
client_ui_capabilities=client_ui_capabilities
9797
)
9898
catalog_schema_str = catalog.render_as_llm_instructions()

0 commit comments

Comments
 (0)