Skip to content

Commit aa9e565

Browse files
committed
feat: enhance icon handling, expand contact data, and refine agent action context and UI for contact lookup sample
1 parent 2e373b4 commit aa9e565

File tree

10 files changed

+127
-64
lines changed

10 files changed

+127
-64
lines changed

renderers/angular/src/lib/catalog/icon.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,8 @@ import { Primitives } from '@a2ui/lit/0.8';
4040
})
4141
export class Icon extends DynamicComponent {
4242
readonly name = input.required<Primitives.StringValue | null>();
43-
protected readonly resolvedName = computed(() => this.resolvePrimitive(this.name()));
43+
protected readonly resolvedName = computed(() => {
44+
const name = this.resolvePrimitive(this.name());
45+
return name ? name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) : null;
46+
});
4447
}

renderers/angular/src/lib/data/markdown.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class MarkdownRenderer {
2424
private sanitizer = inject(DomSanitizer);
2525

2626
private markdownIt = MarkdownIt({
27+
linkify: true,
2728
highlight: (str, lang) => {
2829
if (lang === 'html') {
2930
const iframe = document.createElement('iframe');
@@ -79,6 +80,7 @@ export class MarkdownRenderer {
7980
case 'em':
8081
tokenName = 'em';
8182
break;
83+
8284
}
8385

8486
if (!tokenName) {
@@ -95,6 +97,11 @@ export class MarkdownRenderer {
9597
token.attrJoin('class', clazz);
9698
}
9799

100+
if (tokenName === 'link') {
101+
token.attrSet('target', '_blank');
102+
token.attrSet('rel', 'noopener noreferrer');
103+
}
104+
98105
if (original) {
99106
return original.call(this, tokens, idx, options, env, self);
100107
} else {

samples/agent/adk/contact_lookup/a2ui_examples.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@
9393
{ "id": "call_text_column", "component": { "Column": { "children": { "explicitList": ["call_primary_text", "call_secondary_text"]} , "distribution": "start", "alignment": "start"} } } ,
9494
{ "id": "info_row_4", "component": { "Row": { "children": { "explicitList": ["call_icon", "call_text_column"]} , "distribution": "start", "alignment": "start"} } } ,
9595
{ "id": "info_rows_column", "weight": 1, "component": { "Column": { "children": { "explicitList": ["info_row_1", "info_row_2", "info_row_3", "info_row_4"]} , "alignment": "stretch"} } } ,
96-
{ "id": "button_1_text", "component": { "Text": { "text": { "literalString": "Follow"} } } } , { "id": "button_1", "component": { "Button": { "child": "button_1_text", "primary": true, "action": { "name": "follow_contact"} } } } ,
97-
{ "id": "button_2_text", "component": { "Text": { "text": { "literalString": "Message"} } } } , { "id": "button_2", "component": { "Button": { "child": "button_2_text", "primary": false, "action": { "name": "send_message"} } } } ,
96+
{ "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" } } ] } } } } ,
97+
{ "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" } } ] } } } } ,
9898
{ "id": "action_buttons_row", "component": { "Row": { "children": { "explicitList": ["button_1", "button_2"]} , "distribution": "center", "alignment": "center"} } } ,
9999
{ "id": "link_text", "component": { "Text": { "text": { "literalString": "[View Full Profile](/profile)"} } } } ,
100100
{ "id": "link_text_wrapper", "component": { "Row": { "children": { "explicitList": ["link_text"]} , "distribution": "center", "alignment": "center"} } } ,
@@ -121,25 +121,26 @@
121121
122122
---BEGIN ACTION_CONFIRMATION_EXAMPLE---
123123
[
124-
{ "beginRendering": { "surfaceId": "action-modal", "root": "modal-wrapper", "styles": { "primaryColor": "#007BFF", "font": "Roboto" } } },
124+
{ "beginRendering": { "surfaceId": "contact-card", "root": "message-success-card"} },
125125
{ "surfaceUpdate": {
126-
"surfaceId": "action-modal",
126+
"surfaceId": "contact-card",
127127
"components": [
128-
{ "id": "modal-wrapper", "component": { "Modal": { "entryPointChild": "hidden-entry-point", "contentChild": "modal-content-column" } } },
129-
{ "id": "hidden-entry-point", "component": { "Text": { "text": { "literalString": "" } } } },
130-
{ "id": "modal-content-column", "component": { "Column": { "children": { "explicitList": ["modal-title", "modal-message", "dismiss-button"] }, "alignment": "center" } } },
131-
{ "id": "modal-title", "component": { "Text": { "usageHint": "h2", "text": { "path": "actionTitle" } } } },
132-
{ "id": "modal-message", "component": { "Text": { "text": { "path": "actionMessage" } } } },
133-
{ "id": "dismiss-button-text", "component": { "Text": { "text": { "literalString": "Dismiss" } } } },
134-
{ "id": "dismiss-button", "component": { "Button": { "child": "dismiss-button-text", "primary": true, "action": { "name": "dismiss_modal" } } } }
128+
{ "id": "success_icon", "component": { "Icon": { "name": { "literalString": "send"}, "size": 48.0, "color": "#4CAF50"} } },
129+
{ "id": "success_title", "component": { "Text": { "text": { "path": "actionTitle"}, "usageHint": "h2"} } },
130+
{ "id": "success_message", "component": { "Text": { "text": { "path": "actionMessage"} } } },
131+
{ "id": "back_button_text", "component": { "Text": { "text": { "literalString": "Back to Profile"} } } },
132+
{ "id": "back_button", "component": { "Button": { "child": "back_button_text", "primary": false, "action": { "name": "view_profile", "context": [ { "key": "contactName", "value": { "path": "contactName" } } ] } } } },
133+
{ "id": "success_column", "component": { "Column": { "children": { "explicitList": ["success_icon", "success_title", "success_message", "back_button"]}, "alignment": "center"} } },
134+
{ "id": "message-success-card", "component": { "Card": { "child": "success_column"} } }
135135
]
136136
} },
137137
{ "dataModelUpdate": {
138-
"surfaceId": "action-modal",
138+
"surfaceId": "contact-card",
139139
"path": "/",
140140
"contents": [
141-
{ "key": "actionTitle", "valueString": "Action Confirmation" },
142-
{ "key": "actionMessage", "valueString": "Your action has been processed." }
141+
{ "key": "actionTitle", "valueString": "Message Sent" },
142+
{ "key": "actionMessage", "valueString": "Your message has been sent to." },
143+
{ "key": "contactName", "valueString": "" }
143144
]
144145
} }
145146
]
@@ -152,10 +153,20 @@
152153
"surfaceId": "contact-card",
153154
"components": [
154155
{ "id": "success_icon", "component": { "Icon": { "name": { "literalString": "check_circle"}, "size": 48.0, "color": "#4CAF50"} } } ,
155-
{ "id": "success_text", "component": { "Text": { "text": { "literalString": "Successfully Followed"}, "usageHint": "h2"} } } ,
156-
{ "id": "success_column", "component": { "Column": { "children": { "explicitList": ["success_icon", "success_text"]} , "alignment": "center"} } } ,
156+
{ "id": "success_text", "component": { "Text": { "text": { "path": "followMessage"}, "usageHint": "h2"} } } ,
157+
{ "id": "back_button_text", "component": { "Text": { "text": { "literalString": "Back to Profile"} } } } ,
158+
{ "id": "back_button", "component": { "Button": { "child": "back_button_text", "primary": false, "action": { "name": "view_profile", "context": [ { "key": "contactName", "value": { "path": "contactName" } } ] } } } } ,
159+
{ "id": "success_column", "component": { "Column": { "children": { "explicitList": ["success_icon", "success_text", "back_button"]} , "alignment": "center"} } } ,
157160
{ "id": "success_card", "component": { "Card": { "child": "success_column"} } }
158161
]
162+
} },
163+
{ "dataModelUpdate": {
164+
"surfaceId": "contact-card",
165+
"path": "/",
166+
"contents": [
167+
{ "key": "followMessage", "valueString": "Successfully Followed" },
168+
{ "key": "contactName", "valueString": "" }
169+
]
159170
} }
160171
]
161172
---END FOLLOW_SUCCESS_EXAMPLE---

