From 985df46f9079d3625a6c4d0db5834fc1cb394220 Mon Sep 17 00:00:00 2001 From: Salaheddine Hamadi Date: Thu, 20 Feb 2025 18:38:20 +0000 Subject: [PATCH 1/2] feat(authentication): Add support for app authentication. The current changes allows user to choose two modes of authentication when using the demo. App authentication and user authentication. To use app authentication, the user will need to select a newly added checkbox. --- apps-script/incident-response/ChatApi.gs | 32 ++++ .../ChatApiAppCredentials.gs | 121 +++++++++++++++ ...eCreator.gs => ChatApiHumanCredentials.gs} | 18 +-- apps-script/incident-response/ChatApp.gs | 2 +- apps-script/incident-response/Consts.gs | 13 +- apps-script/incident-response/DocsApi.gs | 2 +- apps-script/incident-response/GeminiApi.gs | 66 ++++++++ apps-script/incident-response/Index.html | 8 +- apps-script/incident-response/JavaScript.html | 100 ++++++------ apps-script/incident-response/README.md | 89 ++++++++++- apps-script/incident-response/Stylesheet.html | 142 +++++++++--------- apps-script/incident-response/VertexAiApi.gs | 55 ------- .../incident-response/WebController.gs | 2 +- apps-script/incident-response/appsscript.json | 69 ++++----- 14 files changed, 488 insertions(+), 231 deletions(-) create mode 100644 apps-script/incident-response/ChatApi.gs create mode 100644 apps-script/incident-response/ChatApiAppCredentials.gs rename apps-script/incident-response/{ChatSpaceCreator.gs => ChatApiHumanCredentials.gs} (81%) create mode 100644 apps-script/incident-response/GeminiApi.gs delete mode 100644 apps-script/incident-response/VertexAiApi.gs diff --git a/apps-script/incident-response/ChatApi.gs b/apps-script/incident-response/ChatApi.gs new file mode 100644 index 00000000..33e038f8 --- /dev/null +++ b/apps-script/incident-response/ChatApi.gs @@ -0,0 +1,32 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * http://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. + */ +/** + * Creates a space in Google Chat with the provided title and members, and posts an + * initial message to it. + * + * @param {Object} formData the data submitted by the user. It should contain the fields + * title, description, and users. + * @return {string} the resource name of the new space. + */ +function handleIncident(formData) { + console.log(formData) + const appCredentialsMode = formData.appCredentials; // Get the appCredentials element + if(appCredentialsMode){ + return handleIncidentWithAppCredentials(formData) + } else{ + return handleIncidentWithHumanCredentials(formData) + } +} \ No newline at end of file diff --git a/apps-script/incident-response/ChatApiAppCredentials.gs b/apps-script/incident-response/ChatApiAppCredentials.gs new file mode 100644 index 00000000..43fbd9d0 --- /dev/null +++ b/apps-script/incident-response/ChatApiAppCredentials.gs @@ -0,0 +1,121 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * http://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. + */ + +function handleIncidentWithAppCredentials(formData) { + const users = formData.users.trim().length > 0 ? formData.users.split(',') : []; + const spaceName = createChatSpaceWithAppCredentials(formData.title); + createHumanMembershipWithAppCredentials(spaceName, getUserEmail()); + for (const user of users ){ + createHumanMembershipWithAppCredentials(spaceName, user); + } + createMessageWithAppCredentials(spaceName, formData.description); + return spaceName; +} + + + +function createChatSpaceWithAppCredentials(spaceName) { + try { + const service = getService_(); + if (!service.hasAccess()) { + console.error(service.getLastError()); + return; + } + // for private apps, the alias can be used + const my_customer_alias = "customers/my_customer" + // Specify the space to create. + const space = { + displayName: spaceName, + spaceType: 'SPACE', + customer: my_customer_alias + }; + // Call Chat API with a service account to create a message. + const createdSpace = Chat.Spaces.create( + space, + {}, + // Authenticate with the service account token. + {'Authorization': 'Bearer ' + service.getAccessToken()}); + // Log details about the created message. + console.log(createdSpace); + return createdSpace.name; + } catch (err) { + // TODO (developer) - Handle exception. + console.log('Failed to create space with error %s', err.message); + } +} + +function createMessageWithAppCredentials(spaceName, message) { + try { + const service = getService_(); + if (!service.hasAccess()) { + console.error(service.getLastError()); + return; + } + + // Call Chat API with a service account to create a message. + const result = Chat.Spaces.Messages.create( + {'text': message}, + spaceName, + {}, + // Authenticate with the service account token. + {'Authorization': 'Bearer ' + service.getAccessToken()}); + + // Log details about the created message. + console.log(result); + } catch (err) { + // TODO (developer) - Handle exception. + console.log('Failed to create message with error %s', err.message); + } +} + +function createHumanMembershipWithAppCredentials(spaceName, email){ + try{ + const service = getService_(); + if (!service.hasAccess()) { + console.error(service.getLastError()); + return; + } + const membership = { + member: { + // TODO(developer): Replace USER_NAME here + name: 'users/'+email, + // User type for the membership + type: 'HUMAN' + } + }; + const result = Chat.Spaces.Members.create( + membership, + spaceName, + {}, + {'Authorization': 'Bearer ' + service.getAccessToken()} + ); + console.log(result) + } catch (err){ + console.log('Failed to create membership with error %s', err.message) + } + +} + + +function getService_() { + return OAuth2.createService(APP_CREDENTIALS.client_email) + .setTokenUrl('https://oauth2.googleapis.com/token') + .setPrivateKey(APP_CREDENTIALS.private_key) + .setIssuer(APP_CREDENTIALS.client_email) + .setSubject(APP_CREDENTIALS.client_email) + .setScope(APP_CREDENTIALS_SCOPES) + .setPropertyStore(PropertiesService.getScriptProperties()); +} \ No newline at end of file diff --git a/apps-script/incident-response/ChatSpaceCreator.gs b/apps-script/incident-response/ChatApiHumanCredentials.gs similarity index 81% rename from apps-script/incident-response/ChatSpaceCreator.gs rename to apps-script/incident-response/ChatApiHumanCredentials.gs index 7b7f45aa..db19957b 100644 --- a/apps-script/incident-response/ChatSpaceCreator.gs +++ b/apps-script/incident-response/ChatApiHumanCredentials.gs @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,11 @@ * title, description, and users. * @return {string} the resource name of the new space. */ -function createChatSpace(formData) { +function handleIncidentWithHumanCredentials(formData) { const users = formData.users.trim().length > 0 ? formData.users.split(',') : []; - const spaceName = setUpSpace_(formData.title, users); - addAppToSpace_(spaceName); - createMessage_(spaceName, formData.description); + const spaceName = setUpSpaceWithHumanCredentials(formData.title, users); + addAppToSpaceWithHumanCredentials(spaceName); + createMessageWithHumanCredentials(spaceName, formData.description); return spaceName; } @@ -36,7 +36,7 @@ function createChatSpace(formData) { * * @return {string} the resource name of the new space. */ -function setUpSpace_(displayName, users) { +function setUpSpaceWithHumanCredentials(displayName, users) { const memberships = users.map(email => ({ member: { name: `users/${email}`, @@ -61,7 +61,7 @@ function setUpSpace_(displayName, users) { * * @return {string} the resource name of the new membership. */ -function addAppToSpace_(spaceName) { +function addAppToSpaceWithHumanCredentials(spaceName) { const request = { member: { name: "users/app", @@ -78,7 +78,7 @@ function addAppToSpace_(spaceName) { * * @return {string} the resource name of the new message. */ -function createMessage_(spaceName, text) { +function createMessageWithHumanCredentials(spaceName, text) { const request = { text: text }; @@ -87,4 +87,4 @@ function createMessage_(spaceName, text) { return message.name; } -// [END chat_incident_response_space_creator] +// [END chat_incident_response_space_creator] \ No newline at end of file diff --git a/apps-script/incident-response/ChatApp.gs b/apps-script/incident-response/ChatApp.gs index 6d6e9da4..5d034b55 100644 --- a/apps-script/incident-response/ChatApp.gs +++ b/apps-script/incident-response/ChatApp.gs @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/apps-script/incident-response/Consts.gs b/apps-script/incident-response/Consts.gs index 0b363969..dab5df44 100644 --- a/apps-script/incident-response/Consts.gs +++ b/apps-script/incident-response/Consts.gs @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,11 @@ */ // [START chat_incident_response_consts] -const PROJECT_ID = 'replace-with-your-project-id'; -const VERTEX_AI_LOCATION_ID = 'us-central1'; -const CLOSE_INCIDENT_COMMAND_ID = 1; +const PROJECT_ID = PROJECT_ID'; +const CLOSE_INCIDENT_COMMAND_ID = 3; +const APP_CREDENTIALS = APP_CREDENTIALS; +const APP_CREDENTIALS_SCOPES = 'https://www.googleapis.com/auth/chat.bot https://www.googleapis.com/auth/chat.app.memberships https://www.googleapis.com/auth/chat.app.spaces.create'; +const GEMINI_API_KEY = GEMINI_API_KEY; -// [END chat_incident_response_consts] + +// [END chat_incident_response_consts] \ No newline at end of file diff --git a/apps-script/incident-response/DocsApi.gs b/apps-script/incident-response/DocsApi.gs index f75ffde2..341477c1 100644 --- a/apps-script/incident-response/DocsApi.gs +++ b/apps-script/incident-response/DocsApi.gs @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/apps-script/incident-response/GeminiApi.gs b/apps-script/incident-response/GeminiApi.gs new file mode 100644 index 00000000..d5d331e4 --- /dev/null +++ b/apps-script/incident-response/GeminiApi.gs @@ -0,0 +1,66 @@ +/** + * Copyright 2025 Google LLC + * + * 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 + * + * http://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. + */ +// [START chat_incident_response_gemini] + +/** + * Summarizes a Chat conversation using the Vertex AI text prediction API. + * + * @param {string} chatHistory The Chat history that will be summarized. + * @return {string} The content from the text prediction response. + */ +function summarizeChatHistory_(chatHistory) { + const prompt = + "Summarize the following conversation between Engineers resolving an incident" + + " in a few sentences. Use only the information from the conversation.\n\n" + + chatHistory; + + const payload = { + "contents": [{ + "parts": [{ + "text": prompt + }] + }] + }; + + const fetchOptions = { + method: 'POST', + contentType: 'application/json', + payload: JSON.stringify(payload) + } + const gemini_endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${GEMINI_API_KEY}`; + + try { + var response = UrlFetchApp.fetch(gemini_endpoint, fetchOptions); + var responseCode = response.getResponseCode(); + var responseBody = response.getContentText(); + + if (responseCode === 200) { + var json = JSON.parse(responseBody); + if (json.candidates && json.candidates.length > 0 && json.candidates[0].content && json.candidates[0].content.parts && json.candidates[0].content.parts.length > 0) { + return json.candidates[0].content.parts[0].text; + } else { + return "Gemini API: Unexpected response structure."; + } + } else { + return "Gemini API Error: " + responseCode + " - " + responseBody; + } + } catch (e) { + console.log("Error during API call: " + e.toString()) + return 'Gemini API error, please check logs.' + } +} + +// [END chat_incident_response_gemini] \ No newline at end of file diff --git a/apps-script/incident-response/Index.html b/apps-script/incident-response/Index.html index 389e6fe6..6cdee34f 100644 --- a/apps-script/incident-response/Index.html +++ b/apps-script/incident-response/Index.html @@ -1,5 +1,5 @@ + \ No newline at end of file diff --git a/apps-script/incident-response/JavaScript.html b/apps-script/incident-response/JavaScript.html index 712686b8..4bcde3f1 100644 --- a/apps-script/incident-response/JavaScript.html +++ b/apps-script/incident-response/JavaScript.html @@ -1,5 +1,5 @@ - + var formDiv = document.getElementById('form'); + var outputDiv = document.getElementById('output'); + var clearDiv = document.getElementById('clear'); + + function handleFormSubmit(formObject) { + event.preventDefault(); + outputDiv.innerHTML = 'Please wait while we create the space...'; + hide(formDiv); + show(outputDiv); + google.script.run + .withSuccessHandler(updateOutput) + .withFailureHandler(onFailure) + .handleIncident(formObject); + } + + function updateOutput(response) { + var spaceId = response.replace('spaces/', ''); + outputDiv.innerHTML = + '

