-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimplementation.js
More file actions
256 lines (236 loc) · 11.3 KB
/
Copy pathimplementation.js
File metadata and controls
256 lines (236 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/**
* satware(R) AI python - primary function (loaded by TypingMind from GitHub).
*
* execute_python: validates { code, packages }, maps package aliases, and
* returns a standalone HTML document that runs the Python code in Pyodide
* (WebAssembly, pinned v314.0.5) at display time - the mermaid-fleet
* render_html architecture. Stdout/stderr, package feedback, matplotlib
* figures (wasm backend) and tracebacks render directly in the document.
*
* Fleet conformance: input coercion + try/catch, invalid input returns
* { isError: true, error } instead of throwing. Escaping policy: Python
* output is inserted via textContent only (escaped by construction);
* no innerHTML with model-generated content - see docs/source-analysis.md.
*/
const PYODIDE_VERSION = 'v314.0.5';
const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/${PYODIDE_VERSION}/full/`;
// v314 no longer bundles the matplotlib Wasm backend with the matplotlib
// package (0.27.x lock had it as a dependency). It is a pure-Python PyPI
// wheel, installed at runtime via micropip. Verified against Pyodide
// v314.0.5: requires-python >=3.12, imports clean with matplotlib 3.10.8.
const MATPLOTLIB_PYODIDE_VERSION = '0.2.3';
const PACKAGE_ALIASES = {
np: 'numpy',
pd: 'pandas',
plt: 'matplotlib',
sklearn: 'scikit-learn',
stats: 'scipy',
};
function execute_python(params) {
try {
const code = params && params.code != null ? String(params.code) : '';
if (!code.trim()) {
return { isError: true, error: 'Parameter "code" is required and must contain Python code.' };
}
let packages = params && params.packages != null ? params.packages : [];
if (!Array.isArray(packages)) {
return { isError: true, error: 'Parameter "packages" must be an array of package names (empty array allowed).' };
}
if (packages.some((p) => typeof p !== 'string' || !p.trim())) {
return { isError: true, error: 'Parameter "packages" must contain only non-empty package name strings.' };
}
packages = [...new Set(packages.map((p) => PACKAGE_ALIASES[p.trim()] || p.trim()))];
// Embed the payload as JSON; escape "</" so "</script>" cannot break out.
const payloadJson = JSON.stringify({ code, packages }).replace(/<\//g, '<\\/');
const htmlString = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>satware® AI Python</title>
<style>
*{box-sizing:border-box}
body{margin:0;padding:12px;font-family:system-ui,-apple-system,sans-serif;font-size:14px;color:#1a1a1a}
.py-status{margin:8px 0;font-style:italic;color:#555}
.py-status::before{content:"";display:inline-block;width:11px;height:11px;border:2px solid #ccc;border-top-color:#07d;border-radius:50%;margin-right:8px;vertical-align:-1px;animation:py-spin 1s linear infinite}
.py-status.done::before{display:none}
.py-status.done{font-style:normal;color:#2a7a2a}
@keyframes py-spin{to{transform:rotate(360deg)}}
.py-stdout{font-family:ui-monospace,Menlo,Consolas,monospace;padding:10px;background:#f5f5f5;border-radius:4px;overflow-x:auto;margin:10px 0;white-space:pre-wrap;line-height:1.4}
.py-stdout:empty{display:none}
.py-stderr{font-family:ui-monospace,Menlo,Consolas,monospace;padding:10px;background:#fff8e8;border-left:3px solid #e6a817;border-radius:4px;white-space:pre-wrap;margin:10px 0}
.py-stderr:empty{display:none}
.py-error{background:#fff0f0;border-left:3px solid #d33;border-radius:4px;padding:10px 12px;font-family:ui-monospace,Menlo,Consolas,monospace;white-space:pre-wrap;margin:10px 0}
.py-plot{margin:10px 0;text-align:center}
.py-plot img,canvas{max-width:100%;height:auto}
</style>
</head>
<body>
<div class="py-status" id="py-status">Initialisiere Pyodide (Python 3.14) - beim ersten Aufruf ca. 10 Sekunden...</div>
<pre class="py-stdout" id="py-stdout"></pre>
<pre class="py-stderr" id="py-stderr"></pre>
<div id="py-plot-root"></div>
<script id="py-payload" type="application/json">${payloadJson}</script>
<script>
(function () {
'use strict';
var payload = JSON.parse(document.getElementById('py-payload').textContent);
var statusEl = document.getElementById('py-status');
var stdoutEl = document.getElementById('py-stdout');
var stderrEl = document.getElementById('py-stderr');
var started = Date.now();
function setStatus(text) { statusEl.textContent = text; }
function done() {
statusEl.classList.add('done');
statusEl.textContent = 'Fertig in ' + ((Date.now() - started) / 1000).toFixed(1) + ' s.';
}
var SUGGESTIONS = [
[/ModuleNotFoundError/, 'Fehlendes Modul im Parameter "packages" ergänzen (z.B. "scikit-learn", nicht "sklearn").'],
[/SyntaxError/, 'Python-Syntax prüfen: Doppelpunkte, Klammern, Einrückung.'],
[/NameError/, 'Variablen vor Verwendung definieren bzw. Schreibweise prüfen.'],
[/MemoryError|allocation|out of memory/i, 'Datenmenge reduzieren oder effizientere Operationen nutzen.']
];
function showError(message) {
var suggestion = '';
for (var i = 0; i < SUGGESTIONS.length; i++) {
if (SUGGESTIONS[i][0].test(message)) { suggestion = SUGGESTIONS[i][1]; break; }
}
var el = document.createElement('div');
el.className = 'py-error';
// textContent only - escaping by construction, never innerHTML.
el.textContent = 'Fehler: ' + message + (suggestion ? '\\n\\nTipp: ' + suggestion : '');
document.body.appendChild(el);
}
var script = document.createElement('script');
script.src = '${PYODIDE_CDN}pyodide.js';
script.onload = function () { main(); };
script.onerror = function () {
showError('Pyodide konnte nicht geladen werden (CDN unerreichbar).', '');
};
document.head.appendChild(script);
async function main() {
try {
// convertNullToNone: matplotlib_pyodide 0.2.3 (archived upstream) checks
// DOM lookups with "is not None" - on v314 a missing element returns
// JsNull, which passes that check and crashes at scrollIntoView.
// The flag restores 0.27.x null->None semantics.
var pyodide = await loadPyodide({ indexURL: '${PYODIDE_CDN}', convertNullToNone: true });
// Capture stdout/stderr; batched lines go through textContent (escaped).
pyodide.setStdout({ batched: function (s) { stdoutEl.textContent += s + '\\n'; } });
pyodide.setStderr({ batched: function (s) { stderrEl.textContent += s + '\\n'; } });
// v314: the Emscripten module handle is gone from the Python pyodide
// module but still reachable on the JS object. Bridge the heap size
// (reserved wasm memory, in MB) so memory_status() keeps working.
window.__py_heap_mb = function () {
try {
return pyodide._module.HEAP8.buffer.byteLength / (1024 * 1024);
} catch (e) {
return null;
}
};
var loaded = [];
if (payload.packages.length > 0) {
setStatus('Lade Pakete: ' + payload.packages.join(', ') + ' ...');
for (var i = 0; i < payload.packages.length; i++) {
var name = payload.packages[i];
try {
await pyodide.loadPackage(name);
loaded.push(name);
stdoutEl.textContent += '\u2713 Paket geladen: ' + name + '\n';
} catch (e) {
stderrEl.textContent += '\u26a0 Paket fehlgeschlagen: ' + name + ' (' + (e && e.message ? e.message : e) + ')\n';
}
}
}
}
}
// v314 fix (#2 F1): the matplotlib Wasm backend lives in a separate
// pure-Python wheel now. Install it via micropip whenever matplotlib
// was requested; on failure fall back to the Pyodide default backend
// (webagg) so plotting code still runs.
var mplPyodideOk = false;
if (loaded.indexOf('matplotlib') !== -1) {
setStatus('Lade matplotlib-pyodide (Wasm-Backend)...');
try {
await pyodide.loadPackage('micropip');
var micropip = pyodide.pyimport('micropip');
await micropip.install('matplotlib-pyodide==${MATPLOTLIB_PYODIDE_VERSION}');
mplPyodideOk = true;
stdoutEl.textContent += '\\u2713 Paket geladen: matplotlib-pyodide ${MATPLOTLIB_PYODIDE_VERSION}\\n';
} catch (e) {
stderrEl.textContent += '\\u26a0 matplotlib-pyodide nicht verfügbar - Plots nutzen den Pyodide-Standard-Backend (' + (e && e.message ? e.message : e) + ')\\n';
}
}
// Auto-import standard aliases for successfully loaded core packages.
var imports = '';
if (loaded.indexOf('numpy') !== -1) imports += 'import numpy as np\\n';
if (loaded.indexOf('pandas') !== -1) {
imports += 'import pandas as pd\\n' +
'pd.set_option("display.max_rows", 20)\\n' +
'pd.set_option("display.max_columns", 10)\\n' +
'pd.set_option("display.precision", 3)\\n' +
'pd.set_option("display.width", 1000)\\n';
}
if (loaded.indexOf('matplotlib') !== -1) {
imports += 'import matplotlib\\n' +
// matplotlib 3.10.8's own render path (backend_agg draw_text) emits
// x/y-as-float deprecation warnings - pure noise the model would
// try to "fix". Filter the category for matplotlib-generated code.
'import warnings\\n' +
'from matplotlib import MatplotlibDeprecationWarning\\n' +
'warnings.filterwarnings("ignore", category=MatplotlibDeprecationWarning)\\n';
if (mplPyodideOk) {
imports += 'matplotlib.use("module://matplotlib_pyodide.wasm_backend")\\n';
}
imports += 'import matplotlib.pyplot as plt\\n' +
'plt.rcParams["figure.figsize"] = (8, 5)\\n' +
'plt.rcParams["figure.dpi"] = 100\\n' +
'plt.rcParams["savefig.bbox"] = "tight"\\n' +
'def show_plot(dpi=100, figsize=None):\\n' +
' if figsize is not None: plt.gcf().set_size_inches(*figsize)\\n' +
' plt.gcf().set_dpi(dpi)\\n' +
' plt.show()\\n';
}
if (imports) pyodide.runPython(imports);
// v314 fix (#2 F2): sys.modules['pyodide']._module was removed.
// Primary: JS bridge (reserved wasm heap, same semantics as before);
// fallback: ctypes sbrk(0) allocator break; never raises.
pyodide.runPython(
'def memory_status():\\n' +
' heap = None\\n' +
' try:\\n' +
' from js import __py_heap_mb as _heapfn\\n' +
' heap = _heapfn()\\n' +
' except Exception:\\n' +
' pass\\n' +
' if heap is None:\\n' +
' try:\\n' +
' import ctypes as _ct\\n' +
' _libc = _ct.CDLL(None)\\n' +
' _libc.sbrk.restype = _ct.c_void_p\\n' +
' heap = _libc.sbrk(0) / (1024 * 1024)\\n' +
' except Exception:\\n' +
' pass\\n' +
' if heap is not None:\\n' +
' print(f"Heap: {heap:.0f} MB")\\n' +
' else:\\n' +
' print("Heap-Info nicht verfügbar")\\n'
);
setStatus('Führe Python-Code aus...');
pyodide.runPython(payload.code);
done();
} catch (err) {
done();
// Pyodide PythonError messages already contain the full traceback.
showError(String(err && err.message ? err.message : err));
}
}
})();
</script>
</body>
</html>`;
return htmlString;
} catch (err) {
return { isError: true, error: String(err && err.message ? err.message : err) };
}
}