@ema.co/mcp-toolkit 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

@@ -550,7 +550,7 @@ export function parseNaturalLanguage(text) {
550
550
  });
551
551
  }
552
552
  }
553
- // Detect tools
553
+ // Detect tools from text
554
554
  const tools = [];
555
555
  for (const { pattern, namespace, action } of TOOL_PATTERNS) {
556
556
  if (pattern.test(text)) {
@@ -1297,8 +1297,9 @@ export function summarizeIntentConfidence(confidence) {
1297
1297
  export function intentToSpec(intent) {
1298
1298
  const nodes = [];
1299
1299
  const resultMappings = [];
1300
- // 1. Add trigger
1300
+ // 1. Add trigger - Voice and Chat both use chat_trigger, Dashboard uses document_trigger
1301
1301
  const triggerId = "trigger";
1302
+ // Note: voice_trigger doesn't exist in API - Voice AI uses chat_trigger with voiceSettings in proto_config
1302
1303
  const triggerType = intent.persona_type === "dashboard" ? "document_trigger" : "chat_trigger";
1303
1304
  nodes.push({
1304
1305
  id: triggerId,
@@ -1349,6 +1350,11 @@ export function intentToSpec(intent) {
1349
1350
  actionName: triggerId,
1350
1351
  output: "user_query",
1351
1352
  },
1353
+ // REQUIRED: Widget binding to fileUpload data sources (uses multiBinding.elements format)
1354
+ datastore_configs: {
1355
+ type: "widget_config_array",
1356
+ widgetNames: ["fileUpload"],
1357
+ },
1352
1358
  },
1353
1359
  });
1354
1360
  }
@@ -1370,26 +1376,13 @@ export function intentToSpec(intent) {
1370
1376
  });
1371
1377
  }
1372
1378
  // 5. Add tool caller if tools defined
1373
- let toolCallerId;
1374
- if (intent.tools && intent.tools.length > 0) {
1375
- toolCallerId = "tool_caller";
1376
- nodes.push({
1377
- id: toolCallerId,
1378
- actionType: "external_action_caller",
1379
- displayName: "External Actions",
1380
- inputs: {
1381
- conversation: {
1382
- type: "action_output",
1383
- actionName: triggerId,
1384
- output: "chat_conversation",
1385
- },
1386
- },
1387
- tools: intent.tools.map((t) => ({
1388
- name: t.action,
1389
- namespace: t.namespace,
1390
- })),
1391
- });
1392
- }
1379
+ // DISABLED: external_action_caller format causes 500 errors
1380
+ // TODO: Fix tool format and re-enable - tools are stored in intent for future use
1381
+ const toolCallerId = undefined;
1382
+ // if (intent.tools && intent.tools.length > 0) {
1383
+ // toolCallerId = "tool_caller";
1384
+ // nodes.push({...});
1385
+ // }
1393
1386
  // 6. Add HITL if required
1394
1387
  let hitlId;
1395
1388
  if (intent.constraints?.require_hitl && toolCallerId) {
@@ -1415,9 +1408,16 @@ export function intentToSpec(intent) {
1415
1408
  actionName: triggerId,
1416
1409
  output: "user_query",
1417
1410
  },
1411
+ // REQUIRED: Widget binding to fusionModel for LLM selection
1412
+ model_config: {
1413
+ type: "widget_config",
1414
+ widgetName: "fusionModel",
1415
+ },
1418
1416
  };