Space created!

Open space

'; + show(outputDiv); + show(clearDiv); + } + + function onFailure(error) { + outputDiv.innerHTML = 'ERROR: ' + error.message; + outputDiv.classList.add('error'); + show(outputDiv); + show(clearDiv); + } + + function onReset() { + outputDiv.innerHTML = ''; + outputDiv.classList.remove('error'); + show(formDiv); + hide(outputDiv); + hide(clearDiv); + } + + function hide(element) { + element.classList.add('hidden'); + } + + function show(element) { + element.classList.remove('hidden'); + } + + \ No newline at end of file diff --git a/apps-script/incident-response/README.md b/apps-script/incident-response/README.md index cbe03ce3..4663e9ad 100644 --- a/apps-script/incident-response/README.md +++ b/apps-script/incident-response/README.md @@ -18,7 +18,7 @@ After the response team resolves the incident, they use a slash command to automatically create a post-mortem in Google Docs. The app adds a user-provided description of the incident resolution, a transcript of the Chat conversation, and a summary of the conversation (automatically -generated using [Vertex AI](https://cloud.google.com/vertex-ai)) to the post-mortem. +generated using [Gemini AI](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference)) to the post-mortem. ## Tutorial @@ -79,7 +79,7 @@ the following APIs: 1. Google Chat API 1. Google Docs API 1. Admin SDK API - 1. Vertex AI API + 1. Gemini AI API 1. Confirm that you're enabling the APIs in the correct project, then click **Next**. 1. Confirm that you're enabling the correct APIs, then click **Enable**. 1. Optionally, to enable the APIs manually: @@ -88,6 +88,13 @@ the following APIs: 1. Click **+ Enable APIs And Services**. 1. Search for each required API, click it, then click **Enable**. +### 4. Create API key + +To use Gemini API, you need to have an API key created: +1. In the Google Cloud ocnsole, click **Menu** > **APIs & Services** > **Credentials** +1. Click **CREATE CRENDETIALAS**, then select **API key**. +1. Create the key + ### 4. Set up authentication and authorization Authentication and authorization lets the Google Chat app access resources in @@ -103,6 +110,8 @@ replace placeholder information with real information. To learn more about authentication in Google Chat, see [Authenticate and authorize Chat apps and Google Chat API requests](https://developers.devsite.corp.google.com/chat/api/guides/auth). +#### 4.1 Authenticate as a user configuration + 1. In the Google Cloud console, go to **Menu** > **APIs & Services** > [**OAuth consent screen**](https://console.cloud.google.com/apis/credentials/consent). 1. Under User type, select **Internal**, then click **Create**. @@ -127,6 +136,68 @@ Chat apps and Google Chat API requests](https://developers.devsite.corp.google.c 1. Click **Save and Continue**. 1. Review the app registration summary, then click **Back to Dashboard**. +#### 4.2 Authenticate as a app configuration + +This is available in [Developer Preview](/workspace/preview) only. + +##### 4.2.1 setup service account and key + +1. In the Google Cloud console, go to **Menu* > **IAM & Admin** > **Service Accounts** +1. Click **Create service account**. +1. Fill in the service account details, then click **Create and continue**. +1. Click **Continue** +1. Click **Done**. Make a note of the email address for the service account. +1. Create private key: + 1. In the Google Cloud console, go to **Menu** > **IAM & Admin** > **Service Accounts**. + [Go to Service Accounts]({{console_url}}iam-admin/serviceaccounts){: + class="button button-blue" + target="console"} + 1. Select your service account. + 1. Click **Keys** > **Add key** > **Create new key**. + 1. Select **JSON**, then click **Create**. + 1. Click **Close**. + +##### 4.2.2 Receive administrator approval + +To use an authorization scope that begins with +`https://www.googleapis.com/auth/chat.app.*`, which are available as part of +a Developer Preview, your Chat app must get a one-time +[administrator approval](https://support.google.com/a?p=chat-app-auth). + +To use the `https://www.googleapis.com/auth/chat.bot` authorization scope, +no administrator approval is required. + +To receive administrator approval, you must prepare your chat app's service account with the following +information: + +* A Google Workspace Marketplace-compatible OAuth client +* App configuration in the Google Workspace Marketplace SDK. + +1. In the Google Cloud console, go to **Menu** > **IAM & Admin** > **Service Accounts**. +1. Click the service account you created for your Chat app. +1. Click **Advanced Setting**. +1. Click **Create Google Workspace Marketplace-compatible OAuth client**. +1. Click **Continue**. + +A confirmation message appears that says a Google Workspace Marketplace-compatible OAuth client has been created. +After that, the Chap app should be configured in **Goolge Workspace Marketplace SDK**. + +1. In the Google Cloud console, enable the Google Workspace Marketplace SDK. +1. In the Google Cloud console, go to go to **Menu** > **APIs & Services** > **Enabled APIs & services** > **Google Workspace Marketplace SDK** > **App Configuration**. +1. Complete the App Configuration page. How you configure your Chat app depends on who your intended audience is and other factors. For help completing the app configuration page, see Configure your app in the Google Workspace Marketplace SDK. For the purposes of this guide, enter the following information: + 1. Under **App visibility**, select **Private** + 1. Under **Installation settings**, select **Individual + admin install**. + 1. Under **App integrations**, select **Chat app**. + 1. Under **OAuth scopes**, enter all the scopes with ``https://www.googleapis.com/auth/chat.app.*`` + 1. Under **Developer information**, enter your **Developer name**, **Developer website URL**, and **Developer email**. + 1. Click **Save draft** + 1. [Set up authorization Chat app](https://support.google.com/a?p=chat-app-auth). + +**Warning**: This example uses an exported service account key for simplicity's sake. Exporting a private key is not recommended +in production because it shouldn't be stored in an insecure location, such as source control. To learn more about secure service account +implementations and best practices, see Choose when to use service accounts. + + ### 5. Create an Apps Script project and connect it to the Google Cloud project Before creating the Apps Script project, copy your Google Cloud project number. @@ -164,6 +235,14 @@ To enable the Admin SDK Directory service: 1. In **Identifier**, select **AdminDirectory**. 1. Click **Add**. +To enable [Oauth2 for Apps Script library](https://github.com/googleworkspace/apps-script-oauth2): + +1. At the left, click **Editor** code. +1. At the left, next to **Libraries**, click **Add a library +**. +1. Enter the script ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`. +1. Click **Look up**, then click **Add**. + + Now that you've created and configured a Google Cloud project and Apps Script project, you're ready to copy the code into your project and run the sample. @@ -175,6 +254,8 @@ you're ready to copy the code into your project and run the sample. 1. In the file `Consts.js`, replace the value of the `PROJECT_ID` with the ID (not the number) of your GCP Project, which you can copy from the [Google Cloud Console](https://console.cloud.google.com/). +1. In the file `Consts.js`, replace `APP_CREDENTIALS` with the content of the `credentials.json` that you created earlier. +1. In the file `Consts.js`, replace `GEMINI_API_KEY` with the API key you created earlier. 1. Click **Deploy** > **New deployment**. 1. Click the icon besides **Select type** and select both **Wep app** and **Add-on**. 1. Name your deployment and click **Deploy**. @@ -211,3 +292,7 @@ The Chat app is ready to respond to messages. ### 8. Open the web page Navigate to the **Web app URL** from the Apps Script deployment to test your app. + +#### 8.1 app auth mode +If the checkbox is selected, the Chat app will use app authentication to create the space, add members and post the message to the space. +If the checkbox is not selected, the Chat app will use human credentials instead and all the actions will be done on behalf of the user. diff --git a/apps-script/incident-response/Stylesheet.html b/apps-script/incident-response/Stylesheet.html index 2e49ec41..35dd5392 100644 --- a/apps-script/incident-response/Stylesheet.html +++ b/apps-script/incident-response/Stylesheet.html @@ -1,5 +1,5 @@ - + * { + box-sizing: border-box; + } + body { + font-family: Roboto, Arial, Helvetica, sans-serif; + } + div.container { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; bottom: 0; left: 0; right: 0; + } + div.content { + width: 80%; + max-width: 1000px; + padding: 1rem; + border: 1px solid #999; + border-radius: 0.25rem; + box-shadow: 0 2px 2px 0 rgba(66, 66, 66, 0.08), 0 2px 4px 2px rgba(66, 66, 66, 0.16); + } + h1 { + text-align: center; + padding-bottom: 1rem; + margin: 0 -1rem 1rem -1rem; + border-bottom: 1px solid #999; + } + #output { + text-align: center; + min-height: 250px; + } + div#clear { + text-align: center; + padding-top: 1rem; + margin: 1rem -1rem 0 -1rem; + border-top: 1px solid #999; + } + input[type=text], textarea { + width: 100%; + padding: 1rem 0.5rem; + margin: 0.5rem 0; + border: 0; + border-bottom: 1px solid #999; + background-color: #f0f0f0; + } + textarea { + height: 5rem; + } + small { + color: #999; + } + input[type=submit], input[type=reset] { + padding: 1rem; + border: none; + background-color: #6200ee; + color: #fff; + border-radius: 0.25rem; + width: 25%; + } + .hidden { + display: none; + } + .text-center { + text-align: center; + } + .error { + color: red; + } + + \ No newline at end of file diff --git a/apps-script/incident-response/VertexAiApi.gs b/apps-script/incident-response/VertexAiApi.gs deleted file mode 100644 index 9df503f6..00000000 --- a/apps-script/incident-response/VertexAiApi.gs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright 2023 Google LLC - * - * 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 - * - * http://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. - */ -// [START chat_incident_response_vertex] - -/** - * Summarizes a Chat conversation using the Vertex AI text prediction API. - * - * @param {string} chatHistory The Chat history that will be summarized. - * @return {string} The content from the text prediction response. - */ -function summarizeChatHistory_(chatHistory) { - const prompt = - "Summarize the following conversation between Engineers resolving an incident" - + " in a few sentences. Use only the information from the conversation.\n\n" - + chatHistory; - const request = { - instances: [ - { prompt: prompt } - ], - parameters: { - temperature: 0.2, - maxOutputTokens: 256, - topK: 40, - topP: 0.95 - } - } - const fetchOptions = { - method: 'POST', - headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() }, - contentType: 'application/json', - payload: JSON.stringify(request) - } - const response = UrlFetchApp.fetch( - `https://${VERTEX_AI_LOCATION_ID}-aiplatform.googleapis.com/v1` - + `/projects/${PROJECT_ID}/locations/${VERTEX_AI_LOCATION_ID}` - + "/publishers/google/models/text-bison:predict", - fetchOptions); - const payload = JSON.parse(response.getContentText()); - return payload.predictions[0].content; -} - -// [END chat_incident_response_vertex] diff --git a/apps-script/incident-response/WebController.gs b/apps-script/incident-response/WebController.gs index 0d52c22f..5487e366 100644 --- a/apps-script/incident-response/WebController.gs +++ b/apps-script/incident-response/WebController.gs @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/apps-script/incident-response/appsscript.json b/apps-script/incident-response/appsscript.json index ffa1f1c8..a9d5bf2b 100644 --- a/apps-script/incident-response/appsscript.json +++ b/apps-script/incident-response/appsscript.json @@ -1,35 +1,36 @@ { - "timeZone": "America/Toronto", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "AdminDirectory", - "version": "directory_v1", - "serviceId": "admin" - }, - { - "userSymbol": "Chat", - "version": "v1", - "serviceId": "chat" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "chat": {}, - "oauthScopes": [ - "https://www.googleapis.com/auth/chat.spaces.create", - "https://www.googleapis.com/auth/chat.memberships", - "https://www.googleapis.com/auth/chat.memberships.app", - "https://www.googleapis.com/auth/chat.messages", - "https://www.googleapis.com/auth/documents", - "https://www.googleapis.com/auth/admin.directory.user.readonly", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/userinfo.email", - "https://www.googleapis.com/auth/cloud-platform" - ], - "webapp": { - "executeAs": "USER_ACCESSING", - "access": "DOMAIN" - } -} + "timeZone": "America/Toronto", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Chat", + "serviceId": "chat", + "version": "v1" + } + ], + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] + }, + "webapp": { + "executeAs": "USER_ACCESSING", + "access": "DOMAIN" + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/chat.spaces", + "https://www.googleapis.com/auth/chat.memberships", + "https://www.googleapis.com/auth/chat.memberships.app", + "https://www.googleapis.com/auth/chat.messages", + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/cloud-platform" + ], + "chat": {} + } \ No newline at end of file From fe0b2c52dd249ca221e0ecd1e15809e46a83c74e Mon Sep 17 00:00:00 2001 From: Salaheddine Hamadi Date: Mon, 3 Mar 2025 19:45:22 +0000 Subject: [PATCH 2/2] Address comments from PR review. --- apps-script/incident-response/ChatApi.gs | 17 ++-- .../ChatApiAppCredentials.gs | 74 ++++++++++---- .../ChatApiHumanCredentials.gs | 29 +++--- apps-script/incident-response/ChatApp.gs | 1 - apps-script/incident-response/Consts.gs | 8 +- apps-script/incident-response/DocsApi.gs | 3 +- apps-script/incident-response/GeminiApi.gs | 3 +- apps-script/incident-response/Index.html | 4 +- apps-script/incident-response/JavaScript.html | 98 +++++++++---------- apps-script/incident-response/README.md | 25 +++-- apps-script/incident-response/appsscript.json | 5 + 11 files changed, 151 insertions(+), 116 deletions(-) diff --git a/apps-script/incident-response/ChatApi.gs b/apps-script/incident-response/ChatApi.gs index 33e038f8..14583697 100644 --- a/apps-script/incident-response/ChatApi.gs +++ b/apps-script/incident-response/ChatApi.gs @@ -14,17 +14,20 @@ * limitations under the License. */ /** - * Creates a space in Google Chat with the provided title and members, and posts an - * initial message to it. + * Handles an incident by creating a chat space, adding members, and posting a message. + * Uses either application credentials or human credentials based on the formData. * - * @param {Object} formData the data submitted by the user. It should contain the fields - * title, description, and users. - * @return {string} the resource name of the new space. + * @param {Object} formData - The data submitted by the user. It should contain the fields: + * - title: The display name of the chat space. + * - description: The description of the incident. + * - users: A comma-separated string of user emails to be added to the space. + * - isAppCredentials: Boolean indicating whether to use application credentials. + * @return {string} The resource name of the new space. */ function handleIncident(formData) { console.log(formData) - const appCredentialsMode = formData.appCredentials; // Get the appCredentials element - if(appCredentialsMode){ + const isAppCredentials = formData.isAppCredentials; // Get the isAppCredentials element + if(isAppCredentials){ return handleIncidentWithAppCredentials(formData) } else{ return handleIncidentWithHumanCredentials(formData) diff --git a/apps-script/incident-response/ChatApiAppCredentials.gs b/apps-script/incident-response/ChatApiAppCredentials.gs index 43fbd9d0..4c06fac5 100644 --- a/apps-script/incident-response/ChatApiAppCredentials.gs +++ b/apps-script/incident-response/ChatApiAppCredentials.gs @@ -13,32 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +// [START handle_incident_with_application_credentials] +/** + * Handles an incident by creating a chat space, adding members, and posting a message. + * All the actions are done using application credentials. + * + * @param {Object} formData - The data submitted by the user. It should contain the fields: + * - title: The display name of the chat space. + * - description: The description of the incident. + * - users: A comma-separated string of user emails to be added to the space. + * @return {string} The resource name of the new space. + */ function handleIncidentWithAppCredentials(formData) { const users = formData.users.trim().length > 0 ? formData.users.split(',') : []; - const spaceName = createChatSpaceWithAppCredentials(formData.title); - createHumanMembershipWithAppCredentials(spaceName, getUserEmail()); + const service = _getService(); + if (!service.hasAccess()) { + console.error(service.getLastError()); + return; + } + const spaceName = _createChatSpaceWithAppCredentials(formData.title, service); + _createHumanMembershipWithAppCredentials(spaceName, getUserEmail(),service); for (const user of users ){ - createHumanMembershipWithAppCredentials(spaceName, user); + _createHumanMembershipWithAppCredentials(spaceName, user, service); } - createMessageWithAppCredentials(spaceName, formData.description); + _createMessageWithAppCredentials(spaceName, formData.description, service); return spaceName; } - - -function createChatSpaceWithAppCredentials(spaceName) { +/** + * Creates a chat space with application credentials. + * + * @param {string} displayName - The name of the chat space. + * @param {object} service - The credentials of the service account. + * @returns {string} The resource name of the new space. + */ +function _createChatSpaceWithAppCredentials(displayName, service) { try { - const service = getService_(); - if (!service.hasAccess()) { - console.error(service.getLastError()); - return; - } // for private apps, the alias can be used const my_customer_alias = "customers/my_customer" // Specify the space to create. const space = { - displayName: spaceName, + displayName: displayName, spaceType: 'SPACE', customer: my_customer_alias }; @@ -56,8 +72,15 @@ function createChatSpaceWithAppCredentials(spaceName) { console.log('Failed to create space with error %s', err.message); } } - -function createMessageWithAppCredentials(spaceName, message) { + /* + * Creates a chat message with application credentials. + * + * @param {string} spaceName - The resource name of the space. + * @param {string} message - The text to be posted. + * @param {object} service - The credentials of the service account. + * @return {string} the resource name of the new space. + */ +function _createMessageWithAppCredentials(spaceName, message, service) { try { const service = getService_(); if (!service.hasAccess()) { @@ -80,8 +103,14 @@ function createMessageWithAppCredentials(spaceName, message) { console.log('Failed to create message with error %s', err.message); } } - -function createHumanMembershipWithAppCredentials(spaceName, email){ +/** + * Creates a human membership in a chat space with application credentials. + * + * @param {string} spaceName - The resource name of the space. + * @param {string} email - The email of the user to be added. + * @param {object} service - The credentials of the service account. + */ +function _createHumanMembershipWithAppCredentials(spaceName, email, service){ try{ const service = getService_(); if (!service.hasAccess()) { @@ -90,7 +119,6 @@ function createHumanMembershipWithAppCredentials(spaceName, email){ } const membership = { member: { - // TODO(developer): Replace USER_NAME here name: 'users/'+email, // User type for the membership type: 'HUMAN' @@ -109,8 +137,11 @@ function createHumanMembershipWithAppCredentials(spaceName, email){ } - -function getService_() { + /* + * Creates a service for the service account. + * @return {object} - The credentials of the service account. + */ +function _getService() { return OAuth2.createService(APP_CREDENTIALS.client_email) .setTokenUrl('https://oauth2.googleapis.com/token') .setPrivateKey(APP_CREDENTIALS.private_key) @@ -118,4 +149,5 @@ function getService_() { .setSubject(APP_CREDENTIALS.client_email) .setScope(APP_CREDENTIALS_SCOPES) .setPropertyStore(PropertiesService.getScriptProperties()); -} \ No newline at end of file +} +// [END handle_incident_with_application_credentials] \ No newline at end of file diff --git a/apps-script/incident-response/ChatApiHumanCredentials.gs b/apps-script/incident-response/ChatApiHumanCredentials.gs index db19957b..cad16521 100644 --- a/apps-script/incident-response/ChatApiHumanCredentials.gs +++ b/apps-script/incident-response/ChatApiHumanCredentials.gs @@ -13,21 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// [START chat_incident_response_space_creator] +// [START handle_incident_with_human_credentials] /** - * Creates a space in Google Chat with the provided title and members, and posts an - * initial message to it. + * Handles an incident by creating a chat space, adding members, and posting a message. + * All the actions are done using human credentials. * - * @param {Object} formData the data submitted by the user. It should contain the fields - * title, description, and users. - * @return {string} the resource name of the new space. + * @param {Object} formData - The data submitted by the user. It should contain the fields: + * - title: The display name of the chat space. + * - description: The description of the incident. + * - users: A comma-separated string of user emails to be added to the space. + * @return {string} The resource name of the new space. */ function handleIncidentWithHumanCredentials(formData) { const users = formData.users.trim().length > 0 ? formData.users.split(',') : []; - const spaceName = setUpSpaceWithHumanCredentials(formData.title, users); - addAppToSpaceWithHumanCredentials(spaceName); - createMessageWithHumanCredentials(spaceName, formData.description); + const spaceName = _setUpSpaceWithHumanCredentials(formData.title, users); + _addAppToSpaceWithHumanCredentials(spaceName); + _createMessageWithHumanCredentials(spaceName, formData.description); return spaceName; } @@ -36,7 +38,7 @@ function handleIncidentWithHumanCredentials(formData) { * * @return {string} the resource name of the new space. */ -function setUpSpaceWithHumanCredentials(displayName, users) { +function _setUpSpaceWithHumanCredentials(displayName, users) { const memberships = users.map(email => ({ member: { name: `users/${email}`, @@ -61,7 +63,7 @@ function setUpSpaceWithHumanCredentials(displayName, users) { * * @return {string} the resource name of the new membership. */ -function addAppToSpaceWithHumanCredentials(spaceName) { +function _addAppToSpaceWithHumanCredentials(spaceName) { const request = { member: { name: "users/app", @@ -78,7 +80,7 @@ function addAppToSpaceWithHumanCredentials(spaceName) { * * @return {string} the resource name of the new message. */ -function createMessageWithHumanCredentials(spaceName, text) { +function _createMessageWithHumanCredentials(spaceName, text) { const request = { text: text }; @@ -86,5 +88,4 @@ function createMessageWithHumanCredentials(spaceName, text) { const message = Chat.Spaces.Messages.create(request, spaceName); return message.name; } - -// [END chat_incident_response_space_creator] \ No newline at end of file +// [END handle_incident_with_human_credentials] \ No newline at end of file diff --git a/apps-script/incident-response/ChatApp.gs b/apps-script/incident-response/ChatApp.gs index 5d034b55..a0d13d79 100644 --- a/apps-script/incident-response/ChatApp.gs +++ b/apps-script/incident-response/ChatApp.gs @@ -185,5 +185,4 @@ function getUserDisplayName_(userMap, userName) { userMap.set(userName, displayName); return displayName; } - // [END chat_incident_response_app] diff --git a/apps-script/incident-response/Consts.gs b/apps-script/incident-response/Consts.gs index dab5df44..39c9bc79 100644 --- a/apps-script/incident-response/Consts.gs +++ b/apps-script/incident-response/Consts.gs @@ -15,11 +15,9 @@ */ // [START chat_incident_response_consts] -const PROJECT_ID = PROJECT_ID'; +const PROJECT_ID = 'replace-with-your-project-id'; const CLOSE_INCIDENT_COMMAND_ID = 3; -const APP_CREDENTIALS = APP_CREDENTIALS; +const APP_CREDENTIALS = 'replace-with-your-app-credentials'; const APP_CREDENTIALS_SCOPES = 'https://www.googleapis.com/auth/chat.bot https://www.googleapis.com/auth/chat.app.memberships https://www.googleapis.com/auth/chat.app.spaces.create'; -const GEMINI_API_KEY = GEMINI_API_KEY; - - +const GEMINI_API_KEY = 'replace-with-your-gemini-api-key'; // [END chat_incident_response_consts] \ No newline at end of file diff --git a/apps-script/incident-response/DocsApi.gs b/apps-script/incident-response/DocsApi.gs index 341477c1..08f6efff 100644 --- a/apps-script/incident-response/DocsApi.gs +++ b/apps-script/incident-response/DocsApi.gs @@ -36,5 +36,4 @@ function createDoc_(title, resolution, chatHistory, chatSummary) { body.appendParagraph(chatHistory); return doc.getUrl(); } - -// [END chat_incident_response_docs] +// [END chat_incident_response_docs] \ No newline at end of file diff --git a/apps-script/incident-response/GeminiApi.gs b/apps-script/incident-response/GeminiApi.gs index d5d331e4..82e35496 100644 --- a/apps-script/incident-response/GeminiApi.gs +++ b/apps-script/incident-response/GeminiApi.gs @@ -16,7 +16,7 @@ // [START chat_incident_response_gemini] /** - * Summarizes a Chat conversation using the Vertex AI text prediction API. + * Summarizes a Chat conversation using the Gemini API text prediction API. * * @param {string} chatHistory The Chat history that will be summarized. * @return {string} The content from the text prediction response. @@ -62,5 +62,4 @@ function summarizeChatHistory_(chatHistory) { return 'Gemini API error, please check logs.' } } - // [END chat_incident_response_gemini] \ No newline at end of file diff --git a/apps-script/incident-response/Index.html b/apps-script/incident-response/Index.html index 6cdee34f..953df8c1 100644 --- a/apps-script/incident-response/Index.html +++ b/apps-script/incident-response/Index.html @@ -47,7 +47,7 @@

