voyageai-cli 1.26.0 → 1.27.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/package.json +1 -1
- package/src/commands/chat.js +281 -78
- package/src/commands/doctor.js +157 -14
- package/src/commands/playground.js +233 -19
- package/src/lib/chat.js +170 -4
- package/src/lib/llm.js +304 -2
- package/src/lib/mongo.js +6 -6
- package/src/lib/prompt.js +60 -1
- package/src/lib/tool-registry.js +194 -0
- package/src/mcp/tools/embedding.js +55 -43
- package/src/mcp/tools/ingest.js +74 -67
- package/src/mcp/tools/management.js +60 -48
- package/src/mcp/tools/retrieval.js +181 -163
- package/src/mcp/tools/utility.js +171 -153
- package/src/playground/icons/dark/128.png +0 -0
- package/src/playground/icons/dark/16.png +0 -0
- package/src/playground/icons/dark/256.png +0 -0
- package/src/playground/icons/dark/32.png +0 -0
- package/src/playground/icons/dark/64.png +0 -0
- package/src/playground/icons/light/128.png +0 -0
- package/src/playground/icons/light/16.png +0 -0
- package/src/playground/icons/light/256.png +0 -0
- package/src/playground/icons/light/32.png +0 -0
- package/src/playground/icons/light/64.png +0 -0
- package/src/playground/index.html +2769 -27
package/src/commands/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ const os = require('os');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const pc = require('picocolors');
|
|
7
|
-
const { getConfigValue } = require('../lib/config');
|
|
7
|
+
const { getConfigValue, setConfigValue, CONFIG_DIR, CONFIG_PATH } = require('../lib/config');
|
|
8
8
|
const { getApiBase } = require('../lib/api');
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -240,15 +240,83 @@ async function checkConfig() {
|
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// ── Fix functions (for --fix mode) ──
|
|
244
|
+
|
|
245
|
+
async function fixApiKey() {
|
|
246
|
+
const readline = require('readline');
|
|
247
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
248
|
+
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
console.log(pc.cyan('\n Fixing: Voyage AI API Key'));
|
|
251
|
+
console.log(pc.dim(' Get a key at: https://dash.voyageai.com/api-keys\n'));
|
|
252
|
+
|
|
253
|
+
rl.question(' Enter your Voyage AI API key: ', (key) => {
|
|
254
|
+
rl.close();
|
|
255
|
+
const trimmed = (key || '').trim();
|
|
256
|
+
if (!trimmed) {
|
|
257
|
+
console.log(pc.yellow(' Skipped (no key entered)'));
|
|
258
|
+
resolve(false);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
setConfigValue('apiKey', trimmed);
|
|
263
|
+
// Also set in env for the current session so the connection check passes
|
|
264
|
+
process.env.VOYAGE_API_KEY = trimmed;
|
|
265
|
+
console.log(pc.green(' ✓ API key saved to ~/.vai/config.json'));
|
|
266
|
+
resolve(true);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.log(pc.red(` ✗ Failed to save: ${err.message}`));
|
|
269
|
+
resolve(false);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function fixConfigPermissions() {
|
|
276
|
+
try {
|
|
277
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
278
|
+
console.log(pc.green(' ✓ Fixed ~/.vai/config.json permissions to 600'));
|
|
279
|
+
return true;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.log(pc.red(` ✗ Failed to fix permissions: ${err.message}`));
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function fixConfigDir() {
|
|
287
|
+
try {
|
|
288
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
289
|
+
console.log(pc.green(' ✓ Created ~/.vai/ directory'));
|
|
290
|
+
return true;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.log(pc.red(` ✗ Failed to create directory: ${err.message}`));
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function fixPdfParse() {
|
|
298
|
+
const { execSync } = require('child_process');
|
|
299
|
+
console.log(pc.cyan('\n Installing pdf-parse...'));
|
|
300
|
+
try {
|
|
301
|
+
execSync('npm install pdf-parse', { stdio: 'pipe' });
|
|
302
|
+
console.log(pc.green(' ✓ pdf-parse installed'));
|
|
303
|
+
return true;
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.log(pc.red(` ✗ Failed to install: ${err.message}`));
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
243
310
|
async function runDoctor(options = {}) {
|
|
244
|
-
const { json, verbose } = options;
|
|
245
|
-
|
|
311
|
+
const { json, verbose, fix } = options;
|
|
312
|
+
|
|
246
313
|
console.log(pc.bold('\n🩺 Voyage AI CLI Health Check\n'));
|
|
247
|
-
|
|
314
|
+
|
|
248
315
|
const results = {};
|
|
249
316
|
let hasError = false;
|
|
250
317
|
let hasWarning = false;
|
|
251
|
-
|
|
318
|
+
const fixable = [];
|
|
319
|
+
|
|
252
320
|
// Run all checks
|
|
253
321
|
const checks = [
|
|
254
322
|
{ key: 'node', fn: checkNodeVersion },
|
|
@@ -258,12 +326,12 @@ async function runDoctor(options = {}) {
|
|
|
258
326
|
{ key: 'pdfParse', fn: checkPdfParse },
|
|
259
327
|
{ key: 'config', fn: checkConfig },
|
|
260
328
|
];
|
|
261
|
-
|
|
329
|
+
|
|
262
330
|
for (const { key, fn } of checks) {
|
|
263
331
|
const check = CHECKS[key];
|
|
264
332
|
const result = await fn();
|
|
265
333
|
results[key] = result;
|
|
266
|
-
|
|
334
|
+
|
|
267
335
|
// Determine status icon
|
|
268
336
|
let icon;
|
|
269
337
|
if (result.ok === true) {
|
|
@@ -276,15 +344,23 @@ async function runDoctor(options = {}) {
|
|
|
276
344
|
icon = warnMark();
|
|
277
345
|
hasWarning = true;
|
|
278
346
|
}
|
|
279
|
-
|
|
347
|
+
|
|
280
348
|
// Print result
|
|
281
349
|
console.log(` ${icon} ${pc.bold(check.name)}: ${result.message}`);
|
|
282
|
-
|
|
350
|
+
|
|
283
351
|
if (result.hint && (verbose || result.ok === false)) {
|
|
284
352
|
console.log(` ${pc.dim(result.hint)}`);
|
|
285
353
|
}
|
|
354
|
+
|
|
355
|
+
// Track fixable issues
|
|
356
|
+
if (result.ok !== true) {
|
|
357
|
+
if (key === 'apiKey' && result.ok === false) fixable.push('apiKey');
|
|
358
|
+
if (key === 'pdfParse' && result.ok === null) fixable.push('pdfParse');
|
|
359
|
+
if (key === 'config' && result.message && result.message.includes('permissions')) fixable.push('configPerms');
|
|
360
|
+
if (key === 'config' && result.ok === null) fixable.push('configDir');
|
|
361
|
+
}
|
|
286
362
|
}
|
|
287
|
-
|
|
363
|
+
|
|
288
364
|
// Summary
|
|
289
365
|
console.log('');
|
|
290
366
|
if (hasError) {
|
|
@@ -294,7 +370,43 @@ async function runDoctor(options = {}) {
|
|
|
294
370
|
} else {
|
|
295
371
|
console.log(pc.green(' ✓ All checks passed. vai is ready to use!\n'));
|
|
296
372
|
}
|
|
297
|
-
|
|
373
|
+
|
|
374
|
+
// --fix mode: attempt automatic repairs
|
|
375
|
+
if (fix && fixable.length > 0) {
|
|
376
|
+
console.log(pc.bold(' 🔧 Attempting fixes...\n'));
|
|
377
|
+
let fixed = 0;
|
|
378
|
+
|
|
379
|
+
for (const item of fixable) {
|
|
380
|
+
if (item === 'configDir') {
|
|
381
|
+
if (fixConfigDir()) fixed++;
|
|
382
|
+
}
|
|
383
|
+
if (item === 'apiKey') {
|
|
384
|
+
if (await fixApiKey()) fixed++;
|
|
385
|
+
}
|
|
386
|
+
if (item === 'configPerms') {
|
|
387
|
+
if (fixConfigPermissions()) fixed++;
|
|
388
|
+
}
|
|
389
|
+
if (item === 'pdfParse') {
|
|
390
|
+
if (await fixPdfParse()) fixed++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log('');
|
|
395
|
+
if (fixed > 0) {
|
|
396
|
+
console.log(pc.green(` ✓ Fixed ${fixed} issue${fixed === 1 ? '' : 's'}. Run ${pc.bold('vai doctor')} again to verify.\n`));
|
|
397
|
+
} else {
|
|
398
|
+
console.log(pc.yellow(' No issues were fixed. See hints above for manual steps.\n'));
|
|
399
|
+
}
|
|
400
|
+
return 0;
|
|
401
|
+
} else if (fix && fixable.length === 0 && (hasError || hasWarning)) {
|
|
402
|
+
console.log(pc.dim(' No auto-fixable issues found. See hints above for manual steps.\n'));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Suggest --fix if there are fixable issues and not in fix mode
|
|
406
|
+
if (!fix && fixable.length > 0) {
|
|
407
|
+
console.log(pc.dim(` Tip: run ${pc.bold('vai doctor --fix')} to attempt automatic repairs\n`));
|
|
408
|
+
}
|
|
409
|
+
|
|
298
410
|
// Suggest next steps
|
|
299
411
|
if (!hasError) {
|
|
300
412
|
console.log(pc.dim(' Next steps:'));
|
|
@@ -302,11 +414,11 @@ async function runDoctor(options = {}) {
|
|
|
302
414
|
console.log(pc.dim(' vai quickstart — Zero-to-search tutorial'));
|
|
303
415
|
console.log(pc.dim(' vai explain — Learn key concepts\n'));
|
|
304
416
|
}
|
|
305
|
-
|
|
417
|
+
|
|
306
418
|
if (json) {
|
|
307
419
|
console.log(JSON.stringify(results, null, 2));
|
|
308
420
|
}
|
|
309
|
-
|
|
421
|
+
|
|
310
422
|
return hasError ? 1 : 0;
|
|
311
423
|
}
|
|
312
424
|
|
|
@@ -316,10 +428,41 @@ function register(program) {
|
|
|
316
428
|
.description('Run health checks on your vai setup')
|
|
317
429
|
.option('--json', 'Output results as JSON')
|
|
318
430
|
.option('-v, --verbose', 'Show hints for all checks')
|
|
431
|
+
.option('-f, --fix', 'Attempt to fix issues automatically')
|
|
319
432
|
.action(async (options) => {
|
|
320
433
|
const exitCode = await runDoctor(options);
|
|
321
434
|
if (exitCode !== 0) process.exit(exitCode);
|
|
322
435
|
});
|
|
323
436
|
}
|
|
324
437
|
|
|
325
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Run all checks programmatically and return structured results.
|
|
440
|
+
* Used by the playground /api/doctor endpoint.
|
|
441
|
+
*/
|
|
442
|
+
async function runChecks() {
|
|
443
|
+
const checks = [
|
|
444
|
+
{ key: 'node', fn: checkNodeVersion },
|
|
445
|
+
{ key: 'apiKey', fn: checkApiKey },
|
|
446
|
+
{ key: 'apiConnection', fn: checkApiConnection },
|
|
447
|
+
{ key: 'mongodb', fn: checkMongoDB },
|
|
448
|
+
{ key: 'pdfParse', fn: checkPdfParse },
|
|
449
|
+
{ key: 'config', fn: checkConfig },
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
const results = {};
|
|
453
|
+
for (const { key, fn } of checks) {
|
|
454
|
+
const result = await fn();
|
|
455
|
+
// Strip picocolors ANSI codes from messages for JSON output
|
|
456
|
+
const clean = (s) => typeof s === 'string' ? s.replace(/\x1b\[[0-9;]*m/g, '') : s;
|
|
457
|
+
results[key] = {
|
|
458
|
+
name: CHECKS[key].name,
|
|
459
|
+
required: CHECKS[key].required,
|
|
460
|
+
ok: result.ok,
|
|
461
|
+
message: clean(result.message),
|
|
462
|
+
hint: result.hint ? clean(result.hint) : null,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return results;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
module.exports = { register, runDoctor, runChecks };
|
|
@@ -277,6 +277,7 @@ function createPlaygroundServer() {
|
|
|
277
277
|
db: proj.db || null,
|
|
278
278
|
collection: proj.collection || null,
|
|
279
279
|
chat: proj.chat || {},
|
|
280
|
+
mode: proj.chat?.mode || 'pipeline',
|
|
280
281
|
}));
|
|
281
282
|
return;
|
|
282
283
|
}
|
|
@@ -308,6 +309,20 @@ function createPlaygroundServer() {
|
|
|
308
309
|
return;
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
// API: Doctor health checks
|
|
313
|
+
if (req.method === 'GET' && req.url === '/api/doctor') {
|
|
314
|
+
try {
|
|
315
|
+
const { runChecks } = require('./doctor');
|
|
316
|
+
const results = await runChecks();
|
|
317
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
318
|
+
res.end(JSON.stringify(results));
|
|
319
|
+
} catch (err) {
|
|
320
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
321
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
311
326
|
// API: Settings origins — where each config value comes from
|
|
312
327
|
if (req.method === 'GET' && req.url === '/api/settings/origins') {
|
|
313
328
|
const { resolveLLMConfig } = require('../lib/llm');
|
|
@@ -337,6 +352,40 @@ function createPlaygroundServer() {
|
|
|
337
352
|
return;
|
|
338
353
|
}
|
|
339
354
|
|
|
355
|
+
// API: List built-in workflows
|
|
356
|
+
if (req.method === 'GET' && req.url === '/api/workflows') {
|
|
357
|
+
try {
|
|
358
|
+
const { listBuiltinWorkflows } = require('../lib/workflow');
|
|
359
|
+
const workflows = listBuiltinWorkflows();
|
|
360
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
361
|
+
res.end(JSON.stringify({ workflows }));
|
|
362
|
+
} catch (err) {
|
|
363
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// API: Get a specific workflow by name
|
|
370
|
+
if (req.method === 'GET' && req.url?.startsWith('/api/workflows/')) {
|
|
371
|
+
const name = decodeURIComponent(req.url.replace('/api/workflows/', ''));
|
|
372
|
+
if (!name) {
|
|
373
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
374
|
+
res.end(JSON.stringify({ error: 'Workflow name is required' }));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const { loadWorkflow } = require('../lib/workflow');
|
|
379
|
+
const definition = loadWorkflow(name);
|
|
380
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
381
|
+
res.end(JSON.stringify({ definition }));
|
|
382
|
+
} catch (err) {
|
|
383
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
384
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
340
389
|
// API: Save chat config (POST) — persists to .vai.json
|
|
341
390
|
// Placed before generic POST handler so it doesn't require Voyage API key
|
|
342
391
|
if (req.method === 'POST' && req.url === '/api/chat/config') {
|
|
@@ -364,6 +413,7 @@ function createPlaygroundServer() {
|
|
|
364
413
|
if (parsed.maxDocs !== undefined) proj.chat.maxContextDocs = parsed.maxDocs;
|
|
365
414
|
if (parsed.rerank !== undefined) proj.chat.rerank = parsed.rerank;
|
|
366
415
|
if (parsed.systemPrompt !== undefined) proj.chat.systemPrompt = parsed.systemPrompt;
|
|
416
|
+
if (parsed.mode !== undefined) proj.chat.mode = parsed.mode;
|
|
367
417
|
|
|
368
418
|
try {
|
|
369
419
|
saveProject(proj, filePath || undefined);
|
|
@@ -401,20 +451,23 @@ function createPlaygroundServer() {
|
|
|
401
451
|
|
|
402
452
|
// API: Chat message (streaming SSE)
|
|
403
453
|
if (req.url === '/api/chat/message') {
|
|
404
|
-
const { query, db, collection, provider, model, maxDocs, rerank, systemPrompt } = parsed;
|
|
454
|
+
const { query, db, collection, provider, model, maxDocs, rerank, systemPrompt, mode } = parsed;
|
|
455
|
+
const isAgent = mode === 'agent';
|
|
456
|
+
|
|
405
457
|
if (!query) {
|
|
406
458
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
407
459
|
res.end(JSON.stringify({ error: 'query is required' }));
|
|
408
460
|
return;
|
|
409
461
|
}
|
|
410
|
-
|
|
462
|
+
// Pipeline mode requires db + collection; agent mode they're optional
|
|
463
|
+
if (!isAgent && (!db || !collection)) {
|
|
411
464
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
412
|
-
res.end(JSON.stringify({ error: 'db and collection are required' }));
|
|
465
|
+
res.end(JSON.stringify({ error: 'db and collection are required for pipeline mode' }));
|
|
413
466
|
return;
|
|
414
467
|
}
|
|
415
468
|
|
|
416
469
|
const { createLLMProvider } = require('../lib/llm');
|
|
417
|
-
const { chatTurn } = require('../lib/chat');
|
|
470
|
+
const { chatTurn, agentChatTurn } = require('../lib/chat');
|
|
418
471
|
const { ChatHistory } = require('../lib/history');
|
|
419
472
|
|
|
420
473
|
let llm;
|
|
@@ -447,21 +500,68 @@ function createPlaygroundServer() {
|
|
|
447
500
|
});
|
|
448
501
|
|
|
449
502
|
try {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
503
|
+
if (isAgent) {
|
|
504
|
+
// Agent mode: LLM decides which tools to call
|
|
505
|
+
for await (const event of agentChatTurn({
|
|
506
|
+
query, llm, history,
|
|
507
|
+
opts: { systemPrompt, db: db || undefined, collection: collection || undefined },
|
|
508
|
+
})) {
|
|
509
|
+
if (event.type === 'tool_call') {
|
|
510
|
+
const { name, args, timeMs, error, result } = event.data;
|
|
511
|
+
// Build a short human-readable summary of the tool result
|
|
512
|
+
let resultSummary = '';
|
|
513
|
+
if (!error && result) {
|
|
514
|
+
if (name === 'vai_query' || name === 'vai_search') {
|
|
515
|
+
const docs = result.results || result.documents || [];
|
|
516
|
+
resultSummary = `Found ${docs.length} result${docs.length !== 1 ? 's' : ''}`;
|
|
517
|
+
} else if (name === 'vai_collections') {
|
|
518
|
+
const colls = result.collections || [];
|
|
519
|
+
resultSummary = colls.length > 0
|
|
520
|
+
? colls.map(c => `<code>${c.name || c}</code>`).slice(0, 5).join(', ')
|
|
521
|
+
: 'No collections found';
|
|
522
|
+
} else if (name === 'vai_models') {
|
|
523
|
+
const models = result.models || [];
|
|
524
|
+
resultSummary = `${models.length} model${models.length !== 1 ? 's' : ''} available`;
|
|
525
|
+
} else if (name === 'vai_embed') {
|
|
526
|
+
const dims = result.dimensions || result.embedding?.length || '?';
|
|
527
|
+
resultSummary = `${dims}-dim vector`;
|
|
528
|
+
} else if (name === 'vai_similarity') {
|
|
529
|
+
const score = result.similarity ?? result.score;
|
|
530
|
+
resultSummary = score !== undefined ? `Score: ${Number(score).toFixed(4)}` : '';
|
|
531
|
+
} else if (name === 'vai_rerank') {
|
|
532
|
+
const items = result.results || [];
|
|
533
|
+
resultSummary = `Reranked ${items.length} result${items.length !== 1 ? 's' : ''}`;
|
|
534
|
+
} else if (name === 'vai_estimate') {
|
|
535
|
+
resultSummary = result.recommendation || '';
|
|
536
|
+
} else if (name === 'vai_explain' || name === 'vai_topics') {
|
|
537
|
+
resultSummary = result.title || result.topic || '';
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
res.write(`event: tool_call\ndata: ${JSON.stringify({ name, args, timeMs, error, resultSummary })}\n\n`);
|
|
541
|
+
} else if (event.type === 'chunk') {
|
|
542
|
+
res.write(`event: chunk\ndata: ${JSON.stringify({ text: event.data })}\n\n`);
|
|
543
|
+
} else if (event.type === 'done') {
|
|
544
|
+
res.write(`event: done\ndata: ${JSON.stringify(event.data)}\n\n`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
// Pipeline mode: fixed RAG retrieval
|
|
549
|
+
for await (const event of chatTurn({
|
|
550
|
+
query, db, collection, llm, history,
|
|
551
|
+
opts: {
|
|
552
|
+
maxDocs: maxDocs || 5,
|
|
553
|
+
rerank: rerank !== false,
|
|
554
|
+
stream: true,
|
|
555
|
+
systemPrompt,
|
|
556
|
+
},
|
|
557
|
+
})) {
|
|
558
|
+
if (event.type === 'retrieval') {
|
|
559
|
+
res.write(`event: retrieval\ndata: ${JSON.stringify(event.data)}\n\n`);
|
|
560
|
+
} else if (event.type === 'chunk') {
|
|
561
|
+
res.write(`event: chunk\ndata: ${JSON.stringify({ text: event.data })}\n\n`);
|
|
562
|
+
} else if (event.type === 'done') {
|
|
563
|
+
res.write(`event: done\ndata: ${JSON.stringify(event.data)}\n\n`);
|
|
564
|
+
}
|
|
465
565
|
}
|
|
466
566
|
}
|
|
467
567
|
} catch (err) {
|
|
@@ -622,6 +722,120 @@ function createPlaygroundServer() {
|
|
|
622
722
|
}));
|
|
623
723
|
return;
|
|
624
724
|
}
|
|
725
|
+
|
|
726
|
+
// API: Validate a workflow definition
|
|
727
|
+
if (req.url === '/api/workflows/validate') {
|
|
728
|
+
const { validateWorkflow } = require('../lib/workflow');
|
|
729
|
+
const { definition } = parsed;
|
|
730
|
+
if (!definition) {
|
|
731
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
732
|
+
res.end(JSON.stringify({ error: 'definition is required' }));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const errors = validateWorkflow(definition);
|
|
736
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
737
|
+
res.end(JSON.stringify({ valid: errors.length === 0, errors }));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// API: Get execution plan (layers + dependency graph)
|
|
742
|
+
if (req.url === '/api/workflows/plan') {
|
|
743
|
+
const { buildExecutionPlan, buildDependencyGraph, validateWorkflow } = require('../lib/workflow');
|
|
744
|
+
const { definition } = parsed;
|
|
745
|
+
if (!definition || !definition.steps) {
|
|
746
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
747
|
+
res.end(JSON.stringify({ error: 'definition with steps is required' }));
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const errors = validateWorkflow(definition);
|
|
751
|
+
if (errors.length > 0) {
|
|
752
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
753
|
+
res.end(JSON.stringify({ error: 'Invalid workflow', errors }));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
757
|
+
const graphMap = buildDependencyGraph(definition.steps);
|
|
758
|
+
// Convert Map<string, Set<string>> to plain object for JSON serialization
|
|
759
|
+
const graph = {};
|
|
760
|
+
for (const [stepId, deps] of graphMap) {
|
|
761
|
+
graph[stepId] = Array.from(deps);
|
|
762
|
+
}
|
|
763
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
764
|
+
res.end(JSON.stringify({ layers, graph }));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// API: Execute a workflow (streaming SSE)
|
|
769
|
+
if (req.url === '/api/workflows/execute') {
|
|
770
|
+
const { executeWorkflow, validateWorkflow: validateWf } = require('../lib/workflow');
|
|
771
|
+
const { definition, inputs } = parsed;
|
|
772
|
+
if (!definition) {
|
|
773
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
774
|
+
res.end(JSON.stringify({ error: 'definition is required' }));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const errors = validateWf(definition);
|
|
778
|
+
if (errors.length > 0) {
|
|
779
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
780
|
+
res.end(JSON.stringify({ error: 'Invalid workflow', errors }));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Stream execution events as SSE
|
|
785
|
+
res.writeHead(200, {
|
|
786
|
+
'Content-Type': 'text/event-stream',
|
|
787
|
+
'Cache-Control': 'no-cache',
|
|
788
|
+
'Connection': 'keep-alive',
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
const result = await executeWorkflow(definition, {
|
|
793
|
+
inputs: inputs || {},
|
|
794
|
+
onStepStart: (stepId, stepDef) => {
|
|
795
|
+
res.write(`event: step_start\ndata: ${JSON.stringify({
|
|
796
|
+
stepId,
|
|
797
|
+
name: stepDef.name || stepId,
|
|
798
|
+
tool: stepDef.tool,
|
|
799
|
+
})}\n\n`);
|
|
800
|
+
},
|
|
801
|
+
onStepComplete: (stepId, output, timeMs) => {
|
|
802
|
+
// Summarize output to avoid huge payloads
|
|
803
|
+
let summary = '';
|
|
804
|
+
if (output && typeof output === 'object') {
|
|
805
|
+
if (output.results) summary = `${output.results.length} results`;
|
|
806
|
+
else if (output.similarity !== undefined) summary = `similarity: ${Number(output.similarity).toFixed(4)}`;
|
|
807
|
+
else if (output.text) summary = output.text.slice(0, 100) + (output.text.length > 100 ? '...' : '');
|
|
808
|
+
else summary = JSON.stringify(output).slice(0, 200);
|
|
809
|
+
}
|
|
810
|
+
res.write(`event: step_complete\ndata: ${JSON.stringify({
|
|
811
|
+
stepId, timeMs, summary,
|
|
812
|
+
output: JSON.stringify(output).length < 5000 ? output : { _truncated: true, summary },
|
|
813
|
+
})}\n\n`);
|
|
814
|
+
},
|
|
815
|
+
onStepSkip: (stepId, reason) => {
|
|
816
|
+
res.write(`event: step_skip\ndata: ${JSON.stringify({ stepId, reason })}\n\n`);
|
|
817
|
+
},
|
|
818
|
+
onStepError: (stepId, error) => {
|
|
819
|
+
res.write(`event: step_error\ndata: ${JSON.stringify({
|
|
820
|
+
stepId,
|
|
821
|
+
error: error.message || String(error),
|
|
822
|
+
})}\n\n`);
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
res.write(`event: done\ndata: ${JSON.stringify({
|
|
827
|
+
output: result.output,
|
|
828
|
+
totalTimeMs: result.totalTimeMs,
|
|
829
|
+
layers: result.layers,
|
|
830
|
+
steps: result.steps,
|
|
831
|
+
})}\n\n`);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
res.end();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
625
839
|
}
|
|
626
840
|
|
|
627
841
|
// 404
|