voyageai-cli 1.30.1 → 1.30.3
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 +4 -4
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +3 -3
- package/src/commands/code-search.js +751 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/embed.js +121 -2
- package/src/commands/index-workspace.js +9 -5
- package/src/commands/playground.js +65 -4
- package/src/commands/quickstart.js +4 -4
- package/src/commands/workflow.js +132 -65
- package/src/lib/api.js +31 -0
- package/src/lib/catalog.js +4 -2
- package/src/lib/code-search.js +315 -0
- package/src/lib/codegen.js +1 -1
- package/src/lib/explanations.js +3 -3
- package/src/lib/github.js +226 -0
- package/src/lib/input.js +92 -1
- package/src/lib/template-engine.js +154 -20
- package/src/lib/workflow-builder.js +753 -0
- package/src/lib/workflow-formatters.js +454 -0
- package/src/lib/workflow-input-cache.js +111 -0
- package/src/lib/workflow-scaffold.js +1 -1
- package/src/lib/workflow.js +124 -8
- package/src/mcp/schemas/index.js +142 -0
- package/src/mcp/server.js +17 -4
- package/src/mcp/tools/authoring.js +662 -0
- package/src/mcp/tools/code-search.js +620 -0
- package/src/mcp/tools/embedding.js +72 -3
- package/src/mcp/tools/ingest.js +2 -5
- package/src/mcp/tools/retrieval.js +2 -15
- package/src/mcp/tools/workspace.js +1 -12
- package/src/mcp/utils.js +20 -0
- package/src/playground/help/workflow-nodes.js +127 -2
- package/src/playground/index.html +2013 -139
- package/src/workflows/code-review.json +110 -0
- package/src/workflows/cost-analysis.json +5 -0
- package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
- package/src/workflows/tests/code-review.happy-path.test.json +121 -0
- package/src/workflows/tests/code-review.no-question.test.json +70 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +2 -2
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
const { ALL_TOOLS } = require('./workflow');
|
|
5
|
+
const { CATEGORIES } = require('./workflow-scaffold');
|
|
6
|
+
|
|
7
|
+
// Reserved words that cannot be used as step IDs
|
|
8
|
+
const RESERVED_STEP_IDS = new Set([
|
|
9
|
+
'index', 'inputs', 'defaults', 'item', 'true', 'false', 'null', 'undefined',
|
|
10
|
+
'output', 'input', 'step', 'steps',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate a step ID.
|
|
15
|
+
* @param {string} id
|
|
16
|
+
* @param {Set<string>} existingIds
|
|
17
|
+
* @returns {string|undefined} error message or undefined if valid
|
|
18
|
+
*/
|
|
19
|
+
function validateStepId(id, existingIds) {
|
|
20
|
+
if (!id) return 'Step ID is required';
|
|
21
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(id)) return 'Step ID must start with a letter or underscore and contain only alphanumeric characters and underscores';
|
|
22
|
+
if (RESERVED_STEP_IDS.has(id)) return `"${id}" is a reserved keyword and cannot be used as a step ID`;
|
|
23
|
+
if (existingIds.has(id)) return `Step ID "${id}" is already used`;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build tool options for selection, sorted alphabetically.
|
|
29
|
+
* @returns {Array<{value: string, label: string, hint?: string}>}
|
|
30
|
+
*/
|
|
31
|
+
function buildToolOptions() {
|
|
32
|
+
const toolHints = {
|
|
33
|
+
query: 'Vector search with Voyage AI embeddings',
|
|
34
|
+
search: 'Full-text or hybrid search',
|
|
35
|
+
rerank: 'Rerank documents by relevance',
|
|
36
|
+
embed: 'Generate embeddings for text',
|
|
37
|
+
similarity: 'Compute similarity between texts',
|
|
38
|
+
ingest: 'Ingest documents into a collection',
|
|
39
|
+
collections: 'List or manage collections',
|
|
40
|
+
models: 'List available models',
|
|
41
|
+
explain: 'Explain an embedding or result',
|
|
42
|
+
estimate: 'Estimate embedding cost',
|
|
43
|
+
code_index: 'Index a codebase',
|
|
44
|
+
code_search: 'Search indexed code',
|
|
45
|
+
code_query: 'Query code with natural language',
|
|
46
|
+
code_find_similar: 'Find similar code snippets',
|
|
47
|
+
code_status: 'Check code index status',
|
|
48
|
+
merge: 'Merge multiple arrays',
|
|
49
|
+
filter: 'Filter results by condition',
|
|
50
|
+
transform: 'Transform data with expressions',
|
|
51
|
+
generate: 'Generate text with an LLM',
|
|
52
|
+
conditional: 'Branch based on a condition',
|
|
53
|
+
loop: 'Loop over items',
|
|
54
|
+
template: 'Render a template string',
|
|
55
|
+
chunk: 'Split text into chunks',
|
|
56
|
+
aggregate: 'Aggregate values',
|
|
57
|
+
http: 'Make an HTTP request',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return [...ALL_TOOLS].sort().map(tool => ({
|
|
61
|
+
value: tool,
|
|
62
|
+
label: tool,
|
|
63
|
+
hint: toolHints[tool] || '',
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Suggest input template references based on existing steps and workflow inputs.
|
|
69
|
+
* @param {object} definition - Partial workflow definition
|
|
70
|
+
* @param {number} stepIndex - Current step index
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
function suggestReferences(definition, stepIndex) {
|
|
74
|
+
const refs = [];
|
|
75
|
+
// Workflow-level inputs
|
|
76
|
+
for (const key of Object.keys(definition.inputs || {})) {
|
|
77
|
+
refs.push(`{{ inputs.${key} }}`);
|
|
78
|
+
}
|
|
79
|
+
// Previous steps
|
|
80
|
+
for (let i = 0; i < stepIndex; i++) {
|
|
81
|
+
const step = definition.steps[i];
|
|
82
|
+
refs.push(`{{ ${step.id}.output }}`);
|
|
83
|
+
if (step.tool === 'query' || step.tool === 'search' || step.tool === 'rerank') {
|
|
84
|
+
refs.push(`{{ ${step.id}.output.results }}`);
|
|
85
|
+
}
|
|
86
|
+
if (step.tool === 'generate') {
|
|
87
|
+
refs.push(`{{ ${step.id}.output.response }}`);
|
|
88
|
+
}
|
|
89
|
+
if (step.tool === 'estimate') {
|
|
90
|
+
refs.push(`{{ ${step.id}.output }}`);
|
|
91
|
+
}
|
|
92
|
+
if (step.tool === 'embed') {
|
|
93
|
+
refs.push(`{{ ${step.id}.output.embedding }}`);
|
|
94
|
+
}
|
|
95
|
+
if (step.tool === 'merge') {
|
|
96
|
+
refs.push(`{{ ${step.id}.output.results }}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return refs;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run the full interactive workflow builder using @clack/prompts.
|
|
104
|
+
* @returns {Promise<{definition: object, name: string, description: string, category: string, author: string}>}
|
|
105
|
+
*/
|
|
106
|
+
async function runInteractiveBuilder() {
|
|
107
|
+
const p = require('@clack/prompts');
|
|
108
|
+
|
|
109
|
+
p.intro(pc.bold(pc.cyan('Interactive Workflow Builder')));
|
|
110
|
+
|
|
111
|
+
// Step 1: Basic info
|
|
112
|
+
const basicInfo = await p.group({
|
|
113
|
+
name: () => p.text({
|
|
114
|
+
message: 'Workflow name',
|
|
115
|
+
placeholder: 'my-workflow',
|
|
116
|
+
validate: v => {
|
|
117
|
+
if (!v) return 'Required';
|
|
118
|
+
if (!/^[a-z][a-z0-9-]*$/.test(v)) return 'Use lowercase letters, numbers, and hyphens only (start with a letter)';
|
|
119
|
+
return undefined;
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
description: () => p.text({
|
|
123
|
+
message: 'Description',
|
|
124
|
+
placeholder: 'What does this workflow do?',
|
|
125
|
+
validate: v => v ? undefined : 'Required',
|
|
126
|
+
}),
|
|
127
|
+
category: () => p.select({
|
|
128
|
+
message: 'Category',
|
|
129
|
+
options: CATEGORIES.map(c => ({ value: c, label: c })),
|
|
130
|
+
}),
|
|
131
|
+
author: () => p.text({
|
|
132
|
+
message: 'Author',
|
|
133
|
+
placeholder: 'Your Name',
|
|
134
|
+
defaultValue: getGitAuthor(),
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (p.isCancel(basicInfo)) {
|
|
139
|
+
p.cancel('Cancelled.');
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const definition = {
|
|
144
|
+
name: basicInfo.name,
|
|
145
|
+
description: basicInfo.description,
|
|
146
|
+
version: '1.0.0',
|
|
147
|
+
inputs: {},
|
|
148
|
+
defaults: {},
|
|
149
|
+
steps: [],
|
|
150
|
+
output: {},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Step 2: Workflow inputs
|
|
154
|
+
p.log.info(pc.bold('Define workflow-level inputs'));
|
|
155
|
+
p.log.message(pc.dim('These are the parameters users provide when running the workflow.'));
|
|
156
|
+
|
|
157
|
+
let addingInputs = true;
|
|
158
|
+
while (addingInputs) {
|
|
159
|
+
const addInput = await p.confirm({
|
|
160
|
+
message: Object.keys(definition.inputs).length === 0
|
|
161
|
+
? 'Add a workflow input?'
|
|
162
|
+
: 'Add another workflow input?',
|
|
163
|
+
initialValue: Object.keys(definition.inputs).length === 0,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (p.isCancel(addInput)) {
|
|
167
|
+
p.cancel('Cancelled.');
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!addInput) {
|
|
172
|
+
addingInputs = false;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const inputInfo = await p.group({
|
|
177
|
+
name: () => p.text({
|
|
178
|
+
message: 'Input name',
|
|
179
|
+
placeholder: 'query',
|
|
180
|
+
validate: v => {
|
|
181
|
+
if (!v) return 'Required';
|
|
182
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(v)) return 'Must be a valid identifier';
|
|
183
|
+
if (definition.inputs[v]) return `Input "${v}" already exists`;
|
|
184
|
+
return undefined;
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
type: () => p.select({
|
|
188
|
+
message: 'Type',
|
|
189
|
+
options: [
|
|
190
|
+
{ value: 'string', label: 'string' },
|
|
191
|
+
{ value: 'number', label: 'number' },
|
|
192
|
+
{ value: 'boolean', label: 'boolean' },
|
|
193
|
+
{ value: 'array', label: 'array' },
|
|
194
|
+
],
|
|
195
|
+
}),
|
|
196
|
+
required: () => p.confirm({ message: 'Required?', initialValue: true }),
|
|
197
|
+
description: () => p.text({ message: 'Description', placeholder: 'What is this input for?' }),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (p.isCancel(inputInfo)) {
|
|
201
|
+
p.cancel('Cancelled.');
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const inputDef = {
|
|
206
|
+
type: inputInfo.type,
|
|
207
|
+
required: inputInfo.required,
|
|
208
|
+
};
|
|
209
|
+
if (inputInfo.description) inputDef.description = inputInfo.description;
|
|
210
|
+
|
|
211
|
+
if (!inputInfo.required) {
|
|
212
|
+
const hasDefault = await p.confirm({ message: 'Set a default value?', initialValue: false });
|
|
213
|
+
if (p.isCancel(hasDefault)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
214
|
+
if (hasDefault) {
|
|
215
|
+
const defaultVal = await p.text({ message: 'Default value' });
|
|
216
|
+
if (p.isCancel(defaultVal)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
217
|
+
if (inputInfo.type === 'number') {
|
|
218
|
+
inputDef.default = Number(defaultVal);
|
|
219
|
+
} else if (inputInfo.type === 'boolean') {
|
|
220
|
+
inputDef.default = defaultVal === 'true';
|
|
221
|
+
} else {
|
|
222
|
+
inputDef.default = defaultVal;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
definition.inputs[inputInfo.name] = inputDef;
|
|
228
|
+
p.log.success(`Added input: ${pc.cyan(inputInfo.name)} (${inputInfo.type}${inputInfo.required ? ', required' : ''})`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 3: Workflow steps
|
|
232
|
+
p.log.info(pc.bold('Define workflow steps'));
|
|
233
|
+
const refs = suggestReferences(definition, 0);
|
|
234
|
+
if (refs.length > 0) {
|
|
235
|
+
p.log.message(pc.dim(`Available references: ${refs.join(', ')}`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const existingStepIds = new Set();
|
|
239
|
+
let addingSteps = true;
|
|
240
|
+
while (addingSteps) {
|
|
241
|
+
const stepNum = definition.steps.length + 1;
|
|
242
|
+
p.log.step(`Step ${stepNum}`);
|
|
243
|
+
|
|
244
|
+
// Show available references
|
|
245
|
+
const currentRefs = suggestReferences(definition, definition.steps.length);
|
|
246
|
+
if (currentRefs.length > 0) {
|
|
247
|
+
p.log.message(pc.dim(`Available references:\n ${currentRefs.join('\n ')}`));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const stepId = await p.text({
|
|
251
|
+
message: 'Step ID',
|
|
252
|
+
placeholder: `step_${stepNum}`,
|
|
253
|
+
validate: v => validateStepId(v, existingStepIds),
|
|
254
|
+
});
|
|
255
|
+
if (p.isCancel(stepId)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
256
|
+
|
|
257
|
+
const tool = await p.select({
|
|
258
|
+
message: 'Tool',
|
|
259
|
+
options: buildToolOptions(),
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(tool)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
262
|
+
|
|
263
|
+
const stepName = await p.text({
|
|
264
|
+
message: 'Step name (human readable)',
|
|
265
|
+
placeholder: `${tool} step`,
|
|
266
|
+
validate: v => v ? undefined : 'Required',
|
|
267
|
+
});
|
|
268
|
+
if (p.isCancel(stepName)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
269
|
+
|
|
270
|
+
// Collect step inputs as key-value pairs
|
|
271
|
+
const stepInputs = {};
|
|
272
|
+
p.log.message(pc.dim('Add inputs for this step. Use template syntax like {{ inputs.query }} or {{ previousStep.output.results }}'));
|
|
273
|
+
|
|
274
|
+
let addingInputPairs = true;
|
|
275
|
+
while (addingInputPairs) {
|
|
276
|
+
const addPair = await p.confirm({
|
|
277
|
+
message: Object.keys(stepInputs).length === 0
|
|
278
|
+
? 'Add a step input?'
|
|
279
|
+
: 'Add another step input?',
|
|
280
|
+
initialValue: Object.keys(stepInputs).length === 0,
|
|
281
|
+
});
|
|
282
|
+
if (p.isCancel(addPair)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
283
|
+
|
|
284
|
+
if (!addPair) {
|
|
285
|
+
addingInputPairs = false;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Suggest common input keys per tool
|
|
290
|
+
const suggestedKeys = getSuggestedInputKeys(tool);
|
|
291
|
+
|
|
292
|
+
const inputKey = await p.text({
|
|
293
|
+
message: 'Input key',
|
|
294
|
+
placeholder: suggestedKeys[0] || 'key',
|
|
295
|
+
validate: v => v ? undefined : 'Required',
|
|
296
|
+
});
|
|
297
|
+
if (p.isCancel(inputKey)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
298
|
+
|
|
299
|
+
const inputValue = await p.text({
|
|
300
|
+
message: `Value for "${inputKey}"`,
|
|
301
|
+
placeholder: currentRefs.length > 0 ? currentRefs[0] : 'value or {{ template }}',
|
|
302
|
+
validate: v => (v !== undefined && v !== '') ? undefined : 'Required',
|
|
303
|
+
});
|
|
304
|
+
if (p.isCancel(inputValue)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
305
|
+
|
|
306
|
+
// Try to parse numbers and booleans for non-template values
|
|
307
|
+
if (!/\{\{/.test(inputValue)) {
|
|
308
|
+
if (inputValue === 'true') stepInputs[inputKey] = true;
|
|
309
|
+
else if (inputValue === 'false') stepInputs[inputKey] = false;
|
|
310
|
+
else if (!isNaN(inputValue) && inputValue.trim() !== '') stepInputs[inputKey] = Number(inputValue);
|
|
311
|
+
else stepInputs[inputKey] = inputValue;
|
|
312
|
+
} else {
|
|
313
|
+
stepInputs[inputKey] = inputValue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const step = {
|
|
318
|
+
id: stepId,
|
|
319
|
+
tool: tool,
|
|
320
|
+
name: stepName,
|
|
321
|
+
inputs: stepInputs,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
definition.steps.push(step);
|
|
325
|
+
existingStepIds.add(stepId);
|
|
326
|
+
|
|
327
|
+
p.log.success(`Added step: ${pc.cyan(stepId)} (${tool})`);
|
|
328
|
+
|
|
329
|
+
const addMore = await p.confirm({
|
|
330
|
+
message: 'Add another step?',
|
|
331
|
+
initialValue: false,
|
|
332
|
+
});
|
|
333
|
+
if (p.isCancel(addMore)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
334
|
+
if (!addMore) addingSteps = false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (definition.steps.length === 0) {
|
|
338
|
+
p.log.error('Workflow must have at least one step.');
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Step 4: Output mapping
|
|
343
|
+
p.log.info(pc.bold('Define workflow output'));
|
|
344
|
+
p.log.message(pc.dim('Map output keys to step results.'));
|
|
345
|
+
|
|
346
|
+
const allRefs = suggestReferences(definition, definition.steps.length);
|
|
347
|
+
if (allRefs.length > 0) {
|
|
348
|
+
p.log.message(pc.dim(`Available references:\n ${allRefs.join('\n ')}`));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let addingOutputs = true;
|
|
352
|
+
while (addingOutputs) {
|
|
353
|
+
const addOutput = await p.confirm({
|
|
354
|
+
message: Object.keys(definition.output).length === 0
|
|
355
|
+
? 'Add an output mapping?'
|
|
356
|
+
: 'Add another output mapping?',
|
|
357
|
+
initialValue: true,
|
|
358
|
+
});
|
|
359
|
+
if (p.isCancel(addOutput)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
360
|
+
if (!addOutput) { addingOutputs = false; break; }
|
|
361
|
+
|
|
362
|
+
const outputKey = await p.text({
|
|
363
|
+
message: 'Output key',
|
|
364
|
+
placeholder: 'result',
|
|
365
|
+
validate: v => v ? undefined : 'Required',
|
|
366
|
+
});
|
|
367
|
+
if (p.isCancel(outputKey)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
368
|
+
|
|
369
|
+
const outputValue = await p.text({
|
|
370
|
+
message: `Value for "${outputKey}"`,
|
|
371
|
+
placeholder: allRefs.length > 0 ? allRefs[allRefs.length - 1] : '{{ stepId.output }}',
|
|
372
|
+
validate: v => v ? undefined : 'Required',
|
|
373
|
+
});
|
|
374
|
+
if (p.isCancel(outputValue)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
375
|
+
|
|
376
|
+
definition.output[outputKey] = outputValue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Step 5: Validate
|
|
380
|
+
const { validateWorkflow, buildExecutionPlan } = require('./workflow');
|
|
381
|
+
const errors = validateWorkflow(definition);
|
|
382
|
+
|
|
383
|
+
if (errors.length > 0) {
|
|
384
|
+
p.log.error(pc.bold('Validation errors:'));
|
|
385
|
+
for (const err of errors) {
|
|
386
|
+
p.log.error(` ${pc.red('x')} ${err}`);
|
|
387
|
+
}
|
|
388
|
+
const fixOrWrite = await p.confirm({
|
|
389
|
+
message: 'Write anyway (you can fix later)?',
|
|
390
|
+
initialValue: false,
|
|
391
|
+
});
|
|
392
|
+
if (p.isCancel(fixOrWrite) || !fixOrWrite) {
|
|
393
|
+
p.cancel('Workflow not saved. Fix the errors and try again.');
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
p.log.success('Workflow validates successfully!');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Step 6: Show execution plan
|
|
401
|
+
try {
|
|
402
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
403
|
+
p.log.info(pc.bold('Execution plan:'));
|
|
404
|
+
for (let i = 0; i < layers.length; i++) {
|
|
405
|
+
const layerSteps = layers[i].map(id => {
|
|
406
|
+
const step = definition.steps.find(s => s.id === id);
|
|
407
|
+
return `${pc.cyan(id)} (${step ? step.tool : '?'})`;
|
|
408
|
+
});
|
|
409
|
+
p.log.message(` Layer ${i + 1}: ${layerSteps.join(', ')}`);
|
|
410
|
+
}
|
|
411
|
+
if (layers.length > 1) {
|
|
412
|
+
p.log.message(pc.dim(` Steps in the same layer run in parallel.`));
|
|
413
|
+
}
|
|
414
|
+
} catch (err) {
|
|
415
|
+
p.log.warn(`Could not build execution plan: ${err.message}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
definition,
|
|
420
|
+
name: basicInfo.name,
|
|
421
|
+
description: basicInfo.description,
|
|
422
|
+
category: basicInfo.category,
|
|
423
|
+
author: basicInfo.author,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Generate a workflow skeleton from a text description.
|
|
429
|
+
* Uses pattern matching against known tool capabilities.
|
|
430
|
+
* @param {string} description
|
|
431
|
+
* @returns {object} workflow definition
|
|
432
|
+
*/
|
|
433
|
+
function workflowFromDescription(description) {
|
|
434
|
+
const desc = description.toLowerCase();
|
|
435
|
+
const definition = {
|
|
436
|
+
name: '',
|
|
437
|
+
description: description,
|
|
438
|
+
version: '1.0.0',
|
|
439
|
+
inputs: {},
|
|
440
|
+
defaults: {},
|
|
441
|
+
steps: [],
|
|
442
|
+
output: {},
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Extract a name from the description
|
|
446
|
+
const nameMatch = description.match(/^(\w[\w\s-]{2,30})/);
|
|
447
|
+
if (nameMatch) {
|
|
448
|
+
definition.name = nameMatch[1].trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
449
|
+
} else {
|
|
450
|
+
definition.name = 'generated-workflow';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Pattern matching for common workflow types
|
|
454
|
+
const hasSearch = /search|find|look up|retrieve|query/i.test(desc);
|
|
455
|
+
const hasCompare = /compare|versus|vs|shootout|side.by.side/i.test(desc);
|
|
456
|
+
const hasGenerate = /generate|summarize|synthesize|answer|write|create text|explain/i.test(desc);
|
|
457
|
+
const hasRerank = /rerank|re-rank|rank|sort by relevance/i.test(desc);
|
|
458
|
+
const hasMerge = /merge|combine|aggregate results/i.test(desc);
|
|
459
|
+
const hasIngest = /ingest|import|load|index documents/i.test(desc);
|
|
460
|
+
const hasEmbed = /embed|embedding|vectorize/i.test(desc);
|
|
461
|
+
const hasSimilarity = /similar|similarity|compare embeddings/i.test(desc);
|
|
462
|
+
const hasDecompose = /decompos|break down|sub.quer/i.test(desc);
|
|
463
|
+
const hasHttp = /http|api|fetch|request|webhook/i.test(desc);
|
|
464
|
+
const hasLoop = /loop|iterate|each|batch/i.test(desc);
|
|
465
|
+
const hasFilter = /filter|exclude|only keep/i.test(desc);
|
|
466
|
+
const hasChunk = /chunk|split|segment/i.test(desc);
|
|
467
|
+
const hasCost = /cost|estimat|price/i.test(desc);
|
|
468
|
+
|
|
469
|
+
// Common inputs
|
|
470
|
+
if (hasSearch || hasRerank || hasGenerate || hasDecompose) {
|
|
471
|
+
definition.inputs.query = { type: 'string', required: true, description: 'The search query or question' };
|
|
472
|
+
}
|
|
473
|
+
if (hasSearch || hasIngest) {
|
|
474
|
+
definition.inputs.collection = { type: 'string', required: true, description: 'MongoDB collection name' };
|
|
475
|
+
}
|
|
476
|
+
if (hasSearch) {
|
|
477
|
+
definition.inputs.limit = { type: 'number', default: 10, description: 'Maximum results to return' };
|
|
478
|
+
}
|
|
479
|
+
if (hasHttp) {
|
|
480
|
+
definition.inputs.url = { type: 'string', required: true, description: 'URL to fetch' };
|
|
481
|
+
}
|
|
482
|
+
if (hasEmbed || hasChunk) {
|
|
483
|
+
definition.inputs.text = { type: 'string', required: true, description: 'Text to process' };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let stepIndex = 0;
|
|
487
|
+
|
|
488
|
+
// Build steps based on detected patterns
|
|
489
|
+
if (hasDecompose) {
|
|
490
|
+
definition.steps.push({
|
|
491
|
+
id: 'decompose',
|
|
492
|
+
tool: 'generate',
|
|
493
|
+
name: 'Decompose into sub-queries',
|
|
494
|
+
inputs: {
|
|
495
|
+
prompt: 'Break the following question into 3 focused sub-questions. Return ONLY a JSON array of strings.\n\nQuestion: {{ inputs.query }}',
|
|
496
|
+
format: 'json',
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
stepIndex++;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (hasChunk) {
|
|
503
|
+
definition.steps.push({
|
|
504
|
+
id: 'chunk_text',
|
|
505
|
+
tool: 'chunk',
|
|
506
|
+
name: 'Split text into chunks',
|
|
507
|
+
inputs: { text: '{{ inputs.text }}', chunkSize: 512 },
|
|
508
|
+
});
|
|
509
|
+
stepIndex++;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (hasEmbed && !hasSearch) {
|
|
513
|
+
definition.steps.push({
|
|
514
|
+
id: 'embed_text',
|
|
515
|
+
tool: 'embed',
|
|
516
|
+
name: 'Generate embeddings',
|
|
517
|
+
inputs: { text: '{{ inputs.text }}', model: 'voyage-4-large' },
|
|
518
|
+
});
|
|
519
|
+
stepIndex++;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (hasIngest) {
|
|
523
|
+
definition.steps.push({
|
|
524
|
+
id: 'ingest_data',
|
|
525
|
+
tool: 'ingest',
|
|
526
|
+
name: 'Ingest documents',
|
|
527
|
+
inputs: { collection: '{{ inputs.collection }}' },
|
|
528
|
+
});
|
|
529
|
+
stepIndex++;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (hasHttp) {
|
|
533
|
+
definition.steps.push({
|
|
534
|
+
id: 'fetch_data',
|
|
535
|
+
tool: 'http',
|
|
536
|
+
name: 'Fetch external data',
|
|
537
|
+
inputs: { url: '{{ inputs.url }}', method: 'GET' },
|
|
538
|
+
});
|
|
539
|
+
stepIndex++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (hasSearch && hasCompare) {
|
|
543
|
+
// Comparison pattern: search with multiple models
|
|
544
|
+
for (const model of ['voyage-4-large', 'voyage-4', 'voyage-4-lite']) {
|
|
545
|
+
const suffix = model.replace('voyage-4', '').replace('-', '') || 'base';
|
|
546
|
+
const id = `search_${suffix === '' ? 'base' : suffix}`;
|
|
547
|
+
definition.steps.push({
|
|
548
|
+
id,
|
|
549
|
+
tool: 'query',
|
|
550
|
+
name: `Search with ${model}`,
|
|
551
|
+
inputs: {
|
|
552
|
+
query: '{{ inputs.query }}',
|
|
553
|
+
collection: '{{ inputs.collection }}',
|
|
554
|
+
model,
|
|
555
|
+
limit: '{{ inputs.limit }}',
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
stepIndex += 3;
|
|
560
|
+
} else if (hasSearch) {
|
|
561
|
+
const searchId = hasDecompose ? 'search_results' : 'search';
|
|
562
|
+
const queryRef = hasDecompose ? '{{ decompose.output.response[0] }}' : '{{ inputs.query }}';
|
|
563
|
+
definition.steps.push({
|
|
564
|
+
id: searchId,
|
|
565
|
+
tool: 'query',
|
|
566
|
+
name: 'Search knowledge base',
|
|
567
|
+
inputs: {
|
|
568
|
+
query: queryRef,
|
|
569
|
+
collection: '{{ inputs.collection }}',
|
|
570
|
+
limit: '{{ inputs.limit }}',
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
stepIndex++;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (hasSimilarity) {
|
|
577
|
+
definition.steps.push({
|
|
578
|
+
id: 'check_similarity',
|
|
579
|
+
tool: 'similarity',
|
|
580
|
+
name: 'Compute similarity',
|
|
581
|
+
inputs: { text1: '{{ inputs.query }}', text2: '{{ inputs.text }}' },
|
|
582
|
+
});
|
|
583
|
+
stepIndex++;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (hasCost) {
|
|
587
|
+
definition.steps.push({
|
|
588
|
+
id: 'estimate_cost',
|
|
589
|
+
tool: 'estimate',
|
|
590
|
+
name: 'Estimate cost',
|
|
591
|
+
inputs: { text: '{{ inputs.query }}', model: 'voyage-4-large' },
|
|
592
|
+
});
|
|
593
|
+
stepIndex++;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (hasMerge && definition.steps.length >= 2) {
|
|
597
|
+
const arrayRefs = definition.steps
|
|
598
|
+
.filter(s => s.tool === 'query' || s.tool === 'search')
|
|
599
|
+
.map(s => `{{ ${s.id}.output.results }}`);
|
|
600
|
+
if (arrayRefs.length >= 2) {
|
|
601
|
+
definition.steps.push({
|
|
602
|
+
id: 'merged',
|
|
603
|
+
tool: 'merge',
|
|
604
|
+
name: 'Merge results',
|
|
605
|
+
inputs: { arrays: arrayRefs, dedup: true },
|
|
606
|
+
});
|
|
607
|
+
stepIndex++;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (hasFilter) {
|
|
612
|
+
const lastResultStep = [...definition.steps].reverse().find(s =>
|
|
613
|
+
s.tool === 'query' || s.tool === 'search' || s.tool === 'merge'
|
|
614
|
+
);
|
|
615
|
+
if (lastResultStep) {
|
|
616
|
+
definition.steps.push({
|
|
617
|
+
id: 'filtered',
|
|
618
|
+
tool: 'filter',
|
|
619
|
+
name: 'Filter results',
|
|
620
|
+
inputs: {
|
|
621
|
+
items: `{{ ${lastResultStep.id}.output.results }}`,
|
|
622
|
+
condition: 'item.score > 0.5',
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
stepIndex++;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (hasRerank) {
|
|
630
|
+
const lastResultStep = [...definition.steps].reverse().find(s =>
|
|
631
|
+
s.tool === 'query' || s.tool === 'search' || s.tool === 'merge' || s.tool === 'filter'
|
|
632
|
+
);
|
|
633
|
+
if (lastResultStep) {
|
|
634
|
+
definition.steps.push({
|
|
635
|
+
id: 'reranked',
|
|
636
|
+
tool: 'rerank',
|
|
637
|
+
name: 'Rerank results',
|
|
638
|
+
inputs: {
|
|
639
|
+
query: '{{ inputs.query }}',
|
|
640
|
+
documents: `{{ ${lastResultStep.id}.output.results }}`,
|
|
641
|
+
model: 'rerank-2.5',
|
|
642
|
+
limit: '{{ inputs.limit }}',
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
stepIndex++;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (hasGenerate) {
|
|
650
|
+
const contextStep = [...definition.steps].reverse().find(s =>
|
|
651
|
+
s.tool === 'rerank' || s.tool === 'query' || s.tool === 'search' || s.tool === 'merge' || s.tool === 'filter'
|
|
652
|
+
);
|
|
653
|
+
definition.steps.push({
|
|
654
|
+
id: 'generate_response',
|
|
655
|
+
tool: 'generate',
|
|
656
|
+
name: 'Generate response',
|
|
657
|
+
inputs: {
|
|
658
|
+
prompt: `Using the following context, provide a comprehensive answer.\n\nQuestion: {{ inputs.query }}`,
|
|
659
|
+
...(contextStep ? { context: `{{ ${contextStep.id}.output.results }}` } : {}),
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
stepIndex++;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Fallback: if no steps were generated, add a basic search step
|
|
666
|
+
if (definition.steps.length === 0) {
|
|
667
|
+
if (!definition.inputs.query) {
|
|
668
|
+
definition.inputs.query = { type: 'string', required: true, description: 'Search query' };
|
|
669
|
+
}
|
|
670
|
+
if (!definition.inputs.collection) {
|
|
671
|
+
definition.inputs.collection = { type: 'string', required: true, description: 'Collection name' };
|
|
672
|
+
}
|
|
673
|
+
definition.steps.push({
|
|
674
|
+
id: 'search',
|
|
675
|
+
tool: 'query',
|
|
676
|
+
name: 'Search',
|
|
677
|
+
inputs: {
|
|
678
|
+
query: '{{ inputs.query }}',
|
|
679
|
+
collection: '{{ inputs.collection }}',
|
|
680
|
+
limit: 10,
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Build output mapping from last step(s)
|
|
686
|
+
const lastStep = definition.steps[definition.steps.length - 1];
|
|
687
|
+
if (lastStep.tool === 'generate') {
|
|
688
|
+
definition.output.response = `{{ ${lastStep.id}.output.response }}`;
|
|
689
|
+
} else {
|
|
690
|
+
definition.output.results = `{{ ${lastStep.id}.output.results }}`;
|
|
691
|
+
}
|
|
692
|
+
// Also expose query if it exists
|
|
693
|
+
if (definition.inputs.query) {
|
|
694
|
+
definition.output.query = '{{ inputs.query }}';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return definition;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Get suggested input keys for a tool.
|
|
702
|
+
* @param {string} tool
|
|
703
|
+
* @returns {string[]}
|
|
704
|
+
*/
|
|
705
|
+
function getSuggestedInputKeys(tool) {
|
|
706
|
+
const suggestions = {
|
|
707
|
+
query: ['query', 'collection', 'model', 'limit', 'filter'],
|
|
708
|
+
search: ['query', 'collection', 'limit'],
|
|
709
|
+
rerank: ['query', 'documents', 'model', 'limit'],
|
|
710
|
+
embed: ['text', 'model'],
|
|
711
|
+
similarity: ['text1', 'text2', 'model'],
|
|
712
|
+
ingest: ['collection', 'documents', 'model'],
|
|
713
|
+
estimate: ['text', 'model'],
|
|
714
|
+
generate: ['prompt', 'context', 'format', 'model'],
|
|
715
|
+
merge: ['arrays', 'dedup'],
|
|
716
|
+
filter: ['items', 'condition'],
|
|
717
|
+
transform: ['items', 'expression'],
|
|
718
|
+
conditional: ['condition', 'ifTrue', 'ifFalse'],
|
|
719
|
+
loop: ['items', 'step', 'as'],
|
|
720
|
+
template: ['template', 'data'],
|
|
721
|
+
chunk: ['text', 'chunkSize', 'overlap'],
|
|
722
|
+
aggregate: ['items', 'operation'],
|
|
723
|
+
http: ['url', 'method', 'headers', 'body'],
|
|
724
|
+
collections: [],
|
|
725
|
+
models: [],
|
|
726
|
+
explain: ['text', 'model'],
|
|
727
|
+
code_index: ['path'],
|
|
728
|
+
code_search: ['query'],
|
|
729
|
+
code_query: ['query'],
|
|
730
|
+
code_find_similar: ['code'],
|
|
731
|
+
code_status: [],
|
|
732
|
+
};
|
|
733
|
+
return suggestions[tool] || [];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function getGitAuthor() {
|
|
737
|
+
try {
|
|
738
|
+
const { execSync } = require('child_process');
|
|
739
|
+
return execSync('git config user.name', { encoding: 'utf8' }).trim();
|
|
740
|
+
} catch {
|
|
741
|
+
return '';
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
module.exports = {
|
|
746
|
+
runInteractiveBuilder,
|
|
747
|
+
workflowFromDescription,
|
|
748
|
+
validateStepId,
|
|
749
|
+
buildToolOptions,
|
|
750
|
+
suggestReferences,
|
|
751
|
+
getSuggestedInputKeys,
|
|
752
|
+
RESERVED_STEP_IDS,
|
|
753
|
+
};
|