Skip to content

Commit 1407dd3

Browse files
authored
Initial import of Chat stand-up app from COL300/Next24 (googleworkspace#467)
1 parent e5d04fb commit 1407dd3

File tree

6 files changed

+407
-0
lines changed

6 files changed

+407
-0
lines changed

ai/standup-chat-app/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Chat API - Stand up with AI
2+
3+
## Project Description
4+
5+
Google Chat application that creates AI summaries of a consolidation Chat threads and posts them back within the top-level Chat message. Use case is using AI to streamline Stand up content within Google Chat.
6+
7+
## Prerequisites
8+
9+
* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled
10+
11+
## Set up your environment
12+
13+
1. Create a Cloud Project
14+
1. Configure OAuth consent screen
15+
1. Enable the Admin SDK API
16+
1. Enable the Generative Language API
17+
1. Enable and configure the Google Chat API with the following values:
18+
1. App status: Live - available to users
19+
1. App name: “Standup”
20+
1. Avatar URL: “https://www.gstatic.com/images/branding/productlogos/chat_2020q4/v8/web-24dp/logo_chat_2020q4_color_2x_web_24dp.png”
21+
1. Description: “Standup App”
22+
1. Enable Interactive features: Disabled
23+
1. Create a Google Gemini API Key
24+
1. Navigate to https://aistudio.google.com/app/apikey
25+
1. Create API key for existing project from step 1
26+
1. Copy the generated key
27+
1. Create and open a standalone Apps Script project
28+
1. From Project Settings, change project to GCP project number of Cloud Project from step 1
29+
1. Add the following script properties:
30+
1. Set `API_KEY` with the API key previously generated as the value.
31+
1. Set `SPREADSHEET_ID` with the file ID of a blank spreadsheet.
32+
1. Set `SPACE_NAME` to the resource name of a Chat space (e.g. `spaces/AAAXYZ`)
33+
1. Enable the Google Chat advanced service
34+
1. Enable the AdminDirectory advanced service
35+
1. Add the project code to Apps Script
36+
1. Enable triggers:
37+
1. Add Time-driven to run function `standup` at the desired interval frequency (e.g. Week timer)
38+
1. Add Time-driven to run function `summarize` at the desired interval frequency (e.g. Hour timer)

ai/standup-chat-app/appsscript.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"timeZone": "America/Los_Angeles",
3+
"exceptionLogging": "STACKDRIVER",
4+
"runtimeVersion": "V8",
5+
"dependencies": {
6+
"enabledAdvancedServices": [
7+
{
8+
"userSymbol": "Chat",
9+
"serviceId": "chat",
10+
"version": "v1"
11+
},
12+
{
13+
"userSymbol": "AdminDirectory",
14+
"serviceId": "admin",
15+
"version": "directory_v1"
16+
}
17+
]
18+
},
19+
"webapp": {
20+
"executeAs": "USER_ACCESSING",
21+
"access": "DOMAIN"
22+
},
23+
"oauthScopes": [
24+
"https://www.googleapis.com/auth/chat.messages",
25+
"https://www.googleapis.com/auth/spreadsheets",
26+
"https://www.googleapis.com/auth/admin.directory.user.readonly",
27+
"https://www.googleapis.com/auth/script.external_request",
28+
"https://www.googleapis.com/auth/chat.spaces.create",
29+
"https://www.googleapis.com/auth/chat.spaces",
30+
"https://www.googleapis.com/auth/chat.spaces.readonly",
31+
"https://www.googleapis.com/auth/chat.spaces.create",
32+
"https://www.googleapis.com/auth/chat.delete",
33+
"https://www.googleapis.com/auth/chat.memberships",
34+
"https://www.googleapis.com/auth/chat.memberships.app",
35+
"https://www.googleapis.com/auth/userinfo.email"
36+
]
37+
}

ai/standup-chat-app/db.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/** @typedef {object} Message
18+
* @property {string} name
19+
* @property {string} text
20+
* @property {object} sender
21+
* @property {string} sender.type
22+
* @property {string} sender.name
23+
* @property {object[]} annotations
24+
* @property {number} annotations.startIndex
25+
* @property {string} annotations.type
26+
* @property {object} annotations.userMention
27+
* @property {number} annotations.length
28+
* @property {string} formattedText
29+
* @property {string} createTime
30+
* @property {string} argumentText
31+
* @property {object} thread
32+
* @property {string} thread.name
33+
* @property {object} space
34+
* @property {string} space.name
35+
*/
36+
37+
38+
class DB {
39+
/**
40+
* params {String} spreadsheetId
41+
*/
42+
constructor(spreadsheetId) {
43+
this.spreadsheetId = spreadsheetId;
44+
this.sheetName = "Messages";
45+
46+
}
47+
48+
/**
49+
* @returns {SpreadsheetApp.Sheet}
50+
*/
51+
get sheet() {
52+
const spreadsheet = SpreadsheetApp.openById(this.spreadsheetId);
53+
let sheet = spreadsheet.getSheetByName(this.sheetName);
54+
55+
// create if it does not exist
56+
if (sheet == undefined) {
57+
sheet = spreadsheet.insertSheet();
58+
sheet.setName(this.sheetName)
59+
}
60+
61+
return sheet;
62+
}
63+
64+
/**
65+
* @returns {Message|undefined}
66+
*/
67+
get last() {
68+
const lastRow = this.sheet.getLastRow()
69+
if (lastRow === 0) return;
70+
return JSON.parse(this.sheet.getSheetValues(lastRow, 1, 1, 2)[0][1]);
71+
}
72+
73+
74+
/**
75+
* @params {Chat_v1.Chat.V1.Schema.Message} message
76+
*/
77+
append(message) {
78+
this.sheet.appendRow([message.name, JSON.stringify(message, null, 2)]);
79+
}
80+
81+
}
82+
83+
84+
/**
85+
* Test function for DB Object
86+
*/
87+
function testDB() {
88+
const db = new DB(SPREADSHEET_ID);
89+
90+
let thread = db.last;
91+
if (thread == undefined) return;
92+
console.log(thread)
93+
94+
db.rowOffset = 1;
95+
thread = db.last;
96+
if (thread == undefined) return;
97+
console.log(thread)
98+
}

