diff --git a/renderers/angular/src/lib/catalog/icon.ts b/renderers/angular/src/lib/catalog/icon.ts index addc7fed..8b399d2b 100644 --- a/renderers/angular/src/lib/catalog/icon.ts +++ b/renderers/angular/src/lib/catalog/icon.ts @@ -40,5 +40,8 @@ import { Primitives } from '@a2ui/lit/0.8'; }) export class Icon extends DynamicComponent { readonly name = input.required(); - protected readonly resolvedName = computed(() => this.resolvePrimitive(this.name())); + protected readonly resolvedName = computed(() => { + const name = this.resolvePrimitive(this.name()); + return name ? name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) : null; + }); } diff --git a/renderers/angular/src/lib/data/markdown.ts b/renderers/angular/src/lib/data/markdown.ts index dac3a77d..3190d660 100644 --- a/renderers/angular/src/lib/data/markdown.ts +++ b/renderers/angular/src/lib/data/markdown.ts @@ -24,6 +24,7 @@ export class MarkdownRenderer { private sanitizer = inject(DomSanitizer); private markdownIt = MarkdownIt({ + linkify: true, highlight: (str, lang) => { if (lang === 'html') { const iframe = document.createElement('iframe'); @@ -79,6 +80,7 @@ export class MarkdownRenderer { case 'em': tokenName = 'em'; break; + } if (!tokenName) { @@ -95,6 +97,11 @@ export class MarkdownRenderer { token.attrJoin('class', clazz); } + if (tokenName === 'link') { + token.attrSet('target', '_blank'); + token.attrSet('rel', 'noopener noreferrer'); + } + if (original) { return original.call(this, tokens, idx, options, env, self); } else { diff --git a/samples/agent/adk/contact_lookup/a2ui_examples.py b/samples/agent/adk/contact_lookup/a2ui_examples.py index dcf74210..c0153c73 100644 --- a/samples/agent/adk/contact_lookup/a2ui_examples.py +++ b/samples/agent/adk/contact_lookup/a2ui_examples.py @@ -93,8 +93,8 @@ { "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": "button_1_text", "component": { "Text": { "text": { "literalString": "Follow"} } } } , { "id": "button_1", "component": { "Button": { "child": "button_1_text", "primary": true, "action": { "name": "follow_contact", "context": [ { "key": "contactName", "value": { "path": "name" } } ] } } } } , + { "id": "button_2_text", "component": { "Text": { "text": { "literalString": "Message"} } } } , { "id": "button_2", "component": { "Button": { "child": "button_2_text", "primary": false, "action": { "name": "send_message", "context": [ { "key": "contactName", "value": { "path": "name" } } ] } } } } , { "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"} } } , @@ -121,25 +121,26 @@ ---BEGIN ACTION_CONFIRMATION_EXAMPLE--- [ - { "beginRendering": { "surfaceId": "action-modal", "root": "modal-wrapper", "styles": { "primaryColor": "#007BFF", "font": "Roboto" } } }, + { "beginRendering": { "surfaceId": "contact-card", "root": "message-success-card"} }, { "surfaceUpdate": { - "surfaceId": "action-modal", + "surfaceId": "contact-card", "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" } } } } + { "id": "success_icon", "component": { "Icon": { "name": { "literalString": "send"}, "size": 48.0, "color": "#4CAF50"} } }, + { "id": "success_title", "component": { "Text": { "text": { "path": "actionTitle"}, "usageHint": "h2"} } }, + { "id": "success_message", "component": { "Text": { "text": { "path": "actionMessage"} } } }, + { "id": "back_button_text", "component": { "Text": { "text": { "literalString": "Back to Profile"} } } }, + { "id": "back_button", "component": { "Button": { "child": "back_button_text", "primary": false, "action": { "name": "view_profile", "context": [ { "key": "contactName", "value": { "path": "contactName" } } ] } } } }, + { "id": "success_column", "component": { "Column": { "children": { "explicitList": ["success_icon", "success_title", "success_message", "back_button"]}, "alignment": "center"} } }, + { "id": "message-success-card", "component": { "Card": { "child": "success_column"} } } ] } }, { "dataModelUpdate": { - "surfaceId": "action-modal", + "surfaceId": "contact-card", "path": "/", "contents": [ - { "key": "actionTitle", "valueString": "Action Confirmation" }, - { "key": "actionMessage", "valueString": "Your action has been processed." } + { "key": "actionTitle", "valueString": "Message Sent" }, + { "key": "actionMessage", "valueString": "Your message has been sent to." }, + { "key": "contactName", "valueString": "" } ] } } ] @@ -152,10 +153,20 @@ "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_text", "component": { "Text": { "text": { "path": "followMessage"}, "usageHint": "h2"} } } , + { "id": "back_button_text", "component": { "Text": { "text": { "literalString": "Back to Profile"} } } } , + { "id": "back_button", "component": { "Button": { "child": "back_button_text", "primary": false, "action": { "name": "view_profile", "context": [ { "key": "contactName", "value": { "path": "contactName" } } ] } } } } , + { "id": "success_column", "component": { "Column": { "children": { "explicitList": ["success_icon", "success_text", "back_button"]} , "alignment": "center"} } } , { "id": "success_card", "component": { "Card": { "child": "success_column"} } } ] + } }, + { "dataModelUpdate": { + "surfaceId": "contact-card", + "path": "/", + "contents": [ + { "key": "followMessage", "valueString": "Successfully Followed" }, + { "key": "contactName", "valueString": "" } + ] } } ] ---END FOLLOW_SUCCESS_EXAMPLE--- diff --git a/samples/agent/adk/contact_lookup/a2ui_schema.py b/samples/agent/adk/contact_lookup/a2ui_schema.py index 4b6038fd..862ab247 100644 --- a/samples/agent/adk/contact_lookup/a2ui_schema.py +++ b/samples/agent/adk/contact_lookup/a2ui_schema.py @@ -182,6 +182,7 @@ "help", "home", "info", + "link", "locationOn", "lock", "lockOpen", diff --git a/samples/agent/adk/contact_lookup/agent_executor.py b/samples/agent/adk/contact_lookup/agent_executor.py index 3ea8ba98..78153c16 100644 --- a/samples/agent/adk/contact_lookup/agent_executor.py +++ b/samples/agent/adk/contact_lookup/agent_executor.py @@ -107,10 +107,11 @@ async def execute( elif action == "send_message": contact_name = ctx.get("contactName", "Unknown") - query = f"USER_WANTS_TO_MESSAGE: {contact_name}" + query = f"ACTION: send_message to {contact_name}" elif action == "follow_contact": - query = "ACTION: follow_contact" + contact_name = ctx.get("contactName", "Unknown") + query = f"ACTION: follow_contact for {contact_name}" elif action == "view_full_profile": contact_name = ctx.get("contactName", "Unknown") diff --git a/samples/agent/adk/contact_lookup/contact_data.json b/samples/agent/adk/contact_lookup/contact_data.json index 5116d89a..b34025ed 100644 --- a/samples/agent/adk/contact_lookup/contact_data.json +++ b/samples/agent/adk/contact_lookup/contact_data.json @@ -1,38 +1,43 @@ [ - { - "id": "1", - "name": "Alex Jordan", - "title": "Product Marketing Manager", - "team": "Team Macally", - "department": "Marketing", - "location": "New York", - "email": "alex.jordan@example.com", - "mobile": "+1 (415) 171-1080", - "calendar": "Free until 4:00 PM", - "imageUrl": "http://localhost:10002/static/profile1.png" - }, - { - "id": "2", - "name": "Casey Smith", - "title": "Digital Marketing Specialist", - "team": "Growth Team", - "department": "Marketing", - "location": "New York", - "email": "casey.smith@example.com", - "mobile": "+1 (415) 222-3333", - "calendar": "In a meeting", - "imageUrl": "http://localhost:10002/static/profile2.png" - }, - { - "id": "3", - "name": "Jordan Taylor", - "title": "Senior Software Engineer", - "team": "Core Platform", - "department": "Engineering", - "location": "San Francisco", - "email": "jordan.taylor@example.com", - "mobile": "+1 (650) 444-5555", - "calendar": "Focus time", - "imageUrl": "http://localhost:10002/static/profile3.png" - } -] \ No newline at end of file + { + "id": "1", + "name": "Alex Jordan", + "title": "Product Marketing Manager", + "team": "Team Macally", + "department": "Marketing", + "location": "New York", + "email": "alex.jordan@example.com", + "mobile": "+1 (415) 171-1080", + "calendar": "Free until 4:00 PM", + "meetupPlace": "San Francisco", + "imageUrl": "http://localhost:10002/static/profile1.png", + "favorite_framework": "Angular", + "firstMorningCoffeeSip": "7am" + }, + { + "id": "2", + "name": "Casey Smith", + "title": "Digital Marketing Specialist", + "team": "Growth Team", + "department": "Marketing", + "location": "New York", + "email": "casey.smith@example.com", + "mobile": "+1 (415) 222-3333", + "calendar": "In a meeting", + "imageUrl": "http://localhost:10002/static/profile2.png", + "githubUrl": "https://github.com/casey-smith" + }, + { + "id": "3", + "name": "Jordan Taylor", + "title": "Senior Software Engineer", + "team": "Core Platform", + "department": "Engineering", + "location": "San Francisco", + "email": "jordan.taylor@example.com", + "mobile": "+1 (650) 444-5555", + "calendar": "Focus time", + "imageUrl": "http://localhost:10002/static/profile3.png" + } +] + diff --git a/samples/agent/adk/contact_lookup/prompt_builder.py b/samples/agent/adk/contact_lookup/prompt_builder.py index 3fe269e6..dc7eb520 100644 --- a/samples/agent/adk/contact_lookup/prompt_builder.py +++ b/samples/agent/adk/contact_lookup/prompt_builder.py @@ -68,18 +68,37 @@ def get_ui_prompt(base_url: str, examples: str) -> str: --- 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.). + 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. If additional important fields (like 'favorite_framework' or 'meetupPlace') are present, you MUST add a new 'Row' to the 'info_rows_column' (matching the structure of existing info rows). + - Use the 'link' icon if the value is a URL. + - Use the 'calendar_today' icon if the value represents a date, time, or schedule. + - Use the 'location_on' icon if the value represents a location or place. + - Otherwise, use the 'star' icon. 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. + b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template. If additional important fields are present, you MUST add a new 'Row' to the 'info_rows_column' with: + - The 'link' icon for URLs. + - The 'calendar_today' icon for date/time/schedule. + - The 'location_on' icon for location/place. + - The 'star' icon for others. - **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. + b. This will render a new card with a "Successfully Followed" message and a "Back" button. + c. Populate the `dataModelUpdate.contents` with: + - `followMessage`: "Successfully followed the contact." (Include the actual contact name at the end) + - `contactName`: The contact's name (for the back button). + d. Respond with a text confirmation like "You are now following this contact." along with the JSON. + + - **For handling actions (e.g., "send_message"):** + a. You MUST use the `ACTION_CONFIRMATION_EXAMPLE` template. + b. Populate the `dataModelUpdate.contents` with: + - `actionTitle`: "Message Sent" + - `actionMessage`: "Your message has been sent to the contact." (Include the actual contact name at the end of the string) + - `contactName`: The contact's name (for the back button). + c. Respond with a text confirmation like "Message sent." along with the JSON. {formatted_examples} diff --git a/samples/client/angular/projects/contact/src/index.html b/samples/client/angular/projects/contact/src/index.html index 95c1a28c..5c6fd656 100644 --- a/samples/client/angular/projects/contact/src/index.html +++ b/samples/client/angular/projects/contact/src/index.html @@ -28,7 +28,7 @@ diff --git a/samples/client/angular/projects/contact/src/server.ts b/samples/client/angular/projects/contact/src/server.ts index d1cab0c3..3743437a 100644 --- a/samples/client/angular/projects/contact/src/server.ts +++ b/samples/client/angular/projects/contact/src/server.ts @@ -36,7 +36,7 @@ app.use( maxAge: '1y', index: false, redirect: false, - }) + }), ); app.post('/a2a', (req, res) => { @@ -61,7 +61,7 @@ app.post('/a2a', (req, res) => { { kind: 'data', data: clientEvent, - metadata: { 'mimeType': 'application/json+a2aui' }, + metadata: { mimeType: 'application/json+a2aui' }, } as Part, ], kind: 'message', @@ -122,7 +122,7 @@ async function fetchWithCustomHeader(url: string | URL | Request, init?: Request async function createOrGetClient() { // Create a client pointing to the agent's Agent Card URL. - client ??= await A2AClient.fromCardUrl('http://localhost:10002/.well-known/agent-card.json', { + client ??= await A2AClient.fromCardUrl('http://localhost:10003/.well-known/agent-card.json', { fetchImpl: fetchWithCustomHeader, }); diff --git a/samples/client/angular/projects/contact/src/styles.css b/samples/client/angular/projects/contact/src/styles.css index d81301b8..bfd09df9 100644 --- a/samples/client/angular/projects/contact/src/styles.css +++ b/samples/client/angular/projects/contact/src/styles.css @@ -170,3 +170,19 @@ body { width: 100svw; height: 100svh; } + +.g-icon { + font-family: 'Google Symbols'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +}