Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 139 additions & 3 deletions src/code.gs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ const GenAIApp = (function () {
let browsing = false;
let reasoning_effort = "medium";
let knowledgeLink = [];
this._codeInterpreterEnabled = false;
this._codeInterpreterContainerId = null;
this._generatedFiles = [];
this._lastContainerId = null;
let compaction_enabled = false;
let compaction_threshold = 10000;

Expand Down Expand Up @@ -295,6 +299,28 @@ const GenAIApp = (function () {
return this;
};

/**
* Enables OpenAI Code Interpreter support for this chat.
* @param {string} [containerId] - OPTIONAL - Explicit container ID to reuse.
* @returns {Chat} - The current Chat instance.
* @example
* const chat = GenAIApp.newChat()
* .addFile(DriveApp.getFileById("YOUR_FILE_ID").getBlob())
* .enableCodeInterpreter()
* .addMessage("Process this file and generate an updated version.");
* chat.run({ model: "gpt-5.4" });
* const generatedFiles = chat.getGeneratedFiles();
* const blob = chat.downloadGeneratedFile(0);
* DriveApp.createFile(blob);
*/
this.enableCodeInterpreter = function (containerId) {
this._codeInterpreterEnabled = true;
if (containerId) {
this._codeInterpreterContainerId = containerId;
}
return this;
};

/**
* Includes the content of a web page in the prompt sent to openAI
* @param {string} url - the url of the webpage you want to fetch
Expand Down Expand Up @@ -425,6 +451,8 @@ const GenAIApp = (function () {
this.run = function (advancedParametersObject) {
this._lastUsage = null;
last_response_id = null;
this._generatedFiles = [];
this._lastContainerId = null;

model = advancedParametersObject?.model ?? model;
temperature = advancedParametersObject?.temperature ?? temperature;
Expand Down Expand Up @@ -508,6 +536,10 @@ 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;
}
Comment on lines +541 to +551
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the current OpenAI Code Interpreter docs, where should clients read the container ID for an auto-created container from a Responses API result?

💡 Result:

Clients should read the auto-created container ID from the Responses API output item named "code_interpreter_call"—the docs say to “look for the code_interpreter_call item in the output of this API request to find the container_id that was generated or used.” [1]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant section around the referenced lines
echo "== src/code.gs: around lines 520-580 =="
nl -ba src/code.gs | sed -n '510,580p'

echo
echo "== Search for _lastContainerId usage =="
rg -n "_lastContainerId" src/code.gs || true

echo
echo "== Search for container_file_citation extraction =="
rg -n "container_file_citation|_extractContainerFileCitations|getContainerId" src/code.gs || true

Repository: scriptit-fr/GenAIApp

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/code.gs: around lines 520-580 =="
awk 'NR>=510 && NR<=580 {printf "%d:%s\n", NR, $0}' src/code.gs | sed -n '1,200p'

echo
echo "== Search for _lastContainerId usage =="
rg -n "_lastContainerId" src/code.gs || true

echo
echo "== Search for container_file_citation extraction / getContainerId =="
rg -n "container_file_citation|_extractContainerFileCitations|getContainerId" src/code.gs || true

Repository: scriptit-fr/GenAIApp

Length of output: 4731


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/code.gs: extractContainerFileCitations implementation =="
awk 'NR>=760 && NR<=920 {printf "%d:%s\n", NR, $0}' src/code.gs | sed -n '1,220p'

echo
echo "== Search for container_id parsing in code.gs =="
rg -n "container_id|containerId|getContainerId|container_file_citation|code_interpreter_call" src/code.gs

echo
echo "== Show around getContainerId =="
awk 'NR>=820 && NR<=890 {printf "%d:%s\n", NR, $0}' src/code.gs

Repository: scriptit-fr/GenAIApp

Length of output: 11513


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "code_interpreter_call" -S src || true
rg -n "container_id" -S src || true

Repository: scriptit-fr/GenAIApp

Length of output: 292


Capture the auto-created code interpreter container_id from code_interpreter_call

src/code.gs currently sets _lastContainerId only from container_file_citation (via _extractContainerFileCitations), which only appears when the assistant emits annotated generated-file citations. In auto mode, the container id for the generated/reused container is instead surfaced on the code_interpreter_call output item, so follow-up runs can’t reuse the container when no annotated files are produced. https://developers.openai.com/api/docs/guides/tools-code-interpreter

Proposed fix
           this._generatedFiles = this._extractContainerFileCitations(responseMessage);
-          if (this._generatedFiles.length > 0) {
-            this._lastContainerId = this._generatedFiles[0].containerId;
-          }
+          const codeInterpreterCall = Array.isArray(responseMessage?.output)
+            ? responseMessage.output.find(item => item?.type === "code_interpreter_call" && item?.container_id)
+            : null;
+          this._lastContainerId =
+            codeInterpreterCall?.container_id ||
+            this._generatedFiles[0]?.containerId ||
+            null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._generatedFiles = this._extractContainerFileCitations(responseMessage);
if (this._generatedFiles.length > 0) {
this._lastContainerId = this._generatedFiles[0].containerId;
}
this._generatedFiles = this._extractContainerFileCitations(responseMessage);
const codeInterpreterCall = Array.isArray(responseMessage?.output)
? responseMessage.output.find(item => item?.type === "code_interpreter_call" && item?.container_id)
: null;
this._lastContainerId =
codeInterpreterCall?.container_id ||
this._generatedFiles[0]?.containerId ||
null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/code.gs` around lines 539 - 542, The code only sets this._lastContainerId
from this._generatedFiles (via _extractContainerFileCitations) and misses
container IDs surfaced on response items of type code_interpreter_call; update
the handling after receiving responseMessage so that you also inspect
responseMessage outputs (or the top-level outputs list) for any
code_interpreter_call item, extract its container_id, and set
this._lastContainerId to that container_id (fall back to existing
_generatedFiles logic if both exist). Modify the logic around
this._generatedFiles = this._extractContainerFileCitations(responseMessage) to
also call a helper or inline extraction for code_interpreter_call entries and
assign the container id to this._lastContainerId when present so follow-up runs
can reuse the auto-created container.


// OpenAI Responses API returns top-level "id"
if (!model.includes("gemini")) {
Expand Down Expand Up @@ -725,9 +757,108 @@ const GenAIApp = (function () {
payload.tools.push(fileSearchTool);
payload.include = ["file_search_call.results"];
}

if (this._codeInterpreterEnabled) {
payload.parallel_tool_calls = false;
if (this._codeInterpreterContainerId) {
payload.tools.push({
type: "container",
container_id: this._codeInterpreterContainerId
Comment on lines +779 to +781
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use code_interpreter tool when reusing container IDs

When enableCodeInterpreter(containerId) is used, the payload adds a tool with type: "container", but the Responses API expects the Code Interpreter tool itself (type: "code_interpreter") with the container ID in its container field. This makes the explicit-container path fail at request time, so users cannot reuse an existing container despite supplying a valid ID.

Useful? React with 👍 / 👎.

});
Comment on lines +778 to +782
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the current OpenAI Responses API documentation, what is the correct tools payload for reusing an existing Code Interpreter container in explicit mode?

💡 Result:

According to the official OpenAI API documentation for the Code Interpreter tool, you can reuse an existing container in "explicit mode" by passing the specific container ID as the value for the container field within your tool configuration payload [1][2]. In explicit mode, you must first create a container using the /v1/containers endpoint [1][2]. Once you have obtained the container ID (e.g., cntr_abc123), you include it directly in the tool configuration as follows [1][3][2]: { "type": "code_interpreter", "container": "cntr_abc123" } This payload is used within the tools array when making a request to the Responses API (e.g., v1/responses) [1][2]. By providing this ID, the model is directed to perform its code execution tasks within that specific, pre-existing sandboxed environment, allowing for persistent file access and state across multiple requests [1][2]. This contrasts with "auto mode," where the container field would instead be an object specifying configuration settings like {"type": "auto", ...} [1][2].

Citations:


Use the documented code_interpreter tool schema when reusing an existing container

OpenAI’s Responses API expects explicit container reuse to be configured via the code_interpreter tool with a top-level container field containing the container ID. Emitting a separate { type: "container", container_id: ... } tool shape does not match the documented schema, so enableCodeInterpreter(containerId) is likely to break for the explicit reuse path.

Proposed fix
-          if (this._codeInterpreterContainerId) {
-            payload.tools.push({
-              type: "container",
-              container_id: this._codeInterpreterContainerId
-            });
-          }
+          if (this._codeInterpreterContainerId) {
+            payload.tools.push({
+              type: "code_interpreter",
+              container: this._codeInterpreterContainerId
+            });
+          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this._codeInterpreterContainerId) {
payload.tools.push({
type: "container",
container_id: this._codeInterpreterContainerId
});
if (this._codeInterpreterContainerId) {
payload.tools.push({
type: "code_interpreter",
container: this._codeInterpreterContainerId
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/code.gs` around lines 763 - 767, The current code pushes a nonstandard {
type: "container", container_id: ... } tool into payload.tools when
this._codeInterpreterContainerId is set; instead, emit the documented
code_interpreter tool schema and remove the ad-hoc container tool. Update the
logic around this._codeInterpreterContainerId /
enableCodeInterpreter(containerId) to push a tool object named
"code_interpreter" that follows the API schema with a top-level container field
set to this._codeInterpreterContainerId (and remove the separate { type:
"container", container_id: ... } entry).

}
else {
payload.tools.push({
type: "code_interpreter",
container: {
type: "auto"
}
});
}
}
return payload;
}

this._extractContainerFileCitations = function (response) {
if (!Array.isArray(response?.output)) {
return [];
}
const citations = [];
response.output.forEach(outputItem => {
const contentItems = Array.isArray(outputItem?.content) ? outputItem.content : [];
contentItems.forEach(contentItem => {
const annotations = Array.isArray(contentItem?.annotations) ? contentItem.annotations : [];
annotations.forEach(annotation => {
const citation = annotation?.container_file_citation;
if (citation?.container_id && citation?.file_id) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse container_file_citation fields from annotations directly

The extraction logic looks for annotation.container_file_citation, but container file annotations are emitted with top-level fields like type, container_id, and file_id. As written, getGeneratedFiles() stays empty even when Code Interpreter generates files, which breaks downloadGeneratedFile() for normal successful runs.

Useful? React with 👍 / 👎.

citations.push({
containerId: citation.container_id,
fileId: citation.file_id,
filename: citation.filename || null
});
}
});
});
});
return citations;
};

this._downloadContainerFile = function (containerId, fileId, filename) {
const endpointUrl = `${apiBaseUrl}/v1/containers/${containerId}/files/${fileId}/content`;
const response = _callGenAIApi(endpointUrl, null, "GET", true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid null payload call path in _callGenAIApi retries

This call passes payload = null, but _callGenAIApi still references payload.model in several non-200 branches (rate limits/server errors/generic failures). If the file download endpoint returns anything other than 200, the retry/error handling can throw a TypeError instead of surfacing the real API failure, making download errors hard to diagnose and bypassing intended retry behavior.

Useful? React with 👍 / 👎.

const blob = response.getBlob();
const contentType = response.getHeaders()["Content-Type"] || blob.getContentType();
if (contentType) {
blob.setContentType(contentType);
}
if (filename) {
blob.setName(filename);
}
return blob;
};

/**
* Returns generated files from the last run call.
* @returns {{containerId: string, fileId: string, filename: string}[]} Generated files metadata.
*/
this.getGeneratedFiles = function () {
return this._generatedFiles;
};

/**
* Downloads a generated file from the last run.
* @param {string|number} fileIdOrIndex - File ID or index from getGeneratedFiles().
* @returns {Blob} Downloaded file blob that can be stored with DriveApp.createFile(blob).
* @example
* const chat = GenAIApp.newChat()
* .addFile(DriveApp.getFileById("YOUR_FILE_ID").getBlob())
* .enableCodeInterpreter()
* .addMessage("Process this file and generate an updated version.");
* chat.run({ model: "gpt-5.4" });
* const files = chat.getGeneratedFiles();
* const blob = chat.downloadGeneratedFile(files[0].fileId);
* DriveApp.createFile(blob);
*/
this.downloadGeneratedFile = function (fileIdOrIndex) {
let targetFile;
if (typeof fileIdOrIndex === "number") {
targetFile = this._generatedFiles[fileIdOrIndex];
}
else {
targetFile = this._generatedFiles.find(file => file.fileId === fileIdOrIndex);
Comment on lines +875 to +885
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the blank-string fallback from being overwritten.

Lines 863-865 default blank input to the first generated file, but Line 869 immediately overrides that with a lookup for the empty string because this branch is no longer chained with else if. downloadGeneratedFile("") and whitespace-only input will still throw.

Suggested fix
-        if (typeof fileIdOrIndex === "number") {
+        else if (typeof fileIdOrIndex === "number") {
           targetFile = this._generatedFiles[fileIdOrIndex];
         }
         else if (typeof fileIdOrIndex === "string") {
           targetFile = this._generatedFiles.find(file => file.fileId === fileIdOrIndex);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/code.gs` around lines 860 - 870, The blank-string fallback is being
overwritten because the numeric-type check is a separate if; change the
standalone "if (typeof fileIdOrIndex === \"number\")" to "else if (typeof
fileIdOrIndex === \"number\")" (or return immediately after setting targetFile)
so the earlier blank/undefined branches that set targetFile =
this._generatedFiles[0] are not replaced; locate the logic around fileIdOrIndex
/ targetFile / this._generatedFiles (e.g., in the downloadGeneratedFile call)
and make the numeric branch part of the same else-if chain.

}
if (!targetFile) {
throw new Error("[GenAIApp] - Generated file not found. Provide a valid file ID or index from getGeneratedFiles().");
}
return this._downloadContainerFile(targetFile.containerId, targetFile.fileId, targetFile.filename);
};

this.getContainerId = function () {
if (this._lastContainerId) {
return this._lastContainerId;
}
return this._generatedFiles[0]?.containerId || this._codeInterpreterContainerId || null;
};

/**
* Builds and returns a payload for a Gemini API call, configuring content, model parameters,
* and tool settings based on advanced options and feature flags such as browsing.
Expand Down Expand Up @@ -1383,7 +1514,7 @@ const GenAIApp = (function () {
* @returns {object} - The response message from the GenAI API.
* @throws {Error} If the API call fails after the maximum number of retries.
*/
function _callGenAIApi(endpoint, payload) {
function _callGenAIApi(endpoint, payload, method = "post", returnRawResponse = false) {
let authMethod = 'Bearer ' + openAIKey;
if (endpoint.includes("google")) {
if (geminiKey) {
Expand Down Expand Up @@ -1413,11 +1544,13 @@ const GenAIApp = (function () {
headers['x-goog-api-key'] = geminiKey;
}
const options = {
method: 'post',
method: method.toLowerCase(),
headers: headers,
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
if (payload !== null && payload !== undefined) {
options.payload = JSON.stringify(payload);
}

let response;
// if the ErrorHandler library is loaded and supports backoff, use it (https://github.com/RomainVialard/ErrorHandler)
Expand All @@ -1441,6 +1574,9 @@ const GenAIApp = (function () {
const responseCode = response.getResponseCode();

if (responseCode === 200) {
if (returnRawResponse) {
return response;
}
// The request was successful, exit the loop.
const parsedResponse = JSON.parse(response.getContentText());
if (endpoint.includes("google")) {
Expand Down
39 changes: 39 additions & 0 deletions src/testFunctions.gs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const GPT_MODEL = "gpt-5.4";
const REASONING_MODEL = "o4-mini";
const GEMINI_MODEL = "gemini-2.5-pro";
const TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID = "";
const TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID = "";

// Run all tests
function testAll() {
Expand All @@ -13,6 +15,13 @@ function testAll() {
testVision();
testMaximumAPICalls();
testInputTokenWarning();
// OpenAI-only tests - require valid Drive file IDs.
if (TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID) {
testCodeInterpreterExcel(TEST_CODE_INTERPRETER_XLSX_DRIVE_FILE_ID);
}
if (TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID) {
testCodeInterpreterPDF(TEST_CODE_INTERPRETER_PDF_DRIVE_FILE_ID);
}
}


Expand Down Expand Up @@ -149,6 +158,36 @@ ${highThresholdResponse}`);
console.log("Input token warning test (high threshold): verify that no warning log was emitted.");
}

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");
const response = chat.run({ model: GPT_MODEL, max_tokens: 4000 });
console.log(`Code Interpreter Excel response:\n${response}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const outputBlob = chat.downloadGeneratedFile(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const createdFile = DriveApp.createFile(outputBlob);
console.log(`Generated Excel file created: ${createdFile.getId()}`);
}

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");
const response = chat.run({ model: GPT_MODEL, max_tokens: 4000 });
console.log(`Code Interpreter PDF response:\n${response}`);
const outputBlob = chat.downloadGeneratedFile(0);
const createdFile = DriveApp.createFile(outputBlob);
console.log(`Generated PDF file created: ${createdFile.getId()}`);
}

// Weather function implementation
function getWeather(cityName) {
return `The weather in ${cityName} is 19°C today.`;
Expand Down