1419
1417
  if (searchId) {
1420
- respondInputs.search_results = {
1418
+ // call_llm uses "named_inputs" not "search_results"
1419
+ // (respond_with_sources doesn't exist in API - mapped to call_llm)
1420
+ respondInputs.named_inputs = {
1421
1421
  type: "action_output",
1422
1422
  actionName: searchId,
1423
1423
  output: "search_results",
@@ -1425,7 +1425,8 @@ export function intentToSpec(intent) {
1425
1425
  }
1426
1426
  nodes.push({
1427
1427
  id: respondId,
1428
- actionType: searchId ? "respond_with_sources" : "call_llm",
1428
+ // respond_with_sources doesn't exist in API - always use call_llm
1429
+ actionType: "call_llm",
1429
1430
  displayName: "Response",
1430
1431
  inputs: respondInputs,
1431
1432
  });
@@ -1433,12 +1434,27 @@ export function intentToSpec(intent) {
1433
1434
  nodeId: respondId,
1434
1435
  output: searchId ? "response_with_sources" : "response_with_sources",
1435
1436
  });
1437
+ // NOTE: Action chains (email_with_validation, doc_to_email, etc.) are NOT processed here.
1438
+ // Complex workflows with action chains should be generated via LLM using:
1439
+ // 1. generateWorkflowPrompt(intent, availableActions) - builds LLM prompt
1440
+ // 2. Pass to Auto Builder or external LLM
1441
+ // 3. Parse the LLM response as WorkflowSpec
1442
+ // This keeps intentToSpec simple (basic RAG workflow) and lets LLM handle complexity.
1443
+ // Build voiceConfig from intent.voice_config
1444
+ const voiceConfig = intent.persona_type === "voice" ? {
1445
+ welcomeMessage: intent.voice_config?.welcome_message,
1446
+ identityAndPurpose: intent.voice_config?.identity ?? intent.description,
1447
+ hangupInstructions: intent.voice_config?.hangup_instructions,
1448
+ // Additional fields can be extracted from intent description in the future
1449
+ } : undefined;
1436
1450
  return {
1437
1451
  name: intent.name,
1438
1452
  description: intent.description,
1439
1453
  personaType: intent.persona_type,
1440
1454
  nodes,
1441
1455
  resultMappings,
1456
+ ...(voiceConfig && { voiceConfig }),
1457
+ ...(intent.persona_type === "chat" && { chatConfig: { name: intent.name } }),
1442
1458
  };
1443
1459
  }
1444
1460
  export function parseInput(input) {
@@ -1583,3 +1599,209 @@ export function generateContentInstructions(semantics) {
1583
1599
  }
1584
1600
  return instructions.join("\n");
1585
1601
  }
1602
+ /**
1603
+ * Determine if intent requires LLM-driven generation or can use simple intentToSpec.
1604
+ *
1605
+ * Simple (use intentToSpec):
1606
+ * - Basic Q&A with search
1607
+ * - Single intent without delivery
1608
+ *
1609
+ * Complex (use LLM):
1610
+ * - Action chains (email, document generation)
1611
+ * - Multiple intents with routing
1612
+ * - HITL requirements
1613
+ * - External tool calls
1614
+ */
1615
+ export function needsLLMGeneration(intent) {
1616
+ // Check for action chains
1617
+ if (intent.action_chains && intent.action_chains.length > 0) {
1618
+ return true;
1619
+ }
1620
+ // Check for email/document delivery
1621
+ if (intent.delivery_config?.methods?.some(m => m.type === "email" || m.type === "document")) {
1622
+ return true;
1623
+ }
1624
+ // Check for HITL requirements
1625
+ if (intent.constraints?.require_hitl) {
1626
+ return true;
1627
+ }
1628
+ // Check for multiple intents (needs categorizer with complex routing)
1629
+ if (intent.intents && intent.intents.length > 2) {
1630
+ return true;
1631
+ }
1632
+ // Check for tools/external actions
1633
+ if (intent.tools && intent.tools.length > 0) {
1634
+ return true;
1635
+ }
1636
+ return false;
1637
+ }
1638
+ /**
1639
+ * Generate a workflow spec or LLM prompt based on intent complexity.
1640
+ *
1641
+ * For simple intents: returns spec directly
1642
+ * For complex intents: returns LLM prompt with full context
1643
+ *
1644
+ * @param intent - Parsed workflow intent
1645
+ * @param availableActions - Actions from ListActions API (optional)
1646
+ * @returns Generation result with either spec or LLM prompt
1647
+ */
1648
+ export function generateWorkflow(intent, availableActions) {
1649
+ const complexity = {
1650
+ has_action_chains: !!(intent.action_chains && intent.action_chains.length > 0),
1651
+ has_email: !!(intent.delivery_config?.methods?.some(m => m.type === "email")),
1652
+ has_hitl: !!intent.constraints?.require_hitl,
1653
+ has_multi_intent: !!(intent.intents && intent.intents.length > 2),
1654
+ chain_ids: intent.action_chains?.map(c => c.id) ?? [],
1655
+ };
1656
+ // Simple workflow - use intentToSpec
1657
+ if (!needsLLMGeneration(intent)) {
1658
+ return {
1659
+ needs_llm: false,
1660
+ spec: intentToSpec(intent),
1661
+ reason: "Simple workflow (basic RAG pattern) - no LLM needed",
1662
+ complexity,
1663
+ };
1664
+ }
1665
+ // Complex workflow - generate LLM prompt
1666
+ const systemPrompt = buildWorkflowGenerationSystemPrompt(availableActions);
1667
+ const userPrompt = buildWorkflowGenerationUserPrompt(intent);
1668
+ return {
1669
+ needs_llm: true,
1670
+ llm_prompt: {
1671
+ system: systemPrompt,
1672
+ user: userPrompt,
1673
+ },
1674
+ reason: `Complex workflow requires LLM: ${complexity.chain_ids.length > 0 ? `chains: ${complexity.chain_ids.join(', ')}` : ''} ${complexity.has_email ? 'email' : ''} ${complexity.has_hitl ? 'HITL' : ''}`.trim(),
1675
+ complexity,
1676
+ };
1677
+ }
1678
+ /**
1679
+ * Build system prompt for LLM workflow generation.
1680
+ */
1681
+ function buildWorkflowGenerationSystemPrompt(availableActions) {
1682
+ const actionList = availableActions
1683
+ ? availableActions.map(a => `- ${a.name}: ${a.description} (inputs: ${a.inputs.join(', ')})`).join('\n')
1684
+ : 'Use standard Ema actions: chat_trigger, search, call_llm, entity_extraction, general_hitl, send_email, chat_categorizer';
1685
+ return `You are an expert workflow designer for the Ema AI platform.
1686
+
1687
+ Your task is to generate a WorkflowSpec JSON that implements the user's requirements.
1688
+
1689
+ ## Available Actions
1690
+ ${actionList}
1691
+
1692
+ ## WorkflowSpec Schema
1693
+ \`\`\`typescript
1694
+ interface WorkflowSpec {
1695
+ name: string;
1696
+ description: string;
1697
+ personaType: "voice" | "chat" | "dashboard";
1698
+ nodes: Node[];
1699
+ resultMappings: { nodeId: string; output: string }[];
1700
+ }
1701
+
1702
+ interface Node {
1703
+ id: string; // Unique identifier (snake_case)
1704
+ actionType: string; // Action name from available actions
1705
+ displayName: string; // Human-readable name
1706
+ inputs?: Record<string, InputBinding>;
1707
+ runIf?: { actionName: string; output: string; enumValue: string }; // Conditional execution
1708
+ categories?: Category[]; // For categorizer nodes
1709
+ }
1710
+
1711
+ interface InputBinding {
1712
+ type: "action_output" | "inline_string" | "widget_config";
1713
+ actionName?: string; // For action_output
1714
+ output?: string; // For action_output
1715
+ value?: string; // For inline_string
1716
+ widgetName?: string; // For widget_config
1717
+ }
1718
+ \`\`\`
1719
+
1720
+ ## Critical Rules
1721
+ 1. ALWAYS start with a trigger node (chat_trigger for voice/chat, document_trigger for dashboard)
1722
+ 2. Wire data correctly: each input must reference a valid upstream output
1723
+ 3. For EMAIL: extract recipient via entity_extraction FIRST, add HITL before send_email
1724
+ 4. Use runIf for conditional execution (after HITL approval)
1725
+ 5. All email recipients must come from entity_extraction, NOT from LLM-generated text
1726
+
1727
+ ## Example: Email with HITL
1728
+ \`\`\`json
1729
+ {
1730
+ "nodes": [
1731
+ { "id": "trigger", "actionType": "chat_trigger", "displayName": "Trigger" },
1732
+ { "id": "extract", "actionType": "entity_extraction", "displayName": "Extract Email",
1733
+ "inputs": { "conversation": { "type": "action_output", "actionName": "trigger", "output": "chat_conversation" } } },
1734
+ { "id": "respond", "actionType": "call_llm", "displayName": "Generate Response",
1735
+ "inputs": { "query": { "type": "action_output", "actionName": "trigger", "output": "user_query" } } },
1736
+ { "id": "hitl", "actionType": "general_hitl", "displayName": "Approve Email",
1737
+ "inputs": { "query": { "type": "action_output", "actionName": "respond", "output": "response" } } },
1738
+ { "id": "send_email", "actionType": "send_email", "displayName": "Send Email",
1739
+ "inputs": {
1740
+ "email_to": { "type": "action_output", "actionName": "extract", "output": "email_address" },
1741
+ "email_body": { "type": "action_output", "actionName": "respond", "output": "response" }
1742
+ },
1743
+ "runIf": { "actionName": "hitl", "output": "hitl_status", "enumValue": "HITL Success" } }
1744
+ ],
1745
+ "resultMappings": [{ "nodeId": "send_email", "output": "email_sent" }]
1746
+ }
1747
+ \`\`\`
1748
+
1749
+ Respond with ONLY valid JSON - no markdown, no explanation.`;
1750
+ }
1751
+ /**
1752
+ * Build user prompt with intent details.
1753
+ */
1754
+ function buildWorkflowGenerationUserPrompt(intent) {
1755
+ const sections = [];
1756
+ sections.push(`## User Request\n${intent.description}`);
1757
+ sections.push(`## Persona Type\n${intent.persona_type ?? 'chat'}`);
1758
+ if (intent.action_chains && intent.action_chains.length > 0) {
1759
+ sections.push(`## Detected Patterns\n${intent.action_chains.map(c => `- ${c.name}: ${c.description}`).join('\n')}`);
1760
+ }
1761
+ if (intent.intents && intent.intents.length > 0) {
1762
+ sections.push(`## User Intents\n${intent.intents.map(i => `- ${i.name}: ${i.description}`).join('\n')}`);
1763
+ }
1764
+ if (intent.entities && intent.entities.length > 0) {
1765
+ sections.push(`## Entities to Extract\n${intent.entities.map(e => `- ${e.name}: ${e.type}`).join('\n')}`);
1766
+ }
1767
+ if (intent.data_sources && intent.data_sources.length > 0) {
1768
+ sections.push(`## Data Sources\n${intent.data_sources.map(d => `- ${d.type}${d.instructions ? `: ${d.instructions}` : ''}`).join('\n')}`);
1769
+ }
1770
+ if (intent.constraints) {
1771
+ const constraints = [];
1772
+ if (intent.constraints.require_hitl)
1773
+ constraints.push('- Require human approval before side effects');
1774
+ if (intent.constraints.require_validation)
1775
+ constraints.push('- Require validation before actions');
1776
+ if (intent.constraints.enable_web_search)
1777
+ constraints.push('- Enable web search');
1778
+ if (constraints.length > 0) {
1779
+ sections.push(`## Constraints\n${constraints.join('\n')}`);
1780
+ }
1781
+ }
1782
+ sections.push('\nGenerate a WorkflowSpec JSON that implements this workflow.');
1783
+ return sections.join('\n\n');
1784
+ }
1785
+ /**
1786
+ * Parse LLM response into WorkflowSpec.
1787
+ * Handles markdown code blocks and validates structure.
1788
+ */
1789
+ export function parseWorkflowSpecFromLLM(llmResponse) {
1790
+ try {
1791
+ // Extract JSON from markdown code block if present
1792
+ let json = llmResponse;
1793
+ const codeBlockMatch = llmResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
1794
+ if (codeBlockMatch) {
1795
+ json = codeBlockMatch[1];
1796
+ }
1797
+ const parsed = JSON.parse(json.trim());
1798
+ // Basic validation
1799
+ if (!parsed.nodes || !Array.isArray(parsed.nodes)) {
1800
+ return null;
1801
+ }
1802
+ return parsed;
1803
+ }
1804
+ catch {
1805
+ return null;
1806
+ }
1807
+ }