@ema.co/mcp-toolkit 2026.2.5 → 2026.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/.context/public/guides/ema-user-guide.md +12 -16
- package/.context/public/guides/mcp-tools-guide.md +203 -334
- package/LICENSE +29 -21
- package/README.md +58 -35
- package/dist/mcp/domain/loop-detection.js +97 -0
- package/dist/mcp/domain/proto-constraints.js +284 -0
- package/dist/mcp/domain/structural-rules.js +12 -5
- package/dist/mcp/domain/validation-rules.js +107 -20
- package/dist/mcp/domain/workflow-graph-optimizer.js +235 -0
- package/dist/mcp/domain/workflow-graph-transforms.js +808 -0
- package/dist/mcp/domain/workflow-graph.js +374 -0
- package/dist/mcp/domain/workflow-optimizer.js +10 -4
- package/dist/mcp/guidance.js +54 -31
- package/dist/mcp/handlers/feedback/index.js +139 -0
- package/dist/mcp/handlers/feedback/store.js +262 -0
- package/dist/mcp/handlers/persona/index.js +237 -8
- package/dist/mcp/handlers/persona/schema.js +27 -0
- package/dist/mcp/handlers/reference/index.js +6 -4
- package/dist/mcp/handlers/workflow/index.js +25 -28
- package/dist/mcp/handlers/workflow/optimize.js +73 -33
- package/dist/mcp/handlers/workflow/validation.js +1 -1
- package/dist/mcp/knowledge-types.js +7 -0
- package/dist/mcp/knowledge.js +146 -834
- package/dist/mcp/resources.js +610 -18
- package/dist/mcp/server.js +233 -2156
- package/dist/mcp/tools.js +91 -5
- package/dist/sdk/generated/agent-catalog.js +615 -0
- package/dist/sdk/generated/deprecated-actions.js +182 -96
- package/dist/sdk/generated/proto-fields.js +2 -1
- package/dist/sdk/generated/protos/service/agent_qa/v1/agent_qa_pb.js +460 -21
- package/dist/sdk/generated/protos/service/auth/v1/auth_pb.js +11 -1
- package/dist/sdk/generated/protos/service/dataingest/v1/dataingest_pb.js +173 -66
- package/dist/sdk/generated/protos/service/feedback/v1/feedback_pb.js +43 -1
- package/dist/sdk/generated/protos/service/llmservice/v1/llmservice_pb.js +26 -21
- package/dist/sdk/generated/protos/service/persona/v1/persona_config_pb.js +100 -89
- package/dist/sdk/generated/protos/service/persona/v1/persona_pb.js +126 -116
- package/dist/sdk/generated/protos/service/persona/v1/shared_widgets/widget_types_pb.js +33 -1
- package/dist/sdk/generated/protos/service/persona/v1/voicebot_widgets/widget_types_pb.js +60 -11
- package/dist/sdk/generated/protos/service/tenant/v1/tenant_pb.js +1 -1
- package/dist/sdk/generated/protos/service/user/v1/user_pb.js +1 -1
- package/dist/sdk/generated/protos/service/utils/v1/agent_qa_pb.js +35 -0
- package/dist/sdk/generated/protos/service/workflows/v1/action_registry_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/action_type_pb.js +6 -1
- package/dist/sdk/generated/protos/service/workflows/v1/chatbot_pb.js +106 -11
- package/dist/sdk/generated/protos/service/workflows/v1/common_forms_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/coordinator_pb.js +1 -1
- package/dist/sdk/generated/protos/service/workflows/v1/external_actions_pb.js +31 -1
- package/dist/sdk/generated/protos/service/workflows/v1/well_known_pb.js +5 -1
- package/dist/sdk/generated/protos/service/workflows/v1/workflow_pb.js +1 -1
- package/dist/sdk/generated/protos/util/tracking_metadata_pb.js +1 -1
- package/dist/sdk/generated/widget-catalog.js +60 -0
- package/docs/README.md +17 -9
- package/package.json +2 -2
- package/.context/public/guides/dashboard-operations.md +0 -286
- package/.context/public/guides/email-patterns.md +0 -125
- package/dist/mcp/domain/intent-architect.js +0 -914
- package/dist/mcp/domain/quality-gates.js +0 -110
- package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
- package/dist/mcp/domain/workflow-intent.js +0 -1806
- package/dist/mcp/domain/workflow-merge.js +0 -449
- package/dist/mcp/domain/workflow-tracer.js +0 -648
- package/dist/mcp/domain/workflow-transformer.js +0 -742
- package/dist/mcp/handlers/persona/intent.js +0 -141
- package/dist/mcp/handlers/workflow/analyze.js +0 -119
- package/dist/mcp/handlers/workflow/compare.js +0 -70
- package/dist/mcp/handlers/workflow/generate.js +0 -384
- package/dist/mcp/handlers-consolidated.js +0 -333
|
@@ -1,742 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow Transformer
|
|
3
|
-
*
|
|
4
|
-
* The LLM-native approach to workflow manipulation:
|
|
5
|
-
* 1. Decompile raw workflow_def to typed WorkflowSpec
|
|
6
|
-
* 2. Expose schema for LLM context
|
|
7
|
-
* 3. LLM transforms the spec directly
|
|
8
|
-
* 4. Compile back to workflow_def
|
|
9
|
-
*
|
|
10
|
-
* WHY THIS APPROACH:
|
|
11
|
-
* - LLMs excel at structured data transformation
|
|
12
|
-
* - Type definitions serve as schema documentation
|
|
13
|
-
* - Validation happens at compile time
|
|
14
|
-
* - No brittle keyword matching
|
|
15
|
-
*/
|
|
16
|
-
import { compileWorkflow } from "./workflow-generator.js";
|
|
17
|
-
import { STRUCTURAL_RULES_FOR_LLM } from "./structural-rules.js";
|
|
18
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
-
// SCHEMA DOCUMENTATION FOR LLM CONTEXT
|
|
20
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
-
/**
|
|
22
|
-
* This schema is designed to be included in LLM context.
|
|
23
|
-
* The LLM reads this, understands the structure, and outputs valid specs.
|
|
24
|
-
*/
|
|
25
|
-
export const WORKFLOW_SCHEMA_FOR_LLM = `
|
|
26
|
-
# Ema Workflow Schema
|
|
27
|
-
|
|
28
|
-
## Overview
|
|
29
|
-
Workflows are defined as a WorkflowSpec object with nodes and connections.
|
|
30
|
-
Each node has typed inputs/outputs. Connections are defined via InputBinding.
|
|
31
|
-
|
|
32
|
-
## Core Types
|
|
33
|
-
|
|
34
|
-
### WorkflowSpec
|
|
35
|
-
\`\`\`typescript
|
|
36
|
-
interface WorkflowSpec {
|
|
37
|
-
name: string; // Workflow identifier
|
|
38
|
-
description: string; // What this workflow does
|
|
39
|
-
personaType: "voice" | "chat" | "dashboard";
|
|
40
|
-
nodes: Node[]; // All workflow nodes
|
|
41
|
-
resultMappings: ResultMapping[]; // Which outputs go to WORKFLOW_OUTPUT
|
|
42
|
-
}
|
|
43
|
-
\`\`\`
|
|
44
|
-
|
|
45
|
-
### Node
|
|
46
|
-
\`\`\`typescript
|
|
47
|
-
interface Node {
|
|
48
|
-
id: string; // Unique node identifier (e.g., "search_1", "categorizer_main")
|
|
49
|
-
actionType: ActionType; // Node type (see ActionType below)
|
|
50
|
-
displayName: string; // Human-readable name
|
|
51
|
-
description?: string; // What this node does
|
|
52
|
-
inputs?: Record<string, InputBinding>; // Input connections
|
|
53
|
-
runIf?: RunIfCondition; // Conditional execution
|
|
54
|
-
categories?: Category[]; // For categorizer nodes only
|
|
55
|
-
tools?: Tool[]; // For custom_agent nodes
|
|
56
|
-
disableHitl?: boolean; // Skip human-in-the-loop
|
|
57
|
-
}
|
|
58
|
-
\`\`\`
|
|
59
|
-
|
|
60
|
-
### ActionType (Available Node Types)
|
|
61
|
-
\`\`\`typescript
|
|
62
|
-
type ActionType =
|
|
63
|
-
// Triggers (entry points)
|
|
64
|
-
| "chat_trigger" // Chat message entry point
|
|
65
|
-
| "voice_trigger" // Phone/voice call entry point
|
|
66
|
-
| "document_trigger" // Document upload entry point
|
|
67
|
-
|
|
68
|
-
// Routing (categorization)
|
|
69
|
-
| "chat_categorizer" // Categorizes chat conversation
|
|
70
|
-
| "text_categorizer" // Categorizes text
|
|
71
|
-
|
|
72
|
-
// Search (retrieval)
|
|
73
|
-
| "search" // Knowledge base search
|
|
74
|
-
| "live_web_search" // Real-time web search
|
|
75
|
-
| "combine_search_results" // Merge multiple searches
|
|
76
|
-
| "conversation_to_search_query" // Summarize conversation for search
|
|
77
|
-
|
|
78
|
-
// Generation (LLM responses)
|
|
79
|
-
| "respond_with_sources" // RAG response with citations
|
|
80
|
-
| "call_llm" // Custom LLM generation
|
|
81
|
-
| "fixed_response" // Template response (no LLM)
|
|
82
|
-
|
|
83
|
-
// Entity extraction
|
|
84
|
-
| "entity_extraction" // Extract structured data from text
|
|
85
|
-
|
|
86
|
-
// External actions
|
|
87
|
-
| "external_action_caller" // Call APIs (Salesforce, ServiceNow, etc.)
|
|
88
|
-
| "send_email_agent" // Send email
|
|
89
|
-
|
|
90
|
-
// Human-in-the-loop
|
|
91
|
-
| "general_hitl" // Human approval step
|
|
92
|
-
|
|
93
|
-
// Validation
|
|
94
|
-
| "response_validator"; // Validate LLM output
|
|
95
|
-
\`\`\`
|
|
96
|
-
|
|
97
|
-
### InputBinding (How Nodes Connect)
|
|
98
|
-
\`\`\`typescript
|
|
99
|
-
interface InputBinding {
|
|
100
|
-
type: "action_output" | "inline_string" | "inline_number" | "inline_bool" | "widget_config" | "llm_inferred";
|
|
101
|
-
|
|
102
|
-
// For action_output (connecting nodes):
|
|
103
|
-
actionName?: string; // Source node ID
|
|
104
|
-
output?: string; // Source output name
|
|
105
|
-
|
|
106
|
-
// For inline values:
|
|
107
|
-
value?: string | number | boolean;
|
|
108
|
-
|
|
109
|
-
// For widget config (persona settings):
|
|
110
|
-
widgetName?: string;
|
|
111
|
-
|
|
112
|
-
// For call_llm named inputs:
|
|
113
|
-
namedInputs?: Array<{ name: string; binding: InputBinding }>;
|
|
114
|
-
}
|
|
115
|
-
\`\`\`
|
|
116
|
-
|
|
117
|
-
### Common Input/Output Names
|
|
118
|
-
|
|
119
|
-
#### trigger (chat_trigger)
|
|
120
|
-
- Outputs: user_query, chat_conversation
|
|
121
|
-
|
|
122
|
-
#### categorizer (chat_categorizer, text_categorizer)
|
|
123
|
-
- Inputs: text (the text to categorize)
|
|
124
|
-
- Outputs: category (the selected category as enum)
|
|
125
|
-
|
|
126
|
-
#### search
|
|
127
|
-
- Inputs: query (search query text)
|
|
128
|
-
- Outputs: search_results (SEARCH_RESULT type)
|
|
129
|
-
|
|
130
|
-
#### call_llm
|
|
131
|
-
- Inputs: query, instructions, named_inputs (for additional context)
|
|
132
|
-
- Outputs: response_with_sources (TEXT_WITH_SOURCES type)
|
|
133
|
-
|
|
134
|
-
#### respond_with_sources
|
|
135
|
-
- Inputs: query, search_results
|
|
136
|
-
- Outputs: response_with_sources (TEXT_WITH_SOURCES type)
|
|
137
|
-
|
|
138
|
-
#### entity_extraction
|
|
139
|
-
- Inputs: text_input, extraction_columns (schema)
|
|
140
|
-
- Outputs: extracted_entities (structured data)
|
|
141
|
-
|
|
142
|
-
#### send_email_agent
|
|
143
|
-
- Inputs: to_email, subject, email_body, sender_email
|
|
144
|
-
- Outputs: email_result
|
|
145
|
-
|
|
146
|
-
#### general_hitl
|
|
147
|
-
- Inputs: summary (what to approve)
|
|
148
|
-
- Outputs: approval_decision (approve/reject enum)
|
|
149
|
-
|
|
150
|
-
### RunIfCondition (Conditional Execution)
|
|
151
|
-
\`\`\`typescript
|
|
152
|
-
interface RunIfCondition {
|
|
153
|
-
sourceAction: string; // Node ID to check
|
|
154
|
-
sourceOutput: string; // Output to compare
|
|
155
|
-
operator: "eq" | "neq"; // Comparison operator
|
|
156
|
-
value: string; // Expected value (category name for categorizers)
|
|
157
|
-
}
|
|
158
|
-
\`\`\`
|
|
159
|
-
|
|
160
|
-
### Category (For Categorizers)
|
|
161
|
-
\`\`\`typescript
|
|
162
|
-
interface Category {
|
|
163
|
-
name: string; // Category identifier (e.g., "billing_inquiry")
|
|
164
|
-
description: string; // When this category applies
|
|
165
|
-
examples?: string[]; // Example user inputs
|
|
166
|
-
}
|
|
167
|
-
\`\`\`
|
|
168
|
-
|
|
169
|
-
### ResultMapping (WORKFLOW_OUTPUT)
|
|
170
|
-
\`\`\`typescript
|
|
171
|
-
interface ResultMapping {
|
|
172
|
-
nodeId: string; // Which node's output to expose
|
|
173
|
-
output: string; // Which output (usually "response_with_sources")
|
|
174
|
-
}
|
|
175
|
-
\`\`\`
|
|
176
|
-
|
|
177
|
-
## Common Patterns
|
|
178
|
-
|
|
179
|
-
### RAG Pattern (Search + Generate)
|
|
180
|
-
\`\`\`typescript
|
|
181
|
-
nodes: [
|
|
182
|
-
{ id: "trigger", actionType: "chat_trigger", displayName: "Trigger" },
|
|
183
|
-
{ id: "search_1", actionType: "search", displayName: "Knowledge Search",
|
|
184
|
-
inputs: { query: { type: "action_output", actionName: "trigger", output: "user_query" } }
|
|
185
|
-
},
|
|
186
|
-
{ id: "respond", actionType: "respond_with_sources", displayName: "Generate Response",
|
|
187
|
-
inputs: {
|
|
188
|
-
query: { type: "action_output", actionName: "trigger", output: "user_query" },
|
|
189
|
-
search_results: { type: "action_output", actionName: "search_1", output: "search_results" }
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
\`\`\`
|
|
194
|
-
|
|
195
|
-
### Intent Routing Pattern
|
|
196
|
-
\`\`\`typescript
|
|
197
|
-
nodes: [
|
|
198
|
-
{ id: "trigger", actionType: "chat_trigger", displayName: "Trigger" },
|
|
199
|
-
{ id: "categorizer", actionType: "chat_categorizer", displayName: "Intent Router",
|
|
200
|
-
inputs: { text: { type: "action_output", actionName: "trigger", output: "user_query" } },
|
|
201
|
-
categories: [
|
|
202
|
-
{ name: "billing", description: "Questions about bills, payments, invoices" },
|
|
203
|
-
{ name: "technical", description: "Technical support issues" },
|
|
204
|
-
{ name: "Fallback", description: "Unclear or general questions" }
|
|
205
|
-
]
|
|
206
|
-
},
|
|
207
|
-
{ id: "billing_response", actionType: "call_llm", displayName: "Billing Handler",
|
|
208
|
-
runIf: { sourceAction: "categorizer", sourceOutput: "category", operator: "eq", value: "billing" },
|
|
209
|
-
inputs: {
|
|
210
|
-
query: { type: "action_output", actionName: "trigger", output: "user_query" },
|
|
211
|
-
instructions: { type: "inline_string", value: "Help with billing inquiries..." }
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
]
|
|
215
|
-
\`\`\`
|
|
216
|
-
|
|
217
|
-
### Email with HITL Pattern
|
|
218
|
-
\`\`\`typescript
|
|
219
|
-
nodes: [
|
|
220
|
-
{ id: "extract_email", actionType: "entity_extraction", displayName: "Extract Email",
|
|
221
|
-
inputs: { text_input: { type: "action_output", actionName: "summarizer", output: "summarized_conversation" } }
|
|
222
|
-
},
|
|
223
|
-
{ id: "approval", actionType: "general_hitl", displayName: "Confirm Email",
|
|
224
|
-
inputs: { summary: { type: "inline_string", value: "Ready to send email. Approve?" } }
|
|
225
|
-
},
|
|
226
|
-
{ id: "send_email", actionType: "send_email_agent", displayName: "Send Email",
|
|
227
|
-
runIf: { sourceAction: "approval", sourceOutput: "approval_decision", operator: "eq", value: "approve" },
|
|
228
|
-
inputs: {
|
|
229
|
-
to_email: { type: "action_output", actionName: "extract_email", output: "extracted_entities" },
|
|
230
|
-
email_body: { type: "action_output", actionName: "body_generator", output: "fixed_response_with_sources" }
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
]
|
|
234
|
-
\`\`\`
|
|
235
|
-
|
|
236
|
-
## Transformation Rules
|
|
237
|
-
|
|
238
|
-
### Rewiring a Connection
|
|
239
|
-
To change where a node gets its input:
|
|
240
|
-
1. Find the node in the nodes array
|
|
241
|
-
2. Update the InputBinding for the target input
|
|
242
|
-
3. Change actionName/output to point to new source
|
|
243
|
-
|
|
244
|
-
Example: Change \`send_email.to_email\` from \`llm_extractor\` to \`entity_extraction\`:
|
|
245
|
-
\`\`\`typescript
|
|
246
|
-
// Before:
|
|
247
|
-
inputs: { to_email: { type: "action_output", actionName: "llm_extractor", output: "response_with_sources" } }
|
|
248
|
-
|
|
249
|
-
// After:
|
|
250
|
-
inputs: { to_email: { type: "action_output", actionName: "entity_extraction", output: "extracted_entities" } }
|
|
251
|
-
\`\`\`
|
|
252
|
-
|
|
253
|
-
### Adding HITL Before an Action
|
|
254
|
-
1. Create a new general_hitl node with unique ID
|
|
255
|
-
2. Add runIf condition to the target node checking HITL approval
|
|
256
|
-
3. Ensure HITL summary describes what's being approved
|
|
257
|
-
|
|
258
|
-
### Removing a Node
|
|
259
|
-
1. Remove the node from the nodes array
|
|
260
|
-
2. Update any nodes that referenced it to use alternative source
|
|
261
|
-
3. Remove from resultMappings if present
|
|
262
|
-
|
|
263
|
-
### Adding a Category
|
|
264
|
-
1. Find the categorizer node
|
|
265
|
-
2. Add to the categories array
|
|
266
|
-
3. Add a handler node with runIf condition for the new category
|
|
267
|
-
|
|
268
|
-
${STRUCTURAL_RULES_FOR_LLM}
|
|
269
|
-
`;
|
|
270
|
-
/**
|
|
271
|
-
* Get the complete schema including structural validation rules.
|
|
272
|
-
* Use this for LLM context when generating or modifying workflows.
|
|
273
|
-
*/
|
|
274
|
-
export function getCompleteSchemaForLLM() {
|
|
275
|
-
return WORKFLOW_SCHEMA_FOR_LLM;
|
|
276
|
-
}
|
|
277
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
278
|
-
// DECOMPILER: workflow_def JSON → WorkflowSpec
|
|
279
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
280
|
-
/**
|
|
281
|
-
* Decompile a raw workflow_def JSON back to a typed WorkflowSpec.
|
|
282
|
-
* This allows LLM to work with clean typed objects instead of raw JSON.
|
|
283
|
-
*/
|
|
284
|
-
export function decompileWorkflow(workflowDef, personaType = "chat") {
|
|
285
|
-
const actions = (workflowDef.actions || []);
|
|
286
|
-
const results = (workflowDef.results || {});
|
|
287
|
-
const enumTypes = (workflowDef.enumTypes || []);
|
|
288
|
-
// Build enum lookup for category extraction
|
|
289
|
-
// DEFENSIVE: Handle multiple API formats for enumType.name
|
|
290
|
-
const enumLookup = new Map();
|
|
291
|
-
for (const et of enumTypes) {
|
|
292
|
-
// Extract name from multiple possible formats:
|
|
293
|
-
// 1. { name: "string" } - simple format
|
|
294
|
-
// 2. { name: { name: "string" } } - nested format
|
|
295
|
-
// 3. { name: { name: { name: "string" } } } - deeply nested API format
|
|
296
|
-
let name;
|
|
297
|
-
const rawName = et.name;
|
|
298
|
-
if (typeof rawName === "string") {
|
|
299
|
-
name = rawName;
|
|
300
|
-
}
|
|
301
|
-
else if (rawName && typeof rawName === "object") {
|
|
302
|
-
const nameObj = rawName;
|
|
303
|
-
if (typeof nameObj.name === "string") {
|
|
304
|
-
name = nameObj.name;
|
|
305
|
-
}
|
|
306
|
-
else if (nameObj.name && typeof nameObj.name === "object") {
|
|
307
|
-
const nestedName = nameObj.name;
|
|
308
|
-
if (typeof nestedName.name === "string") {
|
|
309
|
-
name = nestedName.name;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
// DEFENSIVE: Check both options (API format) and values (compiled format)
|
|
314
|
-
const optionsArray = (et.options || et.values || []);
|
|
315
|
-
if (name && optionsArray.length > 0) {
|
|
316
|
-
enumLookup.set(name, optionsArray
|
|
317
|
-
.filter(o => typeof o.name === "string")
|
|
318
|
-
.map(o => ({
|
|
319
|
-
name: o.name,
|
|
320
|
-
description: o.description || "",
|
|
321
|
-
examples: o.examples,
|
|
322
|
-
})));
|
|
323
|
-
}
|
|
324
|
-
else if (!name && optionsArray.length > 0) {
|
|
325
|
-
console.warn(`[workflow-transformer] EnumType has categories but name could not be extracted:`, JSON.stringify(rawName).slice(0, 100));
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
// Decompile each action to a Node
|
|
329
|
-
const nodes = actions.map(action => decompileAction(action, enumLookup));
|
|
330
|
-
// Decompile result mappings
|
|
331
|
-
const resultMappings = Object.values(results).map(r => ({
|
|
332
|
-
nodeId: r.actionName,
|
|
333
|
-
output: r.outputName,
|
|
334
|
-
}));
|
|
335
|
-
return {
|
|
336
|
-
name: extractWorkflowName(workflowDef),
|
|
337
|
-
description: workflowDef.description || "",
|
|
338
|
-
personaType,
|
|
339
|
-
nodes,
|
|
340
|
-
resultMappings,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Extract workflow name/persona ID from workflowName namespace.
|
|
345
|
-
* DEFENSIVE: Handles multiple namespace formats:
|
|
346
|
-
* - ["ema", "personas", "<id>"] - standard 3-part
|
|
347
|
-
* - ["ema", "tenant", "personas", "<id>"] - 4-part with tenant
|
|
348
|
-
* - Last segment is typically the persona ID
|
|
349
|
-
*/
|
|
350
|
-
function extractWorkflowName(workflowDef) {
|
|
351
|
-
const wn = workflowDef.workflowName;
|
|
352
|
-
// Try to extract from namespaces array
|
|
353
|
-
if (wn?.name?.namespaces && Array.isArray(wn.name.namespaces)) {
|
|
354
|
-
const namespaces = wn.name.namespaces;
|
|
355
|
-
// Standard format: ["ema", "personas", "<persona_id>"]
|
|
356
|
-
if (namespaces.length >= 3 && namespaces[1] === "personas") {
|
|
357
|
-
return namespaces[2];
|
|
358
|
-
}
|
|
359
|
-
// Alternative format: ["ema", "workflows", "<id>"]
|
|
360
|
-
if (namespaces.length >= 3 && namespaces[1] === "workflows") {
|
|
361
|
-
return namespaces[2];
|
|
362
|
-
}
|
|
363
|
-
// Extended format with tenant: ["ema", "<tenant>", "personas", "<persona_id>"]
|
|
364
|
-
if (namespaces.length >= 4 && namespaces[2] === "personas") {
|
|
365
|
-
return namespaces[3];
|
|
366
|
-
}
|
|
367
|
-
// Fallback: last non-empty segment (best guess)
|
|
368
|
-
for (let i = namespaces.length - 1; i >= 0; i--) {
|
|
369
|
-
if (typeof namespaces[i] === "string" && namespaces[i].length > 0) {
|
|
370
|
-
console.warn(`[workflow-transformer] Unexpected namespace format: ${namespaces.join("/")} - using last segment`);
|
|
371
|
-
return namespaces[i];
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
// Try wn.name.name directly
|
|
376
|
-
if (wn?.name?.name && typeof wn.name.name === "string") {
|
|
377
|
-
return wn.name.name;
|
|
378
|
-
}
|
|
379
|
-
return workflowDef.displayName || "workflow";
|
|
380
|
-
}
|
|
381
|
-
function decompileAction(action, enumLookup) {
|
|
382
|
-
// DEFENSIVE: Handle multiple action definition formats
|
|
383
|
-
// Format 1: { action: { name: { name: "search" } } }
|
|
384
|
-
// Format 2: { actionType: "search" }
|
|
385
|
-
// Format 3: { action: "search" } (string directly)
|
|
386
|
-
let actionName = "unknown";
|
|
387
|
-
const actionDef = action.action;
|
|
388
|
-
if (typeof actionDef === "string") {
|
|
389
|
-
actionName = actionDef;
|
|
390
|
-
}
|
|
391
|
-
else if (actionDef && typeof actionDef === "object") {
|
|
392
|
-
const actionObj = actionDef;
|
|
393
|
-
if (typeof actionObj.name === "string") {
|
|
394
|
-
actionName = actionObj.name;
|
|
395
|
-
}
|
|
396
|
-
else if (actionObj.name && typeof actionObj.name === "object") {
|
|
397
|
-
const nameObj = actionObj.name;
|
|
398
|
-
if (typeof nameObj.name === "string") {
|
|
399
|
-
actionName = nameObj.name;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// Fallback to actionType field if action.name extraction failed
|
|
404
|
-
if (actionName === "unknown" && typeof action.actionType === "string") {
|
|
405
|
-
actionName = action.actionType;
|
|
406
|
-
}
|
|
407
|
-
if (actionName === "unknown") {
|
|
408
|
-
console.warn(`[workflow-transformer] Could not extract action type for node "${action.name}". ` +
|
|
409
|
-
`action field: ${JSON.stringify(action.action).slice(0, 100)}`);
|
|
410
|
-
}
|
|
411
|
-
const displaySettings = action.displaySettings;
|
|
412
|
-
const inputs = (action.inputs || {});
|
|
413
|
-
const runIf = action.runIf;
|
|
414
|
-
const typeArgs = action.typeArguments;
|
|
415
|
-
// Map raw action name to ActionType
|
|
416
|
-
const actionType = mapToActionType(actionName);
|
|
417
|
-
// Decompile inputs
|
|
418
|
-
const decompInputs = {};
|
|
419
|
-
for (const [key, value] of Object.entries(inputs)) {
|
|
420
|
-
const binding = decompileBinding(value);
|
|
421
|
-
if (binding) {
|
|
422
|
-
decompInputs[key] = binding;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
// Decompile runIf
|
|
426
|
-
let decompRunIf;
|
|
427
|
-
if (runIf) {
|
|
428
|
-
decompRunIf = decompileRunIf(runIf);
|
|
429
|
-
}
|
|
430
|
-
// Get categories for categorizers
|
|
431
|
-
let categories;
|
|
432
|
-
if (actionType.includes("categorizer") && typeArgs?.categories?.enumType?.name?.name) {
|
|
433
|
-
categories = enumLookup.get(typeArgs.categories.enumType.name.name);
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
id: action.name,
|
|
437
|
-
actionType,
|
|
438
|
-
displayName: displaySettings?.displayName || action.name,
|
|
439
|
-
description: displaySettings?.description,
|
|
440
|
-
inputs: Object.keys(decompInputs).length > 0 ? decompInputs : undefined,
|
|
441
|
-
runIf: decompRunIf,
|
|
442
|
-
categories,
|
|
443
|
-
disableHitl: action.disableHumanInteraction,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
function mapToActionType(rawName) {
|
|
447
|
-
const mapping = {
|
|
448
|
-
chat_trigger: "chat_trigger",
|
|
449
|
-
voice_trigger: "voice_trigger",
|
|
450
|
-
document_trigger: "document_trigger",
|
|
451
|
-
chat_categorizer: "chat_categorizer",
|
|
452
|
-
text_categorizer: "text_categorizer",
|
|
453
|
-
conversation_to_search_query: "conversation_to_search_query",
|
|
454
|
-
search: "search",
|
|
455
|
-
live_web_search: "live_web_search",
|
|
456
|
-
combine_search_results: "combine_search_results",
|
|
457
|
-
respond_with_sources: "respond_with_sources",
|
|
458
|
-
call_llm: "call_llm",
|
|
459
|
-
fixed_response: "fixed_response",
|
|
460
|
-
entity_extraction: "entity_extraction",
|
|
461
|
-
entity_extraction_with_documents: "entity_extraction",
|
|
462
|
-
external_action_caller: "external_action_caller",
|
|
463
|
-
send_email_agent: "send_email_agent",
|
|
464
|
-
general_hitl: "general_hitl",
|
|
465
|
-
hitl: "general_hitl",
|
|
466
|
-
response_validator: "response_validator",
|
|
467
|
-
// Additional aliases/mappings
|
|
468
|
-
dashboard_trigger: "document_trigger",
|
|
469
|
-
};
|
|
470
|
-
const result = mapping[rawName];
|
|
471
|
-
if (!result) {
|
|
472
|
-
// WARN but preserve the raw name - don't silently convert to call_llm
|
|
473
|
-
// This allows the caller to see unknown types and handle appropriately
|
|
474
|
-
// Unknown types are cast to ActionType - compilation will succeed but
|
|
475
|
-
// the workflow may need additional handling downstream
|
|
476
|
-
console.warn(`[workflow-transformer] Unknown action type: "${rawName}" - preserving as-is. May need to add to mapping.`);
|
|
477
|
-
return rawName;
|
|
478
|
-
}
|
|
479
|
-
return result;
|
|
480
|
-
}
|
|
481
|
-
function decompileBinding(value) {
|
|
482
|
-
const v = value;
|
|
483
|
-
// Action output binding
|
|
484
|
-
if (v.actionOutput) {
|
|
485
|
-
const ao = v.actionOutput;
|
|
486
|
-
return {
|
|
487
|
-
type: "action_output",
|
|
488
|
-
actionName: ao.actionName,
|
|
489
|
-
output: ao.output,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
// Widget config binding
|
|
493
|
-
if (v.widgetConfig) {
|
|
494
|
-
const wc = v.widgetConfig;
|
|
495
|
-
return {
|
|
496
|
-
type: "widget_config",
|
|
497
|
-
widgetName: wc.widgetName,
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
// Inline value binding
|
|
501
|
-
if (v.inline) {
|
|
502
|
-
const inline = v.inline;
|
|
503
|
-
if (inline.wellKnown?.stringValue !== undefined) {
|
|
504
|
-
return {
|
|
505
|
-
type: "inline_string",
|
|
506
|
-
value: inline.wellKnown.stringValue,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
if (inline.wellKnown?.boolValue !== undefined) {
|
|
510
|
-
return {
|
|
511
|
-
type: "inline_bool",
|
|
512
|
-
value: inline.wellKnown.boolValue,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
if (inline.wellKnown?.int64Value !== undefined) {
|
|
516
|
-
return {
|
|
517
|
-
type: "inline_number",
|
|
518
|
-
value: parseInt(inline.wellKnown.int64Value, 10),
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
if (inline.enumValue) {
|
|
522
|
-
return {
|
|
523
|
-
type: "inline_string",
|
|
524
|
-
value: inline.enumValue,
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
// Multi-binding (named inputs)
|
|
529
|
-
if (v.multiBinding) {
|
|
530
|
-
const mb = v.multiBinding;
|
|
531
|
-
const namedInputs = [];
|
|
532
|
-
for (const elem of mb.elements || []) {
|
|
533
|
-
if (elem.namedBinding?.name && elem.namedBinding.value) {
|
|
534
|
-
const binding = decompileBinding(elem.namedBinding.value);
|
|
535
|
-
if (binding) {
|
|
536
|
-
namedInputs.push({ name: elem.namedBinding.name, binding });
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
if (namedInputs.length > 0) {
|
|
541
|
-
return {
|
|
542
|
-
type: "action_output", // Container type
|
|
543
|
-
namedInputs,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
function decompileRunIf(runIf) {
|
|
550
|
-
const lhs = runIf.lhs;
|
|
551
|
-
const rhs = runIf.rhs;
|
|
552
|
-
const operator = runIf.operator;
|
|
553
|
-
if (!lhs?.actionOutput || !rhs?.inline?.enumValue)
|
|
554
|
-
return undefined;
|
|
555
|
-
const opMap = {
|
|
556
|
-
1: "eq",
|
|
557
|
-
2: "neq",
|
|
558
|
-
3: "gt",
|
|
559
|
-
4: "lt",
|
|
560
|
-
5: "gte",
|
|
561
|
-
6: "lte",
|
|
562
|
-
};
|
|
563
|
-
return {
|
|
564
|
-
sourceAction: lhs.actionOutput.actionName || "",
|
|
565
|
-
sourceOutput: lhs.actionOutput.output || "",
|
|
566
|
-
operator: opMap[operator] || "eq",
|
|
567
|
-
value: rhs.inline.enumValue,
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* Transform a workflow using an LLM-generated spec modification.
|
|
572
|
-
*
|
|
573
|
-
* The expected workflow:
|
|
574
|
-
* 1. Decompile existing workflow to WorkflowSpec
|
|
575
|
-
* 2. LLM reads spec + schema + user request
|
|
576
|
-
* 3. LLM outputs modified WorkflowSpec (or a diff/patch)
|
|
577
|
-
* 4. This function compiles back to workflow_def
|
|
578
|
-
*
|
|
579
|
-
* @param originalDef - The original workflow_def JSON
|
|
580
|
-
* @param modifiedSpec - The LLM-modified WorkflowSpec
|
|
581
|
-
* @returns TransformResult with compiled workflow
|
|
582
|
-
*/
|
|
583
|
-
/**
|
|
584
|
-
* Validate workflow namespace structure.
|
|
585
|
-
* The Ema API requires a specific namespace format for deployment.
|
|
586
|
-
* Invalid namespaces cause SILENT deployment failures (returns 200 OK but nothing changes).
|
|
587
|
-
*/
|
|
588
|
-
export function validateWorkflowNamespace(ns) {
|
|
589
|
-
if (!ns) {
|
|
590
|
-
return { valid: false, error: "Workflow namespace (workflowName) is missing" };
|
|
591
|
-
}
|
|
592
|
-
if (typeof ns !== "object") {
|
|
593
|
-
return { valid: false, error: `Workflow namespace must be an object, got ${typeof ns}` };
|
|
594
|
-
}
|
|
595
|
-
const namespace = ns;
|
|
596
|
-
if (!Array.isArray(namespace.namespaces)) {
|
|
597
|
-
return { valid: false, error: "Workflow namespace.namespaces must be an array" };
|
|
598
|
-
}
|
|
599
|
-
if (typeof namespace.name !== "string" || namespace.name.length === 0) {
|
|
600
|
-
return { valid: false, error: "Workflow namespace.name must be a non-empty string" };
|
|
601
|
-
}
|
|
602
|
-
return { valid: true };
|
|
603
|
-
}
|
|
604
|
-
export function transformWorkflow(originalDef, modifiedSpec) {
|
|
605
|
-
const changes = [];
|
|
606
|
-
const errors = [];
|
|
607
|
-
const warnings = [];
|
|
608
|
-
try {
|
|
609
|
-
// Validate original workflow has valid namespace (required for deployment)
|
|
610
|
-
// This is a WARNING not an error - transformation can proceed but deployment may fail
|
|
611
|
-
const namespaceValidation = validateWorkflowNamespace(originalDef.workflowName);
|
|
612
|
-
if (!namespaceValidation.valid) {
|
|
613
|
-
warnings.push(`WARNING: ${namespaceValidation.error}. Deployment may fail silently without a valid namespace.`);
|
|
614
|
-
console.warn(`[workflow-transformer] ${namespaceValidation.error}`);
|
|
615
|
-
}
|
|
616
|
-
// Compile the modified spec
|
|
617
|
-
const compiled = compileWorkflow(modifiedSpec);
|
|
618
|
-
// Preserve workflowName from original (important for deployment)
|
|
619
|
-
// If original has no namespace, the compiled result will also lack one (which is fine for local use)
|
|
620
|
-
if (originalDef.workflowName) {
|
|
621
|
-
compiled.workflow_def.workflowName = originalDef.workflowName;
|
|
622
|
-
}
|
|
623
|
-
// Detect what changed
|
|
624
|
-
const originalSpec = decompileWorkflow(originalDef, modifiedSpec.personaType);
|
|
625
|
-
changes.push(...detectChanges(originalSpec, modifiedSpec));
|
|
626
|
-
return {
|
|
627
|
-
success: true,
|
|
628
|
-
spec: modifiedSpec,
|
|
629
|
-
workflow_def: compiled.workflow_def,
|
|
630
|
-
changes,
|
|
631
|
-
errors,
|
|
632
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
catch (e) {
|
|
636
|
-
errors.push(`Compilation failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
637
|
-
return {
|
|
638
|
-
success: false,
|
|
639
|
-
spec: modifiedSpec,
|
|
640
|
-
workflow_def: originalDef,
|
|
641
|
-
changes,
|
|
642
|
-
errors,
|
|
643
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
function detectChanges(original, modified) {
|
|
648
|
-
const changes = [];
|
|
649
|
-
const originalIds = new Set(original.nodes.map(n => n.id));
|
|
650
|
-
const modifiedIds = new Set(modified.nodes.map(n => n.id));
|
|
651
|
-
// Added nodes
|
|
652
|
-
for (const id of modifiedIds) {
|
|
653
|
-
if (!originalIds.has(id)) {
|
|
654
|
-
changes.push(`Added node: ${id}`);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
// Removed nodes
|
|
658
|
-
for (const id of originalIds) {
|
|
659
|
-
if (!modifiedIds.has(id)) {
|
|
660
|
-
changes.push(`Removed node: ${id}`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Modified nodes (simplified check)
|
|
664
|
-
for (const modNode of modified.nodes) {
|
|
665
|
-
const origNode = original.nodes.find(n => n.id === modNode.id);
|
|
666
|
-
if (origNode) {
|
|
667
|
-
// Check if inputs changed
|
|
668
|
-
const origInputs = JSON.stringify(origNode.inputs || {});
|
|
669
|
-
const modInputs = JSON.stringify(modNode.inputs || {});
|
|
670
|
-
if (origInputs !== modInputs) {
|
|
671
|
-
changes.push(`Modified ${modNode.id} inputs`);
|
|
672
|
-
}
|
|
673
|
-
// Check if runIf changed
|
|
674
|
-
if (JSON.stringify(origNode.runIf) !== JSON.stringify(modNode.runIf)) {
|
|
675
|
-
changes.push(`Modified ${modNode.id} runIf condition`);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return changes;
|
|
680
|
-
}
|
|
681
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
682
|
-
// HELPERS FOR LLM CONTEXT
|
|
683
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
684
|
-
/**
|
|
685
|
-
* Generate a summary of a workflow for LLM context.
|
|
686
|
-
*/
|
|
687
|
-
export function summarizeSpec(spec) {
|
|
688
|
-
const lines = [
|
|
689
|
-
`## Workflow: ${spec.name}`,
|
|
690
|
-
`Type: ${spec.personaType}`,
|
|
691
|
-
`Description: ${spec.description}`,
|
|
692
|
-
``,
|
|
693
|
-
`### Nodes (${spec.nodes.length})`,
|
|
694
|
-
];
|
|
695
|
-
for (const node of spec.nodes) {
|
|
696
|
-
const inputs = node.inputs ? Object.keys(node.inputs).join(", ") : "none";
|
|
697
|
-
const condition = node.runIf
|
|
698
|
-
? ` [if ${node.runIf.sourceAction}.${node.runIf.sourceOutput} ${node.runIf.operator} "${node.runIf.value}"]`
|
|
699
|
-
: "";
|
|
700
|
-
lines.push(`- ${node.id} (${node.actionType}): ${node.displayName}${condition}`);
|
|
701
|
-
lines.push(` Inputs: ${inputs}`);
|
|
702
|
-
}
|
|
703
|
-
lines.push(``, `### Output Mappings`);
|
|
704
|
-
for (const rm of spec.resultMappings) {
|
|
705
|
-
lines.push(`- ${rm.nodeId}.${rm.output}`);
|
|
706
|
-
}
|
|
707
|
-
return lines.join("\n");
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Create an LLM prompt for workflow modification.
|
|
711
|
-
* This should be used by the MCP handler to construct the LLM call.
|
|
712
|
-
*/
|
|
713
|
-
export function createModificationPrompt(currentSpec, userRequest) {
|
|
714
|
-
return `
|
|
715
|
-
You are modifying an Ema workflow. Your output should be a valid WorkflowSpec JSON.
|
|
716
|
-
|
|
717
|
-
## Current Workflow
|
|
718
|
-
${summarizeSpec(currentSpec)}
|
|
719
|
-
|
|
720
|
-
## Current Spec (JSON)
|
|
721
|
-
\`\`\`json
|
|
722
|
-
${JSON.stringify(currentSpec, null, 2)}
|
|
723
|
-
\`\`\`
|
|
724
|
-
|
|
725
|
-
## Schema Reference
|
|
726
|
-
${WORKFLOW_SCHEMA_FOR_LLM}
|
|
727
|
-
|
|
728
|
-
## User Request
|
|
729
|
-
${userRequest}
|
|
730
|
-
|
|
731
|
-
## Instructions
|
|
732
|
-
1. Analyze the user request
|
|
733
|
-
2. Determine what nodes/connections need to change
|
|
734
|
-
3. Output the COMPLETE modified WorkflowSpec as valid JSON
|
|
735
|
-
4. Preserve all unchanged nodes exactly as they are
|
|
736
|
-
5. Only modify what's necessary for the request
|
|
737
|
-
|
|
738
|
-
## Output
|
|
739
|
-
Return ONLY the modified WorkflowSpec JSON, no explanation needed:
|
|
740
|
-
\`\`\`json
|
|
741
|
-
`;
|
|
742
|
-
}
|