Skip to content

Commit 5e2fc51

Browse files
committed
feat(ai): revamp Knowledge Graph pipeline with ModernBERT ONNX 2-model engine and isolated worker queue
- Replace single LLM generator with 2-pass ModernBERT ONNX pipeline (NER + RE) & structural markdown parser - Create independent GraphQueue and GraphWorker in background utilityProcess - Add live top progress bar, note inspector, and clean full-height canvas layout to KnowledgeGraph.jsx - Add ModernBERT 2-model ONNX weight downloader card (~70MB) in KnowledgeGraphSettings.jsx - Clean up unused floating widgets and chat progress toasts
1 parent e3f2eb5 commit 5e2fc51

30 files changed

Lines changed: 1475 additions & 414 deletions

‎.github/workflows/cd.yml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ jobs:
4242
| File | Type | Notes |
4343
|------|------|-------|
4444
| `Notely Setup *.exe` | **Installer** (recommended) | Installs to your system, fast launch every time |
45-
| `Notely *.exe` | **Portable** | Single file, no install — extracts on every launch |
4645
| `Notely-*-win.zip` | **Portable ZIP** | Extract once, run directly — portable + fast |
4746
files: |
4847
release/*.exe

‎ai/core/AIConfig.js‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ const fs = require('fs');
77
const { app, safeStorage } = require('electron');
88

99
class AIConfig {
10-
constructor() {
11-
this.appDataDir = app.getPath('appData');
10+
constructor(customAppDataDir = null) {
11+
if (customAppDataDir) {
12+
this.appDataDir = customAppDataDir;
13+
} else {
14+
try {
15+
this.appDataDir = app ? app.getPath('appData') : path.join(process.env.APPDATA || process.env.HOME || '', 'Notely');
16+
} catch {
17+
this.appDataDir = path.join(process.env.APPDATA || process.env.HOME || '', 'Notely');
18+
}
19+
}
1220
this.configDir = path.join(this.appDataDir, 'notely');
1321
this.configPath = path.join(this.configDir, 'ai-config.json');
1422
this.ensureConfigDir();

‎ai/core/Agent.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ class Agent {
185185
/**
186186
* Build relationship graph
187187
*/
188-
async buildRelationshipGraph() {
188+
async buildRelationshipGraph(onProgress = null) {
189189
if (!this.graphBuilder) {
190190
return { success: false, error: 'Graph builder not initialized' };
191191
}
192-
return this.graphBuilder.rebuild();
192+
return this.graphBuilder.rebuild(onProgress);
193193
}
194194

195195
/**

‎ai/embeddings/ModelDownloader.js‎

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,24 @@ class ModelDownloader {
2020
this.vocabUrl = `https://huggingface.co/Xenova/bge-small-en-v1.5/${vocabUrlPart}`;
2121
this.progressCallback = null;
2222

23-
this.qwenONNXDir = path.join(this.modelDir, 'qwen-onnx');
24-
this.qwenONNXFiles = [
25-
{ name: 'config.json', url: 'https://huggingface.co/onnx-community/Qwen2.5-0.5B-Instruct/resolve/main/config.json' },
26-
{ name: 'generation_config.json', url: 'https://huggingface.co/onnx-community/Qwen2.5-0.5B-Instruct/resolve/main/generation_config.json' },
27-
{ name: 'special_tokens_map.json', url: 'https://huggingface.co/onnx-community/Qwen2.5-0.5B-Instruct/resolve/main/special_tokens_map.json' },
28-
{ name: 'tokenizer.json', url: 'https://huggingface.co/onnx-community/Qwen2.5-0.5B-Instruct/resolve/main/tokenizer.json' },
29-
{ name: 'tokenizer_config.json', url: 'https://huggingface.co/onnx-community/Qwen2.5-0.5B-Instruct/resolve/main/tokenizer_config.json' },
30-
{ name: 'onnx/model_quantized.onnx', url: 'https://huggingface.co/onnx-community/Qwen2.5-0.5B-Instruct/resolve/main/onnx/model_quantized.onnx' }
23+
this.smolLM2ONNXDir = path.join(this.modelDir, 'smollm2-135m-onnx');
24+
this.smolLM2ONNXFiles = [
25+
{ name: 'config.json', url: 'https://huggingface.co/onnx-community/SmolLM2-135M-Instruct-ONNX/resolve/main/config.json' },
26+
{ name: 'generation_config.json', url: 'https://huggingface.co/onnx-community/SmolLM2-135M-Instruct-ONNX/resolve/main/generation_config.json' },
27+
{ name: 'special_tokens_map.json', url: 'https://huggingface.co/onnx-community/SmolLM2-135M-Instruct-ONNX/resolve/main/special_tokens_map.json' },
28+
{ name: 'tokenizer.json', url: 'https://huggingface.co/onnx-community/SmolLM2-135M-Instruct-ONNX/resolve/main/tokenizer.json' },
29+
{ name: 'tokenizer_config.json', url: 'https://huggingface.co/onnx-community/SmolLM2-135M-Instruct-ONNX/resolve/main/tokenizer_config.json' },
30+
{ name: 'onnx/model_quantized.onnx', url: 'https://huggingface.co/onnx-community/SmolLM2-135M-Instruct-ONNX/resolve/main/onnx/model_quantized.onnx' }
3131
];
3232
}
3333

3434
isGraphModelDownloaded() {
35-
return this.qwenONNXFiles.every(file => fs.existsSync(path.join(this.qwenONNXDir, file.name)));
35+
return this.smolLM2ONNXFiles.every(file => fs.existsSync(path.join(this.smolLM2ONNXDir, file.name)));
3636
}
3737

3838
async downloadGraphModel(onProgress = null) {
3939
if (this.isGraphModelDownloaded()) {
40-
log.info('Graph Qwen ONNX model already downloaded');
40+
log.info('Graph SmolLM2 ONNX model already downloaded');
4141
return true;
4242
}
4343
if (isDownloadingGraph) {
@@ -50,28 +50,28 @@ class ModelDownloader {
5050
this.progressCallback = onProgress;
5151

5252
try {
53-
if (!fs.existsSync(this.qwenONNXDir)) {
54-
fs.mkdirSync(this.qwenONNXDir, { recursive: true });
53+
if (!fs.existsSync(this.smolLM2ONNXDir)) {
54+
fs.mkdirSync(this.smolLM2ONNXDir, { recursive: true });
5555
}
5656

57-
log.info('Starting Qwen 2.5 ONNX model download from HuggingFace...');
57+
log.info('Starting SmolLM2 ONNX model download from HuggingFace...');
5858

5959
let completedCount = 0;
60-
for (const file of this.qwenONNXFiles) {
61-
const destPath = path.join(this.qwenONNXDir, file.name);
60+
for (const file of this.smolLM2ONNXFiles) {
61+
const destPath = path.join(this.smolLM2ONNXDir, file.name);
6262
const destDir = path.dirname(destPath);
6363
if (!fs.existsSync(destDir)) {
6464
fs.mkdirSync(destDir, { recursive: true });
6565
}
6666

67-
log.info(`Downloading Qwen ONNX asset: ${file.name}...`);
67+
log.info(`Downloading SmolLM2 ONNX asset: ${file.name}...`);
6868

6969
const isModelFile = file.name.endsWith('.onnx');
7070

7171
await this.downloadFile(file.url, destPath, (bytesRead, totalBytes) => {
72-
if (isModelFile && totalBytes > 0) {
73-
const baseProgress = Math.round((completedCount / this.qwenONNXFiles.length) * 100);
74-
const currentFileProgress = Math.round((bytesRead / totalBytes) * (100 / this.qwenONNXFiles.length));
72+
if (totalBytes > 0) {
73+
const baseProgress = Math.round((completedCount / this.smolLM2ONNXFiles.length) * 100);
74+
const currentFileProgress = Math.round((bytesRead / totalBytes) * (100 / this.smolLM2ONNXFiles.length));
7575
graphProgress = Math.min(99, baseProgress + currentFileProgress);
7676
if (this.progressCallback) {
7777
this.progressCallback(graphProgress);
@@ -80,19 +80,19 @@ class ModelDownloader {
8080
});
8181

8282
completedCount++;
83-
graphProgress = Math.round((completedCount / this.qwenONNXFiles.length) * 100);
83+
graphProgress = Math.round((completedCount / this.smolLM2ONNXFiles.length) * 100);
8484
if (this.progressCallback) {
8585
this.progressCallback(graphProgress);
8686
}
8787
}
8888

89-
log.info('Qwen 2.5 ONNX model downloaded successfully');
89+
log.info('SmolLM2 ONNX model downloaded successfully');
9090
isDownloadingGraph = false;
9191
graphProgress = 100;
9292
return true;
9393
} catch (err) {
9494
isDownloadingGraph = false;
95-
log.error('Failed to download Qwen ONNX model', err);
95+
log.error('Failed to download SmolLM2 ONNX model', err);
9696
throw err;
9797
}
9898
}
@@ -117,6 +117,33 @@ class ModelDownloader {
117117
};
118118
}
119119

120+
deleteModel() {
121+
try {
122+
const modelPath = path.join(this.modelDir, 'model.onnx');
123+
const vocabPath = path.join(this.modelDir, 'vocab.txt');
124+
if (fs.existsSync(modelPath)) fs.unlinkSync(modelPath);
125+
if (fs.existsSync(vocabPath)) fs.unlinkSync(vocabPath);
126+
log.info('Deleted local embedding model files.');
127+
return true;
128+
} catch (err) {
129+
log.error('Failed to delete embedding model files', err);
130+
throw err;
131+
}
132+
}
133+
134+
deleteGraphModel() {
135+
try {
136+
if (fs.existsSync(this.smolLM2ONNXDir)) {
137+
fs.rmSync(this.smolLM2ONNXDir, { recursive: true, force: true });
138+
}
139+
log.info('Deleted local graph ONNX model files.');
140+
return true;
141+
} catch (err) {
142+
log.error('Failed to delete graph model files', err);
143+
throw err;
144+
}
145+
}
146+
120147
async download(onProgress = null) {
121148
if (this.isModelDownloaded()) {
122149
log.info('Model already downloaded');
@@ -171,7 +198,12 @@ class ModelDownloader {
171198
const file = fs.createWriteStream(dest);
172199

173200
const request = (targetUrl) => {
174-
https.get(targetUrl, (response) => {
201+
const options = {
202+
headers: {
203+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) NotelyApp/0.1.27 Chrome/120.0.0.0 Electron/28.0.0 Safari/537.36'
204+
}
205+
};
206+
https.get(targetUrl, options, (response) => {
175207
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307 || response.statusCode === 308) {
176208
// Handle redirects (including relative paths)
177209
let redirectUrl = response.headers.location;
@@ -201,6 +233,10 @@ class ModelDownloader {
201233

202234
response.on('end', () => {
203235
file.end();
236+
});
237+
238+
file.on('finish', () => {
239+
file.close();
204240
resolve();
205241
});
206242
}).on('error', (err) => {

‎ai/embeddings/ONNXEmbedder.js‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ class ONNXEmbedder {
1818
if (this.isLoaded) return;
1919
try {
2020
log.info('Loading local ONNX embedding session...');
21-
try {
22-
this.ort = require('onnxruntime-node');
23-
} catch (err) {
24-
log.warn('Failed to load native onnxruntime-node. Trying onnxruntime-web (WASM) fallback:', err.message);
25-
this.ort = require('onnxruntime-web');
21+
this.ort = require('onnxruntime-web');
22+
this.ort.env.wasm.numThreads = 1;
23+
24+
const { pathToFileURL } = require('url');
25+
let wasmDir = path.dirname(require.resolve('onnxruntime-web')) + path.sep;
26+
if (wasmDir.includes('app.asar')) {
27+
wasmDir = wasmDir.replace('app.asar', 'app.asar.unpacked');
2628
}
29+
this.ort.env.wasm.wasmPaths = pathToFileURL(wasmDir).href;
2730

2831
const modelPath = path.join(this.modelDir, 'model.onnx');
2932
const vocabPath = path.join(this.modelDir, 'vocab.txt');

‎ai/graph/GraphBuilder.js‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class GraphBuilder {
1919
/**
2020
* Scan notes and rebuild the Knowledge Graph
2121
*/
22-
async rebuild() {
22+
async rebuild(onProgress = null) {
2323
if (this.isRebuilding) {
2424
log.warn('Rebuild already in progress');
2525
return { success: false, error: 'Rebuild already in progress' };
@@ -43,13 +43,21 @@ class GraphBuilder {
4343

4444
// Find all markdown files in the workspace
4545
const workspaceFiles = this._getWorkspaceMarkdownFiles();
46-
log.info(`Found ${workspaceFiles.length} markdown notes to index for graph`);
47-
logDb.addLog('graph', `Found ${workspaceFiles.length} markdown notes to index for graph`, 'info');
46+
const total = workspaceFiles.length;
47+
log.info(`Found ${total} markdown notes to index for graph`);
48+
logDb.addLog('graph', `Found ${total} markdown notes to index for graph`, 'info');
4849

4950
let processedCount = 0;
5051
let failedCount = 0;
5152

52-
for (const filePath of workspaceFiles) {
53+
for (let i = 0; i < total; i++) {
54+
// Yield event loop between heavy CPU/LLM processing steps so main thread stays 100% responsive
55+
await new Promise(resolve => setTimeout(resolve, 50));
56+
57+
const filePath = workspaceFiles[i];
58+
if (typeof onProgress === 'function') {
59+
onProgress({ current: i + 1, total, noteName: path.basename(filePath) });
60+
}
5361
try {
5462
if (!fs.existsSync(filePath)) {
5563
failedCount++;

‎ai/graph/GraphDB.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,24 @@ class GraphDB {
186186
return { nodeCount, edgeCount, sizeBytes };
187187
}
188188

189+
getNodeCount() {
190+
if (!this.db) return 0;
191+
try {
192+
return this.db.prepare('SELECT COUNT(*) as count FROM entities').get()?.count || 0;
193+
} catch {
194+
return 0;
195+
}
196+
}
197+
198+
getEdgeCount() {
199+
if (!this.db) return 0;
200+
try {
201+
return this.db.prepare('SELECT COUNT(*) as count FROM relationships').get()?.count || 0;
202+
} catch {
203+
return 0;
204+
}
205+
}
206+
189207
/**
190208
* Purge all entities and relationships
191209
*/

0 commit comments

Comments
 (0)