@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.
- package/dist/mcp/handlers-consolidated.js +400 -14
- package/dist/mcp/prompts.js +80 -123
- package/dist/mcp/server.js +134 -209
- package/dist/mcp/tools-consolidated.js +212 -150
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/client.js +58 -90
- package/dist/sdk/demo-generator.js +978 -0
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +15 -1
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +187 -39
- package/dist/sdk/workflow-intent.js +246 -24
- package/dist/sdk/workflow-optimizer.js +665 -0
- package/dist/sdk/workflow-tracer.js +648 -0
- package/dist/sdk/workflow-transformer.js +10 -0
- package/dist/sdk/workflow-validator.js +391 -0
- package/docs/.temp/datasource-attach.har +198369 -0
- package/docs/.temp/grpcweb.gar +1 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +163 -197
- package/docs/openapi.json +8000 -0
- package/docs/release-process.md +153 -0
- package/docs/test-persona-creation.md +196 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +3 -1
- package/resources/templates/demo-scenarios/README.md +63 -0
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
20
|
+
// voice_trigger doesn't exist in API - Voice AI uses chat_trigger
|
|
18
21
|
document_trigger: ["triggers", "emainternal"],
|
|
19
|
-
chat_categorizer: ["
|
|
20
|
-
text_categorizer: ["
|
|
21
|
-
search: ["
|
|
22
|
-
live_web_search: ["
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
fixed_response: ["
|
|
26
|
-
external_action_caller: ["
|
|
27
|
-
general_hitl: ["
|
|
28
|
-
send_email_agent: ["
|
|
29
|
-
conversation_to_search_query: ["
|
|
30
|
-
entity_extraction: ["
|
|
31
|
-
combine_search_results: ["
|
|
32
|
-
response_validator: ["
|
|
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
|
-
|
|
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:
|
|
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
|
-
{
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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,
|