From 085c87443eb39fe8e1dee33c8e056e5a0da3e311 Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Tue, 3 Mar 2026 10:00:35 -0800 Subject: [PATCH 1/4] Revert "Revert "feat: Upgrade A2UI examples to v0.9 schema (#751)" (#758)" This reverts commit ebad27973577e16a52af4691d096accc9a373c5e. --- .github/workflows/python_samples_build.yml | 4 + samples/agent/adk/contact_lookup/agent.py | 10 +- .../{ => 0.8}/action_confirmation.json | 0 .../examples/{ => 0.8}/contact_card.json | 0 .../examples/{ => 0.8}/contact_list.json | 0 .../examples/{ => 0.8}/follow_success.json | 0 .../examples/0.9/action_confirmation.json | 84 + .../examples/0.9/contact_card.json | 318 ++++ .../examples/0.9/contact_list.json | 141 ++ .../examples/0.9/follow_success.json | 42 + .../adk/contact_lookup/prompt_builder.py | 20 +- .../a2ui_examples.py | 247 +-- .../adk/contact_multiple_surfaces/agent.py | 27 +- .../{ => 0.8}/action_confirmation.json | 0 .../examples/{ => 0.8}/chart_node_click.json | 0 .../examples/{ => 0.8}/contact_card.json | 0 .../examples/{ => 0.8}/contact_list.json | 0 .../examples/{ => 0.8}/floor_plan.json | 0 .../examples/{ => 0.8}/multi_surface.json | 0 .../examples/{ => 0.8}/org_chart.json | 0 .../examples/0.9/action_confirmation.json | 84 + .../examples/0.9/chart_node_click.json | 329 ++++ .../examples/0.9/contact_card.json | 311 ++++ .../examples/0.9/contact_list.json | 141 ++ .../examples/0.9/floor_plan.json | 63 + .../examples/0.9/multi_surface.json | 373 ++++ .../examples/0.9/org_chart.json | 71 + .../inline_catalog_0.9.json | 1620 +++++++++++++++++ .../prompt_builder.py | 33 +- samples/agent/adk/mcp_app_proxy/agent.py | 4 +- samples/agent/adk/migrate_v08_to_v09.py | 268 +++ samples/agent/adk/restaurant_finder/agent.py | 10 +- .../examples/{ => 0.8}/booking_form.json | 0 .../examples/{ => 0.8}/confirmation.json | 0 .../{ => 0.8}/single_column_list.json | 0 .../examples/{ => 0.8}/two_column_list.json | 0 .../examples/0.9/booking_form.json | 131 ++ .../examples/0.9/confirmation.json | 100 + .../examples/0.9/single_column_list.json | 162 ++ .../examples/0.9/two_column_list.json | 247 +++ .../adk/restaurant_finder/prompt_builder.py | 14 +- samples/agent/adk/rizzcharts/agent.py | 18 +- .../0.8}/rizzcharts_catalog_definition.json | 0 .../0.9/rizzcharts_catalog_definition.json | 1218 +++++++++++++ .../rizzcharts_catalog/{ => 0.8}/chart.json | 0 .../rizzcharts_catalog/{ => 0.8}/map.json | 0 .../rizzcharts_catalog/0.9/chart.json | 87 + .../examples/rizzcharts_catalog/0.9/map.json | 86 + .../standard_catalog/{ => 0.8}/chart.json | 0 .../standard_catalog/{ => 0.8}/map.json | 0 .../examples/standard_catalog/0.9/chart.json | 94 + .../examples/standard_catalog/0.9/map.json | 101 + .../agent/adk/rizzcharts/prompt_builder.py | 84 + .../adk/tests/test_examples_validation.py | 135 ++ 54 files changed, 6527 insertions(+), 150 deletions(-) rename samples/agent/adk/contact_lookup/examples/{ => 0.8}/action_confirmation.json (100%) rename samples/agent/adk/contact_lookup/examples/{ => 0.8}/contact_card.json (100%) rename samples/agent/adk/contact_lookup/examples/{ => 0.8}/contact_list.json (100%) rename samples/agent/adk/contact_lookup/examples/{ => 0.8}/follow_success.json (100%) create mode 100644 samples/agent/adk/contact_lookup/examples/0.9/action_confirmation.json create mode 100644 samples/agent/adk/contact_lookup/examples/0.9/contact_card.json create mode 100644 samples/agent/adk/contact_lookup/examples/0.9/contact_list.json create mode 100644 samples/agent/adk/contact_lookup/examples/0.9/follow_success.json rename samples/agent/adk/contact_multiple_surfaces/examples/{ => 0.8}/action_confirmation.json (100%) rename samples/agent/adk/contact_multiple_surfaces/examples/{ => 0.8}/chart_node_click.json (100%) rename samples/agent/adk/contact_multiple_surfaces/examples/{ => 0.8}/contact_card.json (100%) rename samples/agent/adk/contact_multiple_surfaces/examples/{ => 0.8}/contact_list.json (100%) rename samples/agent/adk/contact_multiple_surfaces/examples/{ => 0.8}/floor_plan.json (100%) rename samples/agent/adk/contact_multiple_surfaces/examples/{ => 0.8}/multi_surface.json (100%) rename samples/agent/adk/contact_multiple_surfaces/examples/{ => 0.8}/org_chart.json (100%) create mode 100644 samples/agent/adk/contact_multiple_surfaces/examples/0.9/action_confirmation.json create mode 100644 samples/agent/adk/contact_multiple_surfaces/examples/0.9/chart_node_click.json create mode 100644 samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_card.json create mode 100644 samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_list.json create mode 100644 samples/agent/adk/contact_multiple_surfaces/examples/0.9/floor_plan.json create mode 100644 samples/agent/adk/contact_multiple_surfaces/examples/0.9/multi_surface.json create mode 100644 samples/agent/adk/contact_multiple_surfaces/examples/0.9/org_chart.json create mode 100644 samples/agent/adk/contact_multiple_surfaces/inline_catalog_0.9.json create mode 100644 samples/agent/adk/migrate_v08_to_v09.py rename samples/agent/adk/restaurant_finder/examples/{ => 0.8}/booking_form.json (100%) rename samples/agent/adk/restaurant_finder/examples/{ => 0.8}/confirmation.json (100%) rename samples/agent/adk/restaurant_finder/examples/{ => 0.8}/single_column_list.json (100%) rename samples/agent/adk/restaurant_finder/examples/{ => 0.8}/two_column_list.json (100%) create mode 100644 samples/agent/adk/restaurant_finder/examples/0.9/booking_form.json create mode 100644 samples/agent/adk/restaurant_finder/examples/0.9/confirmation.json create mode 100644 samples/agent/adk/restaurant_finder/examples/0.9/single_column_list.json create mode 100644 samples/agent/adk/restaurant_finder/examples/0.9/two_column_list.json rename samples/agent/adk/rizzcharts/{ => catalog_schemas/0.8}/rizzcharts_catalog_definition.json (100%) create mode 100644 samples/agent/adk/rizzcharts/catalog_schemas/0.9/rizzcharts_catalog_definition.json rename samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/{ => 0.8}/chart.json (100%) rename samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/{ => 0.8}/map.json (100%) create mode 100644 samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/chart.json create mode 100644 samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/map.json rename samples/agent/adk/rizzcharts/examples/standard_catalog/{ => 0.8}/chart.json (100%) rename samples/agent/adk/rizzcharts/examples/standard_catalog/{ => 0.8}/map.json (100%) create mode 100644 samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/chart.json create mode 100644 samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/map.json create mode 100644 samples/agent/adk/rizzcharts/prompt_builder.py create mode 100644 samples/agent/adk/tests/test_examples_validation.py diff --git a/.github/workflows/python_samples_build.yml b/.github/workflows/python_samples_build.yml index 8ed807e60..ac0fc330b 100644 --- a/.github/workflows/python_samples_build.yml +++ b/.github/workflows/python_samples_build.yml @@ -51,6 +51,10 @@ jobs: working-directory: samples/agent/adk run: uv run pyink --check . + - name: Validate Sample Examples + run: | + PYTHONPATH=agent_sdks/python/src uv run --project agent_sdks/python pytest -vv samples/agent/adk/tests/test_examples_validation.py + - name: Build contact_lookup working-directory: samples/agent/adk/contact_lookup run: uv build . diff --git a/samples/agent/adk/contact_lookup/agent.py b/samples/agent/adk/contact_lookup/agent.py index 400981394..a34d3a977 100644 --- a/samples/agent/adk/contact_lookup/agent.py +++ b/samples/agent/adk/contact_lookup/agent.py @@ -39,7 +39,7 @@ from google.genai import types from prompt_builder import get_text_prompt, ROLE_DESCRIPTION, WORKFLOW_DESCRIPTION, UI_DESCRIPTION from tools import get_contact_info -from a2ui.core.schema.constants import VERSION_0_8, A2UI_OPEN_TAG, A2UI_CLOSE_TAG +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9, A2UI_OPEN_TAG, A2UI_CLOSE_TAG from a2ui.core.schema.manager import A2uiSchemaManager from a2ui.core.parser.parser import parse_response, ResponsePart from a2ui.basic_catalog.provider import BasicCatalog @@ -62,7 +62,7 @@ def __init__(self, base_url: str): self._schema_managers: Dict[str, A2uiSchemaManager] = {} self._ui_runners: Dict[str, Runner] = {} - for version in [VERSION_0_8]: + for version in [VERSION_0_8, VERSION_0_9]: schema_manager = self._build_schema_manager(version) self._schema_managers[version] = schema_manager agent = self._build_llm_agent(schema_manager) @@ -77,7 +77,11 @@ def agent_card(self) -> AgentCard: def _build_schema_manager(self, version: str) -> A2uiSchemaManager: return A2uiSchemaManager( version=version, - catalogs=[BasicCatalog.get_config(version=version, examples_path="examples")], + catalogs=[ + BasicCatalog.get_config( + version=version, examples_path=f"examples/{version}" + ) + ], ) def _build_agent_card(self) -> AgentCard: diff --git a/samples/agent/adk/contact_lookup/examples/action_confirmation.json b/samples/agent/adk/contact_lookup/examples/0.8/action_confirmation.json similarity index 100% rename from samples/agent/adk/contact_lookup/examples/action_confirmation.json rename to samples/agent/adk/contact_lookup/examples/0.8/action_confirmation.json diff --git a/samples/agent/adk/contact_lookup/examples/contact_card.json b/samples/agent/adk/contact_lookup/examples/0.8/contact_card.json similarity index 100% rename from samples/agent/adk/contact_lookup/examples/contact_card.json rename to samples/agent/adk/contact_lookup/examples/0.8/contact_card.json diff --git a/samples/agent/adk/contact_lookup/examples/contact_list.json b/samples/agent/adk/contact_lookup/examples/0.8/contact_list.json similarity index 100% rename from samples/agent/adk/contact_lookup/examples/contact_list.json rename to samples/agent/adk/contact_lookup/examples/0.8/contact_list.json diff --git a/samples/agent/adk/contact_lookup/examples/follow_success.json b/samples/agent/adk/contact_lookup/examples/0.8/follow_success.json similarity index 100% rename from samples/agent/adk/contact_lookup/examples/follow_success.json rename to samples/agent/adk/contact_lookup/examples/0.8/follow_success.json diff --git a/samples/agent/adk/contact_lookup/examples/0.9/action_confirmation.json b/samples/agent/adk/contact_lookup/examples/0.9/action_confirmation.json new file mode 100644 index 000000000..98b6c85be --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/0.9/action_confirmation.json @@ -0,0 +1,84 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "action-modal", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#007BFF", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "action-modal", + "components": [ + { + "id": "root", + "component": "Modal", + "trigger": "hidden-entry-point", + "content": "modal-content-column" + }, + { + "id": "hidden-entry-point", + "component": "Text", + "text": "" + }, + { + "id": "modal-content-column", + "component": "Column", + "children": [ + "modal-title", + "modal-message", + "dismiss-button" + ], + "align": "center" + }, + { + "id": "modal-title", + "component": "Text", + "variant": "h2", + "text": { + "path": "/actionTitle" + } + }, + { + "id": "modal-message", + "component": "Text", + "text": { + "path": "/actionMessage" + } + }, + { + "id": "dismiss-button-text", + "component": "Text", + "text": "Dismiss" + }, + { + "id": "dismiss-button", + "component": "Button", + "child": "dismiss-button-text", + "variant": "primary", + "action": { + "event": { + "name": "dismiss_modal" + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "action-modal", + "path": "/", + "value": { + "actionTitle": "Action Confirmation", + "actionMessage": "Your action has been processed." + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_lookup/examples/0.9/contact_card.json b/samples/agent/adk/contact_lookup/examples/0.9/contact_card.json new file mode 100644 index 000000000..aebfa3366 --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/0.9/contact_card.json @@ -0,0 +1,318 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "contact-card", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "contact-card", + "components": [ + { + "id": "profile_image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "variant": "avatar", + "fit": "cover" + }, + { + "id": "user_heading", + "component": "Text", + "weight": 1, + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "description_text_1", + "component": "Text", + "text": { + "path": "/title" + } + }, + { + "id": "description_text_2", + "component": "Text", + "text": { + "path": "/team" + } + }, + { + "id": "description_column", + "component": "Column", + "children": [ + "user_heading", + "description_text_1", + "description_text_2" + ], + "align": "center" + }, + { + "id": "calendar_icon", + "component": "Icon", + "name": "calendar_today" + }, + { + "id": "calendar_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/calendar" + } + }, + { + "id": "calendar_secondary_text", + "component": "Text", + "text": "Calendar" + }, + { + "id": "calendar_text_column", + "component": "Column", + "children": [ + "calendar_primary_text", + "calendar_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_1", + "component": "Row", + "children": [ + "calendar_icon", + "calendar_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "location_icon", + "component": "Icon", + "name": "location_on" + }, + { + "id": "location_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/location" + } + }, + { + "id": "location_secondary_text", + "component": "Text", + "text": "Location" + }, + { + "id": "location_text_column", + "component": "Column", + "children": [ + "location_primary_text", + "location_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_2", + "component": "Row", + "children": [ + "location_icon", + "location_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "mail_icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "mail_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/email" + } + }, + { + "id": "mail_secondary_text", + "component": "Text", + "text": "Email" + }, + { + "id": "mail_text_column", + "component": "Column", + "children": [ + "mail_primary_text", + "mail_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_3", + "component": "Row", + "children": [ + "mail_icon", + "mail_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "div", + "component": "Divider" + }, + { + "id": "call_icon", + "component": "Icon", + "name": "call" + }, + { + "id": "call_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/mobile" + } + }, + { + "id": "call_secondary_text", + "component": "Text", + "text": "Mobile" + }, + { + "id": "call_text_column", + "component": "Column", + "children": [ + "call_primary_text", + "call_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_4", + "component": "Row", + "children": [ + "call_icon", + "call_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_rows_column", + "component": "Column", + "weight": 1, + "children": [ + "info_row_1", + "info_row_2", + "info_row_3", + "info_row_4" + ], + "align": "stretch" + }, + { + "id": "button_1_text", + "component": "Text", + "text": "Follow" + }, + { + "id": "button_1", + "component": "Button", + "child": "button_1_text", + "variant": "primary", + "action": { + "event": { + "name": "follow_contact" + } + } + }, + { + "id": "button_2_text", + "component": "Text", + "text": "Message" + }, + { + "id": "button_2", + "component": "Button", + "child": "button_2_text", + "variant": "default", + "action": { + "event": { + "name": "send_message" + } + } + }, + { + "id": "action_buttons_row", + "component": "Row", + "children": [ + "button_1", + "button_2" + ], + "justify": "center", + "align": "center" + }, + { + "id": "link_text", + "component": "Text", + "text": "[View Full Profile](/profile)" + }, + { + "id": "link_text_wrapper", + "component": "Row", + "children": [ + "link_text" + ], + "justify": "center", + "align": "center" + }, + { + "id": "main_column", + "component": "Column", + "children": [ + "profile_image", + "description_column", + "div", + "info_rows_column", + "action_buttons_row", + "link_text_wrapper" + ], + "align": "stretch" + }, + { + "id": "root", + "component": "Card", + "child": "main_column" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "contact-card", + "path": "/", + "value": { + "name": "", + "title": "", + "team": "", + "location": "", + "email": "", + "mobile": "", + "calendar": "", + "imageUrl": "" + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_lookup/examples/0.9/contact_list.json b/samples/agent/adk/contact_lookup/examples/0.9/contact_list.json new file mode 100644 index 000000000..fd2727da4 --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/0.9/contact_list.json @@ -0,0 +1,141 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "contact-list", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#007BFF", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "contact-list", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "title-heading", + "item-list" + ] + }, + { + "id": "title-heading", + "component": "Text", + "variant": "h1", + "text": "Found Contacts" + }, + { + "id": "item-list", + "component": "List", + "direction": "vertical", + "children": { + "componentId": "item-card-template", + "path": "/contacts" + } + }, + { + "id": "item-card-template", + "component": "Card", + "child": "card-layout" + }, + { + "id": "card-layout", + "component": "Row", + "children": [ + "template-image", + "card-details", + "view-button" + ], + "align": "center" + }, + { + "id": "template-image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "fit": "cover" + }, + { + "id": "card-details", + "component": "Column", + "children": [ + "template-name", + "template-title" + ] + }, + { + "id": "template-name", + "component": "Text", + "variant": "h3", + "text": { + "path": "/name" + } + }, + { + "id": "template-title", + "component": "Text", + "text": { + "path": "/title" + } + }, + { + "id": "view-button-text", + "component": "Text", + "text": "View" + }, + { + "id": "view-button", + "component": "Button", + "child": "view-button-text", + "variant": "primary", + "action": { + "event": { + "name": "view_profile", + "context": { + "contactName": { + "path": "/name" + }, + "department": { + "path": "/department" + } + } + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "contact-list", + "path": "/", + "value": { + "contacts": { + "contact1": { + "name": "Alice Wonderland", + "phone": "+1-555-123-4567", + "email": "alice@example.com", + "imageUrl": "https://example.com/alice.jpg", + "title": "Mad Hatter", + "department": "Wonderland" + }, + "contact2": { + "name": "Bob The Builder", + "phone": "+1-555-765-4321", + "email": "bob@example.com", + "imageUrl": "https://example.com/bob.jpg", + "title": "Construction", + "department": "Building" + } + } + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_lookup/examples/0.9/follow_success.json b/samples/agent/adk/contact_lookup/examples/0.9/follow_success.json new file mode 100644 index 000000000..631404ebe --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/0.9/follow_success.json @@ -0,0 +1,42 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "contact-card", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "contact-card", + "components": [ + { + "id": "success_icon", + "component": "Icon", + "name": "check" + }, + { + "id": "success_text", + "component": "Text", + "text": "Successfully Followed", + "variant": "h2" + }, + { + "id": "success_column", + "component": "Column", + "children": [ + "success_icon", + "success_text" + ], + "align": "center" + }, + { + "id": "root", + "component": "Card", + "child": "success_column" + } + ] + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_lookup/prompt_builder.py b/samples/agent/adk/contact_lookup/prompt_builder.py index 8fc94b641..ab2070d9a 100644 --- a/samples/agent/adk/contact_lookup/prompt_builder.py +++ b/samples/agent/adk/contact_lookup/prompt_builder.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from a2ui.core.schema.constants import VERSION_0_8, A2UI_OPEN_TAG, A2UI_CLOSE_TAG +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9, A2UI_OPEN_TAG, A2UI_CLOSE_TAG from a2ui.core.schema.manager import A2uiSchemaManager from a2ui.basic_catalog.provider import BasicCatalog @@ -22,14 +22,14 @@ ) WORKFLOW_DESCRIPTION = """ -Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` attribute. +Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` (for spec version v0.8) or `"variant": "primary"` (for spec version v0.9+) attribute. """ UI_DESCRIPTION = f""" - **For finding contacts (e.g., "Who is Alex Jordan?"):** a. You MUST call the `get_contact_info` tool. - b. If the tool returns a **single contact**, you MUST use the `CONTACT_CARD_EXAMPLE` template. Populate the `dataModelUpdate.contents` with the contact's details (name, title, email, etc.). - c. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. Populate the `dataModelUpdate.contents` with the list of contacts for the "contacts" key. + b. If the tool returns a **single contact**, you MUST use the `CONTACT_CARD_EXAMPLE` template. Populate the `dataModelUpdate.contents` (v0.8) or `updateDataModel.value` (v0.9+) with the contact's details (name, title, email, etc.). + c. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. Populate the `dataModelUpdate.contents` (v0.8) or `updateDataModel.value` (v0.9+) with the list of contacts for the "contacts" key. d. If the tool returns an **empty list**, respond with text only and an empty JSON list: "I couldn't find anyone by that name.{A2UI_OPEN_TAG}[]{A2UI_CLOSE_TAG}" - **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** @@ -64,16 +64,22 @@ def get_text_prompt() -> str: if __name__ == "__main__": # Example of how to use the A2UI Schema Manager to generate a system prompt + my_version = VERSION_0_9 contact_prompt = A2uiSchemaManager( - VERSION_0_8, - catalogs=[BasicCatalog.get_config(version=VERSION_0_8, examples_path="examples")], + my_version, + catalogs=[ + BasicCatalog.get_config( + version=my_version, + examples_path=f"examples/{my_version}", + ) + ], ).generate_system_prompt( role_description=ROLE_DESCRIPTION, workflow_description=WORKFLOW_DESCRIPTION, ui_description=UI_DESCRIPTION, include_schema=True, include_examples=True, - validate_examples=False, + validate_examples=False, # Use invalid examples to test retry logic ) print(contact_prompt) with open("generated_prompt.txt", "w") as f: diff --git a/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py b/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py index 915c54a0f..df99bcea6 100644 --- a/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py +++ b/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py @@ -16,27 +16,22 @@ import logging import os from pathlib import Path +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9 import jsonschema logger = logging.getLogger(__name__) -# Map logical example names (used in prompt) to filenames -EXAMPLE_FILES = { - "CONTACT_LIST_EXAMPLE": "contact_list.json", - "CONTACT_CARD_EXAMPLE": "contact_card.json", - "ACTION_CONFIRMATION_EXAMPLE": "action_confirmation.json", - "ORG_CHART_EXAMPLE": "org_chart.json", - "MULTI_SURFACE_EXAMPLE": "multi_surface.json", - "CHART_NODE_CLICK_EXAMPLE": "chart_node_click.json", -} - FLOOR_PLAN_FILE = "floor_plan.json" LOCATION_SURFACE_ID = "location-surface" -def load_floor_plan_example(html_content: str = "") -> list[dict]: +def load_floor_plan_example( + version: Optional[str] = None, html_content: str = "" +) -> list[dict]: """Constructs the JSON for the location surface displaying the floor plan.""" + if not version: + return [] import os title_suffix = ( @@ -44,97 +39,131 @@ def load_floor_plan_example(html_content: str = "") -> list[dict]: if os.environ.get("USE_MCP_SANDBOX", "true").lower() == "true" else "(iFrame)" ) - return [ - { - "beginRendering": { - "surfaceId": LOCATION_SURFACE_ID, - "root": "floor-plan-card", - } - }, - { - "surfaceUpdate": { - "surfaceId": LOCATION_SURFACE_ID, - "components": [ - { - "id": "floor-plan-card", - "component": {"Card": {"child": "floor-plan-col"}}, - }, - { - "id": "floor-plan-col", - "component": { - "Column": { - "children": { - "explicitList": [ - "floor-plan-title", - "floor-plan-comp", - "dismiss-fp", - ] - } - } - }, - }, - { - "id": "floor-plan-title", - "component": { - "Text": { - "usageHint": "h2", - "text": { - "literalString": f"Office Floor Plan {title_suffix}" - }, - } - }, - }, - { - "id": "floor-plan-comp", - "component": ( - { - "McpApp": { - "htmlContent": html_content, - "height": 400, - "allowedTools": ["chart_node_click"], - } - } - if os.environ.get("USE_MCP_SANDBOX", "true").lower() == "true" - else { - "WebFrame": { - "html": html_content, - "height": 400, - "interactionMode": "interactive", - "allowedEvents": ["chart_node_click"], - } - } - ), - }, - { - "id": "dismiss-fp-text", - "component": {"Text": {"text": {"literalString": "Close Map"}}}, - }, - { - "id": "dismiss-fp", - "component": { - "Button": { - "child": "dismiss-fp-text", - # Represents closing the FloorPlan overlay - "action": {"name": "close_modal", "context": []}, - } - }, - }, - ], - } - }, - ] + if version == VERSION_0_8: + return [ + { + "beginRendering": { + "surfaceId": LOCATION_SURFACE_ID, + "root": "floor-plan-card", + } + }, + { + "surfaceUpdate": { + "surfaceId": LOCATION_SURFACE_ID, + "components": [ + { + "id": "floor-plan-card", + "component": {"Card": {"child": "floor-plan-col"}}, + }, + { + "id": "floor-plan-col", + "component": { + "Column": { + "children": { + "explicitList": [ + "floor-plan-title", + "floor-plan-comp", + "dismiss-fp", + ] + } + } + }, + }, + { + "id": "floor-plan-title", + "component": { + "Text": { + "usageHint": "h2", + "text": { + "literalString": f"Office Floor Plan {title_suffix}" + }, + } + }, + }, + { + "id": "floor-plan-comp", + "component": ( + { + "McpApp": { + "htmlContent": html_content, + "height": 400, + "allowedTools": ["chart_node_click"], + } + } + if os.environ.get("USE_MCP_SANDBOX", "true").lower() + == "true" + else { + "WebFrame": { + "html": html_content, + "height": 400, + "interactionMode": "interactive", + "allowedEvents": ["chart_node_click"], + } + } + ), + }, + { + "id": "dismiss-fp-text", + "component": {"Text": {"text": {"literalString": "Close Map"}}}, + }, + { + "id": "dismiss-fp", + "component": { + "Button": { + "child": "dismiss-fp-text", + # Represents closing the FloorPlan overlay + "action": {"name": "close_modal", "context": []}, + } + }, + }, + ], + } + }, + ] + + if version == VERSION_0_9: + return [ + { + "version": "v0.9", + "createSurface": { + "surfaceId": LOCATION_SURFACE_ID, + "catalogId": "inline_catalog", + }, + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": LOCATION_SURFACE_ID, + "components": [{ + "id": "root", + "component": "WebFrame", + "interactionMode": "interactive", + "height": 400, + "html": html_content, + }], + }, + }, + ] -def load_close_modal_example() -> list[dict]: +def load_close_modal_example(version: Optional[str] = None) -> list[dict]: """Constructs the JSON for closing the floor plan modal.""" - return [{"deleteSurface": {"surfaceId": LOCATION_SURFACE_ID}}] + if version == VERSION_0_8: + return [{"deleteSurface": {"surfaceId": LOCATION_SURFACE_ID}}] + if version == VERSION_0_9: + return [{ + "version": "v0.9", + "deleteSurface": { + "surfaceId": LOCATION_SURFACE_ID, + }, + }] -def load_send_message_example(contact_name: str) -> str: +def load_send_message_example(contact_name: str, version: Optional[str] = None) -> str: """Constructs the JSON string for the send message confirmation.""" from pathlib import Path - examples_dir = Path(os.path.dirname(__file__)) / "examples" + examples_dir = Path(os.path.dirname(__file__)) / "examples" / version action_file = examples_dir / "action_confirmation.json" if action_file.exists(): @@ -144,12 +173,24 @@ def load_send_message_example(contact_name: str) -> str: "Your action has been processed.", f"Message sent to {contact_name}!" ) return json_content - return ( - '[{ "beginRendering": { "surfaceId": "action-modal", "root":' - ' "modal-wrapper" } }, { "surfaceUpdate": { "surfaceId": "action-modal",' - ' "components": [ { "id": "modal-wrapper", "component": { "Modal": {' - ' "entryPointChild": "hidden", "contentChild": "msg", "open": true } } },' - ' { "id": "hidden", "component": { "Text": { "text": {"literalString": "' - ' "} } } }, { "id": "msg", "component": { "Text": { "text":' - ' {"literalString": "Message Sent (Fallback)"} } } } ] } }]' - ) + if version == VERSION_0_8: + return ( + '[{ "beginRendering": { "surfaceId": "action-modal", "root":' + ' "modal-wrapper" } }, { "surfaceUpdate": { "surfaceId": "action-modal",' + ' "components": [ { "id": "modal-wrapper", "component": { "Modal": {' + ' "entryPointChild": "hidden", "contentChild": "msg", "open": true } } },' + ' { "id": "hidden", "component": { "Text": { "text": {"literalString": "' + ' "} } } }, { "id": "msg", "component": { "Text": { "text":' + ' {"literalString": "Message Sent (Fallback)"} } } } ] } }]' + ) + + if version == VERSION_0_9: + return ( + '[{ "version": "v0.9", "createSurface": {' + ' "surfaceId": "action-modal", "catalogId": "inline_catalog" } }, {' + ' "version": "v0.9", "updateComponents": { "surfaceId": "action-modal",' + ' "components": [ { "id": "root", "component": "Modal", "trigger": "hidden",' + ' "content": "msg", "open": true }, { "id": "hidden", "component": "Text",' + ' "text": " " }, { "id": "msg", "component": "Text", "text":' + ' "Message Sent (Fallback)" } ] } } ]' + ) diff --git a/samples/agent/adk/contact_multiple_surfaces/agent.py b/samples/agent/adk/contact_multiple_surfaces/agent.py index 2d5c73fa9..8b90474dc 100644 --- a/samples/agent/adk/contact_multiple_surfaces/agent.py +++ b/samples/agent/adk/contact_multiple_surfaces/agent.py @@ -21,6 +21,7 @@ import jsonschema +from a2ui_examples import load_floor_plan_example from google.adk.agents.llm_agent import LlmAgent from google.adk.artifacts import InMemoryArtifactService from google.adk.memory.in_memory_memory_service import InMemoryMemoryService @@ -63,7 +64,7 @@ def __init__(self, base_url: str): self._schema_managers: Dict[str, A2uiSchemaManager] = {} self._ui_runners: Dict[str, Runner] = {} - for version in [VERSION_0_8]: + for version in [VERSION_0_8, VERSION_0_9]: schema_manager = self._build_schema_manager(version) self._schema_managers[version] = schema_manager agent = self._build_llm_agent(schema_manager) @@ -83,7 +84,11 @@ def get_schema_manager(self, version: Optional[str]) -> Optional[A2uiSchemaManag def _build_schema_manager(self, version: str) -> A2uiSchemaManager: return A2uiSchemaManager( version=version, - catalogs=[BasicCatalog.get_config(version=version, examples_path="examples")], + catalogs=[ + BasicCatalog.get_config( + version=version, examples_path=f"examples/{version}" + ) + ], schema_modifiers=[remove_strict_validation], accepts_inline_catalogs=True, ) @@ -169,12 +174,18 @@ def _build_llm_agent( tools=[get_contact_info], ) - async def _handle_action(self, query: str) -> dict[str, Any] | None: + async def _handle_action( + self, query: str, ui_version: Optional[str] = None + ) -> dict[str, Any] | None: """Handles simulated UI actions like close_modal or view_location.""" if not query.startswith("ACTION:"): return None - from a2ui_examples import load_floor_plan_example, load_close_modal_example, load_send_message_example + from a2ui_examples import ( + load_floor_plan_example, + load_close_modal_example, + load_send_message_example, + ) if "send_message" in query: logger.info("--- ContactAgent.stream: Detected send_message ACTION ---") @@ -184,7 +195,7 @@ async def _handle_action(self, query: str) -> dict[str, Any] | None: contact_name = query.split("(contact:")[1].split(")")[0].strip() except Exception: pass - json_content = load_send_message_example(contact_name) + json_content = load_send_message_example(contact_name, ui_version) final_response_content = ( f"Message sent to {contact_name}\n" f"{A2UI_OPEN_TAG}\n{json_content}\n{A2UI_CLOSE_TAG}" @@ -225,7 +236,7 @@ async def _handle_action(self, query: str) -> dict[str, Any] | None: ), } - json_content = json.dumps(load_floor_plan_example(html_content)) + json_content = json.dumps(load_floor_plan_example(ui_version, html_content)) logger.info(f"--- ContactAgent.stream: Sending Floor Plan ---") final_response_content = ( @@ -240,7 +251,7 @@ async def _handle_action(self, query: str) -> dict[str, Any] | None: elif "close_modal" in query: logger.info("--- ContactAgent.stream: Handling close_modal ACTION ---") # Action maps to closing the FloorPlan overlay - json_content = json.dumps(load_close_modal_example()) + json_content = json.dumps(load_close_modal_example(ui_version)) final_response_content = ( f"Modal closed.\n{A2UI_OPEN_TAG}\n{json_content}\n{A2UI_CLOSE_TAG}" @@ -363,7 +374,7 @@ async def stream( logger.info(f"--- ContactAgent.stream: Received query: '{query}' ---") # --- Check for User Action --- - action_response = await self._handle_action(query) + action_response = await self._handle_action(query, ui_version) if action_response: yield action_response return diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/action_confirmation.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.8/action_confirmation.json similarity index 100% rename from samples/agent/adk/contact_multiple_surfaces/examples/action_confirmation.json rename to samples/agent/adk/contact_multiple_surfaces/examples/0.8/action_confirmation.json diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/chart_node_click.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.8/chart_node_click.json similarity index 100% rename from samples/agent/adk/contact_multiple_surfaces/examples/chart_node_click.json rename to samples/agent/adk/contact_multiple_surfaces/examples/0.8/chart_node_click.json diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/contact_card.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.8/contact_card.json similarity index 100% rename from samples/agent/adk/contact_multiple_surfaces/examples/contact_card.json rename to samples/agent/adk/contact_multiple_surfaces/examples/0.8/contact_card.json diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/contact_list.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.8/contact_list.json similarity index 100% rename from samples/agent/adk/contact_multiple_surfaces/examples/contact_list.json rename to samples/agent/adk/contact_multiple_surfaces/examples/0.8/contact_list.json diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/floor_plan.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.8/floor_plan.json similarity index 100% rename from samples/agent/adk/contact_multiple_surfaces/examples/floor_plan.json rename to samples/agent/adk/contact_multiple_surfaces/examples/0.8/floor_plan.json diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/multi_surface.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.8/multi_surface.json similarity index 100% rename from samples/agent/adk/contact_multiple_surfaces/examples/multi_surface.json rename to samples/agent/adk/contact_multiple_surfaces/examples/0.8/multi_surface.json diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/org_chart.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.8/org_chart.json similarity index 100% rename from samples/agent/adk/contact_multiple_surfaces/examples/org_chart.json rename to samples/agent/adk/contact_multiple_surfaces/examples/0.8/org_chart.json diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/0.9/action_confirmation.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/action_confirmation.json new file mode 100644 index 000000000..98b6c85be --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/action_confirmation.json @@ -0,0 +1,84 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "action-modal", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#007BFF", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "action-modal", + "components": [ + { + "id": "root", + "component": "Modal", + "trigger": "hidden-entry-point", + "content": "modal-content-column" + }, + { + "id": "hidden-entry-point", + "component": "Text", + "text": "" + }, + { + "id": "modal-content-column", + "component": "Column", + "children": [ + "modal-title", + "modal-message", + "dismiss-button" + ], + "align": "center" + }, + { + "id": "modal-title", + "component": "Text", + "variant": "h2", + "text": { + "path": "/actionTitle" + } + }, + { + "id": "modal-message", + "component": "Text", + "text": { + "path": "/actionMessage" + } + }, + { + "id": "dismiss-button-text", + "component": "Text", + "text": "Dismiss" + }, + { + "id": "dismiss-button", + "component": "Button", + "child": "dismiss-button-text", + "variant": "primary", + "action": { + "event": { + "name": "dismiss_modal" + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "action-modal", + "path": "/", + "value": { + "actionTitle": "Action Confirmation", + "actionMessage": "Your action has been processed." + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/0.9/chart_node_click.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/chart_node_click.json new file mode 100644 index 000000000..e3784e20f --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/chart_node_click.json @@ -0,0 +1,329 @@ +[ + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "contact-card", + "components": [ + { + "id": "profile_image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "variant": "avatar", + "fit": "cover" + }, + { + "id": "user_heading", + "component": "Text", + "weight": 1, + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "description_text_1", + "component": "Text", + "text": { + "path": "/title" + } + }, + { + "id": "description_text_2", + "component": "Text", + "text": { + "path": "/team" + } + }, + { + "id": "description_column", + "component": "Column", + "children": [ + "user_heading", + "description_text_1", + "description_text_2" + ], + "align": "center" + }, + { + "id": "calendar_icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "calendar_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/calendar" + } + }, + { + "id": "calendar_secondary_text", + "component": "Text", + "text": "Calendar" + }, + { + "id": "calendar_text_column", + "component": "Column", + "children": [ + "calendar_primary_text", + "calendar_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_1", + "component": "Row", + "children": [ + "calendar_icon", + "calendar_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "location_icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/location" + } + }, + { + "id": "location_secondary_text", + "component": "Text", + "text": "Location" + }, + { + "id": "location_text_column", + "component": "Column", + "children": [ + "location_primary_text", + "location_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_2", + "component": "Row", + "children": [ + "location_icon", + "location_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "mail_icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "mail_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/email" + } + }, + { + "id": "mail_secondary_text", + "component": "Text", + "text": "Email" + }, + { + "id": "mail_text_column", + "component": "Column", + "children": [ + "mail_primary_text", + "mail_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_3", + "component": "Row", + "children": [ + "mail_icon", + "mail_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "div", + "component": "Divider" + }, + { + "id": "call_icon", + "component": "Icon", + "name": "call" + }, + { + "id": "call_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/mobile" + } + }, + { + "id": "call_secondary_text", + "component": "Text", + "text": "Mobile" + }, + { + "id": "call_text_column", + "component": "Column", + "children": [ + "call_primary_text", + "call_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_4", + "component": "Row", + "children": [ + "call_icon", + "call_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_rows_column", + "component": "Column", + "weight": 1, + "children": [ + "info_row_1", + "info_row_2", + "info_row_3", + "info_row_4" + ], + "align": "stretch" + }, + { + "id": "message-button-text", + "component": "Text", + "text": "Message" + }, + { + "id": "message-button", + "component": "Button", + "child": "message-button-text", + "action": { + "event": { + "name": "send_message", + "context": { + "contactName": { + "path": "/name" + } + } + } + } + }, + { + "id": "location-button-text", + "component": "Text", + "text": "Location" + }, + { + "id": "location-button", + "component": "Button", + "child": "location-button-text", + "action": { + "event": { + "name": "view_location", + "context": { + "contactId": { + "path": "/id" + } + } + } + } + }, + { + "id": "action_buttons_row", + "component": "Row", + "children": [ + "message-button", + "location-button" + ], + "justify": "center", + "align": "center" + }, + { + "id": "main_column", + "component": "Column", + "children": [ + "profile_image", + "description_column", + "div", + "info_rows_column", + "action_buttons_row" + ], + "align": "stretch" + }, + { + "id": "root", + "component": "Card", + "child": "main_column" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "contact-card", + "path": "/", + "value": { + "name": "John Smith", + "title": "VP Marketing", + "team": "Marketing", + "location": "New York", + "email": "john.smith@example.com", + "mobile": "+1 (212) 555-0101", + "calendar": "Free", + "imageUrl": "http://localhost:10004/static/profile1.png" + } + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "org-chart-view", + "path": "/", + "value": { + "hierarchy": { + "0": { + "title": "CEO", + "name": "Jane Doe" + }, + "1": { + "title": "VP Marketing", + "name": "John Smith" + } + } + } + } + }, + { + "version": "v0.9", + "deleteSurface": { + "surfaceId": "location-modal" + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_card.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_card.json new file mode 100644 index 000000000..07f4c2626 --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_card.json @@ -0,0 +1,311 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "contact-card", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "contact-card", + "components": [ + { + "id": "profile_image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "variant": "avatar", + "fit": "cover" + }, + { + "id": "user_heading", + "component": "Text", + "weight": 1, + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "description_text_1", + "component": "Text", + "text": { + "path": "/title" + } + }, + { + "id": "description_text_2", + "component": "Text", + "text": { + "path": "/team" + } + }, + { + "id": "description_column", + "component": "Column", + "children": [ + "user_heading", + "description_text_1", + "description_text_2" + ], + "align": "center" + }, + { + "id": "calendar_icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "calendar_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/calendar" + } + }, + { + "id": "calendar_secondary_text", + "component": "Text", + "text": "Calendar" + }, + { + "id": "calendar_text_column", + "component": "Column", + "children": [ + "calendar_primary_text", + "calendar_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_1", + "component": "Row", + "children": [ + "calendar_icon", + "calendar_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "location_icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/location" + } + }, + { + "id": "location_secondary_text", + "component": "Text", + "text": "Location" + }, + { + "id": "location_text_column", + "component": "Column", + "children": [ + "location_primary_text", + "location_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_2", + "component": "Row", + "children": [ + "location_icon", + "location_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "mail_icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "mail_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/email" + } + }, + { + "id": "mail_secondary_text", + "component": "Text", + "text": "Email" + }, + { + "id": "mail_text_column", + "component": "Column", + "children": [ + "mail_primary_text", + "mail_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_3", + "component": "Row", + "children": [ + "mail_icon", + "mail_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "div", + "component": "Divider" + }, + { + "id": "call_icon", + "component": "Icon", + "name": "call" + }, + { + "id": "call_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/mobile" + } + }, + { + "id": "call_secondary_text", + "component": "Text", + "text": "Mobile" + }, + { + "id": "call_text_column", + "component": "Column", + "children": [ + "call_primary_text", + "call_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_4", + "component": "Row", + "children": [ + "call_icon", + "call_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_rows_column", + "component": "Column", + "weight": 1, + "children": [ + "info_row_1", + "info_row_2", + "info_row_3", + "info_row_4" + ], + "align": "stretch" + }, + { + "id": "message-button-text", + "component": "Text", + "text": "Message" + }, + { + "id": "message-button", + "component": "Button", + "child": "message-button-text", + "action": { + "event": { + "name": "send_message", + "context": { + "contactName": { + "path": "/name" + } + } + } + } + }, + { + "id": "location-button-text", + "component": "Text", + "text": "Location" + }, + { + "id": "location-button", + "component": "Button", + "child": "location-button-text", + "action": { + "event": { + "name": "view_location", + "context": { + "contactId": { + "path": "/id" + } + } + } + } + }, + { + "id": "action_buttons_row", + "component": "Row", + "children": [ + "message-button", + "location-button" + ], + "justify": "center", + "align": "center" + }, + { + "id": "main_column", + "component": "Column", + "children": [ + "profile_image", + "description_column", + "div", + "info_rows_column", + "action_buttons_row" + ], + "align": "stretch" + }, + { + "id": "root", + "component": "Card", + "child": "main_column" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "contact-card", + "path": "/", + "value": { + "name": "", + "title": "", + "team": "", + "location": "", + "email": "", + "mobile": "", + "calendar": "", + "imageUrl": "" + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_list.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_list.json new file mode 100644 index 000000000..fd2727da4 --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/contact_list.json @@ -0,0 +1,141 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "contact-list", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#007BFF", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "contact-list", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "title-heading", + "item-list" + ] + }, + { + "id": "title-heading", + "component": "Text", + "variant": "h1", + "text": "Found Contacts" + }, + { + "id": "item-list", + "component": "List", + "direction": "vertical", + "children": { + "componentId": "item-card-template", + "path": "/contacts" + } + }, + { + "id": "item-card-template", + "component": "Card", + "child": "card-layout" + }, + { + "id": "card-layout", + "component": "Row", + "children": [ + "template-image", + "card-details", + "view-button" + ], + "align": "center" + }, + { + "id": "template-image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "fit": "cover" + }, + { + "id": "card-details", + "component": "Column", + "children": [ + "template-name", + "template-title" + ] + }, + { + "id": "template-name", + "component": "Text", + "variant": "h3", + "text": { + "path": "/name" + } + }, + { + "id": "template-title", + "component": "Text", + "text": { + "path": "/title" + } + }, + { + "id": "view-button-text", + "component": "Text", + "text": "View" + }, + { + "id": "view-button", + "component": "Button", + "child": "view-button-text", + "variant": "primary", + "action": { + "event": { + "name": "view_profile", + "context": { + "contactName": { + "path": "/name" + }, + "department": { + "path": "/department" + } + } + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "contact-list", + "path": "/", + "value": { + "contacts": { + "contact1": { + "name": "Alice Wonderland", + "phone": "+1-555-123-4567", + "email": "alice@example.com", + "imageUrl": "https://example.com/alice.jpg", + "title": "Mad Hatter", + "department": "Wonderland" + }, + "contact2": { + "name": "Bob The Builder", + "phone": "+1-555-765-4321", + "email": "bob@example.com", + "imageUrl": "https://example.com/bob.jpg", + "title": "Construction", + "department": "Building" + } + } + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/0.9/floor_plan.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/floor_plan.json new file mode 100644 index 000000000..cc21bf4a6 --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/floor_plan.json @@ -0,0 +1,63 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "location-surface", + "catalogId": "inline_catalog" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "location-surface", + "components": [ + { + "id": "root", + "component": "Card", + "child": "floor-plan-col" + }, + { + "id": "floor-plan-col", + "component": "Column", + "children": [ + "floor-plan-title", + "floor-plan-comp", + "dismiss-fp" + ] + }, + { + "id": "floor-plan-title", + "component": "Text", + "variant": "h2", + "text": "Office Floor Plan" + }, + { + "id": "floor-plan-comp", + "component": "WebFrame", + "url": "http://localhost:10004/static/floorplan.html?data=%7B%22mappings%22%3A%20%5B%7B%22deskId%22%3A%20%22desk-1%22%2C%20%22contactId%22%3A%20%224%22%2C%20%22contactName%22%3A%20%22Jane%20Doe%22%7D%2C%20%7B%22deskId%22%3A%20%22desk-2%22%2C%20%22contactId%22%3A%20%221%22%2C%20%22contactName%22%3A%20%22Alex%20Jordan%22%7D%2C%20%7B%22deskId%22%3A%20%22desk-3%22%2C%20%22contactId%22%3A%20%223%22%2C%20%22contactName%22%3A%20%22Jordan%20Taylor%22%7D%2C%20%7B%22deskId%22%3A%20%22desk-4%22%2C%20%22contactId%22%3A%20%225%22%2C%20%22contactName%22%3A%20%22John%20Smith%22%7D%2C%20%7B%22deskId%22%3A%20%22desk-5%22%2C%20%22contactId%22%3A%20%226%22%2C%20%22contactName%22%3A%20%22Alice%20Johnson%22%7D%2C%20%7B%22deskId%22%3A%20%22desk-7%22%2C%20%22contactId%22%3A%20%222%22%2C%20%22contactName%22%3A%20%22Casey%20Smith%22%7D%5D%7D", + "height": 400, + "interactionMode": "interactive", + "allowedEvents": [ + "chart_node_click" + ] + }, + { + "id": "dismiss-fp-text", + "component": "Text", + "text": "Close Map" + }, + { + "id": "dismiss-fp", + "component": "Button", + "child": "dismiss-fp-text", + "action": { + "event": { + "name": "close_modal", + "context": {} + } + } + } + ] + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/0.9/multi_surface.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/multi_surface.json new file mode 100644 index 000000000..ccaa04f80 --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/multi_surface.json @@ -0,0 +1,373 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "contact-card", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json" + } + }, + { + "version": "v0.9", + "createSurface": { + "surfaceId": "org-chart-view", + "catalogId": "inline_catalog" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "contact-card", + "components": [ + { + "id": "profile_image", + "component": "Image", + "url": { + "path": "/imageUrl" + }, + "variant": "avatar", + "fit": "cover" + }, + { + "id": "user_heading", + "component": "Text", + "weight": 1, + "text": { + "path": "/name" + }, + "variant": "h2" + }, + { + "id": "description_text_1", + "component": "Text", + "text": { + "path": "/title" + } + }, + { + "id": "description_text_2", + "component": "Text", + "text": { + "path": "/team" + } + }, + { + "id": "description_column", + "component": "Column", + "children": [ + "user_heading", + "description_text_1", + "description_text_2" + ], + "align": "center" + }, + { + "id": "calendar_icon", + "component": "Icon", + "name": "calendarToday" + }, + { + "id": "calendar_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/calendar" + } + }, + { + "id": "calendar_secondary_text", + "component": "Text", + "text": "Calendar" + }, + { + "id": "calendar_text_column", + "component": "Column", + "children": [ + "calendar_primary_text", + "calendar_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_1", + "component": "Row", + "children": [ + "calendar_icon", + "calendar_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "location_icon", + "component": "Icon", + "name": "locationOn" + }, + { + "id": "location_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/location" + } + }, + { + "id": "location_secondary_text", + "component": "Text", + "text": "Location" + }, + { + "id": "location_text_column", + "component": "Column", + "children": [ + "location_primary_text", + "location_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_2", + "component": "Row", + "children": [ + "location_icon", + "location_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "mail_icon", + "component": "Icon", + "name": "mail" + }, + { + "id": "mail_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/email" + } + }, + { + "id": "mail_secondary_text", + "component": "Text", + "text": "Email" + }, + { + "id": "mail_text_column", + "component": "Column", + "children": [ + "mail_primary_text", + "mail_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_3", + "component": "Row", + "children": [ + "mail_icon", + "mail_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "div", + "component": "Divider" + }, + { + "id": "call_icon", + "component": "Icon", + "name": "call" + }, + { + "id": "call_primary_text", + "component": "Text", + "variant": "h5", + "text": { + "path": "/mobile" + } + }, + { + "id": "call_secondary_text", + "component": "Text", + "text": "Mobile" + }, + { + "id": "call_text_column", + "component": "Column", + "children": [ + "call_primary_text", + "call_secondary_text" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_row_4", + "component": "Row", + "children": [ + "call_icon", + "call_text_column" + ], + "justify": "start", + "align": "start" + }, + { + "id": "info_rows_column", + "component": "Column", + "weight": 1, + "children": [ + "info_row_1", + "info_row_2", + "info_row_3", + "info_row_4" + ], + "align": "stretch" + }, + { + "id": "message-button-text", + "component": "Text", + "text": "Message" + }, + { + "id": "message-button", + "component": "Button", + "child": "message-button-text", + "action": { + "event": { + "name": "send_message", + "context": { + "contactName": { + "path": "/name" + } + } + } + } + }, + { + "id": "location-button-text", + "component": "Text", + "text": "Location" + }, + { + "id": "location-button", + "component": "Button", + "child": "location-button-text", + "action": { + "event": { + "name": "view_location", + "context": { + "contactId": { + "path": "/id" + } + } + } + } + }, + { + "id": "action_buttons_row", + "component": "Row", + "children": [ + "message-button", + "location-button" + ], + "justify": "center", + "align": "center" + }, + { + "id": "main_column", + "component": "Column", + "children": [ + "profile_image", + "description_column", + "div", + "info_rows_column", + "action_buttons_row" + ], + "align": "stretch" + }, + { + "id": "root", + "component": "Card", + "child": "main_column" + } + ] + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "org-chart-view", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "org-chart" + ] + }, + { + "id": "org-chart", + "component": "OrgChart", + "chain": { + "path": "/hierarchy" + }, + "action": { + "event": { + "name": "chart_node_click", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "contact-card", + "path": "/", + "value": { + "name": "Casey Smith", + "title": "Digital Marketing Specialist", + "team": "Marketing", + "location": "New York", + "email": "casey@example.com", + "mobile": "+1 (212) 555-0123", + "calendar": "In a meeting", + "imageUrl": "http://localhost:10004/static/profile2.png" + } + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "org-chart-view", + "path": "/", + "value": { + "hierarchy": { + "0": { + "title": "CEO", + "name": "Jane Doe" + }, + "1": { + "title": "VP Marketing", + "name": "John Smith" + }, + "2": { + "title": "Director", + "name": "Alice Johnson" + }, + "3": { + "title": "Specialist", + "name": "Casey Smith" + } + } + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_multiple_surfaces/examples/0.9/org_chart.json b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/org_chart.json new file mode 100644 index 000000000..3d46dddfe --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/examples/0.9/org_chart.json @@ -0,0 +1,71 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "org-chart-view", + "catalogId": "inline_catalog" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "org-chart-view", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "title", + "org-chart" + ] + }, + { + "id": "title", + "component": "Text", + "variant": "h2", + "text": "Organizational Chart" + }, + { + "id": "org-chart", + "component": "OrgChart", + "chain": { + "path": "/hierarchy" + }, + "action": { + "event": { + "name": "chart_node_click", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "org-chart-view", + "path": "/", + "value": { + "hierarchy": { + "0": { + "title": "CEO", + "name": "Jane Doe" + }, + "1": { + "title": "VP Marketing", + "name": "John Smith" + }, + "2": { + "title": "Director", + "name": "Alice Johnson" + }, + "3": { + "title": "Digital Marketing Specialist", + "name": "Casey Smith" + } + } + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_multiple_surfaces/inline_catalog_0.9.json b/samples/agent/adk/contact_multiple_surfaces/inline_catalog_0.9.json new file mode 100644 index 000000000..465193a9d --- /dev/null +++ b/samples/agent/adk/contact_multiple_surfaces/inline_catalog_0.9.json @@ -0,0 +1,1620 @@ +{ + "catalogId": "inline_catalog", + "title": "Contact Multiple Surfaces Inline Catalog", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Text" + }, + "text": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text content to display. While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation." + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ], + "default": "body" + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + }, + "Image": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Image" + }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the image to display." + }, + "fit": { + "type": "string", + "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", + "enum": [ + "contain", + "cover", + "fill", + "none", + "scaleDown" + ], + "default": "fill" + }, + "variant": { + "type": "string", + "description": "A hint for the image size and style.", + "enum": [ + "icon", + "avatar", + "smallFeature", + "mediumFeature", + "largeFeature", + "header" + ], + "default": "mediumFeature" + } + }, + "required": [ + "component", + "url" + ] + } + ], + "unevaluatedProperties": false + }, + "Icon": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Icon" + }, + "name": { + "description": "The name of the icon to display.", + "oneOf": [ + { + "type": "string", + "enum": [ + "accountCircle", + "add", + "arrowBack", + "arrowForward", + "attachFile", + "calendarToday", + "call", + "camera", + "check", + "close", + "delete", + "download", + "edit", + "event", + "error", + "fastForward", + "favorite", + "favoriteOff", + "folder", + "help", + "home", + "info", + "locationOn", + "lock", + "lockOpen", + "mail", + "menu", + "moreVert", + "moreHoriz", + "notificationsOff", + "notifications", + "pause", + "payment", + "person", + "phone", + "photo", + "play", + "print", + "refresh", + "rewind", + "search", + "send", + "settings", + "share", + "shoppingCart", + "skipNext", + "skipPrevious", + "star", + "starHalf", + "starOff", + "stop", + "upload", + "visibility", + "visibilityOff", + "volumeDown", + "volumeMute", + "volumeOff", + "volumeUp", + "warning" + ] + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "component", + "name" + ] + } + ], + "unevaluatedProperties": false + }, + "Video": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Video" + }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the video to display." + } + }, + "required": [ + "component", + "url" + ] + } + ], + "unevaluatedProperties": false + }, + "AudioPlayer": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "AudioPlayer" + }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the audio to be played." + }, + "description": { + "description": "A description of the audio, such as a title or summary.", + "$ref": "common_types.json#/$defs/DynamicString" + } + }, + "required": [ + "component", + "url" + ] + } + ], + "unevaluatedProperties": false + }, + "Row": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children horizontally. To create a grid layout, nest Columns within this Row.", + "properties": { + "component": { + "const": "Row" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (horizontally). Use 'spaceBetween' to push items to the edges, or 'start'/'end'/'center' to pack them together.", + "enum": [ + "center", + "end", + "spaceAround", + "spaceBetween", + "spaceEvenly", + "start", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (vertically). This is similar to the CSS 'align-items' property, but uses camelCase values (e.g., 'start').", + "enum": [ + "start", + "center", + "end", + "stretch" + ], + "default": "stretch" + } + }, + "required": [ + "component", + "children" + ] + } + ], + "unevaluatedProperties": false + }, + "Column": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children vertically. To create a grid layout, nest Rows within this Column.", + "properties": { + "component": { + "const": "Column" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (vertically). Use 'spaceBetween' to push items to the edges (e.g. header at top, footer at bottom), or 'start'/'end'/'center' to pack them together.", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (horizontally). This is similar to the CSS 'align-items' property.", + "enum": [ + "center", + "end", + "start", + "stretch" + ], + "default": "stretch" + } + }, + "required": [ + "component", + "children" + ] + } + ], + "unevaluatedProperties": false + }, + "List": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "List" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "direction": { + "type": "string", + "description": "The direction in which the list items are laid out.", + "enum": [ + "vertical", + "horizontal" + ], + "default": "vertical" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis.", + "enum": [ + "start", + "center", + "end", + "stretch" + ], + "default": "stretch" + } + }, + "required": [ + "component", + "children" + ] + } + ], + "unevaluatedProperties": false + }, + "Card": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Card" + }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the single child component to be rendered inside the card. To display multiple elements, you MUST wrap them in a layout component (like Column or Row) and pass that container's ID here. Do NOT pass multiple IDs or a non-existent ID. Do NOT define the child component inline." + } + }, + "required": [ + "component", + "child" + ] + } + ], + "unevaluatedProperties": false + }, + "Tabs": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Tabs" + }, + "tabs": { + "type": "array", + "description": "An array of objects, where each object defines a tab with a title and a child component.", + "items": { + "type": "object", + "properties": { + "title": { + "description": "The tab title.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Do NOT define the component inline." + } + }, + "required": [ + "title", + "child" + ], + "additionalProperties": false + } + } + }, + "required": [ + "component", + "tabs" + ] + } + ], + "unevaluatedProperties": false + }, + "Modal": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Modal" + }, + "trigger": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." + }, + "content": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." + } + }, + "required": [ + "component", + "trigger", + "content" + ] + } + ], + "unevaluatedProperties": false + }, + "Divider": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Divider" + }, + "axis": { + "type": "string", + "description": "The orientation of the divider.", + "enum": [ + "horizontal", + "vertical" + ], + "default": "horizontal" + } + }, + "required": [ + "component" + ] + } + ], + "unevaluatedProperties": false + }, + "Button": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Button" + }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Use a 'Text' component for a labeled button. Only use an 'Icon' if the requirements explicitly ask for an icon-only button. Do NOT define the child component inline." + }, + "variant": { + "type": "string", + "description": "A hint for the button style. If omitted, a default button style is used. 'primary' indicates this is the main call-to-action button. 'borderless' means the button has no visual border or background, making its child content appear like a clickable link.", + "enum": [ + "default", + "primary", + "borderless" + ], + "default": "default" + }, + "action": { + "$ref": "common_types.json#/$defs/Action" + } + }, + "required": [ + "component", + "child", + "action" + ] + } + ], + "unevaluatedProperties": false + }, + "TextField": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "TextField" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The value of the text field." + }, + "variant": { + "type": "string", + "description": "The type of input field to display.", + "enum": [ + "longText", + "number", + "shortText", + "obscured" + ], + "default": "shortText" + }, + "validationRegexp": { + "type": "string", + "description": "A regular expression used for client-side validation of the input." + } + }, + "required": [ + "component", + "label" + ] + } + ], + "unevaluatedProperties": false + }, + "CheckBox": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "CheckBox" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text to display next to the checkbox." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "The current state of the checkbox (true for checked, false for unchecked)." + } + }, + "required": [ + "component", + "label", + "value" + ] + } + ], + "unevaluatedProperties": false + }, + "ChoicePicker": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "description": "A component that allows selecting one or more options from a list.", + "properties": { + "component": { + "const": "ChoicePicker" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The label for the group of options." + }, + "variant": { + "type": "string", + "description": "A hint for how the choice picker should be displayed and behave.", + "enum": [ + "multipleSelection", + "mutuallyExclusive" + ], + "default": "mutuallyExclusive" + }, + "options": { + "type": "array", + "description": "The list of available options to choose from.", + "items": { + "type": "object", + "properties": { + "label": { + "description": "The text to display for this option.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "value": { + "type": "string", + "description": "The stable value associated with this option." + } + }, + "required": [ + "label", + "value" + ], + "additionalProperties": false + } + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicStringList", + "description": "The list of currently selected values. This should be bound to a string array in the data model." + }, + "displayStyle": { + "type": "string", + "description": "The display style of the component.", + "enum": [ + "checkbox", + "chips" + ], + "default": "checkbox" + }, + "filterable": { + "type": "boolean", + "description": "If true, displays a search input to filter the options.", + "default": false + } + }, + "required": [ + "component", + "options", + "value" + ] + } + ], + "unevaluatedProperties": false + }, + "Slider": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Slider" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The label for the slider." + }, + "min": { + "type": "number", + "description": "The minimum value of the slider.", + "default": 0 + }, + "max": { + "type": "number", + "description": "The maximum value of the slider." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The current value of the slider." + } + }, + "required": [ + "component", + "value", + "max" + ] + } + ], + "unevaluatedProperties": false + }, + "DateTimeInput": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "DateTimeInput" + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." + }, + "enableDate": { + "type": "boolean", + "description": "If true, allows the user to select a date.", + "default": false + }, + "enableTime": { + "type": "boolean", + "description": "If true, allows the user to select a time.", + "default": false + }, + "min": { + "allOf": [ + { + "$ref": "common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The minimum allowed date/time in ISO 8601 format." + }, + "max": { + "allOf": [ + { + "$ref": "common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The maximum allowed date/time in ISO 8601 format." + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + } + }, + "required": [ + "component", + "value" + ] + } + ], + "unevaluatedProperties": false + }, + "OrgChart": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "OrgChart" + }, + "chain": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "title", + "name" + ] + } + }, + { + "$ref": "common_types.json#/$defs/DataBinding" + } + ] + }, + "action": { + "$ref": "common_types.json#/$defs/Action" + } + }, + "required": [ + "component", + "chain" + ] + } + ], + "unevaluatedProperties": false + }, + "WebFrame": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "WebFrame" + }, + "url": { + "type": "string" + }, + "html": { + "type": "string" + }, + "height": { + "type": "number" + }, + "interactionMode": { + "type": "string", + "enum": [ + "readOnly", + "interactive" + ] + }, + "allowedEvents": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "component", + "url" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "weight": { + "type": "number", + "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." + } + } + }, + "theme": { + "type": "object", + "properties": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "additionalProperties": true + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/Text" + }, + { + "$ref": "#/components/Image" + }, + { + "$ref": "#/components/Icon" + }, + { + "$ref": "#/components/Video" + }, + { + "$ref": "#/components/AudioPlayer" + }, + { + "$ref": "#/components/Row" + }, + { + "$ref": "#/components/Column" + }, + { + "$ref": "#/components/List" + }, + { + "$ref": "#/components/Card" + }, + { + "$ref": "#/components/Tabs" + }, + { + "$ref": "#/components/Modal" + }, + { + "$ref": "#/components/Divider" + }, + { + "$ref": "#/components/Button" + }, + { + "$ref": "#/components/TextField" + }, + { + "$ref": "#/components/CheckBox" + }, + { + "$ref": "#/components/ChoicePicker" + }, + { + "$ref": "#/components/Slider" + }, + { + "$ref": "#/components/DateTimeInput" + }, + { + "$ref": "#/components/OrgChart" + }, + { + "$ref": "#/components/WebFrame" + } + ], + "discriminator": { + "propertyName": "component" + } + }, + "anyFunction": { + "oneOf": [ + { + "$ref": "#/functions/required" + }, + { + "$ref": "#/functions/regex" + }, + { + "$ref": "#/functions/length" + }, + { + "$ref": "#/functions/numeric" + }, + { + "$ref": "#/functions/email" + }, + { + "$ref": "#/functions/formatString" + }, + { + "$ref": "#/functions/formatNumber" + }, + { + "$ref": "#/functions/formatCurrency" + }, + { + "$ref": "#/functions/formatDate" + }, + { + "$ref": "#/functions/pluralize" + }, + { + "$ref": "#/functions/openUrl" + }, + { + "$ref": "#/functions/and" + }, + { + "$ref": "#/functions/or" + }, + { + "$ref": "#/functions/not" + } + ] + } + }, + "functions": { + "required": { + "type": "object", + "description": "Validation function: check if a value is NOT null or empty.", + "properties": { + "call": { + "const": "required" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicValue" + } + }, + "required": [ + "value" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "regex": { + "type": "object", + "description": "Validation function: check if a string matches a regular expression.", + "properties": { + "call": { + "const": "regex" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicString" + }, + "pattern": { + "type": "string" + } + }, + "required": [ + "value", + "pattern" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "length": { + "type": "object", + "description": "Validation function: check string or array length.", + "properties": { + "call": { + "const": "length" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicValue" + }, + "min": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "required": [ + "value" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "numeric": { + "type": "object", + "description": "Validation function: check if value is numeric within range.", + "properties": { + "call": { + "const": "numeric" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber" + }, + "min": { + "type": "number" + }, + "max": { + "type": "number" + } + }, + "required": [ + "value" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "email": { + "type": "object", + "description": "Validation function: check if value is a valid email address.", + "properties": { + "call": { + "const": "email" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicString" + } + }, + "required": [ + "value" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "formatString": { + "type": "object", + "description": "Formats a string using placeholders.", + "properties": { + "call": { + "const": "formatString" + }, + "args": { + "type": "object", + "properties": { + "template": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": { + "$ref": "common_types.json#/$defs/DynamicValue" + } + } + }, + "required": [ + "template", + "values" + ] + }, + "returnType": { + "const": "string" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "formatNumber": { + "type": "object", + "description": "Formats a number as a string.", + "properties": { + "call": { + "const": "formatNumber" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber" + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber" + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean" + } + }, + "required": [ + "value" + ] + }, + "returnType": { + "const": "string" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "formatCurrency": { + "type": "object", + "description": "Formats a number as a currency string.", + "properties": { + "call": { + "const": "formatCurrency" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber" + }, + "currency": { + "$ref": "common_types.json#/$defs/DynamicString" + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber" + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean" + } + }, + "required": [ + "currency", + "value" + ] + }, + "returnType": { + "const": "string" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "formatDate": { + "type": "object", + "description": "Formats a timestamp into a string using a pattern.", + "properties": { + "call": { + "const": "formatDate" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicValue" + }, + "format": { + "$ref": "common_types.json#/$defs/DynamicString" + } + }, + "required": [ + "format", + "value" + ] + }, + "returnType": { + "const": "string" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "pluralize": { + "type": "object", + "description": "Returns a localized string based on count.", + "properties": { + "call": { + "const": "pluralize" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber" + }, + "zero": { + "$ref": "common_types.json#/$defs/DynamicString" + }, + "one": { + "$ref": "common_types.json#/$defs/DynamicString" + }, + "two": { + "$ref": "common_types.json#/$defs/DynamicString" + }, + "few": { + "$ref": "common_types.json#/$defs/DynamicString" + }, + "many": { + "$ref": "common_types.json#/$defs/DynamicString" + }, + "other": { + "$ref": "common_types.json#/$defs/DynamicString" + } + }, + "required": [ + "value", + "other" + ] + }, + "returnType": { + "const": "string" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "openUrl": { + "type": "object", + "description": "Opens the specified URL.", + "properties": { + "call": { + "const": "openUrl" + }, + "args": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "returnType": { + "const": "void" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "and": { + "type": "object", + "description": "Logical AND.", + "properties": { + "call": { + "const": "and" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "common_types.json#/$defs/DynamicBoolean" + } + } + }, + "required": [ + "values" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "or": { + "type": "object", + "description": "Logical OR.", + "properties": { + "call": { + "const": "or" + }, + "args": { + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "$ref": "common_types.json#/$defs/DynamicBoolean" + } + } + }, + "required": [ + "values" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + }, + "not": { + "type": "object", + "description": "Logical NOT.", + "properties": { + "call": { + "const": "not" + }, + "args": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicBoolean" + } + }, + "required": [ + "value" + ] + }, + "returnType": { + "const": "boolean" + } + }, + "required": [ + "call", + "args" + ], + "unevaluatedProperties": false + } + } +} diff --git a/samples/agent/adk/contact_multiple_surfaces/prompt_builder.py b/samples/agent/adk/contact_multiple_surfaces/prompt_builder.py index 147dd4410..1c8e4a102 100644 --- a/samples/agent/adk/contact_multiple_surfaces/prompt_builder.py +++ b/samples/agent/adk/contact_multiple_surfaces/prompt_builder.py @@ -14,10 +14,12 @@ import json -from a2ui.core.schema.constants import VERSION_0_8, A2UI_OPEN_TAG, A2UI_CLOSE_TAG -from a2ui.core.schema.manager import A2uiSchemaManager +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9, A2UI_OPEN_TAG, A2UI_CLOSE_TAG +from a2ui.core.schema.manager import A2uiSchemaManager, CatalogConfig from a2ui.basic_catalog.provider import BasicCatalog from a2ui.core.schema.common_modifiers import remove_strict_validation +from a2ui.core.schema.catalog_provider import A2uiCatalogProvider, FileSystemCatalogProvider +from typing import Dict, Any ROLE_DESCRIPTION = ( "You are a helpful contact lookup assistant. Your final output MUST be a a2ui UI" @@ -25,14 +27,14 @@ ) WORKFLOW_DESCRIPTION = """ -Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` attribute. +Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` (for spec version v0.8) or `"variant": "primary"` attribute (for spec version v0.9+). """ UI_DESCRIPTION = f""" - **For finding contacts (e.g., "Who is Alex Jordan?"):** a. You MUST call the `get_contact_info` tool. b. If the tool returns a **single contact**, you MUST use the `MULTI_SURFACE_EXAMPLE` template. Provide BOTH the Contact Card and the Org Chart in a single response. - c. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. Populate the `dataModelUpdate.contents` with the list of contacts for the "contacts" key. + c. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. Populate the `dataModelUpdate.contents` (v0.8) or `updateDataModel.value` (v0.9+) with the list of contacts for the "contacts" key. d. If the tool returns an **empty list**, respond with text only and an empty JSON list: "I couldn't find anyone by that name.{A2UI_OPEN_TAG}[]{A2UI_CLOSE_TAG}" - **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** @@ -41,7 +43,7 @@ - **For handling actions (e.g., "USER_WANTS_TO_EMAIL: ..."):** a. You MUST use the `ACTION_CONFIRMATION_EXAMPLE` template. - b. Populate the `dataModelUpdate.contents` with a confirmation title and message (e.g., title: "Email Drafted", message: "Drafting an email to Alex Jordan..."). + b. Populate the `updateDataModel.value` with a confirmation title and message (e.g., title: "Email Drafted", message: "Drafting an email to Alex Jordan..."). """ @@ -67,9 +69,17 @@ def get_text_prompt() -> str: if __name__ == "__main__": # Example of how to use the A2UI Schema Manager to generate a system prompt my_base_url = "http://localhost:8000" + my_version = VERSION_0_9 + inline_catalog_path = f"inline_catalog_{my_version}.json" schema_manager = A2uiSchemaManager( - VERSION_0_8, - catalogs=[BasicCatalog.get_config(version=VERSION_0_8, examples_path="examples")], + my_version, + catalogs=[ + CatalogConfig.from_path( + name="contact_multiple_surfaces_inline_catalog", + catalog_path=inline_catalog_path, + examples_path=f"examples/{my_version}", + ), + ], accepts_inline_catalogs=True, schema_modifiers=[remove_strict_validation], ) @@ -86,11 +96,10 @@ def get_text_prompt() -> str: f.write(contact_prompt) print("\nGenerated prompt saved to generated_prompt.txt") - client_ui_capabilities_str = ( - '{"inlineCatalogs":[{"catalogId": "inline_catalog",' - ' "components":{"OrgChart":{"type":"object","properties":{"chain":{"oneOf":[{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]},{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"name":{"type":"string"}},"required":["title","name"]}}]},"action":{"type":"object","properties":{"name":{"type":"string"},"context":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"object","properties":{"path":{"type":"string"},"literalString":{"type":"string"},"literalNumber":{"type":"number"},"literalBoolean":{"type":"boolean"}}}},"required":["key","value"]}}},"required":["name"]}},"required":["chain"]},"WebFrame":{"type":"object","properties":{"url":{"type":"string"},"html":{"type":"string"},"height":{"type":"number"},"interactionMode":{"type":"string","enum":["readOnly","interactive"]},"allowedEvents":{"type":"array","items":{"type":"string"}}}}}}]}' - ) - client_ui_capabilities = json.loads(client_ui_capabilities_str) + with open(inline_catalog_path, "r", encoding="utf-8") as f: + inline_catalog = json.load(f) + + client_ui_capabilities = {"inlineCatalogs": [inline_catalog]} inline_catalog = schema_manager.get_selected_catalog( client_ui_capabilities=client_ui_capabilities, ) diff --git a/samples/agent/adk/mcp_app_proxy/agent.py b/samples/agent/adk/mcp_app_proxy/agent.py index b28af9604..220c8ad38 100644 --- a/samples/agent/adk/mcp_app_proxy/agent.py +++ b/samples/agent/adk/mcp_app_proxy/agent.py @@ -19,7 +19,7 @@ from a2a.types import AgentCapabilities, AgentCard, AgentSkill from a2ui.a2a import get_a2ui_agent_extension from a2ui.adk.a2a_extension.send_a2ui_to_client_toolset import A2uiEnabledProvider, A2uiCatalogProvider, A2uiExamplesProvider, SendA2uiToClientToolset -from a2ui.core.schema.manager import A2uiSchemaManager, VERSION_0_8, CatalogConfig +from a2ui.core.schema.manager import A2uiSchemaManager, VERSION_0_8, VERSION_0_9, CatalogConfig from google.adk.agents.llm_agent import LlmAgent from google.adk.artifacts import InMemoryArtifactService from google.adk.memory.in_memory_memory_service import InMemoryMemoryService @@ -77,7 +77,7 @@ def __init__( self._schema_managers: Dict[str, A2uiSchemaManager] = {} self._ui_runners: Dict[str, Runner] = {} - for version in [VERSION_0_8]: + for version in [VERSION_0_8, VERSION_0_9]: schema_manager = self._build_schema_manager(version) self._schema_managers[version] = schema_manager agent = self._build_llm_agent(schema_manager) diff --git a/samples/agent/adk/migrate_v08_to_v09.py b/samples/agent/adk/migrate_v08_to_v09.py new file mode 100644 index 000000000..15239bf09 --- /dev/null +++ b/samples/agent/adk/migrate_v08_to_v09.py @@ -0,0 +1,268 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import argparse +from pathlib import Path + +VERSION_0_9_CATALOG_ID = "https://a2ui.org/specification/v0_9/basic_catalog.json" + + +def _convert_map(v_map): + res = {} + for m_item in v_map: + m_key = m_item["key"] + m_val_key = [mk for mk in m_item.keys() if mk.startswith("value")][0] + m_val = m_item[m_val_key] + if m_val_key == "valueMap": + res[m_key] = _convert_map(m_val) + else: + res[m_key] = m_val + return res + + +def migrate_v08_to_v09(v08_data, catalog_id=VERSION_0_9_CATALOG_ID): + v09_data = [] + + # We need to track which component is the root for each surface + surface_roots = {} + + for msg in v08_data: + new_msg = {"version": "v0.9"} + + if "beginRendering" in msg: + br = msg["beginRendering"] + surface_id = br["surfaceId"] + root_id = br["root"] + surface_roots[surface_id] = root_id + + new_br = { + "surfaceId": surface_id, + "catalogId": catalog_id, + } + if "styles" in br: + new_br["theme"] = br["styles"] + + new_msg["createSurface"] = new_br + + elif "surfaceUpdate" in msg: + su = msg["surfaceUpdate"] + surface_id = su["surfaceId"] + root_id = surface_roots.get(surface_id) + + new_components = [] + for comp_wrapper in su["components"]: + comp_id = comp_wrapper["id"] + # Rename the designated root component to "root" + if comp_id == root_id: + comp_id = "root" + + # Unwrap component type and properties + if "component" not in comp_wrapper: + # Possibly already migrated or invalid format? Skip or attempt to handle? + # Some custom components might be different. + continue + + comp_keys = list(comp_wrapper["component"].keys()) + if not comp_keys: + continue + comp_type = comp_keys[0] + comp_props = comp_wrapper["component"][comp_type] + + new_comp = {"id": comp_id, "component": comp_type} + + # Add weight if present + if "weight" in comp_wrapper: + new_comp["weight"] = comp_wrapper["weight"] + + # Transform properties + for k, v in comp_props.items(): + if k == "children": + if "explicitList" in v: + # Update child IDs if they were the root + new_comp["children"] = [ + c if c != root_id else "root" for c in v["explicitList"] + ] + elif "template" in v: + template = v["template"] + new_comp["children"] = { + "componentId": ( + template["componentId"] + if template["componentId"] != root_id + else "root" + ), + "path": template.get("dataBinding", ""), + } + elif k == "child": + new_comp["child"] = v if v != root_id else "root" + elif k == "tabItems": + new_comp["tabs"] = [ + { + "title": item["title"], + "child": item["child"] if item["child"] != root_id else "root", + } + for item in v + ] + elif k == "entryPointChild": + new_comp["trigger"] = v if v != root_id else "root" + elif k == "contentChild": + new_comp["content"] = v if v != root_id else "root" + elif k == "usageHint": + new_comp["variant"] = v + elif k == "action": + new_action = {"event": {"name": v["name"]}} + if "context" in v: + new_context = {} + for item in v["context"]: + # Unwrap context value if needed + val = item["value"] + if isinstance(val, dict) and "literalString" in val: + val = val["literalString"] + elif isinstance(val, dict) and "literalNumber" in val: + val = val["literalNumber"] + elif isinstance(val, dict) and "literalBoolean" in val: + val = val["literalBoolean"] + + new_context[item["key"]] = val + new_action["event"]["context"] = new_context + new_comp["action"] = new_action + elif k == "fit": + new_comp["fit"] = "scaleDown" if v == "scale-down" else v + elif k == "alignment": + new_comp["align"] = v + elif k == "distribution": + new_comp["justify"] = v + elif k == "text" and comp_type == "TextField": + new_comp["value"] = v + elif k == "type" and comp_type == "TextField": + new_comp["variant"] = v + elif k == "primary": + # Map Button primary to variant + if comp_type == "Button": + new_comp["variant"] = "primary" if v else "default" + else: + new_comp[k] = v + else: + # General literal unwrapping + if isinstance(v, dict) and "literalString" in v: + v = v["literalString"] + elif isinstance(v, dict) and "literalNumber" in v: + v = v["literalNumber"] + elif isinstance(v, dict) and "literalBoolean" in v: + v = v["literalBoolean"] + + # Icon specific handling + if comp_type == "Icon": + if k == "name": + # Map common icon names to v0.9 enum + icon_map = { + "check_circle": "check", + "calendar_today": "calendarToday", + "location_on": "locationOn", + } + v = icon_map.get(v, v) + elif k in ["size", "color"]: + # Skip properties not in basic catalog v0.9 + continue + + # Image specific handling + if comp_type == "Image": + if k in ["width", "height"]: + continue + + new_comp[k] = v + + new_components.append(new_comp) + + new_msg["updateComponents"] = { + "surfaceId": surface_id, + "components": new_components, + } + + elif "dataModelUpdate" in msg: + dmu = msg["dataModelUpdate"] + surface_id = dmu["surfaceId"] + path = dmu.get("path", "/") + + # Simple transformation for now: convert contents array to object + value_obj = {} + for item in dmu.get("contents", []): + key = item["key"] + # Find the value key (valueString, valueNumber, etc.) + val_key = next((k for k in item.keys() if k.startswith("value")), None) + if val_key is None: + continue + val = item[val_key] + + # Recursive map conversion if needed + if val_key == "valueMap": + val = _convert_map(val) + + value_obj[key] = val + + new_msg["updateDataModel"] = { + "surfaceId": surface_id, + "path": path, + "value": value_obj, + } + + elif "deleteSurface" in msg: + new_msg["deleteSurface"] = {"surfaceId": msg["deleteSurface"]["surfaceId"]} + + v09_data.append(new_msg) + + return v09_data + + +def process_file(src_file, dst_file, catalog_id): + print(f"Migrating {src_file.name}...") + with open(src_file, "r", encoding="utf-8") as f: + v08_data = json.load(f) + + v09_data = migrate_v08_to_v09(v08_data, catalog_id=catalog_id) + + dst_file.parent.mkdir(parents=True, exist_ok=True) + with open(dst_file, "w", encoding="utf-8") as f: + json.dump(v09_data, f, indent=2) + + +def main(): + parser = argparse.ArgumentParser(description="Migrate A2UI v0.8 examples to v0.9") + parser.add_argument("--src", required=True, help="Source file or directory") + parser.add_argument("--dst", required=True, help="Destination file or directory") + parser.add_argument( + "--catalog_id", + default=VERSION_0_9_CATALOG_ID, + help="Catalog ID to use in v0.9", + ) + + args = parser.parse_args() + + src = Path(args.src) + dst = Path(args.dst) + + if src.is_file(): + process_file(src, dst, args.catalog_id) + elif src.is_dir(): + for json_file in src.glob("*.json"): + process_file(json_file, dst / json_file.name, args.catalog_id) + else: + print(f"Error: {src} is not a file or directory") + exit(1) + + print("Migration complete.") + + +if __name__ == "__main__": + main() diff --git a/samples/agent/adk/restaurant_finder/agent.py b/samples/agent/adk/restaurant_finder/agent.py index b327a348c..bbfe87950 100644 --- a/samples/agent/adk/restaurant_finder/agent.py +++ b/samples/agent/adk/restaurant_finder/agent.py @@ -40,7 +40,7 @@ UI_DESCRIPTION, ) from tools import get_restaurants -from a2ui.core.schema.constants import VERSION_0_8, A2UI_OPEN_TAG, A2UI_CLOSE_TAG +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9, A2UI_OPEN_TAG, A2UI_CLOSE_TAG from a2ui.core.schema.manager import A2uiSchemaManager from a2ui.core.parser.parser import parse_response, ResponsePart from a2ui.basic_catalog.provider import BasicCatalog @@ -64,7 +64,7 @@ def __init__(self, base_url: str): self._schema_managers: Dict[str, A2uiSchemaManager] = {} self._ui_runners: Dict[str, Runner] = {} - for version in [VERSION_0_8]: + for version in [VERSION_0_8, VERSION_0_9]: schema_manager = self._build_schema_manager(version) self._schema_managers[version] = schema_manager agent = self._build_llm_agent(schema_manager) @@ -79,7 +79,11 @@ def agent_card(self) -> AgentCard: def _build_schema_manager(self, version: str) -> A2uiSchemaManager: return A2uiSchemaManager( version=version, - catalogs=[BasicCatalog.get_config(version=version, examples_path="examples")], + catalogs=[ + BasicCatalog.get_config( + version=version, examples_path=f"examples/{version}" + ) + ], schema_modifiers=[remove_strict_validation], ) diff --git a/samples/agent/adk/restaurant_finder/examples/booking_form.json b/samples/agent/adk/restaurant_finder/examples/0.8/booking_form.json similarity index 100% rename from samples/agent/adk/restaurant_finder/examples/booking_form.json rename to samples/agent/adk/restaurant_finder/examples/0.8/booking_form.json diff --git a/samples/agent/adk/restaurant_finder/examples/confirmation.json b/samples/agent/adk/restaurant_finder/examples/0.8/confirmation.json similarity index 100% rename from samples/agent/adk/restaurant_finder/examples/confirmation.json rename to samples/agent/adk/restaurant_finder/examples/0.8/confirmation.json diff --git a/samples/agent/adk/restaurant_finder/examples/single_column_list.json b/samples/agent/adk/restaurant_finder/examples/0.8/single_column_list.json similarity index 100% rename from samples/agent/adk/restaurant_finder/examples/single_column_list.json rename to samples/agent/adk/restaurant_finder/examples/0.8/single_column_list.json diff --git a/samples/agent/adk/restaurant_finder/examples/two_column_list.json b/samples/agent/adk/restaurant_finder/examples/0.8/two_column_list.json similarity index 100% rename from samples/agent/adk/restaurant_finder/examples/two_column_list.json rename to samples/agent/adk/restaurant_finder/examples/0.8/two_column_list.json diff --git a/samples/agent/adk/restaurant_finder/examples/0.9/booking_form.json b/samples/agent/adk/restaurant_finder/examples/0.9/booking_form.json new file mode 100644 index 000000000..62ef5ce54 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/0.9/booking_form.json @@ -0,0 +1,131 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "booking-form", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#FF0000", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "booking-form", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "booking-title", + "restaurant-image", + "restaurant-address", + "party-size-field", + "datetime-field", + "dietary-field", + "submit-button" + ] + }, + { + "id": "booking-title", + "component": "Text", + "variant": "h2", + "text": { + "path": "/title" + } + }, + { + "id": "restaurant-image", + "component": "Image", + "url": { + "path": "/imageUrl" + } + }, + { + "id": "restaurant-address", + "component": "Text", + "text": { + "path": "/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 + }, + { + "id": "dietary-field", + "component": "TextField", + "label": "Dietary Requirements", + "value": { + "path": "/dietary" + } + }, + { + "id": "submit-button", + "component": "Button", + "child": "submit-reservation-text", + "action": { + "event": { + "name": "submit_booking", + "context": { + "restaurantName": { + "path": "/restaurantName" + }, + "partySize": { + "path": "/partySize" + }, + "reservationTime": { + "path": "/reservationTime" + }, + "dietary": { + "path": "/dietary" + }, + "imageUrl": { + "path": "/imageUrl" + } + } + } + } + }, + { + "id": "submit-reservation-text", + "component": "Text", + "text": "Submit Reservation" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "booking-form", + "path": "/", + "value": { + "title": "Book a Table at [RestaurantName]", + "address": "[Restaurant Address]", + "restaurantName": "[RestaurantName]", + "partySize": "2", + "reservationTime": "", + "dietary": "", + "imageUrl": "" + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/restaurant_finder/examples/0.9/confirmation.json b/samples/agent/adk/restaurant_finder/examples/0.9/confirmation.json new file mode 100644 index 000000000..219002b46 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/0.9/confirmation.json @@ -0,0 +1,100 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "confirmation", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#FF0000", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "confirmation", + "components": [ + { + "id": "root", + "component": "Card", + "child": "confirmation-column" + }, + { + "id": "confirmation-column", + "component": "Column", + "children": [ + "confirm-title", + "confirm-image", + "divider1", + "confirm-details", + "divider2", + "confirm-dietary", + "divider3", + "confirm-text" + ] + }, + { + "id": "confirm-title", + "component": "Text", + "variant": "h2", + "text": { + "path": "/title" + } + }, + { + "id": "confirm-image", + "component": "Image", + "url": { + "path": "/imageUrl" + } + }, + { + "id": "confirm-details", + "component": "Text", + "text": { + "path": "/bookingDetails" + } + }, + { + "id": "confirm-dietary", + "component": "Text", + "text": { + "path": "/dietaryRequirements" + } + }, + { + "id": "confirm-text", + "component": "Text", + "variant": "h5", + "text": "We look forward to seeing you!" + }, + { + "id": "divider1", + "component": "Divider" + }, + { + "id": "divider2", + "component": "Divider" + }, + { + "id": "divider3", + "component": "Divider" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "confirmation", + "path": "/", + "value": { + "title": "Booking at [RestaurantName]", + "bookingDetails": "[PartySize] people at [Time]", + "dietaryRequirements": "Dietary Requirements: [Requirements]", + "imageUrl": "[ImageUrl]" + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/restaurant_finder/examples/0.9/single_column_list.json b/samples/agent/adk/restaurant_finder/examples/0.9/single_column_list.json new file mode 100644 index 000000000..904d652b7 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/0.9/single_column_list.json @@ -0,0 +1,162 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "default", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#FF0000", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "default", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "title-heading", + "item-list" + ] + }, + { + "id": "title-heading", + "component": "Text", + "variant": "h1", + "text": { + "path": "/title" + } + }, + { + "id": "item-list", + "component": "List", + "direction": "vertical", + "children": { + "componentId": "item-card-template", + "path": "/items" + } + }, + { + "id": "item-card-template", + "component": "Card", + "child": "card-layout" + }, + { + "id": "card-layout", + "component": "Row", + "children": [ + "template-image", + "card-details" + ] + }, + { + "id": "template-image", + "component": "Image", + "weight": 1, + "url": { + "path": "/imageUrl" + } + }, + { + "id": "card-details", + "component": "Column", + "weight": 2, + "children": [ + "template-name", + "template-rating", + "template-detail", + "template-link", + "template-book-button" + ] + }, + { + "id": "template-name", + "component": "Text", + "variant": "h3", + "text": { + "path": "/name" + } + }, + { + "id": "template-rating", + "component": "Text", + "text": { + "path": "/rating" + } + }, + { + "id": "template-detail", + "component": "Text", + "text": { + "path": "/detail" + } + }, + { + "id": "template-link", + "component": "Text", + "text": { + "path": "/infoLink" + } + }, + { + "id": "template-book-button", + "component": "Button", + "child": "book-now-text", + "variant": "primary", + "action": { + "event": { + "name": "book_restaurant", + "context": { + "restaurantName": { + "path": "/name" + }, + "imageUrl": { + "path": "/imageUrl" + }, + "address": { + "path": "/address" + } + } + } + } + }, + { + "id": "book-now-text", + "component": "Text", + "text": "Book Now" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "default", + "path": "/", + "value": { + "items": { + "item1": { + "name": "The Fancy Place", + "rating": 4.8, + "detail": "Fine dining experience", + "infoLink": "https://example.com/fancy", + "imageUrl": "https://example.com/fancy.jpg", + "address": "123 Main St" + }, + "item2": { + "name": "Quick Bites", + "rating": 4.2, + "detail": "Casual and fast", + "infoLink": "https://example.com/quick", + "imageUrl": "https://example.com/quick.jpg", + "address": "456 Oak Ave" + } + } + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/restaurant_finder/examples/0.9/two_column_list.json b/samples/agent/adk/restaurant_finder/examples/0.9/two_column_list.json new file mode 100644 index 000000000..df7e7e6fc --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/0.9/two_column_list.json @@ -0,0 +1,247 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "default", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#FF0000", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "default", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "title-heading", + "restaurant-row-1" + ] + }, + { + "id": "title-heading", + "component": "Text", + "variant": "h1", + "text": { + "path": "/title" + } + }, + { + "id": "restaurant-row-1", + "component": "Row", + "children": [ + "item-card-1", + "item-card-2" + ] + }, + { + "id": "item-card-1", + "component": "Card", + "weight": 1, + "child": "card-layout-1" + }, + { + "id": "card-layout-1", + "component": "Column", + "children": [ + "template-image-1", + "card-details-1" + ] + }, + { + "id": "template-image-1", + "component": "Image", + "url": { + "path": "/items/0/imageUrl" + } + }, + { + "id": "card-details-1", + "component": "Column", + "children": [ + "template-name-1", + "template-rating-1", + "template-detail-1", + "template-link-1", + "template-book-button-1" + ] + }, + { + "id": "template-name-1", + "component": "Text", + "variant": "h3", + "text": { + "path": "/items/0/name" + } + }, + { + "id": "template-rating-1", + "component": "Text", + "text": { + "path": "/items/0/rating" + } + }, + { + "id": "template-detail-1", + "component": "Text", + "text": { + "path": "/items/0/detail" + } + }, + { + "id": "template-link-1", + "component": "Text", + "text": { + "path": "/items/0/infoLink" + } + }, + { + "id": "template-book-button-1", + "component": "Button", + "child": "book-now-text-1", + "action": { + "event": { + "name": "book_restaurant", + "context": { + "restaurantName": { + "path": "/items/0/name" + }, + "imageUrl": { + "path": "/items/0/imageUrl" + }, + "address": { + "path": "/items/0/address" + } + } + } + } + }, + { + "id": "book-now-text-1", + "component": "Text", + "text": "Book Now" + }, + { + "id": "item-card-2", + "component": "Card", + "weight": 1, + "child": "card-layout-2" + }, + { + "id": "card-layout-2", + "component": "Column", + "children": [ + "template-image-2", + "card-details-2" + ] + }, + { + "id": "template-image-2", + "component": "Image", + "url": { + "path": "/items/1/imageUrl" + } + }, + { + "id": "card-details-2", + "component": "Column", + "children": [ + "template-name-2", + "template-rating-2", + "template-detail-2", + "template-link-2", + "template-book-button-2" + ] + }, + { + "id": "template-name-2", + "component": "Text", + "variant": "h3", + "text": { + "path": "/items/1/name" + } + }, + { + "id": "template-rating-2", + "component": "Text", + "text": { + "path": "/items/1/rating" + } + }, + { + "id": "template-detail-2", + "component": "Text", + "text": { + "path": "/items/1/detail" + } + }, + { + "id": "template-link-2", + "component": "Text", + "text": { + "path": "/items/1/infoLink" + } + }, + { + "id": "template-book-button-2", + "component": "Button", + "child": "book-now-text-2", + "action": { + "event": { + "name": "book_restaurant", + "context": { + "restaurantName": { + "path": "/items/1/name" + }, + "imageUrl": { + "path": "/items/1/imageUrl" + }, + "address": { + "path": "/items/1/address" + } + } + } + } + }, + { + "id": "book-now-text-2", + "component": "Text", + "text": "Book Now" + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "default", + "path": "/", + "value": { + "title": "Top Restaurants", + "items": { + "item1": { + "name": "The Fancy Place", + "rating": 4.8, + "detail": "Fine dining experience", + "infoLink": "https://example.com/fancy", + "imageUrl": "https://example.com/fancy.jpg", + "address": "123 Main St" + }, + "item2": { + "name": "Quick Bites", + "rating": 4.2, + "detail": "Casual and fast", + "infoLink": "https://example.com/quick", + "imageUrl": "https://example.com/quick.jpg", + "address": "456 Oak Ave" + } + } + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/restaurant_finder/prompt_builder.py b/samples/agent/adk/restaurant_finder/prompt_builder.py index 0d4a2619d..704fe36b0 100644 --- a/samples/agent/adk/restaurant_finder/prompt_builder.py +++ b/samples/agent/adk/restaurant_finder/prompt_builder.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from a2ui.core.schema.constants import VERSION_0_8 +from a2ui.core.schema.constants import VERSION_0_9 from a2ui.core.schema.manager import A2uiSchemaManager from a2ui.basic_catalog.provider import BasicCatalog from a2ui.core.schema.common_modifiers import remove_strict_validation @@ -23,7 +23,7 @@ ) UI_DESCRIPTION = """ -- If the query is for a list of restaurants, use the restaurant data you have already received from the `get_restaurants` tool to populate the `dataModelUpdate.contents` array (e.g., as a `valueMap` for the "items" key). +- If the query is for a list of restaurants, use the restaurant data you have already received from the `get_restaurants` tool to populate the `dataModelUpdate.contents` (v0.8) or `updateDataModel.value` (v0.9+) object (e.g., for the "items" key). - If the number of restaurants is 5 or fewer, you MUST use the `SINGLE_COLUMN_LIST_EXAMPLE` template. - If the number of restaurants is more than 5, you MUST use the `TWO_COLUMN_LIST_EXAMPLE` template. - If the query is to book a restaurant (e.g., "USER_WANTS_TO_BOOK..."), you MUST use the `BOOKING_FORM_EXAMPLE` template. @@ -58,9 +58,15 @@ def get_text_prompt() -> str: # You can now easily construct a prompt with the relevant examples. # For a different agent (e.g., a flight booker), you would pass in # different examples but use the same `get_ui_prompt` function. + version = VERSION_0_9 restaurant_prompt = A2uiSchemaManager( - VERSION_0_8, - catalogs=[BasicCatalog.get_config(version=VERSION_0_8, examples_path="examples")], + version, + catalogs=[ + BasicCatalog.get_config( + version=version, + examples_path=f"examples/{version}", + ) + ], schema_modifiers=[remove_strict_validation], ).generate_system_prompt( role_description=ROLE_DESCRIPTION, diff --git a/samples/agent/adk/rizzcharts/agent.py b/samples/agent/adk/rizzcharts/agent.py index 44e8a070d..313408eab 100644 --- a/samples/agent/adk/rizzcharts/agent.py +++ b/samples/agent/adk/rizzcharts/agent.py @@ -22,7 +22,7 @@ from a2ui.adk.a2a_extension.send_a2ui_to_client_toolset import SendA2uiToClientToolset, A2uiEnabledProvider, A2uiCatalogProvider, A2uiExamplesProvider from a2ui.core.schema.manager import A2uiSchemaManager, CatalogConfig from a2ui.basic_catalog.provider import BasicCatalog -from a2ui.core.schema.constants import VERSION_0_8 +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9 from google.adk.agents.llm_agent import LlmAgent from google.adk.agents.readonly_context import ReadonlyContext from google.adk.planners.built_in_planner import BuiltInPlanner @@ -65,8 +65,8 @@ 4. **Construct the JSON Payload:** * Use the **entire** JSON array from the chosen example as the base value for the `a2ui_json` argument. - * **Generate a new `surfaceId`:** You MUST generate a new, unique `surfaceId` for this request (e.g., `sales_breakdown_q3_surface`, `regional_outliers_northeast_surface`). This new ID must be used for the `surfaceId` in all three messages within the JSON array (`beginRendering`, `surfaceUpdate`, `dataModelUpdate`). - * **Update the title Text:** You MUST update the `literalString` value for the `Text` component (the component with `id: "page_header"`) to accurately reflect the specific user query. For example, if the user asks for "Q3" sales, update the generic template text to "Q3 2025 Sales by Product Category". + * **Generate a new `surfaceId`:** You MUST generate a new, unique `surfaceId` for this request (e.g., `sales_breakdown_q3_surface`, `regional_outliers_northeast_surface`). This new ID must be used for the `surfaceId` in all three messages within the JSON array (`createSurface`, `updateComponents`, `updateDataModel`). + * **Update the title Text:** You MUST update the `text` property of the `Text` component (the component with `id: "page_header"`) to accurately reflect the specific user query. For example, if the user asks for "Q3" sales, update the generic template text to "Q3 2025 Sales by Product Category". * Ensure the generated JSON perfectly matches the A2UI specification. It will be validated against the json_schema and rejected if it does not conform. * If you get an error in the tool response apologize to the user and let them know they should try again. @@ -118,7 +118,7 @@ def __init__( self._schema_managers: Dict[str, A2uiSchemaManager] = {} self._ui_runners: Dict[str, Runner] = {} - for version in [VERSION_0_8]: + for version in [VERSION_0_8, VERSION_0_9]: schema_manager = self._build_schema_manager(version) self._schema_managers[version] = schema_manager agent = self._build_llm_agent(schema_manager) @@ -146,12 +146,14 @@ def _build_schema_manager(self, version: str) -> A2uiSchemaManager: catalogs=[ CatalogConfig.from_path( name="rizzcharts", - catalog_path="rizzcharts_catalog_definition.json", - examples_path="examples/rizzcharts_catalog", + catalog_path=( + f"catalog_schemas/{version}/rizzcharts_catalog_definition.json" + ), + examples_path=f"examples/rizzcharts_catalog/{version}", ), BasicCatalog.get_config( - version=VERSION_0_8, - examples_path="examples/standard_catalog", + version=version, + examples_path=f"examples/standard_catalog/{version}", ), ], accepts_inline_catalogs=True, diff --git a/samples/agent/adk/rizzcharts/rizzcharts_catalog_definition.json b/samples/agent/adk/rizzcharts/catalog_schemas/0.8/rizzcharts_catalog_definition.json similarity index 100% rename from samples/agent/adk/rizzcharts/rizzcharts_catalog_definition.json rename to samples/agent/adk/rizzcharts/catalog_schemas/0.8/rizzcharts_catalog_definition.json diff --git a/samples/agent/adk/rizzcharts/catalog_schemas/0.9/rizzcharts_catalog_definition.json b/samples/agent/adk/rizzcharts/catalog_schemas/0.9/rizzcharts_catalog_definition.json new file mode 100644 index 000000000..7f5bc5f0c --- /dev/null +++ b/samples/agent/adk/rizzcharts/catalog_schemas/0.9/rizzcharts_catalog_definition.json @@ -0,0 +1,1218 @@ +{ + "catalogId": "https://github.com/google/A2UI/blob/main/samples/agent/adk/rizzcharts/rizzcharts_catalog_definition.json", + "components": { + "Text": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Text" + }, + "text": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text content to display. While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation." + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body" + ], + "default": "body" + } + }, + "required": [ + "component", + "text" + ] + } + ], + "unevaluatedProperties": false + }, + "Image": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Image" + }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the image to display." + }, + "fit": { + "type": "string", + "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", + "enum": [ + "contain", + "cover", + "fill", + "none", + "scaleDown" + ], + "default": "fill" + }, + "variant": { + "type": "string", + "description": "A hint for the image size and style.", + "enum": [ + "icon", + "avatar", + "smallFeature", + "mediumFeature", + "largeFeature", + "header" + ], + "default": "mediumFeature" + } + }, + "required": [ + "component", + "url" + ] + } + ], + "unevaluatedProperties": false + }, + "Icon": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Icon" + }, + "name": { + "description": "The name of the icon to display.", + "oneOf": [ + { + "type": "string", + "enum": [ + "accountCircle", + "add", + "arrowBack", + "arrowForward", + "attachFile", + "calendarToday", + "call", + "camera", + "check", + "close", + "delete", + "download", + "edit", + "event", + "error", + "fastForward", + "favorite", + "favoriteOff", + "folder", + "help", + "home", + "info", + "locationOn", + "lock", + "lockOpen", + "mail", + "menu", + "moreVert", + "moreHoriz", + "notificationsOff", + "notifications", + "pause", + "payment", + "person", + "phone", + "photo", + "play", + "print", + "refresh", + "rewind", + "search", + "send", + "settings", + "share", + "shoppingCart", + "skipNext", + "skipPrevious", + "star", + "starHalf", + "starOff", + "stop", + "upload", + "visibility", + "visibilityOff", + "volumeDown", + "volumeMute", + "volumeOff", + "volumeUp", + "warning" + ] + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "component", + "name" + ] + } + ], + "unevaluatedProperties": false + }, + "Video": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Video" + }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the video to display." + } + }, + "required": [ + "component", + "url" + ] + } + ], + "unevaluatedProperties": false + }, + "AudioPlayer": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "AudioPlayer" + }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the audio to be played." + }, + "description": { + "description": "A description of the audio, such as a title or summary.", + "$ref": "common_types.json#/$defs/DynamicString" + } + }, + "required": [ + "component", + "url" + ] + } + ], + "unevaluatedProperties": false + }, + "Row": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children horizontally. To create a grid layout, nest Columns within this Row.", + "properties": { + "component": { + "const": "Row" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (horizontally). Use 'spaceBetween' to push items to the edges, or 'start'/'end'/'center' to pack them together.", + "enum": [ + "center", + "end", + "spaceAround", + "spaceBetween", + "spaceEvenly", + "start", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (vertically). This is similar to the CSS 'align-items' property, but uses camelCase values (e.g., 'start').", + "enum": [ + "start", + "center", + "end", + "stretch" + ], + "default": "stretch" + } + }, + "required": [ + "component", + "children" + ] + } + ], + "unevaluatedProperties": false + }, + "Column": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A layout component that arranges its children vertically. To create a grid layout, nest Rows within this Column.", + "properties": { + "component": { + "const": "Column" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (vertically). Use 'spaceBetween' to push items to the edges (e.g. header at top, footer at bottom), or 'start'/'end'/'center' to pack them together.", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly", + "stretch" + ], + "default": "start" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (horizontally). This is similar to the CSS 'align-items' property.", + "enum": [ + "center", + "end", + "start", + "stretch" + ], + "default": "stretch" + } + }, + "required": [ + "component", + "children" + ] + } + ], + "unevaluatedProperties": false + }, + "List": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "List" + }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "direction": { + "type": "string", + "description": "The direction in which the list items are laid out.", + "enum": [ + "vertical", + "horizontal" + ], + "default": "vertical" + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis.", + "enum": [ + "start", + "center", + "end", + "stretch" + ], + "default": "stretch" + } + }, + "required": [ + "component", + "children" + ] + } + ], + "unevaluatedProperties": false + }, + "Card": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Card" + }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the single child component to be rendered inside the card. To display multiple elements, you MUST wrap them in a layout component (like Column or Row) and pass that container's ID here. Do NOT pass multiple IDs or a non-existent ID. Do NOT define the child component inline." + } + }, + "required": [ + "component", + "child" + ] + } + ], + "unevaluatedProperties": false + }, + "Tabs": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Tabs" + }, + "tabs": { + "type": "array", + "description": "An array of objects, where each object defines a tab with a title and a child component.", + "items": { + "type": "object", + "properties": { + "title": { + "description": "The tab title.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Do NOT define the component inline." + } + }, + "required": [ + "title", + "child" + ], + "additionalProperties": false + } + } + }, + "required": [ + "component", + "tabs" + ] + } + ], + "unevaluatedProperties": false + }, + "Modal": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Modal" + }, + "trigger": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." + }, + "content": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." + } + }, + "required": [ + "component", + "trigger", + "content" + ] + } + ], + "unevaluatedProperties": false + }, + "Divider": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Divider" + }, + "axis": { + "type": "string", + "description": "The orientation of the divider.", + "enum": [ + "horizontal", + "vertical" + ], + "default": "horizontal" + } + }, + "required": [ + "component" + ] + } + ], + "unevaluatedProperties": false + }, + "Button": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Button" + }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Use a 'Text' component for a labeled button. Only use an 'Icon' if the requirements explicitly ask for an icon-only button. Do NOT define the child component inline." + }, + "variant": { + "type": "string", + "description": "A hint for the button style. If omitted, a default button style is used. 'primary' indicates this is the main call-to-action button. 'borderless' means the button has no visual border or background, making its child content appear like a clickable link.", + "enum": [ + "default", + "primary", + "borderless" + ], + "default": "default" + }, + "action": { + "$ref": "common_types.json#/$defs/Action" + } + }, + "required": [ + "component", + "child", + "action" + ] + } + ], + "unevaluatedProperties": false + }, + "TextField": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "TextField" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The value of the text field." + }, + "variant": { + "type": "string", + "description": "The type of input field to display.", + "enum": [ + "longText", + "number", + "shortText", + "obscured" + ], + "default": "shortText" + }, + "validationRegexp": { + "type": "string", + "description": "A regular expression used for client-side validation of the input." + } + }, + "required": [ + "component", + "label" + ] + } + ], + "unevaluatedProperties": false + }, + "CheckBox": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "CheckBox" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text to display next to the checkbox." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "The current state of the checkbox (true for checked, false for unchecked)." + } + }, + "required": [ + "component", + "label", + "value" + ] + } + ], + "unevaluatedProperties": false + }, + "ChoicePicker": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "description": "A component that allows selecting one or more options from a list.", + "properties": { + "component": { + "const": "ChoicePicker" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The label for the group of options." + }, + "variant": { + "type": "string", + "description": "A hint for how the choice picker should be displayed and behave.", + "enum": [ + "multipleSelection", + "mutuallyExclusive" + ], + "default": "mutuallyExclusive" + }, + "options": { + "type": "array", + "description": "The list of available options to choose from.", + "items": { + "type": "object", + "properties": { + "label": { + "description": "The text to display for this option.", + "$ref": "common_types.json#/$defs/DynamicString" + }, + "value": { + "type": "string", + "description": "The stable value associated with this option." + } + }, + "required": [ + "label", + "value" + ], + "additionalProperties": false + } + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicStringList", + "description": "The list of currently selected values. This should be bound to a string array in the data model." + }, + "displayStyle": { + "type": "string", + "description": "The display style of the component.", + "enum": [ + "checkbox", + "chips" + ], + "default": "checkbox" + }, + "filterable": { + "type": "boolean", + "description": "If true, displays a search input to filter the options.", + "default": false + } + }, + "required": [ + "component", + "options", + "value" + ] + } + ], + "unevaluatedProperties": false + }, + "Slider": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "Slider" + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The label for the slider." + }, + "min": { + "type": "number", + "description": "The minimum value of the slider.", + "default": 0 + }, + "max": { + "type": "number", + "description": "The maximum value of the slider." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The current value of the slider." + } + }, + "required": [ + "component", + "value", + "max" + ] + } + ], + "unevaluatedProperties": false + }, + "DateTimeInput": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "$ref": "common_types.json#/$defs/Checkable" + }, + { + "type": "object", + "properties": { + "component": { + "const": "DateTimeInput" + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." + }, + "enableDate": { + "type": "boolean", + "description": "If true, allows the user to select a date.", + "default": false + }, + "enableTime": { + "type": "boolean", + "description": "If true, allows the user to select a time.", + "default": false + }, + "min": { + "allOf": [ + { + "$ref": "common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The minimum allowed date/time in ISO 8601 format." + }, + "max": { + "allOf": [ + { + "$ref": "common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" + }, + "then": { + "oneOf": [ + { + "format": "date" + }, + { + "format": "time" + }, + { + "format": "date-time" + } + ] + } + } + ], + "description": "The maximum allowed date/time in ISO 8601 format." + }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + } + }, + "required": [ + "component", + "value" + ] + } + ], + "unevaluatedProperties": false + }, + "Canvas": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "Renders the UI element in a stateful panel next to the chat window.", + "properties": { + "component": { + "const": "Canvas" + }, + "children": { + "$ref": "common_types.json#/$defs/ChildList", + "description": "Defines the children of the canvas." + } + }, + "required": [ + "component", + "children" + ] + } + ], + "unevaluatedProperties": false + }, + "Chart": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "An interactive chart that uses a hierarchical list of objects for its data.", + "properties": { + "component": { + "const": "Chart" + }, + "type": { + "type": "string", + "description": "The type of chart to render.", + "enum": [ + "doughnut", + "pie" + ] + }, + "title": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The title of the chart." + }, + "chartData": { + "description": "The data for the chart, provided as a list of items. Can be a literal array or a data model path.", + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ChartItem" + } + }, + { + "$ref": "common_types.json#/$defs/DataBinding" + } + ] + } + }, + "required": [ + "component", + "type", + "chartData" + ] + } + ], + "unevaluatedProperties": false + }, + "GoogleMap": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "$ref": "#/$defs/CatalogComponentCommon" + }, + { + "type": "object", + "description": "A component to display a Google Map with pins.", + "properties": { + "component": { + "const": "GoogleMap" + }, + "center": { + "$ref": "#/$defs/DynamicLatLng", + "description": "The center point of the map, containing latitude and longitude." + }, + "zoom": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The zoom level of the map." + }, + "pins": { + "description": "A list of pin objects to display on the map. Can be a literal array or a data model path.", + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/MapPin" + } + }, + { + "$ref": "common_types.json#/$defs/DataBinding" + } + ] + } + }, + "required": [ + "component", + "center", + "zoom" + ] + } + ], + "unevaluatedProperties": false + } + }, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": { + "weight": { + "$ref": "common_types.json#/$defs/DynamicNumber" + } + } + }, + "ChartItem": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "number" + }, + "drillDown": { + "type": "array", + "description": "An optional list of items for the next level of data.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "number" + } + }, + "required": [ + "label", + "value" + ] + } + } + }, + "required": [ + "label", + "value" + ] + }, + "DynamicLatLng": { + "oneOf": [ + { + "type": "object", + "properties": { + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + } + }, + "required": [ + "lat", + "lng" + ], + "additionalProperties": false + }, + { + "$ref": "common_types.json#/$defs/DataBinding" + } + ] + }, + "MapPin": { + "type": "object", + "properties": { + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "background": { + "type": "string", + "description": "Hex color code for the pin background (e.g., '#FBBC04')." + }, + "borderColor": { + "type": "string", + "description": "Hex color code for the pin border (e.g., '#000000')." + }, + "glyphColor": { + "type": "string", + "description": "Hex color code for the pin's glyph/icon (e.g., '#000000')." + } + }, + "required": [ + "lat", + "lng", + "name" + ], + "additionalProperties": false + }, + "anyComponent": { + "oneOf": [ + { + "$ref": "#/components/AudioPlayer" + }, + { + "$ref": "#/components/Button" + }, + { + "$ref": "#/components/Canvas" + }, + { + "$ref": "#/components/Card" + }, + { + "$ref": "#/components/Chart" + }, + { + "$ref": "#/components/CheckBox" + }, + { + "$ref": "#/components/ChoicePicker" + }, + { + "$ref": "#/components/Column" + }, + { + "$ref": "#/components/DateTimeInput" + }, + { + "$ref": "#/components/Divider" + }, + { + "$ref": "#/components/GoogleMap" + }, + { + "$ref": "#/components/Icon" + }, + { + "$ref": "#/components/Image" + }, + { + "$ref": "#/components/List" + }, + { + "$ref": "#/components/Modal" + }, + { + "$ref": "#/components/Row" + }, + { + "$ref": "#/components/Slider" + }, + { + "$ref": "#/components/Tabs" + }, + { + "$ref": "#/components/Text" + }, + { + "$ref": "#/components/TextField" + }, + { + "$ref": "#/components/Video" + } + ] + }, + "anyFunction": { + "enum": [ + "none" + ] + } + } +} \ No newline at end of file diff --git a/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/chart.json b/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.8/chart.json similarity index 100% rename from samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/chart.json rename to samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.8/chart.json diff --git a/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/map.json b/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.8/map.json similarity index 100% rename from samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/map.json rename to samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.8/map.json diff --git a/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/chart.json b/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/chart.json new file mode 100644 index 000000000..655c08f6c --- /dev/null +++ b/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/chart.json @@ -0,0 +1,87 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sales-dashboard", + "catalogId": "https://github.com/google/A2UI/blob/main/samples/agent/adk/rizzcharts/rizzcharts_catalog_definition.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sales-dashboard", + "components": [ + { + "id": "root", + "component": "Canvas", + "children": [ + "chart-container" + ] + }, + { + "id": "chart-container", + "component": "Column", + "children": [ + "sales-chart" + ], + "align": "center" + }, + { + "id": "sales-chart", + "component": "Chart", + "type": "doughnut", + "title": { + "path": "/chart.title" + }, + "chartData": { + "path": "/chart.items" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "sales-dashboard", + "path": "/", + "value": { + "chart.title": "Sales by Category", + "chart.items[0].label": "Apparel", + "chart.items[0].value": 41, + "chart.items[0].drillDown[0].label": "Tops", + "chart.items[0].drillDown[0].value": 31, + "chart.items[0].drillDown[1].label": "Bottoms", + "chart.items[0].drillDown[1].value": 38, + "chart.items[0].drillDown[2].label": "Outerwear", + "chart.items[0].drillDown[2].value": 20, + "chart.items[0].drillDown[3].label": "Footwear", + "chart.items[0].drillDown[3].value": 11, + "chart.items[1].label": "Home Goods", + "chart.items[1].value": 15, + "chart.items[1].drillDown[0].label": "Pillow", + "chart.items[1].drillDown[0].value": 8, + "chart.items[1].drillDown[1].label": "Coffee Maker", + "chart.items[1].drillDown[1].value": 16, + "chart.items[1].drillDown[2].label": "Area Rug", + "chart.items[1].drillDown[2].value": 3, + "chart.items[1].drillDown[3].label": "Bath Towels", + "chart.items[1].drillDown[3].value": 14, + "chart.items[2].label": "Electronics", + "chart.items[2].value": 28, + "chart.items[2].drillDown[0].label": "Phones", + "chart.items[2].drillDown[0].value": 25, + "chart.items[2].drillDown[1].label": "Laptops", + "chart.items[2].drillDown[1].value": 27, + "chart.items[2].drillDown[2].label": "TVs", + "chart.items[2].drillDown[2].value": 21, + "chart.items[2].drillDown[3].label": "Other", + "chart.items[2].drillDown[3].value": 27, + "chart.items[3].label": "Health & Beauty", + "chart.items[3].value": 10, + "chart.items[4].label": "Other", + "chart.items[4].value": 6 + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/map.json b/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/map.json new file mode 100644 index 000000000..a403a461f --- /dev/null +++ b/samples/agent/adk/rizzcharts/examples/rizzcharts_catalog/0.9/map.json @@ -0,0 +1,86 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "la-map-view", + "catalogId": "https://github.com/google/A2UI/blob/main/samples/agent/adk/rizzcharts/rizzcharts_catalog_definition.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "la-map-view", + "components": [ + { + "id": "root", + "component": "Canvas", + "children": [ + "map-layout-container" + ] + }, + { + "id": "map-layout-container", + "component": "Column", + "children": [ + "map-header", + "location-map" + ], + "align": "stretch" + }, + { + "id": "map-header", + "component": "Text", + "text": "Points of Interest in Los Angeles", + "variant": "h2" + }, + { + "id": "location-map", + "component": "GoogleMap", + "center": { + "path": "/mapConfig.center" + }, + "zoom": { + "path": "/mapConfig.zoom" + }, + "pins": { + "path": "/mapConfig.locations" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "la-map-view", + "path": "/", + "value": { + "mapConfig.center.lat": 34.0522, + "mapConfig.center.lng": -118.2437, + "mapConfig.zoom": 11, + "mapConfig.locations[0].lat": 34.0135, + "mapConfig.locations[0].lng": -118.4947, + "mapConfig.locations[0].name": "Google Store Santa Monica", + "mapConfig.locations[0].description": "Your local destination for Google hardware.", + "mapConfig.locations[0].background": "#4285F4", + "mapConfig.locations[0].borderColor": "#FFFFFF", + "mapConfig.locations[0].glyphColor": "#FFFFFF", + "mapConfig.locations[1].lat": 34.1341, + "mapConfig.locations[1].lng": -118.3215, + "mapConfig.locations[1].name": "Griffith Observatory", + "mapConfig.locations[2].lat": 34.134, + "mapConfig.locations[2].lng": -118.3397, + "mapConfig.locations[2].name": "Hollywood Sign Viewpoint", + "mapConfig.locations[3].lat": 34.0453, + "mapConfig.locations[3].lng": -118.2673, + "mapConfig.locations[3].name": "Crypto.com Arena", + "mapConfig.locations[4].lat": 34.0639, + "mapConfig.locations[4].lng": -118.3592, + "mapConfig.locations[4].name": "LACMA", + "mapConfig.locations[5].lat": 33.985, + "mapConfig.locations[5].lng": -118.4729, + "mapConfig.locations[5].name": "Venice Beach Boardwalk" + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/rizzcharts/examples/standard_catalog/chart.json b/samples/agent/adk/rizzcharts/examples/standard_catalog/0.8/chart.json similarity index 100% rename from samples/agent/adk/rizzcharts/examples/standard_catalog/chart.json rename to samples/agent/adk/rizzcharts/examples/standard_catalog/0.8/chart.json diff --git a/samples/agent/adk/rizzcharts/examples/standard_catalog/map.json b/samples/agent/adk/rizzcharts/examples/standard_catalog/0.8/map.json similarity index 100% rename from samples/agent/adk/rizzcharts/examples/standard_catalog/map.json rename to samples/agent/adk/rizzcharts/examples/standard_catalog/0.8/map.json diff --git a/samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/chart.json b/samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/chart.json new file mode 100644 index 000000000..e6517ae65 --- /dev/null +++ b/samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/chart.json @@ -0,0 +1,94 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sales-dashboard", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#00BFFF", + "font": "Arial" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sales-dashboard", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "chart-title", + "category-list" + ] + }, + { + "id": "chart-title", + "component": "Text", + "text": { + "path": "/chart.title" + }, + "variant": "h2" + }, + { + "id": "category-list", + "component": "List", + "direction": "vertical", + "children": { + "componentId": "category-item-template", + "path": "/chart.items" + } + }, + { + "id": "category-item-template", + "component": "Card", + "child": "item-row" + }, + { + "id": "item-row", + "component": "Row", + "justify": "spaceBetween", + "children": [ + "item-label", + "item-value" + ] + }, + { + "id": "item-label", + "component": "Text", + "text": { + "path": "/label" + } + }, + { + "id": "item-value", + "component": "Text", + "text": { + "path": "/value" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "sales-dashboard", + "path": "/", + "value": { + "chart.title": "Sales by Category", + "chart.items[0].label": "Apparel", + "chart.items[0].value": 41, + "chart.items[1].label": "Home Goods", + "chart.items[1].value": 15, + "chart.items[2].label": "Electronics", + "chart.items[2].value": 28, + "chart.items[3].label": "Health & Beauty", + "chart.items[3].value": 10, + "chart.items[4].label": "Other", + "chart.items[4].value": 6 + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/map.json b/samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/map.json new file mode 100644 index 000000000..d1c1521c3 --- /dev/null +++ b/samples/agent/adk/rizzcharts/examples/standard_catalog/0.9/map.json @@ -0,0 +1,101 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "la-map-view", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "theme": { + "primaryColor": "#4285F4", + "font": "Roboto" + } + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "la-map-view", + "components": [ + { + "id": "root", + "component": "Column", + "children": [ + "map-header", + "map-image", + "location-list" + ], + "align": "stretch" + }, + { + "id": "map-header", + "component": "Text", + "text": "Points of Interest in Los Angeles", + "variant": "h2" + }, + { + "id": "map-image", + "component": "Image", + "url": "https://maps.googleapis.com/maps/api/staticmap?center=Los+Angeles,CA&zoom=11&size=600x300&key=YOUR_API_KEY", + "fit": "cover" + }, + { + "id": "location-list", + "component": "List", + "direction": "vertical", + "children": { + "componentId": "location-card-template", + "path": "/mapConfig.locations" + } + }, + { + "id": "location-card-template", + "component": "Card", + "child": "location-details" + }, + { + "id": "location-details", + "component": "Column", + "children": [ + "location-name", + "location-description" + ] + }, + { + "id": "location-name", + "component": "Text", + "text": { + "path": "/name" + }, + "variant": "h4" + }, + { + "id": "location-description", + "component": "Text", + "text": { + "path": "/description" + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "la-map-view", + "path": "/", + "value": { + "mapConfig.locations[0].name": "Google Store Santa Monica", + "mapConfig.locations[0].description": "Your local destination for Google hardware.", + "mapConfig.locations[1].name": "Griffith Observatory", + "mapConfig.locations[1].description": "A public observatory with views of the Hollywood Sign.", + "mapConfig.locations[2].name": "Hollywood Sign Viewpoint", + "mapConfig.locations[2].description": "Iconic landmark in the Hollywood Hills.", + "mapConfig.locations[3].name": "Crypto.com Arena", + "mapConfig.locations[3].description": "Multi-purpose sports and entertainment arena.", + "mapConfig.locations[4].name": "LACMA", + "mapConfig.locations[4].description": "Los Angeles County Museum of Art.", + "mapConfig.locations[5].name": "Venice Beach Boardwalk", + "mapConfig.locations[5].description": "Famous oceanfront promenade." + } + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/rizzcharts/prompt_builder.py b/samples/agent/adk/rizzcharts/prompt_builder.py new file mode 100644 index 000000000..75225e2b9 --- /dev/null +++ b/samples/agent/adk/rizzcharts/prompt_builder.py @@ -0,0 +1,84 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Prompt builder for the rizzcharts agent.""" + +# pylint: disable=g-importing-member, line-too-long +from a2ui.core.schema.constants import VERSION_0_9 +from a2ui.core.schema.manager import A2uiSchemaManager, CatalogConfig +from a2ui.basic_catalog.provider import BasicCatalog +from a2ui.core.schema.common_modifiers import remove_strict_validation +from agent import ROLE_DESCRIPTION, WORKFLOW_DESCRIPTION, UI_DESCRIPTION + + +if __name__ == "__main__": + version = VERSION_0_9 + schema_manager = A2uiSchemaManager( + version, + catalogs=[ + CatalogConfig.from_path( + name="rizzcharts", + catalog_path="rizzcharts_catalog_definition.json", + examples_path=f"examples/rizzcharts_catalog/{version}", + ), + BasicCatalog.get_config( + version=version, + examples_path=f"examples/standard_catalog/{version}", + ), + ], + accepts_inline_catalogs=True, + schema_modifiers=[remove_strict_validation], + ) + + # Generate prompt for rizzcharts catalog + print("Building prompt and validating rizzcharts examples...") + system_prompt = schema_manager.generate_system_prompt( + role_description=ROLE_DESCRIPTION, + workflow_description=WORKFLOW_DESCRIPTION, + ui_description=UI_DESCRIPTION, + include_schema=True, + include_examples=True, + validate_examples=True, + ) + + output = system_prompt + + # Also validate standard catalog examples + print("Validating standard catalog examples...") + # We can trigger this by selecting the basic catalog + std_prompt = schema_manager.generate_system_prompt( + role_description=ROLE_DESCRIPTION, + workflow_description=WORKFLOW_DESCRIPTION, + ui_description=UI_DESCRIPTION, + client_ui_capabilities={ + "supported_catalog_ids": [ + "https://a2ui.org/specification/v0_9/basic_catalog.json" + ] + }, + include_schema=False, + include_examples=True, + validate_examples=True, + ) + + if std_prompt: + output += "\n\n### Standard Catalog Examples:\n" + # Find the start of examples in std_prompt + if "### Examples:" in std_prompt: + output += std_prompt.split("### Examples:")[1] + + print(output) + + with open("generated_prompt.txt", "w") as f: + f.write(output) + print("\nGenerated prompt saved to generated_prompt.txt") diff --git a/samples/agent/adk/tests/test_examples_validation.py b/samples/agent/adk/tests/test_examples_validation.py new file mode 100644 index 000000000..3cdd386c6 --- /dev/null +++ b/samples/agent/adk/tests/test_examples_validation.py @@ -0,0 +1,135 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import json +from pathlib import Path +from typing import Dict, Any +import pytest + +from a2ui.core.schema.constants import VERSION_0_9 +from a2ui.core.schema.manager import A2uiSchemaManager, CatalogConfig +from a2ui.basic_catalog.provider import BasicCatalog +from a2ui.core.schema.common_modifiers import remove_strict_validation +from a2ui.core.schema.catalog_provider import A2uiCatalogProvider + + +ROOT_DIR = Path(__file__).parent.parent.parent.parent.parent # a2ui root +SAMPLES_DIR = ROOT_DIR / "samples" / "agent" / "adk" + +SAMPLE_CONFIGS = [ + { + "name": "contact_lookup", + "path": SAMPLES_DIR / "contact_lookup", + "catalogs": [ + BasicCatalog.get_config( + version=VERSION_0_9, + examples_path="examples/0.9", + ) + ], + "schema_modifiers": [], + "validate": False, # Use invalid examples to test retry logic + }, + { + "name": "contact_multiple_surfaces", + "path": SAMPLES_DIR / "contact_multiple_surfaces", + "catalogs": [ + CatalogConfig.from_path( + name="contact_multiple_surfaces_inline_catalog", + catalog_path="inline_catalog_0.9.json", + examples_path=f"examples/{VERSION_0_9}", + ), + BasicCatalog.get_config( + version=VERSION_0_9, + ), + ], + "schema_modifiers": [remove_strict_validation], + "validate": True, + }, + { + "name": "restaurant_finder", + "path": SAMPLES_DIR / "restaurant_finder", + "catalogs": [ + BasicCatalog.get_config( + version=VERSION_0_9, + examples_path="examples/0.9", + ) + ], + "schema_modifiers": [remove_strict_validation], + "validate": True, + }, + { + "name": "rizzcharts", + "path": SAMPLES_DIR / "rizzcharts", + "catalogs": [ + CatalogConfig.from_path( + name="rizzcharts", + catalog_path="catalog_schemas/0.9/rizzcharts_catalog_definition.json", + examples_path="examples/rizzcharts_catalog/0.9", + ), + BasicCatalog.get_config( + version=VERSION_0_9, + examples_path="examples/standard_catalog/0.9", + ), + ], + "schema_modifiers": [remove_strict_validation], + "validate": True, + }, +] + + +@pytest.mark.parametrize("config", SAMPLE_CONFIGS) +def test_sample_examples_validation(config): + """Validates that all examples for a given sample pass A2UI validation.""" + do_validate = config.get("validate", True) + sample_path = config["path"] + os.chdir(sample_path) # Change to sample dir to resolve relative catalog paths if any + + manager = A2uiSchemaManager( + VERSION_0_9, + catalogs=config["catalogs"], + accepts_inline_catalogs=True, + schema_modifiers=config["schema_modifiers"], + ) + + # Iterate through each catalog and validate its examples + for catalog in manager._supported_catalogs: + examples_path = manager._catalog_example_paths.get(catalog.catalog_id) + if not examples_path: + continue + + # manager.load_examples(catalog, validate=True) returns a combined string. + # It internally calls _validate_example which logs warnings on failure. + # To strictly fail the test, we want to capture those failures or re-implement. + + path = Path(examples_path) + if not path.is_absolute(): + path = sample_path / path + + assert ( + path.is_dir() + ), f"Examples directory not found: {path} for sample {config['name']}" + + for filename in os.listdir(path): + if filename.endswith(".json"): + full_path = path / filename + with open(full_path, "r", encoding="utf-8") as f: + content = json.load(f) + try: + if do_validate: + catalog.validator.validate(content) + except Exception as e: + pytest.fail( + f"Validation failed for {full_path} in sample {config['name']}: {e}" + ) From a4b98179e67bebc8a2877347734f5ab6ff1156da Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Wed, 25 Mar 2026 06:00:14 +0000 Subject: [PATCH 2/4] Update component_gallery sample --- .../agent/adk/component_gallery/__main__.py | 4 +- samples/agent/adk/component_gallery/agent.py | 12 +- .../adk/component_gallery/agent_executor.py | 5 +- .../adk/component_gallery/gallery_examples.py | 542 +++++++++++++++++- 4 files changed, 533 insertions(+), 30 deletions(-) diff --git a/samples/agent/adk/component_gallery/__main__.py b/samples/agent/adk/component_gallery/__main__.py index 15d5d95fc..c6322e90f 100644 --- a/samples/agent/adk/component_gallery/__main__.py +++ b/samples/agent/adk/component_gallery/__main__.py @@ -28,7 +28,7 @@ from starlette.middleware.cors import CORSMiddleware from starlette.staticfiles import StaticFiles from dotenv import load_dotenv -from a2ui.core.schema.constants import VERSION_0_8 +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9 from agent_executor import ComponentGalleryExecutor @@ -51,7 +51,7 @@ def main(host, port): try: extensions = [] - for v in [VERSION_0_8]: + for v in [VERSION_0_8, VERSION_0_9]: extensions.append(get_a2ui_agent_extension(v)) capabilities = AgentCapabilities( streaming=True, diff --git a/samples/agent/adk/component_gallery/agent.py b/samples/agent/adk/component_gallery/agent.py index 855d4d935..e73401f16 100644 --- a/samples/agent/adk/component_gallery/agent.py +++ b/samples/agent/adk/component_gallery/agent.py @@ -16,7 +16,7 @@ import logging from collections.abc import AsyncIterable -from typing import Any +from typing import Any, Optional import json from a2a.types import DataPart, Part, TextPart @@ -37,14 +37,18 @@ class ComponentGalleryAgent: def __init__(self, base_url: str): self.base_url = base_url - async def stream(self, query: str, session_id: str) -> AsyncIterable[dict[str, Any]]: + async def stream( + self, query: str, session_id: str, active_ui_version: Optional[str] + ) -> AsyncIterable[dict[str, Any]]: """Streams the gallery or responses to actions.""" - logger.info(f"Stream called with query: {query}") + logger.info( + f"Stream called with query: {query} and active_ui_version: {active_ui_version}" + ) # Initial Load or Reset if "WHO_ARE_YOU" in query or "START" in query: # Simple trigger for initial load - gallery_json = get_gallery_json() + gallery_json = get_gallery_json(active_ui_version) yield { "is_task_complete": True, "parts": parse_response_to_parts( diff --git a/samples/agent/adk/component_gallery/agent_executor.py b/samples/agent/adk/component_gallery/agent_executor.py index ce9c44232..ef8afd8c8 100644 --- a/samples/agent/adk/component_gallery/agent_executor.py +++ b/samples/agent/adk/component_gallery/agent_executor.py @@ -37,7 +37,8 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non query = "START" # Default start ui_event_part = None - try_activate_a2ui_extension(context, self._agent_card) + active_ui_version = try_activate_a2ui_extension(context, self._agent_card) + logger.info(f"Active UI version: {active_ui_version}") if context.message and context.message.parts: for part in context.message.parts: @@ -63,7 +64,7 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non updater = TaskUpdater(event_queue, task.id, task.context_id) - async for item in self.agent.stream(query, task.context_id): + async for item in self.agent.stream(query, task.context_id, active_ui_version): final_parts = item["parts"] await updater.update_status( diff --git a/samples/agent/adk/component_gallery/gallery_examples.py b/samples/agent/adk/component_gallery/gallery_examples.py index f241ea320..8990f9dcc 100644 --- a/samples/agent/adk/component_gallery/gallery_examples.py +++ b/samples/agent/adk/component_gallery/gallery_examples.py @@ -15,17 +15,29 @@ """Defines the Component Gallery 'Kitchen Sink' example.""" import json +from typing import Optional +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9 -def get_gallery_json() -> str: +def get_gallery_json(version: Optional[str]) -> str: """Returns the JSON structure for the Component Gallery surfaces.""" + # Default to v0.8 if no version is provided + if not version: + version = VERSION_0_8 + + if version == VERSION_0_9: + return get_v0_9_gallery_json() + else: + return get_v0_8_gallery_json() + + +def get_v0_8_gallery_json() -> str: + """Returns the JSON structure for the Component Gallery surfaces (v0.8).""" + messages = [] - # Common Data Model - # We use a single global data model for simplicity across all demo surfaces. - # Common Data Model Content - # We define the content here and inject it into EACH surface so they all share the same initial state. + # Common Data Model for v0.8 gallery_data_content = { "key": "galleryData", "valueMap": [ @@ -44,17 +56,25 @@ def get_gallery_json() -> str: def add_demo_surface(surface_id, component_def): root_id = f"{surface_id}-root" - components = [] - components.append({"id": root_id, "component": component_def}) - - messages.append({"beginRendering": {"surfaceId": surface_id, "root": root_id}}) messages.append( - {"surfaceUpdate": {"surfaceId": surface_id, "components": components}} + { + "beginRendering": { + "surfaceId": surface_id, + "root": root_id, + } + } ) - - # Inject data model for this surface messages.append({ - "dataModelUpdate": {"surfaceId": surface_id, "contents": [gallery_data_content]} + "surfaceUpdate": { + "surfaceId": surface_id, + "components": [{"id": root_id, "component": component_def}], + } + }) + messages.append({ + "dataModelUpdate": { + "surfaceId": surface_id, + "contents": [gallery_data_content], + } }) # 1. TextField @@ -181,7 +201,12 @@ def add_demo_surface(surface_id, component_def): btn_text_id = "demo-button-text" messages.append( - {"beginRendering": {"surfaceId": button_surface_id, "root": btn_root_id}} + { + "beginRendering": { + "surfaceId": button_surface_id, + "root": btn_root_id, + } + } ) messages.append({ "surfaceUpdate": { @@ -218,7 +243,12 @@ def add_demo_surface(surface_id, component_def): tab2_id = "tab-2-content" messages.append( - {"beginRendering": {"surfaceId": tabs_surface_id, "root": tabs_root_id}} + { + "beginRendering": { + "surfaceId": tabs_surface_id, + "root": tabs_root_id, + } + } ) messages.append({ "surfaceUpdate": { @@ -260,7 +290,12 @@ def add_demo_surface(surface_id, component_def): # 9. Icon icon_surface_id = "demo-icon" messages.append( - {"beginRendering": {"surfaceId": icon_surface_id, "root": "icon-root"}} + { + "beginRendering": { + "surfaceId": icon_surface_id, + "root": "icon-root", + } + } ) messages.append({ "surfaceUpdate": { @@ -294,7 +329,14 @@ def add_demo_surface(surface_id, component_def): # 10. Divider div_surface_id = "demo-divider" - messages.append({"beginRendering": {"surfaceId": div_surface_id, "root": "div-root"}}) + messages.append( + { + "beginRendering": { + "surfaceId": div_surface_id, + "root": "div-root", + } + } + ) messages.append({ "surfaceUpdate": { "surfaceId": div_surface_id, @@ -327,7 +369,12 @@ def add_demo_surface(surface_id, component_def): # 11. Card card_surface_id = "demo-card" messages.append( - {"beginRendering": {"surfaceId": card_surface_id, "root": "card-root"}} + { + "beginRendering": { + "surfaceId": card_surface_id, + "root": "card-root", + } + } ) messages.append({ "surfaceUpdate": { @@ -363,7 +410,12 @@ def add_demo_surface(surface_id, component_def): # Modal needs an entry point (Button) and content. modal_surface_id = "demo-modal" messages.append( - {"beginRendering": {"surfaceId": modal_surface_id, "root": "modal-root"}} + { + "beginRendering": { + "surfaceId": modal_surface_id, + "root": "modal-root", + } + } ) messages.append({ "surfaceUpdate": { @@ -405,7 +457,12 @@ def add_demo_surface(surface_id, component_def): # 14. List list_surface_id = "demo-list" messages.append( - {"beginRendering": {"surfaceId": list_surface_id, "root": "list-root"}} + { + "beginRendering": { + "surfaceId": list_surface_id, + "root": "list-root", + } + } ) messages.append({ "surfaceUpdate": { @@ -455,12 +512,18 @@ def add_demo_surface(surface_id, component_def): ) # Response Surface + resp_surface_id = "response-surface" messages.append( - {"beginRendering": {"surfaceId": "response-surface", "root": "response-text"}} + { + "beginRendering": { + "surfaceId": resp_surface_id, + "root": "response-text", + } + } ) messages.append({ "surfaceUpdate": { - "surfaceId": "response-surface", + "surfaceId": resp_surface_id, "components": [{ "id": "response-text", "component": { @@ -479,3 +542,438 @@ def add_demo_surface(surface_id, component_def): }) return json.dumps(messages, indent=2) + + +def get_v0_9_gallery_json() -> str: + """Returns the JSON structure for the Component Gallery surfaces (v0.9).""" + + messages = [] + + # Common Data Model for v0.9 + gallery_data_content = { + "galleryData": { + "textField": "Hello World", + "checkbox": False, + "checkboxChecked": True, + "slider": 30, + "date": "2025-10-26", + "favorites": ["A"], + "favoritesChips": [], + "favoritesFilter": [], + } + } + + def add_message(msg): + msg["version"] = "v0.9" + messages.append(msg) + + def add_demo_surface(surface_id, component_def): + root_id = "root" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + + add_message({"createSurface": {"surfaceId": surface_id, "catalogId": catalog_id}}) + + # In v0.9, components include their id and component type directly + full_component = {"id": root_id} + full_component.update(component_def) + + add_message( + { + "updateComponents": { + "surfaceId": surface_id, + "components": [full_component], + } + } + ) + add_message( + { + "updateDataModel": { + "surfaceId": surface_id, + "value": gallery_data_content, + } + } + ) + + # 1. TextField + add_demo_surface( + "demo-text", + { + "component": "TextField", + "label": "Enter some text", + "value": {"path": "galleryData/textField"}, + }, + ) + + # 1b. TextField (Regex) + add_demo_surface( + "demo-text-regex", + { + "component": "TextField", + "label": "Enter exactly 5 digits", + "value": {"path": "galleryData/textFieldRegex"}, + "validationRegexp": "^\\d{5}$", + }, + ) + + # 2. CheckBox + add_demo_surface( + "demo-checkbox", + { + "component": "CheckBox", + "label": "Toggle me", + "value": {"path": "galleryData/checkbox"}, + }, + ) + + # 3. Slider + add_demo_surface( + "demo-slider", + { + "component": "Slider", + "label": "Adjust slider", + "value": {"path": "galleryData/slider"}, + "min": 0, + "max": 100, + }, + ) + + # 4. DateTimeInput + add_demo_surface( + "demo-date", + { + "component": "DateTimeInput", + "label": "Select Date", + "value": {"path": "galleryData/date"}, + "enableDate": True, + }, + ) + + # 5. ChoicePicker + add_demo_surface( + "demo-multichoice", + { + "component": "ChoicePicker", + "label": "Select favorites", + "value": {"path": "galleryData/favorites"}, + "options": [ + {"label": "Apple", "value": "A"}, + {"label": "Banana", "value": "B"}, + {"label": "Cherry", "value": "C"}, + ], + }, + ) + + # 5b. ChoicePicker (Chips) + add_demo_surface( + "demo-multichoice-chips", + { + "component": "ChoicePicker", + "label": "Select tags", + "value": {"path": "galleryData/favoritesChips"}, + "variant": "multipleSelection", + "displayStyle": "chips", + "options": [ + {"label": "Work", "value": "work"}, + {"label": "Home", "value": "home"}, + {"label": "Urgent", "value": "urgent"}, + {"label": "Later", "value": "later"}, + ], + }, + ) + + # 5c. ChoicePicker (Filterable) + add_demo_surface( + "demo-multichoice-filter", + { + "component": "ChoicePicker", + "label": "Select countries", + "value": {"path": "galleryData/favoritesFilter"}, + "filterable": True, + "options": [ + {"label": "United States", "value": "US"}, + {"label": "Canada", "value": "CA"}, + {"label": "United Kingdom", "value": "UK"}, + {"label": "Australia", "value": "AU"}, + {"label": "Germany", "value": "DE"}, + {"label": "France", "value": "FR"}, + {"label": "Japan", "value": "JP"}, + ], + }, + ) + + # 6. Image + add_demo_surface( + "demo-image", + { + "component": "Image", + "url": "http://localhost:10005/assets/a2ui.png", + "variant": "mediumFeature", + }, + ) + + # 7. Button + button_surface_id = "demo-button" + btn_root_id = "root" + btn_text_id = "demo-button-text" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + + add_message( + {"createSurface": {"surfaceId": button_surface_id, "catalogId": catalog_id}} + ) + add_message({ + "updateComponents": { + "surfaceId": button_surface_id, + "components": [ + { + "id": btn_text_id, + "component": "Text", + "text": "Trigger Action", + }, + { + "id": btn_root_id, + "component": "Button", + "child": btn_text_id, + "variant": "primary", + "action": { + "event": { + "name": "custom_action", + "context": { + "info": "Custom Button Clicked", + }, + }, + }, + }, + ], + } + }) + + # 8. Tabs + tabs_surface_id = "demo-tabs" + tabs_root_id = "root" + tab1_id = "tab-1-content" + tab2_id = "tab-2-content" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + + add_message( + {"createSurface": {"surfaceId": tabs_surface_id, "catalogId": catalog_id}} + ) + add_message({ + "updateComponents": { + "surfaceId": tabs_surface_id, + "components": [ + { + "id": tab1_id, + "component": "Text", + "text": "First Tab Content", + }, + { + "id": tab2_id, + "component": "Text", + "text": "Second Tab Content", + }, + { + "id": tabs_root_id, + "component": "Tabs", + "tabs": [ + {"title": "View One", "child": tab1_id}, + {"title": "View Two", "child": tab2_id}, + ], + }, + ], + } + }) + + # 9. Icon + icon_surface_id = "demo-icon" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + add_message( + {"createSurface": {"surfaceId": icon_surface_id, "catalogId": catalog_id}} + ) + add_message({ + "updateComponents": { + "surfaceId": icon_surface_id, + "components": [ + { + "id": "root", + "component": "Row", + "children": ["icon-1", "icon-2", "icon-3"], + "justify": "spaceEvenly", + "align": "center", + }, + { + "id": "icon-1", + "component": "Icon", + "name": "star", + }, + { + "id": "icon-2", + "component": "Icon", + "name": "home", + }, + { + "id": "icon-3", + "component": "Icon", + "name": "settings", + }, + ], + } + }) + + # 10. Divider + div_surface_id = "demo-divider" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + add_message({"createSurface": {"surfaceId": div_surface_id, "catalogId": catalog_id}}) + add_message({ + "updateComponents": { + "surfaceId": div_surface_id, + "components": [ + { + "id": "root", + "component": "Column", + "children": ["div-text-1", "div-horiz", "div-text-2"], + "justify": "start", + "align": "stretch", + }, + { + "id": "div-text-1", + "component": "Text", + "text": "Above Divider", + }, + {"id": "div-horiz", "component": "Divider", "axis": "horizontal"}, + { + "id": "div-text-2", + "component": "Text", + "text": "Below Divider", + }, + ], + } + }) + + # 11. Card + card_surface_id = "demo-card" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + add_message( + {"createSurface": {"surfaceId": card_surface_id, "catalogId": catalog_id}} + ) + add_message({ + "updateComponents": { + "surfaceId": card_surface_id, + "components": [ + { + "id": "root", + "component": "Card", + "child": "card-text", + }, + { + "id": "card-text", + "component": "Text", + "text": "I am inside a Card", + }, + ], + } + }) + + # 12. Video + add_demo_surface( + "demo-video", + { + "component": "Video", + "url": ( + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + ), + }, + ) + + # 13. Modal + modal_surface_id = "demo-modal" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + add_message( + {"createSurface": {"surfaceId": modal_surface_id, "catalogId": catalog_id}} + ) + add_message({ + "updateComponents": { + "surfaceId": modal_surface_id, + "components": [ + { + "id": "root", + "component": "Modal", + "trigger": "modal-btn", + "content": "modal-content", + }, + { + "id": "modal-btn", + "component": "Button", + "child": "modal-btn-text", + "variant": "default", + "action": {"functionCall": {"call": "noop"}}, + }, + { + "id": "modal-btn-text", + "component": "Text", + "text": "Open Modal", + }, + { + "id": "modal-content", + "component": "Text", + "text": "This is the modal content!", + }, + ], + } + }) + + # 14. List + list_surface_id = "demo-list" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + add_message( + {"createSurface": {"surfaceId": list_surface_id, "catalogId": catalog_id}} + ) + add_message({ + "updateComponents": { + "surfaceId": list_surface_id, + "components": [ + { + "id": "root", + "component": "List", + "children": ["list-item-1", "list-item-2", "list-item-3"], + "direction": "vertical", + "align": "stretch", + }, + {"id": "list-item-1", "component": "Text", "text": "Item 1"}, + {"id": "list-item-2", "component": "Text", "text": "Item 2"}, + {"id": "list-item-3", "component": "Text", "text": "Item 3"}, + ], + } + }) + + # 15. AudioPlayer + add_demo_surface( + "demo-audio", + { + "component": "AudioPlayer", + "url": "http://localhost:10005/assets/audio.mp3", + "description": "Local Audio Sample", + }, + ) + + # Response Surface + resp_surface_id = "response-surface" + catalog_id = "https://a2ui.org/specification/v0_9/basic_catalog.json" + add_message( + {"createSurface": {"surfaceId": resp_surface_id, "catalogId": catalog_id}} + ) + add_message({ + "updateComponents": { + "surfaceId": resp_surface_id, + "components": [{ + "id": "root", + "component": "Text", + "text": ( + "Interact with the gallery to see responses. This view is" + " updated by the agent by relaying the raw action" + " commands it received from the client" + ), + }], + } + }) + + return json.dumps(messages, indent=2) From 5b4c60b55411a72f089d05eb3c4fd7ede3129a87 Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Wed, 25 Mar 2026 06:04:42 +0000 Subject: [PATCH 3/4] Update contact_multiple_surfaces sample --- samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py | 1 + samples/agent/adk/contact_multiple_surfaces/agent.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py b/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py index df99bcea6..b628c601c 100644 --- a/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py +++ b/samples/agent/adk/contact_multiple_surfaces/a2ui_examples.py @@ -16,6 +16,7 @@ import logging import os from pathlib import Path +from typing import Optional from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9 import jsonschema diff --git a/samples/agent/adk/contact_multiple_surfaces/agent.py b/samples/agent/adk/contact_multiple_surfaces/agent.py index 8b90474dc..00428a5f6 100644 --- a/samples/agent/adk/contact_multiple_surfaces/agent.py +++ b/samples/agent/adk/contact_multiple_surfaces/agent.py @@ -40,7 +40,7 @@ from google.genai import types from prompt_builder import get_text_prompt, ROLE_DESCRIPTION, WORKFLOW_DESCRIPTION, UI_DESCRIPTION from tools import get_contact_info -from a2ui.core.schema.constants import VERSION_0_8, A2UI_OPEN_TAG, A2UI_CLOSE_TAG +from a2ui.core.schema.constants import VERSION_0_8, VERSION_0_9, A2UI_OPEN_TAG, A2UI_CLOSE_TAG from a2ui.core.schema.common_modifiers import remove_strict_validation from a2ui.core.schema.manager import A2uiSchemaManager from a2ui.core.parser.parser import parse_response, ResponsePart From 5c02c65c704e7503b722688664f972d0cd0509e1 Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Wed, 25 Mar 2026 06:20:47 +0000 Subject: [PATCH 4/4] Update the mcp_calculator sample --- samples/agent/adk/mcp_app_proxy/agent.py | 13 +++-- .../{ => catalogs/0.8}/mcp_app_catalog.json | 0 .../catalogs/0.9/mcp_app_catalog.json | 50 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) rename samples/agent/adk/mcp_app_proxy/{ => catalogs/0.8}/mcp_app_catalog.json (100%) create mode 100644 samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json diff --git a/samples/agent/adk/mcp_app_proxy/agent.py b/samples/agent/adk/mcp_app_proxy/agent.py index 220c8ad38..ad0a94e42 100644 --- a/samples/agent/adk/mcp_app_proxy/agent.py +++ b/samples/agent/adk/mcp_app_proxy/agent.py @@ -72,6 +72,11 @@ def __init__( self._agent_name = "mcp_app_proxy_agent" self._user_id = "remote_agent" + + self._session_service = InMemorySessionService() + self._memory_service = InMemoryMemoryService() + self._artifact_service = InMemoryArtifactService() + self._text_runner: Optional[Runner] = self._build_runner(self._build_llm_agent()) self._schema_managers: Dict[str, A2uiSchemaManager] = {} @@ -105,7 +110,7 @@ def _build_schema_manager(self, version: str) -> A2uiSchemaManager: catalogs=[ CatalogConfig.from_path( name="mcp_app_proxy", - catalog_path="mcp_app_catalog.json", + catalog_path=f"catalogs/{version}/mcp_app_catalog.json", ), ], accepts_inline_catalogs=True, @@ -150,9 +155,9 @@ def _build_runner(self, agent: LlmAgent) -> Runner: return Runner( app_name=self._agent_name, agent=agent, - artifact_service=InMemoryArtifactService(), - session_service=InMemorySessionService(), - memory_service=InMemoryMemoryService(), + artifact_service=self._artifact_service, + session_service=self._session_service, + memory_service=self._memory_service, ) def _build_llm_agent( diff --git a/samples/agent/adk/mcp_app_proxy/mcp_app_catalog.json b/samples/agent/adk/mcp_app_proxy/catalogs/0.8/mcp_app_catalog.json similarity index 100% rename from samples/agent/adk/mcp_app_proxy/mcp_app_catalog.json rename to samples/agent/adk/mcp_app_proxy/catalogs/0.8/mcp_app_catalog.json diff --git a/samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json b/samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json new file mode 100644 index 000000000..99619e984 --- /dev/null +++ b/samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json @@ -0,0 +1,50 @@ +{ + "catalogId": "a2ui.org:a2ui/v0.9/mcp_app_catalog.json", + "components": { + "McpApp": { + "type": "object", + "allOf": [ + { + "$ref": "common_types.json#/$defs/ComponentCommon" + }, + { + "type": "object", + "properties": { + "component": { + "const": "McpApp" + }, + "content": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The HTML content of the app." + }, + "title": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The title of the app." + }, + "allowedTools": { + "type": "array", + "description": "List of tool names the app is allowed to call.", + "items": { + "type": "string" + } + } + }, + "required": [ + "component", + "content" + ], + "additionalProperties": false + } + ] + } + }, + "$defs": { + "anyComponent": { + "oneOf": [ + { + "$ref": "#/$defs/McpApp" + } + ] + } + } +}