@ema.co/mcp-toolkit 1.5.1 → 1.6.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.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

@@ -0,0 +1,290 @@
1
+ /**
2
+ * Structural Rules for LLM Context
3
+ *
4
+ * These rules encode the validation logic from:
5
+ * - workflow-fixer.ts
6
+ * - workflow-execution-analyzer.ts
7
+ * - knowledge.ts (detectWorkflowIssues)
8
+ *
9
+ * PURPOSE: Feed these to the LLM so it can self-validate during generation/transformation.
10
+ * This is the "teach the LLM the rules" approach vs "fix after the fact".
11
+ */
12
+ export const STRUCTURAL_INVARIANTS = [
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ // GRAPH INVARIANTS (Cycles, Connectivity)
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+ {
17
+ id: "no_cycles",
18
+ name: "No Circular Dependencies",
19
+ rule: "A workflow graph must be a DAG (Directed Acyclic Graph). No node can depend on its own output through any chain of connections.",
20
+ violation: "Node A → Node B → Node C → Node A creates infinite loop",
21
+ fix: "Break the cycle by removing one edge or restructuring the workflow",
22
+ severity: "critical",
23
+ },
24
+ {
25
+ id: "all_nodes_reachable",
26
+ name: "All Nodes Reachable from Trigger",
27
+ rule: "Every node must be reachable from the trigger node. Orphan nodes waste resources and indicate incomplete wiring.",
28
+ violation: "Node 'search_old' exists but has no path from trigger",
29
+ fix: "Either connect the node to the workflow or remove it",
30
+ severity: "warning",
31
+ },
32
+ {
33
+ id: "all_paths_reach_output",
34
+ name: "All Paths Must Reach WORKFLOW_OUTPUT",
35
+ rule: "Every execution path from trigger must eventually reach a node that's connected to WORKFLOW_OUTPUT.",
36
+ violation: "Branch 'billing' ends at 'search_billing' with no response node",
37
+ fix: "Add a response node (respond_with_sources, call_llm, fixed_response) and connect to WORKFLOW_OUTPUT",
38
+ severity: "critical",
39
+ },
40
+ {
41
+ id: "has_trigger",
42
+ name: "Must Have Entry Point",
43
+ rule: "Every workflow must have exactly one trigger node (chat_trigger, voice_trigger, etc.)",
44
+ violation: "Workflow has no trigger - no entry point for execution",
45
+ fix: "Add a trigger node at the start of the workflow",
46
+ severity: "critical",
47
+ },
48
+ {
49
+ id: "has_output",
50
+ name: "Must Have WORKFLOW_OUTPUT",
51
+ rule: "Every workflow must have at least one resultMapping to WORKFLOW_OUTPUT. This is how responses reach users.",
52
+ violation: "No resultMappings defined - responses go nowhere",
53
+ fix: "Add resultMappings: [{ nodeId: 'response_node', output: 'response_with_sources' }]",
54
+ severity: "critical",
55
+ },
56
+ // ═══════════════════════════════════════════════════════════════════════════
57
+ // BRANCHING INVARIANTS (Categorizers, Conditions)
58
+ // ═══════════════════════════════════════════════════════════════════════════
59
+ {
60
+ id: "categorizer_has_fallback",
61
+ name: "Categorizers Must Have Fallback",
62
+ rule: "Every categorizer must have a 'Fallback' (or 'Other') category. Otherwise, unrecognized intents cause undefined behavior.",
63
+ violation: "Categorizer has categories [billing, technical] but no fallback",
64
+ fix: "Add category: { name: 'Fallback', description: 'For unclear or other requests' }",
65
+ severity: "critical",
66
+ },
67
+ {
68
+ id: "all_categories_have_handlers",
69
+ name: "Every Category Needs a Handler",
70
+ rule: "Each category in a categorizer must have at least one node with runIf condition for that category.",
71
+ violation: "Category 'technical' exists but no node has runIf checking for it",
72
+ fix: "Add a node with runIf: { sourceAction: 'categorizer', value: 'technical' }",
73
+ severity: "critical",
74
+ },
75
+ {
76
+ id: "runif_category_exists",
77
+ name: "RunIf Must Reference Valid Category",
78
+ rule: "When a runIf condition checks a category, that category must exist in the categorizer's categories array.",
79
+ violation: "runIf checks for 'support' but categorizer only has ['billing', 'technical']",
80
+ fix: "Either add 'support' to categories or update runIf to use existing category",
81
+ severity: "critical",
82
+ },
83
+ // ═══════════════════════════════════════════════════════════════════════════
84
+ // RESPONSE INVARIANTS (No duplicate responses)
85
+ // ═══════════════════════════════════════════════════════════════════════════
86
+ {
87
+ id: "no_parallel_responders",
88
+ name: "No Ungated Parallel Responders",
89
+ rule: "If multiple response nodes can execute in the same branch (no mutually-exclusive runIf), they will ALL execute, causing duplicate responses.",
90
+ violation: "Both 'respond_billing' and 'respond_general' can execute when category='billing'",
91
+ fix: "Add runIf conditions so only ONE response node executes per path. Use catch-all for fallback.",
92
+ severity: "critical",
93
+ },
94
+ {
95
+ id: "response_after_search",
96
+ name: "Response Nodes Need Data",
97
+ rule: "Response nodes (respond_with_sources, call_llm) should receive search_results or context. Don't generate responses without data.",
98
+ violation: "respond_with_sources has no search_results input",
99
+ fix: "Connect a search node's output to the response node's search_results input",
100
+ severity: "warning",
101
+ },
102
+ // ═══════════════════════════════════════════════════════════════════════════
103
+ // HITL INVARIANTS (Human-in-the-loop)
104
+ // ═══════════════════════════════════════════════════════════════════════════
105
+ {
106
+ id: "hitl_has_both_paths",
107
+ name: "HITL Must Have Success AND Failure Paths",
108
+ rule: "A general_hitl node has two outcomes: approval and rejection. Both MUST have downstream handlers.",
109
+ violation: "HITL 'approval' only has success path - rejections hang",
110
+ fix: "Add handler for hitl.approval_decision = 'reject' (typically fixed_response with apology)",
111
+ severity: "critical",
112
+ },
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+ // TYPE INVARIANTS (Data types must match)
115
+ // ═══════════════════════════════════════════════════════════════════════════
116
+ {
117
+ id: "type_compatibility",
118
+ name: "Input/Output Types Must Match",
119
+ rule: "When connecting nodes, the output type must be compatible with the input type. TEXT_WITH_SOURCES ≠ CHAT_CONVERSATION.",
120
+ violation: "search.query expects TEXT_WITH_SOURCES but receives trigger.chat_conversation (CHAT_CONVERSATION)",
121
+ fix: "Use trigger.user_query instead, or add conversation_to_search_query to convert",
122
+ severity: "critical",
123
+ },
124
+ {
125
+ id: "email_recipient_type",
126
+ name: "Email Recipients Must Be Email Addresses",
127
+ rule: "send_email.to_email must receive an actual email address, not text content (summaries, responses, etc.)",
128
+ violation: "to_email receives respond_with_sources.response (text, not email)",
129
+ fix: "Use entity_extraction to extract email_address, then connect to to_email",
130
+ severity: "critical",
131
+ },
132
+ ];
133
+ export const EXECUTION_RULES = [
134
+ {
135
+ id: "runif_gates_execution",
136
+ name: "RunIf Conditions Gate Execution",
137
+ description: "A node with runIf only executes when the condition is true. It's like an 'if' statement.",
138
+ implication: "Nodes without runIf ALWAYS execute if their inputs are available",
139
+ },
140
+ {
141
+ id: "parallel_execution",
142
+ name: "Independent Nodes Execute in Parallel",
143
+ description: "Nodes that don't depend on each other's output can execute simultaneously.",
144
+ implication: "If two response nodes both get triggered, BOTH will respond (causing duplicate messages)",
145
+ },
146
+ {
147
+ id: "categorizer_single_output",
148
+ name: "Categorizers Produce Single Category",
149
+ description: "A categorizer evaluates input and produces ONE category value (or Fallback).",
150
+ implication: "Use runIf: { value: 'category_name' } to gate nodes to specific categories",
151
+ },
152
+ {
153
+ id: "workflow_output_aggregates",
154
+ name: "WORKFLOW_OUTPUT Collects All Responses",
155
+ description: "Multiple nodes can map to WORKFLOW_OUTPUT. ALL mapped outputs are returned.",
156
+ implication: "If 3 response nodes map to output and all execute, user gets 3 responses",
157
+ },
158
+ {
159
+ id: "dead_end_no_output",
160
+ name: "Dead-End Nodes Produce No User Response",
161
+ description: "If a node's output isn't connected to another node or WORKFLOW_OUTPUT, its result is lost.",
162
+ implication: "Always trace from response nodes to WORKFLOW_OUTPUT",
163
+ },
164
+ ];
165
+ export const COMMON_STRUCTURAL_MISTAKES = [
166
+ {
167
+ id: "triple_response",
168
+ symptom: "User receives 3 identical (or similar) responses",
169
+ cause: "Multiple response nodes without mutually-exclusive runIf conditions all executed",
170
+ fix: "Add runIf conditions so only one response node fires per category. Check that response nodes are gated by the categorizer output.",
171
+ },
172
+ {
173
+ id: "no_response",
174
+ symptom: "User sends message but gets no response",
175
+ cause: "Response node not connected to WORKFLOW_OUTPUT, or all runIf conditions false",
176
+ fix: "Verify resultMappings include the response node. Add Fallback category that always responds.",
177
+ },
178
+ {
179
+ id: "wrong_response_branch",
180
+ symptom: "User asks about billing but gets technical response",
181
+ cause: "runIf condition checking wrong category, or categorizer misconfigured",
182
+ fix: "Verify runIf.value matches the exact category name (case-sensitive). Check categorizer categories.",
183
+ },
184
+ {
185
+ id: "email_to_garbage",
186
+ symptom: "Emails sent to invalid addresses or contain 'undefined'",
187
+ cause: "to_email connected to text output instead of entity_extraction.email_address",
188
+ fix: "Add entity_extraction node, extract email_address, connect to send_email.to_email",
189
+ },
190
+ {
191
+ id: "infinite_loop",
192
+ symptom: "Workflow times out or causes server error",
193
+ cause: "Circular dependency: Node A → B → C → A",
194
+ fix: "Review node connections. Use workflow tracer to visualize. Break the cycle.",
195
+ },
196
+ {
197
+ id: "orphan_leftover",
198
+ symptom: "Workflow has nodes that seem unused",
199
+ cause: "Nodes created but never connected, or connections removed without removing node",
200
+ fix: "Either connect the node or remove it. Use analyzeWorkflow to detect orphans.",
201
+ },
202
+ {
203
+ id: "search_no_results",
204
+ symptom: "Response says 'I don't have information' even when KB has data",
205
+ cause: "search.query receives wrong input type (CHAT_CONVERSATION instead of TEXT_WITH_SOURCES)",
206
+ fix: "Connect trigger.user_query (not chat_conversation) to search.query",
207
+ },
208
+ ];
209
+ // ─────────────────────────────────────────────────────────────────────────────
210
+ // Format for LLM Context
211
+ // ─────────────────────────────────────────────────────────────────────────────
212
+ /**
213
+ * Generate markdown documentation for LLM context.
214
+ * Include this in WORKFLOW_SCHEMA_FOR_LLM for self-validation.
215
+ */
216
+ export const STRUCTURAL_RULES_FOR_LLM = `
217
+ ## Structural Validation Rules
218
+
219
+ BEFORE finalizing any workflow modification, verify these rules:
220
+
221
+ ### Graph Rules (CRITICAL)
222
+
223
+ 1. **No Cycles**: Workflow must be a DAG. No node can depend on itself through any chain.
224
+ - BAD: trigger → search → llm → search (cycle!)
225
+ - GOOD: trigger → search → llm → output (linear)
226
+
227
+ 2. **All Nodes Reachable**: Every node must have a path from trigger.
228
+ - If a node has no incoming connections and isn't the trigger, it's orphaned.
229
+
230
+ 3. **All Paths Reach Output**: Every branch must eventually connect to WORKFLOW_OUTPUT.
231
+ - If a categorizer branch has no response node connected to output, that path is dead.
232
+
233
+ ### Categorizer Rules (CRITICAL)
234
+
235
+ 4. **Fallback Required**: Every categorizer must have a 'Fallback' category.
236
+ - This catches all unrecognized intents.
237
+
238
+ 5. **Category-Handler Mapping**: Each category needs at least one node with matching runIf.
239
+ - If categories are [billing, technical, Fallback], you need 3 handler paths.
240
+
241
+ 6. **RunIf Category Must Exist**: If runIf checks category='support', 'support' must be in categories.
242
+
243
+ ### Response Rules (CRITICAL)
244
+
245
+ 7. **One Response Per Path**: In any execution path, exactly ONE response node should execute.
246
+ - Multiple ungated responders = multiple responses to user (bad UX).
247
+ - Use mutually-exclusive runIf conditions to gate responders.
248
+
249
+ 8. **Response Nodes Need WORKFLOW_OUTPUT**: Response nodes must appear in resultMappings.
250
+ - Otherwise the response is generated but never sent to user.
251
+
252
+ ### Type Rules (CRITICAL)
253
+
254
+ 9. **Type Compatibility**:
255
+ | Input Expects | Can Connect From |
256
+ |---------------|------------------|
257
+ | TEXT_WITH_SOURCES | trigger.user_query, conversation_to_search_query.summarized_conversation |
258
+ | CHAT_CONVERSATION | trigger.chat_conversation |
259
+ | SEARCH_RESULT | search.search_results, combine_search_results.combined_results |
260
+ | EMAIL_ADDRESS | entity_extraction.email_address (NOT text outputs) |
261
+
262
+ ### HITL Rules
263
+
264
+ 10. **Both Paths Required**: general_hitl needs handlers for both approval AND rejection.
265
+
266
+ ### Self-Check Checklist
267
+
268
+ After generating/modifying a workflow, verify:
269
+ - [ ] Has trigger node
270
+ - [ ] Has WORKFLOW_OUTPUT with resultMappings
271
+ - [ ] All categories have handler nodes
272
+ - [ ] Categorizer has Fallback
273
+ - [ ] No orphan nodes (all reachable from trigger)
274
+ - [ ] No dead-end paths (all reach output)
275
+ - [ ] Response nodes are mutually exclusive (gated by runIf)
276
+ - [ ] Email recipients come from entity_extraction
277
+ - [ ] No circular dependencies
278
+ `;
279
+ // ─────────────────────────────────────────────────────────────────────────────
280
+ // Exports
281
+ // ─────────────────────────────────────────────────────────────────────────────
282
+ export function getAllStructuralRules() {
283
+ return STRUCTURAL_RULES_FOR_LLM;
284
+ }
285
+ export function getInvariantById(id) {
286
+ return STRUCTURAL_INVARIANTS.find(i => i.id === id);
287
+ }
288
+ export function getCriticalInvariants() {
289
+ return STRUCTURAL_INVARIANTS.filter(i => i.severity === "critical");
290
+ }
@@ -9,28 +9,91 @@
9
9
  * 2. Construct nodes based on user requirements
