From 6153b1304a621fdcb9c9de9e43fb23b72981d59b Mon Sep 17 00:00:00 2001 From: hrithik18k Date: Sat, 25 Apr 2026 11:15:48 +0530 Subject: [PATCH 1/3] feat(agent-sdk/template): define template schema and data-binding syntax (#721) - Add JSON Schema (schema.json) for the A2UI template format - Defines required fields: name, version, catalogId, components - Defines optional fields: description, surfaceId, theme, dataSchema, dataModel - Uses JSON Schema draft 2020-12 consistent with the v0.9 spec - Add placeholder syntax specification (template_schema.md) - Formalizes {{path.to.field}} expression syntax and resolution rules - Documents type preservation (whole-string) vs string coercion (inline) - Specifies error behavior: KeyError on missing keys, no silent fallbacks - Clarifies relationship between {{}} (server-side inflation) and {"path": "..."} (client-side data binding) - Documents LLM response format (templateName + data) - Shows inflated output example (createSurface + updateComponents + updateDataModel) - Add 5 reference template examples: - restaurant-card: restaurant display with booking button - contact-card: contact info with icons and message button - product-detail: product with image and add-to-cart button - weather-summary: weather conditions card - booking-form: interactive form demonstrating {{}} + data binding coexistence - Add Template-based Inference section to agent_development.md All templates validated against schema.json via jsonschema. Part of #657. Closes #721. --- agent_sdks/python/agent_development.md | 70 +++++ .../a2ui/template/examples/booking_form.json | 109 +++++++ .../a2ui/template/examples/contact_card.json | 119 ++++++++ .../template/examples/product_detail.json | 95 ++++++ .../template/examples/restaurant_card.json | 100 ++++++ .../template/examples/weather_summary.json | 97 ++++++ .../python/src/a2ui/template/schema.json | 78 +++++ .../src/a2ui/template/template_schema.md | 289 ++++++++++++++++++ 8 files changed, 957 insertions(+) create mode 100644 agent_sdks/python/src/a2ui/template/examples/booking_form.json create mode 100644 agent_sdks/python/src/a2ui/template/examples/contact_card.json create mode 100644 agent_sdks/python/src/a2ui/template/examples/product_detail.json create mode 100644 agent_sdks/python/src/a2ui/template/examples/restaurant_card.json create mode 100644 agent_sdks/python/src/a2ui/template/examples/weather_summary.json create mode 100644 agent_sdks/python/src/a2ui/template/schema.json create mode 100644 agent_sdks/python/src/a2ui/template/template_schema.md diff --git a/agent_sdks/python/agent_development.md b/agent_sdks/python/agent_development.md index 04bb588b5..0799e8c27 100644 --- a/agent_sdks/python/agent_development.md +++ b/agent_sdks/python/agent_development.md @@ -399,4 +399,74 @@ agent_card = AgentCard( ``` +### 5. Template-based Inference + +For agents that render a fixed set of known UI layouts, **template-based +inference** offers a more efficient alternative to raw schema-based generation. +Instead of the LLM generating full A2UI JSON payloads, it selects a pre-defined +template and provides only the dynamic data. + +**Benefits:** +- **Smaller LLM output** — the model returns a template name + data, not an + entire component tree. +- **Guaranteed valid UI** — templates are pre-validated at design time. +- **Visual authoring** — templates can be created in the A2UI Composer. + +#### How It Works + +1. The agent registers templates with the `A2uiTemplateManager`. +2. The LLM receives a catalog of template names and their `dataSchema`. +3. The LLM returns a compact response: + ```json + { + "templateName": "restaurant-card", + "data": { + "restaurant": { + "name": "The French Bistro", + "cuisine": "French", + "rating": 4.7 + } + } + } + ``` +4. The SDK inflates `{{}}` placeholders and produces standard v0.9 A2UI messages. + +#### Template Format + +Templates are stored as JSON files in the `template/examples/` directory. Each +template contains: + +- **`name`** — Unique kebab-case identifier (e.g. `restaurant-card`). +- **`catalogId`** — The A2UI catalog the components belong to. +- **`dataSchema`** — JSON Schema describing required LLM data. +- **`components`** — Flat list of v0.9 components with `{{placeholder}}` + expressions. +- **`dataModel`** (optional) — Default values for the data model. + +#### Placeholder Syntax + +String values may contain `{{path.to.field}}` expressions that resolve against +the LLM-provided `data`: + +```json +{"text": "{{restaurant.name}}"} // Whole-string → preserves type +{"text": "Rating: {{restaurant.rating}}"} // Inline → string interpolation +``` + +See [`template/template_schema.md`](src/a2ui/template/template_schema.md) for +the full specification including resolution rules, error behavior, and the +distinction between `{{}}` (server-side inflation) and `{"path": "..."}` +(client-side data binding). + +#### Reference Templates + +The SDK includes 5 reference templates in +[`template/examples/`](src/a2ui/template/examples/): +| Template | Use Case | +|----------|----------| +| `restaurant-card` | Restaurant display with booking button | +| `contact-card` | Contact info with message button | +| `product-detail` | Product with add-to-cart button | +| `weather-summary` | Weather conditions card | +| `booking-form` | Interactive reservation form (demonstrates `{{}}` + data binding coexistence) | diff --git a/agent_sdks/python/src/a2ui/template/examples/booking_form.json b/agent_sdks/python/src/a2ui/template/examples/booking_form.json new file mode 100644 index 000000000..8f96cac0a --- /dev/null +++ b/agent_sdks/python/src/a2ui/template/examples/booking_form.json @@ -0,0 +1,109 @@ +{ + "name": "booking-form", + "description": "An interactive reservation form with date/time picker, party size input, dietary requirements, and a submit button. Uses data binding for live form state.", + "version": "1.0.0", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "dataSchema": { + "type": "object", + "required": ["booking"], + "properties": { + "booking": { + "type": "object", + "required": ["restaurantName"], + "properties": { + "restaurantName": { "type": "string", "description": "Name of the restaurant to book." }, + "imageUrl": { "type": "string", "description": "Restaurant image URL." }, + "address": { "type": "string", "description": "Restaurant address." }, + "minDate": { "type": "string", "format": "date", "description": "Earliest available booking date (ISO 8601)." }, + "defaultPartySize": { "type": "string", "description": "Suggested party size." } + } + } + } + }, + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "form-title", + "restaurant-image", + "restaurant-address", + "party-size-field", + "datetime-field", + "dietary-field", + "submit-btn" + ] + }, + { + "id": "form-title", + "component": "Text", + "variant": "h2", + "text": "Book a Table at {{booking.restaurantName}}" + }, + { + "id": "restaurant-image", + "component": "Image", + "url": "{{booking.imageUrl}}", + "description": "Photo of {{booking.restaurantName}}" + }, + { + "id": "restaurant-address", + "component": "Text", + "variant": "caption", + "text": "{{booking.address}}" + }, + { + "id": "party-size-field", + "component": "TextField", + "label": "Party Size", + "value": { "path": "/partySize" }, + "variant": "number" + }, + { + "id": "datetime-field", + "component": "DateTimeInput", + "label": "Date & Time", + "value": { "path": "/reservationTime" }, + "enableDate": true, + "enableTime": true, + "min": "{{booking.minDate}}" + }, + { + "id": "dietary-field", + "component": "TextField", + "label": "Dietary Requirements", + "value": { "path": "/dietary" } + }, + { + "id": "submit-btn-text", + "component": "Text", + "text": "Submit Reservation" + }, + { + "id": "submit-btn", + "component": "Button", + "child": "submit-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "submit_booking", + "context": { + "restaurantName": "{{booking.restaurantName}}", + "partySize": { "path": "/partySize" }, + "reservationTime": { "path": "/reservationTime" }, + "dietary": { "path": "/dietary" } + } + } + } + } + ], + "dataModel": { + "title": "Book a Table at {{booking.restaurantName}}", + "restaurantName": "{{booking.restaurantName}}", + "address": "{{booking.address}}", + "partySize": "{{booking.defaultPartySize}}", + "reservationTime": "", + "dietary": "", + "imageUrl": "{{booking.imageUrl}}" + } +} diff --git a/agent_sdks/python/src/a2ui/template/examples/contact_card.json b/agent_sdks/python/src/a2ui/template/examples/contact_card.json new file mode 100644 index 000000000..f609f3897 --- /dev/null +++ b/agent_sdks/python/src/a2ui/template/examples/contact_card.json @@ -0,0 +1,119 @@ +{ + "name": "contact-card", + "description": "Displays a contact with name, email, phone number, and a button to send a message.", + "version": "1.0.0", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "dataSchema": { + "type": "object", + "required": ["contact"], + "properties": { + "contact": { + "type": "object", + "required": ["name", "email", "phone"], + "properties": { + "name": { "type": "string", "description": "Full name of the contact." }, + "email": { "type": "string", "description": "Email address." }, + "phone": { "type": "string", "description": "Phone number." }, + "role": { "type": "string", "description": "Job title or role." } + } + } + } + }, + "components": [ + { + "id": "root", + "component": "Card", + "child": "card-layout" + }, + { + "id": "card-layout", + "component": "Column", + "children": [ + "contact-name", + "contact-role", + "divider-1", + "email-row", + "phone-row", + "divider-2", + "message-btn" + ] + }, + { + "id": "contact-name", + "component": "Text", + "variant": "h2", + "text": "{{contact.name}}" + }, + { + "id": "contact-role", + "component": "Text", + "variant": "caption", + "text": "{{contact.role}}" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "email-row", + "component": "Row", + "children": ["email-icon", "email-text"], + "align": "center" + }, + { + "id": "email-icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "email-text", + "component": "Text", + "text": "{{contact.email}}" + }, + { + "id": "phone-row", + "component": "Row", + "children": ["phone-icon", "phone-text"], + "align": "center" + }, + { + "id": "phone-icon", + "component": "Icon", + "name": "phone" + }, + { + "id": "phone-text", + "component": "Text", + "text": "{{contact.phone}}" + }, + { + "id": "divider-2", + "component": "Divider" + }, + { + "id": "message-btn-text", + "component": "Text", + "text": "Send Message" + }, + { + "id": "message-btn", + "component": "Button", + "child": "message-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "send_message", + "context": { + "contactName": "{{contact.name}}", + "contactEmail": "{{contact.email}}" + } + } + } + } + ], + "dataModel": { + "contactName": "{{contact.name}}", + "contactEmail": "{{contact.email}}", + "contactPhone": "{{contact.phone}}" + } +} diff --git a/agent_sdks/python/src/a2ui/template/examples/product_detail.json b/agent_sdks/python/src/a2ui/template/examples/product_detail.json new file mode 100644 index 000000000..de1a1938f --- /dev/null +++ b/agent_sdks/python/src/a2ui/template/examples/product_detail.json @@ -0,0 +1,95 @@ +{ + "name": "product-detail", + "description": "Displays a product with title, image, price, description, and an add-to-cart button.", + "version": "1.0.0", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "dataSchema": { + "type": "object", + "required": ["product"], + "properties": { + "product": { + "type": "object", + "required": ["title", "price", "description"], + "properties": { + "title": { "type": "string", "description": "Product name." }, + "price": { "type": "string", "description": "Formatted price string (e.g. '$29.99')." }, + "description": { "type": "string", "description": "Product description." }, + "imageUrl": { "type": "string", "description": "Product image URL." }, + "sku": { "type": "string", "description": "Stock-keeping unit identifier." } + } + } + } + }, + "components": [ + { + "id": "root", + "component": "Card", + "child": "product-layout" + }, + { + "id": "product-layout", + "component": "Column", + "children": [ + "product-image", + "product-title", + "product-price", + "divider-1", + "product-desc", + "cart-btn" + ] + }, + { + "id": "product-image", + "component": "Image", + "url": "{{product.imageUrl}}", + "description": "Image of {{product.title}}", + "variant": "largeFeature" + }, + { + "id": "product-title", + "component": "Text", + "variant": "h2", + "text": "{{product.title}}" + }, + { + "id": "product-price", + "component": "Text", + "variant": "h3", + "text": "{{product.price}}" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "product-desc", + "component": "Text", + "text": "{{product.description}}" + }, + { + "id": "cart-btn-text", + "component": "Text", + "text": "Add to Cart" + }, + { + "id": "cart-btn", + "component": "Button", + "child": "cart-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "add_to_cart", + "context": { + "productTitle": "{{product.title}}", + "productSku": "{{product.sku}}", + "productPrice": "{{product.price}}" + } + } + } + } + ], + "dataModel": { + "productTitle": "{{product.title}}", + "productPrice": "{{product.price}}" + } +} diff --git a/agent_sdks/python/src/a2ui/template/examples/restaurant_card.json b/agent_sdks/python/src/a2ui/template/examples/restaurant_card.json new file mode 100644 index 000000000..24f8bce53 --- /dev/null +++ b/agent_sdks/python/src/a2ui/template/examples/restaurant_card.json @@ -0,0 +1,100 @@ +{ + "name": "restaurant-card", + "description": "Displays a restaurant with its name, cuisine type, star rating, and a booking button.", + "version": "1.0.0", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "dataSchema": { + "type": "object", + "required": ["restaurant"], + "properties": { + "restaurant": { + "type": "object", + "required": ["name", "cuisine", "rating"], + "properties": { + "name": { "type": "string", "description": "The restaurant name." }, + "cuisine": { "type": "string", "description": "The cuisine type (e.g. French, Italian)." }, + "rating": { "type": "number", "description": "Star rating from 0 to 5." }, + "imageUrl": { "type": "string", "description": "URL of the restaurant image." }, + "address": { "type": "string", "description": "The restaurant street address." } + } + } + } + }, + "components": [ + { + "id": "root", + "component": "Card", + "child": "card-content" + }, + { + "id": "card-content", + "component": "Column", + "children": [ + "restaurant-image", + "restaurant-name", + "cuisine-text", + "rating-text", + "divider-1", + "address-text", + "book-btn" + ] + }, + { + "id": "restaurant-image", + "component": "Image", + "url": "{{restaurant.imageUrl}}", + "description": "Photo of {{restaurant.name}}", + "variant": "header" + }, + { + "id": "restaurant-name", + "component": "Text", + "variant": "h2", + "text": "{{restaurant.name}}" + }, + { + "id": "cuisine-text", + "component": "Text", + "text": "Cuisine: {{restaurant.cuisine}}" + }, + { + "id": "rating-text", + "component": "Text", + "text": "Rating: {{restaurant.rating}} / 5" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "address-text", + "component": "Text", + "variant": "caption", + "text": "{{restaurant.address}}" + }, + { + "id": "book-btn-text", + "component": "Text", + "text": "Book a Table" + }, + { + "id": "book-btn", + "component": "Button", + "child": "book-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "book_restaurant", + "context": { + "restaurantName": "{{restaurant.name}}" + } + } + } + } + ], + "dataModel": { + "restaurantName": "{{restaurant.name}}", + "cuisine": "{{restaurant.cuisine}}", + "rating": "{{restaurant.rating}}" + } +} diff --git a/agent_sdks/python/src/a2ui/template/examples/weather_summary.json b/agent_sdks/python/src/a2ui/template/examples/weather_summary.json new file mode 100644 index 000000000..65793ad7b --- /dev/null +++ b/agent_sdks/python/src/a2ui/template/examples/weather_summary.json @@ -0,0 +1,97 @@ +{ + "name": "weather-summary", + "description": "Displays current weather conditions for a location including temperature, condition, and a brief forecast.", + "version": "1.0.0", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "dataSchema": { + "type": "object", + "required": ["weather"], + "properties": { + "weather": { + "type": "object", + "required": ["location", "temperature", "condition"], + "properties": { + "location": { "type": "string", "description": "City or location name." }, + "temperature": { "type": "string", "description": "Current temperature with unit (e.g. '72°F')." }, + "condition": { "type": "string", "description": "Weather condition (e.g. 'Sunny', 'Rainy')." }, + "humidity": { "type": "string", "description": "Humidity percentage (e.g. '65%')." }, + "forecast": { "type": "string", "description": "Brief forecast summary for the next period." } + } + } + } + }, + "components": [ + { + "id": "root", + "component": "Card", + "child": "weather-layout" + }, + { + "id": "weather-layout", + "component": "Column", + "children": [ + "location-heading", + "temp-condition-row", + "divider-1", + "details-row", + "divider-2", + "forecast-text" + ] + }, + { + "id": "location-heading", + "component": "Text", + "variant": "h2", + "text": "{{weather.location}}" + }, + { + "id": "temp-condition-row", + "component": "Row", + "children": ["temp-text", "condition-text"], + "justify": "spaceBetween", + "align": "center" + }, + { + "id": "temp-text", + "component": "Text", + "variant": "h1", + "text": "{{weather.temperature}}" + }, + { + "id": "condition-text", + "component": "Text", + "variant": "h3", + "text": "{{weather.condition}}" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "details-row", + "component": "Row", + "children": ["humidity-text"], + "justify": "start" + }, + { + "id": "humidity-text", + "component": "Text", + "variant": "caption", + "text": "Humidity: {{weather.humidity}}" + }, + { + "id": "divider-2", + "component": "Divider" + }, + { + "id": "forecast-text", + "component": "Text", + "text": "{{weather.forecast}}" + } + ], + "dataModel": { + "location": "{{weather.location}}", + "temperature": "{{weather.temperature}}", + "condition": "{{weather.condition}}" + } +} diff --git a/agent_sdks/python/src/a2ui/template/schema.json b/agent_sdks/python/src/a2ui/template/schema.json new file mode 100644 index 000000000..99ee70bf9 --- /dev/null +++ b/agent_sdks/python/src/a2ui/template/schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/agent_sdk/template/schema.json", + "title": "A2UI Template", + "description": "Defines a reusable A2UI UI template. Templates contain static component trees with {{placeholder}} expressions that are resolved at inflation time using data provided by an LLM. The inflated output is a standard sequence of v0.9 A2UI messages.", + "type": "object", + "required": ["name", "version", "catalogId", "components"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "A unique, human-readable identifier for this template. Must be kebab-case. This is the value the LLM returns in its 'templateName' response field.", + "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$", + "examples": ["restaurant-card", "booking-form", "contact-card"] + }, + "description": { + "type": "string", + "description": "A concise, human-readable summary of the UI this template renders. Included in LLM prompts to help the model choose the right template." + }, + "version": { + "type": "string", + "description": "The semantic version of this template.", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "examples": ["1.0.0"] + }, + "catalogId": { + "type": "string", + "description": "The A2UI catalog that this template's components belong to. Must match a valid catalog ID (e.g. the basic catalog URL). This value is used in the generated 'createSurface' message.", + "examples": ["https://a2ui.org/specification/v0_9/basic_catalog.json"] + }, + "surfaceId": { + "type": "string", + "description": "The surface ID to use when inflating this template into A2UI messages. If omitted, defaults to the template name.", + "default": "" + }, + "theme": { + "type": "object", + "description": "Optional theme parameters for the generated surface. Passed through to the 'createSurface' message.", + "additionalProperties": true + }, + "dataSchema": { + "type": "object", + "description": "A JSON Schema object describing the shape of the data the LLM must provide to inflate this template. Used by the SDK to (1) generate LLM prompt instructions and (2) validate the LLM response at runtime.", + "additionalProperties": true + }, + "components": { + "type": "array", + "description": "A flat, ordered list of A2UI component descriptors following the v0.9 catalog format. The first component with id 'root' serves as the component tree root. String-typed property values may contain {{placeholder}} expressions that reference fields from the LLM-provided data.", + "minItems": 1, + "items": { + "$ref": "#/$defs/TemplateComponent" + } + }, + "dataModel": { + "type": "object", + "description": "An optional default data model object. Values may contain {{placeholder}} expressions. After inflation, this becomes the 'value' of an 'updateDataModel' message. Use this for initial field values, labels bound via {\"path\": \"...\"} in components, etc.", + "additionalProperties": true + } + }, + "$defs": { + "TemplateComponent": { + "type": "object", + "description": "A single A2UI component descriptor. Follows the v0.9 catalog component format with the addition that any string value may contain {{placeholder}} expressions.", + "required": ["id", "component"], + "properties": { + "id": { + "type": "string", + "description": "Unique component ID within this template." + }, + "component": { + "type": "string", + "description": "The component type name matching a catalog entry (e.g. 'Text', 'Card', 'Button', 'Column')." + } + }, + "additionalProperties": true + } + } +} diff --git a/agent_sdks/python/src/a2ui/template/template_schema.md b/agent_sdks/python/src/a2ui/template/template_schema.md new file mode 100644 index 000000000..017444af3 --- /dev/null +++ b/agent_sdks/python/src/a2ui/template/template_schema.md @@ -0,0 +1,289 @@ +# A2UI Template Schema & Data-Binding Specification + +This document defines the formal schema for A2UI templates and the syntax rules +for `{{}}` dynamic placeholder expressions used in template-based inference. + +## Overview + +A **template** is a reusable A2UI component tree stored as a JSON file. Instead +of asking the LLM to generate a full A2UI message payload from scratch (which is +large and error-prone), the agent: + +1. Presents the LLM with a **catalog of template names** and their `dataSchema`. +2. The LLM returns a compact response: a **`templateName`** + a **`data`** + object. +3. The SDK's `A2uiTemplateManager` **inflates** the template by resolving all + `{{}}` placeholders with the LLM-supplied data. +4. The inflated output is a standard sequence of **v0.9 A2UI messages** + (`createSurface`, `updateComponents`, `updateDataModel`). + +This pattern reduces LLM output size, improves UI consistency, and enables the +[A2UI Composer](https://a2ui-composer.ag-ui.com/) to visually manage template +libraries. + +### Relationship to A2UI Data Binding + +Templates use **two distinct mechanisms** for dynamic values: + +| Mechanism | Syntax | When Resolved | Who Resolves | Purpose | +|-----------|--------|---------------|-------------|---------| +| **Template placeholder** | `{{path.to.field}}` | Server-side, at inflation time | `A2uiTemplateManager` | Inject LLM-provided data into the component tree before sending to client | +| **Data binding** | `{"path": "/field"}` | Client-side, at render time | A2UI renderer | Bind UI components to the live data model for interactive two-way binding | + +Both can coexist in a single template. Template placeholders are resolved first, +producing standard A2UI messages that the client renders as usual. + +--- + +## Template File Format + +Templates are stored as JSON files and validated against +[`schema.json`](schema.json) in this package. + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `string` | Unique kebab-case identifier (e.g. `restaurant-card`). This is the value the LLM returns to select the template. | +| `version` | `string` | Semantic version of this template (e.g. `1.0.0`). | +| `catalogId` | `string` | The A2UI catalog ID that this template's components belong to. Used in the generated `createSurface` message. | +| `components` | `array` | Flat, ordered list of v0.9 component descriptors. Must contain a component with `"id": "root"`. | + +### Optional Fields + +| Field | Type | Description | +|-------|------|-------------| +| `description` | `string` | Human-readable summary. Included in LLM prompts. | +| `surfaceId` | `string` | Surface ID for the generated messages. Defaults to the template `name`. | +| `theme` | `object` | Theme parameters passed to `createSurface`. | +| `dataSchema` | `object` | JSON Schema describing the data the LLM must provide. Used for prompt generation and runtime validation. | +| `dataModel` | `object` | Default data model values. May contain `{{}}` placeholders. Becomes the `updateDataModel` message value. | + +### Example Structure + +```json +{ + "name": "restaurant-card", + "description": "Displays a restaurant with name, cuisine, rating, and a booking button.", + "version": "1.0.0", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "dataSchema": { + "type": "object", + "required": ["restaurant"], + "properties": { + "restaurant": { + "type": "object", + "required": ["name", "cuisine", "rating"], + "properties": { + "name": { "type": "string" }, + "cuisine": { "type": "string" }, + "rating": { "type": "number" } + } + } + } + }, + "components": [ + { "id": "root", "component": "Card", "child": "card-content" }, + { "id": "card-content", "component": "Column", "children": ["title", "cuisine-text", "rating-text", "book-btn"] }, + { "id": "title", "component": "Text", "variant": "h2", "text": "{{restaurant.name}}" }, + ... + ], + "dataModel": { + "restaurantName": "{{restaurant.name}}", + "cuisine": "{{restaurant.cuisine}}", + "rating": "{{restaurant.rating}}" + } +} +``` + +--- + +## Placeholder Syntax: `{{}}` + +### Basic Form + +Any string value inside `components` or `dataModel` may contain one or more +placeholder expressions: + +``` +{{path.to.value}} +``` + +Placeholders are delimited by double curly braces (`{{` and `}}`). The content +between the braces is a **dot-separated path** into the LLM-provided `data` +object. + +### Path Resolution Rules + +| Rule | Placeholder | Data | Result | +|------|-------------|------|--------| +| Top-level key | `{{name}}` | `{"name": "Café Lux"}` | `"Café Lux"` | +| Nested key | `{{restaurant.cuisine}}` | `{"restaurant": {"cuisine": "French"}}` | `"French"` | +| Array index | `{{items.0.title}}` | `{"items": [{"title": "Soup"}]}` | `"Soup"` | +| Deep nesting | `{{order.items.2.name}}` | `{"order": {"items": [{}, {}, {"name": "Wine"}]}}` | `"Wine"` | + +### Value Semantics + +The behavior depends on how the placeholder appears in the string: + +| Pattern | Example | Data | Result | Type | +|---------|---------|------|--------|------| +| **Whole-string placeholder** | `"{{rating}}"` | `{"rating": 4.7}` | `4.7` | Preserves original type (number, boolean, array, object) | +| **Inline interpolation** | `"Rating: {{rating}}/5"` | `{"rating": 4.7}` | `"Rating: 4.7/5"` | Always string (values are coerced via `str()`) | +| **Multiple placeholders** | `"{{city}}, {{country}}"` | `{"city": "Paris", "country": "France"}` | `"Paris, France"` | Always string | + +#### Whole-String Placeholder (Type Preservation) + +When a string value consists of **exactly one placeholder with no surrounding +text**, the placeholder resolves to the raw value from the data, preserving its +original JSON type: + +```json +// Template: +{"text": "{{restaurant.name}}"} // → "Café Lux" (string) +{"max": "{{slider.max}}"} // → 100 (number) +{"enableDate": "{{config.dates}}"} // → true (boolean) +``` + +This is critical for non-string component properties like `min`, `max`, +`enableDate`, etc. + +#### Inline Interpolation (String Coercion) + +When the placeholder appears alongside other text, all values are coerced to +strings via `str()`: + +```json +// Template: +{"text": "Rating: {{restaurant.rating}} stars"} +// Data: {"restaurant": {"rating": 4.7}} +// Result: {"text": "Rating: 4.7 stars"} +``` + +### Error Behavior + +| Scenario | Behavior | +|----------|----------| +| Missing key | **Raises `KeyError`** — there are no silent fallbacks or empty-string defaults. | +| Wrong type at path segment | **Raises `TypeError`** — e.g. indexing into a string. | +| Malformed placeholder (unclosed `{{`) | **Treated as literal text** — no substitution occurs. | +| Empty placeholder `{{}}` | **Raises `ValueError`** — path must be non-empty. | + +> **Design rationale:** Failing loudly on missing keys prevents the LLM from +> silently producing broken UIs. The `dataSchema` field allows the SDK to +> validate the LLM's data *before* attempting inflation. + +### Limitations (v1) + +- **No expressions:** `{{price * 1.1}}` is NOT supported. Placeholders are + pure path lookups. +- **No filters:** `{{name | uppercase}}` is NOT supported. +- **No conditionals:** `{{#if available}}` is NOT supported. +- **No default values:** `{{name || "Unknown"}}` is NOT supported. + +These may be considered for future versions if there is community demand. + +--- + +## LLM Response Format + +When an agent uses template-based inference, the LLM returns a compact JSON +response selecting a template and providing data: + +```json +{ + "templateName": "restaurant-card", + "data": { + "restaurant": { + "name": "The French Bistro", + "cuisine": "French", + "rating": 4.7 + } + } +} +``` + +The SDK: +1. Looks up `"restaurant-card"` in the registered template catalog. +2. Validates `data` against the template's `dataSchema`. +3. Inflates all `{{}}` placeholders. +4. Packages the result as a standard v0.9 A2UI message sequence: + - `createSurface` (with `catalogId` and optional `theme`) + - `updateComponents` (the inflated component tree) + - `updateDataModel` (the inflated data model, if present) + +### Multiple Templates + +The LLM may return multiple templates in a single response: + +```json +[ + { + "templateName": "restaurant-card", + "data": { "restaurant": { "name": "Café Lux", "cuisine": "French", "rating": 4.5 } } + }, + { + "templateName": "booking-form", + "data": { "booking": { "restaurantName": "Café Lux", "minDate": "2026-01-15" } } + } +] +``` + +--- + +## Inflated Output Example + +Given the `restaurant-card` template and the data above, the inflated output is: + +```json +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "restaurant-card", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "restaurant-card", + "components": [ + { "id": "root", "component": "Card", "child": "card-content" }, + { "id": "card-content", "component": "Column", "children": ["title", "cuisine-text", "rating-text", "book-btn"] }, + { "id": "title", "component": "Text", "variant": "h2", "text": "The French Bistro" }, + { "id": "cuisine-text", "component": "Text", "text": "Cuisine: French" }, + { "id": "rating-text", "component": "Text", "text": "Rating: 4.7 / 5" }, + { "id": "book-btn-text", "component": "Text", "text": "Book a Table" }, + { "id": "book-btn", "component": "Button", "child": "book-btn-text", "variant": "primary", "action": { "event": { "name": "book_restaurant", "context": { "restaurantName": "The French Bistro" } } } } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "restaurant-card", + "path": "/", + "value": { + "restaurantName": "The French Bistro", + "cuisine": "French", + "rating": 4.7 + } + } + } +] +``` + +--- + +## Reference Templates + +The [`examples/`](examples/) directory provides 5 reference templates: + +| File | Template Name | Use Case | +|------|---------------|----------| +| [`restaurant_card.json`](examples/restaurant_card.json) | `restaurant-card` | Restaurant name, cuisine, rating, and booking button | +| [`contact_card.json`](examples/contact_card.json) | `contact-card` | Contact with name, email, phone, and message button | +| [`product_detail.json`](examples/product_detail.json) | `product-detail` | Product title, price, description, and add-to-cart button | +| [`weather_summary.json`](examples/weather_summary.json) | `weather-summary` | Location, temperature, condition, and forecast | +| [`booking_form.json`](examples/booking_form.json) | `booking-form` | Interactive reservation form with date, time, and party size | From 9c224df2632bdff0282f341b0d0ed995e50d3947 Mon Sep 17 00:00:00 2001 From: hrithik18k Date: Sat, 25 Apr 2026 11:29:03 +0530 Subject: [PATCH 2/3] chore: update CHANGELOG for #721 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..b0be515ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## Unreleased + +### Added +- feat(agent-sdk/template): add template schema, `{{}}` data-binding spec, and 5 reference templates ([#721](https://github.com/google/A2UI/issues/721)) From 8b0a1970f353f4f408eb1a0829cdaa897bb40a38 Mon Sep 17 00:00:00 2001 From: hrithik18k Date: Sat, 25 Apr 2026 11:36:54 +0530 Subject: [PATCH 3/3] fix(agent-sdk/template): address Gemini code review suggestions - Add format: uri to catalogId in schema.json - Add prefixItems to enforce root as first component - Fix defaultPartySize type: string -> number in booking_form.json - Remove redundant dataModel fields from booking_form.json - Clarify dot-key limitation in template_schema.md - Add kebab-case pattern to surfaceId in schema.json --- .../python/src/a2ui/template/examples/booking_form.json | 8 ++------ agent_sdks/python/src/a2ui/template/schema.json | 8 ++++++++ agent_sdks/python/src/a2ui/template/template_schema.md | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/agent_sdks/python/src/a2ui/template/examples/booking_form.json b/agent_sdks/python/src/a2ui/template/examples/booking_form.json index 8f96cac0a..4785af5e8 100644 --- a/agent_sdks/python/src/a2ui/template/examples/booking_form.json +++ b/agent_sdks/python/src/a2ui/template/examples/booking_form.json @@ -15,7 +15,7 @@ "imageUrl": { "type": "string", "description": "Restaurant image URL." }, "address": { "type": "string", "description": "Restaurant address." }, "minDate": { "type": "string", "format": "date", "description": "Earliest available booking date (ISO 8601)." }, - "defaultPartySize": { "type": "string", "description": "Suggested party size." } + "defaultPartySize": { "type": "number", "description": "Suggested party size." } } } } @@ -98,12 +98,8 @@ } ], "dataModel": { - "title": "Book a Table at {{booking.restaurantName}}", - "restaurantName": "{{booking.restaurantName}}", - "address": "{{booking.address}}", "partySize": "{{booking.defaultPartySize}}", "reservationTime": "", - "dietary": "", - "imageUrl": "{{booking.imageUrl}}" + "dietary": "" } } diff --git a/agent_sdks/python/src/a2ui/template/schema.json b/agent_sdks/python/src/a2ui/template/schema.json index 99ee70bf9..319985a6e 100644 --- a/agent_sdks/python/src/a2ui/template/schema.json +++ b/agent_sdks/python/src/a2ui/template/schema.json @@ -25,12 +25,14 @@ }, "catalogId": { "type": "string", + "format": "uri", "description": "The A2UI catalog that this template's components belong to. Must match a valid catalog ID (e.g. the basic catalog URL). This value is used in the generated 'createSurface' message.", "examples": ["https://a2ui.org/specification/v0_9/basic_catalog.json"] }, "surfaceId": { "type": "string", "description": "The surface ID to use when inflating this template into A2UI messages. If omitted, defaults to the template name.", + "pattern": "^([a-z][a-z0-9]*(-[a-z0-9]+)*)?$", "default": "" }, "theme": { @@ -47,6 +49,12 @@ "type": "array", "description": "A flat, ordered list of A2UI component descriptors following the v0.9 catalog format. The first component with id 'root' serves as the component tree root. String-typed property values may contain {{placeholder}} expressions that reference fields from the LLM-provided data.", "minItems": 1, + "prefixItems": [ + { + "properties": { "id": { "const": "root" } }, + "required": ["id"] + } + ], "items": { "$ref": "#/$defs/TemplateComponent" } diff --git a/agent_sdks/python/src/a2ui/template/template_schema.md b/agent_sdks/python/src/a2ui/template/template_schema.md index 017444af3..6db4b4815 100644 --- a/agent_sdks/python/src/a2ui/template/template_schema.md +++ b/agent_sdks/python/src/a2ui/template/template_schema.md @@ -115,6 +115,8 @@ object. ### Path Resolution Rules +Note: Keys containing literal dots are not supported in path expressions; dots are always treated as delimiters. + | Rule | Placeholder | Data | Result | |------|-------------|------|--------| | Top-level key | `{{name}}` | `{"name": "Café Lux"}` | `"Café Lux"` |