@ema.co/mcp-toolkit 2026.2.23 → 2026.2.24
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/dist/mcp/demo-generator.js +177 -0
- package/dist/mcp/domain/validation-rules.js +13 -0
- package/dist/mcp/domain/workflow-graph-optimizer.js +96 -14
- package/dist/mcp/domain/workflow-graph-transforms.js +50 -54
- package/dist/mcp/domain/workflow-graph.js +39 -17
- package/dist/mcp/handlers/data/index.js +74 -15
- package/dist/mcp/handlers/reference/index.js +34 -11
- package/dist/mcp/handlers/workflow/adapter.js +7 -4
- package/dist/mcp/handlers/workflow/optimize.js +9 -5
- package/dist/mcp/knowledge.js +174 -13
- package/dist/mcp/resources-dynamic.js +5 -0
- package/dist/mcp/tools.js +4 -4
- package/dist/sdk/generated/api-client/client/client.gen.js +3 -3
- package/dist/sdk/generated/api-client/client/index.js +5 -5
- package/dist/sdk/generated/api-client/client/utils.gen.js +4 -4
- package/dist/sdk/generated/api-client/client.gen.js +1 -1
- package/dist/sdk/generated/api-client/core/utils.gen.js +1 -1
- package/dist/sdk/generated/api-client/index.js +1 -1
- package/dist/sdk/generated/api-client/sdk.gen.js +2 -2
- package/package.json +2 -2
- package/dist/mcp/domain/workflow-optimizer.js +0 -671
- package/dist/mcp/handlers/workflow/fix.js +0 -587
|
@@ -301,6 +301,134 @@ export const DEMO_SCENARIOS = {
|
|
|
301
301
|
},
|
|
302
302
|
],
|
|
303
303
|
},
|
|
304
|
+
"finance-dunning": {
|
|
305
|
+
id: "finance-dunning",
|
|
306
|
+
name: "Finance Automated Dunning",
|
|
307
|
+
description: "AR dunning assistant: balance inquiries, payment links, disputes, payment plans, and escalation",
|
|
308
|
+
persona_types: ["chat", "voice"],
|
|
309
|
+
tags: ["finance", "ar", "dunning", "collections", "receivables", "billing"],
|
|
310
|
+
intents: [
|
|
311
|
+
{
|
|
312
|
+
name: "balance_inquiry",
|
|
313
|
+
description: "What do I owe? Current balance and aging",
|
|
314
|
+
example_questions: ["What's my balance?", "Do I have any overdue invoices?", "What do I owe?"],
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "payment_made",
|
|
318
|
+
description: "Customer says they already paid",
|
|
319
|
+
example_questions: ["I paid last week", "I already sent the wire", "Check your records"],
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "dispute",
|
|
323
|
+
description: "Dispute invoice or amount",
|
|
324
|
+
example_questions: ["This invoice is wrong", "We never received that order", "I'm disputing this charge"],
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "payment_plan",
|
|
328
|
+
description: "Request installments or extension",
|
|
329
|
+
example_questions: ["Can I pay in two installments?", "I need 30 more days", "Can we set up a payment plan?"],
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "how_to_pay",
|
|
333
|
+
description: "How to pay or payment link",
|
|
334
|
+
example_questions: ["How do I pay?", "Where's the payment link?", "What payment methods do you accept?"],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "escalate",
|
|
338
|
+
description: "Request to speak to a person",
|
|
339
|
+
example_questions: ["I want to talk to someone", "Transfer me to collections", "I need to speak to AR"],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "Fallback",
|
|
343
|
+
description: "Anything else",
|
|
344
|
+
example_questions: ["What's the weather?", "Tell me about your company"],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
entities: [
|
|
348
|
+
{
|
|
349
|
+
type: "customer",
|
|
350
|
+
count: 3,
|
|
351
|
+
template: {
|
|
352
|
+
id: "CUST-AR-{{id}}",
|
|
353
|
+
name: "{{name}}",
|
|
354
|
+
email: "{{email}}",
|
|
355
|
+
payment_terms_days: 30,
|
|
356
|
+
payment_plan_active: false,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
type: "invoice",
|
|
361
|
+
count: 3,
|
|
362
|
+
template: {
|
|
363
|
+
invoice_id: "INV-{{id}}",
|
|
364
|
+
customer_id: "CUST-AR-{{cust}}",
|
|
365
|
+
amount_due: "{{amount}}",
|
|
366
|
+
due_date: "{{due_date}}",
|
|
367
|
+
days_past_due: "{{days}}",
|
|
368
|
+
dunning_level: "{{level}}",
|
|
369
|
+
payment_link: "{{payment_link}}",
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
type: "dunning_policy",
|
|
374
|
+
count: 1,
|
|
375
|
+
template: {
|
|
376
|
+
reminder_1_days: "1-7",
|
|
377
|
+
reminder_2_days: "8-14",
|
|
378
|
+
final_notice_days: "15-30",
|
|
379
|
+
escalation_days: "31+",
|
|
380
|
+
payment_plan_note: "Payment plan requests are reviewed by AR within 1 business day.",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
qa_pairs: [
|
|
385
|
+
{
|
|
386
|
+
phase: "intro",
|
|
387
|
+
question: "Hi, I have a question about my invoice.",
|
|
388
|
+
answer_template: "Hi! I'm here to help with your invoice and payment questions. I can look up your balance, share the payment link, or help with a dispute or payment plan request. What would you like to do?",
|
|
389
|
+
intent: "balance_inquiry",
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
phase: "main",
|
|
393
|
+
question: "What do I owe?",
|
|
394
|
+
answer_template: "Your current balance is {{amount_due}} (invoice {{invoice_id}}, due {{due_date}}). You can pay here: {{payment_link}}. If you need a payment plan or have already paid, tell me and I'll help.",
|
|
395
|
+
intent: "balance_inquiry",
|
|
396
|
+
required_entity_type: "invoice",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
phase: "main",
|
|
400
|
+
question: "How do I pay?",
|
|
401
|
+
answer_template: "You can pay online using this link: {{payment_link}}. We accept major credit cards and ACH. If you've already paid, please have your payment reference or date ready and I can help verify.",
|
|
402
|
+
intent: "how_to_pay",
|
|
403
|
+
required_entity_type: "invoice",
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
phase: "main",
|
|
407
|
+
question: "I already paid last week.",
|
|
408
|
+
answer_template: "Thanks for letting us know. Our AR team will verify the payment. Can you share the payment reference number or the date you paid? We'll follow up if anything doesn't match.",
|
|
409
|
+
intent: "payment_made",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
phase: "advanced",
|
|
413
|
+
question: "This invoice is wrong.",
|
|
414
|
+
answer_template: "I'm sorry to hear that. I'm not able to adjust invoices here. I've escalated this to our AR team—they'll contact you to resolve the dispute. You should hear back within 1 business day.",
|
|
415
|
+
intent: "dispute",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
phase: "advanced",
|
|
419
|
+
question: "Can I pay in two installments?",
|
|
420
|
+
answer_template: "Payment plan requests are reviewed by our AR team. I've submitted your request; someone will get back to you within 1 business day with options.",
|
|
421
|
+
intent: "payment_plan",
|
|
422
|
+
required_entity_type: "dunning_policy",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
phase: "closing",
|
|
426
|
+
question: "I want to talk to someone.",
|
|
427
|
+
answer_template: "No problem. I've requested that our AR team contact you. You should hear from someone within 1 business day. Is there anything else I can help with in the meantime?",
|
|
428
|
+
intent: "escalate",
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
304
432
|
};
|
|
305
433
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
306
434
|
// Sample Entity Data
|
|
@@ -473,6 +601,55 @@ export const SAMPLE_ENTITIES = {
|
|
|
473
601
|
manager: "Bob Director",
|
|
474
602
|
},
|
|
475
603
|
],
|
|
604
|
+
invoice: [
|
|
605
|
+
{
|
|
606
|
+
id: "INV-2025-001234",
|
|
607
|
+
invoice_id: "INV-2025-001234",
|
|
608
|
+
customer_id: "CUST-AR-001",
|
|
609
|
+
amount_due: 12500.0,
|
|
610
|
+
currency: "USD",
|
|
611
|
+
due_date: "2025-01-20",
|
|
612
|
+
days_past_due: 34,
|
|
613
|
+
dunning_level: 3,
|
|
614
|
+
payment_link: "https://pay.example.com/INV-2025-001234",
|
|
615
|
+
line_items_summary: "Subscription Q1 2025",
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
id: "INV-2025-005678",
|
|
619
|
+
invoice_id: "INV-2025-005678",
|
|
620
|
+
customer_id: "CUST-AR-002",
|
|
621
|
+
amount_due: 4850.0,
|
|
622
|
+
currency: "USD",
|
|
623
|
+
due_date: "2025-02-01",
|
|
624
|
+
days_past_due: 22,
|
|
625
|
+
dunning_level: 3,
|
|
626
|
+
payment_link: "https://pay.example.com/INV-2025-005678",
|
|
627
|
+
line_items_summary: "Professional services February 2025",
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
id: "INV-2025-009999",
|
|
631
|
+
invoice_id: "INV-2025-009999",
|
|
632
|
+
customer_id: "CUST-AR-003",
|
|
633
|
+
amount_due: 899.0,
|
|
634
|
+
currency: "USD",
|
|
635
|
+
due_date: "2025-02-15",
|
|
636
|
+
days_past_due: 8,
|
|
637
|
+
dunning_level: 2,
|
|
638
|
+
payment_link: "https://pay.example.com/INV-2025-009999",
|
|
639
|
+
line_items_summary: "Monthly license March 2025",
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
dunning_policy: [
|
|
643
|
+
{
|
|
644
|
+
id: "dunning-policy",
|
|
645
|
+
name: "Dunning Policy",
|
|
646
|
+
reminder_1_days: "1-7",
|
|
647
|
+
reminder_2_days: "8-14",
|
|
648
|
+
final_notice_days: "15-30",
|
|
649
|
+
escalation_days: "31+",
|
|
650
|
+
payment_plan_note: "Payment plan requests are reviewed by AR within 1 business day. Do not send reminders to customers on active payment plans.",
|
|
651
|
+
},
|
|
652
|
+
],
|
|
476
653
|
benefit: [
|
|
477
654
|
{
|
|
478
655
|
id: "BEN-001",
|
|
@@ -68,6 +68,19 @@ export const INPUT_SOURCE_RULES = [
|
|
|
68
68
|
severity: "warning",
|
|
69
69
|
fix: "Use user_query for query, pass chat_conversation via named_inputs if needed",
|
|
70
70
|
},
|
|
71
|
+
// Rule validation inputs
|
|
72
|
+
{
|
|
73
|
+
actionPattern: "rule_validation_with_documents",
|
|
74
|
+
recommended: "entity_extraction.extraction_columns for map_of_extracted_columns, workflowInput.document for primary_docs",
|
|
75
|
+
avoid: ["omitting primary_docs", "wiring phases in parallel instead of sequentially"],
|
|
76
|
+
reason: "rule_validation_with_documents requires both extracted columns and original documents. " +
|
|
77
|
+
"primary_docs gives the validator context from the original document. " +
|
|
78
|
+
"When chaining multiple validation phases, wire them sequentially (phase1.ruleset_output → phase2.map_of_extracted_columns) " +
|
|
79
|
+
"so each phase builds on prior validation results.",
|
|
80
|
+
severity: "critical",
|
|
81
|
+
fix: "Wire entity_extraction.extraction_columns → rule_validation.map_of_extracted_columns AND " +
|
|
82
|
+
"workflowInput.document → rule_validation.primary_docs. For multi-phase: chain sequentially, not in parallel.",
|
|
83
|
+
},
|
|
71
84
|
// Email-specific rules - GUIDANCE ONLY (not hard backend constraints)
|
|
72
85
|
// NOTE: Backend accepts TEXT_WITH_SOURCES for email_to, so this is soft guidance
|
|
73
86
|
// The correct pattern is: entity_extraction → fixed_response with {{email}} template → send_email
|
|
@@ -12,7 +12,7 @@ import { AUTO_TRANSFORMS, ADVISORY_TRANSFORMS, } from "./workflow-graph-transfor
|
|
|
12
12
|
function deepClone(obj) {
|
|
13
13
|
return JSON.parse(JSON.stringify(obj));
|
|
14
14
|
}
|
|
15
|
-
function calculateMetrics(
|
|
15
|
+
function calculateMetrics(graph) {
|
|
16
16
|
const nodeCount = graph.nodes.size;
|
|
17
17
|
const edgeCount = graph.edges.length;
|
|
18
18
|
// Dead nodes: total - reachable from trigger
|
|
@@ -50,6 +50,83 @@ function generateImprovementSummary(before, after) {
|
|
|
50
50
|
summary += `. ${before.nodeCount} \u2192 ${after.nodeCount} nodes.`;
|
|
51
51
|
return summary;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Show the immediate neighborhood (parents → node → children) of a set of nodes
|
|
55
|
+
* from the graph state BEFORE a transform ran. Used to explain why nodes were
|
|
56
|
+
* classified as dead/trailing so the viewer can verify the reasoning.
|
|
57
|
+
*
|
|
58
|
+
* Example output:
|
|
59
|
+
* orphaned_search (search)
|
|
60
|
+
* ← (no parents — unreachable from trigger)
|
|
61
|
+
* → respond_unused
|
|
62
|
+
* respond_unused (respond_for_external_actions)
|
|
63
|
+
* ← orphaned_search
|
|
64
|
+
* → (no children — does not reach any output)
|
|
65
|
+
*/
|
|
66
|
+
function buildNodeNeighborhood(nodeIds, graph) {
|
|
67
|
+
if (nodeIds.length === 0)
|
|
68
|
+
return undefined;
|
|
69
|
+
const resultNodeIds = new Set(graph.outputMappings.map((m) => m.nodeId));
|
|
70
|
+
const lines = [];
|
|
71
|
+
for (const nodeId of nodeIds) {
|
|
72
|
+
const node = graph.nodes.get(nodeId);
|
|
73
|
+
if (!node)
|
|
74
|
+
continue;
|
|
75
|
+
const parents = [...(graph.reverse.get(nodeId) ?? [])];
|
|
76
|
+
const children = [...(graph.forward.get(nodeId) ?? [])];
|
|
77
|
+
const parentStr = parents.length > 0
|
|
78
|
+
? parents.join(" · ")
|
|
79
|
+
: "(no parents)";
|
|
80
|
+
const childStr = children.length > 0
|
|
81
|
+
? children.map((c) => resultNodeIds.has(c) ? `${c} [→OUTPUT]` : c).join(" · ")
|
|
82
|
+
: "(no children — does not reach any output)";
|
|
83
|
+
lines.push(` ${nodeId} (${node.actionType})`);
|
|
84
|
+
lines.push(` ← ${parentStr}`);
|
|
85
|
+
lines.push(` → ${childStr}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n") || undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build a compact edge-list flow diagram of the workflow graph.
|
|
91
|
+
*
|
|
92
|
+
* Lists nodes in topological order (or declaration order for triggerless workflows),
|
|
93
|
+
* showing depth, type, children, and which nodes map to outputs.
|
|
94
|
+
*/
|
|
95
|
+
function buildFlowDiagram(graph) {
|
|
96
|
+
if (graph.nodes.size === 0)
|
|
97
|
+
return "(empty workflow — no nodes)";
|
|
98
|
+
const resultNodeIds = new Set(graph.outputMappings.map((m) => m.nodeId));
|
|
99
|
+
const lines = [];
|
|
100
|
+
// Header line
|
|
101
|
+
const triggerNote = graph.trigger
|
|
102
|
+
? `entry: ${graph.trigger}`
|
|
103
|
+
: "no recognized trigger (dashboard/document workflow — all nodes kept)";
|
|
104
|
+
lines.push(`${graph.nodes.size} nodes · ${graph.edges.length} edges · ${triggerNote}`);
|
|
105
|
+
// Topological order for consistent depth-first traversal; fall back to declaration order
|
|
106
|
+
const topoOrder = getTopologicalOrder(graph);
|
|
107
|
+
const nodeOrder = topoOrder.length === graph.nodes.size ? topoOrder : [...graph.nodes.keys()];
|
|
108
|
+
for (const nodeId of nodeOrder) {
|
|
109
|
+
const node = graph.nodes.get(nodeId);
|
|
110
|
+
if (!node)
|
|
111
|
+
continue;
|
|
112
|
+
const children = [...(graph.forward.get(nodeId) ?? [])];
|
|
113
|
+
const tags = [];
|
|
114
|
+
if (node.isTrigger)
|
|
115
|
+
tags.push("ENTRY");
|
|
116
|
+
if (resultNodeIds.has(nodeId))
|
|
117
|
+
tags.push("→OUTPUT");
|
|
118
|
+
if (node.hasSideEffects && !node.isTrigger)
|
|
119
|
+
tags.push("side-effect");
|
|
120
|
+
const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
121
|
+
// Indent up to 6 levels to avoid runaway indentation on deep graphs
|
|
122
|
+
const indent = " ".repeat(Math.min(node.depth, 6));
|
|
123
|
+
lines.push(`${indent}${nodeId} (${node.actionType})${tagStr}`);
|
|
124
|
+
if (children.length > 0) {
|
|
125
|
+
lines.push(`${indent} └─► ${children.join(" · ")}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
53
130
|
function validateWorkflow(workflowDef) {
|
|
54
131
|
const errors = [];
|
|
55
132
|
const warnings = [];
|
|
@@ -97,7 +174,7 @@ export function optimizeWorkflow(workflowDef, options) {
|
|
|
97
174
|
// Graceful handling of invalid input
|
|
98
175
|
if (!workflowDef || typeof workflowDef !== "object") {
|
|
99
176
|
return {
|
|
100
|
-
workflowDef: workflowDef ?? {},
|
|
177
|
+
workflowDef: (workflowDef ?? {}),
|
|
101
178
|
modified: false,
|
|
102
179
|
appliedTransforms: [],
|
|
103
180
|
advisories: [],
|
|
@@ -107,6 +184,7 @@ export function optimizeWorkflow(workflowDef, options) {
|
|
|
107
184
|
improvement: "No changes needed.",
|
|
108
185
|
},
|
|
109
186
|
validation: { valid: false, errors: ["Invalid workflow_def input"], warnings: [] },
|
|
187
|
+
flow_diagram: "(no workflow_def provided)",
|
|
110
188
|
};
|
|
111
189
|
}
|
|
112
190
|
try {
|
|
@@ -114,14 +192,16 @@ export function optimizeWorkflow(workflowDef, options) {
|
|
|
114
192
|
let currentWorkflowDef = deepClone(workflowDef);
|
|
115
193
|
// Step 2: Calculate "before" metrics
|
|
116
194
|
let graph = buildWorkflowGraph(currentWorkflowDef);
|
|
117
|
-
const beforeMetrics = calculateMetrics(
|
|
195
|
+
const beforeMetrics = calculateMetrics(graph);
|
|
118
196
|
// Step 3: Auto-apply transforms (convergence loop)
|
|
197
|
+
// Captures pass number and before-graph neighborhood for each change.
|
|
119
198
|
const appliedTransforms = [];
|
|
120
199
|
if (autoApply && maxPasses > 0) {
|
|
121
200
|
for (let pass = 0; pass < maxPasses; pass++) {
|
|
122
201
|
let anyApplied = false;
|
|
123
202
|
for (const transform of AUTO_TRANSFORMS) {
|
|
124
203
|
try {
|
|
204
|
+
const graphBefore = graph; // snapshot before this transform
|
|
125
205
|
const result = transform.apply(currentWorkflowDef, graph);
|
|
126
206
|
if (result && result.applied) {
|
|
127
207
|
currentWorkflowDef = result.workflowDef;
|
|
@@ -131,6 +211,12 @@ export function optimizeWorkflow(workflowDef, options) {
|
|
|
131
211
|
description: result.description,
|
|
132
212
|
nodesRemoved: result.nodesRemoved,
|
|
133
213
|
nodesModified: result.nodesModified,
|
|
214
|
+
pass: pass + 1,
|
|
215
|
+
// Show neighborhood of affected nodes in the before-graph so the
|
|
216
|
+
// viewer can verify why each node was classified as removable.
|
|
217
|
+
flow_context: result.nodesRemoved.length > 0
|
|
218
|
+
? buildNodeNeighborhood(result.nodesRemoved, graphBefore)
|
|
219
|
+
: undefined,
|
|
134
220
|
});
|
|
135
221
|
anyApplied = true;
|
|
136
222
|
}
|
|
@@ -143,17 +229,15 @@ export function optimizeWorkflow(workflowDef, options) {
|
|
|
143
229
|
break; // Converged
|
|
144
230
|
}
|
|
145
231
|
}
|
|
146
|
-
// Step 4: Calculate "after" metrics
|
|
232
|
+
// Step 4: Calculate "after" metrics and rebuild final graph
|
|
147
233
|
graph = buildWorkflowGraph(currentWorkflowDef);
|
|
148
|
-
const afterMetrics = calculateMetrics(
|
|
234
|
+
const afterMetrics = calculateMetrics(graph);
|
|
149
235
|
// Step 5: Run advisory transforms
|
|
150
236
|
const advisories = [];
|
|
151
237
|
for (const transform of ADVISORY_TRANSFORMS) {
|
|
152
238
|
try {
|
|
153
239
|
const result = transform.apply(currentWorkflowDef, graph);
|
|
154
240
|
if (result && !result.applied) {
|
|
155
|
-
// Advisory: applied=false means it's a finding, not a fix
|
|
156
|
-
// Extract affected nodes from the description or result fields
|
|
157
241
|
const affectedNodes = extractAffectedNodes(result);
|
|
158
242
|
advisories.push({
|
|
159
243
|
id: transform.id,
|
|
@@ -166,21 +250,18 @@ export function optimizeWorkflow(workflowDef, options) {
|
|
|
166
250
|
// Advisory failure: skip
|
|
167
251
|
}
|
|
168
252
|
}
|
|
169
|
-
// Step 6: Generate improvement summary
|
|
253
|
+
// Step 6: Generate improvement summary + validation + flow diagram
|
|
170
254
|
const improvement = generateImprovementSummary(beforeMetrics, afterMetrics);
|
|
171
|
-
// Step 7: Post-optimization validation
|
|
172
255
|
const validation = validateWorkflow(currentWorkflowDef);
|
|
256
|
+
const flow_diagram = buildFlowDiagram(graph);
|
|
173
257
|
return {
|
|
174
258
|
workflowDef: currentWorkflowDef,
|
|
175
259
|
modified: appliedTransforms.length > 0,
|
|
176
260
|
appliedTransforms,
|
|
177
261
|
advisories,
|
|
178
|
-
metrics: {
|
|
179
|
-
before: beforeMetrics,
|
|
180
|
-
after: afterMetrics,
|
|
181
|
-
improvement,
|
|
182
|
-
},
|
|
262
|
+
metrics: { before: beforeMetrics, after: afterMetrics, improvement },
|
|
183
263
|
validation,
|
|
264
|
+
flow_diagram,
|
|
184
265
|
};
|
|
185
266
|
}
|
|
186
267
|
catch {
|
|
@@ -196,6 +277,7 @@ export function optimizeWorkflow(workflowDef, options) {
|
|
|
196
277
|
improvement: "No changes needed.",
|
|
197
278
|
},
|
|
198
279
|
validation: { valid: false, errors: ["Optimization failed unexpectedly"], warnings: [] },
|
|
280
|
+
flow_diagram: "(optimization failed)",
|
|
199
281
|
};
|
|
200
282
|
}
|
|
201
283
|
}
|
|
@@ -66,32 +66,46 @@ function nodeSignature(action) {
|
|
|
66
66
|
}
|
|
67
67
|
return JSON.stringify({ actionType, version, inputWiring });
|
|
68
68
|
}
|
|
69
|
-
/** Check if an action has side effects based on its type. */
|
|
70
|
-
function hasSideEffects(action) {
|
|
71
|
-
const actionType = extractActionType(action);
|
|
72
|
-
const SIDE_EFFECT_FREE = new Set([
|
|
73
|
-
"search", "call_llm", "entity_extraction", "respond_with_sources",
|
|
74
|
-
"respond_for_external_actions", "conversation_to_search_query",
|
|
75
|
-
"combine_search_results", "text_categorizer", "chat_categorizer",
|
|
76
|
-
"response_validator", "fixed_response", "live_web_search",
|
|
77
|
-
"ai_web_search", "json_mapper", "custom_agent",
|
|
78
|
-
]);
|
|
79
|
-
const TRIGGER_TYPES = new Set(["chat_trigger", "voice_trigger", "document_trigger"]);
|
|
80
|
-
if (TRIGGER_TYPES.has(actionType))
|
|
81
|
-
return false;
|
|
82
|
-
if (SIDE_EFFECT_FREE.has(actionType))
|
|
83
|
-
return false;
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
/** Check if an action is a trigger node. */
|
|
87
|
-
function isTriggerAction(action) {
|
|
88
|
-
const actionType = extractActionType(action);
|
|
89
|
-
return actionType === "chat_trigger" || actionType === "voice_trigger" || actionType === "document_trigger";
|
|
90
|
-
}
|
|
91
69
|
/** Get all node names from actions array. */
|
|
92
70
|
function getActionNames(actions) {
|
|
93
71
|
return new Set(actions.map((a) => a.name).filter(Boolean));
|
|
94
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Recursively rewire actionOutput references from oldName → newName in a binding.
|
|
75
|
+
* Mirrors extractEdgesFromBinding in workflow-graph.ts: handles the same nesting
|
|
76
|
+
* depth so graph construction and reference rewiring stay symmetric.
|
|
77
|
+
*
|
|
78
|
+
* Handles:
|
|
79
|
+
* actionOutput → base case: mutate actionName if matches
|
|
80
|
+
* multiBinding → recurse into elements[]
|
|
81
|
+
* namedBinding → recurse into .value
|
|
82
|
+
*/
|
|
83
|
+
function rewireBinding(binding, oldName, newName) {
|
|
84
|
+
if (!binding || typeof binding !== "object")
|
|
85
|
+
return;
|
|
86
|
+
if (binding.actionOutput) {
|
|
87
|
+
const ao = binding.actionOutput;
|
|
88
|
+
if (ao.actionName === oldName) {
|
|
89
|
+
ao.actionName = newName;
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (binding.multiBinding) {
|
|
94
|
+
const mb = binding.multiBinding;
|
|
95
|
+
const elements = mb.elements ?? [];
|
|
96
|
+
for (const elem of elements) {
|
|
97
|
+
rewireBinding(elem, oldName, newName);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (binding.namedBinding) {
|
|
102
|
+
const nb = binding.namedBinding;
|
|
103
|
+
const value = nb.value;
|
|
104
|
+
if (value) {
|
|
105
|
+
rewireBinding(value, oldName, newName);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
95
109
|
/**
|
|
96
110
|
* Rewire all references from oldName to newName in the actions array.
|
|
97
111
|
* This updates actionOutput bindings, multiBinding refs, and runIf refs.
|
|
@@ -101,35 +115,7 @@ function rewireReferences(actions, oldName, newName) {
|
|
|
101
115
|
const inputs = action.inputs;
|
|
102
116
|
if (inputs) {
|
|
103
117
|
for (const val of Object.values(inputs)) {
|
|
104
|
-
|
|
105
|
-
if (binding?.actionOutput) {
|
|
106
|
-
const ao = binding.actionOutput;
|
|
107
|
-
if (ao.actionName === oldName) {
|
|
108
|
-
ao.actionName = newName;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (binding?.multiBinding) {
|
|
112
|
-
const mb = binding.multiBinding;
|
|
113
|
-
const elements = mb.elements ?? [];
|
|
114
|
-
for (const elem of elements) {
|
|
115
|
-
if (elem.namedBinding) {
|
|
116
|
-
const nb = elem.namedBinding;
|
|
117
|
-
const value = nb.value;
|
|
118
|
-
if (value?.actionOutput) {
|
|
119
|
-
const ao = value.actionOutput;
|
|
120
|
-
if (ao.actionName === oldName) {
|
|
121
|
-
ao.actionName = newName;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
else if (elem.actionOutput) {
|
|
126
|
-
const ao = elem.actionOutput;
|
|
127
|
-
if (ao.actionName === oldName) {
|
|
128
|
-
ao.actionName = newName;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
118
|
+
rewireBinding(val, oldName, newName);
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
121
|
// Rewire runIf
|
|
@@ -187,7 +173,7 @@ const removeDeadNodes = {
|
|
|
187
173
|
const node = graph.nodes.get(nodeId);
|
|
188
174
|
const hasDisplayName = node.displayName && node.displayName !== nodeId;
|
|
189
175
|
const isReferencedInResults = Object.values(results).some((r) => r.actionName === nodeId);
|
|
190
|
-
const hasActionOutputInputs = [...node.inputs.values()].some((ref) => ref.type === "action_output");
|
|
176
|
+
const hasActionOutputInputs = [...node.inputs.values()].some((ref) => ref.type === "action_output" || ref.type === "multi_binding");
|
|
191
177
|
if (hasDisplayName || isReferencedInResults || hasActionOutputInputs) {
|
|
192
178
|
// Intentional node: skip removal (could attempt wiring, but
|
|
193
179
|
// wiring heuristics are risky so we just keep it)
|
|
@@ -310,9 +296,10 @@ const mergeIdenticalNodes = {
|
|
|
310
296
|
// Group by signature, skipping triggers and side-effect nodes
|
|
311
297
|
const groups = new Map();
|
|
312
298
|
for (const action of actions) {
|
|
313
|
-
|
|
299
|
+
const graphNode = graph.nodes.get(action.name);
|
|
300
|
+
if (graphNode?.isTrigger)
|
|
314
301
|
continue;
|
|
315
|
-
if (hasSideEffects
|
|
302
|
+
if (graphNode?.hasSideEffects ?? true)
|
|
316
303
|
continue;
|
|
317
304
|
const sig = nodeSignature(action);
|
|
318
305
|
if (!groups.has(sig))
|
|
@@ -521,6 +508,15 @@ const removeTrailingNodes = {
|
|
|
521
508
|
try {
|
|
522
509
|
if (graph.nodes.size === 0)
|
|
523
510
|
return null;
|
|
511
|
+
// Dashboard/document workflows have no recognized trigger action.
|
|
512
|
+
// Without a trigger, every supplier node (which has no forward path to a result
|
|
513
|
+
// because it IS an upstream input) would be incorrectly classified as trailing.
|
|
514
|
+
if (!graph.trigger)
|
|
515
|
+
return null;
|
|
516
|
+
// Without any result mappings we cannot identify what contributes to output,
|
|
517
|
+
// so every non-trigger side-effect-free node would appear trailing.
|
|
518
|
+
if (graph.outputMappings.length === 0)
|
|
519
|
+
return null;
|
|
524
520
|
// Build set of nodes that contribute to output mappings
|
|
525
521
|
const resultNodes = new Set(graph.outputMappings.map((m) => m.nodeId));
|
|
526
522
|
// Find nodes that are downstream of result nodes but don't contribute to results
|
|
@@ -117,23 +117,8 @@ export function buildWorkflowGraph(workflowDef) {
|
|
|
117
117
|
}
|
|
118
118
|
else if (binding.multiBinding) {
|
|
119
119
|
inputRefs.set(inputName, { type: "multi_binding" });
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
const elements = mb.elements ?? [];
|
|
123
|
-
for (const elem of elements) {
|
|
124
|
-
if (elem.namedBinding) {
|
|
125
|
-
const nb = elem.namedBinding;
|
|
126
|
-
const value = nb.value;
|
|
127
|
-
if (value?.actionOutput) {
|
|
128
|
-
const ao = value.actionOutput;
|
|
129
|
-
addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
else if (elem.actionOutput) {
|
|
133
|
-
const ao = elem.actionOutput;
|
|
134
|
-
addEdge(edges, forward, reverse, ao.actionName, nodeId, ao.output, inputName);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
120
|
+
// Recursively traverse nested multiBinding/namedBinding structures to find all actionOutput refs
|
|
121
|
+
extractEdgesFromBinding(binding, nodeId, inputName, edges, forward, reverse);
|
|
137
122
|
}
|
|
138
123
|
else if (binding.widgetConfig) {
|
|
139
124
|
inputRefs.set(inputName, { type: "widget" });
|
|
@@ -363,6 +348,43 @@ function extractEnumTypeName(et) {
|
|
|
363
348
|
}
|
|
364
349
|
return undefined;
|
|
365
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Recursively extract actionOutput edges from an arbitrarily nested binding structure.
|
|
353
|
+
*
|
|
354
|
+
* Handles:
|
|
355
|
+
* actionOutput → base case: emit edge
|
|
356
|
+
* multiBinding → recurse into elements[]
|
|
357
|
+
* namedBinding → recurse into .value
|
|
358
|
+
*
|
|
359
|
+
* This covers nested proto patterns like:
|
|
360
|
+
* { multiBinding: { elements: [{ namedBinding: { value: { multiBinding: { elements: [{ actionOutput: {...} }] } } } }] } }
|
|
361
|
+
*/
|
|
362
|
+
function extractEdgesFromBinding(binding, targetNode, targetInput, edges, forward, reverse) {
|
|
363
|
+
if (!binding || typeof binding !== "object")
|
|
364
|
+
return;
|
|
365
|
+
if (binding.actionOutput) {
|
|
366
|
+
const ao = binding.actionOutput;
|
|
367
|
+
if (ao.actionName) {
|
|
368
|
+
addEdge(edges, forward, reverse, ao.actionName, targetNode, ao.output ?? "", targetInput);
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (binding.multiBinding) {
|
|
373
|
+
const mb = binding.multiBinding;
|
|
374
|
+
const elements = mb.elements ?? [];
|
|
375
|
+
for (const elem of elements) {
|
|
376
|
+
extractEdgesFromBinding(elem, targetNode, targetInput, edges, forward, reverse);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (binding.namedBinding) {
|
|
381
|
+
const nb = binding.namedBinding;
|
|
382
|
+
const value = nb.value;
|
|
383
|
+
if (value) {
|
|
384
|
+
extractEdgesFromBinding(value, targetNode, targetInput, edges, forward, reverse);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
366
388
|
function addEdge(edges, forward, reverse, source, target, sourceOutput, targetInput) {
|
|
367
389
|
edges.push({ source, target, sourceOutput, targetInput });
|
|
368
390
|
if (!forward.has(source))
|