|
| 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 | + |
0 commit comments