@ema.co/mcp-toolkit 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +321 -0
  3. package/config.example.yaml +32 -0
  4. package/dist/cli/index.js +333 -0
  5. package/dist/config.js +136 -0
  6. package/dist/emaClient.js +398 -0
  7. package/dist/index.js +109 -0
  8. package/dist/mcp/handlers-consolidated.js +851 -0
  9. package/dist/mcp/index.js +15 -0
  10. package/dist/mcp/prompts.js +1753 -0
  11. package/dist/mcp/resources.js +624 -0
  12. package/dist/mcp/server.js +4723 -0
  13. package/dist/mcp/tools-consolidated.js +590 -0
  14. package/dist/mcp/tools-legacy.js +736 -0
  15. package/dist/models.js +8 -0
  16. package/dist/scheduler.js +21 -0
  17. package/dist/sdk/client.js +788 -0
  18. package/dist/sdk/config.js +136 -0
  19. package/dist/sdk/contracts.js +429 -0
  20. package/dist/sdk/generation-schema.js +189 -0
  21. package/dist/sdk/index.js +39 -0
  22. package/dist/sdk/knowledge.js +2780 -0
  23. package/dist/sdk/models.js +8 -0
  24. package/dist/sdk/state.js +88 -0
  25. package/dist/sdk/sync-options.js +216 -0
  26. package/dist/sdk/sync.js +220 -0
  27. package/dist/sdk/validation-rules.js +355 -0
  28. package/dist/sdk/workflow-generator.js +291 -0
  29. package/dist/sdk/workflow-intent.js +1585 -0
  30. package/dist/state.js +88 -0
  31. package/dist/sync.js +416 -0
  32. package/dist/syncOptions.js +216 -0
  33. package/dist/ui.js +334 -0
  34. package/docs/advisor-comms-assistant-fixes.md +175 -0
  35. package/docs/api-contracts.md +216 -0
  36. package/docs/auto-builder-analysis.md +271 -0
  37. package/docs/data-architecture.md +166 -0
  38. package/docs/ema-auto-builder-guide.html +394 -0
  39. package/docs/ema-user-guide.md +1121 -0
  40. package/docs/mcp-tools-guide.md +149 -0
  41. package/docs/naming-conventions.md +218 -0
  42. package/docs/tool-consolidation-proposal.md +427 -0
  43. package/package.json +98 -0
  44. package/resources/templates/chat-ai/README.md +119 -0
  45. package/resources/templates/chat-ai/persona-config.json +111 -0
  46. package/resources/templates/dashboard-ai/README.md +156 -0
  47. package/resources/templates/dashboard-ai/persona-config.json +180 -0
  48. package/resources/templates/voice-ai/README.md +123 -0
  49. package/resources/templates/voice-ai/persona-config.json +74 -0
  50. package/resources/templates/voice-ai/workflow-prompt.md +120 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Validation Rules - Single Source of Truth
