Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
70 changes: 70 additions & 0 deletions agent_sdks/python/agent_development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
105 changes: 105 additions & 0 deletions agent_sdks/python/src/a2ui/template/examples/booking_form.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"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": "number", "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": {
"partySize": "{{booking.defaultPartySize}}",
"reservationTime": "",
"dietary": ""
}
}
119 changes: 119 additions & 0 deletions agent_sdks/python/src/a2ui/template/examples/contact_card.json
Original file line number Diff line number Diff line change
@@ -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}}"
}
}
Loading