@ema.co/mcp-toolkit 1.4.3 → 1.5.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 +647 -85
- package/dist/mcp/tools-consolidated.js +73 -42
- package/dist/sdk/index.js +4 -0
- package/dist/sdk/knowledge.js +934 -0
- package/dist/sdk/workflow-execution-analyzer.js +412 -0
- package/dist/sdk/workflow-fixer.js +272 -0
- package/dist/sdk/workflow-transformer.js +600 -0
- package/docs/llm-native-workflow-design.md +252 -0
- package/package.json +1 -1
|
@@ -0,0 +1,600 @@
|
|
|
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
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// SCHEMA DOCUMENTATION FOR LLM CONTEXT
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
/**
|
|
21
|
+
* This schema is designed to be included in LLM context.
|
|
22
|
+
* The LLM reads this, understands the structure, and outputs valid specs.
|
|
23
|
+
*/
|
|
24
|
+
export const WORKFLOW_SCHEMA_FOR_LLM = `
|
|
25
|
+
# Ema Workflow Schema
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
Workflows are defined as a WorkflowSpec object with nodes and connections.
|
|
29
|
+
Each node has typed inputs/outputs. Connections are defined via InputBinding.
|
|
30
|
+
|
|
31
|
+
## Core Types
|
|
32
|
+
|
|
33
|
+
### WorkflowSpec
|
|
34
|
+
\`\`\`typescript
|
|
35
|
+
interface WorkflowSpec {
|
|
36
|
+
name: string; // Workflow identifier
|
|
37
|
+
description: string; // What this workflow does
|
|
38
|
+
personaType: "voice" | "chat" | "dashboard";
|
|
39
|
+
nodes: Node[]; // All workflow nodes
|
|
40
|
+
resultMappings: ResultMapping[]; // Which outputs go to WORKFLOW_OUTPUT
|
|
41
|
+
}
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
### Node
|
|
45
|
+
\`\`\`typescript
|
|
46
|
+
interface Node {
|
|
47
|
+
id: string; // Unique node identifier (e.g., "search_1", "categorizer_main")
|
|
48
|
+
actionType: ActionType; // Node type (see ActionType below)
|
|
49
|
+
displayName: string; // Human-readable name
|
|
50
|
+
description?: string; // What this node does
|
|
51
|
+
inputs?: Record<string, InputBinding>; // Input connections
|
|
52
|
+
runIf?: RunIfCondition; // Conditional execution
|
|
53
|
+
categories?: Category[]; // For categorizer nodes only
|
|
54
|
+
tools?: Tool[]; // For custom_agent nodes
|
|
55
|
+
disableHitl?: boolean; // Skip human-in-the-loop
|
|
56
|
+
}
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
### ActionType (Available Node Types)
|
|
60
|
+
\`\`\`typescript
|
|
61
|
+
type ActionType =
|
|
62
|
+
// Triggers (entry points)
|
|
63
|
+
| "chat_trigger" // Chat/voice entry point
|
|
64
|
+
| "document_trigger" // Document upload entry point
|
|
65
|
+
|
|
66
|
+
// Routing (categorization)
|
|
67
|
+
| "chat_categorizer" // Categorizes chat conversation
|
|
68
|
+
| "text_categorizer" // Categorizes text
|
|
69
|
+
|
|
70
|
+
// Search (retrieval)
|
|
71
|
+
| "search" // Knowledge base search
|
|
72
|
+
| "live_web_search" // Real-time web search
|
|
73
|
+
| "combine_search_results" // Merge multiple searches
|
|
74
|
+
| "conversation_to_search_query" // Summarize conversation for search
|
|
75
|
+
|
|
76
|
+
// Generation (LLM responses)
|
|
77
|
+
| "respond_with_sources" // RAG response with citations
|
|
78
|
+
| "call_llm" // Custom LLM generation
|
|
79
|
+
| "fixed_response" // Template response (no LLM)
|
|
80
|
+
|
|
81
|
+
// Entity extraction
|
|
82
|
+
| "entity_extraction" // Extract structured data from text
|
|
83
|
+
|
|
84
|
+
// External actions
|
|
85
|
+
| "external_action_caller" // Call APIs (Salesforce, ServiceNow, etc.)
|
|
86
|
+
| "send_email_agent" // Send email
|
|
87
|
+
|
|
88
|
+
// Human-in-the-loop
|
|
89
|
+
| "general_hitl" // Human approval step
|
|
90
|
+
|
|
91
|
+
// Validation
|
|
92
|
+
| "response_validator"; // Validate LLM output
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
### InputBinding (How Nodes Connect)
|
|
96
|
+
\`\`\`typescript
|
|
97
|
+
interface InputBinding {
|
|
98
|
+
type: "action_output" | "inline_string" | "inline_number" | "inline_bool" | "widget_config" | "llm_inferred";
|
|
99
|
+
|
|
100
|
+
// For action_output (connecting nodes):
|
|
101
|
+
actionName?: string; // Source node ID
|
|
102
|
+
output?: string; // Source output name
|
|
103
|
+
|
|
104
|
+
// For inline values:
|
|
105
|
+
value?: string | number | boolean;
|
|
106
|
+
|
|
107
|
+
// For widget config (persona settings):
|
|
108
|
+
widgetName?: string;
|
|
109
|
+
|
|
110
|
+
// For call_llm named inputs:
|
|
111
|
+
namedInputs?: Array<{ name: string; binding: InputBinding }>;
|
|
112
|
+
}
|
|
113
|
+
\`\`\`
|
|
114
|
+
|
|
115
|
+
### Common Input/Output Names
|
|
116
|
+
|
|
117
|
+
#### trigger (chat_trigger)
|
|
118
|
+
- Outputs: user_query, chat_conversation
|
|
119
|
+
|
|
120
|
+
#### categorizer (chat_categorizer, text_categorizer)
|
|
121
|
+
- Inputs: text (the text to categorize)
|
|
122
|
+
- Outputs: category (the selected category as enum)
|
|
123
|
+
|
|
124
|
+
#### search
|
|
125
|
+
- Inputs: query (search query text)
|
|
126
|
+
- Outputs: search_results (SEARCH_RESULT type)
|
|
127
|
+
|
|
128
|
+
#### call_llm
|
|
129
|
+
- Inputs: query, instructions, named_inputs (for additional context)
|
|
130
|
+
- Outputs: response_with_sources (TEXT_WITH_SOURCES type)
|
|
131
|
+
|
|
132
|
+
#### respond_with_sources
|
|
133
|
+
- Inputs: query, search_results
|
|
134
|
+
- Outputs: response_with_sources (TEXT_WITH_SOURCES type)
|
|
135
|
+
|
|
136
|
+
#### entity_extraction
|
|
137
|
+
- Inputs: text_input, extraction_columns (schema)
|
|
138
|
+
- Outputs: extracted_entities (structured data)
|
|
139
|
+
|
|
140
|
+
#### send_email_agent
|
|
141
|
+
- Inputs: to_email, subject, email_body, sender_email
|
|
142
|
+
- Outputs: email_result
|
|
143
|
+
|
|
144
|
+
#### general_hitl
|
|
145
|
+
- Inputs: summary (what to approve)
|
|
146
|
+
- Outputs: approval_decision (approve/reject enum)
|
|
147
|
+
|
|
148
|
+
### RunIfCondition (Conditional Execution)
|
|
149
|
+
\`\`\`typescript
|
|
150
|
+
interface RunIfCondition {
|
|
151
|
+
sourceAction: string; // Node ID to check
|
|
152
|
+
sourceOutput: string; // Output to compare
|
|
153
|
+
operator: "eq" | "neq"; // Comparison operator
|
|
154
|
+
value: string; // Expected value (category name for categorizers)
|
|
155
|
+
}
|
|
156
|
+
\`\`\`
|
|
157
|
+
|
|
158
|
+
### Category (For Categorizers)
|
|
159
|
+
\`\`\`typescript
|
|
160
|
+
interface Category {
|
|
161
|
+
name: string; // Category identifier (e.g., "billing_inquiry")
|
|
162
|
+
description: string; // When this category applies
|
|
163
|
+
examples?: string[]; // Example user inputs
|
|
164
|
+
}
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
### ResultMapping (WORKFLOW_OUTPUT)
|
|
168
|
+
\`\`\`typescript
|
|
169
|
+
interface ResultMapping {
|
|
170
|
+
nodeId: string; // Which node's output to expose
|
|
171
|
+
output: string; // Which output (usually "response_with_sources")
|
|
172
|
+
}
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
## Common Patterns
|
|
176
|
+
|
|
177
|
+
### RAG Pattern (Search + Generate)
|
|
178
|
+
\`\`\`typescript
|
|
179
|
+
nodes: [
|
|
180
|
+
{ id: "trigger", actionType: "chat_trigger", displayName: "Trigger" },
|
|
181
|
+
{ id: "search_1", actionType: "search", displayName: "Knowledge Search",
|
|
182
|
+
inputs: { query: { type: "action_output", actionName: "trigger", output: "user_query" } }
|
|
183
|
+
},
|
|
184
|
+
{ id: "respond", actionType: "respond_with_sources", displayName: "Generate Response",
|
|
185
|
+
inputs: {
|
|
186
|
+
query: { type: "action_output", actionName: "trigger", output: "user_query" },
|
|
187
|
+
search_results: { type: "action_output", actionName: "search_1", output: "search_results" }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
### Intent Routing Pattern
|
|
194
|
+
\`\`\`typescript
|
|
195
|
+
nodes: [
|
|
196
|
+
{ id: "trigger", actionType: "chat_trigger", displayName: "Trigger" },
|
|
197
|
+
{ id: "categorizer", actionType: "chat_categorizer", displayName: "Intent Router",
|
|
198
|
+
inputs: { text: { type: "action_output", actionName: "trigger", output: "user_query" } },
|
|
199
|
+
categories: [
|
|
200
|
+
{ name: "billing", description: "Questions about bills, payments, invoices" },
|
|
201
|
+
{ name: "technical", description: "Technical support issues" },
|
|
202
|
+
{ name: "Fallback", description: "Unclear or general questions" }
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
{ id: "billing_response", actionType: "call_llm", displayName: "Billing Handler",
|
|
206
|
+
runIf: { sourceAction: "categorizer", sourceOutput: "category", operator: "eq", value: "billing" },
|
|
207
|
+
inputs: {
|
|
208
|
+
query: { type: "action_output", actionName: "trigger", output: "user_query" },
|
|
209
|
+
instructions: { type: "inline_string", value: "Help with billing inquiries..." }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
### Email with HITL Pattern
|
|
216
|
+
\`\`\`typescript
|
|
217
|
+
nodes: [
|
|
218
|
+
{ id: "extract_email", actionType: "entity_extraction", displayName: "Extract Email",
|
|
219
|
+
inputs: { text_input: { type: "action_output", actionName: "summarizer", output: "summarized_conversation" } }
|
|
220
|
+
},
|
|
221
|
+
{ id: "approval", actionType: "general_hitl", displayName: "Confirm Email",
|
|
222
|
+
inputs: { summary: { type: "inline_string", value: "Ready to send email. Approve?" } }
|
|
223
|
+
},
|
|
224
|
+
{ id: "send_email", actionType: "send_email_agent", displayName: "Send Email",
|
|
225
|
+
runIf: { sourceAction: "approval", sourceOutput: "approval_decision", operator: "eq", value: "approve" },
|
|
226
|
+
inputs: {
|
|
227
|
+
to_email: { type: "action_output", actionName: "extract_email", output: "extracted_entities" },
|
|
228
|
+
email_body: { type: "action_output", actionName: "body_generator", output: "fixed_response_with_sources" }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
\`\`\`
|
|
233
|
+
|
|
234
|
+
## Transformation Rules
|
|
235
|
+
|
|
236
|
+
### Rewiring a Connection
|
|
237
|
+
To change where a node gets its input:
|
|
238
|
+
1. Find the node in the nodes array
|
|
239
|
+
2. Update the InputBinding for the target input
|
|
240
|
+
3. Change actionName/output to point to new source
|
|
241
|
+
|
|
242
|
+
Example: Change \`send_email.to_email\` from \`llm_extractor\` to \`entity_extraction\`:
|
|
243
|
+
\`\`\`typescript
|
|
244
|
+
// Before:
|
|
245
|
+
inputs: { to_email: { type: "action_output", actionName: "llm_extractor", output: "response_with_sources" } }
|
|
246
|
+
|
|
247
|
+
// After:
|
|
248
|
+
inputs: { to_email: { type: "action_output", actionName: "entity_extraction", output: "extracted_entities" } }
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
### Adding HITL Before an Action
|
|
252
|
+
1. Create a new general_hitl node with unique ID
|
|
253
|
+
2. Add runIf condition to the target node checking HITL approval
|
|
254
|
+
3. Ensure HITL summary describes what's being approved
|
|
255
|
+
|
|
256
|
+
### Removing a Node
|
|
257
|
+
1. Remove the node from the nodes array
|
|
258
|
+
2. Update any nodes that referenced it to use alternative source
|
|
259
|
+
3. Remove from resultMappings if present
|
|
260
|
+
|
|
261
|
+
### Adding a Category
|
|
262
|
+
1. Find the categorizer node
|
|
263
|
+
2. Add to the categories array
|
|
264
|
+
3. Add a handler node with runIf condition for the new category
|
|
265
|
+
`;
|
|
266
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
267
|
+
// DECOMPILER: workflow_def JSON → WorkflowSpec
|
|
268
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
269
|
+
/**
|
|
270
|
+
* Decompile a raw workflow_def JSON back to a typed WorkflowSpec.
|
|
271
|
+
* This allows LLM to work with clean typed objects instead of raw JSON.
|
|
272
|
+
*/
|
|
273
|
+
export function decompileWorkflow(workflowDef, personaType = "chat") {
|
|
274
|
+
const actions = (workflowDef.actions || []);
|
|
275
|
+
const results = (workflowDef.results || {});
|
|
276
|
+
const enumTypes = (workflowDef.enumTypes || []);
|
|
277
|
+
// Build enum lookup for category extraction
|
|
278
|
+
const enumLookup = new Map();
|
|
279
|
+
for (const et of enumTypes) {
|
|
280
|
+
const name = et.name?.name?.name;
|
|
281
|
+
const options = (et.options || []);
|
|
282
|
+
if (name) {
|
|
283
|
+
enumLookup.set(name, options.map(o => ({
|
|
284
|
+
name: o.name,
|
|
285
|
+
description: o.description,
|
|
286
|
+
examples: o.examples,
|
|
287
|
+
})));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Decompile each action to a Node
|
|
291
|
+
const nodes = actions.map(action => decompileAction(action, enumLookup));
|
|
292
|
+
// Decompile result mappings
|
|
293
|
+
const resultMappings = Object.values(results).map(r => ({
|
|
294
|
+
nodeId: r.actionName,
|
|
295
|
+
output: r.outputName,
|
|
296
|
+
}));
|
|
297
|
+
return {
|
|
298
|
+
name: extractWorkflowName(workflowDef),
|
|
299
|
+
description: workflowDef.description || "",
|
|
300
|
+
personaType,
|
|
301
|
+
nodes,
|
|
302
|
+
resultMappings,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function extractWorkflowName(workflowDef) {
|
|
306
|
+
const wn = workflowDef.workflowName;
|
|
307
|
+
if (wn?.name?.namespaces?.[2]) {
|
|
308
|
+
return wn.name.namespaces[2]; // persona ID
|
|
309
|
+
}
|
|
310
|
+
return workflowDef.displayName || "workflow";
|
|
311
|
+
}
|
|
312
|
+
function decompileAction(action, enumLookup) {
|
|
313
|
+
const actionDef = action.action;
|
|
314
|
+
const actionName = actionDef?.name?.name || "unknown";
|
|
315
|
+
const displaySettings = action.displaySettings;
|
|
316
|
+
const inputs = (action.inputs || {});
|
|
317
|
+
const runIf = action.runIf;
|
|
318
|
+
const typeArgs = action.typeArguments;
|
|
319
|
+
// Map raw action name to ActionType
|
|
320
|
+
const actionType = mapToActionType(actionName);
|
|
321
|
+
// Decompile inputs
|
|
322
|
+
const decompInputs = {};
|
|
323
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
324
|
+
const binding = decompileBinding(value);
|
|
325
|
+
if (binding) {
|
|
326
|
+
decompInputs[key] = binding;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Decompile runIf
|
|
330
|
+
let decompRunIf;
|
|
331
|
+
if (runIf) {
|
|
332
|
+
decompRunIf = decompileRunIf(runIf);
|
|
333
|
+
}
|
|
334
|
+
// Get categories for categorizers
|
|
335
|
+
let categories;
|
|
336
|
+
if (actionType.includes("categorizer") && typeArgs?.categories?.enumType?.name?.name) {
|
|
337
|
+
categories = enumLookup.get(typeArgs.categories.enumType.name.name);
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
id: action.name,
|
|
341
|
+
actionType,
|
|
342
|
+
displayName: displaySettings?.displayName || action.name,
|
|
343
|
+
description: displaySettings?.description,
|
|
344
|
+
inputs: Object.keys(decompInputs).length > 0 ? decompInputs : undefined,
|
|
345
|
+
runIf: decompRunIf,
|
|
346
|
+
categories,
|
|
347
|
+
disableHitl: action.disableHumanInteraction,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function mapToActionType(rawName) {
|
|
351
|
+
const mapping = {
|
|
352
|
+
chat_trigger: "chat_trigger",
|
|
353
|
+
document_trigger: "document_trigger",
|
|
354
|
+
chat_categorizer: "chat_categorizer",
|
|
355
|
+
text_categorizer: "text_categorizer",
|
|
356
|
+
conversation_to_search_query: "conversation_to_search_query",
|
|
357
|
+
search: "search",
|
|
358
|
+
live_web_search: "live_web_search",
|
|
359
|
+
combine_search_results: "combine_search_results",
|
|
360
|
+
respond_with_sources: "respond_with_sources",
|
|
361
|
+
call_llm: "call_llm",
|
|
362
|
+
fixed_response: "fixed_response",
|
|
363
|
+
entity_extraction: "entity_extraction",
|
|
364
|
+
entity_extraction_with_documents: "entity_extraction",
|
|
365
|
+
external_action_caller: "external_action_caller",
|
|
366
|
+
send_email_agent: "send_email_agent",
|
|
367
|
+
general_hitl: "general_hitl",
|
|
368
|
+
hitl: "general_hitl",
|
|
369
|
+
response_validator: "response_validator",
|
|
370
|
+
};
|
|
371
|
+
return mapping[rawName] || "call_llm";
|
|
372
|
+
}
|
|
373
|
+
function decompileBinding(value) {
|
|
374
|
+
const v = value;
|
|
375
|
+
// Action output binding
|
|
376
|
+
if (v.actionOutput) {
|
|
377
|
+
const ao = v.actionOutput;
|
|
378
|
+
return {
|
|
379
|
+
type: "action_output",
|
|
380
|
+
actionName: ao.actionName,
|
|
381
|
+
output: ao.output,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// Widget config binding
|
|
385
|
+
if (v.widgetConfig) {
|
|
386
|
+
const wc = v.widgetConfig;
|
|
387
|
+
return {
|
|
388
|
+
type: "widget_config",
|
|
389
|
+
widgetName: wc.widgetName,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
// Inline value binding
|
|
393
|
+
if (v.inline) {
|
|
394
|
+
const inline = v.inline;
|
|
395
|
+
if (inline.wellKnown?.stringValue !== undefined) {
|
|
396
|
+
return {
|
|
397
|
+
type: "inline_string",
|
|
398
|
+
value: inline.wellKnown.stringValue,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (inline.wellKnown?.boolValue !== undefined) {
|
|
402
|
+
return {
|
|
403
|
+
type: "inline_bool",
|
|
404
|
+
value: inline.wellKnown.boolValue,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (inline.wellKnown?.int64Value !== undefined) {
|
|
408
|
+
return {
|
|
409
|
+
type: "inline_number",
|
|
410
|
+
value: parseInt(inline.wellKnown.int64Value, 10),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (inline.enumValue) {
|
|
414
|
+
return {
|
|
415
|
+
type: "inline_string",
|
|
416
|
+
value: inline.enumValue,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Multi-binding (named inputs)
|
|
421
|
+
if (v.multiBinding) {
|
|
422
|
+
const mb = v.multiBinding;
|
|
423
|
+
const namedInputs = [];
|
|
424
|
+
for (const elem of mb.elements || []) {
|
|
425
|
+
if (elem.namedBinding?.name && elem.namedBinding.value) {
|
|
426
|
+
const binding = decompileBinding(elem.namedBinding.value);
|
|
427
|
+
if (binding) {
|
|
428
|
+
namedInputs.push({ name: elem.namedBinding.name, binding });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (namedInputs.length > 0) {
|
|
433
|
+
return {
|
|
434
|
+
type: "action_output", // Container type
|
|
435
|
+
namedInputs,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
function decompileRunIf(runIf) {
|
|
442
|
+
const lhs = runIf.lhs;
|
|
443
|
+
const rhs = runIf.rhs;
|
|
444
|
+
const operator = runIf.operator;
|
|
445
|
+
if (!lhs?.actionOutput || !rhs?.inline?.enumValue)
|
|
446
|
+
return undefined;
|
|
447
|
+
const opMap = {
|
|
448
|
+
1: "eq",
|
|
449
|
+
2: "neq",
|
|
450
|
+
3: "gt",
|
|
451
|
+
4: "lt",
|
|
452
|
+
5: "gte",
|
|
453
|
+
6: "lte",
|
|
454
|
+
};
|
|
455
|
+
return {
|
|
456
|
+
sourceAction: lhs.actionOutput.actionName || "",
|
|
457
|
+
sourceOutput: lhs.actionOutput.output || "",
|
|
458
|
+
operator: opMap[operator] || "eq",
|
|
459
|
+
value: rhs.inline.enumValue,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Transform a workflow using an LLM-generated spec modification.
|
|
464
|
+
*
|
|
465
|
+
* The expected workflow:
|
|
466
|
+
* 1. Decompile existing workflow to WorkflowSpec
|
|
467
|
+
* 2. LLM reads spec + schema + user request
|
|
468
|
+
* 3. LLM outputs modified WorkflowSpec (or a diff/patch)
|
|
469
|
+
* 4. This function compiles back to workflow_def
|
|
470
|
+
*
|
|
471
|
+
* @param originalDef - The original workflow_def JSON
|
|
472
|
+
* @param modifiedSpec - The LLM-modified WorkflowSpec
|
|
473
|
+
* @returns TransformResult with compiled workflow
|
|
474
|
+
*/
|
|
475
|
+
export function transformWorkflow(originalDef, modifiedSpec) {
|
|
476
|
+
const changes = [];
|
|
477
|
+
const errors = [];
|
|
478
|
+
try {
|
|
479
|
+
// Compile the modified spec
|
|
480
|
+
const compiled = compileWorkflow(modifiedSpec);
|
|
481
|
+
// Preserve workflowName from original (important for deployment)
|
|
482
|
+
compiled.workflow_def.workflowName = originalDef.workflowName;
|
|
483
|
+
// Detect what changed
|
|
484
|
+
const originalSpec = decompileWorkflow(originalDef, modifiedSpec.personaType);
|
|
485
|
+
changes.push(...detectChanges(originalSpec, modifiedSpec));
|
|
486
|
+
return {
|
|
487
|
+
success: true,
|
|
488
|
+
spec: modifiedSpec,
|
|
489
|
+
workflow_def: compiled.workflow_def,
|
|
490
|
+
changes,
|
|
491
|
+
errors,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
errors.push(`Compilation failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
spec: modifiedSpec,
|
|
499
|
+
workflow_def: originalDef,
|
|
500
|
+
changes,
|
|
501
|
+
errors,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function detectChanges(original, modified) {
|
|
506
|
+
const changes = [];
|
|
507
|
+
const originalIds = new Set(original.nodes.map(n => n.id));
|
|
508
|
+
const modifiedIds = new Set(modified.nodes.map(n => n.id));
|
|
509
|
+
// Added nodes
|
|
510
|
+
for (const id of modifiedIds) {
|
|
511
|
+
if (!originalIds.has(id)) {
|
|
512
|
+
changes.push(`Added node: ${id}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Removed nodes
|
|
516
|
+
for (const id of originalIds) {
|
|
517
|
+
if (!modifiedIds.has(id)) {
|
|
518
|
+
changes.push(`Removed node: ${id}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Modified nodes (simplified check)
|
|
522
|
+
for (const modNode of modified.nodes) {
|
|
523
|
+
const origNode = original.nodes.find(n => n.id === modNode.id);
|
|
524
|
+
if (origNode) {
|
|
525
|
+
// Check if inputs changed
|
|
526
|
+
const origInputs = JSON.stringify(origNode.inputs || {});
|
|
527
|
+
const modInputs = JSON.stringify(modNode.inputs || {});
|
|
528
|
+
if (origInputs !== modInputs) {
|
|
529
|
+
changes.push(`Modified ${modNode.id} inputs`);
|
|
530
|
+
}
|
|
531
|
+
// Check if runIf changed
|
|
532
|
+
if (JSON.stringify(origNode.runIf) !== JSON.stringify(modNode.runIf)) {
|
|
533
|
+
changes.push(`Modified ${modNode.id} runIf condition`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return changes;
|
|
538
|
+
}
|
|
539
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
540
|
+
// HELPERS FOR LLM CONTEXT
|
|
541
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
542
|
+
/**
|
|
543
|
+
* Generate a summary of a workflow for LLM context.
|
|
544
|
+
*/
|
|
545
|
+
export function summarizeSpec(spec) {
|
|
546
|
+
const lines = [
|
|
547
|
+
`## Workflow: ${spec.name}`,
|
|
548
|
+
`Type: ${spec.personaType}`,
|
|
549
|
+
`Description: ${spec.description}`,
|
|
550
|
+
``,
|
|
551
|
+
`### Nodes (${spec.nodes.length})`,
|
|
552
|
+
];
|
|
553
|
+
for (const node of spec.nodes) {
|
|
554
|
+
const inputs = node.inputs ? Object.keys(node.inputs).join(", ") : "none";
|
|
555
|
+
const condition = node.runIf
|
|
556
|
+
? ` [if ${node.runIf.sourceAction}.${node.runIf.sourceOutput} ${node.runIf.operator} "${node.runIf.value}"]`
|
|
557
|
+
: "";
|
|
558
|
+
lines.push(`- ${node.id} (${node.actionType}): ${node.displayName}${condition}`);
|
|
559
|
+
lines.push(` Inputs: ${inputs}`);
|
|
560
|
+
}
|
|
561
|
+
lines.push(``, `### Output Mappings`);
|
|
562
|
+
for (const rm of spec.resultMappings) {
|
|
563
|
+
lines.push(`- ${rm.nodeId}.${rm.output}`);
|
|
564
|
+
}
|
|
565
|
+
return lines.join("\n");
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Create an LLM prompt for workflow modification.
|
|
569
|
+
* This should be used by the MCP handler to construct the LLM call.
|
|
570
|
+
*/
|
|
571
|
+
export function createModificationPrompt(currentSpec, userRequest) {
|
|
572
|
+
return `
|
|
573
|
+
You are modifying an Ema workflow. Your output should be a valid WorkflowSpec JSON.
|
|
574
|
+
|
|
575
|
+
## Current Workflow
|
|
576
|
+
${summarizeSpec(currentSpec)}
|
|
577
|
+
|
|
578
|
+
## Current Spec (JSON)
|
|
579
|
+
\`\`\`json
|
|
580
|
+
${JSON.stringify(currentSpec, null, 2)}
|
|
581
|
+
\`\`\`
|
|
582
|
+
|
|
583
|
+
## Schema Reference
|
|
584
|
+
${WORKFLOW_SCHEMA_FOR_LLM}
|
|
585
|
+
|
|
586
|
+
## User Request
|
|
587
|
+
${userRequest}
|
|
588
|
+
|
|
589
|
+
## Instructions
|
|
590
|
+
1. Analyze the user request
|
|
591
|
+
2. Determine what nodes/connections need to change
|
|
592
|
+
3. Output the COMPLETE modified WorkflowSpec as valid JSON
|
|
593
|
+
4. Preserve all unchanged nodes exactly as they are
|
|
594
|
+
5. Only modify what's necessary for the request
|
|
595
|
+
|
|
596
|
+
## Output
|
|
597
|
+
Return ONLY the modified WorkflowSpec JSON, no explanation needed:
|
|
598
|
+
\`\`\`json
|
|
599
|
+
`;
|
|
600
|
+
}
|