3
+ *
4
+ * This file defines all workflow validation rules in a structured format.
5
+ * These rules are used by:
6
+ * 1. knowledge.ts - Runtime validation in detectWorkflowIssues()
7
+ * 2. MCP tools - analyze_workflow, suggest_workflow_fixes
8
+ * 3. Documentation generation - Can be exported to update RULE.md
9
+ *
10
+ * To avoid duplication, all rule definitions should live HERE and be
11
+ * imported/referenced elsewhere.
12
+ */
13
+ // NOTE: More specific patterns MUST come before less specific ones!
14
+ // The matching uses `includes()`, so "conversation_to_search_query" would match "search" if "search" comes first.
15
+ export const INPUT_SOURCE_RULES = [
16
+ // Most specific patterns first
17
+ {
18
+ actionPattern: "conversation_to_search_query",
19
+ recommended: "chat_conversation",
20
+ avoid: ["user_query"],
21
+ reason: "conversation_to_search_query needs CHAT_CONVERSATION to summarize. user_query is already TEXT_WITH_SOURCES.",
22
+ severity: "critical",
23
+ fix: "Connect trigger.chat_conversation → conversation_to_search_query.conversation",
24
+ },
25
+ {
26
+ actionPattern: "chat_categorizer",
27
+ recommended: "chat_conversation",
28
+ avoid: ["user_query", "summarized_conversation"],
29
+ reason: "Categorizers need full conversation history for accurate intent classification. Using user_query loses multi-turn context and causes misclassification.",
30
+ severity: "critical",
31
+ fix: "Connect trigger.chat_conversation → chat_categorizer.conversation",
32
+ },
33
+ {
34
+ actionPattern: "text_categorizer",
35
+ recommended: "user_query",
36
+ avoid: ["chat_conversation"],
37
+ reason: "text_categorizer expects TEXT_WITH_SOURCES, not CHAT_CONVERSATION. Use chat_categorizer instead for conversations.",
38
+ severity: "critical",
39
+ fix: "Use chat_categorizer for conversation routing, or use user_query/summarized_conversation for text_categorizer",
40
+ },
41
+ {
42
+ actionPattern: "respond_with_sources",
43
+ recommended: "user_query",
44
+ avoid: ["chat_conversation"],
45
+ reason: "respond_with_sources.query expects TEXT_WITH_SOURCES. For conversation context, use named_inputs.",
46
+ severity: "critical",
47
+ fix: "Use trigger.user_query for query input",
48
+ },
49
+ {
50
+ actionPattern: "live_web_search",
51
+ recommended: "user_query",
52
+ avoid: ["chat_conversation"],
53
+ reason: "Web search expects TEXT_WITH_SOURCES query. Use conversation_to_search_query first if you need conversation context.",
54
+ severity: "critical",
55
+ fix: "Use trigger.user_query or summarized_conversation from conversation_to_search_query",
56
+ },
57
+ {
58
+ actionPattern: "call_llm",
59
+ recommended: "user_query",
60
+ avoid: [],
61
+ reason: "call_llm.query expects TEXT_WITH_SOURCES. For conversation context, pass chat_conversation via named_inputs instead.",
62
+ severity: "warning",
63
+ fix: "Use user_query for query, pass chat_conversation via named_inputs if needed",
64
+ },
65
+ // Email-specific rules
66
+ {
67
+ actionPattern: "send_email",
68
+ recommended: "entity_extraction.email_address",
69
+ avoid: ["summarized_conversation", "response_with_sources", "search_results", "generated_content"],
70
+ reason: "email_to field requires an EMAIL ADDRESS, not summarized text or generated content. " +
71
+ "Using text outputs will cause invalid emails or send to wrong recipients.",
72
+ severity: "critical",
73
+ fix: "Use entity_extraction to extract email_address from conversation, then connect to send_email.email_to. " +
74
+ "Add HITL confirmation before sending to verify recipient.",
75
+ },
76
+ // Template and fixed response patterns
77
+ {
78
+ actionPattern: "fixed_response",
79
+ recommended: "named_inputs with {{variable}} syntax",
80
+ avoid: ["hardcoded long text", "embedded templates without variables"],
81
+ reason: "fixed_response supports {{variable_name}} syntax for dynamic content. " +
82
+ "Variables come from named_inputs (preferred) or extracted_variables (JSON). " +
83
+ "Use for short, structured messages - NOT for long templates.",
84
+ severity: "info",
85
+ fix: "For short confirmations: Use fixed_response with {{Customer_Name}}, {{Order_ID}} variables. " +
86
+ "For email/document templates: Use data source templates with fill_document_template or generate_document.",
87
+ },
88
+ {
89
+ actionPattern: "generate_document",
90
+ recommended: "LLM templating (call_llm with structured prompt) → generate_document",
91
+ avoid: ["hardcoded template content", "inline template text", "skipping LLM formatting step"],
92
+ reason: "For dynamic, context-dependent content: Use LLM templating with structured prompts. " +
93
+ "Only use data source templates for strict regulatory/pixel-perfect formats.",
94
+ severity: "info",
95
+ fix: "RECOMMENDED: call_llm (with structured ## section headers - LLM determines appropriate sections) → generate_document. " +
96
+ "Pass data via named_inputs. Set temperature 0.3-0.5 for consistent formatting. " +
97
+ "ONLY use template engine for strict compliance documents.",
98
+ },
99
+ {
100
+ actionPattern: "send_email",
101
+ recommended: "entity_extraction.email_address for recipients, template from data source for body",
102
+ avoid: ["hardcoded email text", "inline HTML templates", "text outputs as recipients"],
103
+ reason: "Email templates should be stored in data sources for maintainability. " +
104
+ "Recipients must come from entity_extraction, NOT from text generation outputs.",
105
+ severity: "critical",
106
+ fix: "For recipient: entity_extraction.email_address. " +
107
+ "For body: Use fixed_response with {{variables}} for simple emails, " +
108
+ "or fill_document_template for complex HTML templates from data sources.",
109
+ },
110
+ // Generic patterns last (least specific)
111
+ {
112
+ actionPattern: "search",
113
+ recommended: "user_query",
114
+ avoid: ["chat_conversation"],
115
+ reason: "Search expects TEXT_WITH_SOURCES query, not CHAT_CONVERSATION. Use conversation_to_search_query first if you need conversation context.",
116
+ severity: "critical",
117
+ fix: "Use trigger.user_query for simple searches, or add conversation_to_search_query to convert chat history",
118
+ },
119
+ ];
120
+ export const ANTI_PATTERNS = [
121
+ {
122
+ id: "text-to-email-recipient",
123
+ name: "Text Content as Email Recipient",
124
+ pattern: "Connecting summarized_conversation, response_with_sources, or other text outputs to send_email.email_to",
125
+ problem: "email_to expects an EMAIL ADDRESS string. Connecting text outputs (summaries, responses, search results) will cause email failures or send to garbage addresses.",
126
+ solution: "Use entity_extraction to extract the email address from conversation, then connect entity_extraction.email_address to send_email.email_to. Add HITL confirmation before sending.",
127
+ detection: {
128
+ issueType: "wrong_input_source",
129
+ condition: "send_email_agent.email_to connected to summarized_conversation, response_with_sources, search_results, or any TEXT_WITH_SOURCES output",
130
+ },
131
+ severity: "critical",
132
+ },
133
+ {
134
+ id: "email-without-validation",
135
+ name: "Email Without Input Validation",
136
+ pattern: "send_email_agent without entity_extraction and HITL confirmation",
137
+ problem: "Sending emails without extracting and validating recipient data risks sending to wrong people or with wrong content. Emails are high-impact actions with external side effects.",
138
+ solution: "Always: 1) Extract required fields (email_address, subject) via entity_extraction, 2) Validate completeness via categorizer, 3) Ask user if missing, 4) Confirm via HITL before sending.",
139
+ detection: {
140
+ issueType: "incomplete_hitl",
141
+ condition: "send_email_agent without preceding entity_extraction AND hitl nodes",
142
+ },
143
+ severity: "critical",
144
+ },
145
+ {
146
+ id: "redundant-search",
147
+ name: "Redundant Search Nodes",
148
+ pattern: "Multiple conditional search nodes with same query source",
149
+ problem: "Only one branch executes at a time, so multiple searches add complexity without benefit.",
150
+ solution: "Use a SINGLE search node, pass results to all response branches via named_inputs.",
151
+ detection: {
152
+ issueType: "redundant_search",
153
+ condition: "Multiple search nodes using the same query source (e.g., summarized_conversation)",
154
+ },
155
+ severity: "warning",
156
+ },
157
+ {
158
+ id: "sequential-llm",
159
+ name: "Sequential LLM Calls",
160
+ pattern: "Multiple sequential LLM calls for simple tasks",
161
+ problem: "Adds latency and reduces coherence. Each LLM call has overhead.",
162
+ solution: "Single call_llm with comprehensive instructions and all context via named_inputs.",
163
+ detection: {
164
+ issueType: "duplicate_llm_processing",
165
+ condition: "Multiple LLM nodes processing same search results sequentially",
166
+ },
167
+ severity: "info",
168
+ },
169
+ {
170
+ id: "llm-for-static",
171
+ name: "LLM for Static Content",
172
+ pattern: "Using call_llm for static responses",
173
+ problem: "Unnecessary cost and latency for content that never changes.",
174
+ solution: "Use fixed_response for templates, disclaimers, error messages.",
175
+ detection: {
176
+ issueType: "unnecessary_llm",
177
+ condition: "call_llm with hardcoded instructions and no dynamic inputs",
178
+ },
179
+ severity: "info",
180
+ },
181
+ {
182
+ id: "missing-fallback",
183
+ name: "Missing Fallback Category",
184
+ pattern: "Categorizer without Fallback category",
185
+ problem: "Unrecognized intents have nowhere to go, causing undefined behavior.",
186
+ solution: "ALWAYS include Fallback category with clarifying question and examples.",
187
+ detection: {
188
+ issueType: "missing_fallback",
189
+ condition: "Categorizer enumType.options does not include 'Fallback'",
190
+ },
191
+ severity: "critical",
192
+ },
193
+ {
194
+ id: "incomplete-hitl",
195
+ name: "Incomplete HITL Paths",
196
+ pattern: "HITL with only success path",
197
+ problem: "Rejected requests have no handling, leaving users without response.",
198
+ solution: "ALWAYS implement both success AND failure paths for general_hitl.",
199
+ detection: {
200
+ issueType: "incomplete_hitl",
201
+ condition: "HITL node missing 'hitl_status_HITL Success' or 'hitl_status_HITL Failure' edge (note: space, not underscore)",
202
+ },
203
+ severity: "critical",
204
+ },
205
+ {
206
+ id: "orphan-nodes",
207
+ name: "Orphan Nodes",
208
+ pattern: "Nodes not connected to workflow (for 'future use')",
209
+ problem: "Adds confusion, maintenance burden, and can cause validation errors.",
210
+ solution: "Remove unused nodes. Add them when actually needed.",
211
+ detection: {
212
+ issueType: "orphan",
213
+ condition: "Node not reachable from trigger via edges",
214
+ },
215
+ severity: "warning",
216
+ },
217
+ {
218
+ id: "unused-output",
219
+ name: "Unused Output",
220
+ pattern: "Node produces output that is not consumed by any downstream node",
221
+ problem: "Node is doing work that goes nowhere. Common with combine_search_results where combined_results isn't wired. Wastes compute and indicates incomplete wiring.",
222
+ solution: "Connect the output to a downstream node that needs it, or remove the node if not needed.",
223
+ detection: {
224
+ issueType: "unused_output",
225
+ condition: "Node output not consumed by any other node's inputs or WORKFLOW_OUTPUT",
226
+ },
227
+ severity: "warning",
228
+ },
229
+ {
230
+ id: "web-search-primary",
231
+ name: "Web Search as Primary",
232
+ pattern: "Using web search as primary/only data source",
233
+ problem: "Web search is slower, less reliable, and content is uncontrolled.",
234
+ solution: "Internal KB should be primary, web search should supplement.",
235
+ detection: {
236
+ issueType: "web_search_only",
237
+ condition: "live_web_search without accompanying search (KB) node",
238
+ },
239
+ severity: "warning",
240
+ },
241
+ {
242
+ id: "missing-category-edge",
243
+ name: "Missing Category Edges",
244
+ pattern: "Categorizer with categories but no outgoing edges",
245
+ problem: "Categories have nowhere to route to, causing dead workflow.",
246
+ solution: "Add edge for EACH category: category_<Name> → handler.trigger_when",
247
+ detection: {
248
+ issueType: "missing_category_edge",
249
+ condition: "Categorizer has enumType.options but no outgoing edges matching category_*",
250
+ },
251
+ severity: "critical",
252
+ },
253
+ {
254
+ id: "missing-workflow-output",
255
+ name: "Missing WORKFLOW_OUTPUT",
256
+ pattern: "Response nodes not connected to WORKFLOW_OUTPUT",
257
+ problem: "Responses never reach the user.",
258
+ solution: "Connect all terminal response nodes to WORKFLOW_OUTPUT.",
259
+ detection: {
260
+ issueType: "missing_workflow_output",
261
+ condition: "No WORKFLOW_OUTPUT node, or response nodes not connected to it",
262
+ },
263
+ severity: "critical",
264
+ },
265
+ ];
266
+ export const OPTIMIZATION_RULES = [
267
+ {
268
+ id: "consolidate-searches",
269
+ name: "Consolidate Redundant Searches",
270
+ currentState: "Multiple search nodes using same query source with different file filters",
271
+ recommendation: "Replace with single search node, remove file filters, let LLM filter results",
272
+ benefit: "Simpler workflow, consistent results, easier maintenance",
273
+ priority: "high",
274
+ },
275
+ {
276
+ id: "parallelize-independent",
277
+ name: "Parallelize Independent Operations",
278
+ currentState: "Sequential nodes that don't depend on each other's output",
279
+ recommendation: "Restructure to branch from same source so they run in parallel",
280
+ benefit: "Reduced latency, faster response times",
281
+ priority: "medium",
282
+ },
283
+ {
284
+ id: "use-purpose-built",
285
+ name: "Use Purpose-Built Response Nodes",
286
+ currentState: "Using call_llm for search-based responses",
287
+ recommendation: "Use respond_with_sources instead - it has built-in citation handling",
288
+ benefit: "Better citations, grounded responses, less configuration",
289
+ priority: "medium",
290
+ },
291
+ {
292
+ id: "reduce-context-window",
293
+ name: "Optimize Context Window",
294
+ currentState: "Default or excessive context_window settings",
295
+ recommendation: "Set context_window based on actual needs (typically 5-10 turns)",
296
+ benefit: "Reduced token usage, faster processing",
297
+ priority: "low",
298
+ },
299
+ ];
300
+ // ─────────────────────────────────────────────────────────────────────────────
301
+ // Helper Functions
302
+ // ─────────────────────────────────────────────────────────────────────────────
303
+ /**
304
+ * Find input source rule for a given action
305
+ */
306
+ export function findInputSourceRule(actionName) {
307
+ return INPUT_SOURCE_RULES.find(rule => actionName.toLowerCase().includes(rule.actionPattern.toLowerCase()));
308
+ }
309
+ /**
310
+ * Find anti-pattern by issue type
311
+ */
312
+ export function findAntiPatternByIssueType(issueType) {
313
+ return ANTI_PATTERNS.find(ap => ap.detection.issueType === issueType);
314
+ }
315
+ /**
316
+ * Get all rules as formatted markdown (for documentation generation)
317
+ */
318
+ export function generateMarkdownDocumentation() {
319
+ let md = "## Auto-Generated Validation Rules\n\n";
320
+ md += "> This section is generated from `src/sdk/validation-rules.ts`\n\n";
321
+ md += "### Input Source Rules\n\n";
322
+ md += "| Action | Recommended | Avoid | Severity |\n";
323
+ md += "|--------|-------------|-------|----------|\n";
324
+ for (const rule of INPUT_SOURCE_RULES) {
325
+ md += `| \`${rule.actionPattern}\` | \`${rule.recommended}\` | ${rule.avoid.map(a => `\`${a}\``).join(", ") || "-"} | ${rule.severity} |\n`;
326
+ }
327
+ md += "\n### Anti-Patterns\n\n";
328
+ for (const ap of ANTI_PATTERNS) {
329
+ md += `#### ❌ ${ap.name}\n\n`;
330
+ md += `**Pattern**: ${ap.pattern}\n\n`;
331
+ md += `**Problem**: ${ap.problem}\n\n`;
332
+ md += `**Solution**: ${ap.solution}\n\n`;
333
+ md += `**Severity**: ${ap.severity}\n\n`;
334
+ }
335
+ md += "### Optimization Opportunities\n\n";
336
+ for (const opt of OPTIMIZATION_RULES) {
337
+ md += `#### ${opt.name} (${opt.priority})\n\n`;
338
+ md += `- **Current**: ${opt.currentState}\n`;
339
+ md += `- **Recommendation**: ${opt.recommendation}\n`;
340
+ md += `- **Benefit**: ${opt.benefit}\n\n`;
341
+ }
342
+ return md;
343
+ }
344
+ /**
345
+ * Export rules in JSON format (for external tools or caching)
346
+ */
347
+ export function exportRulesAsJSON() {
348
+ return {
349
+ version: "1.0.0",
350
+ generatedAt: new Date().toISOString(),
351
+ inputSourceRules: INPUT_SOURCE_RULES,
352
+ antiPatterns: ANTI_PATTERNS,
353
+ optimizationRules: OPTIMIZATION_RULES,
354
+ };
355
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Workflow Compiler
3
+ *
4
+ * Compiles workflow specifications into Ema workflow_def and proto_config.
5
+ * This is a pure compiler - no hardcoded patterns or biased generators.
6
+ *
7
+ * The AI assistant should:
8
+ * 1. Read patterns from ema://catalog/patterns for reference
9
+ * 2. Construct nodes based on user requirements
10
+ * 3. Pass to compileWorkflow() to generate deployment-ready JSON
11
+ */
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Namespace mappings
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ const ACTION_NAMESPACES = {
16
+ chat_trigger: ["triggers", "emainternal"],
17
+ document_trigger: ["triggers", "emainternal"],
18
+ chat_categorizer: ["routing", "emainternal"],
19
+ text_categorizer: ["routing", "emainternal"],
20
+ search: ["search", "emainternal"],
21
+ live_web_search: ["search", "emainternal"],
22
+ respond_with_sources: ["generation", "emainternal"],
23
+ call_llm: ["generation", "emainternal"],
24
+ fixed_response: ["generation", "emainternal"],
25
+ external_action_caller: ["external", "emainternal"],
26
+ general_hitl: ["collaboration", "emainternal"],
27
+ send_email_agent: ["external", "emainternal"],
28
+ conversation_to_search_query: ["search", "emainternal"],
29
+ entity_extraction: ["entity", "emainternal"],
30
+ combine_search_results: ["search", "emainternal"],
31
+ response_validator: ["validation", "emainternal"],
32
+ };
33
+ const OPERATOR_MAP = {
34
+ eq: 1,
35
+ neq: 2,
36
+ gt: 3,
37
+ lt: 4,
38
+ gte: 5,
39
+ lte: 6,
40
+ };
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ // Compiler
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ /**
45
+ * Compile a workflow specification into deployment-ready JSON.
46
+ *
47
+ * @param spec - The workflow specification with nodes and result mappings
48
+ * @returns Compiled workflow_def and proto_config
49
+ */
50
+ export function compileWorkflow(spec) {
51
+ const actions = [];
52
+ const enumTypes = [];
53
+ const enumTypeName = spec.nodes.some((n) => n.actionType.includes("categorizer"))
54
+ ? `${spec.name.replace(/\s+/g, "_")}_Categories`
55
+ : undefined;
56
+ // Build actions from nodes
57
+ for (const node of spec.nodes) {
58
+ const action = buildAction(node, enumTypeName);
59
+ actions.push(action);
60
+ // Build enum type for categorizer
61
+ if (node.categories && enumTypeName) {
62
+ const values = node.categories.map((cat, idx) => ({
63
+ name: cat.name,
64
+ number: idx + 1,
65
+ options: {},
66
+ }));
67
+ // Always include Fallback
68
+ if (!node.categories.some((c) => c.name === "Fallback")) {
69
+ values.push({ name: "Fallback", number: values.length + 1, options: {} });
70
+ }
71
+ enumTypes.push({
72
+ name: enumTypeName,
73
+ values,
74
+ options: { allowAlias: true },
75
+ });
76
+ }
77
+ }
78
+ // Build results mapping
79
+ const results = {};
80
+ for (const mapping of spec.resultMappings) {
81
+ const key = `WORKFLOW_OUTPUT_${Object.keys(results).length}`;
82
+ results[key] = {
83
+ actionName: mapping.nodeId,
84
+ outputName: mapping.output,
85
+ };
86
+ }
87
+ // Build workflow_def
88
+ // CRITICAL: Only include enumTypes if there are valid entries with proper names
89
+ // Empty or malformed enumTypes causes workflow engine to crash
90
+ const validEnumTypes = enumTypes.filter((e) => typeof e.name === "string" && e.name.length > 0);
91
+ const workflowDef = {
92
+ workflowName: {
93
+ name: {
94
+ namespaces: ["ema", "workflows"],
95
+ name: spec.name.toLowerCase().replace(/\s+/g, "_"),
96
+ },
97
+ },
98
+ actions,
99
+ results,
100
+ };
101
+ // Only add enumTypes if there are valid ones
102
+ if (validEnumTypes.length > 0) {
103
+ workflowDef.enumTypes = validEnumTypes;
104
+ }
105
+ // Build proto_config
106
+ const protoConfig = buildProtoConfig(spec);
107
+ return { workflow_def: workflowDef, proto_config: protoConfig };
108
+ }
109
+ function buildAction(node, enumTypeName) {
110
+ const namespaces = ACTION_NAMESPACES[node.actionType] ?? ["custom", "emainternal"];
111
+ const action = {
112
+ // CRITICAL: Use "name" not "actionName" - this is the node identifier in the workflow
113
+ name: node.id,
114
+ actionDisplayName: node.displayName,
115
+ actionDescription: node.description ?? "",
116
+ action: {
117
+ name: { namespaces, name: node.actionType },
118
+ },
119
+ inputs: {},
120
+ disableHumanInteraction: node.disableHitl ?? false,
121
+ };
122
+ // Build inputs
123
+ if (node.inputs) {
124
+ const inputs = {};
125
+ for (const [inputName, binding] of Object.entries(node.inputs)) {
126
+ inputs[inputName] = buildInputBinding(binding);
127
+ }
128
+ action.inputs = inputs;
129
+ }
130
+ // Build runIf condition
131
+ if (node.runIf) {
132
+ action.runIf = {
133
+ lhs: {
134
+ actionOutput: {
135
+ actionName: node.runIf.sourceAction,
136
+ output: node.runIf.sourceOutput,
137
+ },
138
+ autoDetectedBinding: false,
139
+ },
140
+ operator: OPERATOR_MAP[node.runIf.operator] ?? 1,
141
+ rhs: {
142
+ inline: { enumValue: node.runIf.value },
143
+ autoDetectedBinding: false,
144
+ },
145
+ };
146
+ }
147
+ // Build categories for categorizer
148
+ if (node.categories && enumTypeName) {
149
+ const categories = node.categories.map((cat) => ({
150
+ category: cat.name,
151
+ conditionDescription: cat.description,
152
+ examplePhrases: cat.examples ?? [],
153
+ }));
154
+ // Always add Fallback
155
+ if (!node.categories.some((c) => c.name === "Fallback")) {
156
+ categories.push({
157
+ category: "Fallback",
158
+ conditionDescription: "User request doesn't match other categories",
159
+ examplePhrases: [],
160
+ });
161
+ }
162
+ action.inputs = {
163
+ ...action.inputs,
164
+ categories: { inline: { array: { value: categories.map((c) => ({ message: c })) } } },
165
+ };
166
+ action.outputType = enumTypeName;
167
+ }
168
+ // Build tools for external_action_caller
169
+ if (node.tools) {
170
+ const toolInputs = {};
171
+ for (const tool of node.tools) {
172
+ const toolKey = `${tool.namespace}/${tool.name}`;
173
+ if (tool.inputs) {
174
+ const toolBindings = {};
175
+ for (const [k, v] of Object.entries(tool.inputs)) {
176
+ toolBindings[k] = buildInputBinding(v);
177
+ }
178
+ toolInputs[toolKey] = { multiBinding: { bindings: toolBindings } };
179
+ }
180
+ else {
181
+ toolInputs[toolKey] = { llmInferred: {} };
182
+ }
183
+ }
184
+ action.inputs = {
185
+ ...action.inputs,
186
+ tools: {
187
+ multiBinding: {
188
+ bindings: toolInputs,
189
+ },
190
+ },
191
+ };
192
+ }
193
+ return action;
194
+ }
195
+ function buildInputBinding(binding) {
196
+ switch (binding.type) {
197
+ case "action_output":
198
+ return {
199
+ actionOutput: {
200
+ actionName: binding.actionName,
201
+ output: binding.output,
202
+ },
203
+ autoDetectedBinding: false,
204
+ };
205
+ case "inline_string":
206
+ return { inline: { string: binding.value }, autoDetectedBinding: false };
207
+ case "inline_number":
208
+ return { inline: { number: binding.value }, autoDetectedBinding: false };
209
+ case "inline_bool":
210
+ return { inline: { bool: binding.value }, autoDetectedBinding: false };
211
+ case "widget_config":
212
+ return { widgetConfig: { widgetName: binding.widgetName }, autoDetectedBinding: false };
213
+ case "llm_inferred":
214
+ return { llmInferred: {} };
215
+ default:
216
+ return { llmInferred: {} };
217
+ }
218
+ }
219
+ function buildProtoConfig(spec) {
220
+ const projectType = spec.personaType === "voice" ? 5 : spec.personaType === "chat" ? 4 : 2;
221
+ const widgets = [
222
+ { widget_type_id: 6, widget_name: "fusionModel", widget_config: { selectedModels: ["gpt-4.1"] } },
223
+ { widget_type_id: 8, widget_name: "dataProtection", widget_config: { enabled: true } },
224
+ ];
225
+ if (spec.personaType === "voice") {
226
+ widgets.push({ widget_type_id: 38, widget_name: "voiceSettings", widget_config: { languageHints: ["en-US"], voiceModel: "default" } }, { widget_type_id: 39, widget_name: "conversationSettings", widget_config: {} }, { widget_type_id: 43, widget_name: "vadSettings", widget_config: { turnTimeout: 5, silenceEndCallTimeout: 30, maxConversationDuration: 300 } });
227
+ }
228
+ if (spec.personaType === "chat") {
229
+ widgets.push({ widget_type_id: 28, widget_name: "chatbotSdkConfig", widget_config: { name: spec.name, theme: { primaryColor: "#0066cc" }, allowedDomains: ["*"] } }, { widget_type_id: 33, widget_name: "feedbackMessage", widget_config: { message: { question: "Was this response helpful?" }, feedbackFrequency: "always" } });
230
+ }
231
+ widgets.push({ widget_type_id: 3, widget_name: "fileUpload", widget_config: { useChunking: true } });
232
+ return {
233
+ project_type: projectType,
234
+ name: spec.name,
235
+ description: spec.description,
236
+ widgets,
237
+ };
238
+ }
239
+ /**
240
+ * Generate voice-specific proto_config widgets
241
+ */
242
+ export function buildVoiceConfig(settings) {
243
+ return [
244
+ {
245
+ widget_type_id: 39,
246
+ widget_name: "conversationSettings",
247
+ widget_config: {
248
+ welcomeMessage: settings.welcomeMessage,
249
+ identityAndPurpose: settings.identityAndPurpose,
250
+ takeActionInstructions: settings.takeActionInstructions,
251
+ hangupInstructions: settings.hangupInstructions,
252
+ transferCallInstructions: settings.transferInstructions ?? "",
253
+ speechCharacteristics: settings.speechCharacteristics ?? "",
254
+ systemPrompt: settings.systemPrompt ?? "",
255
+ waitMessage: settings.waitMessage ?? "One moment please...",
256
+ },
257
+ },
258
+ {
259
+ widget_type_id: 38,
260
+ widget_name: "voiceSettings",
261
+ widget_config: {
262
+ languageHints: settings.languageHints ?? ["en-US"],
263
+ voiceModel: "default",
264
+ },
265
+ },
266
+ ];
267
+ }
268
+ /**
269
+ * Generate chat-specific proto_config widgets
270
+ */
271
+ export function buildChatConfig(settings) {
272
+ return [
273
+ {
274
+ widget_type_id: 28,
275
+ widget_name: "chatbotSdkConfig",
276
+ widget_config: {
277
+ name: settings.name,
278
+ theme: { primaryColor: settings.primaryColor ?? "#0066cc" },
279
+ allowedDomains: settings.allowedDomains ?? ["*"],
280
+ },
281
+ },
282
+ {
283
+ widget_type_id: 33,
284
+ widget_name: "feedbackMessage",
285
+ widget_config: {
286
+ message: { question: settings.feedbackQuestion ?? "Was this response helpful?" },
287
+ feedbackFrequency: "always",
288
+ },
289
+ },
290
+ ];
291
+ }