diff --git a/ai/autosummarize/README.md b/ai/autosummarize/README.md new file mode 100644 index 000000000..ae31c5e07 --- /dev/null +++ b/ai/autosummarize/README.md @@ -0,0 +1,35 @@ +# Editor Add-on: Sheets - AutoSummarize AI + +## Project Description + +Google Workspace Editor Add-on for Google Sheets that uses AI to create AI summaries in bulk for a listing of Google Docs, Sheets, and Slides files. + + +## Prerequisites + +* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled + +## Set up your environment + + +1. Create a Cloud Project + 1. Enable the Vertex AI API + 1. Create a Service Account and grant the role Service Account Token Creator Role + 1. Create a private key with type JSON. This will download the JSON file for use in the next section. +1. Open an Apps Script Project bound to a Google Sheets Spreadsheet. + 1. Rename the script to `Autosummarize AI`. + 1. From Project Settings, change project to GCP project number of Cloud Project from step 1 + 1. Add a Script Property. Enter `model_id` as the property name and `gemini-pro-vision` as the value. + 1. Add a Script Property. Enter `project_location` as the property name and `us-central1` as the value. + 1. Add a Script Property. Enter `service_account_key` as the property name and paste the JSON key from the service account as the value. +1. Add `OAuth2 v43` Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`. +1. Enable the `Drive v3` advanced service. +1. Add the project code to Apps Script + + +## Usage + +1. Insert one or more links to any Google Doc, Sheet, or Slides files in a column. +1. Select one or more of the links in the sheet. +1. From the `Sheets` menu, select `Extensions > AutoSummarize AI > Open AutoSummarize AI` +1. Click Get summaries button. diff --git a/ai/autosummarize/appsscript.json b/ai/autosummarize/appsscript.json new file mode 100644 index 000000000..5aed4b13d --- /dev/null +++ b/ai/autosummarize/appsscript.json @@ -0,0 +1,22 @@ +{ + "timeZone": "America/Denver", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43", + "developmentMode": false + } + ], + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + } +} \ No newline at end of file diff --git a/ai/autosummarize/gemini.js b/ai/autosummarize/gemini.js new file mode 100644 index 000000000..35bbb65bd --- /dev/null +++ b/ai/autosummarize/gemini.js @@ -0,0 +1,112 @@ +/* +Copyright 2024 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 + + 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. +*/ + +function scriptPropertyWithDefault(key, defaultValue = undefined) { + const scriptProperties = PropertiesService.getScriptProperties(); + const value = scriptProperties.getProperty(key); + if (value) { + return value; + } + return defaultValue; +} + +const VERTEX_AI_LOCATION = scriptPropertyWithDefault('project_location', 'us-central1'); +const MODEL_ID = scriptPropertyWithDefault('model_id', 'gemini-pro-vision'); +const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault('service_account_key'); + + +/** + * Packages prompt and necessary settings, then sends a request to + * Vertex API. Returns the response as an JSON object extracted from the + * Vertex API response object. + * + * @param {string} prompt The prompt to senb to Vertex AI API. + * @param {string} options.temperature The temperature setting set by user. + * @param {string} options.tokens The number of tokens to limit to the prompt. + */ +function getAiSummary(parts, options = {}) { + options = Object.assign({}, { temperature: 0.1, tokens: 8192}, options ?? {}) + const request = { + "contents": [ + { + "role": "user", + "parts": parts, + } + ], + "generationConfig": { + "temperature": options.temperature, + "topK": 1, + "topP": 1, + "maxOutputTokens": options.tokens, + "stopSequences": [] + }, + } + + const credentials = credentialsForVertexAI(); + + const fetchOptions = { + method: 'POST', + headers: { + 'Authorization': `Bearer ${credentials.accessToken}` + }, + contentType: 'application/json', + muteHttpExceptions: true, + payload: JSON.stringify(request) + } + + const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + + `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent` + const response = UrlFetchApp.fetch(url, fetchOptions); + + + const responseCode = response.getResponseCode(); + if (responseCode >= 400) { + throw new Error(`Unable to process file: Error code ${responseCode}`); + } + + const responseText = response.getContentText(); + const parsedResponse = JSON.parse(responseText); + if (parsedResponse.error) { + throw new Error(parsedResponse.error.message); + } + const text = parsedResponse.candidates[0].content.parts[0].text + return text +} + +/** + * Gets credentials required to call Vertex API using a Service Account. + * Requires use of Service Account Key stored with project + * + * @return {!Object} Containing the Cloud Project Id and the access token. + */ +function credentialsForVertexAI() { + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } + + const parsedCredentials = JSON.parse(credentials); + const service = OAuth2.createService("Vertex") + .setTokenUrl('https://oauth2.googleapis.com/token') + .setPrivateKey(parsedCredentials['private_key']) + .setIssuer(parsedCredentials['client_email']) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials['project_id'], + accessToken: service.getAccessToken(), + } +} diff --git a/ai/autosummarize/main.js b/ai/autosummarize/main.js new file mode 100644 index 000000000..c6ebe7b39 --- /dev/null +++ b/ai/autosummarize/main.js @@ -0,0 +1,135 @@ +/* +Copyright 2024 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 + + 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. +*/ + +/** + * Creates a menu entry in the Google Sheets Extensions menu when the document is opened. + * + * @param {object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + SpreadsheetApp.getUi().createAddonMenu() + .addItem('📄 Open AutoSummarize AI', 'showSidebar') + .addSeparator() + .addItem('❎ Quick summary', 'doAutoSummarizeAI') + .addItem('❌ Remove all summaries', 'removeAllSummaries') + .addToUi(); +} + +/** + * Runs when the add-on is installed; calls onOpen() to ensure menu creation and + * any other initializion work is done immediately. This method is only used by + * the desktop add-on and is never called by the mobile version. + * + * @param {object} e The event parameter for a simple onInstall trigger. + */ +function onInstall(e) { + onOpen(e); +} + +/** + * Opens sidebar in Sheets with AutoSummarize AI interface. + */ +function showSidebar() { + const ui = HtmlService.createHtmlOutputFromFile('sidebar') + .setTitle('AutoSummarize AI'); + SpreadsheetApp.getUi().showSidebar(ui); +} + + +/** + * Deletes all of the AutoSummarize AI created sheets + * i.e. any sheets with prefix of 'AutoSummarize AI' + */ +function removeAllSummaries() { + const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); + const allSheets = spreadsheet.getSheets(); + + allSheets.forEach(function (sheet) { + const sheetName = sheet.getName(); + // Check if the sheet name starts with "AutoSummarize AI" + if (sheetName.startsWith("AutoSummarize AI")) { + spreadsheet.deleteSheet(sheet) + } + }); +} + +/** + * Wrapper function for add-on. + */ +function doAutoSummarizeAI(customPrompt1, customPrompt2, temperature = .1, tokens = 2048) { + // Get selected cell values. + console.log("Getting selection..."); + let selection = SpreadsheetApp.getSelection() + .getActiveRange() + .getRichTextValues() + .map(value => { + if (value[0].getLinkUrl()) { + return value[0].getLinkUrl(); + } + return value[0].getText(); + }); + + // Get AI summary + const data = summarizeFiles(selection, customPrompt1, customPrompt2, temperature, tokens); + + // Add and format a new new sheet. + const now = new Date(); + const nowFormatted = Utilities.formatDate(now, now.getTimezoneOffset().toString(), "MM/dd HH:mm"); + let sheetName = `AutoSummarize AI (${nowFormatted})`; + if (SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)) { + sheetName = `AutoSummarize AI (${nowFormatted}:${now.getSeconds()})`; + } + let aiSheet = SpreadsheetApp.getActiveSpreadsheet() + .insertSheet() + .setName(sheetName); + let aiSheetHeaderStyle = SpreadsheetApp.newTextStyle() + .setFontSize(12) + .setBold(true) + .setFontFamily("Google Sans") + .setForegroundColor("#ffffff") + .build(); + let aiSheetValuesStyle = SpreadsheetApp.newTextStyle() + .setFontSize(10) + .setBold(false) + .setFontFamily("Google Sans") + .setForegroundColor("#000000") + .build(); + aiSheet.getRange("A1:E1") + .setBackground("#434343") + .setTextStyle(aiSheetHeaderStyle) + .setValues([["Link", "Title",`Summary from Gemini AI [Temperature: ${temperature}]`, `Custom Prompt #1: ${customPrompt1}`, `Custom Prompt #2: ${customPrompt2}`]]) + .setWrap(true); + aiSheet.setColumnWidths(1, 1, 100); + aiSheet.setColumnWidths(2, 1, 300); + aiSheet.setColumnWidths(3, 3, 300); + + // Copy results + aiSheet + .getRange(`A2:E${data.length + 1}`) + .setValues(data); + + aiSheet.getRange(`A2:E${data.length + 1}`) + .setBackground("#ffffff") + .setTextStyle(aiSheetValuesStyle) + .setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP) + .setVerticalAlignment("top"); + aiSheet.getRange(`C2:E${data.length + 1}`) + .setBackground("#efefef") + .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); + + aiSheet.deleteColumns(8, 19); + aiSheet.deleteRows(aiSheet.getLastRow() + 1, aiSheet.getMaxRows() - aiSheet.getLastRow()); +} diff --git a/ai/autosummarize/sidebar.html b/ai/autosummarize/sidebar.html new file mode 100644 index 000000000..a8889c6ff --- /dev/null +++ b/ai/autosummarize/sidebar.html @@ -0,0 +1,357 @@ + + + + + +
+▼ How to + Use...
+ + + + + +▲ Custom + Prompts
+ ++
+ +
▼ Prompt + Settings
+ + + + + + + +