ai/standup-chat-app/gemini.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/**
18+
* Makes a simple content-only call to Gemini AI.
19+
*
20+
* @param {string} text Prompt to pass to Gemini API.
21+
* @param {string} API_KEY Developer API Key enabled to call Gemini.
22+
*
23+
* @return {string} Response from AI call.
24+
*/
25+
function generateContent(text, API_KEY) {
26+
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`;
27+
28+
return JSON.parse(UrlFetchApp.fetch(url, {
29+
method: "POST",
30+
headers: {
31+
"content-type": "application/json"
32+
},
33+
payload: JSON.stringify({
34+
contents: [{
35+
parts: [
36+
{text}
37+
]
38+
}]
39+
}),
40+
}).getContentText())
41+
}

ai/standup-chat-app/main.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/** TODO
18+
* Update global variables for your project settings
19+
* */
20+
const API_KEY = PropertiesService.getScriptProperties().getProperty("API_KEY");
21+
const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID"); // e.g. "1O0IW7fW1QeFLa7tIrv_h7_PlSUTB6kd0miQO_sXo7p0"
22+
const SPACE_NAME = PropertiesService.getScriptProperties().getProperty("SPACE_NAME"); // e.g. "spaces/AAAABCa12Cc"
23+
24+
const SUMMARY_HEADER = `\n\n*Gemini Generated Summary*\n\n`;
25+
26+
27+
/**
28+
* Sends the message to create new standup instance.
29+
* Called by trigger on interval of standup, e.g. Weekly
30+
*
31+
* @return {string} The thread name of the message sent.
32+
*/
33+
function standup() {
34+
const db = new DB(SPREADSHEET_ID);
35+
36+
const last = db.last;
37+
38+
let text = `<users/all> Please share your weekly update here.\n\n*Source Code*: <https://script.google.com/corp/home/projects/${ScriptApp.getScriptId()}/edit|Apps Script>`;
39+
40+
if (last) {
41+
text += `\n*Last Week*: <${linkToThread(last)}|View thread>`;
42+
}
43+
44+
const message = Chat.Spaces.Messages.create({
45+
text,
46+
}, PropertiesService.getScriptProperties().getProperty("spaceName") // Demo replaces => SPACE_NAME
47+
);
48+
49+
db.append(message);
50+
51+
console.log(`Thread Name: ${message.thread.name}`)
52+
return message.thread.name
53+
}
54+
55+
/**
56+
* Uses AI to create a summary of messages for a stand up period.
57+
* Called by trigger on interval required to summarize, e.g. Hourly
58+
*
59+
* @return n/a
60+
*/
61+
function summarize() {
62+
const db = new DB(SPREADSHEET_ID);
63+
const last = db.last;
64+
65+
if (last == undefined) return;
66+
67+
const filter = `thread.name=${last.thread.name}`;
68+
let { messages } = Chat.Spaces.Messages.list(PropertiesService.getScriptProperties().getProperty("spaceName"), { filter }); // Demo replaces => SPACE_NAME
69+
70+
messages = (messages ?? [])
71+
.slice(1)
72+
.filter(message => message.slashCommand === undefined)
73+
74+
if (messages.length === 0) {
75+
return;
76+
}
77+
78+
const history = messages
79+
.map(({ sender, text }) => `${cachedGetSenderDisplayName(sender)}: ${text}`)
80+
.join('/n');
81+
82+
const response = generateContent(
83+
`Summarize the following weekly tasks and discussion per team member in a single concise sentence for each individual with an extra newline between members, but without using markdown or any special character except for newlines: ${history}`,
84+
API_KEY);
85+
const summary = response.candidates[0].content?.parts[0].text;
86+
87+
if (summary == undefined) {
88+
return;
89+
}
90+
91+
Chat.Spaces.Messages.update({
92+
text: last.formattedText + SUMMARY_HEADER + summary.replace("**", "*")
93+
},
94+
last.name,
95+
{ update_mask: "text" }
96+
);
97+
98+
}
99+
100+
/**
101+
* Gets the display name from AdminDirectory Services.
102+
*
103+
* @param {!Object} sender
104+
* @return {string} User name on success | 'Unknown' if not.
105+
*/
106+
function getSenderDisplayName(sender) {
107+
try {
108+
const user = AdminDirectory.Users.get(
109+
sender.name.replace("users/", ""),
110+
{ projection: 'BASIC', viewType: 'domain_public' });
111+
return user.name.displayName ?? user.name.fullName;
112+
} catch (e) {
113+
console.error("Unable to get display name");
114+
return "Unknown"
115+
};
116+
}
117+
118+
const cachedGetSenderDisplayName = memoize(getSenderDisplayName);
119+
120+
/**
121+
* @params {Chat_v1.Chat.V1.Schema.Message|Message} message
122+
* @returns {String}
123+
*/
124+
function linkToThread(message) {
125+
// https://chat.google.com/room/SPACE/THREAD/
126+
return `https://chat.google.com/room/${message.space.name.split("/").pop()}/${message.thread.name.split("/").pop()}`;
127+
}

0 commit comments

Comments
 (0)