open-webui / src /lib /workers /pyodide.worker.ts
github-actions[bot]
GitHub deploy: 98f3b3200a726a9eee1f707bdd2850c746a18889
81e42f7
raw
history blame
4.93 kB
import { loadPyodide, type PyodideInterface } from 'pyodide';
declare global {
interface Window {
stdout: string | null;
stderr: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result: any;
pyodide: PyodideInterface;
packages: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
}
async function loadPyodideAndPackages(packages: string[] = []) {
self.stdout = null;
self.stderr = null;
self.result = null;
self.pyodide = await loadPyodide({
indexURL: '/pyodide/',
stdout: (text) => {
console.log('Python output:', text);
if (self.stdout) {
self.stdout += `${text}\n`;
} else {
self.stdout = `${text}\n`;
}
},
stderr: (text) => {
console.log('An error occurred:', text);
if (self.stderr) {
self.stderr += `${text}\n`;
} else {
self.stderr = `${text}\n`;
}
},
packages: ['micropip']
});
let mountDir = '/mnt';
self.pyodide.FS.mkdirTree(mountDir);
// self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, mountDir);
// // Load persisted files from IndexedDB (Initial Sync)
// await new Promise<void>((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();
// }
// });
// });
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;
console.log(event.data);
// 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];
}
// make sure loading is done
await loadPyodideAndPackages(self.packages);
try {
// check if matplotlib is imported in the code
if (code.includes('matplotlib')) {
// Override plt.show() to return base64 image
await self.pyodide.runPythonAsync(`import base64
import os
from io import BytesIO
# before importing matplotlib
# to avoid the wasm backend (which needs js.document', not available in worker)
os.environ["MPLBACKEND"] = "AGG"
import matplotlib.pyplot
_old_show = matplotlib.pyplot.show
assert _old_show, "matplotlib.pyplot.show"
def show(*, block=None):
buf = BytesIO()
matplotlib.pyplot.savefig(buf, format="png")
buf.seek(0)
# encode to a base64 str
img_str = base64.b64encode(buf.read()).decode('utf-8')
matplotlib.pyplot.clf()
buf.close()
print(f"data:image/png;base64,{img_str}")
matplotlib.pyplot.show = show`);
}
self.result = await self.pyodide.runPythonAsync(code);
// Safely process and recursively serialize the result
self.result = processResult(self.result);
console.log('Python result:', self.result);
// Persist any changes to IndexedDB
// await new Promise<void>((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();
}
self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr });
};
function processResult(result: any): any {
// Catch and always return JSON-safe string representations
try {
if (result == null) {
// Handle null and undefined
return null;
}
if (typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean') {
// Handle primitive types directly
return result;
}
if (typeof result === 'bigint') {
// Convert BigInt to a string for JSON-safe representation
return result.toString();
}
if (Array.isArray(result)) {
// If it's an array, recursively process items
return result.map((item) => processResult(item));
}
if (typeof result.toJs === 'function') {
// If it's a Pyodide proxy object (e.g., Pandas DF, Numpy Array), convert to JS and process recursively
return processResult(result.toJs());
}
if (typeof result === 'object') {
// Convert JS objects to a recursively serialized representation
const processedObject: { [key: string]: any } = {};
for (const key in result) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
processedObject[key] = processResult(result[key]);
}
}
return processedObject;
}
// Stringify anything that's left (e.g., Proxy objects that cannot be directly processed)
return JSON.stringify(result);
} catch (err) {
// In case something unexpected happens, we return a stringified fallback
return `[processResult error]: ${err.message || err.toString()}`;
}
}
export default {};