Skip to content

Commit 6776750

Browse files
PierrickVouletpierrick
andauthored
feat: add Vertex AI sample (#310)
Co-authored-by: pierrick <pierrick@google.com>
1 parent f2b3398 commit 6776750

File tree

9 files changed

+5649
-0
lines changed

9 files changed

+5649
-0
lines changed

apps-script/vertex-ai/.clasp.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"scriptId": "1NKZZ1hVxkktQnpcIJTKDObk4InxeBXgfP9OtjAi7K3-he4hVYaB8aKPq"
3+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2026 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 Vertex AI Agent operations.
16+
17+
// Submits a query to the AI agent and returns the response string synchronously
18+
function queryAgent(input) {
19+
let systemPrompt = "SYSTEM PROMPT START Do not respond with tables but use bullet points instead." +
20+
" Do not ask the user follow-up questions or converse with them as history is not kept in this interface." +
21+
" SYSTEM PROMPT END\n\n";
22+
23+
const requestPayload = {
24+
"class_method": "async_stream_query",
25+
"input": {
26+
"user_id": "vertex_ai_add_on",
27+
"message": { "role": "user", "parts": [{ "text": systemPrompt + input.text }] },
28+
"state_delta": {
29+
"enterprise-ai_999": `${ScriptApp.getOAuthToken()}`
30+
}
31+
}
32+
};
33+
34+
const responseContentText = UrlFetchApp.fetch(
35+
`https://${getLocation()}-aiplatform.googleapis.com/v1/${getReasoningEngine()}:streamQuery?alt=sse`,
36+
{
37+
method: 'post',
38+
headers: { 'Authorization': `Bearer ${ScriptApp.getOAuthToken()}` },
39+
contentType: 'application/json',
40+
payload: JSON.stringify(requestPayload),
41+
muteHttpExceptions: true
42+
}
43+
).getContentText();
44+
45+
if (isInDebugMode()) {
46+
console.log(`Response: ${responseContentText}`);
47+
}
48+
49+
const events = responseContentText.split('\n').map(s => s.replace(/^data:\s*/, '')).filter(s => s.trim().length > 0);
50+
console.log(`Received ${events.length} agent events.`);
51+
52+
let author = "default";
53+
let answerText = "";
54+
for (const eventJson of events) {
55+
if (isInDebugMode()) {
56+
console.log("Event: " + eventJson);
57+
}
58+
const event = JSON.parse(eventJson);
59+
60+
// Retrieve the agent responsible for generating the content
61+
author = event.author;
62+
63+
// Ignore events that are not useful for the end-user
64+
if (!event.content) {
65+
console.log(`${author}: internal event`);
66+
continue;
67+
}
68+
69+
// Handle text answers
70+
const parts = event.content.parts || [];
71+
const textPart = parts.find(p => p.text);
72+
if (textPart) {
73+
answerText += textPart.text;
74+
}
75+
}
76+
return { author: author, text: answerText };
77+
}
78+
79+
// --- UI Management ---
80+
81+
// Sends an answer as a Chat message.
82+
function answer(author, text, success) {
83+
const widgets = createMarkdownWidgets(text);
84+
createMessage(buildMessage(author, [wrapWidgetsInCardsV2(widgets)], success));
85+
}
86+
87+
// Sends a request to the AI agent and processes the response for Chat UI
88+
function requestAgent(input) {
89+
try {
90+
const response = queryAgent(input);
91+
if (response.text) {
92+
answer(response.author, response.text, true);
93+
}
94+
} catch (err) {
95+
answer(response.author, err.message, false);
96+
}
97+
}
98+
99+
// Builds a Chat message for the given author, state, and cards_v2.
100+
function buildMessage(author, cardsV2, success = true) {
101+
const messageBuilder = CardService.newChatResponseBuilder();
102+
messageBuilder.setText(`${getAuthorEmoji(author)} *${snakeToUserReadable(author)}* ${success ? '✅' : '❌'}`);
103+
cardsV2.forEach(cardV2 => { messageBuilder.addCardsV2(cardV2) });
104+
let message = JSON.parse(messageBuilder.build().printJson());
105+
106+
if (isInDebugMode()) {
107+
console.log(`Built message: ${JSON.stringify(message)}`);
108+
}
109+
110+
return message;
111+
}
112+
113+
// Converts a snake_case_string to a user-readable Title Case string.
114+
function snakeToUserReadable(snakeCaseString = "") {
115+
return snakeCaseString.replace(/_/g, ' ').split(' ').map(word => {
116+
if (!word) return '';
117+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
118+
}).join(' ');
119+
}
120+
121+
// Wraps the given widgets in Chat cards_v2 structure.
122+
function wrapWidgetsInCardsV2(widgets = []) {
123+
const section = CardService.newCardSection();
124+
widgets.forEach(widget => { section.addWidget(widget) });
125+
return CardService.newCardWithId().setCard(CardService.newCardBuilder().addSection(section).build());
126+
}
127+
128+
// Returns an emoji representing the author.
129+
function getAuthorEmoji(author) {
130+
switch (author) {
131+
case "enterprise_ai": return "ℹ️";
132+
default: return "🤖";
133+
}
134+
}
135+
136+
// Creates widgets for markdown text response.
137+
function createMarkdownWidgets(markdown) {
138+
if (!markdown) return [];
139+
const textParagraph = CardService.newTextParagraph();
140+
textParagraph.setText(new showdown.Converter().makeHtml(markdown));
141+
return [textParagraph];
142+
}

apps-script/vertex-ai/Chat.gs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2026 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.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 ${getAddonCredentials().getAccessToken()}` }
55+
).name;
56+
}
57+
58+
// Creates a Chat message in the configured space.
59+
function createMessage(message) {
60+
const spaceName = getConfiguredChat();
61+
console.log(`Creating message in space ${spaceName}...`);
62+
return Chat.Spaces.Messages.create(
63+
message,
64+
spaceName,
65+
{},
66+
{ 'Authorization': `Bearer ${getAddonCredentials().getAccessToken()}` }
67+
).name;
68+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2026 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 getAddonCredentials() {
17+
const credentials = PropertiesService.getScriptProperties().getProperty('APP_SERVICE_ACCOUNT_KEY');
18+
if (!credentials) {
19+
throw new Error("APP_SERVICE_ACCOUNT_KEY script property must be set.");
20+
}
21+
const parsedCredentials = JSON.parse(credentials);
22+
return OAuth2.createService("AppSA")
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
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+
}

apps-script/vertex-ai/Env.gs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2026 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+
}

apps-script/vertex-ai/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Vertex AI Google Workspace add-on
2+
3+
**Note:** This sample is part of an official Google Codelab ([link pending](#)).
4+
5+
Integrates with a custom ADK agent hosted in Vertex AI Agent Engine.
6+
7+
## Supported Applications
8+
9+
- **Gmail**: Adds a contextual sidebar to the inbox. When an email is selected, the Add-on extracts its subject and body natively and passes it as context to the ADK agent, allowing users to ask questions in its specific context.
10+
11+
- **Google Chat**: Adds a chat app integration allowing users to converse directly with the ADK agent natively within a Chat DM space.

0 commit comments

Comments
 (0)