voyageai-cli 1.24.0 → 1.26.1
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/cli.js +2 -0
- package/src/commands/about.js +1 -1
- package/src/commands/bug.js +1 -1
- package/src/commands/chat.js +281 -78
- package/src/commands/playground.js +73 -19
- package/src/commands/scaffold.js +23 -1
- package/src/commands/workflow.js +336 -0
- package/src/lib/chat.js +170 -4
- package/src/lib/explanations.js +53 -0
- package/src/lib/llm.js +304 -2
- package/src/lib/mongo.js +6 -6
- package/src/lib/prompt.js +60 -1
- package/src/lib/scaffold-structure.js +8 -9
- package/src/lib/telemetry.js +1 -1
- package/src/lib/template-engine.js +240 -0
- package/src/lib/templates/nextjs/README.md.tpl +78 -55
- package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
- package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
- package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
- package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
- package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
- package/src/lib/templates/nextjs/theme.js.tpl +138 -65
- package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
- package/src/lib/tool-registry.js +194 -0
- package/src/lib/workflow-utils.js +65 -0
- package/src/lib/workflow.js +1259 -0
- package/src/mcp/tools/embedding.js +55 -43
- package/src/mcp/tools/ingest.js +74 -67
- package/src/mcp/tools/management.js +54 -101
- 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/icons/watermark.png +0 -0
- package/src/playground/index.html +633 -83
- package/src/workflows/consistency-check.json +64 -0
- package/src/workflows/cost-analysis.json +69 -0
- package/src/workflows/multi-collection-search.json +80 -0
- package/src/workflows/research-and-summarize.json +46 -0
- package/src/workflows/smart-ingest.json +63 -0
package/src/commands/scaffold.js
CHANGED
|
@@ -129,6 +129,23 @@ function registerScaffold(program) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
// Add binary files (copied as-is from templates dir)
|
|
134
|
+
if (structure.binaryFiles) {
|
|
135
|
+
for (const file of structure.binaryFiles) {
|
|
136
|
+
const srcPath = path.join(__dirname, '..', 'lib', 'templates', target, file.source);
|
|
137
|
+
const destPath = path.join(projectDir, file.output);
|
|
138
|
+
const binarySize = fs.existsSync(srcPath) ? fs.statSync(srcPath).size : 0;
|
|
139
|
+
manifest.push({
|
|
140
|
+
path: file.output,
|
|
141
|
+
fullPath: destPath,
|
|
142
|
+
source: `${target}/${file.source}`,
|
|
143
|
+
size: binarySize,
|
|
144
|
+
binary: true,
|
|
145
|
+
binarySrc: srcPath,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
132
149
|
|
|
133
150
|
// JSON output mode
|
|
134
151
|
if (opts.json) {
|
|
@@ -171,7 +188,12 @@ function registerScaffold(program) {
|
|
|
171
188
|
|
|
172
189
|
// Write all files
|
|
173
190
|
for (const file of manifest) {
|
|
174
|
-
|
|
191
|
+
if (file.binary) {
|
|
192
|
+
ensureDir(path.dirname(file.fullPath));
|
|
193
|
+
fs.copyFileSync(file.binarySrc, file.fullPath);
|
|
194
|
+
} else {
|
|
195
|
+
writeFile(file.fullPath, file.content);
|
|
196
|
+
}
|
|
175
197
|
if (!opts.quiet) {
|
|
176
198
|
console.log(` ${ui.cyan('✓')} ${file.path}`);
|
|
177
199
|
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
const ui = require('../lib/ui');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse repeatable --input key=value options into an object.
|
|
8
|
+
* Used as Commander's option reducer.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} pair - "key=value" string
|
|
11
|
+
* @param {object} prev - Accumulated object
|
|
12
|
+
* @returns {object}
|
|
13
|
+
*/
|
|
14
|
+
function collectInputs(pair, prev) {
|
|
15
|
+
const eq = pair.indexOf('=');
|
|
16
|
+
if (eq === -1) {
|
|
17
|
+
throw new Error(`Invalid input format: "${pair}". Expected key=value`);
|
|
18
|
+
}
|
|
19
|
+
const key = pair.slice(0, eq).trim();
|
|
20
|
+
const value = pair.slice(eq + 1).trim();
|
|
21
|
+
prev[key] = value;
|
|
22
|
+
return prev;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register the workflow command on a Commander program.
|
|
27
|
+
* @param {import('commander').Command} program
|
|
28
|
+
*/
|
|
29
|
+
function registerWorkflow(program) {
|
|
30
|
+
const wfCmd = program
|
|
31
|
+
.command('workflow')
|
|
32
|
+
.alias('wf')
|
|
33
|
+
.description('Run, validate, and manage composable RAG workflows');
|
|
34
|
+
|
|
35
|
+
// ── workflow run <file> ──
|
|
36
|
+
wfCmd
|
|
37
|
+
.command('run <file>')
|
|
38
|
+
.description('Execute a workflow file or built-in template')
|
|
39
|
+
.option('--input <key=value>', 'Set a workflow input (repeatable)', collectInputs, {})
|
|
40
|
+
.option('--db <name>', 'Override default database')
|
|
41
|
+
.option('--collection <name>', 'Override default collection')
|
|
42
|
+
.option('--json', 'Output results as JSON', false)
|
|
43
|
+
.option('--quiet', 'Suppress progress output', false)
|
|
44
|
+
.option('--dry-run', 'Show execution plan without running', false)
|
|
45
|
+
.option('--verbose', 'Show step details', false)
|
|
46
|
+
.action(async (file, opts) => {
|
|
47
|
+
const { loadWorkflow, executeWorkflow, buildExecutionPlan, validateWorkflow } = require('../lib/workflow');
|
|
48
|
+
|
|
49
|
+
let definition;
|
|
50
|
+
try {
|
|
51
|
+
definition = loadWorkflow(file);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(ui.error(err.message));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validation
|
|
58
|
+
const errors = validateWorkflow(definition);
|
|
59
|
+
if (errors.length > 0) {
|
|
60
|
+
console.error(ui.error('Workflow validation failed:'));
|
|
61
|
+
for (const e of errors) console.error(` ${pc.red('-')} ${e}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const workflowName = definition.name || file;
|
|
66
|
+
|
|
67
|
+
if (opts.dryRun) {
|
|
68
|
+
// Dry run: show plan
|
|
69
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
70
|
+
const stepMap = new Map(definition.steps.map(s => [s.id, s]));
|
|
71
|
+
|
|
72
|
+
if (opts.json) {
|
|
73
|
+
console.log(JSON.stringify({ name: workflowName, layers, steps: definition.steps, inputs: opts.input }, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(`${pc.bold('vai workflow:')} ${workflowName} ${pc.dim('(dry run)')}`);
|
|
79
|
+
console.log(pc.dim('═'.repeat(50)));
|
|
80
|
+
console.log();
|
|
81
|
+
|
|
82
|
+
// Show inputs
|
|
83
|
+
const inputKeys = Object.keys(opts.input);
|
|
84
|
+
if (inputKeys.length > 0) {
|
|
85
|
+
console.log(pc.bold('Inputs:'));
|
|
86
|
+
for (const [k, v] of Object.entries(opts.input)) {
|
|
87
|
+
console.log(` ${pc.cyan(k)}: ${JSON.stringify(v)}`);
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Show execution plan
|
|
93
|
+
console.log(pc.bold('Execution plan:'));
|
|
94
|
+
let stepNum = 1;
|
|
95
|
+
for (let i = 0; i < layers.length; i++) {
|
|
96
|
+
const layer = layers[i];
|
|
97
|
+
for (const stepId of layer) {
|
|
98
|
+
const step = stepMap.get(stepId);
|
|
99
|
+
const toolDesc = `${step.tool}(${summarizeInputs(step.inputs)})`;
|
|
100
|
+
console.log(` ${pc.dim(`${stepNum}.`)} ${pc.cyan(stepId)} ${pc.dim('->')} ${toolDesc}`);
|
|
101
|
+
stepNum++;
|
|
102
|
+
}
|
|
103
|
+
if (layer.length > 1) {
|
|
104
|
+
console.log(` ${pc.dim(`(${layer.join(' and ')} run in parallel)`)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Execute workflow
|
|
112
|
+
try {
|
|
113
|
+
const result = await executeWorkflow(definition, {
|
|
114
|
+
inputs: opts.input,
|
|
115
|
+
db: opts.db,
|
|
116
|
+
collection: opts.collection,
|
|
117
|
+
dryRun: false,
|
|
118
|
+
verbose: opts.verbose,
|
|
119
|
+
json: opts.json,
|
|
120
|
+
onStepStart: !opts.quiet ? (stepId, step) => {
|
|
121
|
+
process.stderr.write(` ${pc.dim('...')} ${step.name || stepId}\r`);
|
|
122
|
+
} : undefined,
|
|
123
|
+
onStepComplete: !opts.quiet ? (stepId, output, durationMs) => {
|
|
124
|
+
const stepDef = definition.steps.find(s => s.id === stepId);
|
|
125
|
+
const summary = summarizeOutput(stepDef?.tool, output);
|
|
126
|
+
console.error(` ${pc.green('✔')} ${stepDef?.name || stepId} ${pc.dim(summary)} ${pc.dim(`[${durationMs}ms]`)}`);
|
|
127
|
+
} : undefined,
|
|
128
|
+
onStepSkip: !opts.quiet ? (stepId, reason) => {
|
|
129
|
+
const stepDef = definition.steps.find(s => s.id === stepId);
|
|
130
|
+
console.error(` ${pc.yellow('⊘')} ${stepDef?.name || stepId} ${pc.dim(`skipped: ${reason}`)}`);
|
|
131
|
+
} : undefined,
|
|
132
|
+
onStepError: !opts.quiet ? (stepId, err) => {
|
|
133
|
+
const stepDef = definition.steps.find(s => s.id === stepId);
|
|
134
|
+
console.error(` ${pc.red('✗')} ${stepDef?.name || stepId} ${pc.red(err.message)}`);
|
|
135
|
+
} : undefined,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!opts.quiet) {
|
|
139
|
+
console.error();
|
|
140
|
+
console.error(`${pc.bold('vai workflow:')} ${workflowName}`);
|
|
141
|
+
console.error(pc.dim('═'.repeat(50)));
|
|
142
|
+
// Step summaries already printed via callbacks
|
|
143
|
+
console.error();
|
|
144
|
+
console.error(`${pc.dim('Complete.')} ${result.steps.length} steps, ${result.totalTimeMs}ms total.`);
|
|
145
|
+
console.error();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Output
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
console.log(JSON.stringify(result.output, null, 2));
|
|
151
|
+
} else if (result.output) {
|
|
152
|
+
// Pretty-print top results if they exist
|
|
153
|
+
const output = result.output;
|
|
154
|
+
if (output.results && Array.isArray(output.results)) {
|
|
155
|
+
const top = output.results.slice(0, 5);
|
|
156
|
+
console.log(pc.bold('Top results:'));
|
|
157
|
+
for (let i = 0; i < top.length; i++) {
|
|
158
|
+
const r = top[i];
|
|
159
|
+
const source = r.source || r.text?.slice(0, 50) || `result ${i + 1}`;
|
|
160
|
+
const score = r.score != null ? ` (${r.score.toFixed(2)})` : '';
|
|
161
|
+
console.log(` ${pc.dim(`[${i + 1}]`)} ${source}${pc.dim(score)}`);
|
|
162
|
+
}
|
|
163
|
+
} else if (output.summary) {
|
|
164
|
+
console.log(output.summary);
|
|
165
|
+
} else if (output.comparison) {
|
|
166
|
+
console.log(pc.bold('Cost comparison:'));
|
|
167
|
+
for (const item of output.comparison) {
|
|
168
|
+
if (item && item.model) {
|
|
169
|
+
console.log(` ${pc.cyan(item.model)}: $${item.totalCost} total (embed: $${item.embeddingCost}, queries: $${item.monthlyQueryCost}/mo)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
console.log(JSON.stringify(output, null, 2));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(ui.error(err.message));
|
|
178
|
+
if (opts.verbose) {
|
|
179
|
+
console.error(pc.dim(err.stack));
|
|
180
|
+
}
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── workflow validate <file> ──
|
|
186
|
+
wfCmd
|
|
187
|
+
.command('validate <file>')
|
|
188
|
+
.description('Validate a workflow file without executing')
|
|
189
|
+
.action((file) => {
|
|
190
|
+
const { loadWorkflow, validateWorkflow, buildExecutionPlan } = require('../lib/workflow');
|
|
191
|
+
|
|
192
|
+
let definition;
|
|
193
|
+
try {
|
|
194
|
+
definition = loadWorkflow(file);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error(ui.error(err.message));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const errors = validateWorkflow(definition);
|
|
201
|
+
if (errors.length > 0) {
|
|
202
|
+
console.error(ui.error(`Workflow has ${errors.length} error(s):`));
|
|
203
|
+
for (const e of errors) console.error(` ${pc.red('-')} ${e}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
208
|
+
console.log(ui.success(`${definition.name} is valid`));
|
|
209
|
+
console.log(` ${pc.dim('Steps:')} ${definition.steps.length}`);
|
|
210
|
+
console.log(` ${pc.dim('Layers:')} ${layers.length} (${layers.map(l => l.length).join(' + ')} steps)`);
|
|
211
|
+
|
|
212
|
+
if (definition.inputs) {
|
|
213
|
+
const required = Object.entries(definition.inputs).filter(([, s]) => s.required).map(([k]) => k);
|
|
214
|
+
if (required.length > 0) {
|
|
215
|
+
console.log(` ${pc.dim('Required inputs:')} ${required.join(', ')}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ── workflow list ──
|
|
221
|
+
wfCmd
|
|
222
|
+
.command('list')
|
|
223
|
+
.description('List built-in workflow templates')
|
|
224
|
+
.action(() => {
|
|
225
|
+
const { listBuiltinWorkflows } = require('../lib/workflow');
|
|
226
|
+
|
|
227
|
+
const workflows = listBuiltinWorkflows();
|
|
228
|
+
if (workflows.length === 0) {
|
|
229
|
+
console.log(pc.dim('No built-in workflows found.'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(pc.bold('Built-in workflow templates:'));
|
|
235
|
+
console.log();
|
|
236
|
+
for (const wf of workflows) {
|
|
237
|
+
console.log(` ${pc.cyan(wf.name.padEnd(28))} ${wf.description}`);
|
|
238
|
+
}
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(pc.dim('Run with: vai workflow run <name> --input key=value'));
|
|
241
|
+
console.log();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── workflow init ──
|
|
245
|
+
wfCmd
|
|
246
|
+
.command('init')
|
|
247
|
+
.description('Scaffold a new workflow file')
|
|
248
|
+
.option('--name <name>', 'Workflow name')
|
|
249
|
+
.option('--output <file>', 'Output file path')
|
|
250
|
+
.action((opts) => {
|
|
251
|
+
const fs = require('fs');
|
|
252
|
+
const name = opts.name || 'my-workflow';
|
|
253
|
+
const filename = opts.output || `./${name}.vai-workflow.json`;
|
|
254
|
+
|
|
255
|
+
const scaffold = {
|
|
256
|
+
$schema: 'https://vai.dev/schemas/workflow-v1.json',
|
|
257
|
+
name: name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
258
|
+
description: 'TODO: describe what this workflow does',
|
|
259
|
+
version: '1.0.0',
|
|
260
|
+
inputs: {
|
|
261
|
+
query: {
|
|
262
|
+
type: 'string',
|
|
263
|
+
description: 'The search query',
|
|
264
|
+
required: true,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
defaults: {},
|
|
268
|
+
steps: [
|
|
269
|
+
{
|
|
270
|
+
id: 'search',
|
|
271
|
+
name: 'Search knowledge base',
|
|
272
|
+
tool: 'query',
|
|
273
|
+
inputs: {
|
|
274
|
+
query: '{{ inputs.query }}',
|
|
275
|
+
limit: 10,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
output: {
|
|
280
|
+
results: '{{ search.output.results }}',
|
|
281
|
+
query: '{{ inputs.query }}',
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
fs.writeFileSync(filename, JSON.stringify(scaffold, null, 2) + '\n');
|
|
286
|
+
console.log(ui.success(`Created ${filename}`));
|
|
287
|
+
console.log(` ${pc.dim('Run with:')} vai workflow run ${filename} --input query="your question"`);
|
|
288
|
+
console.log(` ${pc.dim('Validate:')} vai workflow validate ${filename}`);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Summarize step inputs for dry-run display.
|
|
294
|
+
*/
|
|
295
|
+
function summarizeInputs(inputs) {
|
|
296
|
+
if (!inputs) return '';
|
|
297
|
+
const parts = [];
|
|
298
|
+
for (const [k, v] of Object.entries(inputs)) {
|
|
299
|
+
if (typeof v === 'string' && v.includes('{{')) {
|
|
300
|
+
parts.push(`${k}=<ref>`);
|
|
301
|
+
} else if (typeof v === 'string') {
|
|
302
|
+
parts.push(`${k}=${v.length > 20 ? v.slice(0, 20) + '...' : v}`);
|
|
303
|
+
} else if (typeof v === 'number' || typeof v === 'boolean') {
|
|
304
|
+
parts.push(`${k}=${v}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return parts.join(', ');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Summarize step output for display.
|
|
312
|
+
*/
|
|
313
|
+
function summarizeOutput(tool, output) {
|
|
314
|
+
if (!output) return '';
|
|
315
|
+
if (output.results && output.resultCount != null) {
|
|
316
|
+
return `${output.resultCount} results`;
|
|
317
|
+
}
|
|
318
|
+
if (output.insertedCount != null) {
|
|
319
|
+
return `${output.insertedCount} docs inserted`;
|
|
320
|
+
}
|
|
321
|
+
if (output.similarity != null) {
|
|
322
|
+
return `similarity: ${output.similarity.toFixed(4)}`;
|
|
323
|
+
}
|
|
324
|
+
if (output.text) {
|
|
325
|
+
return `${output.text.length} chars`;
|
|
326
|
+
}
|
|
327
|
+
if (output.embedding) {
|
|
328
|
+
return `${output.dimensions}d embedding`;
|
|
329
|
+
}
|
|
330
|
+
if (output.totalCost != null) {
|
|
331
|
+
return `$${output.totalCost}`;
|
|
332
|
+
}
|
|
333
|
+
return '';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = { registerWorkflow, collectInputs };
|
package/src/lib/chat.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Chat Orchestrator
|
|
5
5
|
*
|
|
6
|
-
* Coordinates the retrieval pipeline (embed
|
|
6
|
+
* Coordinates the retrieval pipeline (embed -> search -> rerank)
|
|
7
7
|
* with LLM generation and history management.
|
|
8
|
+
* Supports both pipeline mode (fixed RAG) and agent mode (tool-calling).
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const { generateEmbeddings, apiRequest } = require('./api');
|
|
@@ -32,12 +33,12 @@ function resolveSourceLabel(doc) {
|
|
|
32
33
|
return doc.source || meta.source || doc._id?.toString() || 'unknown';
|
|
33
34
|
}
|
|
34
35
|
const { getMongoCollection } = require('./mongo');
|
|
35
|
-
const { buildMessages } = require('./prompt');
|
|
36
|
+
const { buildMessages, buildAgentMessages } = require('./prompt');
|
|
36
37
|
const { getDefaultModel, DEFAULT_RERANK_MODEL } = require('./catalog');
|
|
37
38
|
const { loadProject } = require('./project');
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* Perform retrieval: embed query
|
|
41
|
+
* Perform retrieval: embed query -> vector search -> optional rerank.
|
|
41
42
|
*
|
|
42
43
|
* @param {object} params
|
|
43
44
|
* @param {string} params.query - User's question
|
|
@@ -154,7 +155,7 @@ async function retrieve({ query, db, collection, opts = {} }) {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/**
|
|
157
|
-
* Execute a single chat turn: retrieve context
|
|
158
|
+
* Execute a single chat turn: retrieve context -> build prompt -> generate response.
|
|
158
159
|
*
|
|
159
160
|
* @param {object} params
|
|
160
161
|
* @param {string} params.query - User's question
|
|
@@ -246,7 +247,172 @@ async function* chatTurn({ query, db, collection, llm, history, opts = {} }) {
|
|
|
246
247
|
};
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Execute a single agent chat turn: LLM decides which tools to call.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} params
|
|
254
|
+
* @param {string} params.query - User's question
|
|
255
|
+
* @param {object} params.llm - LLM provider instance (must have chatWithTools)
|
|
256
|
+
* @param {import('./history').ChatHistory} params.history - Chat history
|
|
257
|
+
* @param {object} [params.opts] - Additional options
|
|
258
|
+
* @param {string} [params.opts.systemPrompt] - Override agent system prompt
|
|
259
|
+
* @param {number} [params.opts.maxIterations] - Max tool-calling iterations (default 10)
|
|
260
|
+
* @param {string} [params.opts.db] - Default database for tool calls
|
|
261
|
+
* @param {string} [params.opts.collection] - Default collection for tool calls
|
|
262
|
+
* @returns {AsyncGenerator<{type: string, data: any}>}
|
|
263
|
+
* Yields: { type: 'tool_call', data: { name, args, result, error, timeMs } }
|
|
264
|
+
* { type: 'chunk', data: string }
|
|
265
|
+
* { type: 'done', data: { fullResponse, toolCalls, metadata } }
|
|
266
|
+
*/
|
|
267
|
+
async function* agentChatTurn({ query, llm, history, opts = {} }) {
|
|
268
|
+
const { getToolDefinitions, executeTool } = require('./tool-registry');
|
|
269
|
+
|
|
270
|
+
const maxIterations = opts.maxIterations || 10;
|
|
271
|
+
const start = Date.now();
|
|
272
|
+
|
|
273
|
+
// 1. Build initial messages
|
|
274
|
+
const initialMessages = buildAgentMessages({
|
|
275
|
+
query,
|
|
276
|
+
history: history.getMessagesWithBudget(8000),
|
|
277
|
+
systemPrompt: opts.systemPrompt,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// 2. Get tool definitions for this provider
|
|
281
|
+
const format = llm.name === 'anthropic' ? 'anthropic' : 'openai';
|
|
282
|
+
const tools = getToolDefinitions(format);
|
|
283
|
+
|
|
284
|
+
// Track messages for the tool-calling loop (mutable copy)
|
|
285
|
+
const messages = [...initialMessages];
|
|
286
|
+
const toolCallLog = [];
|
|
287
|
+
|
|
288
|
+
// 3. Agent loop
|
|
289
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
290
|
+
const response = await llm.chatWithTools(messages, tools);
|
|
291
|
+
|
|
292
|
+
// Text response: done
|
|
293
|
+
if (response.type === 'text') {
|
|
294
|
+
const fullResponse = response.content;
|
|
295
|
+
yield { type: 'chunk', data: fullResponse };
|
|
296
|
+
|
|
297
|
+
const totalTimeMs = Date.now() - start;
|
|
298
|
+
|
|
299
|
+
// Store turns in history
|
|
300
|
+
await history.addTurn({ role: 'user', content: query });
|
|
301
|
+
await history.addTurn({
|
|
302
|
+
role: 'assistant',
|
|
303
|
+
content: fullResponse,
|
|
304
|
+
metadata: {
|
|
305
|
+
mode: 'agent',
|
|
306
|
+
llmProvider: llm.name,
|
|
307
|
+
llmModel: llm.model,
|
|
308
|
+
toolCallCount: toolCallLog.length,
|
|
309
|
+
iterationCount: iteration + 1,
|
|
310
|
+
totalTimeMs,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
yield {
|
|
315
|
+
type: 'done',
|
|
316
|
+
data: {
|
|
317
|
+
fullResponse,
|
|
318
|
+
toolCalls: toolCallLog,
|
|
319
|
+
metadata: {
|
|
320
|
+
mode: 'agent',
|
|
321
|
+
iterationCount: iteration + 1,
|
|
322
|
+
toolCallCount: toolCallLog.length,
|
|
323
|
+
totalTimeMs,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Tool calls: execute each and continue loop
|
|
331
|
+
if (response.type === 'tool_calls') {
|
|
332
|
+
// Append assistant tool-call message
|
|
333
|
+
messages.push(llm.formatAssistantToolCall(response));
|
|
334
|
+
|
|
335
|
+
for (const call of response.calls) {
|
|
336
|
+
const callStart = Date.now();
|
|
337
|
+
let result;
|
|
338
|
+
let error = null;
|
|
339
|
+
|
|
340
|
+
// Inject default db/collection if not provided
|
|
341
|
+
const args = { ...call.arguments };
|
|
342
|
+
if (opts.db && !args.db) args.db = opts.db;
|
|
343
|
+
if (opts.collection && !args.collection) args.collection = opts.collection;
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
result = await executeTool(call.name, args);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
error = err.message;
|
|
349
|
+
result = { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const callTimeMs = Date.now() - callStart;
|
|
353
|
+
|
|
354
|
+
// Extract text content from result for the LLM
|
|
355
|
+
const resultText = result.content
|
|
356
|
+
? result.content.map(c => c.text || JSON.stringify(c)).join('\n')
|
|
357
|
+
: JSON.stringify(result.structuredContent || {});
|
|
358
|
+
|
|
359
|
+
// Append tool result message
|
|
360
|
+
messages.push(llm.formatToolResult(call.id, resultText, !!error));
|
|
361
|
+
|
|
362
|
+
const logEntry = {
|
|
363
|
+
name: call.name,
|
|
364
|
+
args,
|
|
365
|
+
result: result.structuredContent || null,
|
|
366
|
+
error,
|
|
367
|
+
timeMs: callTimeMs,
|
|
368
|
+
};
|
|
369
|
+
toolCallLog.push(logEntry);
|
|
370
|
+
|
|
371
|
+
yield { type: 'tool_call', data: logEntry };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Continue loop to let LLM see results and decide next action
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Max iterations reached: yield a fallback message
|
|
380
|
+
const fallback = 'I reached the maximum number of tool-calling iterations. Here is what I found so far based on the tool results above.';
|
|
381
|
+
yield { type: 'chunk', data: fallback };
|
|
382
|
+
|
|
383
|
+
await history.addTurn({ role: 'user', content: query });
|
|
384
|
+
await history.addTurn({
|
|
385
|
+
role: 'assistant',
|
|
386
|
+
content: fallback,
|
|
387
|
+
metadata: {
|
|
388
|
+
mode: 'agent',
|
|
389
|
+
llmProvider: llm.name,
|
|
390
|
+
llmModel: llm.model,
|
|
391
|
+
toolCallCount: toolCallLog.length,
|
|
392
|
+
iterationCount: maxIterations,
|
|
393
|
+
totalTimeMs: Date.now() - start,
|
|
394
|
+
maxIterationsReached: true,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
yield {
|
|
399
|
+
type: 'done',
|
|
400
|
+
data: {
|
|
401
|
+
fullResponse: fallback,
|
|
402
|
+
toolCalls: toolCallLog,
|
|
403
|
+
metadata: {
|
|
404
|
+
mode: 'agent',
|
|
405
|
+
iterationCount: maxIterations,
|
|
406
|
+
toolCallCount: toolCallLog.length,
|
|
407
|
+
totalTimeMs: Date.now() - start,
|
|
408
|
+
maxIterationsReached: true,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
249
414
|
module.exports = {
|
|
250
415
|
retrieve,
|
|
251
416
|
chatTurn,
|
|
417
|
+
agentChatTurn,
|
|
252
418
|
};
|
package/src/lib/explanations.js
CHANGED
|
@@ -1434,6 +1434,50 @@ const concepts = {
|
|
|
1434
1434
|
'vai chat --db myapp --collection knowledge',
|
|
1435
1435
|
],
|
|
1436
1436
|
},
|
|
1437
|
+
|
|
1438
|
+
workflows: {
|
|
1439
|
+
title: 'Agentic Workflows',
|
|
1440
|
+
summary: 'Composable, multi-step RAG pipelines as JSON files',
|
|
1441
|
+
content: [
|
|
1442
|
+
`${pc.cyan('Workflows')} are composable, multi-step RAG pipelines defined as portable`,
|
|
1443
|
+
`JSON files. Think Docker Compose or GitHub Actions, but for search and retrieval`,
|
|
1444
|
+
`pipelines.`,
|
|
1445
|
+
``,
|
|
1446
|
+
`${pc.bold('Why workflows?')} Instead of writing bash scripts to chain vai commands,`,
|
|
1447
|
+
`a workflow file captures the intent declaratively. Workflows are reproducible,`,
|
|
1448
|
+
`shareable (commit them to git), and inspectable.`,
|
|
1449
|
+
``,
|
|
1450
|
+
`${pc.bold('How it works:')}`,
|
|
1451
|
+
`Each workflow defines a DAG (directed acyclic graph) of steps. Each step maps`,
|
|
1452
|
+
`to a vai operation (query, search, rerank, embed, etc.) or a control flow`,
|
|
1453
|
+
`operation (merge, filter, transform, generate). Steps reference outputs from`,
|
|
1454
|
+
`previous steps using ${pc.cyan('{{ template expressions }}')} for data flow.`,
|
|
1455
|
+
``,
|
|
1456
|
+
`${pc.bold('Template expressions:')}`,
|
|
1457
|
+
` ${pc.cyan('{{ inputs.query }}')} workflow input parameter`,
|
|
1458
|
+
` ${pc.cyan('{{ search.output.results }}')} results from a previous step`,
|
|
1459
|
+
` ${pc.cyan('{{ merge.output.results[0] }}')} array indexing`,
|
|
1460
|
+
` ${pc.cyan('{{ defaults.db }}')} workflow default value`,
|
|
1461
|
+
``,
|
|
1462
|
+
`${pc.bold('Parallel execution:')} The engine automatically detects independent steps`,
|
|
1463
|
+
`and runs them in parallel. No configuration needed.`,
|
|
1464
|
+
``,
|
|
1465
|
+
`${pc.bold('Step types:')}`,
|
|
1466
|
+
` ${pc.dim('VAI tools:')} query, search, rerank, embed, similarity, ingest,`,
|
|
1467
|
+
` collections, models, explain, estimate`,
|
|
1468
|
+
` ${pc.dim('Control flow:')} merge, filter, transform, generate`,
|
|
1469
|
+
``,
|
|
1470
|
+
`${pc.bold('Built-in templates:')} Run ${pc.cyan('vai workflow list')} to see available`,
|
|
1471
|
+
`templates like multi-collection-search, smart-ingest, research-and-summarize.`,
|
|
1472
|
+
].join('\n'),
|
|
1473
|
+
links: ['https://github.com/mrlynn/voyageai-cli#workflows'],
|
|
1474
|
+
tryIt: [
|
|
1475
|
+
'vai workflow list',
|
|
1476
|
+
'vai workflow init --name my-pipeline',
|
|
1477
|
+
'vai workflow validate ./my-pipeline.vai-workflow.json',
|
|
1478
|
+
'vai workflow run multi-collection-search --input query="test" --dry-run',
|
|
1479
|
+
],
|
|
1480
|
+
},
|
|
1437
1481
|
};
|
|
1438
1482
|
|
|
1439
1483
|
/**
|
|
@@ -1598,6 +1642,15 @@ const aliases = {
|
|
|
1598
1642
|
conversational: 'chat',
|
|
1599
1643
|
'chat-history': 'chat',
|
|
1600
1644
|
llm: 'chat',
|
|
1645
|
+
// Workflow aliases
|
|
1646
|
+
workflow: 'workflows',
|
|
1647
|
+
workflows: 'workflows',
|
|
1648
|
+
'rag-pipeline': 'workflows',
|
|
1649
|
+
composable: 'workflows',
|
|
1650
|
+
'workflow-engine': 'workflows',
|
|
1651
|
+
'vai-workflow': 'workflows',
|
|
1652
|
+
pipeline: 'workflows',
|
|
1653
|
+
dag: 'workflows',
|
|
1601
1654
|
};
|
|
1602
1655
|
|
|
1603
1656
|
/**
|