@ema.co/mcp-toolkit 2026.1.26-4 → 2026.1.27
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/action/index.js +14 -2
- package/dist/mcp/handlers/data/index.js +72 -6
- package/dist/mcp/handlers/env/index.js +3 -3
- package/dist/mcp/handlers/reference/index.js +15 -2
- package/dist/mcp/handlers/workflow/analyze.js +53 -105
- package/dist/mcp/handlers/workflow/deploy.js +15 -0
- package/dist/mcp/handlers/workflow/generate.js +3 -6
- package/dist/mcp/handlers/workflow/index.js +18 -3
- package/dist/mcp/handlers/workflow/modify.js +9 -29
- package/dist/mcp/handlers/workflow/optimize.js +22 -108
- package/dist/mcp/prompts.js +12 -15
- package/dist/mcp/resources.js +8 -0
- package/dist/mcp/server.js +196 -250
- package/dist/mcp/tools.js +25 -8
- package/dist/sdk/action-schema-parser.js +11 -5
- package/dist/sdk/client.js +1 -1
- package/dist/sdk/guidance.js +58 -35
- package/dist/sdk/index.js +8 -7
- package/dist/sdk/knowledge.js +99 -1938
- package/dist/sdk/quality-gates.js +60 -336
- package/dist/sdk/validation-rules.js +33 -0
- package/dist/sdk/workflow-fixer.js +29 -360
- package/dist/sdk/workflow-intent.js +43 -3
- package/docs/dashboard-operations.md +35 -0
- package/docs/ema-user-guide.md +66 -0
- package/docs/mcp-tools-guide.md +40 -3
- package/package.json +1 -2
- package/resources/action-schema.json +0 -5678
|
@@ -1,386 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quality Gates - Pre-Deploy Validation
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* DEPRECATED: This module used detectWorkflowIssues which has been removed.
|
|
5
|
+
*
|
|
6
|
+
* The LLM should analyze workflows using rules from:
|
|
7
|
+
* - ema://rules/anti-patterns
|
|
8
|
+
* - ema://rules/input-sources
|
|
9
|
+
* - ema://rules/optimizations
|
|
10
|
+
*
|
|
11
|
+
* Backend validation happens when workflow is deployed.
|
|
12
|
+
* These quality gates are kept for backwards compatibility but are minimal.
|
|
6
13
|
*/
|
|
7
|
-
import { detectWorkflowIssues } from "./knowledge.js";
|
|
8
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
-
// Quality
|
|
15
|
+
// Minimal Quality Gates (structural only)
|
|
10
16
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
-
const QUALITY_GATES = [
|
|
12
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
-
// CRITICAL GATES (Blocking)
|
|
14
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
-
{
|
|
16
|
-
id: "has_trigger",
|
|
17
|
-
name: "Has Entry Point",
|
|
18
|
-
description: "Workflow must have a trigger node as entry point",
|
|
19
|
-
blocking: true,
|
|
20
|
-
severity: "critical",
|
|
21
|
-
check: (workflow) => {
|
|
22
|
-
const actions = (workflow.actions || []);
|
|
23
|
-
const hasTrigger = actions.some(a => {
|
|
24
|
-
const actionName = a.action?.name;
|
|
25
|
-
const name = (actionName?.name || "").toLowerCase();
|
|
26
|
-
return name.includes("trigger");
|
|
27
|
-
});
|
|
28
|
-
return {
|
|
29
|
-
passed: hasTrigger,
|
|
30
|
-
message: hasTrigger
|
|
31
|
-
? "Workflow has a valid trigger"
|
|
32
|
-
: "Missing trigger node - workflow has no entry point",
|
|
33
|
-
};
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "has_output",
|
|
38
|
-
name: "Has Workflow Output",
|
|
39
|
-
description: "Workflow must have at least one WORKFLOW_OUTPUT defined",
|
|
40
|
-
blocking: true,
|
|
41
|
-
severity: "critical",
|
|
42
|
-
check: (workflow) => {
|
|
43
|
-
const results = workflow.results;
|
|
44
|
-
const hasOutput = results && Object.keys(results).length > 0;
|
|
45
|
-
return {
|
|
46
|
-
passed: !!hasOutput,
|
|
47
|
-
message: hasOutput
|
|
48
|
-
? `Workflow has ${Object.keys(results).length} output(s) defined`
|
|
49
|
-
: "No WORKFLOW_OUTPUT defined - responses won't reach users",
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
id: "no_critical_issues",
|
|
55
|
-
name: "No Critical Issues",
|
|
56
|
-
description: "Workflow must not have any critical structural issues",
|
|
57
|
-
blocking: true,
|
|
58
|
-
severity: "critical",
|
|
59
|
-
check: (workflow) => {
|
|
60
|
-
const issues = detectWorkflowIssues(workflow);
|
|
61
|
-
const criticalIssues = issues.filter((i) => i.severity === "critical");
|
|
62
|
-
return {
|
|
63
|
-
passed: criticalIssues.length === 0,
|
|
64
|
-
message: criticalIssues.length === 0
|
|
65
|
-
? "No critical issues detected"
|
|
66
|
-
: `${criticalIssues.length} critical issue(s) found`,
|
|
67
|
-
details: criticalIssues.map((i) => `${i.type}: ${i.reason}`),
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
},
|
|
17
|
+
export const QUALITY_GATES = [
|
|
71
18
|
{
|
|
72
|
-
id: "
|
|
73
|
-
name: "
|
|
74
|
-
description: "
|
|
19
|
+
id: "has_actions",
|
|
20
|
+
name: "Has Actions",
|
|
21
|
+
description: "Workflow must have at least one action",
|
|
75
22
|
blocking: true,
|
|
76
23
|
severity: "critical",
|
|
77
24
|
check: (workflow) => {
|
|
78
25
|
const actions = (workflow.actions || []);
|
|
79
|
-
const results = workflow.results;
|
|
80
|
-
if (!results || Object.keys(results).length === 0) {
|
|
81
|
-
return { passed: false, message: "No outputs to check" };
|
|
82
|
-
}
|
|
83
|
-
// Get all output nodes
|
|
84
|
-
const outputNodes = new Set(Object.values(results).map((r) => r.actionName));
|
|
85
|
-
// Build reverse adjacency (who feeds into whom)
|
|
86
|
-
const feedsInto = new Map();
|
|
87
|
-
for (const action of actions) {
|
|
88
|
-
const name = action.name;
|
|
89
|
-
feedsInto.set(name, new Set());
|
|
90
|
-
const inputs = action.inputs;
|
|
91
|
-
if (inputs) {
|
|
92
|
-
for (const binding of Object.values(inputs)) {
|
|
93
|
-
const bindingObj = binding;
|
|
94
|
-
const actionOutput = bindingObj?.actionOutput;
|
|
95
|
-
if (actionOutput?.actionName) {
|
|
96
|
-
const source = actionOutput.actionName;
|
|
97
|
-
if (!feedsInto.has(source)) {
|
|
98
|
-
feedsInto.set(source, new Set());
|
|
99
|
-
}
|
|
100
|
-
feedsInto.get(source).add(name);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Check if each output node is reachable from trigger
|
|
106
|
-
const reachableFromTrigger = new Set();
|
|
107
|
-
const queue = ["trigger"];
|
|
108
|
-
while (queue.length > 0) {
|
|
109
|
-
const current = queue.shift();
|
|
110
|
-
if (reachableFromTrigger.has(current))
|
|
111
|
-
continue;
|
|
112
|
-
reachableFromTrigger.add(current);
|
|
113
|
-
const children = feedsInto.get(current) || new Set();
|
|
114
|
-
for (const child of children) {
|
|
115
|
-
if (!reachableFromTrigger.has(child)) {
|
|
116
|
-
queue.push(child);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const unreachableOutputs = Array.from(outputNodes).filter(n => !reachableFromTrigger.has(n));
|
|
121
|
-
return {
|
|
122
|
-
passed: unreachableOutputs.length === 0,
|
|
123
|
-
message: unreachableOutputs.length === 0
|
|
124
|
-
? "All output nodes are reachable from trigger"
|
|
125
|
-
: `${unreachableOutputs.length} output node(s) unreachable from trigger`,
|
|
126
|
-
details: unreachableOutputs.length > 0
|
|
127
|
-
? [`Unreachable: ${unreachableOutputs.join(", ")}`]
|
|
128
|
-
: undefined,
|
|
129
|
-
};
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
-
// WARNING GATES (Non-blocking but important)
|
|
134
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
135
|
-
{
|
|
136
|
-
id: "hitl_for_side_effects",
|
|
137
|
-
name: "HITL for Side Effects",
|
|
138
|
-
description: "Actions with external side effects should have human approval",
|
|
139
|
-
blocking: false,
|
|
140
|
-
severity: "warning",
|
|
141
|
-
check: (workflow) => {
|
|
142
|
-
const actions = (workflow.actions || []);
|
|
143
|
-
const sideEffectNodes = actions.filter(a => {
|
|
144
|
-
const actionName = a.action?.name;
|
|
145
|
-
const name = (actionName?.name || "").toLowerCase();
|
|
146
|
-
return name.includes("email") ||
|
|
147
|
-
name.includes("external") ||
|
|
148
|
-
name.includes("create_") ||
|
|
149
|
-
name.includes("update_") ||
|
|
150
|
-
name.includes("delete_");
|
|
151
|
-
});
|
|
152
|
-
const hitlNodes = new Set(actions
|
|
153
|
-
.filter(a => {
|
|
154
|
-
const actionName = a.action?.name;
|
|
155
|
-
const name = (actionName?.name || "").toLowerCase();
|
|
156
|
-
return name.includes("hitl");
|
|
157
|
-
})
|
|
158
|
-
.map(a => a.name));
|
|
159
|
-
// Check if side effect nodes have HITL upstream
|
|
160
|
-
const unprotected = sideEffectNodes.filter(a => {
|
|
161
|
-
const inputs = a.inputs;
|
|
162
|
-
if (!inputs)
|
|
163
|
-
return true;
|
|
164
|
-
// Check if any input comes from a HITL node
|
|
165
|
-
for (const binding of Object.values(inputs)) {
|
|
166
|
-
const bindingObj = binding;
|
|
167
|
-
const actionOutput = bindingObj?.actionOutput;
|
|
168
|
-
if (actionOutput?.actionName && hitlNodes.has(actionOutput.actionName)) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return true;
|
|
173
|
-
});
|
|
174
|
-
return {
|
|
175
|
-
passed: unprotected.length === 0,
|
|
176
|
-
message: unprotected.length === 0
|
|
177
|
-
? "All side-effect actions have HITL protection"
|
|
178
|
-
: `${unprotected.length} side-effect action(s) without HITL approval`,
|
|
179
|
-
details: unprotected.map(a => `${a.name}: Consider adding human approval before this action`),
|
|
180
|
-
};
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
id: "categorizer_has_fallback",
|
|
185
|
-
name: "Categorizer Has Fallback",
|
|
186
|
-
description: "Intent categorizers should have a Fallback category",
|
|
187
|
-
blocking: false,
|
|
188
|
-
severity: "warning",
|
|
189
|
-
check: (workflow) => {
|
|
190
|
-
const enumTypes = (workflow.enumTypes || []);
|
|
191
|
-
const missingFallback = enumTypes.filter(e => {
|
|
192
|
-
if (!e.values)
|
|
193
|
-
return false;
|
|
194
|
-
return !e.values.some(v => v.name.toLowerCase() === "fallback");
|
|
195
|
-
});
|
|
196
26
|
return {
|
|
197
|
-
passed:
|
|
198
|
-
message:
|
|
199
|
-
?
|
|
200
|
-
:
|
|
201
|
-
details: missingFallback.map(e => `${e.name}: Add Fallback category for unmatched intents`),
|
|
27
|
+
passed: actions.length > 0,
|
|
28
|
+
message: actions.length > 0
|
|
29
|
+
? `Workflow has ${actions.length} action(s)`
|
|
30
|
+
: "Workflow has no actions",
|
|
202
31
|
};
|
|
203
32
|
},
|
|
204
33
|
},
|
|
205
34
|
{
|
|
206
|
-
id: "
|
|
207
|
-
name: "
|
|
208
|
-
description: "
|
|
35
|
+
id: "has_results",
|
|
36
|
+
name: "Has Results Mapping",
|
|
37
|
+
description: "Voice AI workflows must have results mapping",
|
|
209
38
|
blocking: false,
|
|
210
39
|
severity: "warning",
|
|
211
40
|
check: (workflow) => {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
return {
|
|
215
|
-
passed: orphans.length === 0,
|
|
216
|
-
message: orphans.length === 0
|
|
217
|
-
? "No orphan nodes detected"
|
|
218
|
-
: `${orphans.length} orphan node(s) found`,
|
|
219
|
-
details: orphans.map((i) => `${i.node}: ${i.reason}`),
|
|
220
|
-
};
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
-
// INFO GATES (Best practices)
|
|
225
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
226
|
-
{
|
|
227
|
-
id: "reasonable_complexity",
|
|
228
|
-
name: "Reasonable Complexity",
|
|
229
|
-
description: "Workflow should not be overly complex",
|
|
230
|
-
blocking: false,
|
|
231
|
-
severity: "info",
|
|
232
|
-
check: (workflow) => {
|
|
233
|
-
const actions = (workflow.actions || []);
|
|
234
|
-
const nodeCount = actions.length;
|
|
235
|
-
// Calculate branching factor
|
|
236
|
-
let branches = 0;
|
|
237
|
-
for (const action of actions) {
|
|
238
|
-
const runIf = action.runIf;
|
|
239
|
-
if (runIf)
|
|
240
|
-
branches++;
|
|
241
|
-
}
|
|
242
|
-
const isComplex = nodeCount > 20 || branches > 10;
|
|
41
|
+
const results = workflow.results;
|
|
42
|
+
const hasResults = results && Object.keys(results).length > 0;
|
|
243
43
|
return {
|
|
244
|
-
passed:
|
|
245
|
-
message:
|
|
246
|
-
? `
|
|
247
|
-
:
|
|
248
|
-
details: isComplex
|
|
249
|
-
? ["Consider breaking into smaller sub-workflows for maintainability"]
|
|
250
|
-
: undefined,
|
|
44
|
+
passed: hasResults || false,
|
|
45
|
+
message: hasResults
|
|
46
|
+
? `Workflow has ${Object.keys(results).length} result mapping(s)`
|
|
47
|
+
: "No results mapping found (may be OK for chat workflows)",
|
|
251
48
|
};
|
|
252
49
|
},
|
|
253
50
|
},
|
|
254
51
|
];
|
|
255
52
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
256
|
-
//
|
|
53
|
+
// Quality Gate Runner
|
|
257
54
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
const warnings = [];
|
|
264
|
-
const passed = [];
|
|
265
|
-
for (const gate of QUALITY_GATES) {
|
|
55
|
+
export function runQualityGates(workflow, gates = QUALITY_GATES) {
|
|
56
|
+
const results = [];
|
|
57
|
+
let blockingFailures = 0;
|
|
58
|
+
let warnings = 0;
|
|
59
|
+
for (const gate of gates) {
|
|
266
60
|
const result = gate.check(workflow);
|
|
267
|
-
|
|
61
|
+
results.push({
|
|
268
62
|
gate_id: gate.id,
|
|
269
63
|
gate_name: gate.name,
|
|
270
64
|
result,
|
|
65
|
+
blocking: gate.blocking,
|
|
271
66
|
severity: gate.severity,
|
|
272
|
-
};
|
|
67
|
+
});
|
|
273
68
|
if (!result.passed) {
|
|
274
69
|
if (gate.blocking) {
|
|
275
|
-
blockingFailures
|
|
70
|
+
blockingFailures++;
|
|
276
71
|
}
|
|
277
|
-
else {
|
|
278
|
-
warnings
|
|
72
|
+
else if (gate.severity === "warning") {
|
|
73
|
+
warnings++;
|
|
279
74
|
}
|
|
280
75
|
}
|
|
281
|
-
else {
|
|
282
|
-
passed.push(report);
|
|
283
|
-
}
|
|
284
76
|
}
|
|
285
|
-
const
|
|
286
|
-
? "fail"
|
|
287
|
-
: warnings.length > 0
|
|
288
|
-
? "warning"
|
|
289
|
-
: "pass";
|
|
290
|
-
const summary = generateSummary(blockingFailures, warnings, passed);
|
|
77
|
+
const deploymentAllowed = blockingFailures === 0;
|
|
291
78
|
return {
|
|
292
|
-
overall_status:
|
|
79
|
+
overall_status: blockingFailures > 0 ? "fail" : warnings > 0 ? "warning" : "pass",
|
|
80
|
+
gates: results,
|
|
293
81
|
blocking_failures: blockingFailures,
|
|
294
82
|
warnings,
|
|
295
|
-
|
|
296
|
-
summary,
|
|
297
|
-
deploy_allowed: blockingFailures.length === 0,
|
|
83
|
+
deployment_allowed: deploymentAllowed,
|
|
298
84
|
};
|
|
299
85
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Quick check if a workflow passes all blocking gates
|
|
302
|
-
*/
|
|
303
|
-
export function canDeploy(workflow) {
|
|
304
|
-
for (const gate of QUALITY_GATES) {
|
|
305
|
-
if (gate.blocking) {
|
|
306
|
-
const result = gate.check(workflow);
|
|
307
|
-
if (!result.passed) {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Get list of all quality gates
|
|
316
|
-
*/
|
|
317
|
-
export function getQualityGates() {
|
|
318
|
-
return [...QUALITY_GATES];
|
|
319
|
-
}
|
|
320
86
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
321
|
-
//
|
|
87
|
+
// Utility Functions
|
|
322
88
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
if (passed.length > 0) {
|
|
332
|
-
parts.push(`🟢 ${passed.length} check(s) passed`);
|
|
333
|
-
}
|
|
334
|
-
if (failures.length === 0) {
|
|
335
|
-
parts.push("✅ Workflow is ready for deployment");
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
parts.push("❌ Deployment blocked - fix critical issues first");
|
|
339
|
-
}
|
|
340
|
-
return parts.join("\n");
|
|
89
|
+
export function isDeploymentAllowed(report) {
|
|
90
|
+
return report.deployment_allowed;
|
|
91
|
+
}
|
|
92
|
+
export function getBlockingIssues(report) {
|
|
93
|
+
return report.gates
|
|
94
|
+
.filter(g => g.blocking && !g.result.passed)
|
|
95
|
+
.map(g => `${g.gate_name}: ${g.result.message}`);
|
|
341
96
|
}
|
|
342
|
-
/**
|
|
343
|
-
* Get a human-readable quality report
|
|
344
|
-
*/
|
|
345
97
|
export function formatQualityReport(report) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
for (const failure of report.blocking_failures) {
|
|
355
|
-
lines.push(`- **${failure.gate_name}**: ${failure.result.message}`);
|
|
356
|
-
if (failure.result.details) {
|
|
357
|
-
for (const detail of failure.result.details) {
|
|
358
|
-
lines.push(` - ${detail}`);
|
|
359
|
-
}
|
|
98
|
+
let output = `Quality Report: ${report.overall_status.toUpperCase()}\n`;
|
|
99
|
+
output += `Deployment Allowed: ${report.deployment_allowed ? "YES" : "NO"}\n\n`;
|
|
100
|
+
for (const gate of report.gates) {
|
|
101
|
+
const status = gate.result.passed ? "✓" : gate.blocking ? "✗" : "⚠";
|
|
102
|
+
output += `${status} ${gate.gate_name}: ${gate.result.message}\n`;
|
|
103
|
+
if (gate.result.details) {
|
|
104
|
+
for (const detail of gate.result.details) {
|
|
105
|
+
output += ` - ${detail}\n`;
|
|
360
106
|
}
|
|
361
107
|
}
|
|
362
|
-
lines.push("");
|
|
363
|
-
}
|
|
364
|
-
if (report.warnings.length > 0) {
|
|
365
|
-
lines.push("### 🟡 Warnings");
|
|
366
|
-
for (const warning of report.warnings) {
|
|
367
|
-
lines.push(`- **${warning.gate_name}**: ${warning.result.message}`);
|
|
368
|
-
if (warning.result.details) {
|
|
369
|
-
for (const detail of warning.result.details) {
|
|
370
|
-
lines.push(` - ${detail}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
lines.push("");
|
|
375
|
-
}
|
|
376
|
-
if (report.passed.length > 0) {
|
|
377
|
-
lines.push("### 🟢 Passed");
|
|
378
|
-
for (const pass of report.passed) {
|
|
379
|
-
lines.push(`- ${pass.gate_name}`);
|
|
380
|
-
}
|
|
381
108
|
}
|
|
382
|
-
|
|
383
|
-
lines.push("---");
|
|
384
|
-
lines.push(report.summary);
|
|
385
|
-
return lines.join("\n");
|
|
109
|
+
return output;
|
|
386
110
|
}
|
|
@@ -255,6 +255,39 @@ export const ANTI_PATTERNS = [
|
|
|
255
255
|
},
|
|
256
256
|
severity: "critical",
|
|
257
257
|
},
|
|
258
|
+
{
|
|
259
|
+
id: "stateless-response-node",
|
|
260
|
+
name: "Stateless Response Node",
|
|
261
|
+
pattern: "Response node receives only user_query without chat_conversation",
|
|
262
|
+
problem: "The final LLM cannot see the conversation history, only the latest message. " +
|
|
263
|
+
"This causes repeated questions - when user answers 'My DOB is Jan 1, 1980', the LLM " +
|
|
264
|
+
"doesn't see it asked the question, so it asks again. Multi-turn context is lost.",
|
|
265
|
+
solution: "Wire trigger.chat_conversation into the response node's named_inputs or history input. " +
|
|
266
|
+
"The LLM needs to see the full conversation transcript to maintain context.",
|
|
267
|
+
detection: {
|
|
268
|
+
issueType: "stateless_response",
|
|
269
|
+
condition: "Response node (respond_for_external_actions, respond_with_sources, call_llm as responder) " +
|
|
270
|
+
"receives trigger.user_query but NOT trigger.chat_conversation",
|
|
271
|
+
},
|
|
272
|
+
severity: "warning",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "search-without-data-sources",
|
|
276
|
+
name: "Search Without Data Sources",
|
|
277
|
+
pattern: "Workflow has search node but no data sources uploaded",
|
|
278
|
+
problem: "The workflow includes a search/v2 node to query knowledge base, but no documents have been " +
|
|
279
|
+
"uploaded to the persona. Search will return empty results, making the RAG workflow useless. " +
|
|
280
|
+
"The AI will have no context to answer questions.",
|
|
281
|
+
solution: "Upload data sources BEFORE deploying or testing the workflow:\n" +
|
|
282
|
+
"1. persona(id='...', data={method:'upload', path:'/path/to/document.pdf'})\n" +
|
|
283
|
+
"2. Enable embedding if not already: persona(id='...', data={method:'embedding', enabled:true})\n" +
|
|
284
|
+
"3. Wait for indexing to complete: persona(id='...', data={method:'stats'}) → check 'success' count",
|
|
285
|
+
detection: {
|
|
286
|
+
issueType: "search_without_data",
|
|
287
|
+
condition: "Workflow contains search/v2 node AND persona has 0 data sources uploaded (check via data.stats)",
|
|
288
|
+
},
|
|
289
|
+
severity: "critical",
|
|
290
|
+
},
|
|
258
291
|
];
|
|
259
292
|
export const OPTIMIZATION_RULES = [
|
|
260
293
|
{
|