Skip to content

Commit 810d3b1

Browse files
committed
fix: auto-load Python packages via pyodide.loadPackagesFromImports — no hardcoded package lists needed
1 parent 4180969 commit 810d3b1

3 files changed

Lines changed: 72 additions & 48 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ TextAgent has undergone significant evolution since its inception. What started
538538

539539
| Date | Commits | Feature / Update |
540540
|------|---------|-----------------:|
541+
| **2026-03-24** | | 🐍 **Python Package Auto-Loading**`exec-python.js` now uses `pyodide.loadPackagesFromImports(code)` to automatically detect and install any Pyodide-supported Python package (numpy, pandas, scipy, scikit-learn, matplotlib, sympy, networkx, etc.) before execution; no more hardcoded package lists — new libraries work without platform upgrades; fixed matplotlib import failure (package was never loaded into Pyodide runtime); suppressed `plt.show()` "non-GUI backend" UserWarning since AGG backend renders charts as inline PNG images |
541542
| **2026-03-23** | | 📷 **OCR Camera Capture** — new 📷 camera button on `{{@OCR:}}` cards for live camera capture; `getUserMedia` with rear-camera preference (`facingMode: 'environment'`); modal overlay with live video feed, 📸 Capture → preview → 🔄 Retake / ✅ Use Photo flow; captured images stored as JPEG (0.85 quality, max 1280px) in `blockUploads` map; native `<input capture="environment">` fallback for browsers without `getUserMedia`; amber-themed modal CSS with dark/light support; Escape/overlay-click/✕ dismissal |
542543
| **2026-03-23** | | 💎 **Hybrid Semantic Search & File Conversion** — Memory indexer now supports hybrid search: FTS5 keyword matching (40%) + EmbeddingGemma 300M cosine similarity (60%); new `public/embedding-worker.js` Web Worker with WASM backend (WebGPU shader workaround); `memory_embeddings` table stores 256-dim float arrays per chunk; 💎 Semantic toggle on Memory card auto-downloads model (~150MB) when files are attached; binary file conversion integrated — DOCX (Mammoth.js), XLSX/XLS/Numbers (SheetJS), PDF (PDF.js+OCR), CSV, HTML, JSON, XML auto-converted to markdown before chunking; `M.convertFileToMarkdown()` public API exposed in `file-converters.js`; `.numbers` Apple Numbers format added to import pipeline |
543544
| **2026-03-23** | | 🐛 **GLM-OCR Model Download Fix** — fixed GLM-OCR model failing to download; `glm_ocr` model type was not supported in Transformers.js `4.0.0-next.7`; upgraded to `4.0.0-next.8` which includes `glm_ocr` model class mapping; both `ai-worker-glm-ocr.js` and `public/ai-worker-glm-ocr.js` updated |
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Changelog — Python Package Auto-Loading
2+
3+
## 2026-03-24
4+
5+
### What Changed
6+
- **Auto-loading Python packages**: `exec-python.js` now uses `pyodide.loadPackagesFromImports(code)` to automatically detect and install any Pyodide-supported Python package before execution — no hardcoded package lists needed.
7+
- **Matplotlib fix**: Previously, matplotlib import failed silently because the package was never loaded into the Pyodide runtime. Now it loads automatically.
8+
- **Warning suppression**: Suppressed the `plt.show()` UserWarning ("non-GUI backend") since the AGG backend is required in-browser and charts are captured as inline PNG images.
9+
- **Universal package support**: Any Pyodide-supported library (numpy, pandas, scipy, scikit-learn, sympy, networkx, etc.) now works without platform changes.
10+
11+
### Files Modified
12+
- `js/exec-python.js` — replaced hardcoded matplotlib/numpy detection with universal `loadPackagesFromImports()` call; added `warnings.filterwarnings('ignore', '.*non-GUI backend.*')` for matplotlib AGG backend
13+
14+
### Impact
15+
- Python code blocks in demo showcase and user documents now correctly load and render matplotlib charts
16+
- Users can import any Pyodide-supported package without needing platform upgrades
17+
- No more "non-GUI backend" warning cluttering chart output

js/exec-python.js

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -117,58 +117,64 @@
117117
outputEl.innerHTML = '<span class="code-output-loading"><i class="bi bi-arrow-repeat"></i> Executing...</span>';
118118

