-
Notifications
You must be signed in to change notification settings - Fork 3
Add Code Interpreter tooling and generated file download support #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
e54cf05
6b2b62a
156d68c
62b1e68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // OpenAI Responses API returns top-level "id" | ||||||||||||||||||||||
| if (!model.includes("gemini")) { | ||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
Comment on lines
+778
to
+782
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 Citations:
Use the documented OpenAI’s Responses API expects explicit container reuse to be configured via the 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| 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) { | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The extraction logic looks for 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); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This call passes 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 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 |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| 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. | ||||||||||||||||||||||
|
|
@@ -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) { | ||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||
|
|
@@ -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")) { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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_callitem in the output of this API request to find thecontainer_idthat was generated or used.” [1]Citations:
🏁 Script executed:
Repository: scriptit-fr/GenAIApp
Length of output: 165
🏁 Script executed:
Repository: scriptit-fr/GenAIApp
Length of output: 4731
🏁 Script executed:
Repository: scriptit-fr/GenAIApp
Length of output: 11513
🏁 Script executed:
Repository: scriptit-fr/GenAIApp
Length of output: 292
Capture the auto-created code interpreter
container_idfromcode_interpreter_callsrc/code.gscurrently sets_lastContainerIdonly fromcontainer_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 thecode_interpreter_calloutput 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-interpreterProposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents