escribano 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -26
- package/dist/actions/generate-artifact-v3.js +5 -3
- package/dist/actions/generate-summary-v3.js +29 -4
- package/dist/adapters/cap.adapter.js +94 -0
- package/dist/adapters/intelligence.adapter.js +202 -0
- package/dist/adapters/intelligence.mlx.adapter.js +258 -185
- package/dist/adapters/storage.adapter.js +81 -0
- package/dist/adapters/whisper.adapter.js +168 -0
- package/dist/batch-context.js +91 -34
- package/dist/config.js +12 -1
- package/dist/db/repositories/subject.sqlite.js +1 -1
- package/dist/domain/context.js +97 -0
- package/dist/domain/index.js +2 -0
- package/dist/domain/observation.js +17 -0
- package/dist/python-utils.js +28 -10
- package/dist/services/subject-grouping.js +36 -9
- package/dist/test-classification-prompts.js +181 -0
- package/dist/tests/cap.adapter.test.js +75 -0
- package/dist/tests/intelligence.adapter.test.js +102 -0
- package/dist/tests/intelligence.mlx.adapter.test.js +13 -8
- package/dist/utils/model-detector.js +105 -2
- package/migrations/010_llm_backend_metadata.sql +25 -0
- package/migrations/011_llm_debug_log.sql +19 -0
- package/migrations/012_llm_debug_log_prompt_result.sql +20 -0
- package/package.json +1 -1
- package/scripts/mlx_bridge.py +574 -74
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Escribano - Intelligence Adapter (MLX
|
|
2
|
+
* Escribano - Intelligence Adapter (MLX)
|
|
3
3
|
*
|
|
4
|
-
* Implements IntelligenceService using MLX-VLM via Unix domain
|
|
5
|
-
* Uses
|
|
4
|
+
* Implements IntelligenceService using MLX-VLM and MLX-LM via Unix domain sockets.
|
|
5
|
+
* Uses separate bridge processes for VLM (frame analysis) and LLM (text generation).
|
|
6
6
|
*
|
|
7
7
|
* Architecture:
|
|
8
|
-
* TypeScript (this file) <--Unix Socket--> Python (mlx_bridge.py)
|
|
8
|
+
* TypeScript (this file) <--Unix Socket--> Python (mlx_bridge.py --mode vlm)
|
|
9
|
+
* TypeScript (this file) <--Unix Socket--> Python (mlx_bridge.py --mode llm)
|
|
10
|
+
*
|
|
11
|
+
* The caller only sees a single IntelligenceService. Internally, we manage:
|
|
12
|
+
* - VLM bridge: spawns lazily on describeImages(), uses -vlm.sock
|
|
13
|
+
* - LLM bridge: spawns lazily on generateText(), uses -llm.sock
|
|
9
14
|
*
|
|
10
15
|
* See docs/adr/006-mlx-vlm-adapter.md for full design.
|
|
11
16
|
*/
|
|
@@ -16,20 +21,15 @@ import { dirname, resolve } from 'node:path';
|
|
|
16
21
|
import { fileURLToPath } from 'node:url';
|
|
17
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
23
|
import { loadConfig } from '../config.js';
|
|
19
|
-
import {
|
|
24
|
+
import { getDbPath } from '../db/index.js';
|
|
25
|
+
import { ESCRIBANO_HOME, ESCRIBANO_VENV_PYTHON, getPythonPath, } from '../python-utils.js';
|
|
26
|
+
import { selectBestMLXModel } from '../utils/model-detector.js';
|
|
20
27
|
function debugLog(...args) {
|
|
21
28
|
const config = loadConfig();
|
|
22
29
|
if (config.verbose) {
|
|
23
|
-
console.log('[
|
|
30
|
+
console.log('[MLX]', ...args);
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
|
-
/** pip binary inside Escribano's managed venv. */
|
|
27
|
-
const _ESCRIBANO_VENV_PIP = resolve(ESCRIBANO_VENV, 'bin', 'pip');
|
|
28
|
-
/**
|
|
29
|
-
* Run a command, streaming stdout/stderr directly to the terminal.
|
|
30
|
-
* Used for long-running setup tasks (venv creation, pip install) so the
|
|
31
|
-
* user can see progress in real time.
|
|
32
|
-
*/
|
|
33
33
|
function runVisible(cmd, args) {
|
|
34
34
|
return new Promise((res, rej) => {
|
|
35
35
|
const proc = spawn(cmd, args, { stdio: 'inherit' });
|
|
@@ -37,9 +37,6 @@ function runVisible(cmd, args) {
|
|
|
37
37
|
proc.on('error', rej);
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Run a command silently (discard output). Used for quick probe checks.
|
|
42
|
-
*/
|
|
43
40
|
function runSilent(cmd, args) {
|
|
44
41
|
return new Promise((res, rej) => {
|
|
45
42
|
const proc = spawn(cmd, args, { stdio: 'ignore' });
|
|
@@ -47,25 +44,19 @@ function runSilent(cmd, args) {
|
|
|
47
44
|
proc.on('error', rej);
|
|
48
45
|
});
|
|
49
46
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Ensure ~/.escribano/venv exists and has mlx-vlm installed.
|
|
52
|
-
* Uses plain `python3 -m venv` — no uv, no pip flags, no fuss.
|
|
53
|
-
* On first run this takes a few minutes; subsequent runs are instant.
|
|
54
|
-
*/
|
|
55
47
|
async function ensureEscribanoVenv() {
|
|
56
48
|
if (!existsSync(ESCRIBANO_HOME)) {
|
|
57
49
|
mkdirSync(ESCRIBANO_HOME, { recursive: true });
|
|
58
50
|
}
|
|
59
51
|
if (!existsSync(ESCRIBANO_VENV_PYTHON)) {
|
|
60
|
-
console.log('[
|
|
61
|
-
await runVisible('python3', ['-m', 'venv',
|
|
52
|
+
console.log('[MLX] First-time setup: creating Python environment at ~/.escribano/venv');
|
|
53
|
+
await runVisible('python3', ['-m', 'venv', `${ESCRIBANO_HOME}/venv`]);
|
|
62
54
|
}
|
|
63
|
-
// Check whether mlx-vlm and required runtime deps are already importable (~0.3s probe)
|
|
64
55
|
let mlxReady = false;
|
|
65
56
|
try {
|
|
66
57
|
await runSilent(ESCRIBANO_VENV_PYTHON, [
|
|
67
58
|
'-c',
|
|
68
|
-
'import mlx_vlm; import torch; import torchvision',
|
|
59
|
+
'import mlx_vlm; import mlx_lm; import torch; import torchvision',
|
|
69
60
|
]);
|
|
70
61
|
mlxReady = true;
|
|
71
62
|
}
|
|
@@ -73,13 +64,12 @@ async function ensureEscribanoVenv() {
|
|
|
73
64
|
// not installed yet
|
|
74
65
|
}
|
|
75
66
|
if (!mlxReady) {
|
|
76
|
-
console.log('[
|
|
77
|
-
// Ensure pip is available in the venv; ignore failures if ensurepip is disabled.
|
|
67
|
+
console.log('[MLX] Installing mlx-vlm into ~/.escribano/venv (first run — this may take a few minutes)...');
|
|
78
68
|
try {
|
|
79
69
|
await runVisible(ESCRIBANO_VENV_PYTHON, ['-m', 'ensurepip', '--upgrade']);
|
|
80
70
|
}
|
|
81
71
|
catch {
|
|
82
|
-
// ensurepip may be unavailable
|
|
72
|
+
// ensurepip may be unavailable
|
|
83
73
|
}
|
|
84
74
|
await runVisible(ESCRIBANO_VENV_PYTHON, [
|
|
85
75
|
'-m',
|
|
@@ -88,25 +78,16 @@ async function ensureEscribanoVenv() {
|
|
|
88
78
|
'mlx-vlm',
|
|
89
79
|
'torch',
|
|
90
80
|
'torchvision',
|
|
81
|
+
'mlx-lm',
|
|
91
82
|
]);
|
|
92
|
-
console.log('[
|
|
83
|
+
console.log('[MLX] mlx-vlm and mlx-lm installed successfully.');
|
|
93
84
|
}
|
|
94
85
|
return ESCRIBANO_VENV_PYTHON;
|
|
95
86
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Resolve the Python executable to use for the MLX bridge.
|
|
98
|
-
* If the user has configured an explicit environment, use it.
|
|
99
|
-
* Otherwise, transparently create and populate ~/.escribano/venv.
|
|
100
|
-
*/
|
|
101
87
|
export async function resolvePythonPath() {
|
|
102
88
|
return getPythonPath() ?? ensureEscribanoVenv();
|
|
103
89
|
}
|
|
104
|
-
// Global cleanup function to track the current bridge instance
|
|
105
90
|
let globalCleanup = null;
|
|
106
|
-
/**
|
|
107
|
-
* Cleanup the MLX bridge process.
|
|
108
|
-
* Should be called explicitly before process exit.
|
|
109
|
-
*/
|
|
110
91
|
export function cleanupMlxBridge() {
|
|
111
92
|
if (globalCleanup) {
|
|
112
93
|
debugLog('Explicit cleanup called');
|
|
@@ -114,12 +95,6 @@ export function cleanupMlxBridge() {
|
|
|
114
95
|
globalCleanup = null;
|
|
115
96
|
}
|
|
116
97
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Create MLX-VLM intelligence service.
|
|
119
|
-
*
|
|
120
|
-
* Note: This adapter only implements describeImages() for VLM processing.
|
|
121
|
-
* Other methods (classify, generate, etc.) are not implemented and will throw.
|
|
122
|
-
*/
|
|
123
98
|
export function createMlxIntelligenceService(_config = {}) {
|
|
124
99
|
// Load unified config (respects env vars, config file, and RAM-aware defaults)
|
|
125
100
|
const config = loadConfig();
|
|
@@ -131,79 +106,112 @@ export function createMlxIntelligenceService(_config = {}) {
|
|
|
131
106
|
bridgeScript: resolve(__dirname, '../../scripts/mlx_bridge.py'),
|
|
132
107
|
startupTimeout: config.mlxStartupTimeout,
|
|
133
108
|
};
|
|
134
|
-
const
|
|
109
|
+
const vlmBridge = {
|
|
110
|
+
process: null,
|
|
111
|
+
socket: null,
|
|
112
|
+
ready: false,
|
|
113
|
+
connecting: false,
|
|
114
|
+
};
|
|
115
|
+
const llmBridge = {
|
|
135
116
|
process: null,
|
|
136
117
|
socket: null,
|
|
137
118
|
ready: false,
|
|
138
119
|
connecting: false,
|
|
120
|
+
loadedModel: null,
|
|
139
121
|
};
|
|
140
|
-
|
|
122
|
+
const getVlmSocketPath = () => mlxConfig.socketPath.replace('.sock', '-vlm.sock');
|
|
123
|
+
const getLlmSocketPath = () => mlxConfig.socketPath.replace('.sock', '-llm.sock');
|
|
141
124
|
const cleanup = () => {
|
|
142
|
-
if (
|
|
125
|
+
if (vlmBridge.socket) {
|
|
143
126
|
try {
|
|
144
|
-
|
|
127
|
+
vlmBridge.socket.destroy();
|
|
145
128
|
}
|
|
146
|
-
catch {
|
|
147
|
-
|
|
129
|
+
catch { }
|
|
130
|
+
vlmBridge.socket = null;
|
|
131
|
+
}
|
|
132
|
+
if (vlmBridge.process) {
|
|
133
|
+
try {
|
|
134
|
+
vlmBridge.process.kill('SIGTERM');
|
|
148
135
|
}
|
|
149
|
-
|
|
136
|
+
catch { }
|
|
137
|
+
vlmBridge.process = null;
|
|
150
138
|
}
|
|
151
|
-
|
|
139
|
+
const vlmSock = getVlmSocketPath();
|
|
140
|
+
if (existsSync(vlmSock)) {
|
|
152
141
|
try {
|
|
153
|
-
|
|
142
|
+
unlinkSync(vlmSock);
|
|
154
143
|
}
|
|
155
|
-
catch {
|
|
156
|
-
|
|
144
|
+
catch { }
|
|
145
|
+
}
|
|
146
|
+
vlmBridge.ready = false;
|
|
147
|
+
if (llmBridge.socket) {
|
|
148
|
+
try {
|
|
149
|
+
llmBridge.socket.destroy();
|
|
157
150
|
}
|
|
158
|
-
|
|
151
|
+
catch { }
|
|
152
|
+
llmBridge.socket = null;
|
|
159
153
|
}
|
|
160
|
-
|
|
161
|
-
if (existsSync(mlxConfig.socketPath)) {
|
|
154
|
+
if (llmBridge.process) {
|
|
162
155
|
try {
|
|
163
|
-
|
|
156
|
+
llmBridge.process.kill('SIGTERM');
|
|
164
157
|
}
|
|
165
|
-
catch {
|
|
166
|
-
|
|
158
|
+
catch { }
|
|
159
|
+
llmBridge.process = null;
|
|
160
|
+
}
|
|
161
|
+
const llmSock = getLlmSocketPath();
|
|
162
|
+
if (existsSync(llmSock)) {
|
|
163
|
+
try {
|
|
164
|
+
unlinkSync(llmSock);
|
|
167
165
|
}
|
|
166
|
+
catch { }
|
|
168
167
|
}
|
|
169
|
-
|
|
168
|
+
llmBridge.ready = false;
|
|
169
|
+
llmBridge.loadedModel = null;
|
|
170
170
|
};
|
|
171
|
-
// Register global cleanup
|
|
172
171
|
globalCleanup = cleanup;
|
|
173
|
-
// Also cleanup on process signals
|
|
174
172
|
process.on('SIGTERM', cleanup);
|
|
175
173
|
process.on('SIGINT', cleanup);
|
|
176
|
-
// Cleanup on beforeExit to ensure it runs before process.exit
|
|
177
174
|
process.on('beforeExit', cleanup);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
*/
|
|
181
|
-
const startBridge = async () => {
|
|
182
|
-
if (bridge.process && bridge.ready) {
|
|
175
|
+
const startBridge = async (bridgeState, mode, _socketPath) => {
|
|
176
|
+
if (bridgeState.process && bridgeState.ready)
|
|
183
177
|
return;
|
|
184
|
-
}
|
|
185
|
-
debugLog('Starting MLX bridge...');
|
|
186
|
-
// Resolve (and if needed, auto-create) the Python environment before spawning.
|
|
178
|
+
debugLog(`Starting ${mode.toUpperCase()} bridge...`);
|
|
187
179
|
const pythonPath = await resolvePythonPath();
|
|
188
180
|
debugLog(`Using Python: ${pythonPath}`);
|
|
189
|
-
return new Promise((
|
|
190
|
-
|
|
181
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
182
|
+
const env = {
|
|
183
|
+
...process.env,
|
|
184
|
+
ESCRIBANO_MLX_SOCKET_PATH: mlxConfig.socketPath,
|
|
185
|
+
ESCRIBANO_DB_PATH: getDbPath(),
|
|
186
|
+
ESCRIBANO_DEBUG_LLM: String(config.debugLlm),
|
|
187
|
+
};
|
|
188
|
+
// Debug: log env vars being passed to Python bridge
|
|
189
|
+
if (config.debugLlm) {
|
|
190
|
+
console.log(`[MLX] Passing DEBUG_LLM=${config.debugLlm} to ${mode} bridge`);
|
|
191
|
+
console.log(`[MLX] DB_PATH: ${getDbPath()}`);
|
|
192
|
+
}
|
|
193
|
+
if (mode === 'vlm') {
|
|
194
|
+
env.ESCRIBANO_VLM_MODEL = mlxConfig.model;
|
|
195
|
+
env.ESCRIBANO_VLM_BATCH_SIZE = String(mlxConfig.batchSize);
|
|
196
|
+
env.ESCRIBANO_VLM_MAX_TOKENS = String(mlxConfig.maxTokens);
|
|
197
|
+
}
|
|
198
|
+
bridgeState.process = spawn(pythonPath, [mlxConfig.bridgeScript, '--mode', mode], {
|
|
191
199
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
192
|
-
env
|
|
193
|
-
...process.env,
|
|
194
|
-
ESCRIBANO_VLM_MODEL: mlxConfig.model,
|
|
195
|
-
ESCRIBANO_VLM_BATCH_SIZE: String(mlxConfig.batchSize),
|
|
196
|
-
ESCRIBANO_VLM_MAX_TOKENS: String(mlxConfig.maxTokens),
|
|
197
|
-
ESCRIBANO_MLX_SOCKET_PATH: mlxConfig.socketPath,
|
|
198
|
-
},
|
|
200
|
+
env,
|
|
199
201
|
});
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
+
if (!bridgeState.process.stdout || !bridgeState.process.stderr) {
|
|
203
|
+
rejectPromise(new Error('Failed to create bridge process streams'));
|
|
202
204
|
return;
|
|
203
205
|
}
|
|
204
|
-
// Handle stdout (ready signal is JSON on first line)
|
|
205
206
|
let readyReceived = false;
|
|
206
|
-
|
|
207
|
+
let startupTimer = null;
|
|
208
|
+
const clearStartupTimer = () => {
|
|
209
|
+
if (startupTimer) {
|
|
210
|
+
clearTimeout(startupTimer);
|
|
211
|
+
startupTimer = null;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
bridgeState.process.stdout.on('data', (data) => {
|
|
207
215
|
const lines = data.toString().trim().split('\n');
|
|
208
216
|
for (const line of lines) {
|
|
209
217
|
if (!readyReceived && line.startsWith('{')) {
|
|
@@ -211,92 +219,91 @@ export function createMlxIntelligenceService(_config = {}) {
|
|
|
211
219
|
const msg = JSON.parse(line);
|
|
212
220
|
if (msg.status === 'ready') {
|
|
213
221
|
readyReceived = true;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
clearStartupTimer();
|
|
223
|
+
bridgeState.ready = true;
|
|
224
|
+
debugLog(`${mode.toUpperCase()} bridge ready: ${msg.model || msg.mode}`);
|
|
225
|
+
resolvePromise();
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
|
-
catch {
|
|
220
|
-
// Not JSON, ignore
|
|
221
|
-
}
|
|
228
|
+
catch { }
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
231
|
});
|
|
225
|
-
|
|
226
|
-
bridge.process.stderr.on('data', (data) => {
|
|
232
|
+
bridgeState.process.stderr.on('data', (data) => {
|
|
227
233
|
const text = data.toString().trim();
|
|
228
|
-
if (text)
|
|
234
|
+
if (text)
|
|
229
235
|
console.log(text);
|
|
230
|
-
}
|
|
231
236
|
});
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
bridge.ready = false;
|
|
237
|
+
bridgeState.process.on('exit', (code, signal) => {
|
|
238
|
+
debugLog(`${mode.toUpperCase()} bridge exited: code=${code} signal=${signal}`);
|
|
239
|
+
bridgeState.process = null;
|
|
240
|
+
bridgeState.ready = false;
|
|
237
241
|
if (!readyReceived) {
|
|
238
|
-
|
|
242
|
+
clearStartupTimer();
|
|
243
|
+
rejectPromise(new Error(`${mode.toUpperCase()} bridge failed to start: exit code ${code}`));
|
|
239
244
|
}
|
|
240
245
|
});
|
|
241
|
-
|
|
242
|
-
debugLog(
|
|
246
|
+
bridgeState.process.on('error', (err) => {
|
|
247
|
+
debugLog(`${mode.toUpperCase()} bridge error: ${err.message}`);
|
|
243
248
|
if (!readyReceived) {
|
|
244
|
-
|
|
249
|
+
clearStartupTimer();
|
|
250
|
+
rejectPromise(new Error(`Failed to start ${mode.toUpperCase()} bridge: ${err.message}`));
|
|
245
251
|
}
|
|
246
252
|
});
|
|
247
|
-
|
|
248
|
-
setTimeout(() => {
|
|
253
|
+
startupTimer = setTimeout(() => {
|
|
249
254
|
if (!readyReceived) {
|
|
250
|
-
|
|
255
|
+
startupTimer = null;
|
|
256
|
+
rejectPromise(new Error(`${mode.toUpperCase()} bridge startup timeout (${mlxConfig.startupTimeout / 1000}s)`));
|
|
251
257
|
}
|
|
252
258
|
}, mlxConfig.startupTimeout);
|
|
253
259
|
});
|
|
254
260
|
};
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return new Promise((resolve, reject) => {
|
|
260
|
-
if (bridge.socket && !bridge.socket.destroyed) {
|
|
261
|
-
resolve(bridge.socket);
|
|
261
|
+
const connect = (bridgeState, socketPath) => {
|
|
262
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
263
|
+
if (bridgeState.socket && !bridgeState.socket.destroyed) {
|
|
264
|
+
resolvePromise(bridgeState.socket);
|
|
262
265
|
return;
|
|
263
266
|
}
|
|
264
|
-
|
|
265
|
-
const
|
|
267
|
+
let connectionTimer = null;
|
|
268
|
+
const clearConnectionTimer = () => {
|
|
269
|
+
if (connectionTimer) {
|
|
270
|
+
clearTimeout(connectionTimer);
|
|
271
|
+
connectionTimer = null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
debugLog(`Connecting to socket: ${socketPath}`);
|
|
275
|
+
const client = createConnection(socketPath);
|
|
266
276
|
client.on('connect', () => {
|
|
277
|
+
clearConnectionTimer();
|
|
267
278
|
debugLog('Socket connected');
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
bridgeState.socket = client;
|
|
280
|
+
resolvePromise(client);
|
|
270
281
|
});
|
|
271
282
|
client.on('error', (err) => {
|
|
283
|
+
clearConnectionTimer();
|
|
272
284
|
debugLog(`Socket error: ${err.message}`);
|
|
273
|
-
|
|
274
|
-
|
|
285
|
+
bridgeState.socket = null;
|
|
286
|
+
rejectPromise(new Error(`Socket connection failed: ${err.message}`));
|
|
275
287
|
});
|
|
276
288
|
client.on('close', () => {
|
|
277
289
|
debugLog('Socket closed');
|
|
278
|
-
|
|
290
|
+
bridgeState.socket = null;
|
|
279
291
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
connectionTimer = setTimeout(() => {
|
|
293
|
+
if (!bridgeState.socket) {
|
|
294
|
+
connectionTimer = null;
|
|
283
295
|
client.destroy();
|
|
284
|
-
|
|
296
|
+
rejectPromise(new Error('Socket connection timeout'));
|
|
285
297
|
}
|
|
286
298
|
}, 5000);
|
|
287
299
|
});
|
|
288
300
|
};
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const sendRequest = async (request, onBatch) => {
|
|
293
|
-
// Ensure bridge is running
|
|
294
|
-
if (!bridge.ready) {
|
|
295
|
-
await startBridge();
|
|
301
|
+
const sendRequest = async (bridgeState, socketPath, mode, request, onBatch) => {
|
|
302
|
+
if (!bridgeState.ready) {
|
|
303
|
+
await startBridge(bridgeState, mode, socketPath);
|
|
296
304
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return new Promise((resolve, reject) => {
|
|
305
|
+
const socket = await connect(bridgeState, socketPath);
|
|
306
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
300
307
|
const responses = [];
|
|
301
308
|
let buffer = '';
|
|
302
309
|
const onData = (chunk) => {
|
|
@@ -310,19 +317,16 @@ export function createMlxIntelligenceService(_config = {}) {
|
|
|
310
317
|
try {
|
|
311
318
|
const response = JSON.parse(line);
|
|
312
319
|
if ('error' in response && response.error) {
|
|
313
|
-
|
|
314
|
-
reject(new Error(response.error));
|
|
320
|
+
rejectPromise(new Error(response.error));
|
|
315
321
|
socket.off('data', onData);
|
|
316
322
|
return;
|
|
317
323
|
}
|
|
324
|
+
responses.push(response);
|
|
318
325
|
if ('done' in response && response.done) {
|
|
319
|
-
// Final response
|
|
320
326
|
socket.off('data', onData);
|
|
321
|
-
|
|
327
|
+
resolvePromise(responses);
|
|
322
328
|
return;
|
|
323
329
|
}
|
|
324
|
-
// Batch response
|
|
325
|
-
responses.push(response);
|
|
326
330
|
if (onBatch && 'progress' in response) {
|
|
327
331
|
const resp = response;
|
|
328
332
|
onBatch(response, resp.progress);
|
|
@@ -330,52 +334,32 @@ export function createMlxIntelligenceService(_config = {}) {
|
|
|
330
334
|
}
|
|
331
335
|
catch {
|
|
332
336
|
debugLog(`Failed to parse response: ${line}`);
|
|
333
|
-
// Continue processing, might be partial
|
|
334
337
|
}
|
|
335
338
|
}
|
|
336
339
|
};
|
|
337
340
|
socket.on('data', onData);
|
|
338
341
|
socket.on('error', (err) => {
|
|
339
342
|
socket.off('data', onData);
|
|
340
|
-
|
|
343
|
+
rejectPromise(new Error(`Socket error: ${err.message}`));
|
|
341
344
|
});
|
|
342
|
-
// Send request
|
|
343
345
|
const requestJson = `${JSON.stringify(request)}\n`;
|
|
344
346
|
debugLog(`Sending request: id=${request.id} method=${request.method}`);
|
|
345
347
|
socket.write(requestJson);
|
|
346
348
|
});
|
|
347
349
|
};
|
|
348
|
-
// Return IntelligenceService implementation
|
|
349
350
|
return {
|
|
350
|
-
/**
|
|
351
|
-
* Classify transcript - NOT IMPLEMENTED for MLX backend.
|
|
352
|
-
*/
|
|
353
351
|
async classify(_transcript, _visualLogs) {
|
|
354
|
-
throw new Error('MLX adapter does not support classify(). Use Ollama backend
|
|
352
|
+
throw new Error('MLX adapter does not support classify(). Use Ollama backend.');
|
|
355
353
|
},
|
|
356
|
-
/**
|
|
357
|
-
* Classify segment - NOT IMPLEMENTED for MLX backend.
|
|
358
|
-
*/
|
|
359
354
|
async classifySegment(_segment, _transcript) {
|
|
360
|
-
throw new Error('MLX adapter does not support classifySegment(). Use Ollama backend
|
|
355
|
+
throw new Error('MLX adapter does not support classifySegment(). Use Ollama backend.');
|
|
361
356
|
},
|
|
362
|
-
/**
|
|
363
|
-
* Extract metadata - NOT IMPLEMENTED for MLX backend.
|
|
364
|
-
*/
|
|
365
357
|
async extractMetadata(_transcript, _classification, _visualLogs) {
|
|
366
|
-
throw new Error('MLX adapter does not support extractMetadata(). Use Ollama backend
|
|
358
|
+
throw new Error('MLX adapter does not support extractMetadata(). Use Ollama backend.');
|
|
367
359
|
},
|
|
368
|
-
/**
|
|
369
|
-
* Generate artifact - NOT IMPLEMENTED for MLX backend.
|
|
370
|
-
*/
|
|
371
360
|
async generate(_artifactType, _context) {
|
|
372
|
-
throw new Error('MLX adapter does not support generate(). Use Ollama backend
|
|
361
|
+
throw new Error('MLX adapter does not support generate(). Use Ollama backend.');
|
|
373
362
|
},
|
|
374
|
-
/**
|
|
375
|
-
* Describe images using MLX-VLM with interleaved batching.
|
|
376
|
-
*
|
|
377
|
-
* This is the primary method for VLM frame processing.
|
|
378
|
-
*/
|
|
379
363
|
async describeImages(images, options = {}) {
|
|
380
364
|
const total = images.length;
|
|
381
365
|
if (total === 0) {
|
|
@@ -391,12 +375,10 @@ export function createMlxIntelligenceService(_config = {}) {
|
|
|
391
375
|
if (response.results) {
|
|
392
376
|
for (const result of response.results) {
|
|
393
377
|
allResults.push(result);
|
|
394
|
-
// Fire callback for each frame
|
|
395
378
|
if (options.onImageProcessed) {
|
|
396
379
|
options.onImageProcessed(result, progress);
|
|
397
380
|
}
|
|
398
381
|
}
|
|
399
|
-
// Log progress every 10 frames
|
|
400
382
|
if (progress.current % 10 === 0 ||
|
|
401
383
|
progress.current === progress.total) {
|
|
402
384
|
console.log(`[VLM] [${progress.current}/${progress.total}] frames processed`);
|
|
@@ -404,7 +386,7 @@ export function createMlxIntelligenceService(_config = {}) {
|
|
|
404
386
|
}
|
|
405
387
|
};
|
|
406
388
|
try {
|
|
407
|
-
await sendRequest({
|
|
389
|
+
await sendRequest(vlmBridge, getVlmSocketPath(), 'vlm', {
|
|
408
390
|
id: requestId,
|
|
409
391
|
method: 'describe_images',
|
|
410
392
|
params: {
|
|
@@ -428,29 +410,120 @@ export function createMlxIntelligenceService(_config = {}) {
|
|
|
428
410
|
throw new Error(`MLX VLM processing failed: ${message}`);
|
|
429
411
|
}
|
|
430
412
|
},
|
|
431
|
-
/**
|
|
432
|
-
* Embed text - NOT IMPLEMENTED for MLX backend.
|
|
433
|
-
*/
|
|
434
413
|
async embedText(_texts, _options) {
|
|
435
|
-
throw new Error('MLX adapter does not support embedText(). Use Ollama backend
|
|
414
|
+
throw new Error('MLX adapter does not support embedText(). Use Ollama backend.');
|
|
436
415
|
},
|
|
437
|
-
/**
|
|
438
|
-
* Extract topics - NOT IMPLEMENTED for MLX backend.
|
|
439
|
-
*/
|
|
440
416
|
async extractTopics(_observations) {
|
|
441
|
-
throw new Error('MLX adapter does not support extractTopics(). Use Ollama backend
|
|
417
|
+
throw new Error('MLX adapter does not support extractTopics(). Use Ollama backend.');
|
|
442
418
|
},
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
419
|
+
async generateText(prompt, options) {
|
|
420
|
+
const modelSelection = await selectBestMLXModel();
|
|
421
|
+
const resolvedModel = options?.model || modelSelection.model;
|
|
422
|
+
const requestId = Date.now();
|
|
423
|
+
const llmSocketPath = getLlmSocketPath();
|
|
424
|
+
try {
|
|
425
|
+
if (llmBridge.loadedModel !== resolvedModel) {
|
|
426
|
+
if (llmBridge.loadedModel) {
|
|
427
|
+
debugLog(`Unloading previous LLM model: ${llmBridge.loadedModel}`);
|
|
428
|
+
await sendRequest(llmBridge, llmSocketPath, 'llm', {
|
|
429
|
+
id: requestId,
|
|
430
|
+
method: 'unload_llm',
|
|
431
|
+
params: {},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
debugLog(`Loading LLM model: ${resolvedModel}`);
|
|
435
|
+
console.log(`[LLM] Loading model: ${resolvedModel}`);
|
|
436
|
+
try {
|
|
437
|
+
await sendRequest(llmBridge, llmSocketPath, 'llm', {
|
|
438
|
+
id: requestId + 1,
|
|
439
|
+
method: 'load_llm',
|
|
440
|
+
params: { model: resolvedModel },
|
|
441
|
+
});
|
|
442
|
+
llmBridge.loadedModel = resolvedModel;
|
|
443
|
+
console.log('[LLM] Model loaded');
|
|
444
|
+
}
|
|
445
|
+
catch (loadError) {
|
|
446
|
+
llmBridge.loadedModel = null;
|
|
447
|
+
throw loadError;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
debugLog(`Generating text (${prompt.length} chars)...`);
|
|
451
|
+
const responses = await sendRequest(llmBridge, llmSocketPath, 'llm', {
|
|
452
|
+
id: requestId + 2,
|
|
453
|
+
method: 'generate_text',
|
|
454
|
+
params: {
|
|
455
|
+
rawPrompt: prompt,
|
|
456
|
+
maxTokens: options?.numPredict ?? 8000,
|
|
457
|
+
temperature: 0.7,
|
|
458
|
+
think: options?.think ?? false,
|
|
459
|
+
debugContext: options?.debugContext,
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
if (responses.length === 0) {
|
|
463
|
+
throw new Error('No response from LLM generation');
|
|
464
|
+
}
|
|
465
|
+
const response = responses[0];
|
|
466
|
+
if (response.error) {
|
|
467
|
+
throw new Error(`Text generation failed: ${response.error}`);
|
|
468
|
+
}
|
|
469
|
+
debugLog(`Generated ${response.text?.length || 0} chars`);
|
|
470
|
+
return response.text || '';
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
const message = error.message;
|
|
474
|
+
console.error(`[LLM] ERROR: ${message}`);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
async loadLlm(model) {
|
|
479
|
+
const requestId = Date.now();
|
|
480
|
+
const llmSocketPath = getLlmSocketPath();
|
|
481
|
+
if (llmBridge.loadedModel && llmBridge.loadedModel !== model) {
|
|
482
|
+
await sendRequest(llmBridge, llmSocketPath, 'llm', {
|
|
483
|
+
id: requestId,
|
|
484
|
+
method: 'unload_llm',
|
|
485
|
+
params: {},
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
await sendRequest(llmBridge, llmSocketPath, 'llm', {
|
|
490
|
+
id: requestId + 1,
|
|
491
|
+
method: 'load_llm',
|
|
492
|
+
params: { model },
|
|
493
|
+
});
|
|
494
|
+
llmBridge.loadedModel = model;
|
|
495
|
+
}
|
|
496
|
+
catch (loadError) {
|
|
497
|
+
llmBridge.loadedModel = null;
|
|
498
|
+
throw loadError;
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
async unloadVlm() {
|
|
502
|
+
if (!vlmBridge.ready)
|
|
503
|
+
return;
|
|
504
|
+
const requestId = Date.now();
|
|
505
|
+
await sendRequest(vlmBridge, getVlmSocketPath(), 'vlm', {
|
|
506
|
+
id: requestId,
|
|
507
|
+
method: 'unload_vlm',
|
|
508
|
+
params: {},
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
async unloadLlm() {
|
|
512
|
+
if (!llmBridge.ready)
|
|
513
|
+
return;
|
|
514
|
+
const requestId = Date.now();
|
|
515
|
+
await sendRequest(llmBridge, getLlmSocketPath(), 'llm', {
|
|
516
|
+
id: requestId,
|
|
517
|
+
method: 'unload_llm',
|
|
518
|
+
params: {},
|
|
519
|
+
});
|
|
520
|
+
llmBridge.loadedModel = null;
|
|
448
521
|
},
|
|
449
522
|
getResourceName() {
|
|
450
523
|
return 'mlx-python';
|
|
451
524
|
},
|
|
452
525
|
getPid() {
|
|
453
|
-
return
|
|
526
|
+
return vlmBridge.process?.pid ?? llmBridge.process?.pid ?? null;
|
|
454
527
|
},
|
|
455
528
|
};
|
|
456
529
|
}
|