From d62bf246076719e53f6d31656e71665a270756f4 Mon Sep 17 00:00:00 2001 From: Benjamin Sayaque <91118734+Benjamin-Sayaque@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:16:53 +0200 Subject: [PATCH 01/11] Document and test Gemini code execution --- src/code.gs | 126 +++++++++++++++++++++++++++++++++++++------ src/testFunctions.gs | 79 +++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 17 deletions(-) diff --git a/src/code.gs b/src/code.gs index 80a8303..81a82ef 100644 --- a/src/code.gs +++ b/src/code.gs @@ -302,8 +302,10 @@ const GenAIApp = (function () { }; /** - * Enables OpenAI Code Interpreter support for this chat. - * @param {string} [containerId] - OPTIONAL - Explicit container ID to reuse. + * Enables code execution support for this chat. + * For OpenAI models, the optional containerId reuses an existing code interpreter container. + * For Gemini models, containerId is ignored because Gemini code execution is stateless and has no container concept. + * @param {string} [containerId] - OPTIONAL - Explicit OpenAI container ID to reuse. Ignored for Gemini models. * @returns {Chat} - The current Chat instance. * @example * const chat = GenAIApp.newChat() @@ -320,7 +322,9 @@ const GenAIApp = (function () { if (containerId) { this._codeInterpreterContainerId = containerId; } - + return this; + }; + /** OPTIONAL * * Enable or disable server-side tool invocations for Gemini (Tool Combination). @@ -548,16 +552,23 @@ const GenAIApp = (function () { console.warn(`[GenAIApp] - Warning: input token usage (${this._lastUsage.input_tokens}) exceeded configured threshold (${this._inputTokenWarningThreshold}) for response ${responseMessage.id}`); } } - this._generatedFiles = this._extractContainerFileCitations(responseMessage); - if (this._generatedFiles.length > 0) { - this._lastContainerId = this._generatedFiles[0].containerId; - const blob = this._downloadContainerFile( - this._generatedFiles[0].containerId, - this._generatedFiles[0].fileId, - this._generatedFiles[0].filename - ); - const createdFile = DriveApp.createFile(blob); - this._lastGeneratedDriveFileUrl = createdFile.getUrl(); + if (model.includes("gemini")) { + if (this._codeInterpreterEnabled) { + this._generatedFiles = this._generatedFiles.concat(this._extractGeminiInlineArtifacts(responseMessage)); + } + } + else { + this._generatedFiles = this._extractContainerFileCitations(responseMessage); + if (this._generatedFiles.length > 0) { + this._lastContainerId = this._generatedFiles[0].containerId; + const blob = this._downloadContainerFile( + this._generatedFiles[0].containerId, + this._generatedFiles[0].fileId, + this._generatedFiles[0].filename + ); + const createdFile = DriveApp.createFile(blob); + this._lastGeneratedDriveFileUrl = createdFile.getUrl(); + } } // OpenAI Responses API returns top-level "id" @@ -844,6 +855,64 @@ const GenAIApp = (function () { return Object.keys(citationsById).map(fileId => citationsById[fileId]); }; + this._extractGeminiInlineArtifacts = function (responseMessage) { + const parts = responseMessage?.parts || []; + if (!Array.isArray(parts)) { + return []; + } + + const inputInlineData = {}; + const collectInputInlineData = (node) => { + if (!node) return; + if (Array.isArray(node)) { + node.forEach(collectInputInlineData); + return; + } + if (typeof node !== "object") return; + + const inlineData = node.inlineData || node.inline_data; + if (inlineData?.data) { + inputInlineData[inlineData.data] = true; + } + Object.keys(node).forEach(key => collectInputInlineData(node[key])); + }; + collectInputInlineData(contents); + + const extensionByMimeType = { + "application/json": "json", + "application/pdf": "pdf", + "application/zip": "zip", + "image/gif": "gif", + "image/jpeg": "jpg", + "image/png": "png", + "image/svg+xml": "svg", + "text/csv": "csv", + "text/html": "html", + "text/plain": "txt" + }; + + const getExtension = (mimeType) => { + if (!mimeType) return "bin"; + return extensionByMimeType[mimeType] || mimeType.split("/").pop().split(";")[0].replace(/^x-/, "") || "bin"; + }; + + const artifacts = []; + parts.forEach(part => { + const inlineData = part?.inlineData; + if (!inlineData?.data || inputInlineData[inlineData.data]) { + return; + } + const mimeType = inlineData.mimeType || inlineData.mime_type || "application/octet-stream"; + artifacts.push({ + mimeType: mimeType, + data: inlineData.data, + filename: `artifact_${artifacts.length}.${getExtension(mimeType)}` + }); + }); + + return artifacts; + }; + this._downloadContainerFile = function (containerId, fileId, filename) { const endpointUrl = `${apiBaseUrl}/v1/containers/${containerId}/files/${fileId}/content`; const response = _callGenAIApi(endpointUrl, null, "GET", true); @@ -860,7 +929,10 @@ const GenAIApp = (function () { /** * Returns generated files from the last run call. - * @returns {{containerId: string, fileId: string, filename: string}[]} Generated files metadata. + * The metadata structure is provider-specific: Gemini code execution artifacts are + * returned as {mimeType, data, filename}, while OpenAI code interpreter files are + * returned as {containerId, fileId, filename}. + * @returns {({mimeType: string, data: string, filename: string}|{containerId: string, fileId: string, filename: string})[]} Generated files metadata. */ this.getGeneratedFiles = function () { return this._generatedFiles; @@ -868,7 +940,9 @@ const GenAIApp = (function () { /** * Downloads a generated file from the last run. - * @param {string|number} fileIdOrIndex - File ID or index from getGeneratedFiles(). + * Gemini files are returned from in-memory base64 artifact data, while OpenAI files + * require an API call to download the file from the referenced code interpreter container. + * @param {string|number} fileIdOrIndex - File ID, filename, or index from getGeneratedFiles(). * @returns {Blob} Downloaded file blob that can be stored with DriveApp.createFile(blob). * @example * const chat = GenAIApp.newChat() @@ -892,15 +966,27 @@ const GenAIApp = (function () { targetFile = this._generatedFiles[fileIdOrIndex]; } else if (typeof fileIdOrIndex === "string") { - targetFile = this._generatedFiles.find(file => file.fileId === fileIdOrIndex); + targetFile = this._generatedFiles.find(file => file.fileId === fileIdOrIndex || file.filename === fileIdOrIndex); } if (!targetFile) { - throw new Error("[GenAIApp] - Generated file not found. Provide a valid file ID or index from getGeneratedFiles()."); + throw new Error("[GenAIApp] - Generated file not found. Provide a valid file ID, filename, or index from getGeneratedFiles()."); + } + if (targetFile.data) { + const blob = Utilities.newBlob(Utilities.base64Decode(targetFile.data), targetFile.mimeType, targetFile.filename); + return blob; } return this._downloadContainerFile(targetFile.containerId, targetFile.fileId, targetFile.filename); }; + /** + * Returns the OpenAI code interpreter container ID associated with generated files. + * Gemini code execution is stateless and has no container concept, so this returns null for Gemini models. + * @returns {string|null} OpenAI container ID, or null for Gemini models/no container. + */ this.getContainerId = function () { + if (model.includes("gemini")) { + return null; + } if (this._lastContainerId) { return this._lastContainerId; } @@ -973,6 +1059,12 @@ const GenAIApp = (function () { }); } + if (this._codeInterpreterEnabled) { + payload.tools.push({ + code_execution: {} + }); + } + return payload; } diff --git a/src/testFunctions.gs b/src/testFunctions.gs index fdb96a8..9c8cb24 100644 --- a/src/testFunctions.gs +++ b/src/testFunctions.gs @@ -15,6 +15,8 @@ function testAll() { testVision(); testMaximumAPICalls(); testInputTokenWarning(); + testGeminiCodeExecution(); + testGeminiCodeExecutionWithArtifact(); // OpenAI-only tests - require valid Drive file IDs. if (TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID) { testCodeInterpreterExcel(TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID); @@ -184,6 +186,83 @@ function testCodeInterpreterPDF(driveFileId) { console.log(`Generated files:\n${JSON.stringify(chat.getGeneratedFiles())}`); } +function testGeminiCodeExecution() { + GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); + + const chat = GenAIApp.newChat(); + chat + .enableCodeInterpreter() + .addMessage("Use code execution to compute the mean, median, and sum of this list: [4, 8, 15, 16, 23, 42]. Return the numeric results in your final answer."); + + const response = chat.run({ model: GEMINI_MODEL, max_tokens: 4000 }); + if (!response) { + throw new Error("Gemini code execution test failed: expected a response."); + } + if (chat.getContainerId() !== null) { + throw new Error("Gemini code execution test failed: Gemini should not return a container ID."); + } + + const generatedFiles = chat.getGeneratedFiles(); + console.log(`Gemini code execution response: +${response}`); + console.log(`Gemini code execution generated files: +${JSON.stringify(generatedFiles)}`); + + generatedFiles.forEach((artifact, index) => { + assertGeminiArtifactMetadata(artifact, `Gemini code execution artifact ${index}`); + const blob = chat.downloadGeneratedFile(index); + assertBlobLike(blob, `Gemini code execution artifact ${index}`); + }); +} + +function testGeminiCodeExecutionWithArtifact() { + GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); + + const chat = GenAIApp.newChat(); + chat + .enableCodeInterpreter() + .addMessage("Use code execution to create a PNG bar chart file showing quarterly revenue values Q1=12, Q2=18, Q3=9, Q4=24. Return the chart as an output artifact."); + + const response = chat.run({ model: GEMINI_MODEL, max_tokens: 4000 }); + if (!response) { + throw new Error("Gemini artifact test failed: expected a response."); + } + + const generatedFiles = chat.getGeneratedFiles(); + console.log(`Gemini code execution artifact response: +${response}`); + console.log(`Gemini code execution artifact files: +${JSON.stringify(generatedFiles)}`); + + if (generatedFiles.length === 0) { + throw new Error("Gemini artifact test failed: expected at least one generated artifact."); + } + + generatedFiles.forEach((artifact, index) => { + assertGeminiArtifactMetadata(artifact, `Gemini artifact ${index}`); + const blob = chat.downloadGeneratedFile(artifact.filename); + assertBlobLike(blob, `Gemini artifact ${index}`); + if (blob.getContentType() !== artifact.mimeType) { + throw new Error(`Gemini artifact ${index} failed: blob mime type ${blob.getContentType()} did not match ${artifact.mimeType}.`); + } + }); + + const savedFile = DriveApp.createFile(chat.downloadGeneratedFile(0)); + console.log(`Gemini generated artifact file url: ${savedFile.getUrl()}`); +} + +function assertGeminiArtifactMetadata(artifact, label) { + if (!artifact || !artifact.mimeType || !artifact.data || !artifact.filename) { + throw new Error(`${label} failed: expected mimeType, data, and filename fields.`); + } +} + +function assertBlobLike(blob, label) { + if (!blob || typeof blob.getBytes !== "function" || blob.getBytes().length === 0) { + throw new Error(`${label} failed: expected a non-empty Blob.`); + } +} + // Weather function implementation function getWeather(cityName) { return `The weather in ${cityName} is 19°C today.`; From 2aa70968f4481cb9521fd09faa082cc442cc8b1f Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:29:39 +0000 Subject: [PATCH 02/11] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- src/code.gs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code.gs b/src/code.gs index 81a82ef..cc30ff4 100644 --- a/src/code.gs +++ b/src/code.gs @@ -898,7 +898,7 @@ const GenAIApp = (function () { const artifacts = []; parts.forEach(part => { - const inlineData = part?.inlineData; + const inlineData = part?.inlineData || part?.inline_data; if (!inlineData?.data || inputInlineData[inlineData.data]) { return; } From f5954a0acd294d20a85365804104e613b0be2150 Mon Sep 17 00:00:00 2001 From: Benjamin Sayaque <91118734+Benjamin-Sayaque@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:51:11 +0200 Subject: [PATCH 03/11] Integrate spreadsheet CSV conversion for Gemini --- src/code.gs | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 2 deletions(-) diff --git a/src/code.gs b/src/code.gs index cc30ff4..9886284 100644 --- a/src/code.gs +++ b/src/code.gs @@ -162,14 +162,18 @@ const GenAIApp = (function () { this.addFile = function (fileInput) { let fileInfo; let blobToBase64; + let sourceFileId = null; + let sourceBlob = null; if (typeof fileInput == 'string') { // assume the input is a Google Drive ID + sourceFileId = fileInput; fileInfo = this._getBlobFromGoogleDrive(fileInput); blobToBase64 = Utilities.base64Encode(fileInfo.blob.getBytes()); } else if (isBlobLike(fileInput)) { // the input is a Blob + sourceBlob = fileInput; const fileBytes = fileInput.getBytes(); const fileSize = fileBytes.length; if (fileSize > MAX_FILE_SIZE) { @@ -205,12 +209,23 @@ const GenAIApp = (function () { }); // Gemini + let geminiMimeType = fileInfo.mimeType; + let geminiBase64Data = blobToBase64; + if (_isSpreadsheetMimeType(fileInfo.mimeType)) { + const csvResult = sourceFileId && fileInfo.mimeType === 'application/vnd.google-apps.spreadsheet' + ? _exportGoogleSheetsToCsv(sourceFileId) + : _convertXlsxBlobToCsv(sourceBlob || fileInfo.blob); + const csvWithMetadata = _formatSpreadsheetCsvForGemini(csvResult); + geminiMimeType = 'text/csv'; + geminiBase64Data = Utilities.base64Encode(csvWithMetadata); + } + contents.push({ role: 'user', parts: [{ inline_data: { - mime_type: fileInfo.mimeType, - data: blobToBase64 + mime_type: geminiMimeType, + data: geminiBase64Data } }] }); @@ -1097,6 +1112,9 @@ const GenAIApp = (function () { case 'image/jpeg': case 'image/gif': case 'image/webp': + case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + case 'application/vnd.ms-excel': + case 'application/vnd.oasis.opendocument.spreadsheet': blob = file.getBlob(); break; @@ -2143,6 +2161,23 @@ const GenAIApp = (function () { typeof x.getBytes === "function" && typeof x.getContentType === "function"; + /** + * Returns true when a MIME type identifies a spreadsheet file that can be + * converted to CSV for Gemini prompts. + * + * @private + * @param {string} mimeType - MIME type to inspect. + * @returns {boolean} Whether the MIME type is a supported spreadsheet. + */ + function _isSpreadsheetMimeType(mimeType) { + return [ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.google-apps.spreadsheet', + 'application/vnd.ms-excel', + 'application/vnd.oasis.opendocument.spreadsheet' + ].includes(mimeType); + } + // OpenAI-only helper: creates a Responses API input_file content object. const createOpenAIInputFileContent = (mimeType, base64Data, filename) => ({ type: "input_file", @@ -2172,6 +2207,168 @@ const GenAIApp = (function () { ); }; + const MAX_SPREADSHEET_CELLS = 500000; + + /** + * Converts an uploaded XLSX blob into CSV text by temporarily converting it to + * a native Google Sheets file. + * + * @private + * @param {Blob} blob - The XLSX blob to convert. + * @returns {{csvText: string, originalFilename: string, sheetNames: string[]}} Conversion result and metadata. + */ + function _convertXlsxBlobToCsv(blob) { + if (!isBlobLike(blob)) { + throw new Error('[GenAIApp] - Invalid XLSX input. Please provide a valid Blob.'); + } + + const originalFilename = blob.getName() || 'spreadsheet.xlsx'; + _assertBlobSizeWithinLimit(blob, originalFilename); + + let convertedFile; + try { + const conversionBlob = blob.copyBlob() + .setName(originalFilename) + .setContentType(MimeType.GOOGLE_SHEETS); + convertedFile = DriveApp.createFile(conversionBlob); + + const spreadsheet = SpreadsheetApp.openById(convertedFile.getId()); + return _spreadsheetToCsvResult(spreadsheet, originalFilename); + } + finally { + if (convertedFile) { + convertedFile.setTrashed(true); + } + } + } + + /** + * Exports a native Google Sheets file to concatenated CSV text. Hidden and + * empty sheets are omitted. + * + * @private + * @param {string} fileId - The Google Sheets Drive file ID. + * @returns {{csvText: string, originalFilename: string, sheetNames: string[]}} Export result and metadata. + */ + function _exportGoogleSheetsToCsv(fileId) { + const file = DriveApp.getFileById(fileId); + const fileSize = file.getSize(); + if (fileSize > MAX_FILE_SIZE) { + throw new Error(`[GenAIApp] - Spreadsheet file too large (${fileSize} bytes). Maximum allowed size is ${MAX_FILE_SIZE} bytes.`); + } + + const spreadsheet = SpreadsheetApp.openById(fileId); + return _spreadsheetToCsvResult(spreadsheet, file.getName()); + } + + /** + * Converts a two-dimensional array to RFC 4180-compatible CSV text. + * + * @private + * @param {Array[]} dataArray - Two-dimensional row/cell array. + * @returns {string} CSV text. + */ + function _arrayToCsv(dataArray) { + if (!dataArray || dataArray.length === 0) { + return ''; + } + + return dataArray.map(row => { + const cells = Array.isArray(row) ? row : [row]; + return cells.map(cell => { + const value = cell === null || typeof cell === 'undefined' ? '' : String(cell); + const escapedValue = value.replace(/"/g, '""'); + return /[",\r\n]/.test(value) ? `"${escapedValue}"` : escapedValue; + }).join(','); + }).join('\n'); + } + + /** + * Creates a concatenated CSV export from visible, non-empty spreadsheet sheets. + * + * @private + * @param {Spreadsheet} spreadsheet - The spreadsheet to export. + * @param {string} originalFilename - Source filename for metadata. + * @returns {{csvText: string, originalFilename: string, sheetNames: string[]}} CSV export and metadata. + */ + function _spreadsheetToCsvResult(spreadsheet, originalFilename) { + const csvSections = []; + const sheetNames = []; + let totalCells = 0; + + spreadsheet.getSheets().forEach(sheet => { + if (sheet.isSheetHidden()) { + return; + } + + const lastRow = sheet.getLastRow(); + const lastColumn = sheet.getLastColumn(); + if (lastRow === 0 || lastColumn === 0) { + return; + } + + totalCells += lastRow * lastColumn; + if (totalCells > MAX_SPREADSHEET_CELLS) { + throw new Error(`[GenAIApp] - Spreadsheet too large to convert (${totalCells} populated-range cells). Maximum allowed is ${MAX_SPREADSHEET_CELLS} cells.`); + } + + const values = sheet.getDataRange().getValues(); + if (_isEmptySheetData(values)) { + return; + } + + sheetNames.push(sheet.getName()); + csvSections.push(`--- Sheet: ${sheet.getName()} ---\n${_arrayToCsv(values)}`); + }); + + return { + csvText: csvSections.join('\n\n'), + originalFilename: originalFilename, + sheetNames: sheetNames + }; + } + + /** + * Formats converted spreadsheet CSV with a filename/sheet metadata preamble for Gemini. + * + * @private + * @param {{csvText: string, originalFilename: string, sheetNames: string[]}} csvResult - Spreadsheet CSV result. + * @returns {string} CSV text prefixed with spreadsheet metadata. + */ + function _formatSpreadsheetCsvForGemini(csvResult) { + const sheetNames = csvResult.sheetNames && csvResult.sheetNames.length > 0 + ? csvResult.sheetNames.join(', ') + : 'None'; + return `[Spreadsheet: ${csvResult.originalFilename} | Sheets: ${sheetNames}]\n\n${csvResult.csvText}`; + } + + /** + * Throws when a blob exceeds the shared maximum file size. + * + * @private + * @param {Blob} blob - Blob to check. + * @param {string} filename - Filename used in the error message. + */ + function _assertBlobSizeWithinLimit(blob, filename) { + const fileSize = blob.getBytes().length; + if (fileSize > MAX_FILE_SIZE) { + throw new Error(`[GenAIApp] - Spreadsheet file too large (${filename}, ${fileSize} bytes). Maximum allowed size is ${MAX_FILE_SIZE} bytes.`); + } + } + + /** + * Returns true when a sheet data array contains no non-empty values. + * + * @private + * @param {Array[]} values - Sheet values from getValues(). + * @returns {boolean} Whether the sheet data is empty. + */ + function _isEmptySheetData(values) { + return !values || values.length === 0 || values.every(row => + !row || row.every(cell => cell === null || typeof cell === 'undefined' || cell === '') + ); + } + /** * Uploads a file to OpenAI and returns the file ID. * From b97e54064986033a769d18b2910d78bbf796068f Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:11:15 +0000 Subject: [PATCH 04/11] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit --- src/code.gs | 100 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/src/code.gs b/src/code.gs index 9886284..fdff241 100644 --- a/src/code.gs +++ b/src/code.gs @@ -208,27 +208,30 @@ const GenAIApp = (function () { content: [contentObj] }); - // Gemini - let geminiMimeType = fileInfo.mimeType; - let geminiBase64Data = blobToBase64; + // Gemini - defer spreadsheet conversion until _buildGeminiPayload if (_isSpreadsheetMimeType(fileInfo.mimeType)) { - const csvResult = sourceFileId && fileInfo.mimeType === 'application/vnd.google-apps.spreadsheet' - ? _exportGoogleSheetsToCsv(sourceFileId) - : _convertXlsxBlobToCsv(sourceBlob || fileInfo.blob); - const csvWithMetadata = _formatSpreadsheetCsvForGemini(csvResult); - geminiMimeType = 'text/csv'; - geminiBase64Data = Utilities.base64Encode(csvWithMetadata); + // Mark for lazy conversion - store metadata instead of converting now + contents.push({ + role: 'user', + parts: [{ + _needsSpreadsheetConversion: true, + _sourceFileId: sourceFileId, + _sourceBlob: sourceBlob || fileInfo.blob, + _mimeType: fileInfo.mimeType + }] + }); + } else { + // Non-spreadsheet files can be encoded immediately + contents.push({ + role: 'user', + parts: [{ + inline_data: { + mime_type: fileInfo.mimeType, + data: blobToBase64 + } + }] + }); } - - contents.push({ - role: 'user', - parts: [{ - inline_data: { - mime_type: geminiMimeType, - data: geminiBase64Data - } - }] - }); return this; }; @@ -1021,8 +1024,42 @@ const GenAIApp = (function () { * @throws {Error} If an incompatible feature is selected (e.g., assistant usage with the Gemini model). */ this._buildGeminiPayload = function (advancedParametersObject) { + // Process contents array to handle lazy spreadsheet conversion + const processedContents = contents.map(content => { + if (content.role === 'user' && content.parts) { + const processedParts = content.parts.map(part => { + if (part._needsSpreadsheetConversion) { + // Perform lazy spreadsheet conversion here + const csvResult = part._sourceFileId && part._mimeType === 'application/vnd.google-apps.spreadsheet' + ? _exportGoogleSheetsToCsv(part._sourceFileId) + : _convertXlsxBlobToCsv(part._sourceBlob); + const csvWithMetadata = _formatSpreadsheetCsvForGemini(csvResult); + + // Issue 2: Validate CSV size before base64 encoding + const csvByteLength = Utilities.newBlob(csvWithMetadata).getBytes().length; + const base64Size = Math.ceil(csvByteLength * 4 / 3); + const MAX_INLINE_UPLOAD_SIZE = 20 * 1024 * 1024; // 20 MB + + if (base64Size > MAX_INLINE_UPLOAD_SIZE) { + throw new Error(`[GenAIApp] - Spreadsheet CSV too large after conversion (${base64Size} bytes base64-encoded). Maximum allowed size for inline upload is ${MAX_INLINE_UPLOAD_SIZE} bytes.`); + } + + return { + inline_data: { + mime_type: 'text/csv', + data: Utilities.base64Encode(csvWithMetadata) + } + }; + } + return part; + }); + return { ...content, parts: processedParts }; + } + return content; + }); + const payload = { - 'contents': contents, + 'contents': processedContents, 'model': model, 'generationConfig': { maxOutputTokens: max_tokens, @@ -2225,19 +2262,24 @@ const GenAIApp = (function () { const originalFilename = blob.getName() || 'spreadsheet.xlsx'; _assertBlobSizeWithinLimit(blob, originalFilename); - let convertedFile; + let convertedFileId; try { - const conversionBlob = blob.copyBlob() - .setName(originalFilename) - .setContentType(MimeType.GOOGLE_SHEETS); - convertedFile = DriveApp.createFile(conversionBlob); + // Use Advanced Drive Service to perform XLSX → Google Sheets conversion + const resource = { + title: originalFilename, + mimeType: MimeType.GOOGLE_SHEETS + }; + const file = Drive.Files.insert(resource, blob, { + convert: true + }); + convertedFileId = file.id; - const spreadsheet = SpreadsheetApp.openById(convertedFile.getId()); + const spreadsheet = SpreadsheetApp.openById(convertedFileId); return _spreadsheetToCsvResult(spreadsheet, originalFilename); } finally { - if (convertedFile) { - convertedFile.setTrashed(true); + if (convertedFileId) { + Drive.Files.remove(convertedFileId); } } } @@ -2312,7 +2354,7 @@ const GenAIApp = (function () { throw new Error(`[GenAIApp] - Spreadsheet too large to convert (${totalCells} populated-range cells). Maximum allowed is ${MAX_SPREADSHEET_CELLS} cells.`); } - const values = sheet.getDataRange().getValues(); + const values = sheet.getDataRange().getDisplayValues(); if (_isEmptySheetData(values)) { return; } From 85b3f69794de49fc7d6b648b5958b9ab882cecf6 Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 15:35:17 +0200 Subject: [PATCH 05/11] Don't convert if openai is provider --- src/code.gs | 125 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 88 insertions(+), 37 deletions(-) diff --git a/src/code.gs b/src/code.gs index fdff241..89c8a77 100644 --- a/src/code.gs +++ b/src/code.gs @@ -168,27 +168,55 @@ const GenAIApp = (function () { if (typeof fileInput == 'string') { // assume the input is a Google Drive ID sourceFileId = fileInput; - fileInfo = this._getBlobFromGoogleDrive(fileInput); - blobToBase64 = Utilities.base64Encode(fileInfo.blob.getBytes()); + const file = DriveApp.getFileById(fileInput); + fileInfo = { + mimeType: file.getMimeType(), + fileName: file.getName() + }; + if (!_isSpreadsheetMimeType(fileInfo.mimeType)) { + fileInfo = this._getBlobFromGoogleDrive(fileInput); + blobToBase64 = Utilities.base64Encode(fileInfo.blob.getBytes()); + } } else if (isBlobLike(fileInput)) { // the input is a Blob sourceBlob = fileInput; - const fileBytes = fileInput.getBytes(); - const fileSize = fileBytes.length; - if (fileSize > MAX_FILE_SIZE) { - throw new Error(`File too large (${fileSize} bytes). Maximum allowed size is ${MAX_FILE_SIZE} bytes.`); - } - blobToBase64 = Utilities.base64Encode(fileBytes); fileInfo = { mimeType: fileInput.getContentType(), fileName: fileInput.getName() }; + if (!_isSpreadsheetMimeType(fileInfo.mimeType)) { + const fileBytes = fileInput.getBytes(); + const fileSize = fileBytes.length; + if (fileSize > MAX_FILE_SIZE) { + throw new Error(`File too large (${fileSize} bytes). Maximum allowed size is ${MAX_FILE_SIZE} bytes.`); + } + blobToBase64 = Utilities.base64Encode(fileBytes); + } } else { throw new Error('Invalid file input provided to addFile() method. Please provide a valid Google Drive file ID or a Blob.'); } + if (_isSpreadsheetMimeType(fileInfo.mimeType)) { + const lazySpreadsheetPart = { + _needsSpreadsheetConversion: true, + _sourceFileId: sourceFileId, + _sourceBlob: sourceBlob, + _mimeType: fileInfo.mimeType, + _fileName: fileInfo.fileName + }; + messages.push({ + role: "user", + content: [lazySpreadsheetPart] + }); + contents.push({ + role: 'user', + parts: [lazySpreadsheetPart] + }); + return this; + } + // OpenAI const contentObj = {}; if (fileInfo.mimeType.startsWith("image/")) { @@ -208,30 +236,15 @@ const GenAIApp = (function () { content: [contentObj] }); - // Gemini - defer spreadsheet conversion until _buildGeminiPayload - if (_isSpreadsheetMimeType(fileInfo.mimeType)) { - // Mark for lazy conversion - store metadata instead of converting now - contents.push({ - role: 'user', - parts: [{ - _needsSpreadsheetConversion: true, - _sourceFileId: sourceFileId, - _sourceBlob: sourceBlob || fileInfo.blob, - _mimeType: fileInfo.mimeType - }] - }); - } else { - // Non-spreadsheet files can be encoded immediately - contents.push({ - role: 'user', - parts: [{ - inline_data: { - mime_type: fileInfo.mimeType, - data: blobToBase64 - } - }] - }); - } + contents.push({ + role: 'user', + parts: [{ + inline_data: { + mime_type: fileInfo.mimeType, + data: blobToBase64 + } + }] + }); return this; }; @@ -738,12 +751,45 @@ const GenAIApp = (function () { let systemInstructions = ""; const userMessages = []; + const resolveOpenAIContentPart = (part) => { + if (!part?._needsSpreadsheetConversion) { + return part; + } + + const fileInfo = part._sourceFileId + ? this._getBlobFromGoogleDrive(part._sourceFileId) + : { + blob: part._sourceBlob, + mimeType: part._mimeType, + fileName: part._fileName + }; + const fileBytes = fileInfo.blob.getBytes(); + const fileSize = fileBytes.length; + if (fileSize > MAX_FILE_SIZE) { + throw new Error(`File too large (${fileSize} bytes). Maximum allowed size is ${MAX_FILE_SIZE} bytes.`); + } + + return createOpenAIInputFileContent( + fileInfo.mimeType, + Utilities.base64Encode(fileBytes), + fileInfo.fileName + ); + }; + for (const message of messages) { if (message.role === "system") { systemInstructions += message.content + "\n"; } else { - userMessages.push(message); + if (Array.isArray(message.content)) { + userMessages.push({ + ...message, + content: message.content.map(resolveOpenAIContentPart) + }); + } + else { + userMessages.push(message); + } } } if (systemInstructions !== "") { @@ -1030,9 +1076,14 @@ const GenAIApp = (function () { const processedParts = content.parts.map(part => { if (part._needsSpreadsheetConversion) { // Perform lazy spreadsheet conversion here - const csvResult = part._sourceFileId && part._mimeType === 'application/vnd.google-apps.spreadsheet' - ? _exportGoogleSheetsToCsv(part._sourceFileId) - : _convertXlsxBlobToCsv(part._sourceBlob); + let csvResult; + if (part._sourceFileId && part._mimeType === 'application/vnd.google-apps.spreadsheet') { + csvResult = _exportGoogleSheetsToCsv(part._sourceFileId); + } + else { + const sourceBlob = part._sourceBlob || (part._sourceFileId ? DriveApp.getFileById(part._sourceFileId).getBlob() : null); + csvResult = _convertXlsxBlobToCsv(sourceBlob); + } const csvWithMetadata = _formatSpreadsheetCsvForGemini(csvResult); // Issue 2: Validate CSV size before base64 encoding @@ -2957,4 +3008,4 @@ const GenAIApp = (function () { privateInstanceBaseUrl = baseUrl; } } -})(); \ No newline at end of file +})(); From e0dd35bc79a07b5c4264357f3f600a8a583819d8 Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 16:22:51 +0200 Subject: [PATCH 06/11] fix --- src/code.gs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/code.gs b/src/code.gs index 89c8a77..1368621 100644 --- a/src/code.gs +++ b/src/code.gs @@ -2310,6 +2310,10 @@ const GenAIApp = (function () { throw new Error('[GenAIApp] - Invalid XLSX input. Please provide a valid Blob.'); } + if (typeof Drive === 'undefined' || !Drive.Files || typeof Drive.Files.insert !== 'function') { + throw new Error('[GenAIApp] - Advanced Drive Service is required for XLSX to CSV conversion. Please enable it in your Apps Script project.'); + } + const originalFilename = blob.getName() || 'spreadsheet.xlsx'; _assertBlobSizeWithinLimit(blob, originalFilename); From 3bc544bd3d79ed6d50b6a74d05d9175ce73a2f15 Mon Sep 17 00:00:00 2001 From: aubrypaul <62645653+aubrypaul@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:31:57 +0200 Subject: [PATCH 07/11] Update src/code.gs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/code.gs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/code.gs b/src/code.gs index b5dbae1..482d49a 100644 --- a/src/code.gs +++ b/src/code.gs @@ -355,11 +355,10 @@ const GenAIApp = (function () { } return this; }; -<<<<<<< codex/add-code-execution-tool-to-payload-builder - -======= - ->>>>>>> main + return this; + }; + + /** OPTIONAL /** OPTIONAL * * Enable or disable server-side tool invocations for Gemini (Tool Combination). From cb567f2b75bb0944a44aebc4eb40c896ff7ff105 Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 16:46:36 +0200 Subject: [PATCH 08/11] fix --- src/code.gs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/code.gs b/src/code.gs index 482d49a..79d6a9b 100644 --- a/src/code.gs +++ b/src/code.gs @@ -355,11 +355,8 @@ const GenAIApp = (function () { } return this; }; - return this; - }; /** OPTIONAL - /** OPTIONAL * * Enable or disable server-side tool invocations for Gemini (Tool Combination). * @param {boolean} enabled - True to enable tool combination. @@ -1029,7 +1026,7 @@ const GenAIApp = (function () { else if (typeof fileIdOrIndex === "string" && fileIdOrIndex.trim() === "") { targetFile = this._generatedFiles[0]; } - if (typeof fileIdOrIndex === "number") { + else if (typeof fileIdOrIndex === "number") { targetFile = this._generatedFiles[fileIdOrIndex]; } else if (typeof fileIdOrIndex === "string") { From 35f0ecc44f829d903fb9d591970d034504b8d527 Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Tue, 9 Jun 2026 18:31:32 +0200 Subject: [PATCH 09/11] fix --- src/code.gs | 85 +++++++++++++++++++++------- src/testFunctions.gs | 131 ++++++++++--------------------------------- 2 files changed, 93 insertions(+), 123 deletions(-) diff --git a/src/code.gs b/src/code.gs index 79d6a9b..c8ced64 100644 --- a/src/code.gs +++ b/src/code.gs @@ -34,6 +34,11 @@ const GenAIApp = (function () { const addedVectorStores = {}; const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB in bytes + + // Timeout for HTTP requests: Workspace accounts allow up to 30 minutes (1800s), + // but consumer accounts are limited to 6 minutes (360s). This value is configurable + // via Apps Script properties and should only be increased for Workspace accounts. + const TIMEOUT_SECONDS = parseInt(PropertiesService.getScriptProperties().getProperty('TIMEOUT_SECONDS') || '360'); /** * @class @@ -355,7 +360,7 @@ const GenAIApp = (function () { } return this; }; - + /** OPTIONAL * * Enable or disable server-side tool invocations for Gemini (Tool Combination). @@ -769,10 +774,28 @@ const GenAIApp = (function () { throw new Error(`File too large (${fileSize} bytes). Maximum allowed size is ${MAX_FILE_SIZE} bytes.`); } + // When exporting Google Sheets/Docs/Presentations, _getBlobFromGoogleDrive returns + // an exported blob (PDF/DOCX/etc.). Use the blob's actual content type and filename + // to ensure metadata matches the exported content, not the original file type. + let mimeType = fileInfo.mimeType; + let fileName = fileInfo.fileName; + if (fileInfo.mimeType === 'application/vnd.google-apps.spreadsheet' || + fileInfo.mimeType === 'application/vnd.google-apps.document' || + fileInfo.mimeType === 'application/vnd.google-apps.presentation') { + mimeType = fileInfo.blob.getContentType() || fileInfo.mimeType; + // Update file extension to match actual content type + const extension = mimeType.includes('pdf') ? '.pdf' : + mimeType.includes('document') ? '.docx' : + mimeType.includes('presentation') ? '.pptx' : ''; + if (extension) { + fileName = fileName.replace(/\.[^/.]+$/, extension); + } + } + return createOpenAIInputFileContent( - fileInfo.mimeType, + mimeType, Utilities.base64Encode(fileBytes), - fileInfo.fileName + fileName ); }; @@ -1073,23 +1096,35 @@ const GenAIApp = (function () { // Process contents array to handle lazy spreadsheet conversion const processedContents = contents.map(content => { if (content.role === 'user' && content.parts) { - const processedParts = content.parts.map(part => { + const contentParts = Array.isArray(content.parts) + ? content.parts + : [content.parts]; + + const processedParts = contentParts.map(part => { if (part._needsSpreadsheetConversion) { - // Perform lazy spreadsheet conversion here let csvResult; + if (part._sourceFileId && part._mimeType === 'application/vnd.google-apps.spreadsheet') { csvResult = _exportGoogleSheetsToCsv(part._sourceFileId); - } - else { - const sourceBlob = part._sourceBlob || (part._sourceFileId ? DriveApp.getFileById(part._sourceFileId).getBlob() : null); + } else { + const sourceBlob = part._sourceBlob || ( + part._sourceFileId + ? DriveApp.getFileById(part._sourceFileId).getBlob() + : null + ); + + if (!sourceBlob) { + throw new Error('[GenAIApp] - Spreadsheet conversion failed: missing source blob or source file ID.'); + } + csvResult = _convertXlsxBlobToCsv(sourceBlob); } + const csvWithMetadata = _formatSpreadsheetCsvForGemini(csvResult); - // Issue 2: Validate CSV size before base64 encoding const csvByteLength = Utilities.newBlob(csvWithMetadata).getBytes().length; - const base64Size = Math.ceil(csvByteLength * 4 / 3); - const MAX_INLINE_UPLOAD_SIZE = 20 * 1024 * 1024; // 20 MB + const base64Size = Math.ceil(csvByteLength / 3) * 4; + const MAX_INLINE_UPLOAD_SIZE = 20 * 1024 * 1024; if (base64Size > MAX_INLINE_UPLOAD_SIZE) { throw new Error(`[GenAIApp] - Spreadsheet CSV too large after conversion (${base64Size} bytes base64-encoded). Maximum allowed size for inline upload is ${MAX_INLINE_UPLOAD_SIZE} bytes.`); @@ -1102,10 +1137,16 @@ const GenAIApp = (function () { } }; } + return part; }); - return { ...content, parts: processedParts }; + + return { + ...content, + parts: processedParts + }; } + return content; }); @@ -1790,7 +1831,7 @@ const GenAIApp = (function () { const options = { method: method.toLowerCase(), headers: headers, - timeoutSeconds: 30 * 60, + timeoutSeconds: TIMEOUT_SECONDS, muteHttpExceptions: true }; if (payload !== null && payload !== undefined) { @@ -2310,7 +2351,7 @@ const GenAIApp = (function () { throw new Error('[GenAIApp] - Invalid XLSX input. Please provide a valid Blob.'); } - if (typeof Drive === 'undefined' || !Drive.Files || typeof Drive.Files.insert !== 'function') { + if (typeof Drive === 'undefined') { throw new Error('[GenAIApp] - Advanced Drive Service is required for XLSX to CSV conversion. Please enable it in your Apps Script project.'); } @@ -2318,22 +2359,24 @@ const GenAIApp = (function () { _assertBlobSizeWithinLimit(blob, originalFilename); let convertedFileId; + try { - // Use Advanced Drive Service to perform XLSX → Google Sheets conversion + // Use Advanced Drive Service to perform XLSX -> Google Sheets conversion const resource = { - title: originalFilename, + name: originalFilename, mimeType: MimeType.GOOGLE_SHEETS }; - const file = Drive.Files.insert(resource, blob, { + + const file = Drive.Files.create(resource, blob, { convert: true }); + convertedFileId = file.id; const spreadsheet = SpreadsheetApp.openById(convertedFileId); return _spreadsheetToCsvResult(spreadsheet, originalFilename); - } - finally { - if (convertedFileId) { + } finally { + if (convertedFileId && typeof Drive.Files.remove === 'function') { Drive.Files.remove(convertedFileId); } } @@ -3012,4 +3055,4 @@ const GenAIApp = (function () { privateInstanceBaseUrl = baseUrl; } } -})(); +})(); \ No newline at end of file diff --git a/src/testFunctions.gs b/src/testFunctions.gs index 9c8cb24..f577ab5 100644 --- a/src/testFunctions.gs +++ b/src/testFunctions.gs @@ -1,6 +1,6 @@ const GPT_MODEL = "gpt-5.4"; const REASONING_MODEL = "o4-mini"; -const GEMINI_MODEL = "gemini-2.5-pro"; +const GEMINI_MODEL = "gemini-3.5-flash"; const TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID = ""; const TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID = ""; @@ -12,12 +12,10 @@ function testAll() { testFunctionCallingOnlyReturnArguments(); testBrowsing(); testKnowledgeLink(); - testVision(); + //testVision(); testMaximumAPICalls(); testInputTokenWarning(); - testGeminiCodeExecution(); - testGeminiCodeExecutionWithArtifact(); - // OpenAI-only tests - require valid Drive file IDs. + // Code interpreter tests - require valid Drive file IDs. if (TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID) { testCodeInterpreterExcel(TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID); } @@ -35,16 +33,26 @@ function runTestAcrossModels(testName, setupFunction, runOptions = {}) { const models = [ { name: GPT_MODEL, label: "GPT" }, - { name: REASONING_MODEL, label: "reasoning" }, { name: GEMINI_MODEL, label: "gemini" } ]; models.forEach(model => { const chat = GenAIApp.newChat(); setupFunction(chat); + const options = { model: model.name, ...runOptions }; const response = chat.run(options); - console.log(`${testName} ${model.label}:\n${response}`); + + let logMessage = `${testName} ${model.label}:\n${response}`; + + if (typeof chat.getGeneratedFiles === 'function') { + const generatedFiles = chat.getGeneratedFiles(); + if (generatedFiles && generatedFiles.length > 0) { + logMessage += `\nGenerated files:\n${JSON.stringify(generatedFiles)}`; + } + } + + console.log(logMessage); }); } @@ -161,106 +169,25 @@ ${highThresholdResponse}`); } function testCodeInterpreterExcel(driveFileId) { - GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); const inputBlob = DriveApp.getFileById(driveFileId).getBlob(); - const chat = GenAIApp.newChat(); - chat - .addFile(inputBlob) - .enableCodeInterpreter() - .addMessage("Add a new column at the end that calculates row totals for all numeric columns. Then generate and attach the updated Excel file as output."); - const response = chat.run({ model: GPT_MODEL, max_tokens: 4000 }); - console.log(`Generated Excel file url: ${response}`); - console.log(`Generated files:\n${JSON.stringify(chat.getGeneratedFiles())}`); + + runTestAcrossModels("Code Interpreter Excel", chat => { + chat + .addFile(inputBlob) + .enableCodeInterpreter() + .addMessage("Add a new column at the end that calculates row totals for all numeric columns. Then generate and attach the updated Excel file as output."); + }, { max_tokens: 4000 }); } function testCodeInterpreterPDF(driveFileId) { - GenAIApp.setOpenAIAPIKey(OPEN_AI_API_KEY); const inputBlob = DriveApp.getFileById(driveFileId).getBlob(); - const chat = GenAIApp.newChat(); - chat - .addFile(inputBlob) - .enableCodeInterpreter() - .addMessage("Add a summary paragraph at the top of the document describing its main contents. Then generate and attach the updated PDF file as output."); - const response = chat.run({ model: GPT_MODEL, max_tokens: 4000 }); - console.log(`Generated PDF file url: ${response}`); - console.log(`Generated files:\n${JSON.stringify(chat.getGeneratedFiles())}`); -} - -function testGeminiCodeExecution() { - GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); - - const chat = GenAIApp.newChat(); - chat - .enableCodeInterpreter() - .addMessage("Use code execution to compute the mean, median, and sum of this list: [4, 8, 15, 16, 23, 42]. Return the numeric results in your final answer."); - - const response = chat.run({ model: GEMINI_MODEL, max_tokens: 4000 }); - if (!response) { - throw new Error("Gemini code execution test failed: expected a response."); - } - if (chat.getContainerId() !== null) { - throw new Error("Gemini code execution test failed: Gemini should not return a container ID."); - } - - const generatedFiles = chat.getGeneratedFiles(); - console.log(`Gemini code execution response: -${response}`); - console.log(`Gemini code execution generated files: -${JSON.stringify(generatedFiles)}`); - - generatedFiles.forEach((artifact, index) => { - assertGeminiArtifactMetadata(artifact, `Gemini code execution artifact ${index}`); - const blob = chat.downloadGeneratedFile(index); - assertBlobLike(blob, `Gemini code execution artifact ${index}`); - }); -} - -function testGeminiCodeExecutionWithArtifact() { - GenAIApp.setGeminiAPIKey(GEMINI_API_KEY); - - const chat = GenAIApp.newChat(); - chat - .enableCodeInterpreter() - .addMessage("Use code execution to create a PNG bar chart file showing quarterly revenue values Q1=12, Q2=18, Q3=9, Q4=24. Return the chart as an output artifact."); - - const response = chat.run({ model: GEMINI_MODEL, max_tokens: 4000 }); - if (!response) { - throw new Error("Gemini artifact test failed: expected a response."); - } - - const generatedFiles = chat.getGeneratedFiles(); - console.log(`Gemini code execution artifact response: -${response}`); - console.log(`Gemini code execution artifact files: -${JSON.stringify(generatedFiles)}`); - - if (generatedFiles.length === 0) { - throw new Error("Gemini artifact test failed: expected at least one generated artifact."); - } - - generatedFiles.forEach((artifact, index) => { - assertGeminiArtifactMetadata(artifact, `Gemini artifact ${index}`); - const blob = chat.downloadGeneratedFile(artifact.filename); - assertBlobLike(blob, `Gemini artifact ${index}`); - if (blob.getContentType() !== artifact.mimeType) { - throw new Error(`Gemini artifact ${index} failed: blob mime type ${blob.getContentType()} did not match ${artifact.mimeType}.`); - } - }); - - const savedFile = DriveApp.createFile(chat.downloadGeneratedFile(0)); - console.log(`Gemini generated artifact file url: ${savedFile.getUrl()}`); -} - -function assertGeminiArtifactMetadata(artifact, label) { - if (!artifact || !artifact.mimeType || !artifact.data || !artifact.filename) { - throw new Error(`${label} failed: expected mimeType, data, and filename fields.`); - } -} - -function assertBlobLike(blob, label) { - if (!blob || typeof blob.getBytes !== "function" || blob.getBytes().length === 0) { - throw new Error(`${label} failed: expected a non-empty Blob.`); - } + + runTestAcrossModels("Code Interpreter PDF", chat => { + chat + .addFile(inputBlob) + .enableCodeInterpreter() + .addMessage("Add a summary paragraph at the top of the document describing its main contents. Then generate and attach the updated PDF file as output."); + }, { max_tokens: 4000 }); } // Weather function implementation From e9130f06c71ba3fb9187b29ab3e338ddde97a64a Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Wed, 10 Jun 2026 11:28:50 +0200 Subject: [PATCH 10/11] fix --- src/code.gs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/code.gs b/src/code.gs index c8ced64..9c6e0c3 100644 --- a/src/code.gs +++ b/src/code.gs @@ -34,11 +34,6 @@ const GenAIApp = (function () { const addedVectorStores = {}; const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB in bytes - - // Timeout for HTTP requests: Workspace accounts allow up to 30 minutes (1800s), - // but consumer accounts are limited to 6 minutes (360s). This value is configurable - // via Apps Script properties and should only be increased for Workspace accounts. - const TIMEOUT_SECONDS = parseInt(PropertiesService.getScriptProperties().getProperty('TIMEOUT_SECONDS') || '360'); /** * @class @@ -1831,7 +1826,7 @@ const GenAIApp = (function () { const options = { method: method.toLowerCase(), headers: headers, - timeoutSeconds: TIMEOUT_SECONDS, + timeoutSeconds: 30 * 60, muteHttpExceptions: true }; if (payload !== null && payload !== undefined) { From cf320d48018114aef9cb673e67a56f5268b4821c Mon Sep 17 00:00:00 2001 From: Paul Aubry Date: Wed, 10 Jun 2026 11:40:16 +0200 Subject: [PATCH 11/11] fix --- src/code.gs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/code.gs b/src/code.gs index 9c6e0c3..e0a6c7c 100644 --- a/src/code.gs +++ b/src/code.gs @@ -770,20 +770,17 @@ const GenAIApp = (function () { } // When exporting Google Sheets/Docs/Presentations, _getBlobFromGoogleDrive returns - // an exported blob (PDF/DOCX/etc.). Use the blob's actual content type and filename - // to ensure metadata matches the exported content, not the original file type. + // an exported blob, which for Google Apps files is PDF. Use the blob's actual + // content type and ensure the filename extension matches the exported content. let mimeType = fileInfo.mimeType; let fileName = fileInfo.fileName; if (fileInfo.mimeType === 'application/vnd.google-apps.spreadsheet' || fileInfo.mimeType === 'application/vnd.google-apps.document' || fileInfo.mimeType === 'application/vnd.google-apps.presentation') { mimeType = fileInfo.blob.getContentType() || fileInfo.mimeType; - // Update file extension to match actual content type - const extension = mimeType.includes('pdf') ? '.pdf' : - mimeType.includes('document') ? '.docx' : - mimeType.includes('presentation') ? '.pptx' : ''; + const extension = mimeType.includes('pdf') ? '.pdf' : ''; if (extension) { - fileName = fileName.replace(/\.[^/.]+$/, extension); + fileName = fileName.replace(/(\.[^/.]+)?$/, extension); } }