mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
177 lines
4.8 KiB
TypeScript
177 lines
4.8 KiB
TypeScript
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 {};
|