@ema.co/mcp-toolkit 2026.2.19 → 2026.2.23
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/cli/index.js +2 -2
- package/dist/mcp/domain/loop-detection.js +46 -54
- package/dist/mcp/domain/sanitizer.js +1 -1
- package/dist/mcp/domain/workflow-graph.js +2 -2
- package/dist/mcp/domain/workflow-path-enumerator.js +7 -4
- package/dist/mcp/guidance.js +53 -0
- package/dist/mcp/handlers/debug/adapter.js +15 -0
- package/dist/mcp/handlers/debug/formatters.js +282 -0
- package/dist/mcp/handlers/debug/index.js +133 -0
- package/dist/mcp/handlers/demo/adapter.js +180 -0
- package/dist/mcp/handlers/env/config.js +2 -2
- package/dist/mcp/handlers/index.js +0 -1
- package/dist/mcp/handlers/persona/adapter.js +135 -0
- package/dist/mcp/handlers/sync/adapter.js +200 -0
- package/dist/mcp/handlers/workflow/adapter.js +174 -0
- package/dist/mcp/handlers/workflow/fix.js +11 -12
- package/dist/mcp/handlers/workflow/index.js +0 -24
- package/dist/mcp/knowledge-guidance-topics.js +615 -0
- package/dist/mcp/knowledge.js +23 -612
- package/dist/mcp/resources-dynamic.js +2395 -0
- package/dist/mcp/resources-validation.js +408 -0
- package/dist/mcp/resources.js +72 -2724
- package/dist/mcp/server.js +33 -832
- package/dist/mcp/tools.js +104 -2
- package/dist/sdk/client-adapter.js +265 -24
- package/dist/sdk/ema-client.js +100 -9
- package/dist/sdk/generated/well-known-types.js +99 -0
- package/dist/sdk/grpc-client.js +115 -1
- package/dist/sync/sdk.js +2 -2
- package/dist/sync.js +4 -3
- package/package.json +3 -2
- package/dist/mcp/handlers/knowledge/index.js +0 -54
package/dist/mcp/resources.js
CHANGED
|
@@ -21,23 +21,19 @@
|
|
|
21
21
|
* - Path traversal is blocked
|
|
22
22
|
* - Secret patterns are detected and blocked
|
|
23
23
|
* - Only ema:// URI scheme is supported
|
|
24
|
+
*
|
|
25
|
+
* Module structure:
|
|
26
|
+
* - resources.ts (this file): Registry, types, security, static resources
|
|
27
|
+
* - resources-dynamic.ts: DYNAMIC_RESOURCES array + fetching helpers
|
|
28
|
+
* - resources-validation.ts: Validation rules generators
|
|
24
29
|
*/
|
|
25
30
|
import * as fs from "fs";
|
|
26
31
|
import * as path from "path";
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Import knowledge catalogs for dynamic resources
|
|
33
|
-
import { AGENT_CATALOG, WORKFLOW_PATTERNS, WIDGET_CATALOG, ALL_DEPRECATED_ACTIONS, DEPRECATED_ACTIONS_WITH_REPLACEMENT, DEPRECATED_ACTIONS_NO_REPLACEMENT, WORKFLOW_ENABLING_CONSTRAINTS, MINIMUM_VIABLE_WORKFLOWS, } from "./knowledge.js";
|
|
34
|
-
import { INPUT_SOURCE_RULES, ANTI_PATTERNS, OPTIMIZATION_RULES } from "./domain/validation-rules.js";
|
|
35
|
-
import { STRUCTURAL_INVARIANTS } from "./domain/structural-rules.js";
|
|
36
|
-
import { mineConstraints, formatConstraintReport } from "./domain/proto-constraints.js";
|
|
37
|
-
import { EmaClient } from "../sdk/client.js";
|
|
38
|
-
import { APISchemaRegistry } from "./domain/workflow-validator.js";
|
|
39
|
-
import { loadConfigFromJsonEnv, loadConfigOptional, resolveBearerToken, getEnvByName, getMasterEnv, } from "../sdk/config.js";
|
|
40
|
-
import { VOICE_TEMPLATE_FALLBACK, VOICE_TEMPLATE_FIELD_DOCS, } from "../sdk/generated/template-fallbacks.js";
|
|
32
|
+
// Import dynamic resources and helpers
|
|
33
|
+
import { DYNAMIC_RESOURCES, getActionByName } from "./resources-dynamic.js";
|
|
34
|
+
// Re-export from split modules so external imports don't break
|
|
35
|
+
export { DYNAMIC_RESOURCES, getActionByName } from "./resources-dynamic.js";
|
|
36
|
+
export { generateValidationRulesForLLM, generateBestPracticesChecklist } from "./resources-validation.js";
|
|
41
37
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
38
|
// Security Utilities
|
|
43
39
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -67,2325 +63,75 @@ const SECRET_PATTERNS = [
|
|
|
67
63
|
const BLOCKED_FILES = [
|
|
68
64
|
".env",
|
|
69
65
|
".env.local",
|
|
70
|
-
".env.development",
|
|
71
|
-
".env.production",
|
|
72
|
-
".env.test",
|
|
73
|
-
"secrets.yaml",
|
|
74
|
-
"secrets.json",
|
|
75
|
-
".npmrc",
|
|
76
|
-
".pypirc",
|
|
77
|
-
"id_rsa",
|
|
78
|
-
"id_ed25519",
|
|
79
|
-
"*.pem",
|
|
80
|
-
"*.key",
|
|
81
|
-
];
|
|
82
|
-
function containsSecrets(content) {
|
|
83
|
-
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
|
|
84
|
-
}
|
|
85
|
-
function isBlockedFile(filename) {
|
|
86
|
-
const basename = path.basename(filename);
|
|
87
|
-
return BLOCKED_FILES.some((blocked) => {
|
|
88
|
-
if (blocked.startsWith("*")) {
|
|
89
|
-
return basename.endsWith(blocked.slice(1));
|
|
90
|
-
}
|
|
91
|
-
return basename === blocked;
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
function hasPathTraversal(uriPath) {
|
|
95
|
-
return uriPath.includes("..") || uriPath.includes("//");
|
|
96
|
-
}
|
|
97
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
-
// Resource Registry
|
|
99
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
-
// Allowlisted resource mappings: uri -> relative file path
|
|
101
|
-
// Only includes files that ACTUALLY exist in this repo (not symlinked)
|
|
102
|
-
// and are useful for MCP consumers
|
|
103
|
-
const RESOURCE_MAP = {
|
|
104
|
-
// Core Documentation (public guides)
|
|
105
|
-
"ema://docs/getting-started": {
|
|
106
|
-
path: ".context/public/guides/mcp-tools-guide.md",
|
|
107
|
-
description: "Quick start guide: first steps, key concepts, essential rules, common operations",
|
|
108
|
-
mimeType: "text/markdown",
|
|
109
|
-
},
|
|
110
|
-
"ema://docs/readme": {
|
|
111
|
-
path: "README.md",
|
|
112
|
-
description: "Ema Toolkit overview: MCP tools, prompts, resources, CLI usage, SDK API, configuration",
|
|
113
|
-
mimeType: "text/markdown",
|
|
114
|
-
},
|
|
115
|
-
"ema://docs/ema-user-guide": {
|
|
116
|
-
path: ".context/public/guides/ema-user-guide.md",
|
|
117
|
-
description: "Ema Platform guide: concepts (AI Employees, Workflows, Agents), glossary, architecture, examples, troubleshooting",
|
|
118
|
-
mimeType: "text/markdown",
|
|
119
|
-
},
|
|
120
|
-
"ema://docs/mcp-tools-guide": {
|
|
121
|
-
path: ".context/public/guides/mcp-tools-guide.md",
|
|
122
|
-
description: "MCP tools usage guide: tool categories, best practices, example call sequences, workflow review patterns",
|
|
123
|
-
mimeType: "text/markdown",
|
|
124
|
-
},
|
|
125
|
-
// Templates - REMOVED (file-backed templates removed in favor of API + generated fallbacks)
|
|
126
|
-
// Use: client.getPersonaTemplates() for live API templates
|
|
127
|
-
// Use: getTemplateFallback("voice") from sdk/generated/template-fallbacks.ts for offline fallbacks
|
|
128
|
-
// See: ema://catalog/persona-templates for dynamic API-backed template listing
|
|
129
|
-
// DEPRECATED: Use ema://docs/usage-guide (dynamic) instead
|
|
130
|
-
// This static resource is kept for backwards compatibility but now generates from guidance module
|
|
131
|
-
};
|
|
132
|
-
// Legacy alias - redirect to dynamic resource
|
|
133
|
-
const LEGACY_STATIC_REDIRECTS = {
|
|
134
|
-
"ema://rules/mcp-usage": "ema://docs/usage-guide",
|
|
135
|
-
};
|
|
136
|
-
const DYNAMIC_RESOURCES = [
|
|
137
|
-
// Agent Catalog - Dynamic list of all workflow agents
|
|
138
|
-
{
|
|
139
|
-
uri: "ema://catalog/agents",
|
|
140
|
-
name: "catalog/agents",
|
|
141
|
-
description: "Complete agent catalog: all available workflow agents with inputs, outputs, critical rules, and usage guidance",
|
|
142
|
-
mimeType: "application/json",
|
|
143
|
-
generate: async (ctx) => JSON.stringify(await getDynamicAgentCatalog({ env: ctx.env }), null, 2),
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
uri: "ema://catalog/agents-summary",
|
|
147
|
-
name: "catalog/agents-summary",
|
|
148
|
-
description: "Agent catalog summary: agent names, categories, and brief descriptions for quick reference",
|
|
149
|
-
mimeType: "text/markdown",
|
|
150
|
-
generate: async (ctx) => {
|
|
151
|
-
const agents = await getDynamicAgentCatalog({ env: ctx.env });
|
|
152
|
-
const byCategory = new Map();
|
|
153
|
-
for (const agent of agents) {
|
|
154
|
-
const cat = agent.category || "other";
|
|
155
|
-
if (!byCategory.has(cat))
|
|
156
|
-
byCategory.set(cat, []);
|
|
157
|
-
byCategory.get(cat).push(agent);
|
|
158
|
-
}
|
|
159
|
-
let md = "# Ema Agent Catalog\n\n";
|
|
160
|
-
md += `> ${agents.length} agents available for workflow composition\n\n`;
|
|
161
|
-
for (const [category, agents] of byCategory) {
|
|
162
|
-
md += `## ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
|
|
163
|
-
md += "| Agent | Description | Key Inputs | Key Outputs |\n";
|
|
164
|
-
md += "|-------|-------------|------------|-------------|\n";
|
|
165
|
-
for (const agent of agents) {
|
|
166
|
-
const inputs = agent.inputs?.slice(0, 2).map((i) => i.name).join(", ") || "-";
|
|
167
|
-
const outputs = agent.outputs?.slice(0, 2).map((o) => o.name).join(", ") || "-";
|
|
168
|
-
md += `| \`${agent.actionName}\` | ${agent.description?.slice(0, 60) || "-"}... | ${inputs} | ${outputs} |\n`;
|
|
169
|
-
}
|
|
170
|
-
md += "\n";
|
|
171
|
-
}
|
|
172
|
-
return md;
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
// Workflow Patterns - Common workflow templates
|
|
176
|
-
{
|
|
177
|
-
uri: "ema://catalog/patterns",
|
|
178
|
-
name: "catalog/patterns",
|
|
179
|
-
description: "Workflow patterns: common workflow structures (kb-search, intent-routing, tool-calling) with examples",
|
|
180
|
-
mimeType: "application/json",
|
|
181
|
-
generate: async () => JSON.stringify(WORKFLOW_PATTERNS, null, 2),
|
|
182
|
-
},
|
|
183
|
-
// Widget Catalog - UI configuration widgets
|
|
184
|
-
{
|
|
185
|
-
uri: "ema://catalog/widgets",
|
|
186
|
-
name: "catalog/widgets",
|
|
187
|
-
description: "Widget catalog: proto_config widget types for voice, chat, and dashboard personas",
|
|
188
|
-
mimeType: "application/json",
|
|
189
|
-
generate: async () => JSON.stringify(WIDGET_CATALOG, null, 2),
|
|
190
|
-
},
|
|
191
|
-
// Validation Rules - Input source and anti-pattern rules
|
|
192
|
-
{
|
|
193
|
-
uri: "ema://rules/input-sources",
|
|
194
|
-
name: "rules/input-sources",
|
|
195
|
-
description: "Input source validation rules: which agent inputs accept which data types (user_query vs chat_conversation)",
|
|
196
|
-
mimeType: "application/json",
|
|
197
|
-
generate: async () => JSON.stringify(INPUT_SOURCE_RULES, null, 2),
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
uri: "ema://rules/anti-patterns",
|
|
201
|
-
name: "rules/anti-patterns",
|
|
202
|
-
description: "Anti-patterns to avoid: common workflow mistakes and how to prevent them",
|
|
203
|
-
mimeType: "application/json",
|
|
204
|
-
generate: async () => JSON.stringify(ANTI_PATTERNS, null, 2),
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
uri: "ema://rules/optimizations",
|
|
208
|
-
name: "rules/optimizations",
|
|
209
|
-
description: "Optimization rules: workflow improvements for better performance and reliability",
|
|
210
|
-
mimeType: "application/json",
|
|
211
|
-
generate: async () => JSON.stringify(OPTIMIZATION_RULES, null, 2),
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
uri: "ema://rules/structural-invariants",
|
|
215
|
-
name: "rules/structural-invariants",
|
|
216
|
-
description: "Structural invariants: hard constraints that workflows must satisfy (no cycles, reachability, etc.)",
|
|
217
|
-
mimeType: "application/json",
|
|
218
|
-
generate: async () => JSON.stringify(STRUCTURAL_INVARIANTS, null, 2),
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
uri: "ema://validation/rules",
|
|
222
|
-
name: "validation/rules",
|
|
223
|
-
description: "Static validation rules: path enumeration, required outputs, multiple writers, response/abstain, category hierarchy. Use these rules DURING workflow generation to avoid errors.",
|
|
224
|
-
mimeType: "text/markdown",
|
|
225
|
-
generate: async () => generateValidationRulesForLLM(),
|
|
226
|
-
},
|
|
227
|
-
// Best Practices - Auto-generated from SDK rules (SINGLE SOURCE OF TRUTH)
|
|
228
|
-
{
|
|
229
|
-
uri: "ema://docs/best-practices",
|
|
230
|
-
name: "docs/best-practices",
|
|
231
|
-
description: "Auto-generated best practices checklist from SDK validation rules. Use BEFORE deploying workflows.",
|
|
232
|
-
mimeType: "text/markdown",
|
|
233
|
-
generate: async () => generateBestPracticesChecklist(),
|
|
234
|
-
},
|
|
235
|
-
// Field Constraints - Mined from proto definitions
|
|
236
|
-
{
|
|
237
|
-
uri: "ema://docs/field-constraints",
|
|
238
|
-
name: "docs/field-constraints",
|
|
239
|
-
description: "Proto field constraints: immutable fields, read-only fields, creation-only fields, conditional fields. Mined from proto definitions.",
|
|
240
|
-
mimeType: "text/markdown",
|
|
241
|
-
generate: async () => {
|
|
242
|
-
const report = mineConstraints();
|
|
243
|
-
return formatConstraintReport(report);
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
// Deprecated Actions - API-first with fallback
|
|
247
|
-
{
|
|
248
|
-
uri: "ema://rules/deprecated-actions",
|
|
249
|
-
name: "rules/deprecated-actions",
|
|
250
|
-
description: "Deprecated actions list: actions to avoid in new workflows, with their replacements and migration notes",
|
|
251
|
-
mimeType: "application/json",
|
|
252
|
-
generate: async (ctx) => {
|
|
253
|
-
// Try API first using existing client infrastructure
|
|
254
|
-
let apiDeprecated = [];
|
|
255
|
-
let source = "fallback";
|
|
256
|
-
try {
|
|
257
|
-
const client = getClientForEnvName(ctx.env);
|
|
258
|
-
if (client) {
|
|
259
|
-
const actions = await client.listActions();
|
|
260
|
-
apiDeprecated = actions.filter(a => a.deprecated).map(a => a.id);
|
|
261
|
-
source = "api";
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
// API unavailable, use fallback
|
|
266
|
-
}
|
|
267
|
-
// Merge API deprecated with fallback replacement info
|
|
268
|
-
const result = {
|
|
269
|
-
_note: "Actions marked deprecated should not be used in new workflows. Use replacements where available.",
|
|
270
|
-
_source: source,
|
|
271
|
-
_api_deprecated_count: apiDeprecated.length,
|
|
272
|
-
// From API (current deprecated status)
|
|
273
|
-
api_deprecated: apiDeprecated.length > 0 ? apiDeprecated : undefined,
|
|
274
|
-
// From fallback (includes replacement info)
|
|
275
|
-
with_replacement: DEPRECATED_ACTIONS_WITH_REPLACEMENT.map(d => ({
|
|
276
|
-
action: d.action,
|
|
277
|
-
version: d.version,
|
|
278
|
-
replacement: d.replacement,
|
|
279
|
-
replacement_version: d.replacementVersion,
|
|
280
|
-
migration_notes: d.migrationNotes,
|
|
281
|
-
})),
|
|
282
|
-
no_replacement: DEPRECATED_ACTIONS_NO_REPLACEMENT.map(d => ({
|
|
283
|
-
action: d.action,
|
|
284
|
-
version: d.version,
|
|
285
|
-
environment: d.environment,
|
|
286
|
-
notes: d.migrationNotes,
|
|
287
|
-
})),
|
|
288
|
-
_tip: "Check workflow(mode='get') for deprecation_warnings on specific workflows",
|
|
289
|
-
};
|
|
290
|
-
return JSON.stringify(result, null, 2);
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
uri: "ema://rules/deprecated-actions-summary",
|
|
295
|
-
name: "rules/deprecated-actions-summary",
|
|
296
|
-
description: "Deprecated actions summary: quick reference table of deprecated actions and replacements",
|
|
297
|
-
mimeType: "text/markdown",
|
|
298
|
-
generate: async (ctx) => {
|
|
299
|
-
// Try API first using existing client infrastructure
|
|
300
|
-
let apiDeprecated = [];
|
|
301
|
-
let source = "fallback";
|
|
302
|
-
try {
|
|
303
|
-
const client = getClientForEnvName(ctx.env);
|
|
304
|
-
if (client) {
|
|
305
|
-
const actions = await client.listActions();
|
|
306
|
-
apiDeprecated = actions.filter(a => a.deprecated).map(a => a.id);
|
|
307
|
-
source = "api";
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// API unavailable, use fallback
|
|
312
|
-
}
|
|
313
|
-
let md = "# Deprecated Actions\n\n";
|
|
314
|
-
md += "> Do NOT use these actions in new workflows. Use replacements where available.\n\n";
|
|
315
|
-
md += `> Source: ${source} (${source === "api" ? "live" : "synced 2026-01-26"})\n\n`;
|
|
316
|
-
if (apiDeprecated.length > 0) {
|
|
317
|
-
md += "## From API (Current)\n\n";
|
|
318
|
-
md += "| Action ID | Status |\n";
|
|
319
|
-
md += "|-----------|--------|\n";
|
|
320
|
-
for (const id of apiDeprecated) {
|
|
321
|
-
md += `| \`${id}\` | DEPRECATED |\n`;
|
|
322
|
-
}
|
|
323
|
-
md += "\n";
|
|
324
|
-
}
|
|
325
|
-
md += "## Actions with Known Replacements\n\n";
|
|
326
|
-
md += "| Deprecated Action | Version | Replacement | Notes |\n";
|
|
327
|
-
md += "|-------------------|---------|-------------|-------|\n";
|
|
328
|
-
for (const d of DEPRECATED_ACTIONS_WITH_REPLACEMENT) {
|
|
329
|
-
const repl = Array.isArray(d.replacement) ? d.replacement.join(" or ") : d.replacement;
|
|
330
|
-
const replVer = d.replacementVersion ? ` ${d.replacementVersion}` : "";
|
|
331
|
-
md += `| \`${d.action}\` | ${d.version} | \`${repl}\`${replVer} | ${d.migrationNotes || "-"} |\n`;
|
|
332
|
-
}
|
|
333
|
-
md += "\n## Actions with No Known Replacement\n\n";
|
|
334
|
-
md += "| Action | Environment | Notes |\n";
|
|
335
|
-
md += "|--------|-------------|-------|\n";
|
|
336
|
-
for (const d of DEPRECATED_ACTIONS_NO_REPLACEMENT) {
|
|
337
|
-
md += `| \`${d.action}\` | ${d.environment || "all"} | ${d.migrationNotes || "-"} |\n`;
|
|
338
|
-
}
|
|
339
|
-
md += `\n**Total Known Deprecated**: ${ALL_DEPRECATED_ACTIONS.length} actions\n`;
|
|
340
|
-
md += `\n> **Best Practice**: Use \`workflow(mode="get")\` to check for deprecation warnings in specific workflows.\n`;
|
|
341
|
-
return md;
|
|
342
|
-
},
|
|
343
|
-
},
|
|
344
|
-
// Workflow Enabling Constraints - Requirements for persona activation
|
|
345
|
-
{
|
|
346
|
-
uri: "ema://rules/workflow-constraints",
|
|
347
|
-
name: "rules/workflow-constraints",
|
|
348
|
-
description: "Workflow enabling constraints: requirements that must be met before a persona can be activated",
|
|
349
|
-
mimeType: "application/json",
|
|
350
|
-
generate: async () => JSON.stringify({
|
|
351
|
-
_note: "These constraints are checked by the Ema backend when enabling a persona.",
|
|
352
|
-
_source: "ema/ema_backend/db/models/personas_model.py:672-756",
|
|
353
|
-
_last_synced: "2026-01-26",
|
|
354
|
-
enabling_constraints: WORKFLOW_ENABLING_CONSTRAINTS,
|
|
355
|
-
minimum_viable_workflows: MINIMUM_VIABLE_WORKFLOWS,
|
|
356
|
-
}, null, 2),
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
uri: "ema://rules/workflow-constraints-summary",
|
|
360
|
-
name: "rules/workflow-constraints-summary",
|
|
361
|
-
description: "Workflow constraints summary: checklist of requirements for enabling a persona",
|
|
362
|
-
mimeType: "text/markdown",
|
|
363
|
-
generate: async () => {
|
|
364
|
-
let md = "# Workflow Enabling Constraints\n\n";
|
|
365
|
-
md += "> These constraints must be satisfied for a persona to be activated.\n\n";
|
|
366
|
-
md += "> Source: ema_backend/db/models/personas_model.py (synced 2026-01-26)\n\n";
|
|
367
|
-
md += "## Required Checks\n\n";
|
|
368
|
-
md += "| # | Constraint | Error State | Fix |\n";
|
|
369
|
-
md += "|---|------------|-------------|-----|\n";
|
|
370
|
-
for (const c of WORKFLOW_ENABLING_CONSTRAINTS) {
|
|
371
|
-
const critical = c.critical ? "**" : "";
|
|
372
|
-
md += `| ${c.id} | ${critical}${c.name}${critical} | \`${c.errorState}\` | ${c.fix} |\n`;
|
|
373
|
-
}
|
|
374
|
-
md += "\n## Minimum Viable Workflows by Type\n\n";
|
|
375
|
-
for (const [type, mvw] of Object.entries(MINIMUM_VIABLE_WORKFLOWS)) {
|
|
376
|
-
md += `### ${type}\n\n`;
|
|
377
|
-
md += `**Structure**: \`${mvw.exampleStructure}\`\n\n`;
|
|
378
|
-
md += `- Required nodes: ${mvw.requiredNodes.map(n => `\`${n}\``).join(", ")}\n`;
|
|
379
|
-
md += `- Required outputs: ${mvw.requiredOutputs.map(o => `\`${o}\``).join(", ")}\n`;
|
|
380
|
-
if (mvw.requiredWidgets) {
|
|
381
|
-
md += `- Required widgets: ${mvw.requiredWidgets.map(w => `\`${w}\``).join(", ")}\n`;
|
|
382
|
-
}
|
|
383
|
-
if (mvw.notes) {
|
|
384
|
-
md += `- Notes: ${mvw.notes}\n`;
|
|
385
|
-
}
|
|
386
|
-
md += "\n";
|
|
387
|
-
}
|
|
388
|
-
md += "## Critical Rule: results Mapping\n\n";
|
|
389
|
-
md += "**Every workflow must have a `results` map connecting action outputs to the persona's output.**\n\n";
|
|
390
|
-
md += "### Chat / Voice Personas\n\n";
|
|
391
|
-
md += "Use `WORKFLOW_OUTPUT` as the key — this is the response shown to the user:\n\n";
|
|
392
|
-
md += "```json\n";
|
|
393
|
-
md += '{\n "results": {\n "WORKFLOW_OUTPUT": {\n "actionName": "respond_node",\n "outputName": "response_with_sources"\n }\n }\n}\n';
|
|
394
|
-
md += "```\n\n";
|
|
395
|
-
md += "Multiple response nodes (e.g., one per categorizer branch) can all map to `WORKFLOW_OUTPUT` — only the one that executes via `runIf` will produce output.\n\n";
|
|
396
|
-
md += "### Dashboard Personas\n\n";
|
|
397
|
-
md += "Use `<actionName>.<outputName>` dot-notation keys — each key becomes a column in the dashboard:\n\n";
|
|
398
|
-
md += "```json\n";
|
|
399
|
-
md += '{\n "results": {\n "entity_extraction_node.extraction_columns": {\n "actionName": "entity_extraction_node",\n "outputName": "extraction_columns"\n },\n "rule_validation_node.ruleset_output": {\n "actionName": "rule_validation_node",\n "outputName": "ruleset_output"\n }\n }\n}\n';
|
|
400
|
-
md += "```\n\n";
|
|
401
|
-
md += "Common dashboard result outputs: `extraction_columns`, `ruleset_output`, `llm_output`.\n";
|
|
402
|
-
return md;
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
// JSON Output Patterns - custom_agent + json_mapper best practices
|
|
406
|
-
{
|
|
407
|
-
uri: "ema://rules/json-output-patterns",
|
|
408
|
-
name: "rules/json-output-patterns",
|
|
409
|
-
description: "How to properly wire custom_agent JSON output to json_mapper and downstream nodes like send_email_agent",
|
|
410
|
-
mimeType: "text/markdown",
|
|
411
|
-
generate: async () => {
|
|
412
|
-
return `# custom_agent + json_mapper Pattern
|
|
413
|
-
|
|
414
|
-
## Problem
|
|
415
|
-
|
|
416
|
-
When using \`custom_agent\` to generate JSON that will be parsed by \`json_mapper\`, the output can be misformatted if not properly configured.
|
|
417
|
-
|
|
418
|
-
Without explicit \`output_fields\`, custom_agent returns the entire JSON as a string blob in \`response_with_sources\`, causing json_mapper to fail or produce incorrect results.
|
|
419
|
-
|
|
420
|
-
## Solution A: Define output_fields (RECOMMENDED)
|
|
421
|
-
|
|
422
|
-
Use \`output_fields\` with extraction columns matching your JSON keys:
|
|
423
|
-
|
|
424
|
-
\`\`\`json
|
|
425
|
-
{
|
|
426
|
-
"name": "email_gen",
|
|
427
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "custom_agent" }, "version": "v1" },
|
|
428
|
-
"inputs": {
|
|
429
|
-
"task_instructions": { "inline": { "wellKnown": { "textWithSources": {
|
|
430
|
-
"text": "Generate notification email with To, Subject, and Body fields.",
|
|
431
|
-
"sources": []
|
|
432
|
-
}}}},
|
|
433
|
-
"output_fields": {
|
|
434
|
-
"inline": { "array": { "values": [
|
|
435
|
-
{ "wellKnown": { "extractionColumn": { "id": "To", "name": "To", "dataType": 1 }}},
|
|
436
|
-
{ "wellKnown": { "extractionColumn": { "id": "Subject", "name": "Subject", "dataType": 1 }}},
|
|
437
|
-
{ "wellKnown": { "extractionColumn": { "id": "Body", "name": "Body", "dataType": 1 }}}
|
|
438
|
-
]}}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
\`\`\`
|
|
443
|
-
|
|
444
|
-
## Solution B: Strict Prompting + json_mapper
|
|
445
|
-
|
|
446
|
-
If not using \`output_fields\`, instruct the LLM to output RAW JSON only (no markdown):
|
|
447
|
-
|
|
448
|
-
\`\`\`
|
|
449
|
-
task_instructions: "Output ONLY the JSON object, no markdown, no explanation. Format: {\\"To\\": \\"...\\", \\"Subject\\": \\"...\\", \\"Body\\": \\"...\\"}"
|
|
450
|
-
\`\`\`
|
|
451
|
-
|
|
452
|
-
Then wire to \`json_mapper\` with matching \`pathIndices\`:
|
|
453
|
-
|
|
454
|
-
\`\`\`json
|
|
455
|
-
{
|
|
456
|
-
"name": "json_map",
|
|
457
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "json_mapper" }, "version": "v1" },
|
|
458
|
-
"inputs": {
|
|
459
|
-
"input_json": { "actionOutput": { "actionName": "email_gen", "output": "response_with_sources" }},
|
|
460
|
-
"json_mapper_config": {
|
|
461
|
-
"inline": { "wellKnown": { "jsonMapperConfig": {
|
|
462
|
-
"rules": [
|
|
463
|
-
{ "fieldName": "to", "pathIndices": [{"stringIndex": "To"}], "type": 3, "defaultValue": {"stringValue": ""} },
|
|
464
|
-
{ "fieldName": "subject", "pathIndices": [{"stringIndex": "Subject"}], "type": 3, "defaultValue": {"stringValue": ""} },
|
|
465
|
-
{ "fieldName": "body", "pathIndices": [{"stringIndex": "Body"}], "type": 3, "defaultValue": {"stringValue": ""} }
|
|
466
|
-
],
|
|
467
|
-
"sampleJson": "{\\"To\\": \\"email@example.com\\", \\"Subject\\": \\"Test\\", \\"Body\\": \\"<p>Hi</p>\\"}"
|
|
468
|
-
}}}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
\`\`\`
|
|
473
|
-
|
|
474
|
-
## Wiring json_mapper to fixed_response
|
|
475
|
-
|
|
476
|
-
json_mapper outputs extracted fields to \`output_json\`. Use \`fixed_response\` with template variables to convert to \`TEXT_WITH_SOURCES\`:
|
|
477
|
-
|
|
478
|
-
\`\`\`json
|
|
479
|
-
{
|
|
480
|
-
"name": "fmt_to",
|
|
481
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
|
|
482
|
-
"inputs": {
|
|
483
|
-
"template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{to}}", "sources": [] }}}},
|
|
484
|
-
"custom_data": { "actionOutput": { "actionName": "json_map", "output": "output_json" }}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
\`\`\`
|
|
488
|
-
|
|
489
|
-
## Complete Email Chain Pattern
|
|
490
|
-
|
|
491
|
-
\`\`\`
|
|
492
|
-
custom_agent (generates JSON)
|
|
493
|
-
→ json_mapper (extracts To, Subject, Body)
|
|
494
|
-
→ fixed_response ×3 ({{to}}, {{subject}}, {{body}})
|
|
495
|
-
→ send_email_agent
|
|
496
|
-
\`\`\`
|
|
497
|
-
|
|
498
|
-
## Critical Rules
|
|
499
|
-
|
|
500
|
-
| Rule | Why |
|
|
501
|
-
|------|-----|
|
|
502
|
-
| Use \`output_fields\` OR strict "ONLY JSON" prompt | Prevents markdown wrapping |
|
|
503
|
-
| json_mapper outputs to \`output_json\` | Not individual field outputs |
|
|
504
|
-
| Use \`fixed_response\` with \`{{fieldName}}\` template | Converts to TEXT_WITH_SOURCES |
|
|
505
|
-
| \`send_email_agent\` needs TEXT_WITH_SOURCES | Direct json_mapper output = type mismatch |
|
|
506
|
-
| \`pathIndices\` are case-sensitive | Must match JSON keys exactly |
|
|
507
|
-
| Include \`defaultValue\` in rules | Prevents null errors |
|
|
508
|
-
|
|
509
|
-
## Debugging
|
|
510
|
-
|
|
511
|
-
If json_mapper fails to extract:
|
|
512
|
-
1. Check if LLM is wrapping JSON in markdown (\\\`\\\`\\\`json ... \\\`\\\`\\\`)
|
|
513
|
-
2. Check pathIndices match actual JSON keys (case-sensitive)
|
|
514
|
-
3. Check custom_agent prompt instructs "ONLY JSON, no markdown"
|
|
515
|
-
4. Add defaultValue to prevent null propagation
|
|
516
|
-
`;
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
// Extraction column format - canonical API shape for entity_extraction / extraction_columns
|
|
520
|
-
{
|
|
521
|
-
uri: "ema://rules/extraction-column-format",
|
|
522
|
-
name: "rules/extraction-column-format",
|
|
523
|
-
description: "Canonical API format for extraction_columns (entity_extraction_with_documents, etc.). Use this shape when generating or fixing workflow_def; do not guess from catalog or local files.",
|
|
524
|
-
mimeType: "text/markdown",
|
|
525
|
-
generate: async () => {
|
|
526
|
-
return `# extraction_columns Format (API Shape)
|
|
527
|
-
|
|
528
|
-
## Rule
|
|
529
|
-
|
|
530
|
-
When generating or fixing workflow_def that includes \`entity_extraction_with_documents\` (or similar) nodes, the \`extraction_columns\` input **must** use this exact structure. The API rejects other shapes (e.g. \`extractionColumns.columns[{name, type}]\`).
|
|
531
|
-
|
|
532
|
-
## Required Shape
|
|
533
|
-
|
|
534
|
-
\`\`\`json
|
|
535
|
-
"extraction_columns": {
|
|
536
|
-
"inline": {
|
|
537
|
-
"array": {
|
|
538
|
-
"values": [
|
|
539
|
-
{ "wellKnown": { "extractionColumn": { "id": "<column_id>", "name": "<display name>", "description": "<optional>", "dataType": 1 } } },
|
|
540
|
-
{ "wellKnown": { "extractionColumn": { "id": "...", "name": "...", "description": "...", "dataType": 1 } } }
|
|
541
|
-
]
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
\`\`\`
|
|
546
|
-
|
|
547
|
-
- \`inline.array.values\`: array of column definitions.
|
|
548
|
-
- Each column: \`wellKnown.extractionColumn\` with \`id\`, \`name\`, optional \`description\`, \`dataType\` (1 = string typical).
|
|
549
|
-
- **Do not** use \`extractionColumns.columns\` or \`{ name, type, description }\` only — the API expects the \`array.values\` + \`wellKnown.extractionColumn\` shape.
|
|
550
|
-
|
|
551
|
-
## id vs name
|
|
552
|
-
|
|
553
|
-
- **id**: Unique identifier for the column (stable key; used in results/downstream). Can be \`col1\`, \`col5\`, \`service_date\`, or any unique string.
|
|
554
|
-
- **name**: Display/semantic name (LLM uses this to extract values). Can match id or differ (e.g. \`id: "col5"\`, \`name: "service_date"\` is valid).
|
|
555
|
-
- **id and name do not have to match.** Using generic ids (\`col1\`, \`col2\`) with semantic names (\`event_id\`, \`service_date\`) is correct.
|
|
556
|
-
|
|
557
|
-
## Duplicate ids
|
|
558
|
-
|
|
559
|
-
- **Within the same extraction_columns array**, every column must have a **unique \`id\`**. Duplicate \`id\` values in one array are invalid.
|
|
560
|
-
- Different actions can reuse the same id strings (e.g. \`event_extraction\` has \`col1\`..\`col6\`, \`validation_results\` has \`col1\`..\`col10\`) — each action has its own array.
|
|
561
|
-
|
|
562
|
-
## dataType Reference (PrimitiveType enum)
|
|
563
|
-
|
|
564
|
-
| Value | Type | Description |
|
|
565
|
-
|-------|------|-------------|
|
|
566
|
-
| 0 | UNKNOWN | Unspecified (avoid) |
|
|
567
|
-
| 1 | STRING | Text values (default) |
|
|
568
|
-
| 2 | NUMBER | Numeric values |
|
|
569
|
-
| 3 | BOOLEAN | True/false values |
|
|
570
|
-
| 4 | ARRAY | Array of repeated values (use with \`arrayElementType\`) |
|
|
571
|
-
| 5 | OBJECT | Grouped/nested structure (contains sub-columns via \`value.objectValue\`) |
|
|
572
|
-
| 6 | DATE | Date values (use with \`formattingOptions.dateFormat\`) |
|
|
573
|
-
| 7 | DOCUMENT_VERSION_ID | Reference to a document version |
|
|
574
|
-
|
|
575
|
-
**Note on enums:** To create an enumerated column, use \`dataType: 1\` (STRING) with \`isEnum: true\` and populate \`possibleValues\`.
|
|
576
|
-
|
|
577
|
-
## Object / Grouped Columns (dataType: 5)
|
|
578
|
-
|
|
579
|
-
Use \`dataType: 5\` to create a **Group** column with sub-columns (nested structure). In the UI, this appears as a parent column with expandable sub-columns.
|
|
580
|
-
|
|
581
|
-
The sub-columns are defined as a nested array in the column's \`value.objectValue.values\`. Each sub-column is a **bare \`ExtractionColumn\`** (no \`wellKnown\` wrapper — that wrapper only appears at the top-level \`extraction_columns.inline.array.values[]\`).
|
|
582
|
-
|
|
583
|
-
\`\`\`json
|
|
584
|
-
{
|
|
585
|
-
"wellKnown": {
|
|
586
|
-
"extractionColumn": {
|
|
587
|
-
"id": "address",
|
|
588
|
-
"name": "Address",
|
|
589
|
-
"description": "Structured address fields",
|
|
590
|
-
"dataType": 5,
|
|
591
|
-
"value": {
|
|
592
|
-
"objectValue": {
|
|
593
|
-
"values": [
|
|
594
|
-
{
|
|
595
|
-
"id": "street",
|
|
596
|
-
"name": "Street Name",
|
|
597
|
-
"description": "Street name",
|
|
598
|
-
"dataType": 1,
|
|
599
|
-
"detailedDescription": "",
|
|
600
|
-
"isEnum": false,
|
|
601
|
-
"possibleValues": [],
|
|
602
|
-
"dependencies": [],
|
|
603
|
-
"fileSources": [],
|
|
604
|
-
"auxiliarySources": []
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
"id": "city",
|
|
608
|
-
"name": "City",
|
|
609
|
-
"dataType": 1,
|
|
610
|
-
"detailedDescription": "",
|
|
611
|
-
"isEnum": false,
|
|
612
|
-
"possibleValues": [],
|
|
613
|
-
"dependencies": [],
|
|
614
|
-
"fileSources": [],
|
|
615
|
-
"auxiliarySources": []
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
"id": "postal_code",
|
|
619
|
-
"name": "Postal Code",
|
|
620
|
-
"dataType": 1,
|
|
621
|
-
"detailedDescription": "",
|
|
622
|
-
"isEnum": false,
|
|
623
|
-
"possibleValues": [],
|
|
624
|
-
"dependencies": [],
|
|
625
|
-
"fileSources": [],
|
|
626
|
-
"auxiliarySources": []
|
|
627
|
-
}
|
|
628
|
-
]
|
|
629
|
-
}
|
|
630
|
-
},
|
|
631
|
-
"arrayElementType": {
|
|
632
|
-
"id": "address",
|
|
633
|
-
"name": "Address",
|
|
634
|
-
"dataType": 5,
|
|
635
|
-
"detailedDescription": "",
|
|
636
|
-
"isEnum": false,
|
|
637
|
-
"possibleValues": [],
|
|
638
|
-
"dependencies": [],
|
|
639
|
-
"fileSources": [],
|
|
640
|
-
"auxiliarySources": []
|
|
641
|
-
},
|
|
642
|
-
"detailedDescription": "",
|
|
643
|
-
"isEnum": false,
|
|
644
|
-
"possibleValues": [],
|
|
645
|
-
"dependencies": [],
|
|
646
|
-
"fileSources": [],
|
|
647
|
-
"auxiliarySources": []
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
\`\`\`
|
|
652
|
-
|
|
653
|
-
**Key rules for grouped columns:**
|
|
654
|
-
- Parent column uses \`dataType: 5\` (OBJECT)
|
|
655
|
-
- Sub-columns live in \`value.objectValue.values\` as **bare ExtractionColumn objects** (no \`wellKnown\` wrapper)
|
|
656
|
-
- Each sub-column needs its own unique \`id\` within the group
|
|
657
|
-
- Sub-columns can themselves be groups (nested groups) up to the nesting depth limit
|
|
658
|
-
- The UI shows the parent as a collapsible group header with sub-columns underneath
|
|
659
|
-
- **Auto-populated \`arrayElementType\`:** The API may auto-populate a sparse \`arrayElementType\` on OBJECT columns (with \`id\`, \`name\`, \`dataType: 5\` but NO sub-columns). This is normal — include it when copying from a working persona but it is not required when creating new columns.
|
|
660
|
-
|
|
661
|
-
**Default field pattern:** Live API data always includes these fields on every column, even when empty. Include them for consistency:
|
|
662
|
-
\`\`\`
|
|
663
|
-
"detailedDescription": "", "isEnum": false, "possibleValues": [],
|
|
664
|
-
"dependencies": [], "fileSources": [], "auxiliarySources": []
|
|
665
|
-
\`\`\`
|
|
666
|
-
|
|
667
|
-
**IMPORTANT — wellKnown wrapping rules:**
|
|
668
|
-
- \`extraction_columns.inline.array.values[]\` = \`Value\` proto type → each entry needs \`{ "wellKnown": { "extractionColumn": {...} } }\`
|
|
669
|
-
- \`objectValue.values[]\` inside a column = \`ExtractionColumn[]\` proto type → bare \`{ "id": "...", "name": "...", "dataType": ... }\`
|
|
670
|
-
- \`arrayElementType\` = \`ExtractionColumn\` proto type → bare \`{ "dataType": ..., ... }\`
|
|
671
|
-
|
|
672
|
-
## Multi-Value / Array Columns (dataType: 4)
|
|
673
|
-
|
|
674
|
-
Use \`dataType: 4\` (ARRAY) with \`arrayElementType\` to extract **repeated values** from a document (e.g., multiple phone numbers, a list of names).
|
|
675
|
-
|
|
676
|
-
The \`arrayElementType\` field is a bare \`ExtractionColumn\` (no \`wellKnown\` wrapper). It typically includes \`id\` and \`name\` (often repeating the parent's values) plus the element \`dataType\`.
|
|
677
|
-
|
|
678
|
-
### Simple Array (repeated scalar values)
|
|
679
|
-
|
|
680
|
-
Extract multiple values of the same primitive type (e.g., list of invoice IDs):
|
|
681
|
-
|
|
682
|
-
\`\`\`json
|
|
683
|
-
{
|
|
684
|
-
"wellKnown": {
|
|
685
|
-
"extractionColumn": {
|
|
686
|
-
"id": "invoice_ids",
|
|
687
|
-
"name": "Invoice(s)",
|
|
688
|
-
"description": "Invoice IDs as a list",
|
|
689
|
-
"dataType": 4,
|
|
690
|
-
"arrayElementType": {
|
|
691
|
-
"id": "invoice_ids",
|
|
692
|
-
"name": "Invoice(s)",
|
|
693
|
-
"description": "Single invoice ID like INV-2024-1423",
|
|
694
|
-
"dataType": 1,
|
|
695
|
-
"isEnum": false,
|
|
696
|
-
"possibleValues": [],
|
|
697
|
-
"dependencies": [],
|
|
698
|
-
"fileSources": [],
|
|
699
|
-
"auxiliarySources": []
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
\`\`\`
|
|
705
|
-
|
|
706
|
-
### Array of Objects (repeated grouped values)
|
|
707
|
-
|
|
708
|
-
Extract multiple structured items (e.g., line items from an invoice). Combine \`dataType: 4\` with an \`arrayElementType\` of \`dataType: 5\` (OBJECT). Sub-columns inside \`objectValue.values\` are **bare ExtractionColumn objects** (no \`wellKnown\` wrapper):
|
|
709
|
-
|
|
710
|
-
\`\`\`json
|
|
711
|
-
{
|
|
712
|
-
"wellKnown": {
|
|
713
|
-
"extractionColumn": {
|
|
714
|
-
"id": "line_items",
|
|
715
|
-
"name": "Items",
|
|
716
|
-
"description": "All line items from the invoice",
|
|
717
|
-
"dataType": 4,
|
|
718
|
-
"value": {
|
|
719
|
-
"objectValue": {
|
|
720
|
-
"values": [
|
|
721
|
-
{
|
|
722
|
-
"id": "item_name",
|
|
723
|
-
"name": "Item Name",
|
|
724
|
-
"dataType": 1,
|
|
725
|
-
"description": "",
|
|
726
|
-
"detailedDescription": "",
|
|
727
|
-
"isEnum": false,
|
|
728
|
-
"possibleValues": [],
|
|
729
|
-
"dependencies": [],
|
|
730
|
-
"fileSources": [],
|
|
731
|
-
"auxiliarySources": []
|
|
732
|
-
},
|
|
733
|
-
{
|
|
734
|
-
"id": "quantity",
|
|
735
|
-
"name": "Quantity",
|
|
736
|
-
"dataType": 2,
|
|
737
|
-
"description": "",
|
|
738
|
-
"detailedDescription": "",
|
|
739
|
-
"isEnum": false,
|
|
740
|
-
"possibleValues": [],
|
|
741
|
-
"dependencies": [],
|
|
742
|
-
"fileSources": [],
|
|
743
|
-
"auxiliarySources": []
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
"id": "unit_price",
|
|
747
|
-
"name": "Unit Price",
|
|
748
|
-
"dataType": 2,
|
|
749
|
-
"description": "",
|
|
750
|
-
"detailedDescription": "",
|
|
751
|
-
"isEnum": false,
|
|
752
|
-
"possibleValues": [],
|
|
753
|
-
"dependencies": [],
|
|
754
|
-
"fileSources": [],
|
|
755
|
-
"auxiliarySources": []
|
|
756
|
-
}
|
|
757
|
-
]
|
|
758
|
-
}
|
|
759
|
-
},
|
|
760
|
-
"arrayElementType": {
|
|
761
|
-
"id": "line_items",
|
|
762
|
-
"name": "Items",
|
|
763
|
-
"dataType": 5,
|
|
764
|
-
"value": {
|
|
765
|
-
"objectValue": {
|
|
766
|
-
"values": [
|
|
767
|
-
{
|
|
768
|
-
"id": "item_name",
|
|
769
|
-
"name": "Item Name",
|
|
770
|
-
"dataType": 1,
|
|
771
|
-
"description": "",
|
|
772
|
-
"detailedDescription": "",
|
|
773
|
-
"isEnum": false,
|
|
774
|
-
"possibleValues": [],
|
|
775
|
-
"dependencies": [],
|
|
776
|
-
"fileSources": [],
|
|
777
|
-
"auxiliarySources": []
|
|
778
|
-
},
|
|
779
|
-
{
|
|
780
|
-
"id": "quantity",
|
|
781
|
-
"name": "Quantity",
|
|
782
|
-
"dataType": 2,
|
|
783
|
-
"description": "",
|
|
784
|
-
"detailedDescription": "",
|
|
785
|
-
"isEnum": false,
|
|
786
|
-
"possibleValues": [],
|
|
787
|
-
"dependencies": [],
|
|
788
|
-
"fileSources": [],
|
|
789
|
-
"auxiliarySources": []
|
|
790
|
-
},
|
|
791
|
-
{
|
|
792
|
-
"id": "unit_price",
|
|
793
|
-
"name": "Unit Price",
|
|
794
|
-
"dataType": 2,
|
|
795
|
-
"description": "",
|
|
796
|
-
"detailedDescription": "",
|
|
797
|
-
"isEnum": false,
|
|
798
|
-
"possibleValues": [],
|
|
799
|
-
"dependencies": [],
|
|
800
|
-
"fileSources": [],
|
|
801
|
-
"auxiliarySources": []
|
|
802
|
-
}
|
|
803
|
-
]
|
|
804
|
-
}
|
|
805
|
-
},
|
|
806
|
-
"description": "",
|
|
807
|
-
"detailedDescription": "",
|
|
808
|
-
"isEnum": false,
|
|
809
|
-
"possibleValues": [],
|
|
810
|
-
"dependencies": [],
|
|
811
|
-
"fileSources": [],
|
|
812
|
-
"auxiliarySources": []
|
|
813
|
-
},
|
|
814
|
-
"detailedDescription": "",
|
|
815
|
-
"isEnum": false,
|
|
816
|
-
"possibleValues": [],
|
|
817
|
-
"dependencies": [],
|
|
818
|
-
"fileSources": [],
|
|
819
|
-
"auxiliarySources": []
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
\`\`\`
|
|
824
|
-
|
|
825
|
-
**Key rules for array columns:**
|
|
826
|
-
- Parent column uses \`dataType: 4\` (ARRAY)
|
|
827
|
-
- \`arrayElementType\` is a bare \`ExtractionColumn\` (no \`wellKnown\` wrapper) — repeats the parent's \`id\` and \`name\`, plus the element \`dataType\`
|
|
828
|
-
- For simple arrays: \`arrayElementType.dataType\` = scalar type (1=STRING, 2=NUMBER, etc.)
|
|
829
|
-
- For arrays of objects: \`arrayElementType.dataType: 5\` with \`value.objectValue.values\` containing bare sub-columns
|
|
830
|
-
- **Dual storage pattern:** For array-of-objects, the sub-columns appear in BOTH \`value.objectValue.values\` (on the parent) AND \`arrayElementType.value.objectValue.values\`. Both sets must match. The \`arrayElementType\` is the canonical schema; the parent \`value.objectValue\` may be auto-populated by the API.
|
|
831
|
-
- Sub-columns inside \`objectValue.values\` are bare ExtractionColumn objects (no \`wellKnown\` wrapper)
|
|
832
|
-
- The UI labels this as "Multi Value" — in the API, it maps to \`dataType: 4\` + \`arrayElementType\`
|
|
833
|
-
- **There is no \`isMultiValue\` API field** — that is a UI-only label. Always use \`dataType: 4\` + \`arrayElementType\` in workflow_def.
|
|
834
|
-
|
|
835
|
-
## Creation-Time Constraints
|
|
836
|
-
|
|
837
|
-
**IMPORTANT:** The following extraction column properties are set at creation time and **cannot be changed** after the column exists:
|
|
838
|
-
|
|
839
|
-
| Property | Constraint |
|
|
840
|
-
|----------|-----------|
|
|
841
|
-
| \`dataType\` | Cannot change after creation (e.g., STRING to ARRAY) |
|
|
842
|
-
| \`arrayElementType\` | Cannot change after creation; defines the array element schema |
|
|
843
|
-
| \`value.objectValue\` structure | Sub-column schema is fixed at creation |
|
|
844
|
-
|
|
845
|
-
To change these properties, you must **delete the column and recreate it** with the new settings.
|
|
846
|
-
|
|
847
|
-
Other properties (\`name\`, \`description\`, \`possibleValues\`, \`formattingOptions\`) can be updated after creation.
|
|
848
|
-
|
|
849
|
-
## Enum Columns
|
|
850
|
-
|
|
851
|
-
To create a column with a fixed set of allowed values, use \`dataType: 1\` (STRING) with \`isEnum: true\` and \`possibleValues\`:
|
|
852
|
-
|
|
853
|
-
\`\`\`json
|
|
854
|
-
{
|
|
855
|
-
"wellKnown": {
|
|
856
|
-
"extractionColumn": {
|
|
857
|
-
"id": "priority",
|
|
858
|
-
"name": "Priority",
|
|
859
|
-
"description": "Task priority level",
|
|
860
|
-
"dataType": 1,
|
|
861
|
-
"isEnum": true,
|
|
862
|
-
"possibleValues": [
|
|
863
|
-
{ "value": { "stringValue": "High", "case": "stringValue" } },
|
|
864
|
-
{ "value": { "stringValue": "Medium", "case": "stringValue" } },
|
|
865
|
-
{ "value": { "stringValue": "Low", "case": "stringValue" } }
|
|
866
|
-
]
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
\`\`\`
|
|
871
|
-
|
|
872
|
-
## Document Input Wiring (Dashboard Personas)
|
|
873
|
-
|
|
874
|
-
Dashboard extraction workflows receive documents through one of two patterns:
|
|
875
|
-
|
|
876
|
-
### Pattern 1: workflowInput (preferred for newer workflows)
|
|
877
|
-
|
|
878
|
-
\`\`\`json
|
|
879
|
-
"workflowInputs": {
|
|
880
|
-
"document-mmf2": {
|
|
881
|
-
"type": { "arrayType": { "wellKnownType": 5, "isList": false }, "isList": false },
|
|
882
|
-
"displayName": "Document"
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
\`\`\`
|
|
886
|
-
|
|
887
|
-
Then in the extraction node:
|
|
888
|
-
\`\`\`json
|
|
889
|
-
"documents": { "workflowInput": { "inputName": "document-mmf2" } }
|
|
890
|
-
\`\`\`
|
|
891
|
-
|
|
892
|
-
### Pattern 2: trigger output (older workflows)
|
|
893
|
-
|
|
894
|
-
\`\`\`json
|
|
895
|
-
"documents": { "actionOutput": { "actionName": "trigger", "output": "user_query" } }
|
|
896
|
-
\`\`\`
|
|
897
|
-
|
|
898
|
-
Note: The output name \`user_query\` is used for documents in older \`document_trigger\` workflows — despite the misleading name, it carries the uploaded document content.
|
|
899
|
-
|
|
900
|
-
## Extraction Node Options
|
|
901
|
-
|
|
902
|
-
### disableHumanInteraction
|
|
903
|
-
|
|
904
|
-
Set to \`true\` to skip the human-in-the-loop review step for extraction results. Default is \`false\` (HITL enabled):
|
|
905
|
-
|
|
906
|
-
\`\`\`json
|
|
907
|
-
{
|
|
908
|
-
"name": "entity_extraction_node",
|
|
909
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "entity_extraction_with_documents" }, "version": "v0" },
|
|
910
|
-
"inputs": { ... },
|
|
911
|
-
"disableHumanInteraction": true
|
|
912
|
-
}
|
|
913
|
-
\`\`\`
|
|
914
|
-
|
|
915
|
-
## How to Use
|
|
916
|
-
|
|
917
|
-
1. Call \`workflow(mode="get", persona_id="<same or similar persona>")\` and use the returned \`workflow_def\` as your format reference.
|
|
918
|
-
2. For extraction_columns specifically, copy the structure from a working action node in that workflow_def, or use this resource.
|
|
919
|
-
3. Generate or edit workflow_def as text/JSON; do not reverse-engineer from local JSON files.
|
|
920
|
-
|
|
921
|
-
See also: \`ema://rules/json-output-patterns\` for custom_agent/output_fields (same array.values + wellKnown.extractionColumn pattern).
|
|
922
|
-
`;
|
|
923
|
-
},
|
|
924
|
-
},
|
|
925
|
-
// Nested Data Format - ARRAY and OBJECT column handling
|
|
926
|
-
{
|
|
927
|
-
uri: "ema://rules/nested-data-format",
|
|
928
|
-
name: "rules/nested-data-format",
|
|
929
|
-
description: "Format guide for nested data structures in dashboard columns (ARRAY and OBJECT types). Use for uploads with sub-tables.",
|
|
930
|
-
mimeType: "text/markdown",
|
|
931
|
-
generate: async () => {
|
|
932
|
-
return `# Nested Data Format Guide
|
|
933
|
-
|
|
934
|
-
## Overview
|
|
935
|
-
|
|
936
|
-
Dashboard columns can contain nested data structures using \`COLUMN_TYPE_ARRAY\` (8) and \`COLUMN_TYPE_OBJECT\` (9).
|
|
937
|
-
These are used for entity extraction results, line items, and other structured data.
|
|
938
|
-
|
|
939
|
-
## Column Types
|
|
940
|
-
|
|
941
|
-
| Type | Enum | Structure | Use Case |
|
|
942
|
-
|------|------|-----------|----------|
|
|
943
|
-
| \`COLUMN_TYPE_ARRAY\` | 8 | \`{ arrayValues: ColumnValue[] }\` | Lists of same-type items (line items, extracted entities) |
|
|
944
|
-
| \`COLUMN_TYPE_OBJECT\` | 9 | \`{ objectValues: { [key]: ColumnValue } }\` | Structured record with named fields |
|
|
945
|
-
|
|
946
|
-
## DashboardInput Format
|
|
947
|
-
|
|
948
|
-
When uploading rows with nested data, use this format:
|
|
949
|
-
|
|
950
|
-
\`\`\`typescript
|
|
951
|
-
// ARRAY column
|
|
952
|
-
{
|
|
953
|
-
name: "line_items",
|
|
954
|
-
array_value: [
|
|
955
|
-
{ name: "element", string_value: "Item 1" },
|
|
956
|
-
{ name: "element", number_value: 100 },
|
|
957
|
-
{ name: "element", object_value: {
|
|
958
|
-
"product": { name: "product", string_value: "Widget" },
|
|
959
|
-
"qty": { name: "qty", number_value: 5 }
|
|
960
|
-
}}
|
|
961
|
-
]
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// OBJECT column
|
|
965
|
-
{
|
|
966
|
-
name: "customer_info",
|
|
967
|
-
object_value: {
|
|
968
|
-
"name": { name: "name", string_value: "Acme Corp" },
|
|
969
|
-
"email": { name: "email", string_value: "contact@acme.com" },
|
|
970
|
-
"orders": { name: "orders", array_value: [...] } // Nested array
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
\`\`\`
|
|
974
|
-
|
|
975
|
-
## Schema Information
|
|
976
|
-
|
|
977
|
-
Dashboard schema includes nested structure details:
|
|
978
|
-
|
|
979
|
-
\`\`\`typescript
|
|
980
|
-
// For ARRAY columns:
|
|
981
|
-
{
|
|
982
|
-
columnType: "COLUMN_TYPE_ARRAY",
|
|
983
|
-
arrayElementType: {
|
|
984
|
-
columnType: "COLUMN_TYPE_OBJECT", // or STRING, NUMBER, etc.
|
|
985
|
-
objectSubColumns: [...] // If element is OBJECT
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// For OBJECT columns:
|
|
990
|
-
{
|
|
991
|
-
columnType: "COLUMN_TYPE_OBJECT",
|
|
992
|
-
objectSubColumns: [
|
|
993
|
-
{ name: "field1", columnType: "COLUMN_TYPE_STRING" },
|
|
994
|
-
{ name: "field2", columnType: "COLUMN_TYPE_NUMBER" },
|
|
995
|
-
]
|
|
996
|
-
}
|
|
997
|
-
\`\`\`
|
|
998
|
-
|
|
999
|
-
## Validation Rules
|
|
1000
|
-
|
|
1001
|
-
| Rule | Limit | Error |
|
|
1002
|
-
|------|-------|-------|
|
|
1003
|
-
| Max array elements | 1000 | \`Array exceeds maximum of 1000 elements\` |
|
|
1004
|
-
| Max nesting depth | 3 levels | \`Nesting depth exceeds maximum of 3\` |
|
|
1005
|
-
| Type mismatch | - | \`Expected array for ARRAY column, got <type>\` |
|
|
1006
|
-
|
|
1007
|
-
## Common Mistakes
|
|
1008
|
-
|
|
1009
|
-
### ❌ Scalar value for ARRAY column
|
|
1010
|
-
\`\`\`typescript
|
|
1011
|
-
// WRONG - passing string where array expected
|
|
1012
|
-
{ name: "line_items", string_value: "item1, item2" }
|
|
1013
|
-
|
|
1014
|
-
// CORRECT
|
|
1015
|
-
{ name: "line_items", array_value: [
|
|
1016
|
-
{ name: "element", string_value: "item1" },
|
|
1017
|
-
{ name: "element", string_value: "item2" }
|
|
1018
|
-
]}
|
|
1019
|
-
\`\`\`
|
|
1020
|
-
|
|
1021
|
-
### ❌ Array value for STRING column
|
|
1022
|
-
\`\`\`typescript
|
|
1023
|
-
// WRONG - passing array where string expected
|
|
1024
|
-
{ name: "customer_name", array_value: [...] }
|
|
1025
|
-
|
|
1026
|
-
// CORRECT
|
|
1027
|
-
{ name: "customer_name", string_value: "Acme Corp" }
|
|
1028
|
-
\`\`\`
|
|
1029
|
-
|
|
1030
|
-
### ❌ Missing element name
|
|
1031
|
-
\`\`\`typescript
|
|
1032
|
-
// WRONG - missing name in array element
|
|
1033
|
-
{ name: "items", array_value: [{ string_value: "item1" }] }
|
|
1034
|
-
|
|
1035
|
-
// CORRECT - always include name
|
|
1036
|
-
{ name: "items", array_value: [{ name: "element", string_value: "item1" }] }
|
|
1037
|
-
\`\`\`
|
|
1038
|
-
|
|
1039
|
-
## Related Resources
|
|
1040
|
-
|
|
1041
|
-
- \`ema://rules/extraction-column-format\` - entity_extraction output format
|
|
1042
|
-
- \`ema://rules/json-output-patterns\` - custom_agent JSON patterns
|
|
1043
|
-
`;
|
|
1044
|
-
},
|
|
1045
|
-
},
|
|
1046
|
-
// Chat Response Wiring - how to avoid duplicate/repetitive responses
|
|
1047
|
-
{
|
|
1048
|
-
uri: "ema://rules/chat-response-wiring",
|
|
1049
|
-
name: "rules/chat-response-wiring",
|
|
1050
|
-
description: "Rules for wiring chat workflows to avoid duplicate or repetitive responses. Chat conversation must flow to chat-aware response nodes.",
|
|
1051
|
-
mimeType: "text/markdown",
|
|
1052
|
-
generate: async () => {
|
|
1053
|
-
return `# Chat Response Wiring Rules
|
|
1054
|
-
|
|
1055
|
-
## The Problem
|
|
1056
|
-
|
|
1057
|
-
Chat workflows that process \`chat_conversation\` through search, extraction, or tool calls **must** wire the results to a **chat-aware response node** — not a generic \`call_llm\`. Without conversation history in the response node, the LLM:
|
|
1058
|
-
|
|
1059
|
-
1. **Repeats questions** already answered in prior turns
|
|
1060
|
-
2. **Loses context** from multi-turn conversations
|
|
1061
|
-
3. **Generates stale greetings** instead of continuing naturally
|
|
1062
|
-
|
|
1063
|
-
## Correct Patterns
|
|
1064
|
-
|
|
1065
|
-
### Pattern 1: Search + Respond (most common)
|
|
1066
|
-
\`\`\`
|
|
1067
|
-
chat_trigger.chat_conversation → conversation_to_search_query.conversation
|
|
1068
|
-
conversation_to_search_query.summarized_conversation → search.query
|
|
1069
|
-
search.search_results → respond_for_external_actions.external_action_result
|
|
1070
|
-
chat_trigger.user_query → respond_for_external_actions.query
|
|
1071
|
-
chat_trigger.chat_conversation → respond_for_external_actions.conversation
|
|
1072
|
-
respond_for_external_actions.response → WORKFLOW_OUTPUT
|
|
1073
|
-
\`\`\`
|
|
1074
|
-
|
|
1075
|
-
> **Note**: \`respond_with_sources/v0\` is DEPRECATED. Use \`respond_for_external_actions\` for all new workflows (search results, tool results, or any context-aware response).
|
|
1076
|
-
|
|
1077
|
-
### Pattern 2: Tool/Action + Respond
|
|
1078
|
-
\`\`\`
|
|
1079
|
-
chat_trigger.chat_conversation → external_action_caller.conversation
|
|
1080
|
-
external_action_caller.tool_execution_result → respond_for_external_actions.external_action_result
|
|
1081
|
-
chat_trigger.user_query → respond_for_external_actions.query
|
|
1082
|
-
chat_trigger.chat_conversation → respond_for_external_actions.conversation
|
|
1083
|
-
respond_for_external_actions.response → WORKFLOW_OUTPUT
|
|
1084
|
-
\`\`\`
|
|
1085
|
-
|
|
1086
|
-
### Pattern 3: call_llm with Conversation Context (advanced)
|
|
1087
|
-
If you must use \`call_llm\` as the terminal responder, you **must** wire \`chat_conversation\` into its \`named_inputs\`:
|
|
1088
|
-
\`\`\`
|
|
1089
|
-
chat_trigger.chat_conversation → call_llm.named_inputs (key: "conversation_history")
|
|
1090
|
-
\`\`\`
|
|
1091
|
-
This ensures the LLM sees prior turns.
|
|
1092
|
-
|
|
1093
|
-
## Anti-Patterns
|
|
1094
|
-
|
|
1095
|
-
### ❌ Stateless call_llm as Responder
|
|
1096
|
-
\`\`\`
|
|
1097
|
-
chat_trigger → search → call_llm (no conversation) → WORKFLOW_OUTPUT
|
|
1098
|
-
\`\`\`
|
|
1099
|
-
**Problem**: call_llm only sees search results, not prior conversation. Repeats questions.
|
|
1100
|
-
|
|
1101
|
-
### ❌ Entity Extraction → call_llm (no history)
|
|
1102
|
-
\`\`\`
|
|
1103
|
-
chat_trigger → entity_extraction → call_llm (stateless) → WORKFLOW_OUTPUT
|
|
1104
|
-
\`\`\`
|
|
1105
|
-
**Problem**: LLM extracts entities but response node doesn't know what was already discussed.
|
|
1106
|
-
|
|
1107
|
-
## Key Insight
|
|
1108
|
-
|
|
1109
|
-
\`respond_for_external_actions\` is **conversation-aware by design** — it incorporates conversation context via its \`conversation\` input. Generic \`call_llm\` does not — you must manually wire \`chat_conversation\` via \`named_inputs\`.
|
|
1110
|
-
|
|
1111
|
-
> **Deprecated**: \`respond_with_sources/v0\` — use \`respond_for_external_actions\` for all new workflows.
|
|
1112
|
-
|
|
1113
|
-
## When to Use Each Response Node
|
|
1114
|
-
|
|
1115
|
-
| Scenario | Response Node | Why |
|
|
1116
|
-
|----------|---------------|-----|
|
|
1117
|
-
| KB/document Q&A | \`respond_for_external_actions\` | Handles search results, conversation-aware |
|
|
1118
|
-
| Tool/API results | \`respond_for_external_actions\` | Explains tool results in conversation context |
|
|
1119
|
-
| Complex multi-step reasoning | \`call_llm/v2\` + named_inputs with chat_conversation | Full control but requires manual history wiring |
|
|
1120
|
-
| Static/template response | \`fixed_response/v1\` | No LLM needed, just template + variables |
|
|
1121
|
-
`;
|
|
1122
|
-
},
|
|
1123
|
-
},
|
|
1124
|
-
// Email Input Wiring - how to wire extracted data to email fields
|
|
1125
|
-
{
|
|
1126
|
-
uri: "ema://rules/email-input-wiring",
|
|
1127
|
-
name: "rules/email-input-wiring",
|
|
1128
|
-
description: "Rules for wiring data to send_email_agent inputs. Entity extraction outputs cannot go directly to email fields — use json_mapper/fixed_response intermediaries.",
|
|
1129
|
-
mimeType: "text/markdown",
|
|
1130
|
-
generate: async () => {
|
|
1131
|
-
return `# Email Input Wiring Rules
|
|
1132
|
-
|
|
1133
|
-
## The Problem
|
|
1134
|
-
|
|
1135
|
-
\`send_email_agent\` inputs (\`email_to\`, \`email_subject\`, \`email_body\`) require **TEXT_WITH_SOURCES** type. But entity_extraction outputs are **ANY** type (structured JSON). Direct wiring causes:
|
|
1136
|
-
|
|
1137
|
-
1. **Type mismatch errors** at deploy time (HTTP 500)
|
|
1138
|
-
2. **Corrupted email addresses** — entire JSON object sent as recipient
|
|
1139
|
-
3. **Missing fields** — structured data not properly decomposed into individual fields
|
|
1140
|
-
|
|
1141
|
-
## Rule: Always Use Intermediary Nodes
|
|
1142
|
-
|
|
1143
|
-
\`\`\`
|
|
1144
|
-
❌ WRONG: entity_extraction → send_email_agent.email_to
|
|
1145
|
-
❌ WRONG: entity_extraction → send_email_agent.email_subject
|
|
1146
|
-
❌ WRONG: call_llm.response → send_email_agent.email_to (sends full text as address)
|
|
1147
|
-
|
|
1148
|
-
✅ RIGHT: entity_extraction → json_mapper → fixed_response({{to}}) → send_email_agent.email_to
|
|
1149
|
-
✅ RIGHT: custom_agent(output_fields=[To,Subject,Body]) → send_email_agent
|
|
1150
|
-
✅ RIGHT: entity_extraction → fixed_response({{email_address}}) → send_email_agent.email_to
|
|
1151
|
-
\`\`\`
|
|
1152
|
-
|
|
1153
|
-
## Pattern A: json_mapper + fixed_response (Recommended)
|
|
1154
|
-
|
|
1155
|
-
Best for decomposing structured extraction results into individual email fields.
|
|
1156
|
-
|
|
1157
|
-
### Step 1: Extract fields with json_mapper
|
|
1158
|
-
\`\`\`json
|
|
1159
|
-
{
|
|
1160
|
-
"name": "extract_fields",
|
|
1161
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "json_mapper" }, "version": "v1" },
|
|
1162
|
-
"inputs": {
|
|
1163
|
-
"json_input": { "actionOutput": { "actionName": "entity_extraction", "output": "extraction_result" }},
|
|
1164
|
-
"json_mapper_config": {
|
|
1165
|
-
"inline": { "wellKnown": { "jsonMapperConfig": {
|
|
1166
|
-
"rules": [
|
|
1167
|
-
{ "fieldName": "to", "pathIndices": [{"stringIndex": "email_address"}], "type": 3 },
|
|
1168
|
-
{ "fieldName": "subject", "pathIndices": [{"stringIndex": "subject_line"}], "type": 3 },
|
|
1169
|
-
{ "fieldName": "body", "pathIndices": [{"stringIndex": "email_body"}], "type": 3 }
|
|
1170
|
-
]
|
|
1171
|
-
}}}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
\`\`\`
|
|
1176
|
-
|
|
1177
|
-
### Step 2: Format each field with fixed_response
|
|
1178
|
-
\`\`\`json
|
|
1179
|
-
{
|
|
1180
|
-
"name": "fmt_to",
|
|
1181
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
|
|
1182
|
-
"inputs": {
|
|
1183
|
-
"template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{to}}", "sources": [] }}}},
|
|
1184
|
-
"custom_data": { "actionOutput": { "actionName": "extract_fields", "output": "output_json" }}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
\`\`\`
|
|
1188
|
-
|
|
1189
|
-
Repeat for \`fmt_subject\` (using \`{{subject}}\`) and \`fmt_body\` (using \`{{body}}\`).
|
|
1190
|
-
|
|
1191
|
-
### Step 3: Wire to send_email_agent
|
|
1192
|
-
\`\`\`json
|
|
1193
|
-
{
|
|
1194
|
-
"name": "send_email",
|
|
1195
|
-
"inputs": {
|
|
1196
|
-
"email_to": { "actionOutput": { "actionName": "fmt_to", "output": "response" }},
|
|
1197
|
-
"email_subject": { "actionOutput": { "actionName": "fmt_subject", "output": "response" }},
|
|
1198
|
-
"email_body": { "actionOutput": { "actionName": "fmt_body", "output": "response" }}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
\`\`\`
|
|
1202
|
-
|
|
1203
|
-
## Pattern B: custom_agent with output_fields (Simpler)
|
|
1204
|
-
|
|
1205
|
-
When the LLM generates email content, use \`custom_agent\` with \`output_fields\` to produce individual TEXT_WITH_SOURCES outputs directly:
|
|
1206
|
-
|
|
1207
|
-
\`\`\`json
|
|
1208
|
-
{
|
|
1209
|
-
"name": "generate_email",
|
|
1210
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "custom_agent" }, "version": "v1" },
|
|
1211
|
-
"inputs": {
|
|
1212
|
-
"task_instructions": { "inline": { "wellKnown": { "textWithSources": { "text": "Generate an email with To, Subject, and Body fields.", "sources": [] }}}},
|
|
1213
|
-
"output_fields": {
|
|
1214
|
-
"inline": { "array": { "values": [
|
|
1215
|
-
{ "wellKnown": { "extractionColumn": { "id": "To", "name": "To", "dataType": 1 }}},
|
|
1216
|
-
{ "wellKnown": { "extractionColumn": { "id": "Subject", "name": "Subject", "dataType": 1 }}},
|
|
1217
|
-
{ "wellKnown": { "extractionColumn": { "id": "Body", "name": "Body", "dataType": 1 }}}
|
|
1218
|
-
]}}
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
\`\`\`
|
|
1223
|
-
|
|
1224
|
-
Each output_field becomes a separate output (e.g., \`generate_email.To\`) that can wire directly to \`send_email_agent\`.
|
|
1225
|
-
|
|
1226
|
-
## Pattern C: fixed_response for Simple Template Formatting
|
|
1227
|
-
|
|
1228
|
-
When you already have a clean extracted value and just need TEXT_WITH_SOURCES wrapping:
|
|
1229
|
-
|
|
1230
|
-
\`\`\`json
|
|
1231
|
-
{
|
|
1232
|
-
"name": "fmt_email",
|
|
1233
|
-
"action": { "name": { "namespaces": ["actions", "emainternal"], "name": "fixed_response" }, "version": "v1" },
|
|
1234
|
-
"inputs": {
|
|
1235
|
-
"template": { "inline": { "wellKnown": { "textWithSources": { "text": "{{email_address}}", "sources": [] }}}},
|
|
1236
|
-
"custom_data": { "actionOutput": { "actionName": "entity_extraction", "output": "extraction_result" }}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
\`\`\`
|
|
1240
|
-
|
|
1241
|
-
## Common Mistakes
|
|
1242
|
-
|
|
1243
|
-
| Mistake | Problem | Fix |
|
|
1244
|
-
|---------|---------|-----|
|
|
1245
|
-
| \`entity_extraction → email_to\` | Type mismatch (ANY → TEXT_WITH_SOURCES) | Add json_mapper + fixed_response |
|
|
1246
|
-
| \`call_llm.response → email_to\` | Full LLM response as email address | Extract with json_mapper or use output_fields |
|
|
1247
|
-
| \`json_mapper → email_to\` | json_mapper outputs JSON_VALUE, not TEXT_WITH_SOURCES | Add fixed_response between json_mapper and email |
|
|
1248
|
-
| Missing fixed_response | Type still mismatched | fixed_response converts ANY → TEXT_WITH_SOURCES |
|
|
1249
|
-
|
|
1250
|
-
## See Also
|
|
1251
|
-
|
|
1252
|
-
- \`ema://rules/anti-patterns\` — includes \`entity-extraction-direct-to-email\` anti-pattern
|
|
1253
|
-
- \`ema://rules/extraction-column-format\` — for output_fields structure
|
|
1254
|
-
`;
|
|
1255
|
-
},
|
|
1256
|
-
},
|
|
1257
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1258
|
-
// Workflow Node Reference - per-node I/O types, semantics, categorizer patterns
|
|
1259
|
-
// Consolidated from .context/public/guides/workflow-builder-patterns.md
|
|
1260
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1261
|
-
{
|
|
1262
|
-
uri: "ema://docs/workflow-node-reference",
|
|
1263
|
-
name: "docs/workflow-node-reference",
|
|
1264
|
-
description: "Complete workflow node reference: per-node I/O types, categorizer patterns (runIf/enumTypes), " +
|
|
1265
|
-
"LLM named_inputs convention, temperature guidelines, data types between nodes",
|
|
1266
|
-
mimeType: "text/markdown",
|
|
1267
|
-
generate: async () => {
|
|
1268
|
-
let md = `# Workflow Node Reference\n\n`;
|
|
1269
|
-
md += `Practical reference for building Ema workflows. Covers per-node I/O semantics, `;
|
|
1270
|
-
md += `categorizer configuration, LLM patterns, and data type compatibility.\n\n`;
|
|
1271
|
-
md += `## Data Types Between Nodes\n\n`;
|
|
1272
|
-
md += `| Type | Code Constant | Description | Example |\n`;
|
|
1273
|
-
md += `|------|---------------|-------------|---------|\n`;
|
|
1274
|
-
md += `| **Plain Text** | \`WELL_KNOWN_TYPE_TEXT_WITH_SOURCES\` | Text with optional citation metadata | User's message, LLM response |\n`;
|
|
1275
|
-
md += `| **Conversation** | \`WELL_KNOWN_TYPE_CHAT_CONVERSATION\` | Structured message history (role + content) | Full chat thread |\n`;
|
|
1276
|
-
md += `| **Search Results** | \`WELL_KNOWN_TYPE_SEARCH_RESULT\` | Retrieved document chunks with citations | KB search results |\n`;
|
|
1277
|
-
md += `| **Enum** | \`WELL_KNOWN_TYPE_ENUM\` | Category/classification signal for routing | \`category::Schedule Appointment\` |\n`;
|
|
1278
|
-
md += `| **Document** | \`WELL_KNOWN_TYPE_DOCUMENT\` | Uploaded file content | PDF, DOCX for extraction |\n`;
|
|
1279
|
-
md += `| **Any** | \`WELL_KNOWN_TYPE_ANY\` | Untyped — needs intermediary for type-safe wiring | entity_extraction output |\n\n`;
|
|
1280
|
-
md += `**Critical**: Types are NOT interchangeable. \`CHAT_CONVERSATION\` into a \`TEXT_WITH_SOURCES\` input causes type mismatch errors. Use converter nodes when needed.\n\n`;
|
|
1281
|
-
md += `---\n\n`;
|
|
1282
|
-
md += `## Input Semantics by Context\n\n`;
|
|
1283
|
-
md += `The same input name means different things depending on the node:\n\n`;
|
|
1284
|
-
md += `| Input Name | In Search Nodes | In Respond Nodes | In Extract Nodes | In Categorizers |\n`;
|
|
1285
|
-
md += `|------------|-----------------|-------------------|-------------------|------------------|\n`;
|
|
1286
|
-
md += `| \`query\` | Search term to look up | User's question to answer | Source text to analyze | N/A |\n`;
|
|
1287
|
-
md += `| \`conversation\` | N/A | N/A (use named_inputs) | N/A | Full history for classification |\n`;
|
|
1288
|
-
md += `| \`trigger_when\` | N/A | "Should I run?" | "Should I run?" | N/A |\n`;
|
|
1289
|
-
md += `| \`named_inputs_*\` | N/A | Additional context | Additional context | N/A |\n\n`;
|
|
1290
|
-
md += `**Mental model:** \`query\` = "What to process?" · \`conversation\` = "Full context" · \`trigger_when\` = "Should I run?" · \`named_inputs_*\` = "Extra context"\n\n`;
|
|
1291
|
-
md += `---\n\n`;
|
|
1292
|
-
md += `## Node Type Reference\n\n`;
|
|
1293
|
-
md += `### chat_trigger\n`;
|
|
1294
|
-
md += `Entry point for chat/voice workflows.\n`;
|
|
1295
|
-
md += `- **Inputs**: None (system event)\n`;
|
|
1296
|
-
md += `- **Outputs**: \`user_query\` (TEXT_WITH_SOURCES), \`chat_conversation\` (CHAT_CONVERSATION)\n`;
|
|
1297
|
-
md += `- **Pairs with**: Intent routers, search nodes, LLM nodes\n\n`;
|
|
1298
|
-
md += `### document_trigger\n`;
|
|
1299
|
-
md += `Entry point for dashboard workflows (file upload per row).\n`;
|
|
1300
|
-
md += `- **Inputs**: None (system event — triggered when row created/file uploaded)\n`;
|
|
1301
|
-
md += `- **Outputs**: \`document_content\` (DOCUMENT), \`row_data\` (ANY — column values)\n`;
|
|
1302
|
-
md += `- **Pairs with**: entity_extraction_with_documents, call_llm, search\n`;
|
|
1303
|
-
md += `- **Critical**: Dashboard personas only. Each row triggers one workflow execution.\n\n`;
|
|
1304
|
-
md += `### chat_categorizer\n`;
|
|
1305
|
-
md += `Classify conversation into intent categories for routing.\n`;
|
|
1306
|
-
md += `- **Inputs**: \`conversation\` (CHAT_CONVERSATION) — must be full history, NOT user_query\n`;
|
|
1307
|
-
md += `- **Outputs**: \`category\` (ENUM) — one per configured category\n`;
|
|
1308
|
-
md += `- **Critical**: Always include Fallback category. Every category needs a handler. Must have \`typeArguments.categories\` pointing to enumType.\n\n`;
|
|
1309
|
-
md += `### text_categorizer/v1\n`;
|
|
1310
|
-
md += `Classify text content (not conversation) for routing.\n`;
|
|
1311
|
-
md += `- **Inputs**: \`named_inputs\` (multiBinding)\n`;
|
|
1312
|
-
md += `- **Outputs**: \`category\` (ENUM)\n`;
|
|
1313
|
-
md += `- **Critical**: \`text_categorizer/v0\` is deprecated — use v1 with \`named_inputs\`.\n\n`;
|
|
1314
|
-
md += `### search/v2\n`;
|
|
1315
|
-
md += `Retrieve relevant documents from uploaded knowledge base.\n`;
|
|
1316
|
-
md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`datastore_configs\` (from widget)\n`;
|
|
1317
|
-
md += `- **Outputs**: \`search_results\` (SEARCH_RESULT)\n`;
|
|
1318
|
-
md += `- **Critical**: NOT an LLM node — do NOT include \`model_config\`. Data must be uploaded first. \`search/v0\` is deprecated.\n\n`;
|
|
1319
|
-
md += `### conversation_to_search_query\n`;
|
|
1320
|
-
md += `Convert multi-turn conversation to a search-optimized query.\n`;
|
|
1321
|
-
md += `- **Inputs**: \`conversation\` (CHAT_CONVERSATION)\n`;
|
|
1322
|
-
md += `- **Outputs**: \`summarized_conversation\` (TEXT_WITH_SOURCES)\n`;
|
|
1323
|
-
md += `- **Critical**: Required for multi-turn chat search. Direct CHAT_CONVERSATION → search causes type mismatch.\n\n`;
|
|
1324
|
-
md += `### call_llm/v2\n`;
|
|
1325
|
-
md += `Generate natural language response using LLM.\n`;
|
|
1326
|
-
md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`named_inputs_*\` (ANY — flexible), \`trigger_when\` (ENUM)\n`;
|
|
1327
|
-
md += `- **Outputs**: \`response_with_sources\` (TEXT_WITH_SOURCES)\n`;
|
|
1328
|
-
md += `- **Critical**: Must wire to WORKFLOW_OUTPUT or response is lost. \`call_llm/v0\` is deprecated.\n\n`;
|
|
1329
|
-
md += `### respond_for_external_actions\n`;
|
|
1330
|
-
md += `Generate conversation-aware response from search results or tool outputs.\n`;
|
|
1331
|
-
md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), \`external_action_result\` (TEXT_WITH_SOURCES/SEARCH_RESULT)\n`;
|
|
1332
|
-
md += `- **Outputs**: \`response\` (TEXT_WITH_SOURCES)\n`;
|
|
1333
|
-
md += `- **Critical**: Replaces deprecated \`respond_with_sources/v0\`. Has built-in citation handling and conversation awareness.\n\n`;
|
|
1334
|
-
md += `### fixed_response/v1\n`;
|
|
1335
|
-
md += `Return a static predefined message.\n`;
|
|
1336
|
-
md += `- **Inputs**: \`trigger_when\` (ENUM), \`named_inputs_*\` for template variables\n`;
|
|
1337
|
-
md += `- **Outputs**: \`fixed_response_with_sources\` (TEXT_WITH_SOURCES)\n`;
|
|
1338
|
-
md += `- **Pairs with**: Categorizers (as fallback), type conversion with \`{{variables}}\` templates, send_email_agent\n\n`;
|
|
1339
|
-
md += `### external_action_caller\n`;
|
|
1340
|
-
md += `Call external APIs/tools (ServiceNow, Salesforce, calendars).\n`;
|
|
1341
|
-
md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES), \`conversation\` (CHAT_CONVERSATION), tool configuration\n`;
|
|
1342
|
-
md += `- **Outputs**: \`tool_execution_result\` (TEXT_WITH_SOURCES)\n`;
|
|
1343
|
-
md += `- **Pairs with**: Entity extractors (for parameters), respond nodes (for result formatting)\n\n`;
|
|
1344
|
-
md += `### entity_extraction_with_documents\n`;
|
|
1345
|
-
md += `Extract structured entities grounded in provided documents.\n`;
|
|
1346
|
-
md += `- **Inputs**: \`documents\` (DOCUMENT), extraction column config\n`;
|
|
1347
|
-
md += `- **Outputs**: \`extraction_columns\` (ANY — structured extraction results)\n`;
|
|
1348
|
-
md += `- **Critical**: Output type is ANY — needs intermediary (json_mapper + fixed_response) before send_email inputs.\n`;
|
|
1349
|
-
md += `- See \`ema://rules/extraction-column-format\` for column schema.\n\n`;
|
|
1350
|
-
md += `### send_email_agent\n`;
|
|
1351
|
-
md += `Send email with specified recipient, subject, and body.\n`;
|
|
1352
|
-
md += `- **Inputs**: \`email_to\` (TEXT_WITH_SOURCES), \`email_subject\` (ANY), \`email_body\` (TEXT_WITH_SOURCES)\n`;
|
|
1353
|
-
md += `- **Outputs**: \`send_status\` (ANY)\n`;
|
|
1354
|
-
md += `- **Critical**: Inputs must be TEXT_WITH_SOURCES — use intermediary chain from ANY-typed sources.\n`;
|
|
1355
|
-
md += `- See \`ema://rules/email-input-wiring\` for wiring patterns.\n\n`;
|
|
1356
|
-
md += `### json_mapper\n`;
|
|
1357
|
-
md += `Extract specific fields from JSON/structured data into individual outputs.\n`;
|
|
1358
|
-
md += `- **Inputs**: \`input_json\` (ANY), mapping rules\n`;
|
|
1359
|
-
md += `- **Outputs**: \`output_json\` per mapped field\n`;
|
|
1360
|
-
md += `- **Pairs with**: entity_extraction (field extraction), fixed_response (type conversion)\n\n`;
|
|
1361
|
-
md += `### rule_validation_with_documents\n`;
|
|
1362
|
-
md += `Check extracted data against business rules (compliance, thresholds).\n`;
|
|
1363
|
-
md += `- **Inputs**: \`primary_docs\` (DOCUMENT), \`map_of_extracted_columns\` (ANY)\n`;
|
|
1364
|
-
md += `- **Outputs**: \`ruleset_output\` (ANY — validation results)\n`;
|
|
1365
|
-
md += `- **Critical**: Rules configured in UI settings panel, not via workflow_def inputs.\n\n`;
|
|
1366
|
-
md += `### live_web_search\n`;
|
|
1367
|
-
md += `Real-time web search for current information.\n`;
|
|
1368
|
-
md += `- **Inputs**: \`query\` (TEXT_WITH_SOURCES)\n`;
|
|
1369
|
-
md += `- **Outputs**: \`web_search_results\` (SEARCH_RESULT)\n`;
|
|
1370
|
-
md += `- **Critical**: No data upload needed (searches the web).\n\n`;
|
|
1371
|
-
md += `### combine_search_results\n`;
|
|
1372
|
-
md += `Merge results from multiple search sources with deduplication.\n`;
|
|
1373
|
-
md += `- **Inputs**: \`search_results_1\` (SEARCH_RESULT), \`search_results_2\` (SEARCH_RESULT)\n`;
|
|
1374
|
-
md += `- **Outputs**: \`combined_results\` (SEARCH_RESULT)\n`;
|
|
1375
|
-
md += `- **Pairs with**: search/v2 + live_web_search, or any two search sources\n`;
|
|
1376
|
-
md += `- **Note**: \`combine_search_results/v0\` is deprecated. For new workflows, prefer \`call_llm\` with multiple \`named_inputs\` to combine search results.\n\n`;
|
|
1377
|
-
md += `### response_validator\n`;
|
|
1378
|
-
md += `Validate LLM output against quality/compliance criteria.\n`;
|
|
1379
|
-
md += `- **Inputs**: \`reference_query\` (TEXT_WITH_SOURCES), \`response_to_validate\` (TEXT_WITH_SOURCES)\n`;
|
|
1380
|
-
md += `- **Outputs**: \`abstain_reason\` (TEXT_WITH_SOURCES — reason for rejection, empty if valid)\n\n`;
|
|
1381
|
-
md += `### abstain_action\n`;
|
|
1382
|
-
md += `Provide a safe decline response when AI should not answer.\n`;
|
|
1383
|
-
md += `- **Inputs**: \`abstain_reason\` (TEXT_WITH_SOURCES)\n`;
|
|
1384
|
-
md += `- **Outputs**: \`abstain_reason\` (TEXT_WITH_SOURCES — decline message)\n\n`;
|
|
1385
|
-
md += `---\n\n`;
|
|
1386
|
-
md += `## Categorizer Configuration\n\n`;
|
|
1387
|
-
md += `### Defining enumTypes\n\n`;
|
|
1388
|
-
md += `Categories must be defined in \`workflow_def.enumTypes\`:\n\n`;
|
|
1389
|
-
md += `\`\`\`json\n`;
|
|
1390
|
-
md += `"enumTypes": [{\n`;
|
|
1391
|
-
md += ` "name": { "name": "intent_categories", "namespaces": [] },\n`;
|
|
1392
|
-
md += ` "options": [\n`;
|
|
1393
|
-
md += ` { "name": "Sales", "description": "Sales inquiries" },\n`;
|
|
1394
|
-
md += ` { "name": "Support", "description": "Technical support" },\n`;
|
|
1395
|
-
md += ` { "name": "General", "description": "General questions" },\n`;
|
|
1396
|
-
md += ` { "name": "Fallback", "description": "Unclear or unmatched intents" }\n`;
|
|
1397
|
-
md += ` ]\n`;
|
|
1398
|
-
md += `}]\n`;
|
|
1399
|
-
md += `\`\`\`\n\n`;
|
|
1400
|
-
md += `### typeArguments\n\n`;
|
|
1401
|
-
md += `The categorizer's \`typeArguments\` must reference the enum:\n\n`;
|
|
1402
|
-
md += `\`\`\`json\n`;
|
|
1403
|
-
md += `"typeArguments": {\n`;
|
|
1404
|
-
md += ` "categories": {\n`;
|
|
1405
|
-
md += ` "enumType": { "name": { "name": "intent_categories", "namespaces": [] } },\n`;
|
|
1406
|
-
md += ` "isList": false\n`;
|
|
1407
|
-
md += ` }\n`;
|
|
1408
|
-
md += `}\n`;
|
|
1409
|
-
md += `\`\`\`\n\n`;
|
|
1410
|
-
md += `**Critical**: Empty \`typeArguments\` causes deploy failure.\n\n`;
|
|
1411
|
-
md += `### runIf Condition Format\n\n`;
|
|
1412
|
-
md += `\`\`\`json\n`;
|
|
1413
|
-
md += `{\n`;
|
|
1414
|
-
md += ` "lhs": {\n`;
|
|
1415
|
-
md += ` "actionOutput": { "actionName": "chat_categorizer", "output": "category" },\n`;
|
|
1416
|
-
md += ` "autoDetectedBinding": false\n`;
|
|
1417
|
-
md += ` },\n`;
|
|
1418
|
-
md += ` "operator": 1,\n`;
|
|
1419
|
-
md += ` "rhs": {\n`;
|
|
1420
|
-
md += ` "inline": { "enumValue": "Market_Impact" },\n`;
|
|
1421
|
-
md += ` "autoDetectedBinding": false\n`;
|
|
1422
|
-
md += ` }\n`;
|
|
1423
|
-
md += `}\n`;
|
|
1424
|
-
md += `\`\`\`\n\n`;
|
|
1425
|
-
md += `| Operator | Meaning | Use Case |\n`;
|
|
1426
|
-
md += `|----------|---------|----------|\n`;
|
|
1427
|
-
md += `| \`1\` | Equals (\`==\`) | Route to handler when category matches |\n`;
|
|
1428
|
-
md += `| \`2\` | Not equals (\`!=\`) | Run for all categories except one |\n\n`;
|
|
1429
|
-
md += `For OR conditions (run for Sales OR General), use \`operator: 2\` with Fallback: \`category != Fallback\`.\n\n`;
|
|
1430
|
-
md += `### Nested Categorizers\n\n`;
|
|
1431
|
-
md += `For complex routing, chain categorizers:\n`;
|
|
1432
|
-
md += `\`\`\`\n`;
|
|
1433
|
-
md += `chat_categorizer (Level 1: HR, IT, General, Fallback)\n`;
|
|
1434
|
-
md += ` └─→ text_categorizer (Level 2 for HR: Benefits, Leave, Payroll, Fallback)\n`;
|
|
1435
|
-
md += `\`\`\`\n\n`;
|
|
1436
|
-
md += `---\n\n`;
|
|
1437
|
-
md += `## LLM Configuration (call_llm)\n\n`;
|
|
1438
|
-
md += `### named_inputs Convention\n\n`;
|
|
1439
|
-
md += `\`call_llm\` accepts additional context via suffix pattern \`named_inputs_<Descriptive_Name>\`:\n\n`;
|
|
1440
|
-
md += `| Named Input | Type | Purpose |\n`;
|
|
1441
|
-
md += `|-------------|------|----------|\n`;
|
|
1442
|
-
md += `| \`named_inputs_Search_Results\` | SEARCH_RESULT | KB search results for RAG |\n`;
|
|
1443
|
-
md += `| \`named_inputs_Conversation\` | CHAT_CONVERSATION | Full conversation history |\n`;
|
|
1444
|
-
md += `| \`named_inputs_Intent\` | ENUM | Detected category from categorizer |\n`;
|
|
1445
|
-
md += `| \`named_inputs_Current_Message\` | TEXT_WITH_SOURCES | Current user message |\n`;
|
|
1446
|
-
md += `| \`named_inputs_Tool_Result\` | TEXT_WITH_SOURCES | External action output |\n\n`;
|
|
1447
|
-
md += `\`named_inputs\` accepts **ANY** type — this is how you pass CHAT_CONVERSATION and SEARCH_RESULT into LLM nodes.\n\n`;
|
|
1448
|
-
md += `### Temperature Guidelines\n\n`;
|
|
1449
|
-
md += `| Use Case | Temperature | Why |\n`;
|
|
1450
|
-
md += `|----------|-------------|-----|\n`;
|
|
1451
|
-
md += `| Entity extraction | 0.0-0.3 | Accuracy over creativity |\n`;
|
|
1452
|
-
md += `| Document generation | 0.3-0.5 | Consistent formatting |\n`;
|
|
1453
|
-
md += `| General Q&A / chat | 0.5-0.7 | Balanced creativity and accuracy |\n`;
|
|
1454
|
-
md += `| Creative writing | 0.7-1.0 | More varied output |\n\n`;
|
|
1455
|
-
md += `### Consolidation Rule\n\n`;
|
|
1456
|
-
md += `If multiple \`call_llm\` nodes share the same inputs and differ only by \`trigger_when\` gate, consolidate them:\n`;
|
|
1457
|
-
md += `1. Create one \`call_llm\` that runs when not Fallback\n`;
|
|
1458
|
-
md += `2. Wire \`categorizer.category → call_llm.named_inputs_Intent\`\n`;
|
|
1459
|
-
md += `3. Update prompt: "Based on the detected intent ({{Intent}}), respond accordingly"\n`;
|
|
1460
|
-
md += `4. Keep separate nodes only when intents require different tools, search sources, or safety constraints.\n\n`;
|
|
1461
|
-
md += `---\n\n`;
|
|
1462
|
-
md += `## See Also\n\n`;
|
|
1463
|
-
md += `- \`ema://rules/input-sources\` — Input source validation rules\n`;
|
|
1464
|
-
md += `- \`ema://rules/extraction-column-format\` — Entity extraction column schema\n`;
|
|
1465
|
-
md += `- \`ema://rules/email-input-wiring\` — Email field wiring patterns\n`;
|
|
1466
|
-
md += `- \`ema://rules/json-output-patterns\` — custom_agent + json_mapper patterns\n`;
|
|
1467
|
-
md += `- \`ema://rules/chat-response-wiring\` — Chat response wiring rules\n`;
|
|
1468
|
-
md += `- \`ema://rules/anti-patterns\` — Common workflow anti-patterns\n`;
|
|
1469
|
-
return md;
|
|
1470
|
-
},
|
|
1471
|
-
},
|
|
1472
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1473
|
-
// Workflow Requirements & Guidance
|
|
1474
|
-
// NOT hardcoded templates - provide requirements and let LLM generate
|
|
1475
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1476
|
-
{
|
|
1477
|
-
uri: "ema://templates/voice-ai/requirements",
|
|
1478
|
-
name: "templates/voice-ai/requirements",
|
|
1479
|
-
description: "Voice AI workflow requirements and guidance. Use workflow(mode='get') for schema, then generate workflow_def.",
|
|
1480
|
-
mimeType: "application/json",
|
|
1481
|
-
generate: async () => {
|
|
1482
|
-
return JSON.stringify({
|
|
1483
|
-
_note: "Requirements and guidance for Voice AI workflows. LLM generates workflow_def based on these.",
|
|
1484
|
-
_usage: "1) workflow(mode='get', persona_id='...') for schema + fingerprint, " +
|
|
1485
|
-
"2) Generate/modify workflow_def, " +
|
|
1486
|
-
"3) workflow(mode='deploy', persona_id='...', base_fingerprint='<fingerprint>', workflow_def={...}) " +
|
|
1487
|
-
"(or workflow_def_path='/path/to/wf.json' for large payloads)",
|
|
1488
|
-
hard_requirements: {
|
|
1489
|
-
workflow_output: {
|
|
1490
|
-
rule: "MUST have results.WORKFLOW_OUTPUT mapped to final action output",
|
|
1491
|
-
example: '{ "results": { "WORKFLOW_OUTPUT": { "actionName": "respond", "outputName": "response" } } }',
|
|
1492
|
-
},
|
|
1493
|
-
workflow_name: {
|
|
1494
|
-
rule: "workflowName MUST be ['ema', 'personas', '<actual_persona_id>']",
|
|
1495
|
-
},
|
|
1496
|
-
trigger: {
|
|
1497
|
-
rule: "Voice AI uses chat_trigger (type CHAT)",
|
|
1498
|
-
namespace: ["triggers", "emainternal"],
|
|
1499
|
-
},
|
|
1500
|
-
response: {
|
|
1501
|
-
rule: "Must produce a response output that wires to WORKFLOW_OUTPUT",
|
|
1502
|
-
},
|
|
1503
|
-
},
|
|
1504
|
-
required_widgets: [
|
|
1505
|
-
{ name: "conversationSettings", type: 39, purpose: "Voice identity, welcome message, instructions" },
|
|
1506
|
-
{ name: "voiceSettings", type: 40, purpose: "Language, voice model" },
|
|
1507
|
-
{ name: "callSettings", type: 41, purpose: "Call forwarding, spam prevention" },
|
|
1508
|
-
{ name: "vadSettings", type: 42, purpose: "Voice activity detection, timeouts" },
|
|
1509
|
-
],
|
|
1510
|
-
common_patterns: {
|
|
1511
|
-
simple_qa: "chat_trigger → search → respond_for_external_actions → WORKFLOW_OUTPUT",
|
|
1512
|
-
with_routing: "chat_trigger → chat_categorizer → [branch per intent] → respond → WORKFLOW_OUTPUT",
|
|
1513
|
-
with_tools: "chat_trigger → categorizer → external_action_caller → respond_for_external_actions → WORKFLOW_OUTPUT",
|
|
1514
|
-
},
|
|
1515
|
-
best_practices: [
|
|
1516
|
-
"Use search/v2 (NOT v0) with datastore_configs",
|
|
1517
|
-
"Include Fallback category in every categorizer",
|
|
1518
|
-
"Use respond_for_external_actions (NOT deprecated respond_with_sources)",
|
|
1519
|
-
"For approval workflows: enable HITL flag on send_email_agent or entity_extraction_with_documents (only nodes that support HITL). general_hitl is NOT deployable.",
|
|
1520
|
-
],
|
|
1521
|
-
_next_step: "Call workflow(mode='get', persona_id='...') to get full schema, then generate workflow_def",
|
|
1522
|
-
}, null, 2);
|
|
1523
|
-
},
|
|
1524
|
-
},
|
|
1525
|
-
{
|
|
1526
|
-
uri: "ema://templates/voice-ai/config",
|
|
1527
|
-
name: "templates/voice-ai/config",
|
|
1528
|
-
description: "Voice AI configuration template (proto_config widgets). Customize values for your use case.",
|
|
1529
|
-
mimeType: "application/json",
|
|
1530
|
-
generate: async () => {
|
|
1531
|
-
const config = {
|
|
1532
|
-
_note: "Voice AI configuration template. Customize values for your use case.",
|
|
1533
|
-
_usage: "persona(method='update', id='<ID>', config={widgets: [<these widgets with your values>]})",
|
|
1534
|
-
widgets: [
|
|
1535
|
-
{
|
|
1536
|
-
name: "conversationSettings",
|
|
1537
|
-
type: 39,
|
|
1538
|
-
conversationSettings: {
|
|
1539
|
-
...VOICE_TEMPLATE_FALLBACK.conversationSettings,
|
|
1540
|
-
},
|
|
1541
|
-
},
|
|
1542
|
-
{
|
|
1543
|
-
name: "voiceSettings",
|
|
1544
|
-
type: 40,
|
|
1545
|
-
voiceSettings: {
|
|
1546
|
-
...VOICE_TEMPLATE_FALLBACK.voiceSettings,
|
|
1547
|
-
},
|
|
1548
|
-
},
|
|
1549
|
-
{
|
|
1550
|
-
name: "callSettings",
|
|
1551
|
-
type: 41,
|
|
1552
|
-
callSettings: {
|
|
1553
|
-
...VOICE_TEMPLATE_FALLBACK.callSettings,
|
|
1554
|
-
},
|
|
1555
|
-
},
|
|
1556
|
-
{
|
|
1557
|
-
name: "vadSettings",
|
|
1558
|
-
type: 42,
|
|
1559
|
-
vadSettings: {
|
|
1560
|
-
...VOICE_TEMPLATE_FALLBACK.vadSettings,
|
|
1561
|
-
},
|
|
1562
|
-
},
|
|
1563
|
-
],
|
|
1564
|
-
field_docs: VOICE_TEMPLATE_FIELD_DOCS,
|
|
1565
|
-
};
|
|
1566
|
-
return JSON.stringify(config, null, 2);
|
|
1567
|
-
},
|
|
1568
|
-
},
|
|
1569
|
-
{
|
|
1570
|
-
uri: "ema://templates/voice-ai/guide",
|
|
1571
|
-
name: "templates/voice-ai/guide",
|
|
1572
|
-
description: "Voice AI creation guide with requirements and step-by-step process",
|
|
1573
|
-
mimeType: "text/markdown",
|
|
1574
|
-
generate: async () => {
|
|
1575
|
-
return `# Voice AI Creation Guide
|
|
1576
|
-
|
|
1577
|
-
## Process (Follow This Order)
|
|
1578
|
-
|
|
1579
|
-
### 1. Create Persona
|
|
1580
|
-
\`\`\`
|
|
1581
|
-
persona(method="create", name="Your Voice AI", type="voice")
|
|
1582
|
-
\`\`\`
|
|
1583
|
-
|
|
1584
|
-
### 2. Get Workflow Schema
|
|
1585
|
-
\`\`\`
|
|
1586
|
-
workflow(mode="get", persona_id="<ID>")
|
|
1587
|
-
\`\`\`
|
|
1588
|
-
This returns:
|
|
1589
|
-
- Current workflow (if any)
|
|
1590
|
-
- Deprecation warnings (fix these first!)
|
|
1591
|
-
- Generation schema (agents, constraints)
|
|
1592
|
-
- Requirements and guidance
|
|
1593
|
-
|
|
1594
|
-
### 3. Generate Workflow
|
|
1595
|
-
Using the schema, generate a workflow_def that:
|
|
1596
|
-
- Has WORKFLOW_OUTPUT in results
|
|
1597
|
-
- Uses non-deprecated actions
|
|
1598
|
-
- Follows the flow pattern: trigger → processing → response → OUTPUT
|
|
1599
|
-
|
|
1600
|
-
### 4. Deploy Workflow
|
|
1601
|
-
\`\`\`
|
|
1602
|
-
workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def={...})
|
|
1603
|
-
\n# For large payloads:\n# workflow(mode="deploy", persona_id="<ID>", base_fingerprint="<fingerprint>", workflow_def_path="/path/to/wf.json")
|
|
1604
|
-
\`\`\`
|
|
1605
|
-
Use \`workflow(mode="validate", ...)\` before deploy to catch structural/semantic issues.
|
|
1606
|
-
|
|
1607
|
-
### 5. Configure Settings
|
|
1608
|
-
\`\`\`
|
|
1609
|
-
persona(method="update", id="<ID>", config={widgets: [...]})
|
|
1610
|
-
\`\`\`
|
|
1611
|
-
|
|
1612
|
-
### 6. Upload Knowledge
|
|
1613
|
-
\`\`\`
|
|
1614
|
-
persona(id="<ID>", data={method:"upload", path:"your-data.txt"})
|
|
1615
|
-
\`\`\`
|
|
1616
|
-
|
|
1617
|
-
## Hard Requirements
|
|
1618
|
-
|
|
1619
|
-
| Requirement | Why |
|
|
1620
|
-
|-------------|-----|
|
|
1621
|
-
| WORKFLOW_OUTPUT | Persona cannot be activated without it |
|
|
1622
|
-
| workflowName format | API rejects invalid namespaces |
|
|
1623
|
-
| Non-deprecated actions | Deprecated actions may fail |
|
|
1624
|
-
|
|
1625
|
-
## Check for Deprecated Actions
|
|
1626
|
-
|
|
1627
|
-
\`workflow(mode="get")\` returns \`deprecation_warnings\` if any actions are deprecated.
|
|
1628
|
-
**Fix these BEFORE deploying.**
|
|
1629
|
-
|
|
1630
|
-
## Common Deprecated Actions
|
|
1631
|
-
|
|
1632
|
-
| Deprecated | Use Instead |
|
|
1633
|
-
|------------|-------------|
|
|
1634
|
-
| search/v0 | search/v2 (requires datastore_configs) |
|
|
1635
|
-
| respond_with_sources | respond_for_external_actions |
|
|
1636
|
-
| call_llm/v0 | call_llm/v2 |
|
|
1637
|
-
|
|
1638
|
-
## Anti-Patterns
|
|
1639
|
-
|
|
1640
|
-
❌ Cloning random existing persona
|
|
1641
|
-
❌ Skipping workflow_def
|
|
1642
|
-
❌ Using deprecated actions
|
|
1643
|
-
❌ Deploying without preview
|
|
1644
|
-
|
|
1645
|
-
${VOICE_TEMPLATE_FIELD_DOCS}
|
|
1646
|
-
`;
|
|
1647
|
-
},
|
|
1648
|
-
},
|
|
1649
|
-
// Persona Templates - Dynamic from API with fallback
|
|
1650
|
-
{
|
|
1651
|
-
uri: "ema://catalog/templates",
|
|
1652
|
-
name: "catalog/templates",
|
|
1653
|
-
description: "Persona templates from Ema API: pre-configured AI Employee templates (chatbot_starter, voicebot, dashboard, etc.)",
|
|
1654
|
-
mimeType: "application/json",
|
|
1655
|
-
generate: async (ctx) => {
|
|
1656
|
-
const templates = await getDynamicPersonaTemplates({ env: ctx.env });
|
|
1657
|
-
if (templates.length > 0) {
|
|
1658
|
-
return JSON.stringify(templates.map(templateDtoToResource), null, 2);
|
|
1659
|
-
}
|
|
1660
|
-
// Fallback when API unavailable - provide guidance
|
|
1661
|
-
return JSON.stringify({
|
|
1662
|
-
_note: "API templates unavailable. Use catalog(method='list', type='templates') tool which connects to API directly, or use template(config='voice|chat|dashboard') for config fallbacks.",
|
|
1663
|
-
fallback_types: ["voice", "chat", "dashboard"],
|
|
1664
|
-
how_to_get: "catalog(method='list', type='templates') or template(config='voice')",
|
|
1665
|
-
}, null, 2);
|
|
1666
|
-
},
|
|
1667
|
-
},
|
|
1668
|
-
{
|
|
1669
|
-
uri: "ema://catalog/templates-summary",
|
|
1670
|
-
name: "catalog/templates-summary",
|
|
1671
|
-
description: "Persona templates summary: template names, categories, and descriptions for quick reference",
|
|
1672
|
-
mimeType: "text/markdown",
|
|
1673
|
-
generate: async (ctx) => {
|
|
1674
|
-
const templates = await getDynamicPersonaTemplates({ env: ctx.env });
|
|
1675
|
-
if (templates.length === 0) {
|
|
1676
|
-
return `# Persona Templates
|
|
1677
|
-
|
|
1678
|
-
> API templates not available. Use one of these alternatives:
|
|
1679
|
-
|
|
1680
|
-
## Option 1: Use the catalog tool directly
|
|
1681
|
-
\`\`\`
|
|
1682
|
-
catalog(method="list", type="templates")
|
|
1683
|
-
\`\`\`
|
|
1684
|
-
This connects to the API and may have different credentials.
|
|
1685
|
-
|
|
1686
|
-
## Option 2: Use config fallbacks
|
|
1687
|
-
\`\`\`
|
|
1688
|
-
template(config="voice") // Voice AI config template
|
|
1689
|
-
template(config="chat") // Chat AI config template
|
|
1690
|
-
template(config="dashboard") // Dashboard AI config template
|
|
1691
|
-
\`\`\`
|
|
1692
|
-
|
|
1693
|
-
## Option 3: Create from scratch
|
|
1694
|
-
\`\`\`
|
|
1695
|
-
persona(method="create", name="My AI", type="voice|chat|dashboard")
|
|
1696
|
-
\`\`\`
|
|
1697
|
-
`;
|
|
1698
|
-
}
|
|
1699
|
-
const byCategory = new Map();
|
|
1700
|
-
for (const t of templates) {
|
|
1701
|
-
const cat = t.category || "GENERAL";
|
|
1702
|
-
if (!byCategory.has(cat))
|
|
1703
|
-
byCategory.set(cat, []);
|
|
1704
|
-
byCategory.get(cat).push(t);
|
|
1705
|
-
}
|
|
1706
|
-
let md = "# Persona Templates (from API)\n\n";
|
|
1707
|
-
md += `> ${templates.length} templates available for creating AI Employees\n\n`;
|
|
1708
|
-
for (const [category, tpls] of byCategory) {
|
|
1709
|
-
md += `## ${category}\n\n`;
|
|
1710
|
-
md += "| Template | Type | Description |\n";
|
|
1711
|
-
md += "|----------|------|-------------|\n";
|
|
1712
|
-
for (const t of tpls) {
|
|
1713
|
-
const triggerType = t.trigger_type || "-";
|
|
1714
|
-
const desc = t.description?.slice(0, 80) || "-";
|
|
1715
|
-
md += `| \`${t.name}\` | ${triggerType} | ${desc}... |\n`;
|
|
1716
|
-
}
|
|
1717
|
-
md += "\n";
|
|
1718
|
-
}
|
|
1719
|
-
return md;
|
|
1720
|
-
},
|
|
1721
|
-
},
|
|
1722
|
-
// Action Schema - Complete action definitions from ema repo
|
|
1723
|
-
{
|
|
1724
|
-
uri: "ema://schema/actions",
|
|
1725
|
-
name: "schema/actions",
|
|
1726
|
-
description: "Complete action schema: all workflow actions with inputs, outputs, types, and documentation from ema_backend/grpc",
|
|
1727
|
-
mimeType: "application/json",
|
|
1728
|
-
generate: async (ctx) => {
|
|
1729
|
-
const schema = await getDynamicActionSchema({ env: ctx.env });
|
|
1730
|
-
return JSON.stringify(schema, null, 2);
|
|
1731
|
-
},
|
|
1732
|
-
},
|
|
1733
|
-
{
|
|
1734
|
-
uri: "ema://schema/actions-summary",
|
|
1735
|
-
name: "schema/actions-summary",
|
|
1736
|
-
description: "Action schema summary: action names, versions, and descriptions for quick reference",
|
|
1737
|
-
mimeType: "text/markdown",
|
|
1738
|
-
generate: async (ctx) => {
|
|
1739
|
-
const schema = await getDynamicActionSchema({ env: ctx.env });
|
|
1740
|
-
if (schema.actions.length === 0) {
|
|
1741
|
-
return "# Action Schema\n\n> No actions loaded. Check API connectivity or bundled schema.\n";
|
|
1742
|
-
}
|
|
1743
|
-
let md = "# Action Schema\n\n";
|
|
1744
|
-
md += `> ${schema.actions.length} actions loaded (source: ${schema.source})\n`;
|
|
1745
|
-
md += `> Version: ${schema.version}\n\n`;
|
|
1746
|
-
const byCategory = new Map();
|
|
1747
|
-
for (const action of schema.actions) {
|
|
1748
|
-
const cat = action.category || "OTHER";
|
|
1749
|
-
if (!byCategory.has(cat))
|
|
1750
|
-
byCategory.set(cat, []);
|
|
1751
|
-
byCategory.get(cat).push(action);
|
|
1752
|
-
}
|
|
1753
|
-
for (const [category, actions] of byCategory) {
|
|
1754
|
-
md += `## ${category.replace("ACTION_CATEGORY_", "")}\n\n`;
|
|
1755
|
-
md += "| Action | Version | Description | Inputs | Outputs |\n";
|
|
1756
|
-
md += "|--------|---------|-------------|--------|--------|\n";
|
|
1757
|
-
for (const action of actions) {
|
|
1758
|
-
const inputs = action.inputs?.size || 0;
|
|
1759
|
-
const outputs = action.outputs?.size || 0;
|
|
1760
|
-
const desc = action.description?.slice(0, 50) || "-";
|
|
1761
|
-
md += `| \`${action.name}\` | ${action.version || "v0"} | ${desc}... | ${inputs} | ${outputs} |\n`;
|
|
1762
|
-
}
|
|
1763
|
-
md += "\n";
|
|
1764
|
-
}
|
|
1765
|
-
return md;
|
|
1766
|
-
},
|
|
1767
|
-
},
|
|
1768
|
-
{
|
|
1769
|
-
uri: "ema://schema/action/:name",
|
|
1770
|
-
name: "schema/action",
|
|
1771
|
-
description: "Individual action schema by name (e.g., ema://schema/action/call_llm)",
|
|
1772
|
-
mimeType: "application/json",
|
|
1773
|
-
generate: async (ctx) => {
|
|
1774
|
-
// This is a template - actual resolution happens in getResource
|
|
1775
|
-
return JSON.stringify({ error: "Use specific action name in URI" }, null, 2);
|
|
1776
|
-
},
|
|
1777
|
-
},
|
|
1778
|
-
{
|
|
1779
|
-
uri: "ema://schema/workflow-def",
|
|
1780
|
-
name: "schema/workflow-def",
|
|
1781
|
-
description: "Complete workflow_def schema with examples for Chat, Voice, and Dashboard personas. Essential reference for building workflows.",
|
|
1782
|
-
mimeType: "text/markdown",
|
|
1783
|
-
generate: async () => {
|
|
1784
|
-
return `# Workflow Definition Schema
|
|
1785
|
-
|
|
1786
|
-
This document describes the structure of \`workflow_def\` - the JSON format used to deploy AI Employee workflows.
|
|
1787
|
-
|
|
1788
|
-
## Two-Level Architecture
|
|
1789
|
-
|
|
1790
|
-
1. **WorkflowSpec** (High-level) - What YOU build:
|
|
1791
|
-
- Nodes, inputs, categories, result mappings
|
|
1792
|
-
- Human-readable, easy to reason about
|
|
1793
|
-
- Gets compiled to workflow_def
|
|
1794
|
-
|
|
1795
|
-
2. **workflow_def** (Low-level) - What gets deployed:
|
|
1796
|
-
- Compiled JSON structure
|
|
1797
|
-
- Namespaces, action references, edges
|
|
1798
|
-
- Generated by MCP from WorkflowSpec
|
|
1799
|
-
|
|
1800
|
-
## WorkflowSpec Structure
|
|
1801
|
-
|
|
1802
|
-
\`\`\`typescript
|
|
1803
|
-
interface WorkflowSpec {
|
|
1804
|
-
name: string; // Workflow display name
|
|
1805
|
-
description: string; // What it does
|
|
1806
|
-
personaType: "voice" | "chat" | "dashboard";
|
|
1807
|
-
nodes: Node[]; // Workflow actions
|
|
1808
|
-
resultMappings: ResultMapping[]; // What outputs to return
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
interface Node {
|
|
1812
|
-
id: string; // Unique identifier (e.g., "trigger", "search", "respond")
|
|
1813
|
-
actionType: string; // Action type (e.g., "chat_trigger", "search", "call_llm")
|
|
1814
|
-
displayName: string; // Human-readable name
|
|
1815
|
-
description?: string; // What this node does
|
|
1816
|
-
inputs?: Record<string, InputBinding>; // Input configuration
|
|
1817
|
-
categories?: Category[]; // For categorizers only
|
|
1818
|
-
tools?: Tool[]; // For external action callers
|
|
1819
|
-
runIf?: RunIfCondition; // Conditional execution
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
interface InputBinding {
|
|
1823
|
-
type: "action_output" | "literal" | "widget_config" | "llm_inferred";
|
|
1824
|
-
actionName?: string; // Source node ID (for action_output)
|
|
1825
|
-
output?: string; // Output name from source
|
|
1826
|
-
value?: any; // Static value (for literal)
|
|
1827
|
-
widgetName?: string; // Widget name (for widget_config)
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
interface Category {
|
|
1831
|
-
name: string; // Category name (e.g., "Sales", "Support", "Fallback")
|
|
1832
|
-
description: string; // When to route here
|
|
1833
|
-
examples?: string[]; // Example phrases
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
interface ResultMapping {
|
|
1837
|
-
nodeId: string; // Node that produces the output
|
|
1838
|
-
output: string; // Output name
|
|
1839
|
-
}
|
|
1840
|
-
\`\`\`
|
|
1841
|
-
|
|
1842
|
-
## Trigger Types by Persona
|
|
1843
|
-
|
|
1844
|
-
| Persona Type | Trigger ActionType | Key Outputs |
|
|
1845
|
-
|--------------|-------------------|-------------|
|
|
1846
|
-
| **Chat** | \`chat_trigger\` | user_query, chat_conversation |
|
|
1847
|
-
| **Voice** | \`chat_trigger\` | user_query, chat_conversation |
|
|
1848
|
-
| **Dashboard** | \`document_trigger\` | document_content, row_data |
|
|
1849
|
-
|
|
1850
|
-
## Example: Chat Persona (KB Search)
|
|
1851
|
-
|
|
1852
|
-
\`\`\`json
|
|
1853
|
-
{
|
|
1854
|
-
"name": "FAQ Bot",
|
|
1855
|
-
"description": "Answer questions from knowledge base",
|
|
1856
|
-
"personaType": "chat",
|
|
1857
|
-
"nodes": [
|
|
1858
|
-
{
|
|
1859
|
-
"id": "trigger",
|
|
1860
|
-
"actionType": "chat_trigger",
|
|
1861
|
-
"displayName": "Chat Input"
|
|
1862
|
-
},
|
|
1863
|
-
{
|
|
1864
|
-
"id": "search",
|
|
1865
|
-
"actionType": "search",
|
|
1866
|
-
"displayName": "Search Knowledge Base",
|
|
1867
|
-
"inputs": {
|
|
1868
|
-
"query": {
|
|
1869
|
-
"type": "action_output",
|
|
1870
|
-
"actionName": "trigger",
|
|
1871
|
-
"output": "user_query"
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
},
|
|
1875
|
-
{
|
|
1876
|
-
"id": "respond",
|
|
1877
|
-
"actionType": "respond_for_external_actions",
|
|
1878
|
-
"displayName": "Generate Response",
|
|
1879
|
-
"inputs": {
|
|
1880
|
-
"search_results": {
|
|
1881
|
-
"type": "action_output",
|
|
1882
|
-
"actionName": "search",
|
|
1883
|
-
"output": "search_results"
|
|
1884
|
-
},
|
|
1885
|
-
"conversation": {
|
|
1886
|
-
"type": "action_output",
|
|
1887
|
-
"actionName": "trigger",
|
|
1888
|
-
"output": "chat_conversation"
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
],
|
|
1893
|
-
"resultMappings": [
|
|
1894
|
-
{ "nodeId": "respond", "output": "response" }
|
|
1895
|
-
]
|
|
1896
|
-
}
|
|
1897
|
-
\`\`\`
|
|
1898
|
-
|
|
1899
|
-
## Example: Dashboard Persona (Document Extraction)
|
|
1900
|
-
|
|
1901
|
-
\`\`\`json
|
|
1902
|
-
{
|
|
1903
|
-
"name": "Invoice Processor",
|
|
1904
|
-
"description": "Extract data from invoices",
|
|
1905
|
-
"personaType": "dashboard",
|
|
1906
|
-
"nodes": [
|
|
1907
|
-
{
|
|
1908
|
-
"id": "trigger",
|
|
1909
|
-
"actionType": "document_trigger",
|
|
1910
|
-
"displayName": "Document Input"
|
|
1911
|
-
},
|
|
1912
|
-
{
|
|
1913
|
-
"id": "extract",
|
|
1914
|
-
"actionType": "entity_extraction",
|
|
1915
|
-
"displayName": "Extract Invoice Data",
|
|
1916
|
-
"inputs": {
|
|
1917
|
-
"document": {
|
|
1918
|
-
"type": "action_output",
|
|
1919
|
-
"actionName": "trigger",
|
|
1920
|
-
"output": "document_content"
|
|
1921
|
-
},
|
|
1922
|
-
"extraction_schema": {
|
|
1923
|
-
"type": "literal",
|
|
1924
|
-
"value": {
|
|
1925
|
-
"fields": ["invoice_number", "amount", "date", "vendor"]
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
},
|
|
1930
|
-
{
|
|
1931
|
-
"id": "respond",
|
|
1932
|
-
"actionType": "fixed_response",
|
|
1933
|
-
"displayName": "Return Results",
|
|
1934
|
-
"inputs": {
|
|
1935
|
-
"data": {
|
|
1936
|
-
"type": "action_output",
|
|
1937
|
-
"actionName": "extract",
|
|
1938
|
-
"output": "extracted_entities"
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
],
|
|
1943
|
-
"resultMappings": [
|
|
1944
|
-
{ "nodeId": "respond", "output": "response" }
|
|
1945
|
-
]
|
|
1946
|
-
}
|
|
1947
|
-
\`\`\`
|
|
1948
|
-
|
|
1949
|
-
## Example: Voice Persona (Intent Routing)
|
|
1950
|
-
|
|
1951
|
-
\`\`\`json
|
|
1952
|
-
{
|
|
1953
|
-
"name": "IT Support Voice",
|
|
1954
|
-
"description": "Handle IT support calls",
|
|
1955
|
-
"personaType": "voice",
|
|
1956
|
-
"nodes": [
|
|
1957
|
-
{
|
|
1958
|
-
"id": "trigger",
|
|
1959
|
-
"actionType": "chat_trigger",
|
|
1960
|
-
"displayName": "Voice Input"
|
|
1961
|
-
},
|
|
1962
|
-
{
|
|
1963
|
-
"id": "categorize",
|
|
1964
|
-
"actionType": "chat_categorizer",
|
|
1965
|
-
"displayName": "Identify Intent",
|
|
1966
|
-
"inputs": {
|
|
1967
|
-
"conversation": {
|
|
1968
|
-
"type": "action_output",
|
|
1969
|
-
"actionName": "trigger",
|
|
1970
|
-
"output": "chat_conversation"
|
|
1971
|
-
}
|
|
1972
|
-
},
|
|
1973
|
-
"categories": [
|
|
1974
|
-
{ "name": "Password Reset", "description": "User needs password help" },
|
|
1975
|
-
{ "name": "Create Ticket", "description": "User wants to create a ticket" },
|
|
1976
|
-
{ "name": "Fallback", "description": "Anything else" }
|
|
1977
|
-
]
|
|
1978
|
-
},
|
|
1979
|
-
{
|
|
1980
|
-
"id": "handle_password",
|
|
1981
|
-
"actionType": "call_llm",
|
|
1982
|
-
"displayName": "Password Reset Handler",
|
|
1983
|
-
"runIf": {
|
|
1984
|
-
"sourceAction": "categorize",
|
|
1985
|
-
"sourceOutput": "category",
|
|
1986
|
-
"operator": "eq",
|
|
1987
|
-
"value": "Password Reset"
|
|
1988
|
-
},
|
|
1989
|
-
"inputs": {
|
|
1990
|
-
"prompt": {
|
|
1991
|
-
"type": "literal",
|
|
1992
|
-
"value": "Guide user through password reset process..."
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
},
|
|
1996
|
-
{
|
|
1997
|
-
"id": "handle_ticket",
|
|
1998
|
-
"actionType": "external_action_caller",
|
|
1999
|
-
"displayName": "Create ServiceNow Ticket",
|
|
2000
|
-
"runIf": {
|
|
2001
|
-
"sourceAction": "categorize",
|
|
2002
|
-
"sourceOutput": "category",
|
|
2003
|
-
"operator": "eq",
|
|
2004
|
-
"value": "Create Ticket"
|
|
2005
|
-
},
|
|
2006
|
-
"tools": [
|
|
2007
|
-
{ "name": "Create_Incident", "namespace": "service_now" }
|
|
2008
|
-
]
|
|
2009
|
-
},
|
|
2010
|
-
{
|
|
2011
|
-
"id": "handle_fallback",
|
|
2012
|
-
"actionType": "call_llm",
|
|
2013
|
-
"displayName": "General Response",
|
|
2014
|
-
"runIf": {
|
|
2015
|
-
"sourceAction": "categorize",
|
|
2016
|
-
"sourceOutput": "category",
|
|
2017
|
-
"operator": "eq",
|
|
2018
|
-
"value": "Fallback"
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
],
|
|
2022
|
-
"resultMappings": [
|
|
2023
|
-
{ "nodeId": "handle_password", "output": "response" },
|
|
2024
|
-
{ "nodeId": "handle_ticket", "output": "result" },
|
|
2025
|
-
{ "nodeId": "handle_fallback", "output": "response" }
|
|
2026
|
-
]
|
|
2027
|
-
}
|
|
2028
|
-
\`\`\`
|
|
2029
|
-
|
|
2030
|
-
## Critical Rules
|
|
2031
|
-
|
|
2032
|
-
1. **Always include Fallback** - Every categorizer MUST have a Fallback category
|
|
2033
|
-
2. **Trigger type matters** - Dashboard uses document_trigger, others use chat_trigger
|
|
2034
|
-
3. **Wire conversations correctly** - Categorizers need chat_conversation, search needs user_query
|
|
2035
|
-
4. **Map results** - At least one node must have a resultMapping to WORKFLOW_OUTPUT
|
|
2036
|
-
5. **Check deprecated actions** - Always verify against ema://rules/deprecated-actions-summary
|
|
2037
|
-
|
|
2038
|
-
## Common Input Bindings
|
|
2039
|
-
|
|
2040
|
-
| Source | Type | Example |
|
|
2041
|
-
|--------|------|---------|
|
|
2042
|
-
| Previous node output | action_output | \`{ type: "action_output", actionName: "search", output: "results" }\` |
|
|
2043
|
-
| Static value | literal | \`{ type: "literal", value: "Hello world" }\` |
|
|
2044
|
-
| Widget config | widget_config | \`{ type: "widget_config", widgetName: "conversationSettings" }\` |
|
|
2045
|
-
| LLM should infer | llm_inferred | \`{ type: "llm_inferred" }\` |
|
|
2046
|
-
|
|
2047
|
-
## Deployment Flow
|
|
2048
|
-
|
|
2049
|
-
1. Build your WorkflowSpec (structure above)
|
|
2050
|
-
2. Call \`workflow(mode="validate", workflow_spec={...})\` to check for errors
|
|
2051
|
-
3. Call \`workflow(mode="get", persona_id="...")\` to get the latest fingerprint (stale-state protection)
|
|
2052
|
-
4. Call \`workflow(mode="deploy", persona_id="...", base_fingerprint="<fingerprint>", workflow_def={...})\` to deploy
|
|
2053
|
-
|
|
2054
|
-
For large payloads, use \`workflow_def_path\` instead of inline \`workflow_def\`.
|
|
2055
|
-
|
|
2056
|
-
The MCP compiles your WorkflowSpec into the API's workflow_def format automatically.
|
|
2057
|
-
|
|
2058
|
-
## Raw workflow_def Input Binding Formats
|
|
2059
|
-
|
|
2060
|
-
When deploying raw workflow_def (not WorkflowSpec), inputs use protobuf-compatible JSON.
|
|
2061
|
-
These formats are CRITICAL - wrong formats cause HTTP 500 with no error details.
|
|
2062
|
-
|
|
2063
|
-
### Action Output (reference another node's output)
|
|
2064
|
-
\`\`\`json
|
|
2065
|
-
{
|
|
2066
|
-
"actionOutput": { "actionName": "source_node_name", "output": "output_name" },
|
|
2067
|
-
"autoDetectedBinding": false
|
|
2068
|
-
}
|
|
2069
|
-
\`\`\`
|
|
2070
|
-
|
|
2071
|
-
### Widget Config (reference a proto_config widget)
|
|
2072
|
-
\`\`\`json
|
|
2073
|
-
{
|
|
2074
|
-
"widgetConfig": { "widgetName": "fusionModel" },
|
|
2075
|
-
"autoDetectedBinding": true
|
|
2076
|
-
}
|
|
2077
|
-
\`\`\`
|
|
2078
|
-
|
|
2079
|
-
### Inline String (STRING type)
|
|
2080
|
-
\`\`\`json
|
|
2081
|
-
{
|
|
2082
|
-
"inline": { "wellKnown": { "stringValue": "your text here" } },
|
|
2083
|
-
"autoDetectedBinding": false
|
|
2084
|
-
}
|
|
2085
|
-
\`\`\`
|
|
2086
|
-
|
|
2087
|
-
### Inline Text With Sources (TEXT_WITH_SOURCES type)
|
|
2088
|
-
\`\`\`json
|
|
2089
|
-
{
|
|
2090
|
-
"inline": {
|
|
2091
|
-
"wellKnown": {
|
|
2092
|
-
"textWithSources": {
|
|
2093
|
-
"text": "your text here",
|
|
2094
|
-
"sources": [],
|
|
2095
|
-
"resultConfidence": 0,
|
|
2096
|
-
"toolSources": [],
|
|
2097
|
-
"resultType": 0
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
},
|
|
2101
|
-
"autoDetectedBinding": false
|
|
2102
|
-
}
|
|
2103
|
-
\`\`\`
|
|
2104
|
-
**WARNING**: STRING ≠ TEXT_WITH_SOURCES. Using stringValue where textWithSources is expected causes HTTP 500.
|
|
2105
|
-
|
|
2106
|
-
### Inline Enum Value
|
|
2107
|
-
\`\`\`json
|
|
2108
|
-
{
|
|
2109
|
-
"inline": { "enumValue": "CategoryName" },
|
|
2110
|
-
"autoDetectedBinding": false
|
|
66
|
+
".env.development",
|
|
67
|
+
".env.production",
|
|
68
|
+
".env.test",
|
|
69
|
+
"secrets.yaml",
|
|
70
|
+
"secrets.json",
|
|
71
|
+
".npmrc",
|
|
72
|
+
".pypirc",
|
|
73
|
+
"id_rsa",
|
|
74
|
+
"id_ed25519",
|
|
75
|
+
"*.pem",
|
|
76
|
+
"*.key",
|
|
77
|
+
];
|
|
78
|
+
function containsSecrets(content) {
|
|
79
|
+
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
|
|
2111
80
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
"namedBinding": {
|
|
2121
|
-
"name": "context_key",
|
|
2122
|
-
"value": {
|
|
2123
|
-
"actionOutput": { "actionName": "source_node", "output": "response_with_sources" },
|
|
2124
|
-
"autoDetectedBinding": false
|
|
2125
|
-
},
|
|
2126
|
-
"description": "",
|
|
2127
|
-
"isOptional": false
|
|
2128
|
-
},
|
|
2129
|
-
"autoDetectedBinding": false
|
|
2130
|
-
}
|
|
2131
|
-
]
|
|
2132
|
-
},
|
|
2133
|
-
"autoDetectedBinding": false
|
|
81
|
+
function isBlockedFile(filename) {
|
|
82
|
+
const basename = path.basename(filename);
|
|
83
|
+
return BLOCKED_FILES.some((blocked) => {
|
|
84
|
+
if (blocked.startsWith("*")) {
|
|
85
|
+
return basename.endsWith(blocked.slice(1));
|
|
86
|
+
}
|
|
87
|
+
return basename === blocked;
|
|
88
|
+
});
|
|
2134
89
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
### Action Name Format
|
|
2138
|
-
\`\`\`json
|
|
2139
|
-
{
|
|
2140
|
-
"action": {
|
|
2141
|
-
"name": { "namespaces": ["actions", "emainternal"], "name": "call_llm" },
|
|
2142
|
-
"version": "v2"
|
|
2143
|
-
}
|
|
90
|
+
function hasPathTraversal(uriPath) {
|
|
91
|
+
return uriPath.includes("..") || uriPath.includes("//");
|
|
2144
92
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
{
|
|
2157
|
-
uri: "ema://schema/workflow-def-json",
|
|
2158
|
-
name: "schema/workflow-def-json",
|
|
2159
|
-
description: "JSON Schema for workflow_def validation. Use for pre-validation before deployment.",
|
|
2160
|
-
mimeType: "application/json",
|
|
2161
|
-
generate: async () => {
|
|
2162
|
-
const { getWorkflowDefSchemaJSON } = await import("./domain/workflow-def-schema.js");
|
|
2163
|
-
return getWorkflowDefSchemaJSON();
|
|
2164
|
-
},
|
|
2165
|
-
},
|
|
2166
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2167
|
-
// Guidance Resources - Single source of truth, multiple export formats
|
|
2168
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2169
|
-
{
|
|
2170
|
-
uri: "ema://docs/usage-guide",
|
|
2171
|
-
name: "docs/usage-guide",
|
|
2172
|
-
description: "Complete MCP usage guide with rules, patterns, and tool reference (markdown format)",
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Resource Registry
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// Allowlisted resource mappings: uri -> relative file path
|
|
97
|
+
// Only includes files that ACTUALLY exist in this repo (not symlinked)
|
|
98
|
+
// and are useful for MCP consumers
|
|
99
|
+
const RESOURCE_MAP = {
|
|
100
|
+
// Core Documentation (public guides)
|
|
101
|
+
"ema://docs/getting-started": {
|
|
102
|
+
path: ".context/public/guides/mcp-tools-guide.md",
|
|
103
|
+
description: "Quick start guide: first steps, key concepts, essential rules, common operations",
|
|
2173
104
|
mimeType: "text/markdown",
|
|
2174
|
-
generate: async () => {
|
|
2175
|
-
const { exportAsMarkdown } = await import("./guidance.js");
|
|
2176
|
-
return exportAsMarkdown();
|
|
2177
|
-
},
|
|
2178
105
|
},
|
|
2179
|
-
{
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
mimeType: "application/json",
|
|
2184
|
-
generate: async () => {
|
|
2185
|
-
const { exportAsJSON } = await import("./guidance.js");
|
|
2186
|
-
return exportAsJSON();
|
|
2187
|
-
},
|
|
106
|
+
"ema://docs/readme": {
|
|
107
|
+
path: "README.md",
|
|
108
|
+
description: "Ema Toolkit overview: MCP tools, prompts, resources, CLI usage, SDK API, configuration",
|
|
109
|
+
mimeType: "text/markdown",
|
|
2188
110
|
},
|
|
2189
|
-
{
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
description: "Guidance exported as Cursor .mdc rule format - for IDE integration",
|
|
111
|
+
"ema://docs/ema-user-guide": {
|
|
112
|
+
path: ".context/public/guides/ema-user-guide.md",
|
|
113
|
+
description: "Ema Platform guide: concepts (AI Employees, Workflows, Agents), glossary, architecture, examples, troubleshooting",
|
|
2193
114
|
mimeType: "text/markdown",
|
|
2194
|
-
generate: async () => {
|
|
2195
|
-
const { exportAsCursorRule } = await import("./guidance.js");
|
|
2196
|
-
return exportAsCursorRule();
|
|
2197
|
-
},
|
|
2198
115
|
},
|
|
2199
|
-
{
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
mimeType: "text/plain",
|
|
2204
|
-
generate: async () => {
|
|
2205
|
-
const { generateServerInstructions } = await import("./guidance.js");
|
|
2206
|
-
return generateServerInstructions();
|
|
2207
|
-
},
|
|
116
|
+
"ema://docs/mcp-tools-guide": {
|
|
117
|
+
path: ".context/public/guides/mcp-tools-guide.md",
|
|
118
|
+
description: "MCP tools usage guide: tool categories, best practices, example call sequences, workflow review patterns",
|
|
119
|
+
mimeType: "text/markdown",
|
|
2208
120
|
},
|
|
2209
|
-
|
|
2210
|
-
//
|
|
2211
|
-
//
|
|
2212
|
-
//
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
return fromEnv;
|
|
2221
|
-
const master = getMasterEnv(cfg);
|
|
2222
|
-
return master?.name ?? cfg.environments[0]?.name ?? "demo";
|
|
2223
|
-
}
|
|
2224
|
-
const WK_ANY = "WELL_KNOWN_TYPE_ANY";
|
|
2225
|
-
function actionDtoToAgentDefinition(a) {
|
|
2226
|
-
// Handle inputs - can be array or object with inputs property
|
|
2227
|
-
const inputsArray = Array.isArray(a.inputs) ? a.inputs : [];
|
|
2228
|
-
const outputsArray = Array.isArray(a.outputs) ? a.outputs : [];
|
|
2229
|
-
return {
|
|
2230
|
-
actionName: a.name ?? a.id,
|
|
2231
|
-
displayName: a.name ?? a.id,
|
|
2232
|
-
category: (a.category ?? "other"),
|
|
2233
|
-
description: a.description ?? "",
|
|
2234
|
-
inputs: inputsArray.map((p) => ({
|
|
2235
|
-
name: p.name ?? "input",
|
|
2236
|
-
type: WK_ANY,
|
|
2237
|
-
required: Boolean(p.required),
|
|
2238
|
-
description: p.description ?? "",
|
|
2239
|
-
})),
|
|
2240
|
-
outputs: outputsArray.map((p) => ({
|
|
2241
|
-
name: p.name ?? "output",
|
|
2242
|
-
type: WK_ANY,
|
|
2243
|
-
description: p.description ?? "",
|
|
2244
|
-
})),
|
|
2245
|
-
whenToUse: "",
|
|
2246
|
-
};
|
|
2247
|
-
}
|
|
2248
|
-
const clientCache = new Map();
|
|
2249
|
-
const agentCatalogCache = new Map();
|
|
2250
|
-
function getClientForEnvName(envName) {
|
|
2251
|
-
const cfg = loadAnyConfig();
|
|
2252
|
-
if (!cfg)
|
|
2253
|
-
return null;
|
|
2254
|
-
const effectiveEnv = envName ?? getDefaultEnvNameFromConfig(cfg);
|
|
2255
|
-
const cached = clientCache.get(effectiveEnv);
|
|
2256
|
-
if (cached)
|
|
2257
|
-
return cached;
|
|
2258
|
-
const envCfg = getEnvByName(cfg, effectiveEnv) ?? getEnvByName(cfg, getDefaultEnvNameFromConfig(cfg));
|
|
2259
|
-
if (!envCfg)
|
|
2260
|
-
return null;
|
|
2261
|
-
const env = {
|
|
2262
|
-
name: envCfg.name,
|
|
2263
|
-
baseUrl: envCfg.baseUrl,
|
|
2264
|
-
bearerToken: resolveBearerToken(envCfg.bearerTokenEnv),
|
|
2265
|
-
};
|
|
2266
|
-
const client = new EmaClient(env);
|
|
2267
|
-
clientCache.set(effectiveEnv, client);
|
|
2268
|
-
return client;
|
|
2269
|
-
}
|
|
2270
|
-
async function getDynamicAgentCatalog(opts) {
|
|
2271
|
-
const cacheKey = opts.env ?? "";
|
|
2272
|
-
const now = Date.now();
|
|
2273
|
-
const cached = agentCatalogCache.get(cacheKey);
|
|
2274
|
-
if (cached && now - cached.ts < 60_000)
|
|
2275
|
-
return cached.agents;
|
|
2276
|
-
const client = getClientForEnvName(opts.env);
|
|
2277
|
-
if (!client)
|
|
2278
|
-
return AGENT_CATALOG;
|
|
2279
|
-
try {
|
|
2280
|
-
const actions = await client.listActions();
|
|
2281
|
-
const agents = actions
|
|
2282
|
-
.filter((a) => typeof a.name === "string" && a.name.trim().length > 0)
|
|
2283
|
-
.map(actionDtoToAgentDefinition);
|
|
2284
|
-
agentCatalogCache.set(cacheKey, { ts: now, agents });
|
|
2285
|
-
return agents.length > 0 ? agents : AGENT_CATALOG;
|
|
2286
|
-
}
|
|
2287
|
-
catch {
|
|
2288
|
-
return AGENT_CATALOG;
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
121
|
+
// Templates - REMOVED (file-backed templates removed in favor of API + generated fallbacks)
|
|
122
|
+
// Use: client.getPersonaTemplates() for live API templates
|
|
123
|
+
// Use: getTemplateFallback("voice") from sdk/generated/template-fallbacks.ts for offline fallbacks
|
|
124
|
+
// See: ema://catalog/persona-templates for dynamic API-backed template listing
|
|
125
|
+
// DEPRECATED: Use ema://docs/usage-guide (dynamic) instead
|
|
126
|
+
// This static resource is kept for backwards compatibility but now generates from guidance module
|
|
127
|
+
};
|
|
128
|
+
// Legacy alias - redirect to dynamic resource
|
|
129
|
+
const LEGACY_STATIC_REDIRECTS = {
|
|
130
|
+
"ema://rules/mcp-usage": "ema://docs/usage-guide",
|
|
131
|
+
};
|
|
2291
132
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2292
|
-
//
|
|
133
|
+
// Workspace Root
|
|
2293
134
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2294
|
-
const templateCatalogCache = new Map();
|
|
2295
|
-
/**
|
|
2296
|
-
* Fetch persona templates from API with caching.
|
|
2297
|
-
* Falls back to empty array if API unavailable (file-backed templates still available via RESOURCE_MAP).
|
|
2298
|
-
*/
|
|
2299
|
-
async function getDynamicPersonaTemplates(opts) {
|
|
2300
|
-
const cacheKey = opts.env ?? "";
|
|
2301
|
-
const now = Date.now();
|
|
2302
|
-
const cached = templateCatalogCache.get(cacheKey);
|
|
2303
|
-
if (cached && now - cached.ts < 60_000)
|
|
2304
|
-
return cached.templates;
|
|
2305
|
-
const client = getClientForEnvName(opts.env);
|
|
2306
|
-
if (!client)
|
|
2307
|
-
return [];
|
|
2308
|
-
try {
|
|
2309
|
-
const templates = await client.getPersonaTemplates();
|
|
2310
|
-
templateCatalogCache.set(cacheKey, { ts: now, templates });
|
|
2311
|
-
return templates;
|
|
2312
|
-
}
|
|
2313
|
-
catch {
|
|
2314
|
-
return [];
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
const actionSchemaCache = new Map();
|
|
2318
|
-
/**
|
|
2319
|
-
* Get action schema with layered loading:
|
|
2320
|
-
* 1. Try bundled schema (resources/action-schema.json)
|
|
2321
|
-
* 2. Augment with live API data if available
|
|
2322
|
-
*/
|
|
2323
|
-
async function getDynamicActionSchema(opts) {
|
|
2324
|
-
const cacheKey = opts.env ?? "";
|
|
2325
|
-
const now = Date.now();
|
|
2326
|
-
const cached = actionSchemaCache.get(cacheKey);
|
|
2327
|
-
if (cached && now - cached.ts < 60_000)
|
|
2328
|
-
return cached.data;
|
|
2329
|
-
const registry = new APISchemaRegistry();
|
|
2330
|
-
// Try to load from bundled schema first (code source of truth)
|
|
2331
|
-
const bundlePath = path.resolve(__dirname, "../../resources/action-schema.json");
|
|
2332
|
-
try {
|
|
2333
|
-
registry.loadFromBundle(bundlePath);
|
|
2334
|
-
}
|
|
2335
|
-
catch {
|
|
2336
|
-
// Bundle not available - try API
|
|
2337
|
-
const client = getClientForEnvName(opts.env);
|
|
2338
|
-
if (client) {
|
|
2339
|
-
try {
|
|
2340
|
-
await registry.load(client);
|
|
2341
|
-
}
|
|
2342
|
-
catch {
|
|
2343
|
-
// Neither bundle nor API available
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
const response = {
|
|
2348
|
-
version: registry.metadata.version ?? "unknown",
|
|
2349
|
-
source: registry.metadata.source ?? "bundle",
|
|
2350
|
-
actions: registry.getAllActions(),
|
|
2351
|
-
};
|
|
2352
|
-
actionSchemaCache.set(cacheKey, { ts: now, data: response });
|
|
2353
|
-
return response;
|
|
2354
|
-
}
|
|
2355
|
-
/**
|
|
2356
|
-
* Get a specific action by name from the schema.
|
|
2357
|
-
*/
|
|
2358
|
-
async function getActionByName(name, env) {
|
|
2359
|
-
const schema = await getDynamicActionSchema({ env });
|
|
2360
|
-
// Find latest version of action
|
|
2361
|
-
const matching = schema.actions.filter(a => a.name === name);
|
|
2362
|
-
if (matching.length === 0)
|
|
2363
|
-
return null;
|
|
2364
|
-
// Return latest version (sort by version desc)
|
|
2365
|
-
return matching.sort((a, b) => (b.version || "v0").localeCompare(a.version || "v0"))[0];
|
|
2366
|
-
}
|
|
2367
|
-
/**
|
|
2368
|
-
* Convert PersonaTemplateDTO to a simplified format for MCP resources.
|
|
2369
|
-
*/
|
|
2370
|
-
function templateDtoToResource(t) {
|
|
2371
|
-
return {
|
|
2372
|
-
id: t.id,
|
|
2373
|
-
name: t.name,
|
|
2374
|
-
description: t.description,
|
|
2375
|
-
category: t.category,
|
|
2376
|
-
trigger_type: t.trigger_type,
|
|
2377
|
-
has_project_template: t.has_project_template,
|
|
2378
|
-
// Documentation fields for diagnosis/recommendations
|
|
2379
|
-
about_template: t.about_template, // Rich description of capabilities
|
|
2380
|
-
how_to_use: t.how_to_use, // Step-by-step usage instructions
|
|
2381
|
-
value_prop: t.value_prop, // Value proposition sections
|
|
2382
|
-
additional_content: t.additional_content, // Extra documentation
|
|
2383
|
-
// Include proto_config for full template details
|
|
2384
|
-
proto_config: t.proto_config,
|
|
2385
|
-
// Include workflow_definition if available
|
|
2386
|
-
workflow_definition: t.workflow_definition,
|
|
2387
|
-
};
|
|
2388
|
-
}
|
|
2389
135
|
// Get workspace root - use centralized path resolution
|
|
2390
136
|
// EMA_WORKSPACE_ROOT env var can override for testing
|
|
2391
137
|
import { getToolkitRoot } from "../sdk/paths.js";
|
|
@@ -2395,6 +141,9 @@ function getWorkspaceRoot() {
|
|
|
2395
141
|
}
|
|
2396
142
|
return getToolkitRoot();
|
|
2397
143
|
}
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
// ResourceRegistry Class
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2398
147
|
export class ResourceRegistry {
|
|
2399
148
|
workspaceRoot;
|
|
2400
149
|
constructor(workspaceRoot) {
|
|
@@ -2636,409 +385,8 @@ To read a resource, use the \`resources/read\` endpoint:
|
|
|
2636
385
|
}
|
|
2637
386
|
}
|
|
2638
387
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2639
|
-
//
|
|
388
|
+
// Helpers
|
|
2640
389
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2641
|
-
/**
|
|
2642
|
-
* Generate validation rules in LLM-friendly Markdown format.
|
|
2643
|
-
* Includes rules from Go validator: required outputs, multiple writers, response/abstain, category hierarchy.
|
|
2644
|
-
*/
|
|
2645
|
-
function generateValidationRulesForLLM() {
|
|
2646
|
-
return `# Workflow Validation Rules
|
|
2647
|
-
|
|
2648
|
-
> **Use these rules DURING workflow generation to avoid errors proactively.**
|
|
2649
|
-
|
|
2650
|
-
These rules are extracted from the Go validator's static validation logic. Follow them when generating or modifying workflows.
|
|
2651
|
-
|
|
2652
|
-
## Rule 1: Required Output on All Paths
|
|
2653
|
-
|
|
2654
|
-
**Rule ID**: \`required_output_all_paths\`
|
|
2655
|
-
**Severity**: Critical
|
|
2656
|
-
**Applies To**: Named results, execution paths
|
|
2657
|
-
|
|
2658
|
-
### Logic
|
|
2659
|
-
|
|
2660
|
-
**Algorithm**: Enumerate all execution paths. For each path, check if all named results are produced.
|
|
2661
|
-
|
|
2662
|
-
**Steps**:
|
|
2663
|
-
1. Enumerate all paths from trigger to completion
|
|
2664
|
-
2. For each path, collect all outputs produced
|
|
2665
|
-
3. For each named result, check if it's in path outputs
|
|
2666
|
-
4. If missing on any path → error
|
|
2667
|
-
|
|
2668
|
-
**Complexity**: O(P * R) where P = paths, R = named results
|
|
2669
|
-
|
|
2670
|
-
### Rules
|
|
2671
|
-
|
|
2672
|
-
**Constraint**: Every execution path must produce all named results.
|
|
2673
|
-
|
|
2674
|
-
**Why**: Named results are contract guarantees. If a path doesn't produce a required output, the contract is violated.
|
|
2675
|
-
|
|
2676
|
-
### Examples
|
|
2677
|
-
|
|
2678
|
-
**Bad Pattern**:
|
|
2679
|
-
\`\`\`json
|
|
2680
|
-
{
|
|
2681
|
-
"path": ["trigger", "categorizer", "billing_branch", "search"],
|
|
2682
|
-
"named_result": "response_with_sources",
|
|
2683
|
-
"produces": [] // Missing response
|
|
2684
|
-
}
|
|
2685
|
-
\`\`\`
|
|
2686
|
-
|
|
2687
|
-
**Why Bad**: Path ends without producing required output.
|
|
2688
|
-
|
|
2689
|
-
**Good Pattern**:
|
|
2690
|
-
\`\`\`json
|
|
2691
|
-
{
|
|
2692
|
-
"path": ["trigger", "categorizer", "billing_branch", "search", "respond_for_external_actions"],
|
|
2693
|
-
"named_result": "response",
|
|
2694
|
-
"produces": ["response"] // ✅ Produced
|
|
2695
|
-
}
|
|
2696
|
-
\`\`\`
|
|
2697
|
-
|
|
2698
|
-
**Why Good**: All paths produce required output.
|
|
2699
|
-
|
|
2700
|
-
### Remediation
|
|
2701
|
-
|
|
2702
|
-
**How to Fix**:
|
|
2703
|
-
1. Identify the path missing the output
|
|
2704
|
-
2. Add a node that produces the required output
|
|
2705
|
-
3. Connect the node's output to WORKFLOW_OUTPUT
|
|
2706
|
-
|
|
2707
|
-
**Common Fixes**:
|
|
2708
|
-
- Add \`respond_for_external_actions\` node after search
|
|
2709
|
-
- Add \`call_llm\` node for text generation
|
|
2710
|
-
- Add \`fixed_response\` for static responses
|
|
2711
|
-
|
|
2712
|
-
**Prevention** (During Generation):
|
|
2713
|
-
- Always ensure every categorizer branch has a response node
|
|
2714
|
-
- Check that all paths end with nodes connected to WORKFLOW_OUTPUT
|
|
2715
|
-
- Verify resultMappings include all required outputs
|
|
2716
|
-
|
|
2717
|
-
---
|
|
2718
|
-
|
|
2719
|
-
## Rule 2: Single Writer Per Output
|
|
2720
|
-
|
|
2721
|
-
**Rule ID**: \`single_writer_per_output\`
|
|
2722
|
-
**Severity**: Critical
|
|
2723
|
-
**Applies To**: Named results, execution paths
|
|
2724
|
-
|
|
2725
|
-
### Logic
|
|
2726
|
-
|
|
2727
|
-
**Algorithm**: For each named result, count how many nodes produce it on each path.
|
|
2728
|
-
|
|
2729
|
-
**Steps**:
|
|
2730
|
-
1. For each named result
|
|
2731
|
-
2. For each execution path
|
|
2732
|
-
3. Count nodes that produce this result
|
|
2733
|
-
4. If count > 1 on any path → error
|
|
2734
|
-
|
|
2735
|
-
### Rules
|
|
2736
|
-
|
|
2737
|
-
**Constraint**: A single named result can only have one producer per execution path.
|
|
2738
|
-
|
|
2739
|
-
**Why**: Multiple producers on the same path cause ambiguity about which value to use.
|
|
2740
|
-
|
|
2741
|
-
### Examples
|
|
2742
|
-
|
|
2743
|
-
**Bad Pattern**:
|
|
2744
|
-
\`\`\`json
|
|
2745
|
-
{
|
|
2746
|
-
"path": ["trigger", "categorizer", "billing"],
|
|
2747
|
-
"named_result": "response_with_sources",
|
|
2748
|
-
"producers": ["respond_billing", "respond_general"] // ❌ Two producers
|
|
2749
|
-
}
|
|
2750
|
-
\`\`\`
|
|
2751
|
-
|
|
2752
|
-
**Why Bad**: Two nodes produce the same output on the same path.
|
|
2753
|
-
|
|
2754
|
-
**Good Pattern**:
|
|
2755
|
-
\`\`\`json
|
|
2756
|
-
{
|
|
2757
|
-
"path": ["trigger", "categorizer", "billing"],
|
|
2758
|
-
"named_result": "response_with_sources",
|
|
2759
|
-
"producers": ["respond_billing"] // ✅ Single producer
|
|
2760
|
-
}
|
|
2761
|
-
\`\`\`
|
|
2762
|
-
|
|
2763
|
-
**Why Good**: Only one node produces the output.
|
|
2764
|
-
|
|
2765
|
-
### Remediation
|
|
2766
|
-
|
|
2767
|
-
**How to Fix**:
|
|
2768
|
-
1. Identify which nodes produce the same output
|
|
2769
|
-
2. Remove one producer or use different named results
|
|
2770
|
-
3. Ensure only one node produces each named result per path
|
|
2771
|
-
|
|
2772
|
-
**Prevention** (During Generation):
|
|
2773
|
-
- Use mutually exclusive runIf conditions to gate responders
|
|
2774
|
-
- Ensure only one response node executes per category branch
|
|
2775
|
-
|
|
2776
|
-
---
|
|
2777
|
-
|
|
2778
|
-
## Rule 3: Response or Abstain Required
|
|
2779
|
-
|
|
2780
|
-
**Rule ID**: \`response_or_abstain_required\`
|
|
2781
|
-
**Severity**: Critical
|
|
2782
|
-
**Applies To**: Execution paths
|
|
2783
|
-
|
|
2784
|
-
### Logic
|
|
2785
|
-
|
|
2786
|
-
**Algorithm**: For each execution path, check if it produces a response or has an abstain reason.
|
|
2787
|
-
|
|
2788
|
-
**Steps**:
|
|
2789
|
-
1. For each completed path
|
|
2790
|
-
2. Check if path produces any response output
|
|
2791
|
-
3. Check if path has abstain reason
|
|
2792
|
-
4. If neither → error
|
|
2793
|
-
|
|
2794
|
-
### Rules
|
|
2795
|
-
|
|
2796
|
-
**Constraint**: Every execution path must either produce a user-visible response or explicitly abstain with a reason.
|
|
2797
|
-
|
|
2798
|
-
**Why**: Users expect responses. Paths that don't respond and don't explain why create poor UX.
|
|
2799
|
-
|
|
2800
|
-
### Examples
|
|
2801
|
-
|
|
2802
|
-
**Bad Pattern**:
|
|
2803
|
-
\`\`\`json
|
|
2804
|
-
{
|
|
2805
|
-
"path": ["trigger", "categorizer", "billing", "search"],
|
|
2806
|
-
"has_response": false,
|
|
2807
|
-
"has_abstain": false // ❌ Neither
|
|
2808
|
-
}
|
|
2809
|
-
\`\`\`
|
|
2810
|
-
|
|
2811
|
-
**Why Bad**: Path ends without response or abstain reason.
|
|
2812
|
-
|
|
2813
|
-
**Good Pattern**:
|
|
2814
|
-
\`\`\`json
|
|
2815
|
-
{
|
|
2816
|
-
"path": ["trigger", "categorizer", "billing", "search", "respond_for_external_actions"],
|
|
2817
|
-
"has_response": true // ✅ Has response
|
|
2818
|
-
}
|
|
2819
|
-
\`\`\`
|
|
2820
|
-
|
|
2821
|
-
**Why Good**: Path produces a response.
|
|
2822
|
-
|
|
2823
|
-
### Remediation
|
|
2824
|
-
|
|
2825
|
-
**How to Fix**:
|
|
2826
|
-
1. Add a response node to the path, OR
|
|
2827
|
-
2. Add an abstain reason to the workflow configuration
|
|
2828
|
-
|
|
2829
|
-
**Prevention** (During Generation):
|
|
2830
|
-
- Always add response nodes to every categorizer branch
|
|
2831
|
-
- If a path shouldn't respond, document why with abstain reason
|
|
2832
|
-
|
|
2833
|
-
---
|
|
2834
|
-
|
|
2835
|
-
## Rule 4: Category Hierarchy Valid
|
|
2836
|
-
|
|
2837
|
-
**Rule ID**: \`category_hierarchy_valid\`
|
|
2838
|
-
**Severity**: Critical
|
|
2839
|
-
**Applies To**: Categorizers
|
|
2840
|
-
|
|
2841
|
-
### Logic
|
|
2842
|
-
|
|
2843
|
-
**Algorithm**: Validate categorizer structure and category definitions.
|
|
2844
|
-
|
|
2845
|
-
**Steps**:
|
|
2846
|
-
1. Check categorizer has Fallback category
|
|
2847
|
-
2. Check category hierarchy rules (TBD - need Go validator analysis)
|
|
2848
|
-
3. Validate category → handler mapping
|
|
2849
|
-
|
|
2850
|
-
### Rules
|
|
2851
|
-
|
|
2852
|
-
**Constraint**: Categorizers must have proper structure and hierarchy.
|
|
2853
|
-
|
|
2854
|
-
**Why**: Invalid category structure causes routing failures.
|
|
2855
|
-
|
|
2856
|
-
### Examples
|
|
2857
|
-
|
|
2858
|
-
**Bad Pattern**:
|
|
2859
|
-
\`\`\`json
|
|
2860
|
-
{
|
|
2861
|
-
"categorizer": {
|
|
2862
|
-
"categories": ["Billing", "Technical"] // ❌ No Fallback
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
2865
|
-
\`\`\`
|
|
2866
|
-
|
|
2867
|
-
**Why Bad**: Missing Fallback category means unrecognized intents have no handler.
|
|
2868
|
-
|
|
2869
|
-
**Good Pattern**:
|
|
2870
|
-
\`\`\`json
|
|
2871
|
-
{
|
|
2872
|
-
"categorizer": {
|
|
2873
|
-
"categories": ["Billing", "Technical", "Fallback"] // ✅ Has Fallback
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
\`\`\`
|
|
2877
|
-
|
|
2878
|
-
**Why Good**: All intents have a handler.
|
|
2879
|
-
|
|
2880
|
-
### Remediation
|
|
2881
|
-
|
|
2882
|
-
**How to Fix**:
|
|
2883
|
-
1. Add Fallback category to categorizer
|
|
2884
|
-
2. Ensure each category has a handler node
|
|
2885
|
-
3. Validate category hierarchy (TBD)
|
|
2886
|
-
|
|
2887
|
-
**Prevention** (During Generation):
|
|
2888
|
-
- Always include Fallback category
|
|
2889
|
-
- Ensure every category has at least one handler node
|
|
2890
|
-
|
|
2891
|
-
---
|
|
2892
|
-
|
|
2893
|
-
## Self-Check Checklist
|
|
2894
|
-
|
|
2895
|
-
After generating or modifying a workflow, verify:
|
|
2896
|
-
|
|
2897
|
-
- [ ] All paths produce all named results
|
|
2898
|
-
- [ ] No multiple writers on same path
|
|
2899
|
-
- [ ] Every path has response or abstain
|
|
2900
|
-
- [ ] Category hierarchy is valid
|
|
2901
|
-
- [ ] Categorizer has Fallback category
|
|
2902
|
-
- [ ] All categories have handler nodes
|
|
2903
|
-
|
|
2904
|
-
---
|
|
2905
|
-
|
|
2906
|
-
## Reference
|
|
2907
|
-
|
|
2908
|
-
- **Source**: workflow-engine/pkg/engine/validator.go
|
|
2909
|
-
- **Error Types**: RESULT_ERROR_TYPE_REQUIRED_OUTPUT_NOT_PRODUCED, RESULT_ERROR_TYPE_MULTIPLE_WRITERS, RESULT_ERROR_TYPE_MISSING_RESPONSE_OR_ABSTAIN_REASON, RESULT_ERROR_TYPE_CATEGORY_HIERARCHY_VIOLATION
|
|
2910
|
-
- **Validation Tool**: Use \`workflow(mode="validate")\` to validate workflows
|
|
2911
|
-
`;
|
|
2912
|
-
}
|
|
2913
|
-
/**
|
|
2914
|
-
* Generate best practices checklist from SDK rules.
|
|
2915
|
-
* SINGLE SOURCE OF TRUTH - all best practices come from:
|
|
2916
|
-
* - ANTI_PATTERNS (validation-rules.ts)
|
|
2917
|
-
* - STRUCTURAL_INVARIANTS (structural-rules.ts)
|
|
2918
|
-
* - INPUT_SOURCE_RULES (validation-rules.ts)
|
|
2919
|
-
* - OPTIMIZATION_RULES (validation-rules.ts)
|
|
2920
|
-
*/
|
|
2921
|
-
function generateBestPracticesChecklist() {
|
|
2922
|
-
// Group anti-patterns by severity
|
|
2923
|
-
const criticalAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "critical");
|
|
2924
|
-
const warningAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "warning");
|
|
2925
|
-
const infoAntiPatterns = ANTI_PATTERNS.filter(ap => ap.severity === "info");
|
|
2926
|
-
// Group structural invariants by severity
|
|
2927
|
-
const criticalInvariants = STRUCTURAL_INVARIANTS.filter(inv => inv.severity === "critical");
|
|
2928
|
-
const warningInvariants = STRUCTURAL_INVARIANTS.filter(inv => inv.severity === "warning");
|
|
2929
|
-
return `# Workflow Best Practices Checklist
|
|
2930
|
-
|
|
2931
|
-
> **Auto-generated from SDK rules** - This is the single source of truth.
|
|
2932
|
-
> Check this BEFORE deploying any workflow.
|
|
2933
|
-
|
|
2934
|
-
## 🛑 CRITICAL (Must Fix Before Deploy)
|
|
2935
|
-
|
|
2936
|
-
### Anti-Patterns to Avoid
|
|
2937
|
-
|
|
2938
|
-
${criticalAntiPatterns.map(ap => `
|
|
2939
|
-
#### ${ap.name}
|
|
2940
|
-
|
|
2941
|
-
**Pattern**: ${ap.pattern}
|
|
2942
|
-
|
|
2943
|
-
**Problem**: ${ap.problem}
|
|
2944
|
-
|
|
2945
|
-
**Solution**: ${ap.solution}
|
|
2946
|
-
|
|
2947
|
-
**Detection**: \`${ap.detection.condition}\`
|
|
2948
|
-
`).join("\n---\n")}
|
|
2949
|
-
|
|
2950
|
-
### Structural Requirements
|
|
2951
|
-
|
|
2952
|
-
${criticalInvariants.map(inv => `
|
|
2953
|
-
- **${inv.name}**: ${inv.rule}
|
|
2954
|
-
- Violation: ${inv.violation}
|
|
2955
|
-
- Fix: ${inv.fix}
|
|
2956
|
-
`).join("")}
|
|
2957
|
-
|
|
2958
|
-
---
|
|
2959
|
-
|
|
2960
|
-
## ⚠️ WARNINGS (Should Fix)
|
|
2961
|
-
|
|
2962
|
-
### Anti-Patterns
|
|
2963
|
-
|
|
2964
|
-
${warningAntiPatterns.map(ap => `
|
|
2965
|
-
- **${ap.name}**: ${ap.problem}
|
|
2966
|
-
- Solution: ${ap.solution}
|
|
2967
|
-
`).join("")}
|
|
2968
|
-
|
|
2969
|
-
### Structural Warnings
|
|
2970
|
-
|
|
2971
|
-
${warningInvariants.map(inv => `
|
|
2972
|
-
- **${inv.name}**: ${inv.rule}
|
|
2973
|
-
- Fix: ${inv.fix}
|
|
2974
|
-
`).join("")}
|
|
2975
|
-
|
|
2976
|
-
---
|
|
2977
|
-
|
|
2978
|
-
## 📋 SUGGESTIONS (Good to Have)
|
|
2979
|
-
|
|
2980
|
-
${infoAntiPatterns.map(ap => `
|
|
2981
|
-
- **${ap.name}**: ${ap.problem}
|
|
2982
|
-
- Recommendation: ${ap.solution}
|
|
2983
|
-
`).join("")}
|
|
2984
|
-
|
|
2985
|
-
---
|
|
2986
|
-
|
|
2987
|
-
## Input Source Rules
|
|
2988
|
-
|
|
2989
|
-
| Action | Recommended Input | Avoid | Why |
|
|
2990
|
-
|--------|-------------------|-------|-----|
|
|
2991
|
-
${INPUT_SOURCE_RULES.filter(r => r.severity === "critical").map(r => `| \`${r.actionPattern}\` | \`${r.recommended}\` | ${r.avoid.join(", ") || "-"} | ${r.reason.slice(0, 60)}... |`).join("\n")}
|
|
2992
|
-
|
|
2993
|
-
---
|
|
2994
|
-
|
|
2995
|
-
## Performance Optimizations
|
|
2996
|
-
|
|
2997
|
-
${OPTIMIZATION_RULES.map(opt => `
|
|
2998
|
-
### ${opt.name} (${opt.priority})
|
|
2999
|
-
|
|
3000
|
-
- **Current State**: ${opt.currentState}
|
|
3001
|
-
- **Recommendation**: ${opt.recommendation}
|
|
3002
|
-
- **Benefit**: ${opt.benefit}
|
|
3003
|
-
`).join("")}
|
|
3004
|
-
|
|
3005
|
-
---
|
|
3006
|
-
|
|
3007
|
-
## Quick Checklist
|
|
3008
|
-
|
|
3009
|
-
Before deploying, verify:
|
|
3010
|
-
|
|
3011
|
-
### Critical
|
|
3012
|
-
- [ ] Response nodes receive \`chat_conversation\` (prevents repeated questions)
|
|
3013
|
-
- [ ] Email recipients from \`entity_extraction\` (not text outputs)
|
|
3014
|
-
- [ ] Categorizers have Fallback category
|
|
3015
|
-
- [ ] HITL has both success AND failure paths
|
|
3016
|
-
- [ ] All paths reach WORKFLOW_OUTPUT
|
|
3017
|
-
- [ ] No circular dependencies
|
|
3018
|
-
|
|
3019
|
-
### Recommended
|
|
3020
|
-
- [ ] \`max_tokens\` set on call_llm nodes
|
|
3021
|
-
- [ ] No orphan nodes (unused/disconnected)
|
|
3022
|
-
- [ ] No redundant search nodes
|
|
3023
|
-
- [ ] Search nodes have data sources uploaded
|
|
3024
|
-
|
|
3025
|
-
---
|
|
3026
|
-
|
|
3027
|
-
## Related Resources
|
|
3028
|
-
|
|
3029
|
-
- \`ema://rules/anti-patterns\` - Full anti-pattern definitions (JSON)
|
|
3030
|
-
- \`ema://rules/input-sources\` - Input source rules (JSON)
|
|
3031
|
-
- \`ema://rules/extraction-column-format\` - extraction_columns API shape (entity_extraction nodes)
|
|
3032
|
-
- \`ema://rules/json-output-patterns\` - custom_agent + json_mapper pattern
|
|
3033
|
-
- \`ema://rules/chat-response-wiring\` - Chat response node wiring (avoid duplicate responses)
|
|
3034
|
-
- \`ema://rules/email-input-wiring\` - Email input wiring (json_mapper/fixed_response patterns)
|
|
3035
|
-
- \`ema://docs/workflow-node-reference\` - Per-node I/O types, categorizer patterns, LLM config
|
|
3036
|
-
- \`ema://rules/structural-invariants\` - Structural rules (JSON)
|
|
3037
|
-
- \`ema://rules/optimizations\` - Optimization rules (JSON)
|
|
3038
|
-
- \`ema://validation/rules\` - Go validator rules reference
|
|
3039
|
-
`;
|
|
3040
|
-
}
|
|
3041
|
-
// Helper to check if result is an error
|
|
3042
390
|
export function isResourceError(result) {
|
|
3043
391
|
return "code" in result;
|
|
3044
392
|
}
|