gemini/use-cases/sheets-integration/Code.gs (287 lines of code) (raw):

/** * 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. */ const location = "us"; const datasetId = "genai_demo"; const connectionId = "genai-connection"; const modelId = "genai-model"; const tableName = "genai-data"; const modelName = "gemini-2.0-flash"; function setup() { projectId = setProjectId(); if (projectId == null) { SpreadsheetApp.getUi().alert( "A valid project ID must be provided to set up the project.", ); return; } dataset = createDataset(projectId); if (!dataset) { return; } table = createTable(projectId); if (!table) { return; } endpoint = createEndpoint(projectId); if (endpoint) { SpreadsheetApp.getActiveSpreadsheet().toast( "Setup completed successfully.", ); } } function createDataset(projectId) { var query = `SELECT * FROM \`${projectId}\`.INFORMATION_SCHEMA.SCHEMATA ` + `WHERE schema_name = '${datasetId}'`; response = runQuery(query); if (response.totalRows == 1) { return response.queryId; } query = `CREATE SCHEMA \`${projectId}.${datasetId}\`\n` + ` OPTIONS ( location = '${location}' );`; try { return runQuery(query).queryId; } catch (error) { console.log(error.details.message); SpreadsheetApp.getUi().alert(error); return null; } } /** * Create BigQuery table if it doesn't exist already */ function createTable(projectId) { var query = `SELECT * FROM \`${projectId}.${datasetId}.INFORMATION_SCHEMA.TABLES\` ` + `WHERE table_name = '${tableName}'`; response = runQuery(query); if (response.totalRows == 1) { return response.queryId; } query = `CREATE TABLE \`${projectId}.${datasetId}.${tableName}\` ` + "(prompt STRING, response STRING);"; try { return runQuery(query).queryId; } catch (error) { console.log(error.details.message); SpreadsheetApp.getUi().alert(error); return null; } } function createEndpoint(projectId) { try { model = BigQuery.Models.get(projectId, datasetId, modelId); return model; } catch { const query = `CREATE MODEL \`${projectId}.${datasetId}.${modelId}\`\n` + `REMOTE WITH CONNECTION \`${projectId}.${location}.${connectionId}\`\n` + `OPTIONS(ENDPOINT = "${modelName}")`; try { return runQuery(query).queryId; } catch (error) { console.log(error.details.message); SpreadsheetApp.getUi().alert(error); return null; } } } function query() { projectId = getProjectId(); if (projectId == null) { SpreadsheetApp.getUi().alert( "A Google Cloud project must be selected using the Setup menu item.", ); } const numColumns = SpreadsheetApp.getActiveRange().getNumColumns(); if (numColumns != 1) { SpreadsheetApp.getUi().alert( "Exactly one column of prompts must be selected.", ); return null; } // Populate temporary table in BigQuery with selected data from sheet const prompts = sanitizePrompts(SpreadsheetApp.getActiveRange().getValues()); populateTable(prompts); const query = `SELECT * FROM ML.GENERATE_TEXT( MODEL \`${datasetId}.${modelId}\`, ` + `(SELECT * FROM \`${projectId}.${datasetId}.${tableName}\`), ` + `STRUCT(${getMaxOutputTokens()} AS max_output_tokens, ${getTemperature()} AS temperature));`; console.log(`Query: ${query}`); try { response = runQuery(query); } catch (error) { console.log(error.details.message); SpreadsheetApp.getUi().alert(error); return null; } const responseMap = {}; invalidResponses = false; for (const row of response.rows) { returnedPrompt = row.f[2].v; console.log(`Prompt: ${returnedPrompt}`); jsonString = row.f[0].v; console.log(jsonString); const jsonData = JSON.parse(jsonString); console.log(jsonData); if (jsonData.candidates) { content = jsonData.candidates[0].content; } // Is it missing content, e.g. due to a finish reason of 4 (RECITATION)? var response = ""; if (!jsonData.candidates || !content) { invalidResponses = true; SpreadsheetApp.getUi().alert(row); } else { response = jsonData.candidates[0].content.parts[0].text; } console.log(`Response: ${response}`); responseMap[returnedPrompt] = response; } if (invalidResponses) { SpreadsheetApp.getActiveSpreadsheet().toast( "Some prompts did not return a response. Check prompts and safety settings.", ); } // Responses come back in any order. // Put these in the same order as the selected cells. responses = []; for (const prompt of prompts) { responses.push(responseMap[prompt]); } writeResponses(responses); } function sanitizePrompts(prompts) { // Sanitize each prompt in the array return prompts.map((row) => row.map((prompt) => { if (typeof prompt !== "string") return prompt; // Keep only letters, numbers, and whitespace return prompt.replace(/[^a-zA-Z0-9\s]/g, ""); }), ); } /** * Given a set of responses, write them one cell to the right of the selected prompts. */ function writeResponses(responses) { const sheet = SpreadsheetApp.getActiveSheet(); const activeRange = sheet.getActiveRange(); // Get information about the active range const startRow = activeRange.getRow(); const startColumn = activeRange.getColumn() + 1; const numRows = activeRange.getNumRows(); const numColumns = activeRange.getNumColumns(); if (responses.length !== numRows) { SpreadsheetApp.getUi().alert( "Number of columns selected does not match number of responses returned.", ); } // Calculate the new range and write the values const newRange = sheet.getRange(startRow, startColumn, numRows, numColumns); newRange.setValues(responses.map((response) => [response])); } function getProjectId(name = "project ID", defaultValue = null) { return ( PropertiesService.getUserProperties().getProperty(name) || defaultValue ); } function setProjectId(name = "project ID") { newValue = setParameter(name, getProjectId()); if (newValue != null) { PropertiesService.getUserProperties().setProperty(name, newValue); } return newValue; } function getTemperature(name = "temperature", defaultValue = 0) { return Number( PropertiesService.getUserProperties().getProperty(name) || defaultValue, ); } function setTemperature(name = "temperature", min = 0, max = 1) { newValue = setParameter(name, getTemperature(), min, max); if (newValue != null) { PropertiesService.getUserProperties().setProperty(name, newValue); } return newValue; } function getMaxOutputTokens(name = "max output tokens", defaultValue = 128) { return Number( PropertiesService.getUserProperties().getProperty(name) || defaultValue, ); } function setMaxOutputTokens(name = "max output tokens", min = 1, max = 8192) { newValue = setParameter(name, getMaxOutputTokens(), min, max); if (newValue != null) { PropertiesService.getUserProperties().setProperty(name, newValue); } return newValue; } function setParameter(name, value = null, min = null, max = null) { const ui = SpreadsheetApp.getUi(); const isNumeric = min != null && max != null; let promptText = `Enter ${name}:`; if (isNumeric) { promptText = `Enter ${name} (between ${min} and ${max}):`; } let prompt = null; if (value != null) { prompt = ui.prompt( promptText, `Current value is ${value}.`, ui.ButtonSet.OK_CANCEL, ); } else { prompt = ui.prompt(promptText, ui.ButtonSet.OK_CANCEL); } if (prompt.getSelectedButton() == ui.Button.OK) { const inputText = prompt.getResponseText().trim(); const inputValue = isNumeric ? Number(inputText) : inputText; if ( inputValue != null && (!isNumeric || (inputValue >= min && inputValue <= max)) ) { console.log(`User updated ${name} to ${inputValue}.`); return inputValue; } else { ui.alert("Please enter a valid input."); return setParameter(name, value, min, max); } } else { console.log("User canceled the dialog."); } return null; // Return null if cancelled or input is invalid after retry } /** * Runs a BigQuery query and logs the results in a spreadsheet. */ function runQuery(query) { const request = { query: query, useLegacySql: false, }; let queryResults = BigQuery.Jobs.query(request, projectId); const jobId = queryResults.jobReference.jobId; // Check on status of the Query Job. let sleepTimeMs = 500; while (!queryResults.jobComplete) { Utilities.sleep(sleepTimeMs); sleepTimeMs *= 2; queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId); } return queryResults; } /** * Insert data from sheet as records into BigQuery table, for use in BQML models */ function populateTable(prompts, responses) { // Delete existing rows from temporary table var query = `DELETE FROM \`${datasetId}.${tableName}\` WHERE TRUE`; response = runQuery(query); // Insert records in batches, to avoid exceeding resource constraints const BATCH_SIZE = 500; for (let i = 0, j = prompts.length; i < j; i += BATCH_SIZE) { const batch = prompts.slice(i, i + BATCH_SIZE); const values = getValuesStr(batch); query = `INSERT \`${datasetId}.${tableName}\` (prompt) VALUES ${values}`; runQuery(query); } } /** * Converts an array into comma-separated strings for use in SQL statement */ function getValuesStr(inputs) { const values = []; for (let i = 0; i < inputs.length; i++) { input = inputs[i]; values.push("(" + inputs[i].map((x) => "'" + x + "'").join() + ")"); } return values.join(); } /** * Create menu items linked to functions */ function onOpen() { SpreadsheetApp.getUi() .createMenu("Gemini") .addItem("Query", "query") .addItem("Setup", "setup") .addSubMenu( SpreadsheetApp.getUi() .createMenu("Configure") .addItem("Temperature", "setTemperature") .addItem("Max Output Tokens", "setMaxOutputTokens"), ) .addToUi(); }