ema-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.
- package/README.md +338 -0
- package/config.example.yaml +32 -0
- package/dist/cli/index.js +333 -0
- package/dist/config.js +136 -0
- package/dist/emaClient.js +398 -0
- package/dist/index.js +109 -0
- package/dist/mcp/handlers-consolidated.js +851 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/prompts.js +1753 -0
- package/dist/mcp/resources.js +624 -0
- package/dist/mcp/server.js +4585 -0
- package/dist/mcp/tools-consolidated.js +590 -0
- package/dist/mcp/tools-legacy.js +736 -0
- package/dist/models.js +8 -0
- package/dist/scheduler.js +21 -0
- package/dist/sdk/client.js +788 -0
- package/dist/sdk/config.js +136 -0
- package/dist/sdk/contracts.js +429 -0
- package/dist/sdk/generation-schema.js +189 -0
- package/dist/sdk/index.js +39 -0
- package/dist/sdk/knowledge.js +2780 -0
- package/dist/sdk/models.js +8 -0
- package/dist/sdk/state.js +88 -0
- package/dist/sdk/sync-options.js +216 -0
- package/dist/sdk/sync.js +220 -0
- package/dist/sdk/validation-rules.js +355 -0
- package/dist/sdk/workflow-generator.js +291 -0
- package/dist/sdk/workflow-intent.js +1585 -0
- package/dist/state.js +88 -0
- package/dist/sync.js +416 -0
- package/dist/syncOptions.js +216 -0
- package/dist/ui.js +334 -0
- package/docs/advisor-comms-assistant-fixes.md +175 -0
- package/docs/api-contracts.md +216 -0
- package/docs/auto-builder-analysis.md +271 -0
- package/docs/data-architecture.md +166 -0
- package/docs/ema-auto-builder-guide.html +394 -0
- package/docs/ema-user-guide.md +1121 -0
- package/docs/mcp-tools-guide.md +149 -0
- package/docs/naming-conventions.md +218 -0
- package/docs/tool-consolidation-proposal.md +427 -0
- package/package.json +95 -0
- package/resources/templates/chat-ai/README.md +119 -0
- package/resources/templates/chat-ai/persona-config.json +111 -0
- package/resources/templates/dashboard-ai/README.md +156 -0
- package/resources/templates/dashboard-ai/persona-config.json +180 -0
- package/resources/templates/voice-ai/README.md +123 -0
- package/resources/templates/voice-ai/persona-config.json +74 -0
- 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
|
+
}
|