10
10
  * 3. Pass to compileWorkflow() to generate deployment-ready JSON
11
11
  */
12
- // ─────────────────────────────────────────────────────────────────────────────
13
- // Namespace mappings
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- const ACTION_NAMESPACES = {
12
+ /**
13
+ * FALLBACK VALUES - Used only when ActionRegistry is not available.
14
+ * These are based on API data as of 2026-01-16.
15
+ *
16
+ * ⚠️ PREFER USING ActionRegistry.load() TO GET CURRENT VALUES FROM API
17
+ */
18
+ const FALLBACK_NAMESPACES = {
16
19
  chat_trigger: ["triggers", "emainternal"],
17
- voice_trigger: ["triggers", "emainternal"],
20
+ // voice_trigger doesn't exist in API - Voice AI uses chat_trigger
18
21
  document_trigger: ["triggers", "emainternal"],
19
- chat_categorizer: ["routing", "emainternal"],
20
- text_categorizer: ["routing", "emainternal"],
21
- search: ["search", "emainternal"],
22
- live_web_search: ["search", "emainternal"],
23
- respond_with_sources: ["generation", "emainternal"],
24
- call_llm: ["generation", "emainternal"],
25
- fixed_response: ["generation", "emainternal"],
26
- external_action_caller: ["external", "emainternal"],
27
- general_hitl: ["collaboration", "emainternal"],
28
- send_email_agent: ["external", "emainternal"],
29
- conversation_to_search_query: ["search", "emainternal"],
30
- entity_extraction: ["entity", "emainternal"],
31
- combine_search_results: ["search", "emainternal"],
32
- response_validator: ["validation", "emainternal"],
22
+ chat_categorizer: ["actions", "emainternal"], // Fixed: was "routing"
23
+ text_categorizer: ["actions", "emainternal"],
24
+ search: ["actions", "emainternal"],
25
+ live_web_search: ["actions", "emainternal"],
26
+ call_llm: ["actions", "emainternal"],
27
+ respond_with_sources: ["actions", "emainternal"], // Maps to call_llm
28
+ fixed_response: ["actions", "emainternal"],
29
+ external_action_caller: ["actions", "emainternal"],
30
+ general_hitl: ["actions", "emainternal"],
31
+ send_email_agent: ["actions", "emainternal"],
32
+ conversation_to_search_query: ["actions", "emainternal"],
33
+ entity_extraction: ["actions", "emainternal"],
34
+ combine_search_results: ["actions", "emainternal"],
35
+ response_validator: ["actions", "emainternal"],
33
36
  };
37
+ /**
38
+ * FALLBACK VERSIONS - Based on API ListActions response 2026-01-16
39
+ *
40
+ * ⚠️ CRITICAL: These must match what the API actually expects!
41
+ * Many actions are v0, not v1 as previously hardcoded.
42
+ */
43
+ const FALLBACK_VERSIONS = {
44
+ chat_trigger: "v1",
45
+ // voice_trigger doesn't exist in API - Voice AI uses chat_trigger
46
+ document_trigger: "v0",
47
+ chat_categorizer: "v0", // Fixed: was v1
48
+ text_categorizer: "v1",
49
+ search: "v2",
50
+ live_web_search: "v0", // Fixed: was v1
51
+ call_llm: "v2", // Fixed: was v1
52
+ respond_with_sources: "v2", // Maps to call_llm:v2
53
+ fixed_response: "v1", // API has both v0 and v1
54
+ external_action_caller: "v0", // Fixed: was v1
55
+ general_hitl: "v0", // Assumed v0
56
+ send_email_agent: "v0", // Fixed: was v1
57
+ conversation_to_search_query: "v0", // Fixed: was v1
58
+ entity_extraction: "v0", // entity_extraction_with_documents is v0
59
+ combine_search_results: "v0", // combine_and_rerank_search_results is v0
60
+ response_validator: "v0", // Fixed: was v1
61
+ };
62
+ /**
63
+ * Map our ActionType to actual API action names.
64
+ * Some names differ between our types and the API.
65
+ */
66
+ const ACTION_TYPE_TO_API_NAME = {
67
+ entity_extraction: "entity_extraction_with_documents",
68
+ combine_search_results: "combine_and_rerank_search_results",
69
+ general_hitl: "hitl",
70
+ // CRITICAL: respond_with_sources doesn't exist in API - map to call_llm
71
+ respond_with_sources: "call_llm",
72
+ };
73
+ /**
74
+ * Get namespaces for an action, using registry if available.
75
+ */
76
+ function getNamespaces(actionType, registry) {
77
+ const apiName = ACTION_TYPE_TO_API_NAME[actionType] ?? actionType;
78
+ if (registry?.isLoaded()) {
79
+ const def = registry.getAction(apiName);
80
+ if (def)
81
+ return def.namespaces;
82
+ }
83
+ return FALLBACK_NAMESPACES[actionType] ?? ["actions", "emainternal"];
84
+ }
85
+ /**
86
+ * Get version for an action, using registry if available.
87
+ */
88
+ function getVersion(actionType, registry) {
89
+ const apiName = ACTION_TYPE_TO_API_NAME[actionType] ?? actionType;
90
+ if (registry?.isLoaded()) {
91
+ const def = registry.getAction(apiName);
92
+ if (def)
93
+ return def.version;
94
+ }
95
+ return FALLBACK_VERSIONS[actionType] ?? "v0";
96
+ }
34
97
  const OPERATOR_MAP = {
35
98
  eq: 1,
36
99
  neq: 2,
@@ -39,16 +102,15 @@ const OPERATOR_MAP = {
39
102
  gte: 5,
40
103
  lte: 6,
41
104
  };
42
- // ─────────────────────────────────────────────────────────────────────────────
43
- // Compiler
44
- // ─────────────────────────────────────────────────────────────────────────────
45
105
  /**
46
106
  * Compile a workflow specification into deployment-ready JSON.
47
107
  *
48
108
  * @param spec - The workflow specification with nodes and result mappings
109
+ * @param options - Optional compilation options (registry for API-driven values)
49
110
  * @returns Compiled workflow_def and proto_config
50
111
  */
51
- export function compileWorkflow(spec) {
112
+ export function compileWorkflow(spec, options) {
113
+ const registry = options?.registry;
52
114
  const actions = [];
53
115
  const enumTypes = [];
54
116
  const enumTypeName = spec.nodes.some((n) => n.actionType?.includes("categorizer"))
@@ -56,7 +118,7 @@ export function compileWorkflow(spec) {
56
118
  : undefined;
57
119
  // Build actions from nodes
58
120
  for (const node of spec.nodes) {
59
- const action = buildAction(node, enumTypeName);
121
+ const action = buildAction(node, enumTypeName, registry);
60
122
  actions.push(action);
61
123
  // Build enum type for categorizer
62
124
  if (node.categories && enumTypeName) {
@@ -93,31 +155,45 @@ export function compileWorkflow(spec) {
93
155
  workflowName: {
94
156
  name: {
95
157
  namespaces: ["ema", "workflows"],
96
- name: spec.name.toLowerCase().replace(/\s+/g, "_"),
158
+ name: spec.name.toLowerCase().replace(/\s+/g, "_").slice(0, 50), // Limit name length
97
159
  },
98
160
  },
99
161
  actions,
162
+ enumTypes: validEnumTypes.length > 0 ? validEnumTypes : [], // Always include, even if empty
163
+ workflowInputs: {},
100
164
  results,
165
+ namedResults: {},
166
+ displayName: spec.name,
167
+ description: spec.description || "",
168
+ namedResultsEditable: false,
169
+ namedResultsEnabled: false,
101
170
  };
102
- // Only add enumTypes if there are valid ones
103
- if (validEnumTypes.length > 0) {
104
- workflowDef.enumTypes = validEnumTypes;
105
- }
106
171
  // Build proto_config
107
172
  const protoConfig = buildProtoConfig(spec);
108
173
  return { workflow_def: workflowDef, proto_config: protoConfig };
109
174
  }
110
- function buildAction(node, enumTypeName) {
111
- const namespaces = ACTION_NAMESPACES[node.actionType] ?? ["custom", "emainternal"];
175
+ function buildAction(node, enumTypeName, registry) {
176
+ // Use registry for API-driven values, fallback to static values
177
+ const namespaces = getNamespaces(node.actionType, registry);
178
+ const version = getVersion(node.actionType, registry);
179
+ // Map our ActionType to actual API action name if different
180
+ const apiActionName = ACTION_TYPE_TO_API_NAME[node.actionType] ?? node.actionType;
112
181
  const action = {
113
182
  // CRITICAL: Use "name" not "actionName" - this is the node identifier in the workflow
114
183
  name: node.id,
115
- actionDisplayName: node.displayName,
116
- actionDescription: node.description ?? "",
117
184
  action: {
118
- name: { namespaces, name: node.actionType },
185
+ name: { namespaces, name: apiActionName },
186
+ version,
119
187
  },
120
188
  inputs: {},
189
+ displaySettings: {
190
+ displayName: node.displayName ?? "",
191
+ description: node.description ?? "",
192
+ coordinates: { x: 800, y: 200 }, // Default position
193
+ showConfig: 0,
194
+ },
195
+ typeArguments: {},
196
+ tools: [],
121
197
  disableHumanInteraction: node.disableHitl ?? false,
122
198
  };
123
199
  // Build inputs
@@ -211,6 +287,15 @@ function buildInputBinding(binding) {
211
287
  return { inline: { bool: binding.value }, autoDetectedBinding: false };
212
288
  case "widget_config":
213
289
  return { widgetConfig: { widgetName: binding.widgetName }, autoDetectedBinding: false };
290
+ case "widget_config_array":
291
+ // For datastore_configs that needs multiBinding with elements array
292
+ return {
293
+ multiBinding: {
294
+ elements: (binding.widgetNames || [binding.widgetName]).map(name => ({
295
+ widgetConfig: { widgetName: name }
296
+ }))
297
+ }
298
+ };
214
299
  case "llm_inferred":
215
300
  return { llmInferred: {} };
216
301
  default:
@@ -219,17 +304,80 @@ function buildInputBinding(binding) {
219
304
  }
220
305
  function buildProtoConfig(spec) {
221
306
  const projectType = spec.personaType === "voice" ? 5 : spec.personaType === "chat" ? 4 : 2;
307
+ // Widget format expected by Ema API:
308
+ // { name: "widgetName", type: <type_id>, widgetName: { ...config... } }
309
+ // The config is stored under a key matching the widget's name
222
310
  const widgets = [
223
- { widget_type_id: 6, widget_name: "fusionModel", widget_config: { selectedModels: ["gpt-4.1"] } },
224
- { widget_type_id: 8, widget_name: "dataProtection", widget_config: { enabled: true } },
311
+ {
312
+ name: "fusionModel",
313
+ type: 6,
314
+ fusionModel: { selectedModels: ["gpt-4.1"] }
315
+ },
316
+ {
317
+ name: "dataProtection",
318
+ type: 8,
319
+ dataProtection: { enabled: true }
320
+ },
225
321
  ];
226
322
  if (spec.personaType === "voice") {
227
- 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 } });
323
+ // Build voice settings from spec.voiceConfig with sensible defaults
324
+ const vc = spec.voiceConfig ?? {};
325
+ const defaultIdentity = `You are ${spec.name}. ${spec.description}`;
326
+ widgets.push({
327
+ name: "voiceSettings",
328
+ type: 38,
329
+ voiceSettings: {
330
+ languageHints: vc.languageHints ?? ["en-US"],
331
+ voiceModel: "default"
332
+ }
333
+ }, {
334
+ name: "conversationSettings",
335
+ type: 39,
336
+ conversationSettings: {
337
+ welcomeMessage: vc.welcomeMessage ?? `Hello, this is ${spec.name}. How can I help you today?`,
338
+ identityAndPurpose: vc.identityAndPurpose ?? defaultIdentity,
339
+ takeActionInstructions: vc.takeActionInstructions ?? "Take appropriate actions based on the user's request. Always confirm before making changes.",
340
+ hangupInstructions: vc.hangupInstructions ?? "End the call politely when the user's request is complete or they ask to hang up.",
341
+ transferCallInstructions: vc.transferInstructions ?? "",
342
+ speechCharacteristics: vc.speechCharacteristics ?? "Professional, friendly, and helpful",
343
+ systemPrompt: vc.systemPrompt ?? "",
344
+ waitMessage: vc.waitMessage ?? "One moment please while I look that up...",
345
+ }
346
+ }, {
347
+ name: "vadSettings",
348
+ type: 43,
349
+ vadSettings: {
350
+ turnTimeout: 5,
351
+ silenceEndCallTimeout: 30,
352
+ maxConversationDuration: 300
353
+ }
354
+ });
228
355
  }
229
356
  if (spec.personaType === "chat") {
230
- 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" } });
357
+ // Build chat settings from spec.chatConfig with sensible defaults
358
+ const cc = spec.chatConfig ?? {};
359
+ widgets.push({
360
+ name: "chatbotSdkConfig",
361
+ type: 28,
362
+ chatbotSdkConfig: {
363
+ name: cc.name ?? spec.name,
364
+ theme: { primaryColor: cc.primaryColor ?? "#0066cc" },
365
+ allowedDomains: cc.allowedDomains ?? ["*"]
366
+ }
367
+ }, {
368
+ name: "feedbackMessage",
369
+ type: 33,
370
+ feedbackMessage: {
371
+ message: { question: cc.feedbackQuestion ?? "Was this response helpful?" },
372
+ feedbackFrequency: "always"
373
+ }
374
+ });
231
375
  }
232
- widgets.push({ widget_type_id: 3, widget_name: "fileUpload", widget_config: { useChunking: true } });
376
+ widgets.push({
377
+ name: "fileUpload",
378
+ type: 3,
379
+ fileUpload: { useChunking: true }
380
+ });
233
381
  return {
234
382
  project_type: projectType,
235
383
  name: spec.name,