-
Notifications
You must be signed in to change notification settings - Fork 111
feat: add GE quickstart sample #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"scriptId": "1YeDvZtq5c5TU2DXFVCNGjA6RCVkUn5x0ip5QVksmSwgsy7gwpI7izPLt"} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| // Copyright 2026 Google LLC. All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the 'License'); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // https://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an 'AS IS' BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| /////////////////////////////////////////////////////// | ||
| // --- Gemini Enterprise AI Agent handling logic --- | ||
| /////////////////////////////////////////////////////// | ||
|
|
||
| // Sends a request to the AI agent and processes the response | ||
| function requestAgent(input) { | ||
| // Sync call that gets all events from agent response | ||
| const responseContentText = UrlFetchApp.fetch( | ||
| `https://${getLocation()}-discoveryengine.googleapis.com/v1/${getReasoningEngine()}/assistants/default_assistant:streamAssist?alt=sse`, | ||
| { | ||
| method: 'post', | ||
| headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` }, | ||
| contentType: 'application/json', | ||
| payload: JSON.stringify({ | ||
| // Always use a new session | ||
| "session" : null, | ||
| // Only use the message text | ||
| "query": { "text": input.text }, | ||
| "agentsSpec": { "agentSpecs": [{ | ||
| "agentId": getAgentId() | ||
| }]} | ||
| }), | ||
| muteHttpExceptions: true | ||
| } | ||
| ).getContentText(); | ||
|
Comment on lines
+22
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The const response = UrlFetchApp.fetch(
`https://${getLocation()}-discoveryengine.googleapis.com/v1/${getReasoningEngine()}/assistants/default_assistant:streamAssist?alt=sse`,
{
method: 'post',
headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
contentType: 'application/json',
payload: JSON.stringify({
// Always use a new session
"session" : null,
// Only use the message text
"query": { "text": input.text },
"agentsSpec": { "agentSpecs": [{
"agentId": getAgentId()
}]}
}),
muteHttpExceptions: true
}
);
if (response.getResponseCode() < 200 || response.getResponseCode() >= 300) {
console.error(`Agent request failed with status ${response.getResponseCode()}: ${response.getContentText()}`);
answer(getAgentId(), "Something went wrong, check the Apps Script logs for more info.", false);
return;
}
const responseContentText = response.getContentText(); |
||
|
|
||
| // Process the SSE response (one line per event) | ||
| const events = responseContentText.split('\n').map(s => s.replace(/^data:\s*/, '')).filter(s => s.trim().length > 0); | ||
| console.log(`Received ${events.length} agent events.`); | ||
| var answerText = ""; | ||
| for (const eventJson of events) { | ||
| if (isInDebugMode()) { | ||
| console.log("Event: " + eventJson); | ||
| } | ||
| const event = JSON.parse(eventJson); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The call to let event;
try {
event = JSON.parse(eventJson);
} catch (e) {
console.error(`Failed to parse agent event: ${eventJson}`, e);
continue; // Skip malformed event and proceed to the next one.
} |
||
|
|
||
| // Ignore internal events | ||
| if (!event.answer) { | ||
| console.log(`Ignored: internal event`); | ||
| continue; | ||
| } | ||
|
|
||
| // Handle text replies | ||
| const replies = event.answer.replies || []; | ||
| for (const reply of replies) { | ||
| const content = reply.groundedContent.content; | ||
| // Process content if any | ||
| if (content) { | ||
| if (isInDebugMode()) { | ||
| console.log(`Processing content: ${JSON.stringify(content)}`); | ||
| } | ||
| // Ignore thought events | ||
| if (content.thought) { | ||
| console.log(`Ignored: thought event`); | ||
| continue; | ||
| } | ||
| answerText += content.text; | ||
| } | ||
| } | ||
|
|
||
| // Send Chat message to answer user | ||
| if (event.answer.state === "SUCCEEDED") { | ||
| console.log(`Answer text: ${answerText}`); | ||
| answer(getAgentId(), answerText); | ||
| } else if (event.answer.state !== "IN_PROGRESS") { | ||
| answer(getAgentId(), "Something went wrong, check the Apps Script logs for more info.", false); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // --- Utility functions --- | ||
|
|
||
| // Sends an answer as a Chat message. | ||
| function answer(author, text, success) { | ||
| const widgets = createMarkdownWidgets(text); | ||
| createMessage(buildMessage(author, [wrapWidgetsInCardsV2(widgets)], success)); | ||
| } | ||
|
|
||
| // Builds a Chat message for the given author, state, and cards_v2. | ||
| function buildMessage(author, cardsV2, success=true) { | ||
| const messageBuilder = CardService.newChatResponseBuilder(); | ||
| messageBuilder.setText(`${getAuthorEmoji(author)} *${snakeToUserReadable(author)}* ${success ? '✅' : '❌'}`); | ||
| cardsV2.forEach(cardV2 => { messageBuilder.addCardsV2(cardV2) }); | ||
| let message = JSON.parse(messageBuilder.build().printJson()); | ||
|
|
||
| if(isInDebugMode()) { | ||
| console.log(`Built message: ${JSON.stringify(message)}`); | ||
| } | ||
|
|
||
| return message; | ||
| } | ||
|
|
||
| // Converts a snake_case_string to a user-readable Title Case string. | ||
| function snakeToUserReadable(snakeCaseString = "") { | ||
| return snakeCaseString.replace(/_/g, ' ').split(' ').map(word => { | ||
| if (!word) return ''; | ||
| return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); | ||
| }).join(' '); | ||
| } | ||
|
|
||
| // Wraps the given widgets in Chat cards_v2 structure. | ||
| function wrapWidgetsInCardsV2(widgets = []) { | ||
| const section = CardService.newCardSection(); | ||
| widgets.forEach(widget => { section.addWidget(widget) }); | ||
| return CardService.newCardWithId().setCard(CardService.newCardBuilder().addSection(section).build()); | ||
| } | ||
|
|
||
| // Returns an emoji representing the author. | ||
| function getAuthorEmoji(author) { | ||
| switch (author) { | ||
| case "default_idea_generation": return "ℹ️"; | ||
| default: return "🤖"; | ||
| } | ||
| } | ||
|
|
||
| // Creates widgets for markdown text response. | ||
| function createMarkdownWidgets(markdown) { | ||
| if (!markdown) return []; | ||
| const textParagraph = CardService.newTextParagraph(); | ||
| textParagraph.setText(new showdown.Converter().makeHtml(markdown)); | ||
| return [textParagraph]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // Copyright 2026 Google LLC. All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the 'License'); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // https://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an 'AS IS' BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Service that handles Google Chat operations. | ||
|
|
||
| // Handle incoming Google Chat message events, actions will be taken via Google Chat API calls | ||
| function onMessage(event) { | ||
| if (isInDebugMode()) { | ||
| console.log(`MESSAGE event received (Chat): ${JSON.stringify(event)}`); | ||
| } | ||
| // Extract data from the event. | ||
| const chatEvent = event.chat; | ||
| setChatConfig(chatEvent.messagePayload.space.name); | ||
|
|
||
| // Request AI agent to answer the message | ||
| requestAgent(chatEvent.messagePayload.message) | ||
| // Respond with an empty response to the Google Chat platform to acknowledge execution | ||
| return null; | ||
|
Comment on lines
+27
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| } | ||
|
|
||
| // --- Utility functions --- | ||
|
|
||
| // The Chat direct message (DM) space associated with the user | ||
| const SPACE_NAME_PROPERTY = "DM_SPACE_NAME" | ||
|
|
||
| // Sets the Chat DM space name for subsequent operations. | ||
| function setChatConfig(spaceName) { | ||
| const userProperties = PropertiesService.getUserProperties(); | ||
| userProperties.setProperty(SPACE_NAME_PROPERTY, spaceName); | ||
| console.log(`Space is set to ${spaceName}`); | ||
| } | ||
|
|
||
| // Retrieved the Chat DM space name to sent messages to. | ||
| function getConfiguredChat() { | ||
| const userProperties = PropertiesService.getUserProperties(); | ||
| return userProperties.getProperty(SPACE_NAME_PROPERTY); | ||
| } | ||
|
|
||
| // Finds the Chat DM space name between the Chat app and the given user. | ||
| function findChatAppDm(userName) { | ||
| return Chat.Spaces.findDirectMessage( | ||
| { 'name': userName }, | ||
| {'Authorization': `Bearer ${getCredentials().getAccessToken()}`} | ||
| ).name; | ||
| } | ||
|
|
||
| // Creates a Chat message in the configured space. | ||
| function createMessage(message) { | ||
| const spaceName = getConfiguredChat(); | ||
| console.log(`Creating message in space ${spaceName}...`); | ||
| return Chat.Spaces.Messages.create( | ||
| message, | ||
| spaceName, | ||
| {}, | ||
| {'Authorization': `Bearer ${getCredentials().getAccessToken()}`} | ||
| ).name; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // Copyright 2026 Google LLC. All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the 'License'); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // https://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an 'AS IS' BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Get credentials from service account to access Gemini Enterprise and Google Chat APIs | ||
| function getCredentials() { | ||
| const credentials = PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_KEY'); | ||
| if (!credentials) { | ||
| throw new Error("SERVICE_ACCOUNT_KEY script property must be set."); | ||
| } | ||
| const parsedCredentials = JSON.parse(credentials); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The call to let parsedCredentials;
try {
parsedCredentials = JSON.parse(credentials);
} catch (e) {
throw new Error('Failed to parse SERVICE_ACCOUNT_KEY. Please ensure it is valid JSON.');
} |
||
| return OAuth2.createService("SA") | ||
| .setTokenUrl('https://oauth2.googleapis.com/token') | ||
| .setPrivateKey(parsedCredentials['private_key']) | ||
| .setIssuer(parsedCredentials['client_email']) | ||
| .setPropertyStore(PropertiesService.getScriptProperties()) | ||
| .setScope([ | ||
| // Gemini Enterprise Assitant scope | ||
| "https://www.googleapis.com/auth/discoveryengine.assist.readwrite", | ||
| // Google Chat scope | ||
| // All Chat operations are taken by the Chat app itself | ||
| "https://www.googleapis.com/auth/chat.bot" | ||
| ]); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Copyright 2026 Google LLC. All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the 'License'); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // https://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an 'AS IS' BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Environment variables | ||
|
|
||
| const REASONING_ENGINE_RESOURCE_NAME = PropertiesService.getScriptProperties().getProperty('REASONING_ENGINE_RESOURCE_NAME'); | ||
|
|
||
| // Get reasoning engine resource name | ||
| function getReasoningEngine() { | ||
| return REASONING_ENGINE_RESOURCE_NAME; | ||
| } | ||
|
|
||
| const LOCATION = PropertiesService.getScriptProperties().getProperty('LOCATION'); | ||
|
|
||
| // Get reasoning engine location | ||
| function getLocation() { | ||
| const parts = REASONING_ENGINE_RESOURCE_NAME.split('/'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The if (!REASONING_ENGINE_RESOURCE_NAME) {
throw new Error('Script property "REASONING_ENGINE_RESOURCE_NAME" must be set.');
}
const parts = REASONING_ENGINE_RESOURCE_NAME.split('/'); |
||
| const locationIndex = parts.indexOf('locations') + 1; | ||
| return parts[locationIndex]; | ||
|
Comment on lines
+29
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic to extract the location assumes that the substring 'locations' is always present in the resource name. If const locationIndex = parts.indexOf('locations');
if (locationIndex === -1 || locationIndex + 1 >= parts.length) {
throw new Error(`Could not determine location from resource name: ${REASONING_ENGINE_RESOURCE_NAME}`);
}
return parts[locationIndex + 1]; |
||
| } | ||
|
|
||
| const DEBUG = parseInt(PropertiesService.getScriptProperties().getProperty('DEBUG')) || 0; | ||
|
|
||
| // Returns whether the application is running in debug mode. | ||
| function isInDebugMode() { | ||
| return DEBUG == 1 | ||
| } | ||
|
|
||
| const AGENT_ID = PropertiesService.getScriptProperties().getProperty('AGENT_ID') || "default_idea_generation"; | ||
|
|
||
| // Get Gemini Enterprise agent ID | ||
| function getAgentId() { | ||
| return AGENT_ID; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Google Chat Gemini Enterprise AI agent as Google Workspace add-on | ||
|
|
||
| Chat app that integrates with a Gemini Enterprise AI agent. | ||
|
|
||
| Please see [tutorial](https://developers.google.com/workspace/add-ons/chat/quickstart-ge-agent). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| { | ||
| "timeZone": "America/New_York", | ||
| "dependencies": { | ||
| "libraries": [ | ||
| { | ||
| "userSymbol": "OAuth2", | ||
| "version": "43", | ||
| "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" | ||
| } | ||
| ], | ||
| "enabledAdvancedServices": [ | ||
| { | ||
| "userSymbol": "Chat", | ||
| "version": "v1", | ||
| "serviceId": "chat" | ||
| } | ||
| ] | ||
| }, | ||
| "exceptionLogging": "STACKDRIVER", | ||
| "runtimeVersion": "V8", | ||
| "addOns": { | ||
| "common": { | ||
| "name": "GE AI Agent Quickstart", | ||
| "logoUrl": "https://developers.google.com/workspace/add-ons/images/quickstart-app-avatar.png" | ||
| }, | ||
| "chat": {} | ||
| }, | ||
| "oauthScopes": [ | ||
| "https://www.googleapis.com/auth/script.external_request" | ||
| ] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Committing the
.clasp.jsonfile with a hardcodedscriptIdis not a recommended practice for collaborative projects. If another developer clones this repository and pushes changes, they could accidentally overwrite the shared Apps Script project. To prevent this, this file should be added to.gitignore, and a template file (e.g.,.clasp.json.example) should be provided instead, with instructions for developers to create their own local.clasp.json.