From ce0ca894fea8a2904bc6f832ff186d5fe53dd0b9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 7 Mar 2026 19:23:18 -0600 Subject: [PATCH] enh: code interpreter pyodide fs --- backend/open_webui/config.py | 35 +- backend/open_webui/main.py | 1 + backend/open_webui/utils/middleware.py | 32 +- .../admin/Settings/CodeExecution.svelte | 4 +- src/lib/components/chat/Chat.svelte | 3 + src/lib/components/chat/ChatControls.svelte | 10 +- .../chat/FileNav/FileEntryRow.svelte | 4 +- src/lib/components/chat/MessageInput.svelte | 6 + src/lib/components/chat/PyodideFileNav.svelte | 367 ++++++++++++++++++ src/lib/stores/index.ts | 3 + src/lib/workers/pyodide.worker.ts | 260 +++++++++++-- src/routes/+layout.svelte | 79 +++- 12 files changed, 727 insertions(+), 77 deletions(-) create mode 100644 src/lib/components/chat/PyodideFileNav.svelte diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 32dfdb7ca9..2e3254a08c 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2296,20 +2296,31 @@ CODE_INTERPRETER_BLOCKED_MODULES = [ ] DEFAULT_CODE_INTERPRETER_PROMPT = """ -#### Tools Available +#### Code Interpreter -1. **Code Interpreter**: `` - - You have access to a Python shell that runs directly in the user's browser, enabling fast execution of code for analysis, calculations, or problem-solving. Use it in this response. - - The Python code you write can incorporate a wide array of libraries, handle data manipulation or visualization, perform API calls for web-related tasks, or tackle virtually any computational challenge. Use this flexibility to **think outside the box, craft elegant solutions, and harness Python's full potential**. - - To use it, **you must enclose your code within `` XML tags** and stop right away. If you don't, the code won't execute. - - When writing code in the code_interpreter XML tag, Do NOT use the triple backticks code block for markdown formatting, example: ```py # python code ``` will cause an error because it is markdown formatting, it is not python code. - - When coding, **always aim to print meaningful outputs** (e.g., results, tables, summaries, or visuals) to better interpret and verify the findings. Avoid relying on implicit outputs; prioritize explicit and clear print statements so the results are effectively communicated to the user. - - After obtaining the printed output, **always provide a concise analysis, interpretation, or next steps to help the user understand the findings or refine the outcome further.** - - If the results are unclear, unexpected, or require validation, refine the code and execute it again as needed. Always aim to deliver meaningful insights from the results, iterating if necessary. - - **If a link to an image, audio, or any file is provided in markdown format in the output, ALWAYS regurgitate word for word, explicitly display it as part of the response to ensure the user can access it easily, do NOT change the link.** - - All responses should be communicated in the chat's primary language, ensuring seamless understanding. If the chat is multilingual, default to English for clarity. +You have access to a Python code interpreter via: `` -Ensure that the tools are effectively utilized to achieve the highest-quality analysis for the user.""" +- The Python shell runs directly in the user's browser for fast execution of analysis, calculations, or problem-solving. Use it in this response. +- You can use a wide array of libraries for data manipulation, visualization, API calls, or any computational task. Think outside the box and harness Python's full potential. +- **You must enclose your code within `` XML tags** and stop right away. If you don't, the code won't execute. +- Do NOT use triple backticks (```py ... ```) inside the XML tags — that is markdown formatting, not executable Python code. +- **Always print meaningful outputs** (results, tables, summaries, visuals). Avoid implicit outputs; use explicit print statements. +- After obtaining output, **provide a concise analysis, interpretation, or next steps** to help the user understand the findings. +- If results are unclear or unexpected, refine the code and re-execute. Iterate until you deliver meaningful insights. +- **If a link to an image, audio, or any file appears in the output, display it exactly as-is** in your response so the user can access it. Do not modify the link. +- Respond in the chat's primary language. Default to English if multilingual. + +Ensure the code interpreter is effectively utilized to achieve the highest-quality analysis for the user.""" + +# Appended to the code interpreter prompt only when engine is pyodide (not jupyter) +CODE_INTERPRETER_PYODIDE_FS_PROMPT = """ + +##### Persistent File System + +- User-uploaded files are available at `/mnt/uploads/`. When the user asks you to work with their files, read from this directory. +- You can also write output files to `/mnt/uploads/` so the user can access and download them from the file browser. +- The file system persists across code executions within the same session. +- Use `import os; os.listdir('/mnt/uploads')` to discover available files.""" #################################### diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 328c49b6c8..4c27e811cb 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -2193,6 +2193,7 @@ async def get_app_config(request: Request): "user_count": user_count, "code": { "engine": app.state.config.CODE_EXECUTION_ENGINE, + "interpreter_engine": app.state.config.CODE_INTERPRETER_ENGINE, }, "audio": { "tts": { diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 00de970ef4..4aa7c2f41e 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1,3 +1,4 @@ +import copy import time import logging import sys @@ -119,6 +120,7 @@ from open_webui.config import ( DEFAULT_VOICE_MODE_PROMPT_TEMPLATE, DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, DEFAULT_CODE_INTERPRETER_PROMPT, + CODE_INTERPRETER_PYODIDE_FS_PROMPT, CODE_INTERPRETER_BLOCKED_MODULES, ) from open_webui.env import ( @@ -2352,18 +2354,36 @@ async def process_chat_payload(request, form_data, user, metadata, model): ) if "code_interpreter" in features and features["code_interpreter"]: + engine = getattr( + request.app.state.config, "CODE_INTERPRETER_ENGINE", "pyodide" + ) + # Skip XML-tag prompt injection when native FC is enabled — # execute_code will be injected as a builtin tool instead if metadata.get("params", {}).get("function_calling") != "native": + prompt = ( + request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE + if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE + != "" + else DEFAULT_CODE_INTERPRETER_PROMPT + ) + + # Append filesystem awareness only for pyodide engine + if engine != "jupyter": + prompt += CODE_INTERPRETER_PYODIDE_FS_PROMPT + form_data["messages"] = add_or_update_user_message( - ( - request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE - if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE - != "" - else DEFAULT_CODE_INTERPRETER_PROMPT - ), + prompt, form_data["messages"], ) + else: + # Native FC: tool docstring can't be dynamic, so inject + # filesystem context into messages for pyodide engine + if engine != "jupyter": + form_data["messages"] = add_or_update_user_message( + CODE_INTERPRETER_PYODIDE_FS_PROMPT, + form_data["messages"], + ) tool_ids = form_data.pop("tool_ids", None) terminal_id = form_data.pop("terminal_id", None) diff --git a/src/lib/components/admin/Settings/CodeExecution.svelte b/src/lib/components/admin/Settings/CodeExecution.svelte index 23388c0a45..cad7970de1 100644 --- a/src/lib/components/admin/Settings/CodeExecution.svelte +++ b/src/lib/components/admin/Settings/CodeExecution.svelte @@ -67,7 +67,7 @@ > {#each engines as engine} - + {/each} @@ -193,7 +193,7 @@ > {#each engines as engine} - + {/each} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index b510dd0b65..b391cd1a51 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -148,6 +148,8 @@ let webSearchEnabled = false; let codeInterpreterEnabled = false; + + let showCommands = false; let generating = false; @@ -2924,6 +2926,7 @@ {stopResponse} {showMessage} {eventTarget} + {codeInterpreterEnabled} /> diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte index 60ec1aa27d..f5957e40e4 100644 --- a/src/lib/components/chat/ChatControls.svelte +++ b/src/lib/components/chat/ChatControls.svelte @@ -10,6 +10,7 @@ import { onDestroy, onMount, tick, getContext } from 'svelte'; import { + config, terminalServers, mobile, showControls, @@ -31,6 +32,7 @@ import Artifacts from './Artifacts.svelte'; import Embeds from './ChatControls/Embeds.svelte'; import FileNav from './FileNav.svelte'; + import PyodideFileNav from './PyodideFileNav.svelte'; import Overview from './Overview.svelte'; const i18n = getContext('i18n'); @@ -50,6 +52,8 @@ export let files; export let modelId; + export let codeInterpreterEnabled = false; + export let pane: Pane | null = null; let largeScreen = false; @@ -67,7 +71,7 @@ $: hasMessages = history?.messages && Object.keys(history.messages).length > 0; $: showControlsTab = $user?.role === 'admin' || ($user?.permissions?.chat?.controls ?? true); - $: showFilesTab = !!$selectedTerminalId; + $: showFilesTab = !!$selectedTerminalId || (codeInterpreterEnabled && $config?.code?.interpreter_engine !== 'jupyter'); $: showOverviewTab = hasMessages; // Tab fallback: if active tab becomes hidden, switch to next available @@ -355,6 +359,8 @@ /> {:else if activeTab === 'files' && $selectedTerminalId} + {:else if activeTab === 'files' && codeInterpreterEnabled} + {:else} {/if} @@ -504,6 +510,8 @@ /> {:else if activeTab === 'files' && $selectedTerminalId} + {:else if activeTab === 'files' && codeInterpreterEnabled} + {:else} {/if} diff --git a/src/lib/components/chat/FileNav/FileEntryRow.svelte b/src/lib/components/chat/FileNav/FileEntryRow.svelte index b572b15255..eb217bcc9b 100644 --- a/src/lib/components/chat/FileNav/FileEntryRow.svelte +++ b/src/lib/components/chat/FileNav/FileEntryRow.svelte @@ -12,8 +12,8 @@ export let entry: FileEntry; export let currentPath: string; - export let terminalUrl: string; - export let terminalKey: string; + export let terminalUrl: string = ''; + export let terminalKey: string = ''; export let onOpen: (entry: FileEntry) => void = () => {}; export let onDownload: (path: string) => void = () => {}; diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 2cbd0d9557..9bf3a08139 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -508,11 +508,17 @@ let showCodeInterpreterButton = false; $: showCodeInterpreterButton = + !$selectedTerminalId && (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === codeInterpreterCapableModels.length && $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter); + // Disable code interpreter when terminal is active (mutually exclusive) + $: if ($selectedTerminalId && codeInterpreterEnabled) { + codeInterpreterEnabled = false; + } + const scrollToBottom = () => { const element = document.getElementById('messages-container'); element.scrollTo({ diff --git a/src/lib/components/chat/PyodideFileNav.svelte b/src/lib/components/chat/PyodideFileNav.svelte new file mode 100644 index 0000000000..45e9693207 --- /dev/null +++ b/src/lib/components/chat/PyodideFileNav.svelte @@ -0,0 +1,367 @@ + + + + + + +
(isDragOver = false)} + on:drop={handleDrop} + role="region" + aria-label={$i18n.t('Pyodide file browser')} +> + {#if isDragOver} +
+ + + + {$i18n.t('Drop files here')} +
+ {/if} + + {#if overlay} +
+ {/if} + + + loadDir(path)} + onRefresh={() => { + if (selectedFile) { + const name = selectedFile.split('/').pop() ?? ''; + openEntry({ name, type: 'file', size: 0 }); + } else { + loadDir(currentPath); + } + }} + onNewFolder={createFolder} + onNewFile={() => {}} + onUploadFiles={uploadFiles} + onMove={() => {}} + > + + + + + + +
+ {#if selectedFile} + + {:else if loading} +
+ +
+ {:else if error} +
+
{error}
+
+ {:else if entries.length === 0} +
+ +
+ {$i18n.t('No files yet. Upload files or run Python code to create them.')} +
+
+ {:else} +
    + {#each entries as entry (entry.name)} + + {/each} +
+ {/if} +
+
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index bdc8fe31ca..6361dcab80 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -72,6 +72,9 @@ export const functions = writable(null); export const toolServers = writable([]); export const terminalServers = writable([]); +// Persistent Pyodide worker for code interpreter FS +export const pyodideWorker: Writable = writable(null); + export const banners: Writable = writable([]); export const settings: Writable = writable({}); diff --git a/src/lib/workers/pyodide.worker.ts b/src/lib/workers/pyodide.worker.ts index 221effca5e..a9334b4faf 100644 --- a/src/lib/workers/pyodide.worker.ts +++ b/src/lib/workers/pyodide.worker.ts @@ -13,6 +13,12 @@ declare global { } } +// --------------------------------------------------------------------------- +// Pyodide bootstrap +// --------------------------------------------------------------------------- + +let pyodideReady: Promise | null = null; + async function loadPyodideAndPackages(packages: string[] = []) { self.stdout = null; self.stderr = null; @@ -40,41 +46,148 @@ async function loadPyodideAndPackages(packages: string[] = []) { packages: ['micropip'] }); - const mountDir = '/mnt'; - self.pyodide.FS.mkdirTree(mountDir); - // self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, mountDir); + // Create the upload directory and mount IDBFS for persistence + const uploadDir = '/mnt/uploads'; + self.pyodide.FS.mkdirTree(uploadDir); + self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, '/mnt'); - // // Load persisted files from IndexedDB (Initial Sync) - // await new Promise((resolve, reject) => { - // self.pyodide.FS.syncfs(true, (err) => { - // if (err) { - // console.error('Error syncing from IndexedDB:', err); - // reject(err); - // } else { - // console.log('Successfully loaded from IndexedDB.'); - // resolve(); - // } - // }); - // }); + // Load persisted files from IndexedDB + await new Promise((resolve) => { + (self.pyodide.FS as any).syncfs(true, (err: Error | null) => { + if (err) { + console.error('Error syncing from IndexedDB:', err); + } + // Always resolve — missing data is fine on first run + resolve(); + }); + }); + + // Ensure /mnt/uploads still exists after sync (first-time init) + try { + self.pyodide.FS.stat(uploadDir); + } catch { + self.pyodide.FS.mkdirTree(uploadDir); + } const micropip = self.pyodide.pyimport('micropip'); - - // await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json'); await micropip.install(packages); } -self.onmessage = async (event) => { - const { id, code, ...context } = event.data; +/** + * Ensure Pyodide is loaded. On the first call, loads and installs packages. + * Subsequent calls reuse the already-loaded instance (persistent worker). + */ +async function ensurePyodide(packages: string[] = []) { + if (!pyodideReady) { + pyodideReady = loadPyodideAndPackages(packages); + } + await pyodideReady; - console.log(event.data); + // Install any additional packages not loaded on init + if (packages.length > 0 && self.pyodide) { + const micropip = self.pyodide.pyimport('micropip'); + await micropip.install(packages); + } +} - // The worker copies the context in its own "memory" (an object mapping name to values) - for (const key of Object.keys(context)) { - self[key] = context[key]; +/** + * Persist the in-memory FS to IndexedDB (fire-and-forget with logging). + */ +function persistFS() { + if (!self.pyodide) return; + (self.pyodide.FS as any).syncfs(false, (err: Error | null) => { + if (err) { + console.error('Error syncing to IndexedDB:', err); + } else { + console.log('Successfully synced to IndexedDB.'); + } + }); +} + +// --------------------------------------------------------------------------- +// FS operations +// --------------------------------------------------------------------------- + +function fsUploadFiles(files: { name: string; data: ArrayBuffer }[], dir = '/mnt/uploads') { + try { + self.pyodide.FS.stat(dir); + } catch { + self.pyodide.FS.mkdirTree(dir); } - // make sure loading is done - await loadPyodideAndPackages(self.packages); + for (const file of files) { + self.pyodide.FS.writeFile(`${dir}/${file.name}`, new Uint8Array(file.data)); + } +} + +function fsList(path: string) { + const entries: { name: string; type: 'file' | 'directory'; size: number }[] = []; + try { + const items = self.pyodide.FS.readdir(path).filter( + (n: string) => n !== '.' && n !== '..' + ); + for (const name of items) { + try { + const stat = self.pyodide.FS.stat(`${path}/${name}`); + const isDir = self.pyodide.FS.isDir(stat.mode); + entries.push({ + name, + type: isDir ? 'directory' : 'file', + size: isDir ? 0 : stat.size + }); + } catch { + // skip inaccessible entries + } + } + } catch { + // directory doesn't exist + } + return entries; +} + +function fsRead(path: string): ArrayBuffer { + const data: Uint8Array = (self.pyodide.FS as any).readFile(path) as Uint8Array; + return data.buffer as ArrayBuffer; +} + +function fsDelete(path: string) { + try { + const stat = self.pyodide.FS.stat(path); + if (self.pyodide.FS.isDir(stat.mode)) { + // Recursively delete directory contents + const items = self.pyodide.FS.readdir(path).filter( + (n: string) => n !== '.' && n !== '..' + ); + for (const item of items) { + fsDelete(`${path}/${item}`); + } + self.pyodide.FS.rmdir(path); + } else { + self.pyodide.FS.unlink(path); + } + } catch { + // already gone + } +} + +function fsMkdir(path: string) { + self.pyodide.FS.mkdirTree(path); +} + +// --------------------------------------------------------------------------- +// Code execution +// --------------------------------------------------------------------------- + +async function executeCode(id: string, code: string, files?: { name: string; data: ArrayBuffer }[]) { + self.stdout = null; + self.stderr = null; + self.result = null; + + // Upload any accompanying files before execution + if (files && files.length > 0) { + fsUploadFiles(files); + persistFS(); + } try { // check if matplotlib is imported in the code @@ -113,25 +226,90 @@ matplotlib.pyplot.show = show`); console.log('Python result:', self.result); - // Persist any changes to IndexedDB - // await new Promise((resolve, reject) => { - // self.pyodide.FS.syncfs(false, (err) => { - // if (err) { - // console.error('Error syncing to IndexedDB:', err); - // reject(err); - // } else { - // console.log('Successfully synced to IndexedDB.'); - // resolve(); - // } - // }); - // }); - } catch (error) { - self.stderr = error.toString(); + // Persist any files the code may have written + persistFS(); + } catch (error: unknown) { + self.stderr = error instanceof Error ? error.message : String(error); } self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr }); +} + +// --------------------------------------------------------------------------- +// Message handler +// --------------------------------------------------------------------------- + +self.onmessage = async (event) => { + const data = event.data; + const { id, type } = data; + + // Backward compatibility: messages without a `type` field are execute requests + if (!type || type === 'execute') { + const { code, files, ...context } = data; + + // Copy context keys (packages, etc.) into worker scope + for (const key of Object.keys(context)) { + if (key !== 'id' && key !== 'type') { + self[key] = context[key]; + } + } + + await ensurePyodide(self.packages); + await executeCode(id, code, files); + return; + } + + // FS operations require Pyodide to be loaded + await ensurePyodide(); + + switch (type) { + case 'fs:upload': { + const { files, dir } = data; + fsUploadFiles(files, dir); + persistFS(); + self.postMessage({ id, type: 'fs:upload', success: true }); + break; + } + + case 'fs:list': { + const entries = fsList(data.path); + self.postMessage({ id, type: 'fs:list', entries }); + break; + } + + case 'fs:read': { + try { + const buffer = fsRead(data.path); + self.postMessage({ id, type: 'fs:read', data: buffer }, { transfer: [buffer] }); + } catch (err: unknown) { + self.postMessage({ id, type: 'fs:read', error: err instanceof Error ? err.message : String(err) }); + } + break; + } + + case 'fs:delete': { + fsDelete(data.path); + persistFS(); + self.postMessage({ id, type: 'fs:delete', success: true }); + break; + } + + case 'fs:mkdir': { + fsMkdir(data.path); + persistFS(); + self.postMessage({ id, type: 'fs:mkdir', success: true }); + break; + } + + default: + console.warn('Unknown message type:', type); + } }; +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + function processResult(result: any): any { // Catch and always return JSON-safe string representations try { @@ -167,9 +345,9 @@ function processResult(result: any): any { } // Stringify anything that's left (e.g., Proxy objects that cannot be directly processed) return JSON.stringify(result); - } catch (err) { + } catch (err: unknown) { // In case something unexpected happens, we return a stringified fallback - return `[processResult error]: ${err.message || err.toString()}`; + return `[processResult error]: ${err instanceof Error ? err.message : String(err)}`; } } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 42768ca99d..36fd7b22d5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -34,8 +34,10 @@ terminalServers, showControls, showFileNavPath, - showFileNavDir + showFileNavDir, + pyodideWorker } from '$lib/stores'; + import { getFileContentById } from '$lib/apis/files'; import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { beforeNavigate } from '$app/navigation'; @@ -184,7 +186,20 @@ }); }; - const executePythonAsWorker = async (id, code, cb) => { + /** + * Get or create the persistent Pyodide worker. + * The worker persists across executions so the virtual FS (IDBFS) is preserved. + */ + const getOrCreateWorker = () => { + let worker = $pyodideWorker; + if (!worker) { + worker = new PyodideWorker(); + pyodideWorker.set(worker); + } + return worker; + }; + + const executePythonAsWorker = async (id, code, cb, files = []) => { let result = null; let stdout = null; let stderr = null; @@ -206,19 +221,44 @@ /\bimport\s+pytz\b|\bfrom\s+pytz\b/.test(code) ? 'pytz' : null ].filter(Boolean); - const pyodideWorker = new PyodideWorker(); + const worker = getOrCreateWorker(); - pyodideWorker.postMessage({ + // Fetch file content from the server and prepare for the worker + let filePayloads = []; + if (files && files.length > 0) { + for (const file of files) { + try { + const fileId = file?.id; + const fileName = file?.filename || file?.name || 'file'; + if (fileId) { + const content = await getFileContentById(fileId); + if (content) { + filePayloads.push({ name: fileName, data: content }); + } + } + } catch (e) { + console.error('Failed to fetch file for Pyodide:', e); + } + } + } + + worker.postMessage({ + type: 'execute', id: id, code: code, - packages: packages + packages: packages, + files: filePayloads.length > 0 ? filePayloads : undefined }); - setTimeout(() => { + // Timeout for this specific execution (not the worker itself) + let timeoutId = setTimeout(() => { if (executing) { executing = false; stderr = 'Execution Time Limit Exceeded'; - pyodideWorker.terminate(); + + // Terminate and recreate the worker on timeout + worker.terminate(); + pyodideWorker.set(null); if (cb) { cb( @@ -237,11 +277,18 @@ } }, 60000); - pyodideWorker.onmessage = (event) => { - console.log('pyodideWorker.onmessage', event); - const { id, ...data } = event.data; + // Use addEventListener so multiple concurrent executions don't clobber each other + const onMessage = (event) => { + const { id: eventId, ...data } = event.data; + // Only handle responses for this execution ID + if (eventId !== id) return; + // Ignore FS responses (they use a type field) + if (data.type && data.type.startsWith('fs:')) return; - console.log(id, data); + console.log('pyodideWorker.onmessage', event); + clearTimeout(timeoutId); + worker.removeEventListener('message', onMessage); + worker.removeEventListener('error', onError); data['stdout'] && (stdout = data['stdout']); data['stderr'] && (stderr = data['stderr']); @@ -265,8 +312,11 @@ executing = false; }; - pyodideWorker.onerror = (event) => { + const onError = (event) => { console.log('pyodideWorker.onerror', event); + clearTimeout(timeoutId); + worker.removeEventListener('message', onMessage); + worker.removeEventListener('error', onError); if (cb) { cb( @@ -284,6 +334,9 @@ } executing = false; }; + + worker.addEventListener('message', onMessage); + worker.addEventListener('error', onError); }; const resolveToolServer = (serverUrl) => { @@ -423,7 +476,7 @@ } else if (data?.session_id === $socket.id) { if (type === 'execute:python') { console.log('execute:python', data); - executePythonAsWorker(data.id, data.code, cb); + executePythonAsWorker(data.id, data.code, cb, data.files || []); } else if (type === 'execute:tool') { console.log('execute:tool', data); executeTool(data, cb);