@ema.co/mcp-toolkit 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/dist/mcp/handlers-consolidated.js +400 -14
- package/dist/mcp/prompts.js +80 -123
- package/dist/mcp/server.js +134 -209
- package/dist/mcp/tools-consolidated.js +212 -150
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/client.js +58 -90
- package/dist/sdk/demo-generator.js +978 -0
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +15 -1
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +187 -39
- package/dist/sdk/workflow-intent.js +246 -24
- package/dist/sdk/workflow-optimizer.js +665 -0
- package/dist/sdk/workflow-tracer.js +648 -0
- package/dist/sdk/workflow-transformer.js +10 -0
- package/dist/sdk/workflow-validator.js +391 -0
- package/docs/.temp/datasource-attach.har +198369 -0
- package/docs/.temp/grpcweb.gar +1 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +163 -197
- package/docs/openapi.json +8000 -0
- package/docs/release-process.md +153 -0
- package/docs/test-persona-creation.md +196 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +3 -1
- package/resources/templates/demo-scenarios/README.md +63 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Optimizer - Proactive Intelligence Engine
|
|
3
|
+
*
|
|
4
|
+
* Goes beyond issue detection to find optimization opportunities,
|
|
5
|
+
* suggest improvements, and calculate workflow health scores.
|
|
6
|
+
* This is the "brains" that makes workflows better.
|
|
7
|
+
*/
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Main Entry Point
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Analyze workflow for optimization opportunities
|
|
13
|
+
*/
|
|
14
|
+
export function analyzeOptimizations(workflow) {
|
|
15
|
+
const actions = workflow.actions || [];
|
|
16
|
+
const graph = buildDependencyGraph(actions);
|
|
17
|
+
const metrics = calculateMetrics(actions, graph);
|
|
18
|
+
const optimizations = [];
|
|
19
|
+
// 1. Find consolidation opportunities (duplicate/redundant nodes)
|
|
20
|
+
optimizations.push(...findConsolidationOpportunities(actions, graph));
|
|
21
|
+
// 2. Find parallelization opportunities
|
|
22
|
+
optimizations.push(...findParallelizationOpportunities(actions, graph));
|
|
23
|
+
// 3. Find passthrough nodes to eliminate
|
|
24
|
+
optimizations.push(...findPassthroughNodes(actions, graph));
|
|
25
|
+
// 4. Find error handling gaps
|
|
26
|
+
optimizations.push(...findErrorHandlingGaps(actions, graph));
|
|
27
|
+
// 5. Find LLM call optimizations
|
|
28
|
+
optimizations.push(...findLlmOptimizations(actions, graph));
|
|
29
|
+
// 6. Find search optimizations
|
|
30
|
+
optimizations.push(...findSearchOptimizations(actions, graph));
|
|
31
|
+
// 7. Find naming improvements
|
|
32
|
+
optimizations.push(...findNamingImprovements(actions));
|
|
33
|
+
// 8. Find safety improvements (HITL for side effects)
|
|
34
|
+
optimizations.push(...findSafetyImprovements(actions, graph));
|
|
35
|
+
// Calculate health score
|
|
36
|
+
const healthScore = calculateWorkflowHealth(actions, metrics, optimizations);
|
|
37
|
+
// Categorize optimizations
|
|
38
|
+
const quickWins = optimizations.filter(o => o.auto_applicable && o.risk === "none" && o.severity !== "suggestion");
|
|
39
|
+
const strategic = optimizations.filter(o => !o.auto_applicable || o.risk !== "none");
|
|
40
|
+
// Generate recommendations
|
|
41
|
+
const recommendations = generateRecommendations(optimizations, metrics, healthScore);
|
|
42
|
+
return {
|
|
43
|
+
workflow_health_score: healthScore,
|
|
44
|
+
grade: healthScoreToGrade(healthScore),
|
|
45
|
+
optimizations,
|
|
46
|
+
quick_wins: quickWins,
|
|
47
|
+
strategic,
|
|
48
|
+
metrics,
|
|
49
|
+
recommendations,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// Graph Building
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
function buildDependencyGraph(actions) {
|
|
56
|
+
const nodes = new Map();
|
|
57
|
+
const edges = new Map();
|
|
58
|
+
const reverseEdges = new Map();
|
|
59
|
+
// Initialize nodes
|
|
60
|
+
for (const action of actions) {
|
|
61
|
+
const actionType = action.action?.name?.name || "unknown";
|
|
62
|
+
nodes.set(action.name, {
|
|
63
|
+
id: action.name,
|
|
64
|
+
type: actionType,
|
|
65
|
+
depth: 0,
|
|
66
|
+
inputs: [],
|
|
67
|
+
outputs: [],
|
|
68
|
+
});
|
|
69
|
+
edges.set(action.name, new Set());
|
|
70
|
+
reverseEdges.set(action.name, new Set());
|
|
71
|
+
}
|
|
72
|
+
// Build edges from input bindings
|
|
73
|
+
for (const action of actions) {
|
|
74
|
+
const node = nodes.get(action.name);
|
|
75
|
+
if (action.inputs) {
|
|
76
|
+
for (const [inputName, binding] of Object.entries(action.inputs)) {
|
|
77
|
+
const bindingObj = binding;
|
|
78
|
+
const actionOutput = bindingObj?.actionOutput;
|
|
79
|
+
if (actionOutput?.actionName) {
|
|
80
|
+
const sourceNode = String(actionOutput.actionName);
|
|
81
|
+
node.inputs.push(sourceNode);
|
|
82
|
+
// Source → Target edge
|
|
83
|
+
edges.get(sourceNode)?.add(action.name);
|
|
84
|
+
reverseEdges.get(action.name)?.add(sourceNode);
|
|
85
|
+
// Update source node outputs
|
|
86
|
+
const sourceGraphNode = nodes.get(sourceNode);
|
|
87
|
+
if (sourceGraphNode) {
|
|
88
|
+
sourceGraphNode.outputs.push(action.name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Calculate depths (BFS from trigger)
|
|
95
|
+
const trigger = actions.find(a => a.action?.name?.name?.includes("trigger"));
|
|
96
|
+
if (trigger) {
|
|
97
|
+
const visited = new Set();
|
|
98
|
+
const queue = [{ node: trigger.name, depth: 0 }];
|
|
99
|
+
while (queue.length > 0) {
|
|
100
|
+
const { node, depth } = queue.shift();
|
|
101
|
+
if (visited.has(node))
|
|
102
|
+
continue;
|
|
103
|
+
visited.add(node);
|
|
104
|
+
const graphNode = nodes.get(node);
|
|
105
|
+
if (graphNode) {
|
|
106
|
+
graphNode.depth = depth;
|
|
107
|
+
}
|
|
108
|
+
for (const child of edges.get(node) || []) {
|
|
109
|
+
if (!visited.has(child)) {
|
|
110
|
+
queue.push({ node: child, depth: depth + 1 });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { nodes, edges, reverseEdges };
|
|
116
|
+
}
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
// Metrics Calculation
|
|
119
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
+
function calculateMetrics(actions, graph) {
|
|
121
|
+
// Count node types
|
|
122
|
+
const nodeTypes = {};
|
|
123
|
+
for (const action of actions) {
|
|
124
|
+
const type = action.action?.name?.name || "unknown";
|
|
125
|
+
nodeTypes[type] = (nodeTypes[type] || 0) + 1;
|
|
126
|
+
}
|
|
127
|
+
// Calculate max depth
|
|
128
|
+
const maxDepth = Math.max(...Array.from(graph.nodes.values()).map(n => n.depth), 0);
|
|
129
|
+
// Count branches (nodes with multiple outputs)
|
|
130
|
+
const branchCount = Array.from(graph.nodes.values()).filter(n => n.outputs.length > 1).length;
|
|
131
|
+
// Count parallel opportunities (nodes at same depth that could run together)
|
|
132
|
+
const nodesByDepth = new Map();
|
|
133
|
+
for (const [id, node] of graph.nodes) {
|
|
134
|
+
const nodes = nodesByDepth.get(node.depth) || [];
|
|
135
|
+
nodes.push(id);
|
|
136
|
+
nodesByDepth.set(node.depth, nodes);
|
|
137
|
+
}
|
|
138
|
+
const parallelOpportunities = Array.from(nodesByDepth.values())
|
|
139
|
+
.filter(nodes => nodes.length > 1).length;
|
|
140
|
+
// Calculate redundancy score
|
|
141
|
+
const redundancyScore = calculateRedundancyScore(actions, graph);
|
|
142
|
+
// Calculate complexity score
|
|
143
|
+
const complexityScore = calculateComplexityScore(actions, graph, maxDepth, branchCount);
|
|
144
|
+
// Count sequential latency factors (LLM + search calls on critical path)
|
|
145
|
+
const latencyFactors = countLatencyFactors(actions, graph);
|
|
146
|
+
return {
|
|
147
|
+
total_nodes: actions.length,
|
|
148
|
+
node_types: nodeTypes,
|
|
149
|
+
max_depth: maxDepth,
|
|
150
|
+
branch_count: branchCount,
|
|
151
|
+
parallel_opportunities: parallelOpportunities,
|
|
152
|
+
redundancy_score: redundancyScore,
|
|
153
|
+
complexity_score: complexityScore,
|
|
154
|
+
estimated_latency_factors: latencyFactors,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function calculateRedundancyScore(actions, graph) {
|
|
158
|
+
let redundancy = 0;
|
|
159
|
+
// Check for duplicate node types with same inputs
|
|
160
|
+
const nodeSignatures = new Map();
|
|
161
|
+
for (const action of actions) {
|
|
162
|
+
const type = action.action?.name?.name || "unknown";
|
|
163
|
+
const node = graph.nodes.get(action.name);
|
|
164
|
+
const signature = `${type}:${node?.inputs.sort().join(",")}`;
|
|
165
|
+
const existing = nodeSignatures.get(signature) || [];
|
|
166
|
+
existing.push(action.name);
|
|
167
|
+
nodeSignatures.set(signature, existing);
|
|
168
|
+
}
|
|
169
|
+
// Each duplicate adds to redundancy
|
|
170
|
+
for (const [_, nodes] of nodeSignatures) {
|
|
171
|
+
if (nodes.length > 1) {
|
|
172
|
+
redundancy += (nodes.length - 1) * 10;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return Math.min(redundancy, 100);
|
|
176
|
+
}
|
|
177
|
+
function calculateComplexityScore(actions, graph, maxDepth, branchCount) {
|
|
178
|
+
// Base complexity from node count
|
|
179
|
+
let complexity = Math.min(actions.length * 5, 30);
|
|
180
|
+
// Add complexity for depth
|
|
181
|
+
complexity += Math.min(maxDepth * 5, 20);
|
|
182
|
+
// Add complexity for branching
|
|
183
|
+
complexity += Math.min(branchCount * 10, 30);
|
|
184
|
+
// Add complexity for conditional logic
|
|
185
|
+
const conditionalNodes = actions.filter(a => a.runIf).length;
|
|
186
|
+
complexity += Math.min(conditionalNodes * 5, 20);
|
|
187
|
+
return Math.min(complexity, 100);
|
|
188
|
+
}
|
|
189
|
+
function countLatencyFactors(actions, graph) {
|
|
190
|
+
// Count LLM and search nodes on the longest path
|
|
191
|
+
const latencyNodes = actions.filter(a => {
|
|
192
|
+
const type = a.action?.name?.name || "";
|
|
193
|
+
return type.includes("llm") ||
|
|
194
|
+
type.includes("respond") ||
|
|
195
|
+
type.includes("search") ||
|
|
196
|
+
type.includes("categorizer");
|
|
197
|
+
});
|
|
198
|
+
// Find the longest sequential chain of latency nodes
|
|
199
|
+
let maxSequential = 0;
|
|
200
|
+
for (const node of latencyNodes) {
|
|
201
|
+
let count = 1;
|
|
202
|
+
let current = node.name;
|
|
203
|
+
// Follow the dependency chain
|
|
204
|
+
while (true) {
|
|
205
|
+
const graphNode = graph.nodes.get(current);
|
|
206
|
+
if (!graphNode || graphNode.outputs.length === 0)
|
|
207
|
+
break;
|
|
208
|
+
// Find a latency node in outputs
|
|
209
|
+
const nextLatency = graphNode.outputs.find(out => latencyNodes.some(ln => ln.name === out));
|
|
210
|
+
if (!nextLatency)
|
|
211
|
+
break;
|
|
212
|
+
count++;
|
|
213
|
+
current = nextLatency;
|
|
214
|
+
}
|
|
215
|
+
maxSequential = Math.max(maxSequential, count);
|
|
216
|
+
}
|
|
217
|
+
return maxSequential;
|
|
218
|
+
}
|
|
219
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
// Optimization Finders
|
|
221
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
222
|
+
function findConsolidationOpportunities(actions, graph) {
|
|
223
|
+
const opportunities = [];
|
|
224
|
+
// Find duplicate search nodes with same query source
|
|
225
|
+
const searchNodes = actions.filter(a => a.action?.name?.name?.includes("search") &&
|
|
226
|
+
!a.action?.name?.name?.includes("combine"));
|
|
227
|
+
const searchByQuery = new Map();
|
|
228
|
+
for (const search of searchNodes) {
|
|
229
|
+
const querySource = getQuerySource(search);
|
|
230
|
+
const existing = searchByQuery.get(querySource) || [];
|
|
231
|
+
existing.push(search);
|
|
232
|
+
searchByQuery.set(querySource, existing);
|
|
233
|
+
}
|
|
234
|
+
for (const [query, duplicates] of searchByQuery) {
|
|
235
|
+
if (duplicates.length > 1) {
|
|
236
|
+
opportunities.push({
|
|
237
|
+
id: `consolidate-search-${hashString(query)}`,
|
|
238
|
+
type: "consolidate_nodes",
|
|
239
|
+
severity: "recommended",
|
|
240
|
+
title: "Consolidate Redundant Searches",
|
|
241
|
+
current_state: `${duplicates.length} search nodes using same query source: "${query}"`,
|
|
242
|
+
proposed_change: "Consolidate into single search node, share results via named_inputs",
|
|
243
|
+
benefit: `Reduce API calls by ${duplicates.length - 1}x, faster execution, consistent results`,
|
|
244
|
+
risk: "low",
|
|
245
|
+
auto_applicable: false, // Requires careful rewiring
|
|
246
|
+
affected_nodes: duplicates.map(d => d.name),
|
|
247
|
+
implementation_hint: "Keep one search, remove others, use named_inputs to pass results to all consumers",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Find duplicate LLM calls with similar prompts
|
|
252
|
+
const llmNodes = actions.filter(a => a.action?.name?.name?.includes("llm") ||
|
|
253
|
+
a.action?.name?.name?.includes("respond"));
|
|
254
|
+
if (llmNodes.length > 2) {
|
|
255
|
+
// Check for sequential LLM calls
|
|
256
|
+
for (const node of llmNodes) {
|
|
257
|
+
const graphNode = graph.nodes.get(node.name);
|
|
258
|
+
if (graphNode) {
|
|
259
|
+
const llmChildren = graphNode.outputs.filter(out => llmNodes.some(ln => ln.name === out));
|
|
260
|
+
if (llmChildren.length > 0) {
|
|
261
|
+
opportunities.push({
|
|
262
|
+
id: `merge-llm-${node.name}`,
|
|
263
|
+
type: "merge_llm_calls",
|
|
264
|
+
severity: "suggestion",
|
|
265
|
+
title: "Consider Merging Sequential LLM Calls",
|
|
266
|
+
current_state: `${node.name} feeds into LLM node(s): ${llmChildren.join(", ")}`,
|
|
267
|
+
proposed_change: "Merge into single comprehensive LLM call with structured prompt",
|
|
268
|
+
benefit: "Reduce latency, potentially lower cost, more coherent output",
|
|
269
|
+
risk: "medium",
|
|
270
|
+
auto_applicable: false,
|
|
271
|
+
affected_nodes: [node.name, ...llmChildren],
|
|
272
|
+
implementation_hint: "Combine prompts, use structured output format, single call_llm with all context",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return opportunities;
|
|
279
|
+
}
|
|
280
|
+
function findParallelizationOpportunities(actions, graph) {
|
|
281
|
+
const opportunities = [];
|
|
282
|
+
// Find nodes at same depth that share a parent but are sequential
|
|
283
|
+
const nodesByParent = new Map();
|
|
284
|
+
for (const [nodeId, graphNode] of graph.nodes) {
|
|
285
|
+
for (const input of graphNode.inputs) {
|
|
286
|
+
const siblings = nodesByParent.get(input) || [];
|
|
287
|
+
siblings.push(nodeId);
|
|
288
|
+
nodesByParent.set(input, siblings);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Check for sequential siblings that could be parallel
|
|
292
|
+
for (const [parent, children] of nodesByParent) {
|
|
293
|
+
if (children.length > 1) {
|
|
294
|
+
// Check if they're actually sequential (one depends on another)
|
|
295
|
+
const sequential = children.filter(child => {
|
|
296
|
+
const node = graph.nodes.get(child);
|
|
297
|
+
return node && children.some(other => other !== child && node.inputs.includes(other));
|
|
298
|
+
});
|
|
299
|
+
if (sequential.length > 0 && sequential.length < children.length) {
|
|
300
|
+
opportunities.push({
|
|
301
|
+
id: `parallelize-${parent}`,
|
|
302
|
+
type: "parallelize_paths",
|
|
303
|
+
severity: "suggestion",
|
|
304
|
+
title: "Parallelization Opportunity",
|
|
305
|
+
current_state: `Children of ${parent} could potentially run in parallel`,
|
|
306
|
+
proposed_change: "Restructure so independent operations branch from same source",
|
|
307
|
+
benefit: "Reduced latency through parallel execution",
|
|
308
|
+
risk: "low",
|
|
309
|
+
auto_applicable: false,
|
|
310
|
+
affected_nodes: [parent, ...children],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return opportunities;
|
|
316
|
+
}
|
|
317
|
+
function findPassthroughNodes(actions, graph) {
|
|
318
|
+
const opportunities = [];
|
|
319
|
+
// Find nodes that just pass data through without transformation
|
|
320
|
+
for (const action of actions) {
|
|
321
|
+
const graphNode = graph.nodes.get(action.name);
|
|
322
|
+
if (!graphNode)
|
|
323
|
+
continue;
|
|
324
|
+
// Skip trigger and output nodes
|
|
325
|
+
const type = action.action?.name?.name || "";
|
|
326
|
+
if (type.includes("trigger") || graphNode.outputs.length === 0)
|
|
327
|
+
continue;
|
|
328
|
+
// Check if this node has single input and single output with no transformation
|
|
329
|
+
if (graphNode.inputs.length === 1 && graphNode.outputs.length === 1) {
|
|
330
|
+
// Check if it's a simple passthrough (no significant configuration)
|
|
331
|
+
const inputCount = Object.keys(action.inputs || {}).length;
|
|
332
|
+
const hasTransformation = type.includes("llm") ||
|
|
333
|
+
type.includes("search") ||
|
|
334
|
+
type.includes("categorizer") ||
|
|
335
|
+
type.includes("extract");
|
|
336
|
+
if (!hasTransformation && inputCount <= 2) {
|
|
337
|
+
opportunities.push({
|
|
338
|
+
id: `eliminate-passthrough-${action.name}`,
|
|
339
|
+
type: "eliminate_passthrough",
|
|
340
|
+
severity: "suggestion",
|
|
341
|
+
title: "Potential Passthrough Node",
|
|
342
|
+
current_state: `${action.name} appears to pass data without significant transformation`,
|
|
343
|
+
proposed_change: "Consider removing and connecting predecessor directly to successor",
|
|
344
|
+
benefit: "Simpler workflow, reduced complexity",
|
|
345
|
+
risk: "low",
|
|
346
|
+
auto_applicable: false,
|
|
347
|
+
affected_nodes: [action.name],
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return opportunities;
|
|
353
|
+
}
|
|
354
|
+
function findErrorHandlingGaps(actions, graph) {
|
|
355
|
+
const opportunities = [];
|
|
356
|
+
// Check for external action callers without error handling
|
|
357
|
+
const externalCallers = actions.filter(a => a.action?.name?.name?.includes("external") ||
|
|
358
|
+
a.action?.name?.name?.includes("email") ||
|
|
359
|
+
a.action?.name?.name?.includes("tool"));
|
|
360
|
+
for (const caller of externalCallers) {
|
|
361
|
+
// Check if there's error handling downstream
|
|
362
|
+
const graphNode = graph.nodes.get(caller.name);
|
|
363
|
+
const hasErrorPath = graphNode?.outputs.some(out => {
|
|
364
|
+
const action = actions.find(a => a.name === out);
|
|
365
|
+
return action?.runIf && JSON.stringify(action.runIf).includes("error");
|
|
366
|
+
});
|
|
367
|
+
if (!hasErrorPath) {
|
|
368
|
+
opportunities.push({
|
|
369
|
+
id: `add-error-handling-${caller.name}`,
|
|
370
|
+
type: "add_error_handling",
|
|
371
|
+
severity: "recommended",
|
|
372
|
+
title: "Add Error Handling",
|
|
373
|
+
current_state: `External action ${caller.name} has no error handling path`,
|
|
374
|
+
proposed_change: "Add conditional path for handling failures gracefully",
|
|
375
|
+
benefit: "Better user experience when external services fail",
|
|
376
|
+
risk: "none",
|
|
377
|
+
auto_applicable: false,
|
|
378
|
+
affected_nodes: [caller.name],
|
|
379
|
+
implementation_hint: "Add runIf condition checking for error status, route to fallback response",
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return opportunities;
|
|
384
|
+
}
|
|
385
|
+
function findLlmOptimizations(actions, graph) {
|
|
386
|
+
const opportunities = [];
|
|
387
|
+
const llmNodes = actions.filter(a => a.action?.name?.name?.includes("llm") ||
|
|
388
|
+
a.action?.name?.name?.includes("respond"));
|
|
389
|
+
// Check for call_llm being used instead of respond_with_sources for search results
|
|
390
|
+
for (const node of llmNodes) {
|
|
391
|
+
const type = node.action?.name?.name || "";
|
|
392
|
+
const graphNode = graph.nodes.get(node.name);
|
|
393
|
+
if (type === "call_llm" && graphNode) {
|
|
394
|
+
// Check if input comes from search
|
|
395
|
+
const hasSearchInput = graphNode.inputs.some(input => {
|
|
396
|
+
const inputAction = actions.find(a => a.name === input);
|
|
397
|
+
return inputAction?.action?.name?.name?.includes("search");
|
|
398
|
+
});
|
|
399
|
+
if (hasSearchInput) {
|
|
400
|
+
opportunities.push({
|
|
401
|
+
id: `use-respond-with-sources-${node.name}`,
|
|
402
|
+
type: "improve_response_quality",
|
|
403
|
+
severity: "recommended",
|
|
404
|
+
title: "Use Purpose-Built Response Node",
|
|
405
|
+
current_state: `${node.name} uses call_llm for search-based responses`,
|
|
406
|
+
proposed_change: "Replace with respond_with_sources for better citation handling",
|
|
407
|
+
benefit: "Better citations, grounded responses, less configuration needed",
|
|
408
|
+
risk: "low",
|
|
409
|
+
auto_applicable: false,
|
|
410
|
+
affected_nodes: [node.name],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return opportunities;
|
|
416
|
+
}
|
|
417
|
+
function findSearchOptimizations(actions, graph) {
|
|
418
|
+
const opportunities = [];
|
|
419
|
+
// Check for web search without KB search
|
|
420
|
+
const hasKbSearch = actions.some(a => a.action?.name?.name === "search" ||
|
|
421
|
+
a.action?.name?.name?.includes("file_search"));
|
|
422
|
+
const hasWebSearch = actions.some(a => a.action?.name?.name?.includes("web_search") ||
|
|
423
|
+
a.action?.name?.name?.includes("live_web"));
|
|
424
|
+
if (hasWebSearch && !hasKbSearch) {
|
|
425
|
+
opportunities.push({
|
|
426
|
+
id: "add-kb-search",
|
|
427
|
+
type: "optimize_search",
|
|
428
|
+
severity: "recommended",
|
|
429
|
+
title: "Add Knowledge Base Search",
|
|
430
|
+
current_state: "Workflow uses web search but no internal KB search",
|
|
431
|
+
proposed_change: "Add KB search as primary source, use web search as supplement",
|
|
432
|
+
benefit: "Faster responses, more controlled content, reduced external dependencies",
|
|
433
|
+
risk: "none",
|
|
434
|
+
auto_applicable: false,
|
|
435
|
+
affected_nodes: actions.filter(a => a.action?.name?.name?.includes("web")).map(a => a.name),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return opportunities;
|
|
439
|
+
}
|
|
440
|
+
function findNamingImprovements(actions) {
|
|
441
|
+
const opportunities = [];
|
|
442
|
+
// Check for generic names
|
|
443
|
+
const genericPatterns = [
|
|
444
|
+
/^node_\d+$/i,
|
|
445
|
+
/^action_\d+$/i,
|
|
446
|
+
/^step_\d+$/i,
|
|
447
|
+
/^untitled/i,
|
|
448
|
+
/^new_/i,
|
|
449
|
+
];
|
|
450
|
+
const poorlyNamed = actions.filter(a => genericPatterns.some(p => p.test(a.name)) ||
|
|
451
|
+
genericPatterns.some(p => p.test(a.actionDisplayName || "")));
|
|
452
|
+
if (poorlyNamed.length > 0) {
|
|
453
|
+
opportunities.push({
|
|
454
|
+
id: "improve-naming",
|
|
455
|
+
type: "improve_naming",
|
|
456
|
+
severity: "suggestion",
|
|
457
|
+
title: "Improve Node Naming",
|
|
458
|
+
current_state: `${poorlyNamed.length} nodes have generic names: ${poorlyNamed.map(n => n.name).join(", ")}`,
|
|
459
|
+
proposed_change: "Use descriptive names that indicate purpose (e.g., 'search_customer_faq', 'respond_billing_inquiry')",
|
|
460
|
+
benefit: "Better maintainability, easier debugging, clearer workflow understanding",
|
|
461
|
+
risk: "none",
|
|
462
|
+
auto_applicable: false,
|
|
463
|
+
affected_nodes: poorlyNamed.map(n => n.name),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return opportunities;
|
|
467
|
+
}
|
|
468
|
+
function findSafetyImprovements(actions, graph) {
|
|
469
|
+
const opportunities = [];
|
|
470
|
+
// Check for external actions without HITL
|
|
471
|
+
const sideEffectNodes = actions.filter(a => {
|
|
472
|
+
const type = a.action?.name?.name || "";
|
|
473
|
+
return type.includes("email") ||
|
|
474
|
+
type.includes("external") ||
|
|
475
|
+
type.includes("create_") ||
|
|
476
|
+
type.includes("update_") ||
|
|
477
|
+
type.includes("delete_");
|
|
478
|
+
});
|
|
479
|
+
const hitlNodes = actions.filter(a => a.action?.name?.name?.includes("hitl"));
|
|
480
|
+
for (const sideEffect of sideEffectNodes) {
|
|
481
|
+
const graphNode = graph.nodes.get(sideEffect.name);
|
|
482
|
+
if (!graphNode)
|
|
483
|
+
continue;
|
|
484
|
+
// Check if there's a HITL node upstream
|
|
485
|
+
const hasUpstreamHitl = graphNode.inputs.some(input => hitlNodes.some(h => h.name === input));
|
|
486
|
+
if (!hasUpstreamHitl) {
|
|
487
|
+
opportunities.push({
|
|
488
|
+
id: `add-hitl-${sideEffect.name}`,
|
|
489
|
+
type: "add_hitl_for_safety",
|
|
490
|
+
severity: "important",
|
|
491
|
+
title: "Add Human Approval Before Side Effect",
|
|
492
|
+
current_state: `${sideEffect.name} can execute without human confirmation`,
|
|
493
|
+
proposed_change: "Add HITL node before this action to confirm before execution",
|
|
494
|
+
benefit: "Prevent unintended external actions, maintain human oversight",
|
|
495
|
+
risk: "none",
|
|
496
|
+
auto_applicable: false,
|
|
497
|
+
affected_nodes: [sideEffect.name],
|
|
498
|
+
implementation_hint: "Insert general_hitl between the previous node and this action, add both success and failure paths",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return opportunities;
|
|
503
|
+
}
|
|
504
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
505
|
+
// Health Score Calculation
|
|
506
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
507
|
+
function calculateWorkflowHealth(actions, metrics, optimizations) {
|
|
508
|
+
let score = 100;
|
|
509
|
+
// Deduct for optimizations based on severity
|
|
510
|
+
for (const opt of optimizations) {
|
|
511
|
+
switch (opt.severity) {
|
|
512
|
+
case "important":
|
|
513
|
+
score -= 15;
|
|
514
|
+
break;
|
|
515
|
+
case "recommended":
|
|
516
|
+
score -= 8;
|
|
517
|
+
break;
|
|
518
|
+
case "suggestion":
|
|
519
|
+
score -= 3;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Deduct for high complexity
|
|
524
|
+
if (metrics.complexity_score > 70) {
|
|
525
|
+
score -= 10;
|
|
526
|
+
}
|
|
527
|
+
else if (metrics.complexity_score > 50) {
|
|
528
|
+
score -= 5;
|
|
529
|
+
}
|
|
530
|
+
// Deduct for high redundancy
|
|
531
|
+
if (metrics.redundancy_score > 30) {
|
|
532
|
+
score -= 10;
|
|
533
|
+
}
|
|
534
|
+
else if (metrics.redundancy_score > 10) {
|
|
535
|
+
score -= 5;
|
|
536
|
+
}
|
|
537
|
+
// Deduct for many sequential latency factors
|
|
538
|
+
if (metrics.estimated_latency_factors > 5) {
|
|
539
|
+
score -= 10;
|
|
540
|
+
}
|
|
541
|
+
else if (metrics.estimated_latency_factors > 3) {
|
|
542
|
+
score -= 5;
|
|
543
|
+
}
|
|
544
|
+
// Bonus for good practices
|
|
545
|
+
const hasTrigger = actions.some(a => a.action?.name?.name?.includes("trigger"));
|
|
546
|
+
const hasResponse = actions.some(a => a.action?.name?.name?.includes("respond") ||
|
|
547
|
+
a.action?.name?.name?.includes("llm"));
|
|
548
|
+
if (hasTrigger && hasResponse) {
|
|
549
|
+
score = Math.min(score + 5, 100);
|
|
550
|
+
}
|
|
551
|
+
return Math.max(score, 0);
|
|
552
|
+
}
|
|
553
|
+
function healthScoreToGrade(score) {
|
|
554
|
+
if (score >= 90)
|
|
555
|
+
return "A";
|
|
556
|
+
if (score >= 80)
|
|
557
|
+
return "B";
|
|
558
|
+
if (score >= 70)
|
|
559
|
+
return "C";
|
|
560
|
+
if (score >= 60)
|
|
561
|
+
return "D";
|
|
562
|
+
return "F";
|
|
563
|
+
}
|
|
564
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
565
|
+
// Recommendations Generator
|
|
566
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
567
|
+
function generateRecommendations(optimizations, metrics, healthScore) {
|
|
568
|
+
const recommendations = [];
|
|
569
|
+
// Top priority optimizations
|
|
570
|
+
const important = optimizations.filter(o => o.severity === "important");
|
|
571
|
+
if (important.length > 0) {
|
|
572
|
+
recommendations.push(`🔴 Address ${important.length} important issue(s): ${important.map(o => o.title).join(", ")}`);
|
|
573
|
+
}
|
|
574
|
+
// Quick wins
|
|
575
|
+
const quickWins = optimizations.filter(o => o.auto_applicable && o.risk === "none");
|
|
576
|
+
if (quickWins.length > 0) {
|
|
577
|
+
recommendations.push(`✅ ${quickWins.length} quick win(s) available for immediate improvement`);
|
|
578
|
+
}
|
|
579
|
+
// Complexity warning
|
|
580
|
+
if (metrics.complexity_score > 60) {
|
|
581
|
+
recommendations.push(`⚠️ High complexity (${metrics.complexity_score}/100). Consider simplifying or breaking into sub-workflows.`);
|
|
582
|
+
}
|
|
583
|
+
// Latency warning
|
|
584
|
+
if (metrics.estimated_latency_factors > 4) {
|
|
585
|
+
recommendations.push(`⏱️ High latency risk: ${metrics.estimated_latency_factors} sequential LLM/search calls. Consider parallelization.`);
|
|
586
|
+
}
|
|
587
|
+
// Redundancy warning
|
|
588
|
+
if (metrics.redundancy_score > 20) {
|
|
589
|
+
recommendations.push(`🔄 Redundancy detected (${metrics.redundancy_score}/100). Consider consolidating duplicate operations.`);
|
|
590
|
+
}
|
|
591
|
+
// Overall assessment
|
|
592
|
+
if (healthScore >= 90) {
|
|
593
|
+
recommendations.push("🌟 Excellent workflow health! Minor optimizations optional.");
|
|
594
|
+
}
|
|
595
|
+
else if (healthScore >= 70) {
|
|
596
|
+
recommendations.push("👍 Good workflow health. Apply recommended optimizations for best results.");
|
|
597
|
+
}
|
|
598
|
+
else if (healthScore >= 50) {
|
|
599
|
+
recommendations.push("⚠️ Workflow needs attention. Apply important fixes first.");
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
recommendations.push("🚨 Workflow needs significant improvements. Review all recommendations.");
|
|
603
|
+
}
|
|
604
|
+
return recommendations;
|
|
605
|
+
}
|
|
606
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
607
|
+
// Utilities
|
|
608
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
609
|
+
function getQuerySource(action) {
|
|
610
|
+
const queryInput = action.inputs?.query;
|
|
611
|
+
const actionOutput = queryInput?.actionOutput;
|
|
612
|
+
if (actionOutput) {
|
|
613
|
+
return `${actionOutput.actionName}.${actionOutput.output}`;
|
|
614
|
+
}
|
|
615
|
+
return "unknown";
|
|
616
|
+
}
|
|
617
|
+
function hashString(str) {
|
|
618
|
+
let hash = 0;
|
|
619
|
+
for (let i = 0; i < str.length; i++) {
|
|
620
|
+
const char = str.charCodeAt(i);
|
|
621
|
+
hash = ((hash << 5) - hash) + char;
|
|
622
|
+
hash = hash & hash;
|
|
623
|
+
}
|
|
624
|
+
return Math.abs(hash).toString(36).substring(0, 8);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get a human-readable summary of the optimization report
|
|
628
|
+
*/
|
|
629
|
+
export function summarizeOptimizationReport(report) {
|
|
630
|
+
const lines = [];
|
|
631
|
+
lines.push(`## Workflow Health Report`);
|
|
632
|
+
lines.push("");
|
|
633
|
+
lines.push(`**Health Score**: ${report.workflow_health_score}/100 (Grade: ${report.grade})`);
|
|
634
|
+
lines.push("");
|
|
635
|
+
lines.push("### Metrics");
|
|
636
|
+
lines.push(`- Nodes: ${report.metrics.total_nodes}`);
|
|
637
|
+
lines.push(`- Max Depth: ${report.metrics.max_depth}`);
|
|
638
|
+
lines.push(`- Branches: ${report.metrics.branch_count}`);
|
|
639
|
+
lines.push(`- Complexity: ${report.metrics.complexity_score}/100`);
|
|
640
|
+
lines.push(`- Redundancy: ${report.metrics.redundancy_score}/100`);
|
|
641
|
+
lines.push(`- Latency Factors: ${report.metrics.estimated_latency_factors}`);
|
|
642
|
+
lines.push("");
|
|
643
|
+
if (report.quick_wins.length > 0) {
|
|
644
|
+
lines.push("### Quick Wins");
|
|
645
|
+
for (const opt of report.quick_wins) {
|
|
646
|
+
lines.push(`- **${opt.title}**: ${opt.proposed_change}`);
|
|
647
|
+
}
|
|
648
|
+
lines.push("");
|
|
649
|
+
}
|
|
650
|
+
if (report.strategic.length > 0) {
|
|
651
|
+
lines.push("### Strategic Improvements");
|
|
652
|
+
for (const opt of report.strategic) {
|
|
653
|
+
const severity = opt.severity === "important" ? "🔴" :
|
|
654
|
+
opt.severity === "recommended" ? "🟡" : "🟢";
|
|
655
|
+
lines.push(`${severity} **${opt.title}**: ${opt.current_state}`);
|
|
656
|
+
lines.push(` → ${opt.proposed_change}`);
|
|
657
|
+
}
|
|
658
|
+
lines.push("");
|
|
659
|
+
}
|
|
660
|
+
lines.push("### Recommendations");
|
|
661
|
+
for (const rec of report.recommendations) {
|
|
662
|
+
lines.push(`- ${rec}`);
|
|
663
|
+
}
|
|
664
|
+
return lines.join("\n");
|
|
665
|
+
}
|