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

@@ -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
+ }