samples/agent/adk/contact_lookup/a2ui_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
"help",
183183
"home",
184184
"info",
185+
"link",
185186
"locationOn",
186187
"lock",
187188
"lockOpen",

samples/agent/adk/contact_lookup/agent_executor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,11 @@ async def execute(
107107

108108
elif action == "send_message":
109109
contact_name = ctx.get("contactName", "Unknown")
110-
query = f"USER_WANTS_TO_MESSAGE: {contact_name}"
110+
query = f"ACTION: send_message to {contact_name}"
111111

112112
elif action == "follow_contact":
113-
query = "ACTION: follow_contact"
113+
contact_name = ctx.get("contactName", "Unknown")
114+
query = f"ACTION: follow_contact for {contact_name}"
114115

115116
elif action == "view_full_profile":
116117
contact_name = ctx.get("contactName", "Unknown")
Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,43 @@
11
[
2-
{
3-
"id": "1",
4-
"name": "Alex Jordan",
5-
"title": "Product Marketing Manager",
6-
"team": "Team Macally",
7-
"department": "Marketing",
8-
"location": "New York",
9-
"email": "[email protected]",
10-
"mobile": "+1 (415) 171-1080",
11-
"calendar": "Free until 4:00 PM",
12-
"imageUrl": "http://localhost:10002/static/profile1.png"
13-
},
14-
{
15-
"id": "2",
16-
"name": "Casey Smith",
17-
"title": "Digital Marketing Specialist",
18-
"team": "Growth Team",
19-
"department": "Marketing",
20-
"location": "New York",
21-
"email": "[email protected]",
22-
"mobile": "+1 (415) 222-3333",
23-
"calendar": "In a meeting",
24-
"imageUrl": "http://localhost:10002/static/profile2.png"
25-
},
26-
{
27-
"id": "3",
28-
"name": "Jordan Taylor",
29-
"title": "Senior Software Engineer",
30-
"team": "Core Platform",
31-
"department": "Engineering",
32-
"location": "San Francisco",
33-
"email": "[email protected]",
34-
"mobile": "+1 (650) 444-5555",
35-
"calendar": "Focus time",
36-
"imageUrl": "http://localhost:10002/static/profile3.png"
37-
}
38-
]
2+
{
3+
"id": "1",
4+
"name": "Alex Jordan",
5+
"title": "Product Marketing Manager",
6+
"team": "Team Macally",
7+
"department": "Marketing",
8+
"location": "New York",
9+
"email": "[email protected]",
10+
"mobile": "+1 (415) 171-1080",
11+
"calendar": "Free until 4:00 PM",
12+
"meetupPlace": "San Francisco",
13+
"imageUrl": "http://localhost:10002/static/profile1.png",
14+
"favorite_framework": "Angular",
15+
"firstMorningCoffeeSip": "7am"
16+
},
17+
{
18+
"id": "2",
19+
"name": "Casey Smith",
20+
"title": "Digital Marketing Specialist",
21+
"team": "Growth Team",
22+
"department": "Marketing",
23+
"location": "New York",
24+
"email": "[email protected]",
25+
"mobile": "+1 (415) 222-3333",
26+
"calendar": "In a meeting",
27+
"imageUrl": "http://localhost:10002/static/profile2.png",
28+
"githubUrl": "https://github.com/casey-smith"
29+
},
30+
{
31+
"id": "3",
32+
"name": "Jordan Taylor",
33+
"title": "Senior Software Engineer",
34+
"team": "Core Platform",
35+
"department": "Engineering",
36+
"location": "San Francisco",
37+
"email": "[email protected]",
38+
"mobile": "+1 (650) 444-5555",
39+
"calendar": "Focus time",
40+
"imageUrl": "http://localhost:10002/static/profile3.png"
41+
}
42+
]
43+

