Skip to content

Commit e3165f7

Browse files
PierrickVouletpierrick
andauthored
feat: add ADK AI Chat agent sample (#276)
Co-authored-by: pierrick <pierrick@google.com>
1 parent 73aabc4 commit e3165f7

File tree

8 files changed

+5550
-0
lines changed

8 files changed

+5550
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"scriptId": "1hAKUi8G9Mynf84ZtZ2Gx8nLD_k-dmWmq4aQKTwURirMcC5BYWOpppD5F"}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright 2025 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the 'License');
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an 'AS IS' BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
////////////////////////////////////////////////////
16+
// --- ADK AI Agent handling logic ---
17+
////////////////////////////////////////////////////
18+
19+
// The prefix used for the User resource name.
20+
const USERS_PREFIX = "users/";
21+
22+
// Sends a request to the AI agent and processes the response using the agent
23+
function requestAgent(userName, input) {
24+
// Sync call that gets all events from agent response
25+
const responseContentText = UrlFetchApp.fetch(
26+
`https://${getLocation()}-aiplatform.googleapis.com/v1/${getReasoningEngine()}:streamQuery?alt=sse`,
27+
{
28+
method: 'post',
29+
headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
30+
contentType: 'application/json',
31+
payload: JSON.stringify({
32+
"class_method": "async_stream_query",
33+
"input": {
34+
"user_id": userName.replace(USERS_PREFIX, ''),
35+
"message": extractContentFromInput(input),
36+
}
37+
}),
38+
muteHttpExceptions: true
39+
}
40+
).getContentText();
41+
42+
// Process the SSE response (one line per event)
43+
const events = responseContentText.split('\n').filter(s => s.trim().length > 0);
44+
console.log(`Received ${events.length} agent events.`);
45+
for (const eventJson of events) {
46+
if (isInDebugMode()) {
47+
console.log("Event: " + eventJson);
48+
}
49+
const event = JSON.parse(eventJson);
50+
51+
// Retrieve the agent responsible for generating the content
52+
const author = event.author;
53+
54+
// Ignore events that are not useful for the end-user
55+
if (!event.content) {
56+
console.log(`${author}: internal event`);
57+
continue;
58+
}
59+
60+
// Handle text answers
61+
const parts = event.content.parts || [];
62+
const textPart = parts.find(p => p.text);
63+
if (textPart) {
64+
const text = textPart.text;
65+
console.log(`${author}: ${text}`);
66+
answer(author, text, event.grounding_metadata);
67+
}
68+
}
69+
}
70+
71+
// Transforms the user input to AI message with contents.
72+
function extractContentFromInput(input) {
73+
// For Chat host apps, the input can contain text and attachments
74+
const parts = [{ "text": input.text }];
75+
if (input.attachment && Array.isArray(input.attachment)) {
76+
for (const attachment of input.attachment) {
77+
parts.push({ "inline_data": {
78+
"mime_type": attachment.contentType,
79+
"data": downloadChatAttachment(
80+
attachment.attachmentDataRef.resourceName
81+
)
82+
}});
83+
}
84+
}
85+
return { "role": "user", "parts": parts };
86+
}
87+
88+
// Sends an answer as a Chat message.
89+
function answer(author, text, groundingMetadata) {
90+
const widgets = getAgentResponseWidgets(author, text, groundingMetadata);
91+
createMessage(buildMessage(author, [wrapWidgetsInCardsV2(widgets)]));
92+
}
93+
94+
// --- Utility functions ---
95+
96+
// Builds a Chat message for the given author and cards_v2.
97+
function buildMessage(author, cardsV2) {
98+
const messageBuilder = CardService.newChatResponseBuilder();
99+
messageBuilder.setText(`${getAuthorEmoji(author)} *${snakeToUserReadable(author)}* ✅`);
100+
cardsV2.forEach(cardV2 => { messageBuilder.addCardsV2(cardV2) });
101+
let message = JSON.parse(messageBuilder.build().printJson());
102+
103+
if(isInDebugMode()) {
104+
console.log(`Built message: ${JSON.stringify(message)}`);
105+
}
106+
107+
return message;
108+
}
109+
110+
// Converts a snake_case_string to a user-readable Title Case string.
111+
function snakeToUserReadable(snakeCaseString = "") {
112+
return snakeCaseString.replace(/_/g, ' ').split(' ').map(word => {
113+
if (!word) return '';
114+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
115+
}).join(' ');
116+
}
117+
118+
// Wraps the given widgets in Chat cards_v2 structure.
119+
function wrapWidgetsInCardsV2(widgets = []) {
120+
const section = CardService.newCardSection();
121+
widgets.forEach(widget => { section.addWidget(widget) });
122+
return CardService.newCardWithId().setCard(CardService.newCardBuilder().addSection(section).build());
123+
}
124+
125+
///////////////////////////////////////////////////////////////
126+
// --- UI rendering logic for the LLM Auditor AI Agent. ---
127+
///////////////////////////////////////////////////////////////
128+
129+
// Returns an emoji representing the author.
130+
function getAuthorEmoji(author) {
131+
switch (author) {
132+
case "critic_agent": return "ℹ️";
133+
case "reviser_agent": return "✏️";
134+
default: return "🤖";
135+
}
136+
}
137+
138+
// Returns the widgets to render for a given agent response.
139+
function getAgentResponseWidgets(name, text, groundingMetadata) {
140+
let widgets = [];
141+
switch (name) {
142+
case "critic_agent":
143+
widgets = createMarkdownAndGroundingWidgets(text, groundingMetadata);
144+
break;
145+
case "reviser_agent":
146+
widgets = createMarkdownWidgets(text);
147+
break;
148+
default:
149+
}
150+
return widgets;
151+
}
152+
153+
// --- Utility functions ---
154+
155+
// Creates widgets for the markdown text and grounding response.
156+
function createMarkdownAndGroundingWidgets(text, groundingMetadata) {
157+
// Remove the references from text
158+
let widgets = createMarkdownWidgets(text.replace(/^\s*```(json)?[\s\S]*?```\s*/i, '').replace(/Reference(s)?:[\s\S]*/i, ''))
159+
// Add sources from grounding data
160+
if (groundingMetadata.grounding_chunks) {
161+
const sourceButtons = CardService.newButtonSet();
162+
for (const groundingChunk of groundingMetadata.grounding_chunks) {
163+
sourceButtons.addButton(CardService.newTextButton()
164+
.setText(groundingChunk.web.domain)
165+
.setOpenLink(CardService.newOpenLink().setUrl(groundingChunk.web.uri)));
166+
}
167+
widgets.push(sourceButtons);
168+
}
169+
return widgets;
170+
}
171+
172+
// Creates widgets for markdown text response.
173+
function createMarkdownWidgets(markdown) {
174+
if (!markdown) return [];
175+
const textParagraph = CardService.newTextParagraph();
176+
textParagraph.setText(new showdown.Converter().makeHtml(markdown));
177+
return [textParagraph];
178+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2025 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the 'License');
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an 'AS IS' BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Service that handles Google Chat operations.
16+
17+
// Handle incoming Google Chat message events, actions will be taken via Google Chat API calls
18+
function onMessage(event) {
19+
if (isInDebugMode()) {
20+
console.log(`Message event received (Chat): ${JSON.stringify(event)}`);
21+
}
22+
// Extract data from the event.
23+
const chatEvent = event.chat;
24+
setChatConfig(chatEvent.messagePayload.space.name);
25+
26+
// Request AI agent to answer the message
27+
requestAgent(chatEvent.user.name, chatEvent.messagePayload.message)
28+
// Respond with an empty response to the Google Chat platform to acknowledge execution
29+
return null;
30+
}
31+
32+
// --- Utility functions ---
33+
34+
// The Chat direct message (DM) space associated with the user
35+
const SPACE_NAME_PROPERTY = "DM_SPACE_NAME"
36+
37+
// Sets the Chat DM space name for subsequent operations.
38+
function setChatConfig(spaceName) {
39+
const userProperties = PropertiesService.getUserProperties();
40+
userProperties.setProperty(SPACE_NAME_PROPERTY, spaceName);
41+
console.log(`Space is set to ${spaceName}`);
42+
}
43+
44+
// Retrieved the Chat DM space name to sent messages to.
45+
function getConfiguredChat() {
46+
const userProperties = PropertiesService.getUserProperties();
47+
return userProperties.getProperty(SPACE_NAME_PROPERTY);
48+
}
49+
50+
// Finds the Chat DM space name between the Chat app and the given user.
51+
function findChatAppDm(userName) {
52+
return Chat.Spaces.findDirectMessage(
53+
{ 'name': userName },
54+
{'Authorization': `Bearer ${getCredentials().getAccessToken()}`}
55+
).name;
56+
}
57+
58+
// Downloads a Chat message attachment and returns its content as a base64 encoded string.
59+
function downloadChatAttachment(attachmentName) {
60+
const response = UrlFetchApp.fetch(
61+
`https://chat.googleapis.com/v1/media/${attachmentName}?alt=media`,
62+
{
63+
method: 'get',
64+
headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
65+
muteHttpExceptions: true
66+
}
67+
);
68+
return Utilities.base64Encode(response.getContent());
69+
}
70+
71+
// Creates a Chat message in the configured space.
72+
function createMessage(message) {
73+
const spaceName = getConfiguredChat();
74+
console.log(`Creating message in space ${spaceName}...`);
75+
return Chat.Spaces.Messages.create(
76+
message,
77+
spaceName,
78+
{},
79+
{'Authorization': `Bearer ${getCredentials().getAccessToken()}`}
80+
).name;
81+
}
82+
83+
// Updates a Chat message in the configured space.
84+
function updateMessage(name, message) {
85+
console.log(`Updating message ${name}...`);
86+
Chat.Spaces.Messages.patch(
87+
message,
88+
name,
89+
{ updateMask: "*" },
90+
{'Authorization': `Bearer ${getCredentials().getAccessToken()}`}
91+
);
92+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2025 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the 'License');
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an 'AS IS' BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Get credentials from service account to access Vertex AI and Google Chat APIs
16+
function getCredentials() {
17+
const credentials = PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_KEY');
18+
if (!credentials) {
19+
throw new Error("SERVICE_ACCOUNT_KEY script property must be set.");
20+
}
21+
const parsedCredentials = JSON.parse(credentials);
22+
return OAuth2.createService("SA")
23+
.setTokenUrl('https://oauth2.googleapis.com/token')
24+
.setPrivateKey(parsedCredentials['private_key'])
25+
.setIssuer(parsedCredentials['client_email'])
26+
.setPropertyStore(PropertiesService.getScriptProperties())
27+
.setScope([
28+
// Vertex AI scope
29+
"https://www.googleapis.com/auth/cloud-platform",
30+
// Google Chat scope
31+
// All Chat operations are taken by the Chat app itself
32+
"https://www.googleapis.com/auth/chat.bot"
33+
]);
34+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2025 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the 'License');
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an 'AS IS' BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Environment variables
16+
17+
const REASONING_ENGINE_RESOURCE_NAME = PropertiesService.getScriptProperties().getProperty('REASONING_ENGINE_RESOURCE_NAME');
18+
19+
// Get reasoning engine resource name
20+
function getReasoningEngine() {
21+
return REASONING_ENGINE_RESOURCE_NAME;
22+
}
23+
24+
const LOCATION = PropertiesService.getScriptProperties().getProperty('LOCATION');
25+
26+
// Get reasoning engine location
27+
function getLocation() {
28+
const parts = REASONING_ENGINE_RESOURCE_NAME.split('/');
29+
const locationIndex = parts.indexOf('locations') + 1;
30+
return parts[locationIndex];
31+
}
32+
33+
const DEBUG = parseInt(PropertiesService.getScriptProperties().getProperty('DEBUG')) || 0;
34+
35+
// Returns whether the application is running in debug mode.
36+
function isInDebugMode() {
37+
return DEBUG == 1
38+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Google Chat ADK AI agent as Google Workspace add-on
2+
3+
Chat app that integrates with an ADK AI agent hosted in Vertex AI Agent Engine.
4+
5+
Please see [tutorial](https://developers.google.com/workspace/add-ons/chat/quickstart-agent).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"timeZone": "America/New_York",
3+
"dependencies": {
4+
"libraries": [
5+
{
6+
"userSymbol": "OAuth2",
7+
"version": "43",
8+
"libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF"
9+
}
10+
],
11+
"enabledAdvancedServices": [
12+
{
13+
"userSymbol": "Chat",
14+
"version": "v1",
15+
"serviceId": "chat"
16+
}
17+
]
18+
},
19+
"exceptionLogging": "STACKDRIVER",
20+
"runtimeVersion": "V8",
21+
"addOns": {
22+
"common": {
23+
"name": "ADK AI Agent Quickstart",
24+
"logoUrl": "https://developers.google.com/workspace/add-ons/images/quickstart-app-avatar.png"
25+
},
26+
"chat": {}
27+
},
28+
"oauthScopes": [
29+
"https://www.googleapis.com/auth/script.external_request"
30+
]
31+
}

0 commit comments

Comments
 (0)