Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion renderers/angular/src/lib/catalog/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@ import { Primitives } from '@a2ui/lit/0.8';
})
export class Icon extends DynamicComponent {
readonly name = input.required<Primitives.StringValue | null>();
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;
});
}
7 changes: 7 additions & 0 deletions renderers/angular/src/lib/data/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -79,6 +80,7 @@ export class MarkdownRenderer {
case 'em':
tokenName = 'em';
break;

}

if (!tokenName) {
Expand All @@ -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 {
Expand Down
43 changes: 27 additions & 16 deletions samples/agent/adk/contact_lookup/a2ui_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"} } } ,
Expand All @@ -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": "" }
]
} }
]
Expand All @@ -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---
Expand Down
1 change: 1 addition & 0 deletions samples/agent/adk/contact_lookup/a2ui_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"help",
"home",
"info",
"link",
"locationOn",
"lock",
"lockOpen",
Expand Down
5 changes: 3 additions & 2 deletions samples/agent/adk/contact_lookup/agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
79 changes: 42 additions & 37 deletions samples/agent/adk/contact_lookup/contact_data.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
{
"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"
}
]

27 changes: 23 additions & 4 deletions samples/agent/adk/contact_lookup/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
2 changes: 1 addition & 1 deletion samples/client/angular/projects/contact/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Google+Symbols:opsz,wght,FILL,GRAD,ROND@20..48,100..700,0..1,-50..200,0..100&display=swap&icon_names=calendar_today,call,location_on,mail,progress_activity,send"
href="https://fonts.googleapis.com/css2?family=Google+Symbols:opsz,wght,FILL,GRAD,ROND@20..48,100..700,0..1,-50..200,0..100&display=swap&icon_names=calendar,calendar_today,call,check,check_circle,code,event,link,location,location_on,mail,map,place,progress_activity,schedule,send,settings,stack,star"
/>
</head>
<body>
Expand Down
6 changes: 3 additions & 3 deletions samples/client/angular/projects/contact/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ app.use(
maxAge: '1y',
index: false,
redirect: false,
})
}),
);

app.post('/a2a', (req, res) => {
Expand All @@ -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',
Expand Down Expand Up @@ -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,
});

Expand Down
16 changes: 16 additions & 0 deletions samples/client/angular/projects/contact/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}