378 lines
10 KiB
JavaScript
378 lines
10 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|