samples/agent/adk/contact_lookup/prompt_builder.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,37 @@ def get_ui_prompt(base_url: str, examples: str) -> str:
6868
--- UI TEMPLATE RULES ---
6969
- **For finding contacts (e.g., "Who is Alex Jordan?"):**
7070
a. You MUST call the `get_contact_info` tool.
71-
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.).
71+
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).
72+
- Use the 'link' icon if the value is a URL.
73+
- Use the 'calendar_today' icon if the value represents a date, time, or schedule.
74+
- Use the 'location_on' icon if the value represents a location or place.
75+
- Otherwise, use the 'star' icon.
7276
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.
7377
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---[]"
7478
7579
- **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):**
7680
a. You MUST call the `get_contact_info` tool with the specific name.
77-
b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template.
81+
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:
82+
- The 'link' icon for URLs.
83+
- The 'calendar_today' icon for date/time/schedule.
84+
- The 'location_on' icon for location/place.
85+
- The 'star' icon for others.
7886
7987
- **For handling actions (e.g., "follow_contact"):**
8088
a. You MUST use the `FOLLOW_SUCCESS_EXAMPLE` template.
81-
b. This will render a new card with a "Successfully Followed" message.
82-
c. Respond with a text confirmation like "You are now following this contact." along with the JSON.
89+
b. This will render a new card with a "Successfully Followed" message and a "Back" button.
90+
c. Populate the `dataModelUpdate.contents` with:
91+
- `followMessage`: "Successfully followed the contact." (Include the actual contact name at the end)
92+
- `contactName`: The contact's name (for the back button).
93+
d. Respond with a text confirmation like "You are now following this contact." along with the JSON.
94+
95+
- **For handling actions (e.g., "send_message"):**
96+
a. You MUST use the `ACTION_CONFIRMATION_EXAMPLE` template.
97+
b. Populate the `dataModelUpdate.contents` with:
98+
- `actionTitle`: "Message Sent"
99+
- `actionMessage`: "Your message has been sent to the contact." (Include the actual contact name at the end of the string)
100+
- `contactName`: The contact's name (for the back button).
101+
c. Respond with a text confirmation like "Message sent." along with the JSON.
83102
84103
{formatted_examples}
85104

