diff --git a/samples/agent/adk/restaurant_finder/__main__.py b/samples/agent/adk/restaurant_finder/__main__.py index ee4e9115b..f816cf6d8 100644 --- a/samples/agent/adk/restaurant_finder/__main__.py +++ b/samples/agent/adk/restaurant_finder/__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 RestaurantAgent from agent_executor import RestaurantAgentExecutor from dotenv import load_dotenv @@ -50,41 +48,19 @@ def main(host, port): " is not TRUE." ) - capabilities = AgentCapabilities( - streaming=True, - extensions=[get_a2ui_agent_extension()], - ) - skill = AgentSkill( - id="find_restaurants", - name="Find Restaurants Tool", - description=( - "Helps find restaurants based on user criteria (e.g., cuisine, location)." - ), - tags=["restaurant", "finder"], - examples=["Find me the top 10 chinese restaurants in the US"], - ) - base_url = f"http://{host}:{port}" - agent_card = AgentCard( - name="Restaurant Agent", - description="This agent helps find restaurants based on user criteria.", - url=base_url, # <-- Use base_url here - version="1.0.0", - default_input_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES, - default_output_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES, - capabilities=capabilities, - skills=[skill], - ) + ui_agent = RestaurantAgent(base_url=base_url, use_ui=True) + text_agent = RestaurantAgent(base_url=base_url, use_ui=False) - agent_executor = RestaurantAgentExecutor(base_url=base_url) + agent_executor = RestaurantAgentExecutor(ui_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/restaurant_finder/a2ui_examples.py b/samples/agent/adk/restaurant_finder/a2ui_examples.py deleted file mode 100644 index b77a685ea..000000000 --- a/samples/agent/adk/restaurant_finder/a2ui_examples.py +++ /dev/null @@ -1,186 +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. - -RESTAURANT_UI_EXAMPLES = """ ----BEGIN SINGLE_COLUMN_LIST_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "default", - "components": [ - {{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "item-list"] }} }} }} }}, - {{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "title" }} }} }} }}, - {{ "id": "item-list", "component": {{ "List": {{ "direction": "vertical", "children": {{ "template": {{ "componentId": "item-card-template", "dataBinding": "/items" }} }} }} }} }}, - {{ "id": "item-card-template", "component": {{ "Card": {{ "child": "card-layout" }} }} }}, - {{ "id": "card-layout", "component": {{ "Row": {{ "children": {{ "explicitList": ["template-image", "card-details"] }} }} }} }}, - {{ "id": "template-image", weight: 1, "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }} }} }} }}, - {{ "id": "card-details", weight: 2, "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name", "template-rating", "template-detail", "template-link", "template-book-button"] }} }} }} }}, - {{ "id": "template-name", "component": {{ "Text": {{ "usageHint": "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", "primary": true, "action": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "address" }} }} ] }} }} }} }}, - {{ "id": "book-now-text", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "default", - "path": "/", - "contents": [ - {{ "key": "items", "valueMap": [ - {{ "key": "item1", "valueMap": [ - {{ "key": "name", "valueString": "The Fancy Place" }}, - {{ "key": "rating", "valueNumber": 4.8 }}, - {{ "key": "detail", "valueString": "Fine dining experience" }}, - {{ "key": "infoLink", "valueString": "https://example.com/fancy" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }}, - {{ "key": "address", "valueString": "123 Main St" }} - ] }}, - {{ "key": "item2", "valueMap": [ - {{ "key": "name", "valueString": "Quick Bites" }}, - {{ "key": "rating", "valueNumber": 4.2 }}, - {{ "key": "detail", "valueString": "Casual and fast" }}, - {{ "key": "infoLink", "valueString": "https://example.com/quick" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }}, - {{ "key": "address", "valueString": "456 Oak Ave" }} - ] }} - ] }} // Populate this with restaurant data - ] - }} }} -] ----END SINGLE_COLUMN_LIST_EXAMPLE--- - ----BEGIN TWO_COLUMN_LIST_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "default", - "components": [ - {{ "id": "root-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["title-heading", "restaurant-row-1"] }} }} }} }}, - {{ "id": "title-heading", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "path": "title" }} }} }} }}, - {{ "id": "restaurant-row-1", "component": {{ "Row": {{ "children": {{ "explicitList": ["item-card-1", "item-card-2"] }} }} }} }}, - {{ "id": "item-card-1", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-1" }} }} }}, - {{ "id": "card-layout-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-1", "card-details-1"] }} }} }} }}, - {{ "id": "template-image-1", "component": {{ "Image": {{ "url": {{ "path": "/items/0/imageUrl" }}, "width": "100%" }} }} }}, - {{ "id": "card-details-1", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-1", "template-rating-1", "template-detail-1", "template-link-1", "template-book-button-1"] }} }} }} }}, - {{ "id": "template-name-1", "component": {{ "Text": {{ "usageHint": "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": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "/items/0/name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "/items/0/imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "/items/0/address" }} }} ] }} }} }} }}, - {{ "id": "book-now-text-1", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }}, - {{ "id": "item-card-2", "weight": 1, "component": {{ "Card": {{ "child": "card-layout-2" }} }} }}, - {{ "id": "card-layout-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-image-2", "card-details-2"] }} }} }} }}, - {{ "id": "template-image-2", "component": {{ "Image": {{ "url": {{ "path": "/items/1/imageUrl" }}, "width": "100%" }} }} }}, - {{ "id": "card-details-2", "component": {{ "Column": {{ "children": {{ "explicitList": ["template-name-2", "template-rating-2", "template-detail-2", "template-link-2", "template-book-button-2"] }} }} }} }}, - {{ "id": "template-name-2", "component": {{ "Text": {{ "usageHint": "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": {{ "name": "book_restaurant", "context": [ {{ "key": "restaurantName", "value": {{ "path": "/items/1/name" }} }}, {{ "key": "imageUrl", "value": {{ "path": "/items/1/imageUrl" }} }}, {{ "key": "address", "value": {{ "path": "/items/1/address" }} }} ] }} }} }} }}, - {{ "id": "book-now-text-2", "component": {{ "Text": {{ "text": {{ "literalString": "Book Now" }} }} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "default", - "path": "/", - "contents": [ - {{ "key": "title", "valueString": "Top Restaurants" }}, - {{ "key": "items", "valueMap": [ - {{ "key": "item1", "valueMap": [ - {{ "key": "name", "valueString": "The Fancy Place" }}, - {{ "key": "rating", "valueNumber": 4.8 }}, - {{ "key": "detail", "valueString": "Fine dining experience" }}, - {{ "key": "infoLink", "valueString": "https://example.com/fancy" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }}, - {{ "key": "address", "valueString": "123 Main St" }} - ] }}, - {{ "key": "item2", "valueMap": [ - {{ "key": "name", "valueString": "Quick Bites" }}, - {{ "key": "rating", "valueNumber": 4.2 }}, - {{ "key": "detail", "valueString": "Casual and fast" }}, - {{ "key": "infoLink", "valueString": "https://example.com/quick" }}, - {{ "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }}, - {{ "key": "address", "valueString": "456 Oak Ave" }} - ] }} - ] }} // Populate this with restaurant data - ] - }} }} -] ----END TWO_COLUMN_LIST_EXAMPLE--- - ----BEGIN BOOKING_FORM_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "booking-form", "root": "booking-form-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "booking-form", - "components": [ - {{ "id": "booking-form-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["booking-title", "restaurant-image", "restaurant-address", "party-size-field", "datetime-field", "dietary-field", "submit-button"] }} }} }} }}, - {{ "id": "booking-title", "component": {{ "Text": {{ "usageHint": "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": {{ "literalString": "Party Size" }}, "text": {{ "path": "partySize" }}, "type": "number" }} }} }}, - {{ "id": "datetime-field", "component": {{ "DateTimeInput": {{ "label": {{ "literalString": "Date & Time" }}, "value": {{ "path": "reservationTime" }}, "enableDate": true, "enableTime": true }} }} }}, - {{ "id": "dietary-field", "component": {{ "TextField": {{ "label": {{ "literalString": "Dietary Requirements" }}, "text": {{ "path": "dietary" }} }} }} }}, - {{ "id": "submit-button", "component": {{ "Button": {{ "child": "submit-reservation-text", "action": {{ "name": "submit_booking", "context": [ {{ "key": "restaurantName", "value": {{ "path": "restaurantName" }} }}, {{ "key": "partySize", "value": {{ "path": "partySize" }} }}, {{ "key": "reservationTime", "value": {{ "path": "reservationTime" }} }}, {{ "key": "dietary", "value": {{ "path": "dietary" }} }}, {{ "key": "imageUrl", "value": {{ "path": "imageUrl" }} }} ] }} }} }} }}, - {{ "id": "submit-reservation-text", "component": {{ "Text": {{ "text": {{ "literalString": "Submit Reservation" }} }} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "booking-form", - "path": "/", - "contents": [ - {{ "key": "title", "valueString": "Book a Table at [RestaurantName]" }}, - {{ "key": "address", "valueString": "[Restaurant Address]" }}, - {{ "key": "restaurantName", "valueString": "[RestaurantName]" }}, - {{ "key": "partySize", "valueString": "2" }}, - {{ "key": "reservationTime", "valueString": "" }}, - {{ "key": "dietary", "valueString": "" }}, - {{ "key": "imageUrl", "valueString": "" }} - ] - }} }} -] ----END BOOKING_FORM_EXAMPLE--- - ----BEGIN CONFIRMATION_EXAMPLE--- -[ - {{ "beginRendering": {{ "surfaceId": "confirmation", "root": "confirmation-card", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ - "surfaceId": "confirmation", - "components": [ - {{ "id": "confirmation-card", "component": {{ "Card": {{ "child": "confirmation-column" }} }} }}, - {{ "id": "confirmation-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["confirm-title", "confirm-image", "divider1", "confirm-details", "divider2", "confirm-dietary", "divider3", "confirm-text"] }} }} }} }}, - {{ "id": "confirm-title", "component": {{ "Text": {{ "usageHint": "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": {{ "usageHint": "h5", "text": {{ "literalString": "We look forward to seeing you!" }} }} }} }}, - {{ "id": "divider1", "component": {{ "Divider": {{}} }} }}, - {{ "id": "divider2", "component": {{ "Divider": {{}} }} }}, - {{ "id": "divider3", "component": {{ "Divider": {{}} }} }} - ] - }} }}, - {{ "dataModelUpdate": {{ - "surfaceId": "confirmation", - "path": "/", - "contents": [ - {{ "key": "title", "valueString": "Booking at [RestaurantName]" }}, - {{ "key": "bookingDetails", "valueString": "[PartySize] people at [Time]" }}, - {{ "key": "dietaryRequirements", "valueString": "Dietary Requirements: [Requirements]" }}, - {{ "key": "imageUrl", "valueString": "[ImageUrl]" }} - ] - }} }} -] ----END CONFIRMATION_EXAMPLE--- -""" diff --git a/samples/agent/adk/restaurant_finder/agent.py b/samples/agent/adk/restaurant_finder/agent.py index e4f9e3fef..1e776d1ee 100644 --- a/samples/agent/adk/restaurant_finder/agent.py +++ b/samples/agent/adk/restaurant_finder/agent.py @@ -19,6 +19,7 @@ from typing import Any import jsonschema +from a2a.types import AgentCapabilities, AgentCard, AgentSkill from google.adk.agents.llm_agent import LlmAgent from google.adk.artifacts import InMemoryArtifactService from google.adk.memory.in_memory_memory_service import InMemoryMemoryService @@ -27,31 +28,17 @@ from google.adk.sessions import InMemorySessionService from google.genai import types from prompt_builder import ( - A2UI_SCHEMA, - RESTAURANT_UI_EXAMPLES, get_text_prompt, - get_ui_prompt, + ROLE_DESCRIPTION, + WORKFLOW_DESCRIPTION, + UI_DESCRIPTION, ) from tools import get_restaurants +from a2ui.inference.schema.manager import A2uiSchemaManager +from a2ui.inference.schema.common_modifiers import remove_strict_validation logger = logging.getLogger(__name__) -AGENT_INSTRUCTION = """ - You are a helpful restaurant finding assistant. Your goal is to help users find and book restaurants using a rich UI. - - To achieve this, you MUST follow this logic: - - 1. **For finding restaurants:** - a. You MUST call the `get_restaurants` tool. Extract the cuisine, location, and a specific number (`count`) of restaurants from the user's query (e.g., for "top 5 chinese places", count is 5). - b. After receiving the data, you MUST follow the instructions precisely to generate the final a2ui UI JSON, using the appropriate UI example from the `prompt_builder.py` based on the number of restaurants. - - 2. **For booking a table (when you receive a query like 'USER_WANTS_TO_BOOK...'):** - a. You MUST use the appropriate UI example from `prompt_builder.py` to generate the UI, populating the `dataModelUpdate.contents` with the details from the user's query. - - 3. **For confirming a booking (when you receive a query like 'User submitted a booking...'):** - a. You MUST use the appropriate UI example from `prompt_builder.py` to generate the confirmation UI, populating the `dataModelUpdate.contents` with the final booking details. -""" - class RestaurantAgent: """An agent that finds restaurants based on user criteria.""" @@ -61,6 +48,15 @@ class RestaurantAgent: 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/", + schema_modifiers=[remove_strict_validation], + ) + if use_ui + else None + ) self._agent = self._build_agent(use_ui) self._user_id = "remote_agent" self._runner = Runner( @@ -71,20 +67,31 @@ 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_restaurants", + name="Find Restaurants Tool", + description=( + "Helps find restaurants based on user criteria (e.g., cuisine, location)." + ), + tags=["restaurant", "finder"], + examples=["Find me the top 10 chinese restaurants in the US"], + ) + + return AgentCard( + name="Restaurant Agent", + description="This agent helps find restaurants based on user criteria.", + url=self.base_url, + version="1.0.0", + default_input_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES, + default_output_modes=RestaurantAgent.SUPPORTED_CONTENT_TYPES, + capabilities=capabilities, + skills=[skill], + ) def get_processing_message(self) -> str: return "Finding restaurants that match your criteria..." @@ -93,13 +100,18 @@ def _build_agent(self, use_ui: bool) -> LlmAgent: """Builds the LLM agent for the restaurant agent.""" LITELLM_MODEL = os.getenv("LITELLM_MODEL", "gemini/gemini-2.5-flash") - if use_ui: - # Construct the full prompt with UI instructions, examples, and schema - instruction = AGENT_INSTRUCTION + get_ui_prompt( - self.base_url, RESTAURANT_UI_EXAMPLES - ) - else: - 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=True, + ) + if use_ui + else get_text_prompt() + ) return LlmAgent( model=LiteLlm(model=LITELLM_MODEL), @@ -133,7 +145,8 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]: current_query_text = query # Ensure schema was loaded - if self.use_ui and self.a2ui_schema_object is None: + effective_catalog = self._schema_manager.get_effective_catalog() + if self.use_ui and not effective_catalog.catalog_schema: logger.error( "--- RestaurantAgent.stream: A2UI_SCHEMA is not loaded. " "Cannot perform UI validation. ---" @@ -230,7 +243,7 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]: logger.info( "--- RestaurantAgent.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/restaurant_finder/agent_executor.py b/samples/agent/adk/restaurant_finder/agent_executor.py index 9664d6071..c4bcecb6b 100644 --- a/samples/agent/adk/restaurant_finder/agent_executor.py +++ b/samples/agent/adk/restaurant_finder/agent_executor.py @@ -41,11 +41,11 @@ class RestaurantAgentExecutor(AgentExecutor): """Restaurant AgentExecutor Example.""" - def __init__(self, base_url: str): + def __init__(self, ui_agent: RestaurantAgent, text_agent: RestaurantAgent): # Instantiate two agents: one for UI and one for text-only. # The appropriate one will be chosen at execution time. - self.ui_agent = RestaurantAgent(base_url=base_url, use_ui=True) - self.text_agent = RestaurantAgent(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/restaurant_finder/examples/booking_form.json b/samples/agent/adk/restaurant_finder/examples/booking_form.json new file mode 100644 index 000000000..e144b78b7 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/booking_form.json @@ -0,0 +1,30 @@ +[ + { "beginRendering": { "surfaceId": "booking-form", "root": "booking-form-column", "styles": { "primaryColor": "#FF0000", "font": "Roboto" } } }, + { "surfaceUpdate": { + "surfaceId": "booking-form", + "components": [ + { "id": "booking-form-column", "component": { "Column": { "children": { "explicitList": ["booking-title", "restaurant-image", "restaurant-address", "party-size-field", "datetime-field", "dietary-field", "submit-button"] } } } } , + { "id": "booking-title", "component": { "Text": { "usageHint": "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": { "literalString": "Party Size" }, "text": { "path": "partySize" }, "type": "number" } } }, + { "id": "datetime-field", "component": { "DateTimeInput": { "label": { "literalString": "Date & Time" }, "value": { "path": "reservationTime" }, "enableDate": true, "enableTime": true } } }, + { "id": "dietary-field", "component": { "TextField": { "label": { "literalString": "Dietary Requirements" }, "text": { "path": "dietary" } } } }, + { "id": "submit-button", "component": { "Button": { "child": "submit-reservation-text", "action": { "name": "submit_booking", "context": [ { "key": "restaurantName", "value": { "path": "restaurantName" } }, { "key": "partySize", "value": { "path": "partySize" } }, { "key": "reservationTime", "value": { "path": "reservationTime" } }, { "key": "dietary", "value": { "path": "dietary" } }, { "key": "imageUrl", "value": { "path": "imageUrl" } } ] } } } }, + { "id": "submit-reservation-text", "component": { "Text": { "text": { "literalString": "Submit Reservation" } } } } + ] + }}, + { "dataModelUpdate": { + "surfaceId": "booking-form", + "path": "/", + "contents": [ + { "key": "title", "valueString": "Book a Table at [RestaurantName]" }, + { "key": "address", "valueString": "[Restaurant Address]" }, + { "key": "restaurantName", "valueString": "[RestaurantName]" }, + { "key": "partySize", "valueString": "2" }, + { "key": "reservationTime", "valueString": "" }, + { "key": "dietary", "valueString": "" }, + { "key": "imageUrl", "valueString": "" } + ] + }} +] \ No newline at end of file diff --git a/samples/agent/adk/restaurant_finder/examples/confirmation.json b/samples/agent/adk/restaurant_finder/examples/confirmation.json new file mode 100644 index 000000000..049e4b9c8 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/confirmation.json @@ -0,0 +1,27 @@ +[ + { "beginRendering": { "surfaceId": "confirmation", "root": "confirmation-card", "styles": { "primaryColor": "#FF0000", "font": "Roboto" } } }, + { "surfaceUpdate": { + "surfaceId": "confirmation", + "components": [ + { "id": "confirmation-card", "component": { "Card": { "child": "confirmation-column" } } }, + { "id": "confirm-title", "component": { "Text": { "usageHint": "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": { "usageHint": "h5", "text": { "literalString": "We look forward to seeing you!" } } } }, + { "id": "divider1", "component": { "Divider": {} } }, + { "id": "divider2", "component": { "Divider": {} } }, + { "id": "divider3", "component": { "Divider": {} } } + ] + }}, + { "dataModelUpdate": { + "surfaceId": "confirmation", + "path": "/", + "contents": [ + { "key": "title", "valueString": "Booking at [RestaurantName]" }, + { "key": "bookingDetails", "valueString": "[PartySize] people at [Time]" }, + { "key": "dietaryRequirements", "valueString": "Dietary Requirements: [Requirements]" }, + { "key": "imageUrl", "valueString": "[ImageUrl]" } + ] + }} +] \ No newline at end of file diff --git a/samples/agent/adk/restaurant_finder/examples/single_column_list.json b/samples/agent/adk/restaurant_finder/examples/single_column_list.json new file mode 100644 index 000000000..377d1b6dc --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/single_column_list.json @@ -0,0 +1,45 @@ +[ + { "beginRendering": { "surfaceId": "default", "root": "root-column", "styles": { "primaryColor": "#FF0000", "font": "Roboto" } } }, + { "surfaceUpdate": { + "surfaceId": "default", + "components": [ + { "id": "root-column", "component": { "Column": { "children": { "explicitList": ["title-heading", "item-list"] } } } }, + { "id": "title-heading", "component": { "Text": { "usageHint": "h1", "text": { "path": "title" } } } }, + { "id": "item-list", "component": { "List": { "direction": "vertical", "children": { "template": { "componentId": "item-card-template", "dataBinding": "/items" } } } } }, + { "id": "item-card-template", "component": { "Card": { "child": "card-layout" } } }, + { "id": "card-layout", "component": { "Row": { "children": { "explicitList": ["template-image", "card-details"] } } } }, + { "id": "template-image", "weight": 1, "component": { "Image": { "url": { "path": "imageUrl" } } } }, + { "id": "card-details", "weight": 2, "component": { "Column": { "children": { "explicitList": ["template-name", "template-rating", "template-detail", "template-link", "template-book-button"] } } } }, + { "id": "template-name", "component": { "Text": { "usageHint": "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", "primary": true, "action": { "name": "book_restaurant", "context": [ { "key": "restaurantName", "value": { "path": "name" } }, { "key": "imageUrl", "value": { "path": "imageUrl" } }, { "key": "address", "value": { "path": "address" } } ] } } } }, + { "id": "book-now-text", "component": { "Text": { "text": { "literalString": "Book Now" } } } } + ] + }}, + {"dataModelUpdate": { + "surfaceId": "default", + "path": "/", + "contents": [ + { "key": "items", "valueMap": [ + { "key": "item1", "valueMap": [ + { "key": "name", "valueString": "The Fancy Place" }, + { "key": "rating", "valueNumber": 4.8 }, + { "key": "detail", "valueString": "Fine dining experience" }, + { "key": "infoLink", "valueString": "https://example.com/fancy" }, + { "key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }, + { "key": "address", "valueString": "123 Main St" } + ] }, + { "key": "item2", "valueMap": [ + { "key": "name", "valueString": "Quick Bites" }, + { "key": "rating", "valueNumber": 4.2 }, + { "key": "detail", "valueString": "Casual and fast" }, + { "key": "infoLink", "valueString": "https://example.com/quick" }, + { "key": "imageUrl", "valueString": "https://example.com/quick.jpg" }, + { "key": "address", "valueString": "456 Oak Ave" } + ] } + ] } + ] + }} +] \ No newline at end of file diff --git a/samples/agent/adk/restaurant_finder/examples/two_column_list.json b/samples/agent/adk/restaurant_finder/examples/two_column_list.json new file mode 100644 index 000000000..51e479699 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/examples/two_column_list.json @@ -0,0 +1,56 @@ +[ + { "beginRendering": { "surfaceId": "default", "root": "root-column", "styles": { "primaryColor": "#FF0000", "font": "Roboto" } } }, + { "surfaceUpdate": { + "surfaceId": "default", + "components": [ + { "id": "root-column", "component": { "Column": { "children": { "explicitList": ["title-heading", "restaurant-row-1"] } } } }, + { "id": "title-heading", "component": { "Text": { "usageHint": "h1", "text": { "path": "title" } } } }, + { "id": "restaurant-row-1", "component": { "Row": { "children": { "explicitList": ["item-card-1", "item-card-2"] } } } }, + { "id": "item-card-1", "weight": 1, "component": { "Card": { "child": "card-layout-1" } } }, + { "id": "card-layout-1", "component": { "Column": { "children": { "explicitList": ["template-image-1", "card-details-1"] } } } }, + { "id": "template-image-1", "component": { "Image": { "url": { "path": "/items/0/imageUrl" }, "width": "100%" } } }, + { "id": "card-details-1", "component": { "Column": { "children": { "explicitList": ["template-name-1", "template-rating-1", "template-detail-1", "template-link-1", "template-book-button-1"] } } } }, + { "id": "template-name-1", "component": { "Text": { "usageHint": "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": { "name": "book_restaurant", "context": [ { "key": "restaurantName", "value": { "path": "/items/0/name" } }, { "key": "imageUrl", "value": { "path": "/items/0/imageUrl" } }, { "key": "address", "value": { "path": "/items/0/address" } } ] } } } }, + { "id": "book-now-text-1", "component": { "Text": { "text": { "literalString": "Book Now" } } } }, + { "id": "item-card-2", "weight": 1, "component": { "Card": { "child": "card-layout-2" } } }, + { "id": "card-layout-2", "component": { "Column": { "children": { "explicitList": ["template-image-2", "card-details-2"] } } } }, + { "id": "template-image-2", "component": { "Image": { "url": { "path": "/items/1/imageUrl" }, "width": "100%" } } }, + { "id": "card-details-2", "component": { "Column": { "children": { "explicitList": ["template-name-2", "template-rating-2", "template-detail-2", "template-link-2", "template-book-button-2"] } } } }, + { "id": "template-name-2", "component": { "Text": { "usageHint": "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": { "name": "book_restaurant", "context": [ { "key": "restaurantName", "value": { "path": "/items/1/name" } }, { "key": "imageUrl", "value": { "path": "/items/1/imageUrl" } }, { "key": "address", "value": { "path": "/items/1/address" } } ] } } } }, + { "id": "book-now-text-2", "component": { "Text": { "text": { "literalString": "Book Now" } } } } + ] + }}, + {"dataModelUpdate": { + "surfaceId": "default", + "path": "/", + "contents": [ + {"key": "title", "valueString": "Top Restaurants" }, + {"key": "items", "valueMap": [ + {"key": "item1", "valueMap": [ + {"key": "name", "valueString": "The Fancy Place" }, + {"key": "rating", "valueNumber": 4.8 }, + {"key": "detail", "valueString": "Fine dining experience" }, + {"key": "infoLink", "valueString": "https://example.com/fancy" }, + {"key": "imageUrl", "valueString": "https://example.com/fancy.jpg" }, + {"key": "address", "valueString": "123 Main St" } + ]}, + {"key": "item2", "valueMap": [ + {"key": "name", "valueString": "Quick Bites" }, + {"key": "rating", "valueNumber": 4.2 }, + {"key": "detail", "valueString": "Casual and fast" }, + {"key": "infoLink", "valueString": "https://example.com/quick" }, + {"key": "imageUrl", "valueString": "https://example.com/quick.jpg" }, + {"key": "address", "valueString": "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 0985df8a3..e6e64a10e 100644 --- a/samples/agent/adk/restaurant_finder/prompt_builder.py +++ b/samples/agent/adk/restaurant_finder/prompt_builder.py @@ -12,818 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -# The A2UI schema remains constant for all A2UI responses. -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"] - } - } -} +from a2ui.inference.schema.manager import A2uiSchemaManager +from a2ui.inference.schema.common_modifiers import remove_strict_validation + +ROLE_DESCRIPTION = ( + "You are a helpful restaurant finding assistant. Your final output MUST be a a2ui" + " UI 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. +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. """ -from a2ui_examples import RESTAURANT_UI_EXAMPLES - - -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. - """ - # The f-string substitution for base_url happens here, at runtime. - formatted_examples = examples.format(base_url=base_url) - - return f""" - You are a helpful restaurant finding 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. - 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. - - --- UI TEMPLATE RULES --- - - 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 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. - - If the query is a booking submission (e.g., "User submitted a booking..."), you MUST use the `CONFIRMATION_EXAMPLE` template. - - {formatted_examples} - - ---BEGIN A2UI JSON SCHEMA--- - {A2UI_SCHEMA} - ---END A2UI JSON SCHEMA--- - """ +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 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. +- If the query is a booking submission (e.g., "User submitted a booking..."), you MUST use the `CONFIRMATION_EXAMPLE` template. +""" def get_text_prompt() -> str: @@ -847,14 +58,24 @@ def get_text_prompt() -> str: if __name__ == "__main__": - # Example of how to use the prompt builder + # Example of how to use the A2UI Schema Manager to generate a system prompt # In your actual application, you would call this from your main agent logic. - my_base_url = "http://localhost:8000" # 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. - restaurant_prompt = get_ui_prompt(my_base_url, RESTAURANT_UI_EXAMPLES) + restaurant_prompt = A2uiSchemaManager( + "0.8", + basic_examples_path="examples/", + schema_modifiers=[remove_strict_validation], + ).generate_system_prompt( + role_description=ROLE_DESCRIPTION, + workflow_description=WORKFLOW_DESCRIPTION, + ui_description=UI_DESCRIPTION, + include_schema=True, + include_examples=True, + validate_examples=True, + ) print(restaurant_prompt)