@ema.co/mcp-toolkit 1.5.2 → 1.7.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/README.md +2 -2
- package/dist/mcp/handlers-consolidated.js +773 -25
- package/dist/mcp/resources.js +124 -0
- package/dist/mcp/server.js +13 -205
- package/dist/mcp/tools-consolidated.js +163 -103
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/action-schema-parser.js +379 -0
- package/dist/sdk/client.js +757 -90
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +59 -2
- package/dist/sdk/intent-architect.js +883 -0
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/sanitizer.js +1121 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +88 -34
- package/dist/sdk/workflow-intent.js +237 -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 +609 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +196 -204
- package/docs/release-process.md +153 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +8 -2
- package/resources/action-schema.json +5678 -0
- package/resources/config/gates.json +88 -0
- package/resources/config/gates.schema.json +77 -0
- package/resources/templates/auto-builder-rules.md +222 -0
- package/resources/templates/demo-scenarios/test-published-package.md +116 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Flow Tracer
|
|
3
|
+
*
|
|
4
|
+
* Simulates and visualizes how data flows through a workflow,
|
|
5
|
+
* helping debug and understand execution paths.
|
|
6
|
+
*/
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
// Latency Estimates (based on typical Ema platform metrics)
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
const LATENCY_ESTIMATES = {
|
|
11
|
+
// Trigger - instant
|
|
12
|
+
"chat_trigger": 10,
|
|
13
|
+
"voice_trigger": 10,
|
|
14
|
+
// Categorizers - LLM call
|
|
15
|
+
"chat_categorizer": 800,
|
|
16
|
+
"text_categorizer": 600,
|
|
17
|
+
// Search - depends on index size
|
|
18
|
+
"search": 500,
|
|
19
|
+
"live_web_search": 2000,
|
|
20
|
+
"knowledge_search": 500,
|
|
21
|
+
// LLM calls
|
|
22
|
+
"call_llm": 1500,
|
|
23
|
+
"respond_with_sources": 1200,
|
|
24
|
+
"conversation_to_search_query": 600,
|
|
25
|
+
// Entity extraction
|
|
26
|
+
"entity_extraction": 800,
|
|
27
|
+
"entity_extraction_with_documents": 1000,
|
|
28
|
+
// HITL - user dependent, estimate 30s average
|
|
29
|
+
"general_hitl": 30000,
|
|
30
|
+
"human_collaboration": 30000,
|
|
31
|
+
// External actions
|
|
32
|
+
"send_email_agent": 1000,
|
|
33
|
+
"external_action_caller": 2000,
|
|
34
|
+
// Quick operations
|
|
35
|
+
"json_mapper": 50,
|
|
36
|
+
"fixed_response": 50,
|
|
37
|
+
"custom_agent": 500,
|
|
38
|
+
// Default
|
|
39
|
+
"default": 500,
|
|
40
|
+
};
|
|
41
|
+
function estimateLatency(actionType) {
|
|
42
|
+
const normalized = actionType.toLowerCase();
|
|
43
|
+
for (const [pattern, latency] of Object.entries(LATENCY_ESTIMATES)) {
|
|
44
|
+
if (normalized.includes(pattern)) {
|
|
45
|
+
return latency;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return LATENCY_ESTIMATES.default;
|
|
49
|
+
}
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Type Detection
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
const TYPE_NAMES = {
|
|
54
|
+
"CHAT_CONVERSATION": "Chat History",
|
|
55
|
+
"TEXT_WITH_SOURCES": "Text + Sources",
|
|
56
|
+
"WELL_KNOWN_TYPE_ANY": "Any",
|
|
57
|
+
"DOCUMENT": "Document",
|
|
58
|
+
"FILE_PATH": "File Path",
|
|
59
|
+
"EMAIL": "Email",
|
|
60
|
+
"JSON": "JSON Object",
|
|
61
|
+
};
|
|
62
|
+
function friendlyTypeName(type) {
|
|
63
|
+
return TYPE_NAMES[type] || type.replace(/WELL_KNOWN_TYPE_/g, "").replace(/_/g, " ");
|
|
64
|
+
}
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Sample Values (for simulation)
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
const SAMPLE_VALUES = {
|
|
69
|
+
"chat_conversation": '{"messages": [{"role": "user", "content": "What is my account balance?"}]}',
|
|
70
|
+
"user_query": "What is my account balance?",
|
|
71
|
+
"summarized_conversation": "User is asking about their account balance",
|
|
72
|
+
"search_results": '[{"title": "Account FAQ", "content": "...", "score": 0.95}]',
|
|
73
|
+
"response_with_sources": "Your account balance is $1,234.56 [Source: Account System]",
|
|
74
|
+
"email_address": "customer@example.com",
|
|
75
|
+
"email_body": "Dear Customer, Your account balance is $1,234.56...",
|
|
76
|
+
"hitl_status": "HITL Success",
|
|
77
|
+
"category": "Account Inquiry",
|
|
78
|
+
};
|
|
79
|
+
function getSampleValue(outputName) {
|
|
80
|
+
const normalized = outputName.toLowerCase().replace(/_/g, "");
|
|
81
|
+
for (const [pattern, value] of Object.entries(SAMPLE_VALUES)) {
|
|
82
|
+
if (normalized.includes(pattern.toLowerCase().replace(/_/g, ""))) {
|
|
83
|
+
return value.length > 50 ? value.slice(0, 47) + "..." : value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return `<${outputName}>`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Trace execution flow through a workflow
|
|
90
|
+
*/
|
|
91
|
+
export function traceWorkflow(workflowDef) {
|
|
92
|
+
const workflow = workflowDef;
|
|
93
|
+
const actions = workflow.actions || [];
|
|
94
|
+
// Build dependency graph
|
|
95
|
+
const graph = buildDependencyGraph(actions);
|
|
96
|
+
// Find all paths from trigger to outputs
|
|
97
|
+
const paths = findAllPaths(actions, graph, workflow.results || {});
|
|
98
|
+
// Calculate stats
|
|
99
|
+
const stats = calculateStats(paths, actions);
|
|
100
|
+
// Generate visualizations
|
|
101
|
+
const ascii = generateAsciiFlow(paths);
|
|
102
|
+
const mermaid = generateMermaidDiagram(actions, graph);
|
|
103
|
+
// Detect trigger type
|
|
104
|
+
const trigger = actions.find(a => a.action?.name?.name?.includes("trigger") || a.name?.includes("trigger"));
|
|
105
|
+
const triggerType = trigger?.action?.name?.name?.includes("voice") ? "voice" : "chat";
|
|
106
|
+
return {
|
|
107
|
+
workflowName: "Workflow",
|
|
108
|
+
triggerType,
|
|
109
|
+
paths,
|
|
110
|
+
stats,
|
|
111
|
+
ascii,
|
|
112
|
+
mermaid,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function buildDependencyGraph(actions) {
|
|
116
|
+
const dependencies = new Map();
|
|
117
|
+
const dependents = new Map();
|
|
118
|
+
const outputs = new Map();
|
|
119
|
+
const conditions = new Map();
|
|
120
|
+
for (const action of actions) {
|
|
121
|
+
const nodeName = action.name;
|
|
122
|
+
dependencies.set(nodeName, new Set());
|
|
123
|
+
if (!dependents.has(nodeName)) {
|
|
124
|
+
dependents.set(nodeName, new Set());
|
|
125
|
+
}
|
|
126
|
+
// Parse inputs to find dependencies
|
|
127
|
+
if (action.inputs) {
|
|
128
|
+
for (const [inputName, binding] of Object.entries(action.inputs)) {
|
|
129
|
+
const bindingObj = binding;
|
|
130
|
+
const actionOutput = bindingObj?.actionOutput;
|
|
131
|
+
if (actionOutput?.actionName) {
|
|
132
|
+
dependencies.get(nodeName).add(actionOutput.actionName);
|
|
133
|
+
if (!dependents.has(actionOutput.actionName)) {
|
|
134
|
+
dependents.set(actionOutput.actionName, new Set());
|
|
135
|
+
}
|
|
136
|
+
dependents.get(actionOutput.actionName).add(nodeName);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Parse runIf conditions
|
|
141
|
+
if (action.runIf) {
|
|
142
|
+
if (action.runIf.enum) {
|
|
143
|
+
conditions.set(nodeName, `${action.runIf.enum.enumType}: ${action.runIf.enum.enumValue}`);
|
|
144
|
+
}
|
|
145
|
+
else if (action.runIf.actionOutput) {
|
|
146
|
+
conditions.set(nodeName, `${action.runIf.actionOutput.actionName}.${action.runIf.actionOutput.output}`);
|
|
147
|
+
dependencies.get(nodeName).add(action.runIf.actionOutput.actionName);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Determine outputs
|
|
151
|
+
const actionType = action.action?.name?.name || action.name;
|
|
152
|
+
const nodeOutputs = inferOutputs(actionType, action);
|
|
153
|
+
outputs.set(nodeName, nodeOutputs);
|
|
154
|
+
}
|
|
155
|
+
return { dependencies, dependents, outputs, conditions };
|
|
156
|
+
}
|
|
157
|
+
function inferOutputs(actionType, action) {
|
|
158
|
+
const type = actionType.toLowerCase();
|
|
159
|
+
if (type.includes("categorizer")) {
|
|
160
|
+
// Categorizers output a category enum
|
|
161
|
+
const categories = action.typeArguments?.categories;
|
|
162
|
+
if (Array.isArray(categories)) {
|
|
163
|
+
return categories.map(c => c.name);
|
|
164
|
+
}
|
|
165
|
+
// Categories might be in a different format
|
|
166
|
+
return ["category"];
|
|
167
|
+
}
|
|
168
|
+
if (type.includes("search")) {
|
|
169
|
+
return ["search_results", "combined_results"];
|
|
170
|
+
}
|
|
171
|
+
if (type.includes("respond_with_sources") || type.includes("call_llm")) {
|
|
172
|
+
return ["response_with_sources", "generated_content"];
|
|
173
|
+
}
|
|
174
|
+
if (type.includes("entity_extraction")) {
|
|
175
|
+
return ["email_address", "phone_number", "extracted_fields"];
|
|
176
|
+
}
|
|
177
|
+
if (type.includes("hitl") || type.includes("human_collaboration")) {
|
|
178
|
+
return ["hitl_status", "human_input"];
|
|
179
|
+
}
|
|
180
|
+
if (type.includes("conversation_to_search") || type.includes("summarizer")) {
|
|
181
|
+
return ["summarized_conversation", "search_query"];
|
|
182
|
+
}
|
|
183
|
+
if (type.includes("trigger")) {
|
|
184
|
+
return ["chat_conversation", "user_query"];
|
|
185
|
+
}
|
|
186
|
+
return ["output"];
|
|
187
|
+
}
|
|
188
|
+
function findAllPaths(actions, graph, results) {
|
|
189
|
+
const paths = [];
|
|
190
|
+
// Find trigger node
|
|
191
|
+
const trigger = actions.find(a => a.action?.name?.name?.includes("trigger") || a.name?.includes("trigger"));
|
|
192
|
+
if (!trigger) {
|
|
193
|
+
return [{
|
|
194
|
+
name: "No Trigger",
|
|
195
|
+
steps: [],
|
|
196
|
+
totalLatencyMs: 0,
|
|
197
|
+
llmCalls: 0,
|
|
198
|
+
externalCalls: 0,
|
|
199
|
+
hasHitl: false,
|
|
200
|
+
sendsEmail: false,
|
|
201
|
+
}];
|
|
202
|
+
}
|
|
203
|
+
// Find categorizers (branching points)
|
|
204
|
+
const categorizers = actions.filter(a => a.action?.name?.name?.includes("categorizer") || a.name?.includes("categorizer"));
|
|
205
|
+
if (categorizers.length === 0) {
|
|
206
|
+
// Linear workflow - single path
|
|
207
|
+
const steps = buildLinearPath(actions, graph, trigger.name);
|
|
208
|
+
paths.push(createFlowPath("Main Flow", steps, actions));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// Branching workflow - trace each branch
|
|
212
|
+
for (const categorizer of categorizers) {
|
|
213
|
+
const categories = categorizer.typeArguments?.categories;
|
|
214
|
+
if (Array.isArray(categories)) {
|
|
215
|
+
for (const category of categories) {
|
|
216
|
+
const branchSteps = buildBranchPath(actions, graph, trigger.name, categorizer.name, category.name);
|
|
217
|
+
paths.push(createFlowPath(`Branch: ${category.name}`, branchSteps, actions));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Single categorizer path if categories not properly defined
|
|
222
|
+
const steps = buildLinearPath(actions, graph, trigger.name);
|
|
223
|
+
paths.push(createFlowPath(`Via ${categorizer.displaySettings?.displayName || categorizer.name}`, steps, actions));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// If no paths were found, create a default main flow
|
|
228
|
+
if (paths.length === 0) {
|
|
229
|
+
const steps = buildLinearPath(actions, graph, trigger.name);
|
|
230
|
+
paths.push(createFlowPath("Main Flow", steps, actions));
|
|
231
|
+
}
|
|
232
|
+
return paths;
|
|
233
|
+
}
|
|
234
|
+
function buildLinearPath(actions, graph, startNode) {
|
|
235
|
+
const steps = [];
|
|
236
|
+
const visited = new Set();
|
|
237
|
+
const queue = [startNode];
|
|
238
|
+
let stepNum = 1;
|
|
239
|
+
while (queue.length > 0) {
|
|
240
|
+
const nodeName = queue.shift();
|
|
241
|
+
if (visited.has(nodeName))
|
|
242
|
+
continue;
|
|
243
|
+
visited.add(nodeName);
|
|
244
|
+
const action = actions.find(a => a.name === nodeName);
|
|
245
|
+
if (!action)
|
|
246
|
+
continue;
|
|
247
|
+
steps.push(createFlowStep(stepNum++, action, graph));
|
|
248
|
+
// Add dependents to queue
|
|
249
|
+
const dependents = graph.dependents.get(nodeName) || new Set();
|
|
250
|
+
for (const dep of dependents) {
|
|
251
|
+
if (!visited.has(dep)) {
|
|
252
|
+
queue.push(dep);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return steps;
|
|
257
|
+
}
|
|
258
|
+
function buildBranchPath(actions, graph, startNode, categorizerName, categoryName) {
|
|
259
|
+
const steps = [];
|
|
260
|
+
const visited = new Set();
|
|
261
|
+
const queue = [startNode];
|
|
262
|
+
let stepNum = 1;
|
|
263
|
+
while (queue.length > 0) {
|
|
264
|
+
const nodeName = queue.shift();
|
|
265
|
+
if (visited.has(nodeName))
|
|
266
|
+
continue;
|
|
267
|
+
visited.add(nodeName);
|
|
268
|
+
const action = actions.find(a => a.name === nodeName);
|
|
269
|
+
if (!action)
|
|
270
|
+
continue;
|
|
271
|
+
// Check if this node has a runIf condition that matches our branch
|
|
272
|
+
const condition = graph.conditions.get(nodeName);
|
|
273
|
+
if (condition) {
|
|
274
|
+
// Skip nodes that are conditioned on different categories
|
|
275
|
+
const isCategoryCondition = condition.includes(categorizerName);
|
|
276
|
+
if (isCategoryCondition && !condition.includes(categoryName)) {
|
|
277
|
+
continue; // Skip this branch
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const step = createFlowStep(stepNum++, action, graph);
|
|
281
|
+
step.branch = categoryName;
|
|
282
|
+
steps.push(step);
|
|
283
|
+
// Add dependents to queue
|
|
284
|
+
const dependents = graph.dependents.get(nodeName) || new Set();
|
|
285
|
+
for (const dep of dependents) {
|
|
286
|
+
if (!visited.has(dep)) {
|
|
287
|
+
queue.push(dep);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return steps;
|
|
292
|
+
}
|
|
293
|
+
function createFlowStep(step, action, graph) {
|
|
294
|
+
const actionType = action.action?.name?.name || action.name || "unknown";
|
|
295
|
+
const displayName = action.displaySettings?.displayName || action.name;
|
|
296
|
+
// Build inputs
|
|
297
|
+
const inputs = [];
|
|
298
|
+
if (action.inputs) {
|
|
299
|
+
for (const [name, binding] of Object.entries(action.inputs)) {
|
|
300
|
+
const bindingObj = binding;
|
|
301
|
+
const actionOutput = bindingObj?.actionOutput;
|
|
302
|
+
inputs.push({
|
|
303
|
+
name,
|
|
304
|
+
source: actionOutput ? `${actionOutput.actionName}.${actionOutput.output}` : "static",
|
|
305
|
+
type: friendlyTypeName("TEXT_WITH_SOURCES"),
|
|
306
|
+
sampleValue: actionOutput ? getSampleValue(actionOutput.output || "") : undefined,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Build outputs
|
|
311
|
+
const outputNames = graph.outputs.get(action.name) || ["output"];
|
|
312
|
+
const consumers = Array.from(graph.dependents.get(action.name) || []);
|
|
313
|
+
const outputs = outputNames.map(name => ({
|
|
314
|
+
name,
|
|
315
|
+
type: friendlyTypeName("TEXT_WITH_SOURCES"),
|
|
316
|
+
consumers,
|
|
317
|
+
sampleValue: getSampleValue(name),
|
|
318
|
+
}));
|
|
319
|
+
// Get condition
|
|
320
|
+
const condition = graph.conditions.get(action.name);
|
|
321
|
+
// Get notes
|
|
322
|
+
const notes = [];
|
|
323
|
+
if (actionType.includes("hitl") || actionType.includes("human")) {
|
|
324
|
+
notes.push("⏸️ Waits for human approval");
|
|
325
|
+
}
|
|
326
|
+
if (actionType.includes("email")) {
|
|
327
|
+
notes.push("📧 Sends external email");
|
|
328
|
+
}
|
|
329
|
+
if (actionType.includes("external_action")) {
|
|
330
|
+
notes.push("🔌 Calls external API");
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
step,
|
|
334
|
+
node: action.name,
|
|
335
|
+
displayName,
|
|
336
|
+
actionType,
|
|
337
|
+
inputs,
|
|
338
|
+
outputs,
|
|
339
|
+
condition,
|
|
340
|
+
estimatedLatencyMs: estimateLatency(actionType),
|
|
341
|
+
notes: notes.length > 0 ? notes : undefined,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function createFlowPath(name, steps, actions) {
|
|
345
|
+
const totalLatencyMs = steps.reduce((sum, s) => sum + (s.estimatedLatencyMs || 0), 0);
|
|
346
|
+
const llmCalls = steps.filter(s => s.actionType.includes("categorizer") ||
|
|
347
|
+
s.actionType.includes("call_llm") ||
|
|
348
|
+
s.actionType.includes("respond")).length;
|
|
349
|
+
const externalCalls = steps.filter(s => s.actionType.includes("email") ||
|
|
350
|
+
s.actionType.includes("external_action")).length;
|
|
351
|
+
const hasHitl = steps.some(s => s.actionType.includes("hitl") || s.actionType.includes("human"));
|
|
352
|
+
const sendsEmail = steps.some(s => s.actionType.includes("email"));
|
|
353
|
+
return {
|
|
354
|
+
name,
|
|
355
|
+
steps,
|
|
356
|
+
totalLatencyMs,
|
|
357
|
+
llmCalls,
|
|
358
|
+
externalCalls,
|
|
359
|
+
hasHitl,
|
|
360
|
+
sendsEmail,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function calculateStats(paths, actions) {
|
|
364
|
+
const allNodeNames = new Set(actions.map(a => a.name));
|
|
365
|
+
const pathNodeSets = paths.map(p => new Set(p.steps.map(s => s.node)));
|
|
366
|
+
// Nodes in ALL paths
|
|
367
|
+
const commonNodes = Array.from(allNodeNames).filter(node => pathNodeSets.every(set => set.has(node)));
|
|
368
|
+
// Nodes in SOME but not all paths
|
|
369
|
+
const optionalNodes = Array.from(allNodeNames).filter(node => pathNodeSets.some(set => set.has(node)) && !commonNodes.includes(node));
|
|
370
|
+
const pathLengths = paths.map(p => p.steps.length);
|
|
371
|
+
const latencies = paths.map(p => p.totalLatencyMs);
|
|
372
|
+
return {
|
|
373
|
+
totalNodes: allNodeNames.size,
|
|
374
|
+
uniquePaths: paths.length,
|
|
375
|
+
maxDepth: Math.max(...pathLengths),
|
|
376
|
+
avgPathLength: pathLengths.reduce((a, b) => a + b, 0) / pathLengths.length,
|
|
377
|
+
commonNodes,
|
|
378
|
+
optionalNodes,
|
|
379
|
+
minLatencyMs: Math.min(...latencies),
|
|
380
|
+
maxLatencyMs: Math.max(...latencies),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
384
|
+
// Visualization: ASCII
|
|
385
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
386
|
+
function generateAsciiFlow(paths) {
|
|
387
|
+
const lines = [];
|
|
388
|
+
lines.push("╔══════════════════════════════════════════════════════════════════╗");
|
|
389
|
+
lines.push("║ WORKFLOW EXECUTION FLOW ║");
|
|
390
|
+
lines.push("╚══════════════════════════════════════════════════════════════════╝");
|
|
391
|
+
lines.push("");
|
|
392
|
+
for (const path of paths) {
|
|
393
|
+
lines.push(`━━━ ${path.name} ━━━`);
|
|
394
|
+
lines.push(` Latency: ~${formatLatency(path.totalLatencyMs)} | LLM calls: ${path.llmCalls} | External: ${path.externalCalls}`);
|
|
395
|
+
if (path.hasHitl)
|
|
396
|
+
lines.push(" ⏸️ Human approval required");
|
|
397
|
+
if (path.sendsEmail)
|
|
398
|
+
lines.push(" 📧 Sends email");
|
|
399
|
+
lines.push("");
|
|
400
|
+
for (let i = 0; i < path.steps.length; i++) {
|
|
401
|
+
const step = path.steps[i];
|
|
402
|
+
const isLast = i === path.steps.length - 1;
|
|
403
|
+
// Node box
|
|
404
|
+
const icon = getNodeIcon(step.actionType);
|
|
405
|
+
const latency = step.estimatedLatencyMs ? ` (~${step.estimatedLatencyMs}ms)` : "";
|
|
406
|
+
lines.push(` ┌─────────────────────────────────────────┐`);
|
|
407
|
+
lines.push(` │ ${icon} ${step.displayName.slice(0, 35).padEnd(35)} │`);
|
|
408
|
+
lines.push(` │ ${step.actionType.slice(0, 37).padEnd(37)} │`);
|
|
409
|
+
// Show inputs
|
|
410
|
+
if (step.inputs.length > 0) {
|
|
411
|
+
lines.push(` │ ← ${step.inputs.map(i => i.source).join(", ").slice(0, 33).padEnd(33)} │`);
|
|
412
|
+
}
|
|
413
|
+
// Show condition
|
|
414
|
+
if (step.condition) {
|
|
415
|
+
lines.push(` │ 🔀 if: ${step.condition.slice(0, 28).padEnd(28)} │`);
|
|
416
|
+
}
|
|
417
|
+
// Show outputs
|
|
418
|
+
if (step.outputs.length > 0 && step.outputs[0].consumers.length > 0) {
|
|
419
|
+
lines.push(` │ → ${step.outputs[0].consumers.join(", ").slice(0, 33).padEnd(33)} │`);
|
|
420
|
+
}
|
|
421
|
+
lines.push(` └─────────────────────────────────────────┘`);
|
|
422
|
+
if (!isLast) {
|
|
423
|
+
lines.push(" │");
|
|
424
|
+
lines.push(" ▼");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
lines.push("");
|
|
428
|
+
}
|
|
429
|
+
return lines.join("\n");
|
|
430
|
+
}
|
|
431
|
+
function getNodeIcon(actionType) {
|
|
432
|
+
const type = actionType.toLowerCase();
|
|
433
|
+
if (type.includes("trigger"))
|
|
434
|
+
return "🎯";
|
|
435
|
+
if (type.includes("categorizer"))
|
|
436
|
+
return "🔀";
|
|
437
|
+
if (type.includes("search"))
|
|
438
|
+
return "🔍";
|
|
439
|
+
if (type.includes("llm") || type.includes("respond"))
|
|
440
|
+
return "🤖";
|
|
441
|
+
if (type.includes("hitl") || type.includes("human"))
|
|
442
|
+
return "👤";
|
|
443
|
+
if (type.includes("email"))
|
|
444
|
+
return "📧";
|
|
445
|
+
if (type.includes("entity_extraction"))
|
|
446
|
+
return "📋";
|
|
447
|
+
if (type.includes("external"))
|
|
448
|
+
return "🔌";
|
|
449
|
+
return "⚙️";
|
|
450
|
+
}
|
|
451
|
+
function formatLatency(ms) {
|
|
452
|
+
if (ms < 1000)
|
|
453
|
+
return `${ms}ms`;
|
|
454
|
+
if (ms < 60000)
|
|
455
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
456
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
457
|
+
}
|
|
458
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
459
|
+
// Visualization: Mermaid
|
|
460
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
461
|
+
function generateMermaidDiagram(actions, graph) {
|
|
462
|
+
const lines = [];
|
|
463
|
+
lines.push("```mermaid");
|
|
464
|
+
lines.push("flowchart TD");
|
|
465
|
+
// Define nodes
|
|
466
|
+
for (const action of actions) {
|
|
467
|
+
const type = action.action?.name?.name || action.name;
|
|
468
|
+
const displayName = action.displaySettings?.displayName || action.name;
|
|
469
|
+
const icon = getNodeIcon(type);
|
|
470
|
+
// Node shape based on type
|
|
471
|
+
if (type.includes("trigger")) {
|
|
472
|
+
lines.push(` ${action.name}(("${icon} ${displayName}"))`);
|
|
473
|
+
}
|
|
474
|
+
else if (type.includes("categorizer")) {
|
|
475
|
+
lines.push(` ${action.name}{{"${icon} ${displayName}"}}`);
|
|
476
|
+
}
|
|
477
|
+
else if (type.includes("hitl") || type.includes("human")) {
|
|
478
|
+
lines.push(` ${action.name}[/"${icon} ${displayName}"/]`);
|
|
479
|
+
}
|
|
480
|
+
else if (type.includes("email") || type.includes("external")) {
|
|
481
|
+
lines.push(` ${action.name}[["${icon} ${displayName}"]]`);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
lines.push(` ${action.name}["${icon} ${displayName}"]`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
lines.push("");
|
|
488
|
+
// Define edges
|
|
489
|
+
for (const action of actions) {
|
|
490
|
+
const deps = graph.dependencies.get(action.name) || new Set();
|
|
491
|
+
const condition = graph.conditions.get(action.name);
|
|
492
|
+
for (const dep of deps) {
|
|
493
|
+
if (condition) {
|
|
494
|
+
// Extract just the category name from condition
|
|
495
|
+
const label = condition.split(":").pop()?.trim() || condition;
|
|
496
|
+
lines.push(` ${dep} -->|${label}| ${action.name}`);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
lines.push(` ${dep} --> ${action.name}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
lines.push("```");
|
|
504
|
+
return lines.join("\n");
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Generate a detailed trace with sample data showing what happens at each step
|
|
508
|
+
*/
|
|
509
|
+
export function generateDetailedTrace(path) {
|
|
510
|
+
const steps = [];
|
|
511
|
+
// Accumulate outputs as we go (simulate data flow)
|
|
512
|
+
const availableData = {
|
|
513
|
+
"trigger.chat_conversation": '{"messages": [{"role": "user", "content": "What is my account balance?"}]}',
|
|
514
|
+
"trigger.user_query": "What is my account balance?",
|
|
515
|
+
};
|
|
516
|
+
for (const flowStep of path.steps) {
|
|
517
|
+
// Gather inputs for this step
|
|
518
|
+
const inputData = {};
|
|
519
|
+
for (const input of flowStep.inputs) {
|
|
520
|
+
inputData[input.name] = availableData[input.source] || input.sampleValue || `<from ${input.source}>`;
|
|
521
|
+
}
|
|
522
|
+
// Generate outputs for this step
|
|
523
|
+
const outputData = {};
|
|
524
|
+
for (const output of flowStep.outputs) {
|
|
525
|
+
const value = output.sampleValue || getSampleValue(output.name);
|
|
526
|
+
outputData[output.name] = value;
|
|
527
|
+
// Make available for downstream nodes
|
|
528
|
+
availableData[`${flowStep.node}.${output.name}`] = value;
|
|
529
|
+
}
|
|
530
|
+
steps.push({
|
|
531
|
+
step: flowStep.step,
|
|
532
|
+
node: flowStep.node,
|
|
533
|
+
description: generateStepDescription(flowStep, inputData),
|
|
534
|
+
inputData,
|
|
535
|
+
outputData,
|
|
536
|
+
timing: `~${flowStep.estimatedLatencyMs || 500}ms`,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
pathName: path.name,
|
|
541
|
+
steps,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function generateStepDescription(step, inputData) {
|
|
545
|
+
const type = step.actionType.toLowerCase();
|
|
546
|
+
if (type.includes("trigger")) {
|
|
547
|
+
return "📥 Receive user message and start workflow";
|
|
548
|
+
}
|
|
549
|
+
if (type.includes("categorizer")) {
|
|
550
|
+
return `🔀 Analyze message to determine intent category`;
|
|
551
|
+
}
|
|
552
|
+
if (type.includes("search")) {
|
|
553
|
+
const query = Object.values(inputData)[0] || "user query";
|
|
554
|
+
return `🔍 Search knowledge base for: "${query.slice(0, 30)}..."`;
|
|
555
|
+
}
|
|
556
|
+
if (type.includes("respond") || type.includes("call_llm")) {
|
|
557
|
+
return "🤖 Generate response using LLM with search results";
|
|
558
|
+
}
|
|
559
|
+
if (type.includes("entity_extraction")) {
|
|
560
|
+
return "📋 Extract structured data (email, phone, etc.) from conversation";
|
|
561
|
+
}
|
|
562
|
+
if (type.includes("hitl") || type.includes("human")) {
|
|
563
|
+
return "👤 WAIT for human approval before proceeding";
|
|
564
|
+
}
|
|
565
|
+
if (type.includes("email")) {
|
|
566
|
+
return "📧 Send email to extracted recipient";
|
|
567
|
+
}
|
|
568
|
+
if (type.includes("summarizer") || type.includes("conversation_to")) {
|
|
569
|
+
return "📝 Summarize conversation into search query";
|
|
570
|
+
}
|
|
571
|
+
return `⚙️ Execute ${step.displayName}`;
|
|
572
|
+
}
|
|
573
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
574
|
+
// Format Helpers
|
|
575
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
576
|
+
/**
|
|
577
|
+
* Format a flow trace as readable text
|
|
578
|
+
*/
|
|
579
|
+
export function formatFlowTrace(trace) {
|
|
580
|
+
const lines = [];
|
|
581
|
+
lines.push("# Workflow Flow Analysis");
|
|
582
|
+
lines.push("");
|
|
583
|
+
lines.push(`**Trigger**: ${trace.triggerType}`);
|
|
584
|
+
lines.push(`**Total Nodes**: ${trace.stats.totalNodes}`);
|
|
585
|
+
lines.push(`**Unique Paths**: ${trace.stats.uniquePaths}`);
|
|
586
|
+
lines.push(`**Latency Range**: ${formatLatency(trace.stats.minLatencyMs)} - ${formatLatency(trace.stats.maxLatencyMs)}`);
|
|
587
|
+
lines.push("");
|
|
588
|
+
lines.push("## Common Nodes (all paths)");
|
|
589
|
+
lines.push(trace.stats.commonNodes.map(n => `- ${n}`).join("\n") || "- None");
|
|
590
|
+
lines.push("");
|
|
591
|
+
lines.push("## Branch-Specific Nodes");
|
|
592
|
+
lines.push(trace.stats.optionalNodes.map(n => `- ${n}`).join("\n") || "- None");
|
|
593
|
+
lines.push("");
|
|
594
|
+
lines.push("## Execution Paths");
|
|
595
|
+
for (const path of trace.paths) {
|
|
596
|
+
lines.push(`\n### ${path.name}`);
|
|
597
|
+
lines.push(`- **Steps**: ${path.steps.length}`);
|
|
598
|
+
lines.push(`- **Est. Latency**: ${formatLatency(path.totalLatencyMs)}`);
|
|
599
|
+
lines.push(`- **LLM Calls**: ${path.llmCalls}`);
|
|
600
|
+
lines.push(`- **External Calls**: ${path.externalCalls}`);
|
|
601
|
+
if (path.hasHitl)
|
|
602
|
+
lines.push("- ⚠️ **Requires human approval**");
|
|
603
|
+
if (path.sendsEmail)
|
|
604
|
+
lines.push("- 📧 **Sends email**");
|
|
605
|
+
lines.push("\n**Step-by-Step:**");
|
|
606
|
+
for (const step of path.steps) {
|
|
607
|
+
const condition = step.condition ? ` (if ${step.condition})` : "";
|
|
608
|
+
lines.push(`${step.step}. ${getNodeIcon(step.actionType)} **${step.displayName}**${condition}`);
|
|
609
|
+
if (step.inputs.length > 0) {
|
|
610
|
+
lines.push(` - Inputs: ${step.inputs.map(i => `\`${i.name}\` ← ${i.source}`).join(", ")}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
lines.push("\n---\n");
|
|
615
|
+
lines.push("## ASCII Flow");
|
|
616
|
+
lines.push("```");
|
|
617
|
+
lines.push(trace.ascii);
|
|
618
|
+
lines.push("```");
|
|
619
|
+
lines.push("\n## Mermaid Diagram");
|
|
620
|
+
lines.push(trace.mermaid);
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Format a detailed trace as readable text
|
|
625
|
+
*/
|
|
626
|
+
export function formatDetailedTrace(trace) {
|
|
627
|
+
const lines = [];
|
|
628
|
+
lines.push(`# Detailed Execution: ${trace.pathName}`);
|
|
629
|
+
lines.push("");
|
|
630
|
+
for (const step of trace.steps) {
|
|
631
|
+
lines.push(`## Step ${step.step}: ${step.node}`);
|
|
632
|
+
lines.push(`**${step.description}** (${step.timing})`);
|
|
633
|
+
lines.push("");
|
|
634
|
+
lines.push("**Inputs:**");
|
|
635
|
+
for (const [name, value] of Object.entries(step.inputData)) {
|
|
636
|
+
lines.push(`- \`${name}\`: \`${value.slice(0, 60)}${value.length > 60 ? "..." : ""}\``);
|
|
637
|
+
}
|
|
638
|
+
lines.push("");
|
|
639
|
+
lines.push("**Outputs:**");
|
|
640
|
+
for (const [name, value] of Object.entries(step.outputData)) {
|
|
641
|
+
lines.push(`- \`${name}\`: \`${value.slice(0, 60)}${value.length > 60 ? "..." : ""}\``);
|
|
642
|
+
}
|
|
643
|
+
lines.push("");
|
|
644
|
+
lines.push("---");
|
|
645
|
+
lines.push("");
|
|
646
|
+
}
|
|
647
|
+
return lines.join("\n");
|
|
648
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - No brittle keyword matching
|
|
15
15
|
*/
|
|
16
16
|
import { compileWorkflow } from "./workflow-generator.js";
|
|
17
|
+
import { STRUCTURAL_RULES_FOR_LLM } from "./structural-rules.js";
|
|
17
18
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
19
|
// SCHEMA DOCUMENTATION FOR LLM CONTEXT
|
|
19
20
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -263,7 +264,16 @@ inputs: { to_email: { type: "action_output", actionName: "entity_extraction", ou
|
|
|
263
264
|
1. Find the categorizer node
|
|
264
265
|
2. Add to the categories array
|
|
265
266
|
3. Add a handler node with runIf condition for the new category
|
|
267
|
+
|
|
268
|
+
${STRUCTURAL_RULES_FOR_LLM}
|
|
266
269
|
`;
|
|
270
|
+
/**
|
|
271
|
+
* Get the complete schema including structural validation rules.
|
|
272
|
+
* Use this for LLM context when generating or modifying workflows.
|
|
273
|
+
*/
|
|
274
|
+
export function getCompleteSchemaForLLM() {
|
|
275
|
+
return WORKFLOW_SCHEMA_FOR_LLM;
|
|
276
|
+
}
|
|
267
277
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
268
278
|
// DECOMPILER: workflow_def JSON → WorkflowSpec
|
|
269
279
|
// ═══════════════════════════════════════════════════════════════════════════
|