samples/client/angular/projects/contact/src/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
<link
3030
rel="stylesheet"
31-
href="https://fonts.googleapis.com/css2?family=Google+Symbols:opsz,wght,FILL,GRAD,[email protected],100..700,0..1,-50..200,0..100&display=swap&icon_names=calendar_today,call,location_on,mail,progress_activity,send"
31+
href="https://fonts.googleapis.com/css2?family=Google+Symbols:opsz,wght,FILL,GRAD,[email protected],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"
3232
/>
3333
</head>
3434
<body>

samples/client/angular/projects/contact/src/server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ app.use(
3636
maxAge: '1y',
3737
index: false,
3838
redirect: false,
39-
})
39+
}),
4040
);
4141

4242
app.post('/a2a', (req, res) => {
@@ -61,7 +61,7 @@ app.post('/a2a', (req, res) => {
6161
{
6262
kind: 'data',
6363
data: clientEvent,
64-
metadata: { 'mimeType': 'application/json+a2aui' },
64+
metadata: { mimeType: 'application/json+a2aui' },
6565
} as Part,
6666
],
6767
kind: 'message',
@@ -122,7 +122,7 @@ async function fetchWithCustomHeader(url: string | URL | Request, init?: Request
122122

123123
async function createOrGetClient() {
124124
// Create a client pointing to the agent's Agent Card URL.
125-
client ??= await A2AClient.fromCardUrl('http://localhost:10002/.well-known/agent-card.json', {
125+
client ??= await A2AClient.fromCardUrl('http://localhost:10003/.well-known/agent-card.json', {
126126
fetchImpl: fetchWithCustomHeader,
127127
});
128128

samples/client/angular/projects/contact/src/styles.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,19 @@ body {
170170
width: 100svw;
171171
height: 100svh;
172172
}
173+
174+
.g-icon {
175+
font-family: 'Google Symbols';
176+
font-weight: normal;
177+
font-style: normal;
178+
font-size: 24px;
179+
line-height: 1;
180+
letter-spacing: normal;
181+
text-transform: none;
182+
display: inline-block;
183+
white-space: nowrap;
184+
word-wrap: normal;
185+
direction: ltr;
186+
-webkit-font-feature-settings: 'liga';
187+
-webkit-font-smoothing: antialiased;
188+
}

0 commit comments

Comments
 (0)