119119
setTimeout(function () {
120-
try {
121-
pyodide.runPython('sys.stdout = StringIO()\nsys.stderr = StringIO()');
122-
123-
var usesMpl = /\bimport\s+matplotlib\b|\bfrom\s+matplotlib\b|\bplt\.\b/.test(code);
124-
if (usesMpl) {
125-
try {
126-
pyodide.runPython("import micropip");
127-
pyodide.runPython("import matplotlib\nmatplotlib.use('AGG')");
128-
} catch (e) { /* ignore */ }
129-
}
120+
// Auto-detect and load ALL imported packages (numpy, pandas, scipy, matplotlib, etc.)
121+
outputEl.innerHTML = '<span class="code-output-loading"><i class="bi bi-arrow-repeat"></i> Loading packages...</span>';
122+
pyodide.loadPackagesFromImports(code).then(function () {
123+
try {
124+
pyodide.runPython('sys.stdout = StringIO()\nsys.stderr = StringIO()');
130125

131-
pyodide.runPython(code);
132-
133-
var stdout = pyodide.runPython('sys.stdout.getvalue()');
134-
var stderr = pyodide.runPython('sys.stderr.getvalue()');
135-
136-
var outputHtml = '';
137-
138-
if (usesMpl) {
139-
try {
140-
pyodide.runPython([
141-
'import matplotlib.pyplot as _plt',
142-
'import base64 as _b64',
143-
'from io import BytesIO as _BytesIO',
144-
'_mdv_figs = []',
145-
'for _fig_num in _plt.get_fignums():',
146-
' _buf = _BytesIO()',
147-
' _plt.figure(_fig_num).savefig(_buf, format="png", dpi=100, bbox_inches="tight")',
148-
' _buf.seek(0)',
149-
' _mdv_figs.append(_b64.b64encode(_buf.read()).decode())',
150-
' _buf.close()',
151-
'_plt.close("all")'
152-
].join('\n'));
153-
var figs = pyodide.runPython('_mdv_figs').toJs();
154-
if (figs && figs.length > 0) {
155-
figs.forEach(function (b64) {
156-
outputHtml += '<div class="python-plot-output"><img src="data:image/png;base64,' + b64 + '" alt="matplotlib plot" style="max-width:100%;border-radius:6px;margin:4px 0" /></div>';
157-
});
158-
}
159-
} catch (e) { /* ignore plot errors */ }
160-
}
126+
// Set matplotlib backend to AGG and suppress plt.show() warning
127+
var usesMpl = /\bimport\s+matplotlib\b|\bfrom\s+matplotlib\b|\bplt\.\b/.test(code);
128+
if (usesMpl) {
129+
try { pyodide.runPython("import matplotlib\nmatplotlib.use('AGG')\nimport warnings\nwarnings.filterwarnings('ignore', '.*non-GUI backend.*')"); } catch (e) { /* ok */ }
130+
}
131+
132+
pyodide.runPython(code);
161133

162-
if (stdout) outputHtml += '<span class="code-output-stdout">' + escapeHtml(stdout) + '</span>';
163-
if (stderr) outputHtml += '<span class="code-output-stderr">' + escapeHtml(stderr) + '</span>';
164-
if (!outputHtml) outputHtml = '<span class="code-output-muted">(no output)</span>';
165-
outputEl.innerHTML = outputHtml;
166-
} catch (runErr) {
167-
outputEl.innerHTML = '<span class="code-output-error">Error: ' + escapeHtml(runErr.message) + '</span>';
168-
} finally {
134+
var stdout = pyodide.runPython('sys.stdout.getvalue()');
135+
var stderr = pyodide.runPython('sys.stderr.getvalue()');
136+
var outputHtml = '';
137+
138+
// Capture matplotlib figures as inline images
139+
if (usesMpl) {
140+
try {
141+
pyodide.runPython([
142+
'import matplotlib.pyplot as _plt',
143+
'import base64 as _b64',
144+
'from io import BytesIO as _BytesIO',
145+
'_mdv_figs = []',
146+
'for _fig_num in _plt.get_fignums():',
147+
' _buf = _BytesIO()',
148+
' _plt.figure(_fig_num).savefig(_buf, format="png", dpi=100, bbox_inches="tight")',
149+
' _buf.seek(0)',
150+
' _mdv_figs.append(_b64.b64encode(_buf.read()).decode())',
151+
' _buf.close()',
152+
'_plt.close("all")'
153+
].join('\n'));
154+
var figs = pyodide.runPython('_mdv_figs').toJs();
155+
if (figs && figs.length > 0) {
156+
figs.forEach(function (b64) {
157+
outputHtml += '<div class="python-plot-output"><img src="data:image/png;base64,' + b64 + '" alt="matplotlib plot" style="max-width:100%;border-radius:6px;margin:4px 0" /></div>';
158+
});
159+
}
160+
} catch (e) { /* ignore plot errors */ }
161+
}
162+
163+
if (stdout) outputHtml += '<span class="code-output-stdout">' + escapeHtml(stdout) + '</span>';
164+
if (stderr) outputHtml += '<span class="code-output-stderr">' + escapeHtml(stderr) + '</span>';
165+
if (!outputHtml) outputHtml = '<span class="code-output-muted">(no output)</span>';
166+
outputEl.innerHTML = outputHtml;
167+
} catch (runErr) {
168+
outputEl.innerHTML = '<span class="code-output-error">Error: ' + escapeHtml(runErr.message) + '</span>';
169+
} finally {
170+
btnRun.disabled = false;
171+
btnRun.innerHTML = '<i class="bi bi-play-fill"></i> Run';
172+
}
173+
}).catch(function (loadErr) {
174+
outputEl.innerHTML = '<span class="code-output-error">Failed to load packages: ' + escapeHtml(loadErr.message) + '</span>';
169175
btnRun.disabled = false;
170176
btnRun.innerHTML = '<i class="bi bi-play-fill"></i> Run';
171-
}
177+
});
172178
}, 50);
173179
}, function (msg) {
174180
outputEl.innerHTML = '<span class="code-output-loading"><i class="bi bi-arrow-repeat"></i> ' + escapeHtml(msg) + '</span>';

0 commit comments

Comments
 (0)