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