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,662 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { validateWorkflow, buildExecutionPlan, buildDependencyGraph, ALL_TOOLS } = require('../../lib/workflow');
|
|
4
|
+
|
|
5
|
+
// ════════════════════════════════════════════════════════════════════
|
|
6
|
+
// Tool catalog: default inputs per tool
|
|
7
|
+
// ════════════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
const TOOL_DEFAULTS = {
|
|
10
|
+
query: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' },
|
|
11
|
+
search: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' },
|
|
12
|
+
rerank: { query: '{{ inputs.query }}', documents: [], model: 'rerank-2.5' },
|
|
13
|
+
embed: { text: '{{ inputs.text }}' },
|
|
14
|
+
similarity: { text1: '{{ inputs.text1 }}', text2: '{{ inputs.text2 }}' },
|
|
15
|
+
ingest: { text: '{{ inputs.text }}', collection: '{{ inputs.collection }}' },
|
|
16
|
+
collections: {},
|
|
17
|
+
models: { category: 'all' },
|
|
18
|
+
explain: { topic: '{{ inputs.topic }}' },
|
|
19
|
+
estimate: { docs: '{{ inputs.docs }}', queries: '{{ inputs.queries }}', months: 12 },
|
|
20
|
+
generate: { prompt: '{{ inputs.prompt }}' },
|
|
21
|
+
template: { text: '' },
|
|
22
|
+
merge: { arrays: [], dedup: true },
|
|
23
|
+
filter: { array: [], condition: '' },
|
|
24
|
+
transform: { array: [], fields: [] },
|
|
25
|
+
conditional: { condition: '', then: [], else: [] },
|
|
26
|
+
loop: { items: [], as: 'item', step: {} },
|
|
27
|
+
chunk: { text: '{{ inputs.text }}', strategy: 'recursive', size: 512 },
|
|
28
|
+
aggregate: { pipeline: [] },
|
|
29
|
+
http: { url: '{{ inputs.url }}', method: 'GET' },
|
|
30
|
+
code_index: { source: '{{ inputs.source }}' },
|
|
31
|
+
code_search: { query: '{{ inputs.query }}' },
|
|
32
|
+
code_query: { query: '{{ inputs.query }}' },
|
|
33
|
+
code_find_similar: { code: '{{ inputs.code }}' },
|
|
34
|
+
code_status: {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ════════════════════════════════════════════════════════════════════
|
|
38
|
+
// Common workflow patterns
|
|
39
|
+
// ════════════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
const PATTERNS = {
|
|
42
|
+
// Search then generate (RAG)
|
|
43
|
+
rag: {
|
|
44
|
+
tools: ['query', 'generate'],
|
|
45
|
+
inputs: {
|
|
46
|
+
query: { type: 'string', required: true, description: 'The question to answer' },
|
|
47
|
+
collection: { type: 'string', required: true, description: 'MongoDB collection with embedded documents' },
|
|
48
|
+
limit: { type: 'number', default: 5, description: 'Number of results to retrieve' },
|
|
49
|
+
},
|
|
50
|
+
steps: [
|
|
51
|
+
{ id: 'retrieve', tool: 'query', name: 'Retrieve relevant documents', inputs: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' } },
|
|
52
|
+
{ id: 'answer', tool: 'generate', name: 'Generate answer from context', inputs: { prompt: 'Answer the following question using the provided context.\n\nQuestion: {{ inputs.query }}', context: '{{ retrieve.output.results }}' } },
|
|
53
|
+
],
|
|
54
|
+
output: { answer: '{{ answer.output.response }}', sources: '{{ retrieve.output.results }}' },
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Search + rerank
|
|
58
|
+
search_rerank: {
|
|
59
|
+
tools: ['search', 'rerank'],
|
|
60
|
+
inputs: {
|
|
61
|
+
query: { type: 'string', required: true, description: 'Search query' },
|
|
62
|
+
collection: { type: 'string', required: true, description: 'MongoDB collection' },
|
|
63
|
+
limit: { type: 'number', default: 10, description: 'Number of results' },
|
|
64
|
+
},
|
|
65
|
+
steps: [
|
|
66
|
+
{ id: 'search_step', tool: 'search', name: 'Vector search', inputs: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: 50 } },
|
|
67
|
+
{ id: 'rerank_step', tool: 'rerank', name: 'Rerank results', inputs: { query: '{{ inputs.query }}', documents: '{{ search_step.output.results }}', model: 'rerank-2.5' } },
|
|
68
|
+
],
|
|
69
|
+
output: { results: '{{ rerank_step.output.results }}' },
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Ingest pipeline
|
|
73
|
+
ingest_pipeline: {
|
|
74
|
+
tools: ['chunk', 'ingest'],
|
|
75
|
+
inputs: {
|
|
76
|
+
text: { type: 'string', required: true, description: 'Text content to ingest' },
|
|
77
|
+
collection: { type: 'string', required: true, description: 'Target collection' },
|
|
78
|
+
source: { type: 'string', default: 'manual', description: 'Source identifier' },
|
|
79
|
+
},
|
|
80
|
+
steps: [
|
|
81
|
+
{ id: 'chunk_step', tool: 'chunk', name: 'Chunk the text', inputs: { text: '{{ inputs.text }}', strategy: 'recursive', size: 512 } },
|
|
82
|
+
{ id: 'ingest_step', tool: 'ingest', name: 'Embed and store chunks', inputs: { text: '{{ inputs.text }}', collection: '{{ inputs.collection }}', source: '{{ inputs.source }}' } },
|
|
83
|
+
],
|
|
84
|
+
output: { chunks: '{{ chunk_step.output.totalChunks }}', inserted: '{{ ingest_step.output.insertedCount }}' },
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Compare embeddings
|
|
88
|
+
compare: {
|
|
89
|
+
tools: ['similarity'],
|
|
90
|
+
inputs: {
|
|
91
|
+
text1: { type: 'string', required: true, description: 'First text' },
|
|
92
|
+
text2: { type: 'string', required: true, description: 'Second text' },
|
|
93
|
+
},
|
|
94
|
+
steps: [
|
|
95
|
+
{ id: 'compare_step', tool: 'similarity', name: 'Compare text similarity', inputs: { text1: '{{ inputs.text1 }}', text2: '{{ inputs.text2 }}' } },
|
|
96
|
+
],
|
|
97
|
+
output: { similarity: '{{ compare_step.output.similarity }}' },
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// Multi-search + merge + rerank
|
|
101
|
+
multi_search: {
|
|
102
|
+
tools: ['query', 'merge', 'rerank'],
|
|
103
|
+
inputs: {
|
|
104
|
+
query: { type: 'string', required: true, description: 'Search query' },
|
|
105
|
+
collection: { type: 'string', required: true, description: 'MongoDB collection' },
|
|
106
|
+
limit: { type: 'number', default: 10, description: 'Number of results' },
|
|
107
|
+
},
|
|
108
|
+
steps: [
|
|
109
|
+
{ id: 'search_broad', tool: 'query', name: 'Broad search', inputs: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: 20 } },
|
|
110
|
+
{ id: 'merge_results', tool: 'merge', name: 'Merge results', inputs: { arrays: ['{{ search_broad.output.results }}'], dedup: true } },
|
|
111
|
+
{ id: 'rerank_merged', tool: 'rerank', name: 'Rerank merged results', inputs: { query: '{{ inputs.query }}', documents: '{{ merge_results.output.results }}' } },
|
|
112
|
+
],
|
|
113
|
+
output: { results: '{{ rerank_merged.output.results }}' },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ════════════════════════════════════════════════════════════════════
|
|
118
|
+
// Pattern matching
|
|
119
|
+
// ════════════════════════════════════════════════════════════════════
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Match a set of requested tools to the best known pattern.
|
|
123
|
+
* @param {string[]} tools
|
|
124
|
+
* @returns {string|null} pattern key or null
|
|
125
|
+
*/
|
|
126
|
+
function matchPattern(tools) {
|
|
127
|
+
const toolSet = new Set(tools);
|
|
128
|
+
|
|
129
|
+
// Score each pattern by how many of its tools are present
|
|
130
|
+
let bestKey = null;
|
|
131
|
+
let bestScore = 0;
|
|
132
|
+
|
|
133
|
+
for (const [key, pattern] of Object.entries(PATTERNS)) {
|
|
134
|
+
const patternTools = new Set(pattern.tools);
|
|
135
|
+
let matches = 0;
|
|
136
|
+
for (const t of patternTools) {
|
|
137
|
+
if (toolSet.has(t)) matches++;
|
|
138
|
+
}
|
|
139
|
+
const score = matches / patternTools.size;
|
|
140
|
+
if (score > bestScore) {
|
|
141
|
+
bestScore = score;
|
|
142
|
+
bestKey = key;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return bestScore >= 0.5 ? bestKey : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ════════════════════════════════════════════════════════════════════
|
|
150
|
+
// Workflow generation
|
|
151
|
+
// ════════════════════════════════════════════════════════════════════
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generate a workflow definition from a description and optional tools list.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} params
|
|
157
|
+
* @param {string} params.description - Natural language description
|
|
158
|
+
* @param {string} [params.category] - Workflow category
|
|
159
|
+
* @param {string[]} [params.tools] - Explicit list of tools to use
|
|
160
|
+
* @returns {{ workflow: object, validation: object, executionPlan: string[][], dependencyGraph: object }}
|
|
161
|
+
*/
|
|
162
|
+
function generateWorkflow({ description, category, tools }) {
|
|
163
|
+
// Determine tools from description if not provided
|
|
164
|
+
const requestedTools = tools && tools.length > 0
|
|
165
|
+
? tools.filter(t => ALL_TOOLS.has(t))
|
|
166
|
+
: inferToolsFromDescription(description);
|
|
167
|
+
|
|
168
|
+
// Try to match a known pattern
|
|
169
|
+
const patternKey = matchPattern(requestedTools);
|
|
170
|
+
let workflow;
|
|
171
|
+
|
|
172
|
+
if (patternKey) {
|
|
173
|
+
workflow = buildFromPattern(patternKey, description, category, requestedTools);
|
|
174
|
+
} else {
|
|
175
|
+
workflow = buildFromToolList(description, category, requestedTools);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Validate
|
|
179
|
+
const validationErrors = validateWorkflow(workflow);
|
|
180
|
+
const layers = validationErrors.length === 0
|
|
181
|
+
? buildExecutionPlan(workflow.steps)
|
|
182
|
+
: [];
|
|
183
|
+
|
|
184
|
+
// Build dependency info
|
|
185
|
+
let dependencyGraph = {};
|
|
186
|
+
if (validationErrors.length === 0) {
|
|
187
|
+
const graph = buildDependencyGraph(workflow.steps);
|
|
188
|
+
for (const [stepId, deps] of graph) {
|
|
189
|
+
dependencyGraph[stepId] = [...deps];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
workflow,
|
|
195
|
+
validation: {
|
|
196
|
+
valid: validationErrors.length === 0,
|
|
197
|
+
errors: validationErrors,
|
|
198
|
+
},
|
|
199
|
+
executionPlan: layers,
|
|
200
|
+
dependencyGraph,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Infer tools from a natural language description.
|
|
206
|
+
* @param {string} description
|
|
207
|
+
* @returns {string[]}
|
|
208
|
+
*/
|
|
209
|
+
function inferToolsFromDescription(description) {
|
|
210
|
+
const lower = description.toLowerCase();
|
|
211
|
+
const tools = [];
|
|
212
|
+
|
|
213
|
+
// Keywords to tool mapping
|
|
214
|
+
const keywords = {
|
|
215
|
+
query: ['search', 'query', 'find', 'retrieve', 'look up', 'lookup', 'rag', 'answer'],
|
|
216
|
+
generate: ['generate', 'summarize', 'answer', 'synthesize', 'write', 'compose', 'rag', 'explain answer'],
|
|
217
|
+
rerank: ['rerank', 're-rank', 'rank', 'sort by relevance', 'precision'],
|
|
218
|
+
embed: ['embed', 'embedding', 'vector', 'vectorize'],
|
|
219
|
+
similarity: ['similar', 'similarity', 'compare', 'distance'],
|
|
220
|
+
ingest: ['ingest', 'import', 'load', 'store', 'index document'],
|
|
221
|
+
chunk: ['chunk', 'split', 'segment', 'partition'],
|
|
222
|
+
merge: ['merge', 'combine', 'concat', 'join', 'union'],
|
|
223
|
+
filter: ['filter', 'exclude', 'include only', 'where'],
|
|
224
|
+
transform: ['transform', 'reshape', 'map', 'rename'],
|
|
225
|
+
template: ['template', 'format', 'compose text'],
|
|
226
|
+
conditional: ['conditional', 'if', 'branch', 'when'],
|
|
227
|
+
loop: ['loop', 'iterate', 'for each', 'batch'],
|
|
228
|
+
http: ['http', 'api', 'fetch', 'request', 'url', 'endpoint', 'webhook'],
|
|
229
|
+
estimate: ['cost', 'estimate', 'price', 'budget'],
|
|
230
|
+
models: ['model', 'models', 'list models', 'catalog'],
|
|
231
|
+
collections: ['collection', 'collections', 'list collections'],
|
|
232
|
+
aggregate: ['aggregate', 'pipeline', 'mongodb aggregate'],
|
|
233
|
+
search: ['full-text search', 'vector search'],
|
|
234
|
+
code_search: ['code search', 'search code', 'codebase search'],
|
|
235
|
+
code_index: ['code index', 'index code', 'index repo', 'index repository'],
|
|
236
|
+
code_query: ['code query', 'ask about code', 'codebase question'],
|
|
237
|
+
code_find_similar: ['find similar code', 'similar code', 'code clone'],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
for (const [tool, kws] of Object.entries(keywords)) {
|
|
241
|
+
for (const kw of kws) {
|
|
242
|
+
if (lower.includes(kw)) {
|
|
243
|
+
tools.push(tool);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Default: if nothing matched, provide a basic query + generate (RAG) workflow
|
|
250
|
+
if (tools.length === 0) {
|
|
251
|
+
tools.push('query', 'generate');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Deduplicate
|
|
255
|
+
return [...new Set(tools)];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build a workflow from a known pattern.
|
|
260
|
+
*/
|
|
261
|
+
function buildFromPattern(patternKey, description, category, requestedTools) {
|
|
262
|
+
const pattern = PATTERNS[patternKey];
|
|
263
|
+
const slug = slugify(description);
|
|
264
|
+
|
|
265
|
+
const workflow = {
|
|
266
|
+
name: slug,
|
|
267
|
+
description,
|
|
268
|
+
version: '1.0.0',
|
|
269
|
+
inputs: { ...pattern.inputs },
|
|
270
|
+
defaults: {},
|
|
271
|
+
steps: pattern.steps.map(s => ({ ...s, inputs: { ...s.inputs } })),
|
|
272
|
+
output: { ...pattern.output },
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Add any extra requested tools not in the pattern as additional steps
|
|
276
|
+
const patternTools = new Set(pattern.tools);
|
|
277
|
+
const extras = requestedTools.filter(t => !patternTools.has(t));
|
|
278
|
+
|
|
279
|
+
for (const tool of extras) {
|
|
280
|
+
const stepId = `${tool}_step`;
|
|
281
|
+
const step = buildStepForTool(tool, stepId, workflow);
|
|
282
|
+
workflow.steps.push(step);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (category) {
|
|
286
|
+
workflow.category = category;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return workflow;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Build a workflow from an explicit tool list with no pattern match.
|
|
294
|
+
*/
|
|
295
|
+
function buildFromToolList(description, category, requestedTools) {
|
|
296
|
+
const slug = slugify(description);
|
|
297
|
+
|
|
298
|
+
// Build inputs based on what tools need
|
|
299
|
+
const inputs = {};
|
|
300
|
+
const toolSet = new Set(requestedTools);
|
|
301
|
+
|
|
302
|
+
if (toolSet.has('query') || toolSet.has('search') || toolSet.has('code_search') || toolSet.has('code_query')) {
|
|
303
|
+
inputs.query = { type: 'string', required: true, description: 'Search query or question' };
|
|
304
|
+
}
|
|
305
|
+
if (toolSet.has('query') || toolSet.has('search') || toolSet.has('ingest')) {
|
|
306
|
+
inputs.collection = { type: 'string', required: true, description: 'MongoDB collection name' };
|
|
307
|
+
}
|
|
308
|
+
if (toolSet.has('query') || toolSet.has('search')) {
|
|
309
|
+
inputs.limit = { type: 'number', default: 10, description: 'Maximum results to return' };
|
|
310
|
+
}
|
|
311
|
+
if (toolSet.has('embed') || toolSet.has('chunk') || toolSet.has('ingest')) {
|
|
312
|
+
inputs.text = { type: 'string', required: true, description: 'Text content to process' };
|
|
313
|
+
}
|
|
314
|
+
if (toolSet.has('similarity')) {
|
|
315
|
+
inputs.text1 = { type: 'string', required: true, description: 'First text to compare' };
|
|
316
|
+
inputs.text2 = { type: 'string', required: true, description: 'Second text to compare' };
|
|
317
|
+
}
|
|
318
|
+
if (toolSet.has('http')) {
|
|
319
|
+
inputs.url = { type: 'string', required: true, description: 'URL for HTTP request' };
|
|
320
|
+
}
|
|
321
|
+
if (toolSet.has('estimate')) {
|
|
322
|
+
inputs.docs = { type: 'number', required: true, description: 'Number of documents' };
|
|
323
|
+
inputs.queries = { type: 'number', default: 0, description: 'Queries per month' };
|
|
324
|
+
}
|
|
325
|
+
if (toolSet.has('code_index')) {
|
|
326
|
+
inputs.source = { type: 'string', required: true, description: 'Path or URL of code to index' };
|
|
327
|
+
}
|
|
328
|
+
if (toolSet.has('code_find_similar')) {
|
|
329
|
+
inputs.code = { type: 'string', required: true, description: 'Code snippet to find similar implementations for' };
|
|
330
|
+
}
|
|
331
|
+
if (toolSet.has('explain')) {
|
|
332
|
+
inputs.topic = { type: 'string', required: true, description: 'Topic to explain' };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Build steps in a sensible order
|
|
336
|
+
const steps = [];
|
|
337
|
+
const output = {};
|
|
338
|
+
|
|
339
|
+
// Data retrieval tools first
|
|
340
|
+
const orderedTools = orderTools(requestedTools);
|
|
341
|
+
let prevArrayStepId = null;
|
|
342
|
+
|
|
343
|
+
for (let i = 0; i < orderedTools.length; i++) {
|
|
344
|
+
const tool = orderedTools[i];
|
|
345
|
+
const stepId = orderedTools.filter((t, j) => j < i && t === tool).length > 0
|
|
346
|
+
? `${tool}_step_${i}`
|
|
347
|
+
: `${tool}_step`;
|
|
348
|
+
|
|
349
|
+
const step = buildStepForTool(tool, stepId, { inputs, steps }, prevArrayStepId);
|
|
350
|
+
steps.push(step);
|
|
351
|
+
|
|
352
|
+
// Track steps that output arrays for chaining
|
|
353
|
+
if (['query', 'search', 'merge', 'filter', 'transform', 'rerank'].includes(tool)) {
|
|
354
|
+
prevArrayStepId = stepId;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Build output from last step(s)
|
|
359
|
+
if (steps.length > 0) {
|
|
360
|
+
const lastStep = steps[steps.length - 1];
|
|
361
|
+
if (lastStep.tool === 'generate') {
|
|
362
|
+
output.response = `{{ ${lastStep.id}.output.response }}`;
|
|
363
|
+
} else if (['query', 'search', 'rerank', 'merge', 'filter', 'transform'].includes(lastStep.tool)) {
|
|
364
|
+
output.results = `{{ ${lastStep.id}.output.results }}`;
|
|
365
|
+
} else {
|
|
366
|
+
output.result = `{{ ${lastStep.id}.output }}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const workflow = {
|
|
371
|
+
name: slug,
|
|
372
|
+
description,
|
|
373
|
+
version: '1.0.0',
|
|
374
|
+
inputs,
|
|
375
|
+
defaults: {},
|
|
376
|
+
steps,
|
|
377
|
+
output,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (category) {
|
|
381
|
+
workflow.category = category;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return workflow;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Build a single step definition for a given tool.
|
|
389
|
+
*/
|
|
390
|
+
function buildStepForTool(tool, stepId, workflowContext, prevArrayStepId) {
|
|
391
|
+
const inputs = workflowContext.inputs || {};
|
|
392
|
+
const step = {
|
|
393
|
+
id: stepId,
|
|
394
|
+
tool,
|
|
395
|
+
name: humanizeTool(tool),
|
|
396
|
+
inputs: {},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
switch (tool) {
|
|
400
|
+
case 'query':
|
|
401
|
+
step.inputs = { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' };
|
|
402
|
+
break;
|
|
403
|
+
case 'search':
|
|
404
|
+
step.inputs = { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' };
|
|
405
|
+
break;
|
|
406
|
+
case 'rerank':
|
|
407
|
+
step.inputs = {
|
|
408
|
+
query: '{{ inputs.query }}',
|
|
409
|
+
documents: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : '{{ inputs.documents }}',
|
|
410
|
+
model: 'rerank-2.5',
|
|
411
|
+
};
|
|
412
|
+
break;
|
|
413
|
+
case 'embed':
|
|
414
|
+
step.inputs = { text: '{{ inputs.text }}' };
|
|
415
|
+
break;
|
|
416
|
+
case 'similarity':
|
|
417
|
+
step.inputs = { text1: '{{ inputs.text1 }}', text2: '{{ inputs.text2 }}' };
|
|
418
|
+
break;
|
|
419
|
+
case 'ingest':
|
|
420
|
+
step.inputs = { text: '{{ inputs.text }}', collection: '{{ inputs.collection }}' };
|
|
421
|
+
break;
|
|
422
|
+
case 'chunk':
|
|
423
|
+
step.inputs = { text: '{{ inputs.text }}', strategy: 'recursive', size: 512 };
|
|
424
|
+
break;
|
|
425
|
+
case 'generate':
|
|
426
|
+
step.inputs = {
|
|
427
|
+
prompt: `Based on the provided context, ${workflowContext.inputs?.query ? 'answer the question: {{ inputs.query }}' : 'generate a response.'}`,
|
|
428
|
+
};
|
|
429
|
+
if (prevArrayStepId) {
|
|
430
|
+
step.inputs.context = `{{ ${prevArrayStepId}.output.results }}`;
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
case 'merge':
|
|
434
|
+
step.inputs = { arrays: prevArrayStepId ? [`{{ ${prevArrayStepId}.output.results }}`] : [], dedup: true };
|
|
435
|
+
break;
|
|
436
|
+
case 'filter':
|
|
437
|
+
step.inputs = {
|
|
438
|
+
array: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : [],
|
|
439
|
+
condition: 'item.score > 0.5',
|
|
440
|
+
};
|
|
441
|
+
break;
|
|
442
|
+
case 'transform':
|
|
443
|
+
step.inputs = {
|
|
444
|
+
array: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : [],
|
|
445
|
+
fields: ['text', 'score'],
|
|
446
|
+
};
|
|
447
|
+
break;
|
|
448
|
+
case 'template':
|
|
449
|
+
step.inputs = { text: 'Workflow result summary' };
|
|
450
|
+
break;
|
|
451
|
+
case 'conditional':
|
|
452
|
+
step.inputs = { condition: 'true', then: [], else: [] };
|
|
453
|
+
break;
|
|
454
|
+
case 'loop':
|
|
455
|
+
step.inputs = {
|
|
456
|
+
items: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : [],
|
|
457
|
+
as: 'doc',
|
|
458
|
+
step: { tool: 'embed', inputs: { text: '{{ doc.text }}' } },
|
|
459
|
+
};
|
|
460
|
+
break;
|
|
461
|
+
case 'http':
|
|
462
|
+
step.inputs = { url: '{{ inputs.url }}', method: 'GET' };
|
|
463
|
+
break;
|
|
464
|
+
case 'estimate':
|
|
465
|
+
step.inputs = { docs: '{{ inputs.docs }}', queries: '{{ inputs.queries }}', months: 12 };
|
|
466
|
+
break;
|
|
467
|
+
case 'models':
|
|
468
|
+
step.inputs = { category: 'all' };
|
|
469
|
+
break;
|
|
470
|
+
case 'collections':
|
|
471
|
+
step.inputs = {};
|
|
472
|
+
break;
|
|
473
|
+
case 'explain':
|
|
474
|
+
step.inputs = { topic: '{{ inputs.topic }}' };
|
|
475
|
+
break;
|
|
476
|
+
case 'aggregate':
|
|
477
|
+
step.inputs = { collection: '{{ inputs.collection }}', pipeline: [] };
|
|
478
|
+
break;
|
|
479
|
+
case 'code_index':
|
|
480
|
+
step.inputs = { source: '{{ inputs.source }}' };
|
|
481
|
+
break;
|
|
482
|
+
case 'code_search':
|
|
483
|
+
step.inputs = { query: '{{ inputs.query }}' };
|
|
484
|
+
break;
|
|
485
|
+
case 'code_query':
|
|
486
|
+
step.inputs = { query: '{{ inputs.query }}' };
|
|
487
|
+
break;
|
|
488
|
+
case 'code_find_similar':
|
|
489
|
+
step.inputs = { code: '{{ inputs.code }}' };
|
|
490
|
+
break;
|
|
491
|
+
case 'code_status':
|
|
492
|
+
step.inputs = {};
|
|
493
|
+
break;
|
|
494
|
+
default:
|
|
495
|
+
step.inputs = TOOL_DEFAULTS[tool] || {};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return step;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Order tools in a sensible execution sequence.
|
|
503
|
+
*/
|
|
504
|
+
function orderTools(tools) {
|
|
505
|
+
const order = [
|
|
506
|
+
'collections', 'models', 'code_status',
|
|
507
|
+
'http',
|
|
508
|
+
'chunk', 'code_index',
|
|
509
|
+
'ingest',
|
|
510
|
+
'embed',
|
|
511
|
+
'query', 'search', 'code_search', 'code_query', 'code_find_similar',
|
|
512
|
+
'similarity',
|
|
513
|
+
'merge',
|
|
514
|
+
'filter', 'transform',
|
|
515
|
+
'rerank',
|
|
516
|
+
'aggregate',
|
|
517
|
+
'estimate', 'explain',
|
|
518
|
+
'template',
|
|
519
|
+
'conditional', 'loop',
|
|
520
|
+
'generate',
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
const orderMap = new Map(order.map((t, i) => [t, i]));
|
|
524
|
+
return [...tools].sort((a, b) => (orderMap.get(a) || 50) - (orderMap.get(b) || 50));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Create a human-readable name for a tool.
|
|
529
|
+
*/
|
|
530
|
+
function humanizeTool(tool) {
|
|
531
|
+
const names = {
|
|
532
|
+
query: 'Query documents', search: 'Vector search', rerank: 'Rerank results',
|
|
533
|
+
embed: 'Generate embedding', similarity: 'Compare similarity',
|
|
534
|
+
ingest: 'Ingest documents', chunk: 'Chunk text', generate: 'Generate response',
|
|
535
|
+
merge: 'Merge results', filter: 'Filter results', transform: 'Transform data',
|
|
536
|
+
template: 'Compose text', conditional: 'Conditional branch', loop: 'Loop over items',
|
|
537
|
+
http: 'HTTP request', estimate: 'Cost estimate', models: 'List models',
|
|
538
|
+
collections: 'List collections', explain: 'Explain topic', aggregate: 'Aggregation pipeline',
|
|
539
|
+
code_index: 'Index codebase', code_search: 'Search code', code_query: 'Query codebase',
|
|
540
|
+
code_find_similar: 'Find similar code', code_status: 'Code index status',
|
|
541
|
+
};
|
|
542
|
+
return names[tool] || tool;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Slugify a description into a workflow name.
|
|
547
|
+
*/
|
|
548
|
+
function slugify(text) {
|
|
549
|
+
return text
|
|
550
|
+
.toLowerCase()
|
|
551
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
552
|
+
.replace(/^-|-$/g, '')
|
|
553
|
+
.slice(0, 50);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ════════════════════════════════════════════════════════════════════
|
|
557
|
+
// Validate workflow tool
|
|
558
|
+
// ════════════════════════════════════════════════════════════════════
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Validate a workflow definition and return structured results.
|
|
562
|
+
*
|
|
563
|
+
* @param {object} params
|
|
564
|
+
* @param {object} params.workflow - The workflow JSON definition
|
|
565
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[], layers: string[][], dependencyGraph: object }}
|
|
566
|
+
*/
|
|
567
|
+
function validateWorkflowTool({ workflow }) {
|
|
568
|
+
const errors = validateWorkflow(workflow);
|
|
569
|
+
let layers = [];
|
|
570
|
+
let dependencyGraph = {};
|
|
571
|
+
|
|
572
|
+
if (errors.length === 0 && Array.isArray(workflow.steps) && workflow.steps.length > 0) {
|
|
573
|
+
layers = buildExecutionPlan(workflow.steps);
|
|
574
|
+
const graph = buildDependencyGraph(workflow.steps);
|
|
575
|
+
for (const [stepId, deps] of graph) {
|
|
576
|
+
dependencyGraph[stepId] = [...deps];
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Separate warnings (non-blocking) from errors
|
|
581
|
+
// For now, validateWorkflow returns all as errors in strict mode
|
|
582
|
+
const warnings = [];
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
valid: errors.length === 0,
|
|
586
|
+
errors,
|
|
587
|
+
warnings,
|
|
588
|
+
layers,
|
|
589
|
+
dependencyGraph,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ════════════════════════════════════════════════════════════════════
|
|
594
|
+
// MCP Tool Registration
|
|
595
|
+
// ════════════════════════════════════════════════════════════════════
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Handler for vai_generate_workflow MCP tool.
|
|
599
|
+
*/
|
|
600
|
+
async function handleGenerateWorkflow(input) {
|
|
601
|
+
const result = generateWorkflow(input);
|
|
602
|
+
|
|
603
|
+
const summary = result.validation.valid
|
|
604
|
+
? `Generated valid workflow "${result.workflow.name}" with ${result.workflow.steps.length} steps across ${result.executionPlan.length} execution layers.`
|
|
605
|
+
: `Generated workflow "${result.workflow.name}" with ${result.validation.errors.length} validation error(s). Review and fix the errors before running.`;
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
structuredContent: result,
|
|
609
|
+
content: [{
|
|
610
|
+
type: 'text',
|
|
611
|
+
text: `${summary}\n\nWorkflow JSON:\n${JSON.stringify(result.workflow, null, 2)}\n\nExecution Plan: ${JSON.stringify(result.executionPlan)}\n\nValidation: ${result.validation.valid ? 'PASSED' : 'FAILED - ' + result.validation.errors.join('; ')}`,
|
|
612
|
+
}],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Handler for vai_validate_workflow MCP tool.
|
|
618
|
+
*/
|
|
619
|
+
async function handleValidateWorkflow(input) {
|
|
620
|
+
const result = validateWorkflowTool(input);
|
|
621
|
+
|
|
622
|
+
const summary = result.valid
|
|
623
|
+
? `Workflow is valid. ${result.layers.length} execution layer(s), ${Object.keys(result.dependencyGraph).length} step(s).`
|
|
624
|
+
: `Workflow has ${result.errors.length} error(s).`;
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
structuredContent: result,
|
|
628
|
+
content: [{
|
|
629
|
+
type: 'text',
|
|
630
|
+
text: `${summary}\n\nErrors: ${result.errors.length > 0 ? result.errors.join('\n') : 'None'}\nWarnings: ${result.warnings.length > 0 ? result.warnings.join('\n') : 'None'}\nExecution Layers: ${JSON.stringify(result.layers)}\nDependency Graph: ${JSON.stringify(result.dependencyGraph, null, 2)}`,
|
|
631
|
+
}],
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Register authoring tools on the MCP server.
|
|
637
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
638
|
+
* @param {object} schemas
|
|
639
|
+
*/
|
|
640
|
+
function registerAuthoringTools(server, schemas) {
|
|
641
|
+
server.tool(
|
|
642
|
+
'vai_generate_workflow',
|
|
643
|
+
'Generate a complete, executable vai workflow JSON from a natural language description. Returns the workflow definition, validation results, and execution plan. The generated workflow uses template expressions for step inputs and follows all vai workflow conventions.',
|
|
644
|
+
schemas.generateWorkflowSchema,
|
|
645
|
+
handleGenerateWorkflow
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
server.tool(
|
|
649
|
+
'vai_validate_workflow',
|
|
650
|
+
'Validate a vai workflow JSON definition. Checks for structural errors, unknown tools, circular dependencies, and missing references. Returns validation errors, warnings, execution plan layers, and the dependency graph.',
|
|
651
|
+
schemas.validateWorkflowSchema,
|
|
652
|
+
handleValidateWorkflow
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
module.exports = {
|
|
657
|
+
registerAuthoringTools,
|
|
658
|
+
handleGenerateWorkflow,
|
|
659
|
+
handleValidateWorkflow,
|
|
660
|
+
generateWorkflow,
|
|
661
|
+
validateWorkflowTool,
|
|
662
|
+
};
|