Incident Manager

- +

@@ -63,4 +63,4 @@

Incident Manager

- \ No newline at end of file + diff --git a/apps-script/incident-response/JavaScript.html b/apps-script/incident-response/JavaScript.html index 4bcde3f1..dd252105 100644 --- a/apps-script/incident-response/JavaScript.html +++ b/apps-script/incident-response/JavaScript.html @@ -15,52 +15,52 @@ --> - \ No newline at end of file + var formDiv = document.getElementById('form'); + var outputDiv = document.getElementById('output'); + var clearDiv = document.getElementById('clear'); + + function handleFormSubmit(formObject) { + event.preventDefault(); + outputDiv.innerHTML = 'Please wait while we create the space...'; + hide(formDiv); + show(outputDiv); + google.script.run + .withSuccessHandler(updateOutput) + .withFailureHandler(onFailure) + .handleIncident(formObject); + } + + function updateOutput(response) { + var spaceId = response.replace('spaces/', ''); + outputDiv.innerHTML = + '

Space created!

Open space

'; + show(outputDiv); + show(clearDiv); + } + + function onFailure(error) { + outputDiv.innerHTML = 'ERROR: ' + error.message; + outputDiv.classList.add('error'); + show(outputDiv); + show(clearDiv); + } + + function onReset() { + outputDiv.innerHTML = ''; + outputDiv.classList.remove('error'); + show(formDiv); + hide(outputDiv); + hide(clearDiv); + } + + function hide(element) { + element.classList.add('hidden'); + } + + function show(element) { + element.classList.remove('hidden'); + } + + diff --git a/apps-script/incident-response/README.md b/apps-script/incident-response/README.md index 4663e9ad..46b76e98 100644 --- a/apps-script/incident-response/README.md +++ b/apps-script/incident-response/README.md @@ -91,7 +91,7 @@ the following APIs: ### 4. Create API key To use Gemini API, you need to have an API key created: -1. In the Google Cloud ocnsole, click **Menu** > **APIs & Services** > **Credentials** +1. In the Google Cloud console, click **Menu** > **APIs & Services** > **Credentials** 1. Click **CREATE CRENDETIALAS**, then select **API key**. 1. Create the key @@ -110,7 +110,7 @@ replace placeholder information with real information. To learn more about authentication in Google Chat, see [Authenticate and authorize Chat apps and Google Chat API requests](https://developers.devsite.corp.google.com/chat/api/guides/auth). -#### 4.1 Authenticate as a user configuration +#### 4.1 Set up authentication as a user 1. In the Google Cloud console, go to **Menu** > **APIs & Services** > [**OAuth consent screen**](https://console.cloud.google.com/apis/credentials/consent). @@ -136,11 +136,11 @@ Chat apps and Google Chat API requests](https://developers.devsite.corp.google.c 1. Click **Save and Continue**. 1. Review the app registration summary, then click **Back to Dashboard**. -#### 4.2 Authenticate as a app configuration +#### 4.2 Set up authentication as an app This is available in [Developer Preview](/workspace/preview) only. -##### 4.2.1 setup service account and key +##### 4.2.1 Setup service account and key 1. In the Google Cloud console, go to **Menu* > **IAM & Admin** > **Service Accounts** 1. Click **Create service account**. @@ -157,6 +157,10 @@ This is available in [Developer Preview](/workspace/preview) only. 1. Select **JSON**, then click **Create**. 1. Click **Close**. +**Warning**: This example uses an exported service account key for simplicity's sake. Exporting a private key is not recommended +in production because it shouldn't be stored in an insecure location, such as source control. To learn more about secure service account +implementations and best practices, see Choose when to use service accounts. + ##### 4.2.2 Receive administrator approval To use an authorization scope that begins with @@ -192,12 +196,6 @@ After that, the Chap app should be configured in **Goolge Workspace Marketplace 1. Under **Developer information**, enter your **Developer name**, **Developer website URL**, and **Developer email**. 1. Click **Save draft** 1. [Set up authorization Chat app](https://support.google.com/a?p=chat-app-auth). - -**Warning**: This example uses an exported service account key for simplicity's sake. Exporting a private key is not recommended -in production because it shouldn't be stored in an insecure location, such as source control. To learn more about secure service account -implementations and best practices, see Choose when to use service accounts. - - ### 5. Create an Apps Script project and connect it to the Google Cloud project Before creating the Apps Script project, copy your Google Cloud project number. @@ -293,6 +291,7 @@ The Chat app is ready to respond to messages. Navigate to the **Web app URL** from the Apps Script deployment to test your app. -#### 8.1 app auth mode -If the checkbox is selected, the Chat app will use app authentication to create the space, add members and post the message to the space. -If the checkbox is not selected, the Chat app will use human credentials instead and all the actions will be done on behalf of the user. +#### 8.1. Select the authentication mode + +If the checkbox 'Use app credentials' is selected, the Chat app will use app authentication to create the space, add members and post the message to the space. +If the checkbox is not selected, the Chat app will use user credentials instead and all the actions will be done on behalf of the user. diff --git a/apps-script/incident-response/appsscript.json b/apps-script/incident-response/appsscript.json index a9d5bf2b..ba330718 100644 --- a/apps-script/incident-response/appsscript.json +++ b/apps-script/incident-response/appsscript.json @@ -8,6 +8,11 @@ "userSymbol": "Chat", "serviceId": "chat", "version": "v1" + }, + { + "userSymbol": "AdminDirectory", + "version": "directory_v1", + "serviceId": "admin" } ], "libraries": [