From 36fa4874b9aa0ddcaa9b3bb76388f33ca736f87a Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Mon, 26 Jan 2026 23:32:55 +0000 Subject: [PATCH] Update the agent/contact_lookup sample to use the a2ui-agent python SDK Tested: - [x] The contact_lookup client successfully connected to the contact_lookup agent and rendered the response correctly. --- .../src/a2ui/inference/schema/manager.py | 7 +- samples/agent/adk/contact_lookup/__main__.py | 36 +- .../agent/adk/contact_lookup/a2ui_examples.py | 162 ---- .../agent/adk/contact_lookup/a2ui_schema.py | 788 ------------------ samples/agent/adk/contact_lookup/agent.py | 84 +- .../adk/contact_lookup/agent_executor.py | 6 +- .../examples/action_confirmation.json | 23 + .../contact_lookup/examples/contact_card.json | 54 ++ .../contact_lookup/examples/contact_list.json | 232 ++++++ .../examples/follow_success.json | 60 ++ .../adk/contact_lookup/prompt_builder.py | 117 +-- samples/agent/adk/uv.lock | 2 +- 12 files changed, 478 insertions(+), 1093 deletions(-) delete mode 100644 samples/agent/adk/contact_lookup/a2ui_examples.py delete mode 100644 samples/agent/adk/contact_lookup/a2ui_schema.py create mode 100644 samples/agent/adk/contact_lookup/examples/action_confirmation.json create mode 100644 samples/agent/adk/contact_lookup/examples/contact_card.json create mode 100644 samples/agent/adk/contact_lookup/examples/contact_list.json create mode 100644 samples/agent/adk/contact_lookup/examples/follow_success.json diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py index e9e00cf9e..cec03ca58 100644 --- a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py @@ -36,7 +36,8 @@ find_repo_root, ) from .catalog import CustomCatalogConfig, A2uiCatalog -from ...extension.a2ui_extension import INLINE_CATALOGS_KEY, SUPPORTED_CATALOG_IDS_KEY +from ...extension.a2ui_extension import INLINE_CATALOGS_KEY, SUPPORTED_CATALOG_IDS_KEY, get_a2ui_agent_extension +from a2a.types import AgentExtension def _load_basic_component(version: str, spec_name: str) -> Dict: @@ -322,3 +323,7 @@ def generate_system_prompt( parts.append(f"### Examples:\n{examples_str}") return "\n\n".join(parts) + + def get_agent_extension(self) -> AgentExtension: + catalog_ids = self._supported_catalogs.keys() + return get_a2ui_agent_extension(supported_catalog_ids=list(catalog_ids)) diff --git a/samples/agent/adk/contact_lookup/__main__.py b/samples/agent/adk/contact_lookup/__main__.py index 13b76a9a1..69ac07eb0 100644 --- a/samples/agent/adk/contact_lookup/__main__.py +++ b/samples/agent/adk/contact_lookup/__main__.py @@ -19,8 +19,6 @@ from a2a.server.apps import A2AStarletteApplication from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.tasks import InMemoryTaskStore -from a2a.types import AgentCapabilities, AgentCard, AgentSkill -from a2ui.extension.a2ui_extension import get_a2ui_agent_extension from agent import ContactAgent from agent_executor import ContactAgentExecutor from dotenv import load_dotenv @@ -50,44 +48,18 @@ def main(host, port): " is not TRUE." ) - capabilities = AgentCapabilities( - streaming=True, - extensions=[get_a2ui_agent_extension()], - ) - skill = AgentSkill( - id="find_contact", - name="Find Contact Tool", - description=( - "Helps find contact information for colleagues (e.g., email, location," - " team)." - ), - tags=["contact", "directory", "people", "finder"], - examples=["Who is David Chen in marketing?", "Find Sarah Lee from engineering"], - ) - base_url = f"http://{host}:{port}" + ui_agent = ContactAgent(base_url=base_url, use_ui=True) + text_agent = ContactAgent(base_url=base_url, use_ui=False) - agent_card = AgentCard( - name="Contact Lookup Agent", - description=( - "This agent helps find contact info for people in your organization." - ), - url=base_url, # <-- Use base_url here - version="1.0.0", - default_input_modes=ContactAgent.SUPPORTED_CONTENT_TYPES, - default_output_modes=ContactAgent.SUPPORTED_CONTENT_TYPES, - capabilities=capabilities, - skills=[skill], - ) - - agent_executor = ContactAgentExecutor(base_url=base_url) + agent_executor = ContactAgentExecutor(ui_agent=ui_agent, text_agent=text_agent) request_handler = DefaultRequestHandler( agent_executor=agent_executor, task_store=InMemoryTaskStore(), ) server = A2AStarletteApplication( - agent_card=agent_card, http_handler=request_handler + agent_card=ui_agent.get_agent_card(), http_handler=request_handler ) import uvicorn diff --git a/samples/agent/adk/contact_lookup/a2ui_examples.py b/samples/agent/adk/contact_lookup/a2ui_examples.py deleted file mode 100644 index dcf74210a..000000000 --- a/samples/agent/adk/contact_lookup/a2ui_examples.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright 2025 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. - -# a2ui_examples.py - -CONTACT_UI_EXAMPLES = """ ----BEGIN CONTACT_LIST_EXAMPLE--- -[ - { "beginRendering": { "surfaceId": "contact-list", "root": "root-column", "styles": { "primaryColor": "#007BFF", "font": "Roboto" } } }, - { "surfaceUpdate": { - "surfaceId": "contact-list", - "components": [ - { "id": "root-column", "component": { "Column": { "children": { "explicitList": ["title-heading", "item-list"] } } } }, - { "id": "title-heading", "component": { "Text": { "usageHint": "h1", "text": { "literalString": "Found Contacts" } } } }, - { "id": "item-list", "component": { "List": { "direction": "vertical", "children": { "template": { "componentId": "item-card-template", "dataBinding": "/contacts" } } } } }, - { "id": "item-card-template", "component": { "Card": { "child": "card-layout" } } }, - { "id": "card-layout", "component": { "Row": { "children": { "explicitList": ["template-image", "card-details", "view-button"] }, "alignment": "center" } } }, - { "id": "template-image", "component": { "Image": { "url": { "path": "imageUrl" }, "fit": "cover" } } }, - { "id": "card-details", "component": { "Column": { "children": { "explicitList": ["template-name", "template-title"] } } } }, - { "id": "template-name", "component": { "Text": { "usageHint": "h3", "text": { "path": "name" } } } }, - { "id": "template-title", "component": { "Text": { "text": { "path": "title" } } } }, - { "id": "view-button-text", "component": { "Text": { "text": { "literalString": "View" } } } }, - { "id": "view-button", "component": { "Button": { "child": "view-button-text", "primary": true, "action": { "name": "view_profile", "context": [ { "key": "contactName", "value": { "path": "name" } }, { "key": "department", "value": { "path": "department" } } ] } } } } - ] - } }, - { "dataModelUpdate": { - "surfaceId": "contact-list", - "path": "/", - "contents": [ - {{ "key": "contacts", "valueMap": [ - {{ "key": "contact1", "valueMap": [ - {{ "key": "name", "valueString": "Alice Wonderland" }}, - {{ "key": "phone", "valueString": "+1-555-123-4567" }}, - {{ "key": "email", "valueString": "alice@example.com" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/alice.jpg" }}, - {{ "key": "title", "valueString": "Mad Hatter" }}, - {{ "key": "department", "valueString": "Wonderland" }} - ] }}, - {{ "key": "contact2", "valueMap": [ - {{ "key": "name", "valueString": "Bob The Builder" }}, - {{ "key": "phone", "valueString": "+1-555-765-4321" }}, - {{ "key": "email", "valueString": "bob@example.com" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/bob.jpg" }}, - {{ "key": "title", "valueString": "Construction" }}, - {{ "key": "department", "valueString": "Building" }} - ] }} - ] }} - ] - } } -] ----END CONTACT_LIST_EXAMPLE--- - ----BEGIN CONTACT_CARD_EXAMPLE--- - -[ - { "beginRendering": { "surfaceId":"contact-card","root":"main_card"} }, - { "surfaceUpdate": { "surfaceId":"contact-card", - "components":[ - { "id": "profile_image", "component": { "Image": { "url": { "path": "imageUrl"}, "usageHint": "avatar", "fit": "cover" } } } , - { "id": "user_heading", "weight": 1, "component": { "Text": { "text": { "path": "name"} , "usageHint": "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": { "explicitList": ["user_heading", "description_text_1", "description_text_2"]} , "alignment": "center"} } } , - { "id": "calendar_icon", "component": { "Icon": { "name": { "literalString": "calendar_today"} } } } , - { "id": "calendar_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "calendar"} } } } , - { "id": "calendar_secondary_text", "component": { "Text": { "text": { "literalString": "Calendar"} } } } , - { "id": "calendar_text_column", "component": { "Column": { "children": { "explicitList": ["calendar_primary_text", "calendar_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "info_row_1", "component": { "Row": { "children": { "explicitList": ["calendar_icon", "calendar_text_column"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "location_icon", "component": { "Icon": { "name": { "literalString": "location_on"} } } } , - { "id": "location_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "location"} } } } , - { "id": "location_secondary_text", "component": { "Text": { "text": { "literalString": "Location"} } } } , - { "id": "location_text_column", "component": { "Column": { "children": { "explicitList": ["location_primary_text", "location_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "info_row_2", "component": { "Row": { "children": { "explicitList": ["location_icon", "location_text_column"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "mail_icon", "component": { "Icon": { "name": { "literalString": "mail"} } } } , - { "id": "mail_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "email"} } } } , - { "id": "mail_secondary_text", "component": { "Text": { "text": { "literalString": "Email"} } } } , - { "id": "mail_text_column", "component": { "Column": { "children": { "explicitList": ["mail_primary_text", "mail_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "info_row_3", "component": { "Row": { "children": { "explicitList": ["mail_icon", "mail_text_column"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "div", "component": { "Divider": { } } } , { "id": "call_icon", "component": { "Icon": { "name": { "literalString": "call"} } } } , - { "id": "call_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "mobile"} } } } , - { "id": "call_secondary_text", "component": { "Text": { "text": { "literalString": "Mobile"} } } } , - { "id": "call_text_column", "component": { "Column": { "children": { "explicitList": ["call_primary_text", "call_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "info_row_4", "component": { "Row": { "children": { "explicitList": ["call_icon", "call_text_column"]} , "distribution": "start", "alignment": "start"} } } , - { "id": "info_rows_column", "weight": 1, "component": { "Column": { "children": { "explicitList": ["info_row_1", "info_row_2", "info_row_3", "info_row_4"]} , "alignment": "stretch"} } } , - { "id": "button_1_text", "component": { "Text": { "text": { "literalString": "Follow"} } } } , { "id": "button_1", "component": { "Button": { "child": "button_1_text", "primary": true, "action": { "name": "follow_contact"} } } } , - { "id": "button_2_text", "component": { "Text": { "text": { "literalString": "Message"} } } } , { "id": "button_2", "component": { "Button": { "child": "button_2_text", "primary": false, "action": { "name": "send_message"} } } } , - { "id": "action_buttons_row", "component": { "Row": { "children": { "explicitList": ["button_1", "button_2"]} , "distribution": "center", "alignment": "center"} } } , - { "id": "link_text", "component": { "Text": { "text": { "literalString": "[View Full Profile](/profile)"} } } } , - { "id": "link_text_wrapper", "component": { "Row": { "children": { "explicitList": ["link_text"]} , "distribution": "center", "alignment": "center"} } } , - { "id": "main_column", "component": { "Column": { "children": { "explicitList": ["profile_image", "description_column", "div", "info_rows_column", "action_buttons_row", "link_text_wrapper"]} , "alignment": "stretch"} } } , - { "id": "main_card", "component": { "Card": { "child": "main_column"} } } - ] - } }, - { "dataModelUpdate": { - "surfaceId": "contact-card", - "path": "/", - "contents": [ - { "key": "name", "valueString": "" }, - { "key": "title", "valueString": "" }, - { "key": "team", "valueString": "" }, - { "key": "location", "valueString": "" }, - { "key": "email", "valueString": "" }, - { "key": "mobile", "valueString": "" }, - { "key": "calendar", "valueString": "" }, - { "key": "imageUrl", "valueString": "" } - ] - } } -] ----END CONTACT_CARD_EXAMPLE--- - ----BEGIN ACTION_CONFIRMATION_EXAMPLE--- -[ - { "beginRendering": { "surfaceId": "action-modal", "root": "modal-wrapper", "styles": { "primaryColor": "#007BFF", "font": "Roboto" } } }, - { "surfaceUpdate": { - "surfaceId": "action-modal", - "components": [ - { "id": "modal-wrapper", "component": { "Modal": { "entryPointChild": "hidden-entry-point", "contentChild": "modal-content-column" } } }, - { "id": "hidden-entry-point", "component": { "Text": { "text": { "literalString": "" } } } }, - { "id": "modal-content-column", "component": { "Column": { "children": { "explicitList": ["modal-title", "modal-message", "dismiss-button"] }, "alignment": "center" } } }, - { "id": "modal-title", "component": { "Text": { "usageHint": "h2", "text": { "path": "actionTitle" } } } }, - { "id": "modal-message", "component": { "Text": { "text": { "path": "actionMessage" } } } }, - { "id": "dismiss-button-text", "component": { "Text": { "text": { "literalString": "Dismiss" } } } }, - { "id": "dismiss-button", "component": { "Button": { "child": "dismiss-button-text", "primary": true, "action": { "name": "dismiss_modal" } } } } - ] - } }, - { "dataModelUpdate": { - "surfaceId": "action-modal", - "path": "/", - "contents": [ - { "key": "actionTitle", "valueString": "Action Confirmation" }, - { "key": "actionMessage", "valueString": "Your action has been processed." } - ] - } } -] ----END ACTION_CONFIRMATION_EXAMPLE--- - ----BEGIN FOLLOW_SUCCESS_EXAMPLE--- -[ - { "beginRendering": { "surfaceId": "contact-card", "root": "success_card"} }, - { "surfaceUpdate": { - "surfaceId": "contact-card", - "components": [ - { "id": "success_icon", "component": { "Icon": { "name": { "literalString": "check_circle"}, "size": 48.0, "color": "#4CAF50"} } } , - { "id": "success_text", "component": { "Text": { "text": { "literalString": "Successfully Followed"}, "usageHint": "h2"} } } , - { "id": "success_column", "component": { "Column": { "children": { "explicitList": ["success_icon", "success_text"]} , "alignment": "center"} } } , - { "id": "success_card", "component": { "Card": { "child": "success_column"} } } - ] - } } -] ----END FOLLOW_SUCCESS_EXAMPLE--- -""" diff --git a/samples/agent/adk/contact_lookup/a2ui_schema.py b/samples/agent/adk/contact_lookup/a2ui_schema.py deleted file mode 100644 index 52ee845e7..000000000 --- a/samples/agent/adk/contact_lookup/a2ui_schema.py +++ /dev/null @@ -1,788 +0,0 @@ -# Copyright 2025 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. - - -# a2ui_schema.py - -A2UI_SCHEMA = r""" -{ - "title": "A2UI Message Schema", - "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.", - "type": "object", - "properties": { - "beginRendering": { - "type": "object", - "description": "Signals the client to begin rendering a surface with a root component and specific styles.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be rendered." - }, - "root": { - "type": "string", - "description": "The ID of the root component to render." - }, - "styles": { - "type": "object", - "description": "Styling information for the UI.", - "properties": { - "font": { - "type": "string", - "description": "The primary font for the UI." - }, - "primaryColor": { - "type": "string", - "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", - "pattern": "^#[0-9a-fA-F]{6}$" - } - } - } - }, - "required": ["root", "surfaceId"] - }, - "surfaceUpdate": { - "type": "object", - "description": "Updates a surface with a new set of components.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown." - }, - "components": { - "type": "array", - "description": "A list containing all UI components for the surface.", - "minItems": 1, - "items": { - "type": "object", - "description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.", - "properties": { - "id": { - "type": "string", - "description": "The unique identifier for this component." - }, - "weight": { - "type": "number", - "description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." - }, - "component": { - "type": "object", - "description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.", - "properties": { - "Text": { - "type": "object", - "properties": { - "text": { - "type": "object", - "description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). 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.", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "usageHint": { - "type": "string", - "description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - } - }, - "required": ["text"] - }, - "Image": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "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", - "scale-down" - ] - }, - "usageHint": { - "type": "string", - "description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.", - "enum": [ - "icon", - "avatar", - "smallFeature", - "mediumFeature", - "largeFeature", - "header" - ] - } - }, - "required": ["url"] - }, - "Icon": { - "type": "object", - "properties": { - "name": { - "type": "object", - "description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').", - "properties": { - "literalString": { - "type": "string", - "enum": [ - "accountCircle", - "add", - "arrowBack", - "arrowForward", - "attachFile", - "calendarToday", - "call", - "camera", - "check", - "close", - "delete", - "download", - "edit", - "event", - "error", - "favorite", - "favoriteOff", - "folder", - "help", - "home", - "info", - "locationOn", - "lock", - "lockOpen", - "mail", - "menu", - "moreVert", - "moreHoriz", - "notificationsOff", - "notifications", - "payment", - "person", - "phone", - "photo", - "print", - "refresh", - "search", - "send", - "settings", - "share", - "shoppingCart", - "star", - "starHalf", - "starOff", - "upload", - "visibility", - "visibilityOff", - "warning" - ] - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["name"] - }, - "Video": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "AudioPlayer": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "description": { - "type": "object", - "description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "Row": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "center", - "end", - "spaceAround", - "spaceBetween", - "spaceEvenly", - "start" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Column": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "start", - "center", - "end", - "spaceBetween", - "spaceAround", - "spaceEvenly" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.", - "enum": ["center", "end", "start", "stretch"] - } - }, - "required": ["children"] - }, - "List": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "direction": { - "type": "string", - "description": "The direction in which the list items are laid out.", - "enum": ["vertical", "horizontal"] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Card": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to be rendered inside the card." - } - }, - "required": ["child"] - }, - "Tabs": { - "type": "object", - "properties": { - "tabItems": { - "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": { - "type": "object", - "description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "child": { - "type": "string" - } - }, - "required": ["title", "child"] - } - } - }, - "required": ["tabItems"] - }, - "Divider": { - "type": "object", - "properties": { - "axis": { - "type": "string", - "description": "The orientation of the divider.", - "enum": ["horizontal", "vertical"] - } - } - }, - "Modal": { - "type": "object", - "properties": { - "entryPointChild": { - "type": "string", - "description": "The ID of the component that opens the modal when interacted with (e.g., a button)." - }, - "contentChild": { - "type": "string", - "description": "The ID of the component to be displayed inside the modal." - } - }, - "required": ["entryPointChild", "contentChild"] - }, - "Button": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to display in the button, typically a Text component." - }, - "primary": { - "type": "boolean", - "description": "Indicates if this button should be styled as the primary action." - }, - "action": { - "type": "object", - "description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.", - "properties": { - "name": { - "type": "string" - }, - "context": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "object", - "description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').", - "properties": { - "path": { - "type": "string" - }, - "literalString": { - "type": "string" - }, - "literalNumber": { - "type": "number" - }, - "literalBoolean": { - "type": "boolean" - } - } - } - }, - "required": ["key", "value"] - } - } - }, - "required": ["name"] - } - }, - "required": ["child", "action"] - }, - "CheckBox": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "object", - "description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').", - "properties": { - "literalBoolean": { - "type": "boolean" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["label", "value"] - }, - "TextField": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "text": { - "type": "object", - "description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "textFieldType": { - "type": "string", - "description": "The type of input field to display.", - "enum": [ - "date", - "longText", - "number", - "shortText", - "obscured" - ] - }, - "validationRegexp": { - "type": "string", - "description": "A regular expression used for client-side validation of the input." - } - }, - "required": ["label"] - }, - "DateTimeInput": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The selected date and/or time value in ISO 8601 format. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "enableDate": { - "type": "boolean", - "description": "If true, allows the user to select a date." - }, - "enableTime": { - "type": "boolean", - "description": "If true, allows the user to select a time." - } - }, - "required": ["value"] - }, - "MultipleChoice": { - "type": "object", - "properties": { - "selections": { - "type": "object", - "description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').", - "properties": { - "literalArray": { - "type": "array", - "items": { - "type": "string" - } - }, - "path": { - "type": "string" - } - } - }, - "options": { - "type": "array", - "description": "An array of available options for the user to choose from.", - "items": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "string", - "description": "The value to be associated with this option when selected." - } - }, - "required": ["label", "value"] - } - }, - "maxAllowedSelections": { - "type": "integer", - "description": "The maximum number of options that the user is allowed to select." - } - }, - "required": ["selections", "options"] - }, - "Slider": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').", - "properties": { - "literalNumber": { - "type": "number" - }, - "path": { - "type": "string" - } - } - }, - "minValue": { - "type": "number", - "description": "The minimum value of the slider." - }, - "maxValue": { - "type": "number", - "description": "The maximum value of the slider." - } - }, - "required": ["value"] - } - } - } - }, - "required": ["id", "component"] - } - } - }, - "required": ["surfaceId", "components"] - }, - "dataModelUpdate": { - "type": "object", - "description": "Updates the data model for a surface.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface this data model update applies to." - }, - "path": { - "type": "string", - "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced." - }, - "contents": { - "type": "array", - "description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.", - "items": { - "type": "object", - "description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.", - "properties": { - "key": { - "type": "string", - "description": "The key for this data entry." - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - }, - "valueMap": { - "description": "Represents a map as an adjacency list.", - "type": "array", - "items": { - "type": "object", - "description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.", - "properties": { - "key": { - "type": "string" - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - } - }, - "required": ["key"] - } - } - }, - "required": ["key"] - } - } - }, - "required": ["contents", "surfaceId"] - }, - "deleteSurface": { - "type": "object", - "description": "Signals the client to delete the surface identified by 'surfaceId'.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be deleted." - } - }, - "required": ["surfaceId"] - } - } -} -""" diff --git a/samples/agent/adk/contact_lookup/agent.py b/samples/agent/adk/contact_lookup/agent.py index 9a09aaad0..0bde787c1 100644 --- a/samples/agent/adk/contact_lookup/agent.py +++ b/samples/agent/adk/contact_lookup/agent.py @@ -19,22 +19,20 @@ from typing import Any import jsonschema -from a2ui_examples import CONTACT_UI_EXAMPLES # Corrected imports from our new/refactored files -from a2ui_schema import A2UI_SCHEMA from google.adk.agents.llm_agent import LlmAgent from google.adk.artifacts import InMemoryArtifactService from google.adk.memory.in_memory_memory_service import InMemoryMemoryService from google.adk.models.lite_llm import LiteLlm from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService +from a2a.types import AgentCapabilities, AgentCard, AgentSkill + from google.genai import types -from prompt_builder import ( - get_text_prompt, - get_ui_prompt, -) +from prompt_builder import get_text_prompt, ROLE_DESCRIPTION, WORKFLOW_DESCRIPTION, UI_DESCRIPTION from tools import get_contact_info +from a2ui.inference.schema.manager import A2uiSchemaManager logger = logging.getLogger(__name__) @@ -47,6 +45,9 @@ class ContactAgent: def __init__(self, base_url: str, use_ui: bool = False): self.base_url = base_url self.use_ui = use_ui + self._schema_manager = ( + A2uiSchemaManager("0.8", basic_examples_path="examples") if use_ui else None + ) self._agent = self._build_agent(use_ui) self._user_id = "remote_agent" self._runner = Runner( @@ -57,20 +58,37 @@ def __init__(self, base_url: str, use_ui: bool = False): memory_service=InMemoryMemoryService(), ) - # --- MODIFICATION: Wrap the schema --- - # Load the A2UI_SCHEMA string into a Python object for validation - try: - # First, load the schema for a *single message* - single_message_schema = json.loads(A2UI_SCHEMA) - - # The prompt instructs the LLM to return a *list* of messages. - # Therefore, our validation schema must be an *array* of the single message schema. - self.a2ui_schema_object = {"type": "array", "items": single_message_schema} - logger.info("A2UI_SCHEMA successfully loaded and wrapped in an array validator.") - except json.JSONDecodeError as e: - logger.error(f"CRITICAL: Failed to parse A2UI_SCHEMA: {e}") - self.a2ui_schema_object = None - # --- END MODIFICATION --- + def get_agent_card(self) -> AgentCard: + capabilities = AgentCapabilities( + streaming=True, + extensions=[self._schema_manager.get_agent_extension()], + ) + skill = AgentSkill( + id="find_contact", + name="Find Contact Tool", + description=( + "Helps find contact information for colleagues (e.g., email, location," + " team)." + ), + tags=["contact", "directory", "people", "finder"], + examples=[ + "Who is David Chen in marketing?", + "Find Sarah Lee from engineering", + ], + ) + + return AgentCard( + name="Contact Lookup Agent", + description=( + "This agent helps find contact info for people in your organization." + ), + url=self.base_url, + version="1.0.0", + default_input_modes=ContactAgent.SUPPORTED_CONTENT_TYPES, + default_output_modes=ContactAgent.SUPPORTED_CONTENT_TYPES, + capabilities=capabilities, + skills=[skill], + ) def get_processing_message(self) -> str: return "Looking up contact information..." @@ -79,11 +97,18 @@ def _build_agent(self, use_ui: bool) -> LlmAgent: """Builds the LLM agent for the contact agent.""" LITELLM_MODEL = os.getenv("LITELLM_MODEL", "gemini/gemini-2.5-flash") - if use_ui: - instruction = get_ui_prompt(self.base_url, CONTACT_UI_EXAMPLES) - else: - # The text prompt function also returns a complete prompt. - instruction = get_text_prompt() + instruction = ( + self._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=False, # Use invalid examples to test retry logic + ) + if use_ui + else get_text_prompt() + ) return LlmAgent( model=LiteLlm(model=LITELLM_MODEL), @@ -116,8 +141,9 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]: attempt = 0 current_query_text = query - # Ensure schema was loaded - if self.use_ui and self.a2ui_schema_object is None: + # Ensure catalog schema was loaded + effective_catalog = self._schema_manager.get_effective_catalog() + if self.use_ui and not effective_catalog.catalog_schema: logger.error( "--- ContactAgent.stream: A2UI_SCHEMA is not loaded. " "Cannot perform UI validation. ---" @@ -219,9 +245,7 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]: logger.info( "--- ContactAgent.stream: Validating against A2UI_SCHEMA... ---" ) - jsonschema.validate( - instance=parsed_json_data, schema=self.a2ui_schema_object - ) + effective_catalog.validator.validate(parsed_json_data) # --- End New Validation Steps --- logger.info( diff --git a/samples/agent/adk/contact_lookup/agent_executor.py b/samples/agent/adk/contact_lookup/agent_executor.py index c58bbce18..1c60551a1 100644 --- a/samples/agent/adk/contact_lookup/agent_executor.py +++ b/samples/agent/adk/contact_lookup/agent_executor.py @@ -41,11 +41,11 @@ class ContactAgentExecutor(AgentExecutor): """Contact AgentExecutor Example.""" - def __init__(self, base_url: str): + def __init__(self, ui_agent: ContactAgent, text_agent: ContactAgent): # Instantiate two agents: one for UI and one for text-only. # The appropriate one will be chosen at execution time. - self.ui_agent = ContactAgent(base_url=base_url, use_ui=True) - self.text_agent = ContactAgent(base_url=base_url, use_ui=False) + self.ui_agent = ui_agent + self.text_agent = text_agent async def execute( self, diff --git a/samples/agent/adk/contact_lookup/examples/action_confirmation.json b/samples/agent/adk/contact_lookup/examples/action_confirmation.json new file mode 100644 index 000000000..f961363d7 --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/action_confirmation.json @@ -0,0 +1,23 @@ +[ + { "beginRendering": { "surfaceId": "action-modal", "root": "modal-wrapper", "styles": { "primaryColor": "#007BFF", "font": "Roboto" } } }, + { "surfaceUpdate": { + "surfaceId": "action-modal", + "components": [ + { "id": "modal-wrapper", "component": { "Modal": { "entryPointChild": "hidden-entry-point", "contentChild": "modal-content-column" } } }, + { "id": "hidden-entry-point", "component": { "Text": { "text": { "literalString": "" } } } }, + { "id": "modal-content-column", "component": { "Column": { "children": { "explicitList": ["modal-title", "modal-message", "dismiss-button"] }, "alignment": "center" } } }, + { "id": "modal-title", "component": { "Text": { "usageHint": "h2", "text": { "path": "actionTitle" } } } }, + { "id": "modal-message", "component": { "Text": { "text": { "path": "actionMessage" } } } }, + { "id": "dismiss-button-text", "component": { "Text": { "text": { "literalString": "Dismiss" } } } }, + { "id": "dismiss-button", "component": { "Button": { "child": "dismiss-button-text", "primary": true, "action": { "name": "dismiss_modal" } } } } + ] + } }, + { "dataModelUpdate": { + "surfaceId": "action-modal", + "path": "/", + "contents": [ + { "key": "actionTitle", "valueString": "Action Confirmation" }, + { "key": "actionMessage", "valueString": "Your action has been processed." } + ] + } } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_lookup/examples/contact_card.json b/samples/agent/adk/contact_lookup/examples/contact_card.json new file mode 100644 index 000000000..70f0787b3 --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/contact_card.json @@ -0,0 +1,54 @@ +[ + { "beginRendering": { "surfaceId":"contact-card","root":"main_card"} }, + { "surfaceUpdate": { "surfaceId":"contact-card", + "components":[ + { "id": "profile_image", "component": { "Image": { "url": { "path": "imageUrl"}, "usageHint": "avatar", "fit": "cover" } } } , + { "id": "user_heading", "weight": 1, "component": { "Text": { "text": { "path": "name"} , "usageHint": "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": { "explicitList": ["user_heading", "description_text_1", "description_text_2"]} , "alignment": "center"} } } , + { "id": "calendar_icon", "component": { "Icon": { "name": { "literalString": "calendar_today"} } } } , + { "id": "calendar_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "calendar"} } } } , + { "id": "calendar_secondary_text", "component": { "Text": { "text": { "literalString": "Calendar"} } } } , + { "id": "calendar_text_column", "component": { "Column": { "children": { "explicitList": ["calendar_primary_text", "calendar_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "info_row_1", "component": { "Row": { "children": { "explicitList": ["calendar_icon", "calendar_text_column"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "location_icon", "component": { "Icon": { "name": { "literalString": "location_on"} } } } , + { "id": "location_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "location"} } } } , + { "id": "location_secondary_text", "component": { "Text": { "text": { "literalString": "Location"} } } } , + { "id": "location_text_column", "component": { "Column": { "children": { "explicitList": ["location_primary_text", "location_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "info_row_2", "component": { "Row": { "children": { "explicitList": ["location_icon", "location_text_column"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "mail_icon", "component": { "Icon": { "name": { "literalString": "mail"} } } } , + { "id": "mail_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "email"} } } } , + { "id": "mail_secondary_text", "component": { "Text": { "text": { "literalString": "Email"} } } } , + { "id": "mail_text_column", "component": { "Column": { "children": { "explicitList": ["mail_primary_text", "mail_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "info_row_3", "component": { "Row": { "children": { "explicitList": ["mail_icon", "mail_text_column"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "div", "component": { "Divider": { } } } , { "id": "call_icon", "component": { "Icon": { "name": { "literalString": "call"} } } } , + { "id": "call_primary_text", "component": { "Text": { "usageHint": "h5", "text": { "path": "mobile"} } } } , + { "id": "call_secondary_text", "component": { "Text": { "text": { "literalString": "Mobile"} } } } , + { "id": "call_text_column", "component": { "Column": { "children": { "explicitList": ["call_primary_text", "call_secondary_text"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "info_row_4", "component": { "Row": { "children": { "explicitList": ["call_icon", "call_text_column"]} , "distribution": "start", "alignment": "start"} } } , + { "id": "info_rows_column", "weight": 1, "component": { "Column": { "children": { "explicitList": ["info_row_1", "info_row_2", "info_row_3", "info_row_4"]} , "alignment": "stretch"} } } , + { "id": "button_1_text", "component": { "Text": { "text": { "literalString": "Follow"} } } } , { "id": "button_1", "component": { "Button": { "child": "button_1_text", "primary": true, "action": { "name": "follow_contact"} } } } , + { "id": "button_2_text", "component": { "Text": { "text": { "literalString": "Message"} } } } , { "id": "button_2", "component": { "Button": { "child": "button_2_text", "primary": false, "action": { "name": "send_message"} } } } , + { "id": "action_buttons_row", "component": { "Row": { "children": { "explicitList": ["button_1", "button_2"]} , "distribution": "center", "alignment": "center"} } } , + { "id": "link_text", "component": { "Text": { "text": { "literalString": "[View Full Profile](/profile)"} } } } , + { "id": "link_text_wrapper", "component": { "Row": { "children": { "explicitList": ["link_text"]} , "distribution": "center", "alignment": "center"} } } , + { "id": "main_column", "component": { "Column": { "children": { "explicitList": ["profile_image", "description_column", "div", "info_rows_column", "action_buttons_row", "link_text_wrapper"]} , "alignment": "stretch"} } } , + { "id": "main_card", "component": { "Card": { "child": "main_column"} } } + ] + } }, + { "dataModelUpdate": { + "surfaceId": "contact-card", + "path": "/", + "contents": [ + { "key": "name", "valueString": "" }, + { "key": "title", "valueString": "" }, + { "key": "team", "valueString": "" }, + { "key": "location", "valueString": "" }, + { "key": "email", "valueString": "" }, + { "key": "mobile", "valueString": "" }, + { "key": "calendar", "valueString": "" }, + { "key": "imageUrl", "valueString": "" } + ] + } } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_lookup/examples/contact_list.json b/samples/agent/adk/contact_lookup/examples/contact_list.json new file mode 100644 index 000000000..4f87d069b --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/contact_list.json @@ -0,0 +1,232 @@ +[ + { + "beginRendering": { + "surfaceId": "contact-list", + "root": "root-column", + "styles": { + "primaryColor": "#007BFF", + "font": "Roboto" + } + } + }, + { + "surfaceUpdate": { + "surfaceId": "contact-list", + "components": [ + { + "id": "root-column", + "component": { + "Column": { + "children": { + "explicitList": [ + "title-heading", + "item-list" + ] + } + } + } + }, + { + "id": "title-heading", + "component": { + "Text": { + "usageHint": "h1", + "text": { + "literalString": "Found Contacts" + } + } + } + }, + { + "id": "item-list", + "component": { + "List": { + "direction": "vertical", + "children": { + "template": { + "componentId": "item-card-template", + "dataBinding": "/contacts" + } + } + } + } + }, + { + "id": "item-card-template", + "component": { + "Card": { + "child": "card-layout" + } + } + }, + { + "id": "card-layout", + "component": { + "Row": { + "children": { + "explicitList": [ + "template-image", + "card-details", + "view-button" + ] + }, + "alignment": "center" + } + } + }, + { + "id": "template-image", + "component": { + "Image": { + "url": { + "path": "imageUrl" + }, + "fit": "cover" + } + } + }, + { + "id": "card-details", + "component": { + "Column": { + "children": { + "explicitList": [ + "template-name", + "template-title" + ] + } + } + } + }, + { + "id": "template-name", + "component": { + "Text": { + "usageHint": "h3", + "text": { + "path": "name" + } + } + } + }, + { + "id": "template-title", + "component": { + "Text": { + "text": { + "path": "title" + } + } + } + }, + { + "id": "view-button-text", + "component": { + "Text": { + "text": { + "literalString": "View" + } + } + } + }, + { + "id": "view-button", + "component": { + "Button": { + "child": "view-button-text", + "primary": true, + "action": { + "name": "view_profile", + "context": [ + { + "key": "contactName", + "value": { + "path": "name" + } + }, + { + "key": "department", + "value": { + "path": "department" + } + } + ] + } + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "contact-list", + "path": "/", + "contents": [ + { + "key": "contacts", + "valueMap": [ + { + "key": "contact1", + "valueMap": [ + { + "key": "name", + "valueString": "Alice Wonderland" + }, + { + "key": "phone", + "valueString": "+1-555-123-4567" + }, + { + "key": "email", + "valueString": "alice@example.com" + }, + { + "key": "imageUrl", + "valueString": "https://example.com/alice.jpg" + }, + { + "key": "title", + "valueString": "Mad Hatter" + }, + { + "key": "department", + "valueString": "Wonderland" + } + ] + }, + { + "key": "contact2", + "valueMap": [ + { + "key": "name", + "valueString": "Bob The Builder" + }, + { + "key": "phone", + "valueString": "+1-555-765-4321" + }, + { + "key": "email", + "valueString": "bob@example.com" + }, + { + "key": "imageUrl", + "valueString": "https://example.com/bob.jpg" + }, + { + "key": "title", + "valueString": "Construction" + }, + { + "key": "department", + "valueString": "Building" + } + ] + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/samples/agent/adk/contact_lookup/examples/follow_success.json b/samples/agent/adk/contact_lookup/examples/follow_success.json new file mode 100644 index 000000000..4822dbed4 --- /dev/null +++ b/samples/agent/adk/contact_lookup/examples/follow_success.json @@ -0,0 +1,60 @@ +[ + { + "beginRendering": { + "surfaceId": "contact-card", + "root": "success_card" + } + }, + { + "surfaceUpdate": { + "surfaceId": "contact-card", + "components": [ + { + "id": "success_icon", + "component": { + "Icon": { + "name": { + "literalString": "check_circle" + }, + "size": 48.0, + "color": "#4CAF50" + } + } + }, + { + "id": "success_text", + "component": { + "Text": { + "text": { + "literalString": "Successfully Followed" + }, + "usageHint": "h2" + } + } + }, + { + "id": "success_column", + "component": { + "Column": { + "children": { + "explicitList": [ + "success_icon", + "success_text" + ] + }, + "alignment": "center" + } + } + }, + { + "id": "success_card", + "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 af472be82..d50dfc2d1 100644 --- a/samples/agent/adk/contact_lookup/prompt_builder.py +++ b/samples/agent/adk/contact_lookup/prompt_builder.py @@ -12,81 +12,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -from a2ui_examples import CONTACT_UI_EXAMPLES -from a2ui_schema import A2UI_SCHEMA - -# This is the agent's master instruction, separate from the UI prompt formatting. -AGENT_INSTRUCTION = """ - You are a helpful contact lookup assistant. Your goal is to help users find colleagues using a rich UI. - - To achieve this, you MUST follow this logic: - - 1. **For finding contacts (e.g., "Who is Alex Jordan?"):** - a. You MUST call the `get_contact_info` tool. Extract the name and department. - b. After receiving the data: - i. If the tool returns a **single contact**, you MUST use the `CONTACT_CARD_EXAMPLE` template. - ii. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. - iii. 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_JSON---[]" - - 2. **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** - a. You MUST call the `get_contact_info` tool with the specific name. - b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template. - - 3. **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. +from a2ui.inference.schema.manager import A2uiSchemaManager + +ROLE_DESCRIPTION = ( + "You are a helpful contact lookup assistant. Your final output MUST be an A2UI JSON" + " response." +) + +WORKFLOW_DESCRIPTION = """ +To generate the response, you MUST follow these rules: +1. Your response MUST be in two parts, separated by the delimiter: `---a2ui_JSON---`. +2. The first part is your conversational text response (e.g., "Here is the contact you requested..."). +3. The second part is a single, raw JSON object which is a list of A2UI messages. +4. The JSON part MUST validate against the A2UI JSON SCHEMA provided below. +5. Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` attribute. """ - -def get_ui_prompt(base_url: str, examples: str) -> str: - """ - Constructs the full prompt with UI instructions, rules, examples, and schema. - - Args: - base_url: The base URL for resolving static assets like logos. - examples: A string containing the specific UI examples for the agent's task. - - Returns: - A formatted string to be used as the system prompt for the LLM. - """ - - # --- THIS IS THE FIX --- - # We no longer call .format() on the examples, as it breaks the JSON. - formatted_examples = examples - # --- END FIX --- - - return f""" - You are a helpful contact lookup assistant. Your final output MUST be a a2ui UI JSON response. - - To generate the response, you MUST follow these rules: - 1. Your response MUST be in two parts, separated by the delimiter: `---a2ui_JSON---`. - 2. The first part is your conversational text response (e.g., "Here is the contact you requested..."). - 3. The second part is a single, raw JSON object which is a list of A2UI messages. - 4. The JSON part MUST validate against the A2UI JSON SCHEMA provided below. - 5. Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` attribute. - - --- UI TEMPLATE RULES --- - - **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. - 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_JSON---[]" - - - **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** - a. You MUST call the `get_contact_info` tool with the specific name. - b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template. - - - **For handling actions (e.g., "follow_contact"):** - a. You MUST use the `FOLLOW_SUCCESS_EXAMPLE` template. - b. This will render a new card with a "Successfully Followed" message. - c. Respond with a text confirmation like "You are now following this contact." along with the JSON. - - {formatted_examples} - - ---BEGIN A2UI JSON SCHEMA--- - {A2UI_SCHEMA} - ---END A2UI JSON SCHEMA--- - """ +UI_DESCRIPTION = """ +- **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. + 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_JSON---[]" + +- **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** + a. You MUST call the `get_contact_info` tool with the specific name. + b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template. + +- **For handling actions (e.g., "follow_contact"):** + a. You MUST use the `FOLLOW_SUCCESS_EXAMPLE` template. + b. This will render a new card with a "Successfully Followed" message. + c. Respond with a text confirmation like "You are now following this contact." along with the JSON. +""" def get_text_prompt() -> str: @@ -109,9 +66,17 @@ def get_text_prompt() -> str: if __name__ == "__main__": - # Example of how to use the prompt builder - my_base_url = "http://localhost:8000" - contact_prompt = get_ui_prompt(my_base_url, CONTACT_UI_EXAMPLES) + # Example of how to use the A2UI Schema Manager to generate a system prompt + contact_prompt = A2uiSchemaManager( + "0.8", basic_examples_path="examples" + ).generate_system_prompt( + role_description=ROLE_DESCRIPTION, + workflow_description=WORKFLOW_DESCRIPTION, + ui_description=UI_DESCRIPTION, + include_schema=True, + include_examples=True, + validate_examples=False, + ) print(contact_prompt) with open("generated_prompt.txt", "w") as f: f.write(contact_prompt) diff --git a/samples/agent/adk/uv.lock b/samples/agent/adk/uv.lock index dfc582e88..0fb42d7e9 100644 --- a/samples/agent/adk/uv.lock +++ b/samples/agent/adk/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.14'",