diff --git a/a2a_agents/python/adk/samples/contact_lookup/a2ui_schema.py b/a2a_agents/python/adk/samples/contact_lookup/a2ui_schema.py index f4c776d80..024b8eea0 100644 --- a/a2a_agents/python/adk/samples/contact_lookup/a2ui_schema.py +++ b/a2a_agents/python/adk/samples/contact_lookup/a2ui_schema.py @@ -45,7 +45,11 @@ "type": "string", "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", "pattern": "^#[0-9a-fA-F]{6}$" - } + }, + "logoUrl": { + "type": "string", + "description": "The URL of the brand logo." + }, } } }, diff --git a/a2a_agents/python/adk/samples/restaurant_finder/a2ui_examples.py b/a2a_agents/python/adk/samples/restaurant_finder/a2ui_examples.py index b77a685ea..e69936877 100644 --- a/a2a_agents/python/adk/samples/restaurant_finder/a2ui_examples.py +++ b/a2a_agents/python/adk/samples/restaurant_finder/a2ui_examples.py @@ -15,7 +15,7 @@ RESTAURANT_UI_EXAMPLES = """ ---BEGIN SINGLE_COLUMN_LIST_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column" }} }}, {{ "surfaceUpdate": {{ "surfaceId": "default", "components": [ @@ -63,7 +63,7 @@ ---BEGIN TWO_COLUMN_LIST_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "beginRendering": {{ "surfaceId": "default", "root": "root-column" }} }}, {{ "surfaceUpdate": {{ "surfaceId": "default", "components": [ @@ -122,7 +122,7 @@ ---BEGIN BOOKING_FORM_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "booking-form", "root": "booking-form-column", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "beginRendering": {{ "surfaceId": "booking-form", "root": "booking-form-column" }} }}, {{ "surfaceUpdate": {{ "surfaceId": "booking-form", "components": [ @@ -155,7 +155,7 @@ ---BEGIN CONFIRMATION_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "confirmation", "root": "confirmation-card", "styles": {{ "primaryColor": "#FF0000", "font": "Roboto" }} }} }}, + {{ "beginRendering": {{ "surfaceId": "confirmation", "root": "confirmation-card" }} }}, {{ "surfaceUpdate": {{ "surfaceId": "confirmation", "components": [ diff --git a/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py b/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py index 6eb2f7aa8..d535ca9da 100644 --- a/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py +++ b/a2a_agents/python/adk/samples/restaurant_finder/prompt_builder.py @@ -13,7 +13,7 @@ # limitations under the License. # The A2UI schema remains constant for all A2UI responses. -A2UI_SCHEMA = r''' +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'.", @@ -43,7 +43,12 @@ "type": "string", "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", "pattern": "^#[0-9a-fA-F]{6}$" + }, + "logoUrl": { + "type": "string", + "description": "The URL of the brand logo or hero image." } + } } }, @@ -787,10 +792,18 @@ } } } -''' +""" from a2ui_examples import RESTAURANT_UI_EXAMPLES +# Configurable Theme Constants +# Uncomment or modify these values to apply server-driven styling. +THEME_CONFIG = { + # "primaryColor": "#ea1277", # Example: Pink/Red + # "logoUrl": "{base_url}/static/logo.png", + # "font": "Courier New", # Example: Roboto, Outfit, etc. +} + def get_ui_prompt(base_url: str, examples: str) -> str: """ @@ -806,6 +819,15 @@ def get_ui_prompt(base_url: str, examples: str) -> str: # The f-string substitution for base_url happens here, at runtime. formatted_examples = examples.format(base_url=base_url) + # Construct dynamic style instructions based on THEME_CONFIG + style_instructions = "" + if THEME_CONFIG: + style_instructions = " - When sending a `beginRendering` message, you MUST include the `styles` object with:\n" + for key, value in THEME_CONFIG.items(): + style_instructions += f' - `{key}`: "{value}"\n' + else: + style_instructions = ' - When sending a `beginRendering` message, you MAY include an empty `styles` object: "styles": {}\n' + return f""" You are a helpful restaurant finding assistant. Your final output MUST be a a2ui UI JSON response. @@ -816,6 +838,7 @@ def get_ui_prompt(base_url: str, examples: str) -> str: 4. The JSON part MUST validate against the A2UI JSON SCHEMA provided below. --- UI TEMPLATE RULES --- +{style_instructions} - 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. diff --git a/samples/client/lit/shell/app.ts b/samples/client/lit/shell/app.ts index 50d2d6108..426e80e9d 100644 --- a/samples/client/lit/shell/app.ts +++ b/samples/client/lit/shell/app.ts @@ -694,11 +694,52 @@ export class A2UILayoutEditor extends SignalWatcher(LitElement) { async #sendAndProcessMessage(request) { const messages = await this.#sendMessage(request); + // Apply server-side styles if present in beginRendering + for (const msg of messages) { + if ('beginRendering' in msg && msg.beginRendering.styles) { + this.#applyServerStyles(msg.beginRendering.styles); + } + } + this.#lastMessages = messages; this.#processor.clearSurfaces(); this.#processor.processMessages(messages); } + #applyServerStyles(styles: any) { + const root = document.documentElement; + + // 1. Primary Color + if (styles.primaryColor) { + root.style.setProperty('--primary-color', styles.primaryColor); + // Generate a simple dim variant for now (could be more sophisticated) + root.style.setProperty('--p-60', styles.primaryColor); + + // Update gradient to use the new primary color + // We'll create a gradient from a lighter version to the primary color + // For now, let's just use the primary color and a shifted hue for a simple dynamic gradient + root.style.setProperty('--primary-gradient', `linear-gradient(135deg, ${styles.primaryColor} 0%, ${styles.primaryColor}dd 100%)`); + } + + // 2. Font Family + if (styles.font) { + root.style.setProperty('--bb-font-family', styles.font); + root.style.setProperty('font-family', styles.font); + } + + + + // 3. Logo / Hero Image + if (styles.logoUrl) { + // Update the config dynamically to show the new hero image + this.config = { + ...this.config, + heroImage: styles.logoUrl, + heroImageDark: styles.logoUrl, // Use same for dark mode unless specified otherwise + }; + } + } + snackbar( diff --git a/samples/client/lit/shell/theme/styles.css b/samples/client/lit/shell/theme/styles.css index 2529df7ba..5db873f65 100644 --- a/samples/client/lit/shell/theme/styles.css +++ b/samples/client/lit/shell/theme/styles.css @@ -11,7 +11,7 @@ /* Global overrides */ * { - font-family: 'Outfit', sans-serif !important; + font-family: var(--bb-font-family, 'Outfit', sans-serif) !important; } /* Card Styling */