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.
@@ -1,11 +1,16 @@
1
1
  /**
2
- * Escribano - Intelligence Adapter (MLX-VLM)
2
+ * Escribano - Intelligence Adapter (MLX)
3
3
  *
4
- * Implements IntelligenceService using MLX-VLM via Unix domain socket.
5
- * Uses interleaved batching for 4.7x speedup over Ollama sequential processing.
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 { ESCRIBANO_HOME, ESCRIBANO_VENV, ESCRIBANO_VENV_PYTHON, getPythonPath, } from '../python-utils.js';
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('[VLM] [MLX]', ...args);
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('[VLM] First-time setup: creating Python environment at ~/.escribano/venv');
61
- await runVisible('python3', ['-m', 'venv', ESCRIBANO_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('[VLM] Installing mlx-vlm into ~/.escribano/venv (first run — this may take a few minutes)...');
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; continue and rely on existing pip if present.
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('[VLM] mlx-vlm installed successfully.');
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 bridge = {
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
- // Cleanup on process exit
122
+ const getVlmSocketPath = () => mlxConfig.socketPath.replace('.sock', '-vlm.sock');
123
+ const getLlmSocketPath = () => mlxConfig.socketPath.replace('.sock', '-llm.sock');
141
124
  const cleanup = () => {
142
- if (bridge.socket) {
125
+ if (vlmBridge.socket) {
143
126
  try {
144
- bridge.socket.destroy();
127
+ vlmBridge.socket.destroy();
145
128
  }
146
- catch {
147
- // Ignore
129
+ catch { }
130
+ vlmBridge.socket = null;
131
+ }
132
+ if (vlmBridge.process) {
133
+ try {
134
+ vlmBridge.process.kill('SIGTERM');
148
135
  }
149
- bridge.socket = null;
136
+ catch { }
137
+ vlmBridge.process = null;
150
138
  }
151
- if (bridge.process) {
139
+ const vlmSock = getVlmSocketPath();
140
+ if (existsSync(vlmSock)) {
152
141
  try {
153
- bridge.process.kill('SIGTERM');
142
+ unlinkSync(vlmSock);
154
143
  }
155
- catch {
156
- // Ignore
144
+ catch { }
145
+ }
146
+ vlmBridge.ready = false;
147
+ if (llmBridge.socket) {
148
+ try {
149
+ llmBridge.socket.destroy();
157
150
  }
158
- bridge.process = null;
151
+ catch { }
152
+ llmBridge.socket = null;
159
153
  }
160
- // Clean up socket file if it exists
161
- if (existsSync(mlxConfig.socketPath)) {
154
+ if (llmBridge.process) {
162
155
  try {
163
- unlinkSync(mlxConfig.socketPath);
156
+ llmBridge.process.kill('SIGTERM');
164
157
  }
165
- catch {
166
- // Ignore
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
- bridge.ready = false;
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
- * Start the Python bridge process.
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((resolve, reject) => {
190
- bridge.process = spawn(pythonPath, [mlxConfig.bridgeScript], {
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 (!bridge.process.stdout || !bridge.process.stderr) {
201
- reject(new Error('Failed to create bridge process streams'));
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
- bridge.process.stdout.on('data', (data) => {
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
- bridge.ready = true;
215
- debugLog(`Bridge ready: ${msg.model}`);
216
- resolve();
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
- // Handle stderr (logs from Python)
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
- // Handle process exit
233
- bridge.process.on('exit', (code, signal) => {
234
- debugLog(`Bridge exited: code=${code} signal=${signal}`);
235
- bridge.process = null;
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
- reject(new Error(`Bridge failed to start: exit code ${code}`));
242
+ clearStartupTimer();
243
+ rejectPromise(new Error(`${mode.toUpperCase()} bridge failed to start: exit code ${code}`));
239
244
  }
240
245
  });
241
- bridge.process.on('error', (err) => {
242
- debugLog(`Bridge error: ${err.message}`);
246
+ bridgeState.process.on('error', (err) => {
247
+ debugLog(`${mode.toUpperCase()} bridge error: ${err.message}`);
243
248
  if (!readyReceived) {
244
- reject(new Error(`Failed to start bridge: ${err.message}`));
249
+ clearStartupTimer();
250
+ rejectPromise(new Error(`Failed to start ${mode.toUpperCase()} bridge: ${err.message}`));
245
251
  }
246
252
  });
247
- // Timeout for ready signal
248
- setTimeout(() => {
253
+ startupTimer = setTimeout(() => {
249
254
  if (!readyReceived) {
250
- reject(new Error(`Bridge startup timeout (${mlxConfig.startupTimeout / 1000}s)`));
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
- * Connect to the Unix socket.
257
- */
258
- const connect = () => {
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
- debugLog(`Connecting to socket: ${mlxConfig.socketPath}`);
265
- const client = createConnection(mlxConfig.socketPath);
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
- bridge.socket = client;
269
- resolve(client);
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
- bridge.socket = null;
274
- reject(new Error(`Socket connection failed: ${err.message}`));
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
- bridge.socket = null;
290
+ bridgeState.socket = null;
279
291
  });
280
- // Timeout
281
- setTimeout(() => {
282
- if (!bridge.socket) {
292
+ connectionTimer = setTimeout(() => {
293
+ if (!bridgeState.socket) {
294
+ connectionTimer = null;
283
295
  client.destroy();
284
- reject(new Error('Socket connection timeout'));
296
+ rejectPromise(new Error('Socket connection timeout'));
285
297
  }
286
298
  }, 5000);
287
299
  });
288
300
  };
289
- /**
290
- * Send request and receive streaming NDJSON responses.
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
- // Connect to socket
298
- const socket = await connect();
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
- // Error response
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
- resolve(responses);
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
- reject(new Error(`Socket error: ${err.message}`));
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 for this operation.');
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 for this operation.');
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 for this operation.');
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 for this operation.');
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 for this operation.');
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 for this operation.');
417
+ throw new Error('MLX adapter does not support extractTopics(). Use Ollama backend.');
442
418
  },
443
- /**
444
- * Generate text - NOT IMPLEMENTED for MLX backend.
445
- */
446
- async generateText(_prompt, _options) {
447
- throw new Error('MLX adapter does not support generateText(). Use Ollama backend for this operation.');
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 bridge.process?.pid ?? null;
526
+ return vlmBridge.process?.pid ?? llmBridge.process?.pid ?? null;
454
527
  },
455
528
  };
456
529
  }