@ema.co/mcp-toolkit 2026.2.13 → 2026.2.23-1

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.

Files changed (67) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/dist/cli/index.js +2 -2
  4. package/dist/mcp/domain/loop-detection.js +89 -0
  5. package/dist/mcp/domain/sanitizer.js +1 -1
  6. package/dist/mcp/domain/structural-rules.js +4 -5
  7. package/dist/mcp/domain/validation-rules.js +5 -5
  8. package/dist/mcp/domain/workflow-graph.js +3 -5
  9. package/dist/mcp/domain/workflow-path-enumerator.js +7 -4
  10. package/dist/mcp/guidance.js +62 -29
  11. package/dist/mcp/handlers/debug/adapter.js +15 -0
  12. package/dist/mcp/handlers/debug/formatters.js +282 -0
  13. package/dist/mcp/handlers/debug/index.js +133 -0
  14. package/dist/mcp/handlers/demo/adapter.js +180 -0
  15. package/dist/mcp/handlers/env/config.js +2 -2
  16. package/dist/mcp/handlers/feedback/index.js +1 -1
  17. package/dist/mcp/handlers/index.js +0 -1
  18. package/dist/mcp/handlers/persona/adapter.js +135 -0
  19. package/dist/mcp/handlers/persona/index.js +237 -8
  20. package/dist/mcp/handlers/persona/schema.js +27 -0
  21. package/dist/mcp/handlers/reference/index.js +6 -4
  22. package/dist/mcp/handlers/sync/adapter.js +200 -0
  23. package/dist/mcp/handlers/workflow/adapter.js +174 -0
  24. package/dist/mcp/handlers/workflow/fix.js +11 -12
  25. package/dist/mcp/handlers/workflow/index.js +12 -40
  26. package/dist/mcp/handlers/workflow/validation.js +1 -1
  27. package/dist/mcp/knowledge-guidance-topics.js +615 -0
  28. package/dist/mcp/knowledge-types.js +7 -0
  29. package/dist/mcp/knowledge.js +75 -1403
  30. package/dist/mcp/resources-dynamic.js +2395 -0
  31. package/dist/mcp/resources-validation.js +408 -0
  32. package/dist/mcp/resources.js +72 -2508
  33. package/dist/mcp/server.js +69 -2825
  34. package/dist/mcp/tools.js +106 -5
  35. package/dist/sdk/client-adapter.js +265 -24
  36. package/dist/sdk/ema-client.js +100 -9
  37. package/dist/sdk/generated/agent-catalog.js +615 -0
  38. package/dist/sdk/generated/api-client/client/client.gen.js +3 -3
  39. package/dist/sdk/generated/api-client/client/index.js +5 -5
  40. package/dist/sdk/generated/api-client/client/utils.gen.js +4 -4
  41. package/dist/sdk/generated/api-client/client.gen.js +1 -1
  42. package/dist/sdk/generated/api-client/core/utils.gen.js +1 -1
  43. package/dist/sdk/generated/api-client/index.js +1 -1
  44. package/dist/sdk/generated/api-client/sdk.gen.js +2 -2
  45. package/dist/sdk/generated/well-known-types.js +99 -0
  46. package/dist/sdk/generated/widget-catalog.js +60 -0
  47. package/dist/sdk/grpc-client.js +115 -1
  48. package/dist/sync/sdk.js +2 -2
  49. package/dist/sync.js +4 -3
  50. package/docs/README.md +17 -9
  51. package/package.json +4 -3
  52. package/.context/public/guides/dashboard-operations.md +0 -349
  53. package/.context/public/guides/email-patterns.md +0 -125
  54. package/.context/public/guides/workflow-builder-patterns.md +0 -708
  55. package/dist/mcp/domain/intent-architect.js +0 -914
  56. package/dist/mcp/domain/quality-gates.js +0 -110
  57. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  58. package/dist/mcp/domain/workflow-intent.js +0 -1806
  59. package/dist/mcp/domain/workflow-merge.js +0 -449
  60. package/dist/mcp/domain/workflow-tracer.js +0 -648
  61. package/dist/mcp/domain/workflow-transformer.js +0 -742
  62. package/dist/mcp/handlers/knowledge/index.js +0 -54
  63. package/dist/mcp/handlers/persona/intent.js +0 -141
  64. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  65. package/dist/mcp/handlers/workflow/compare.js +0 -70
  66. package/dist/mcp/handlers/workflow/generate.js +0 -384
  67. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -1,1806 +0,0 @@
1
- /**
2
- * WorkflowIntent - Intermediate Representation for Workflow Generation
3
- *
4
- * Normalizes any input (natural language, partial spec, full spec) into a
5
- * structured intent that can be validated, clarified, and then compiled.
6
- *
7
- * Key concepts:
8
- * - **Action Chains**: Understand end-to-end flows (generate doc → attach → send)
9
- * - **Entity Relationships**: Who (client, advisor), what (brief, report), how (email, download)
10
- * - **Type Compatibility**: Ensure output types match expected input types
11
- */
12
- /**
13
- * Schema for LLM extraction of output semantics.
14
- * Pass this to an LLM along with user input to get structured understanding.
15
- */
16
- export const OUTPUT_SEMANTICS_EXTRACTION_SCHEMA = {
17
- name: "extract_output_semantics",
18
- description: "Extract semantic attributes about the desired output from user's request",
19
- parameters: {
20
- type: "object",
21
- properties: {
22
- output_type: {
23
- type: "object",
24
- properties: {
25
- primary: { type: "string", description: "What type of output? (brief, report, email, summary, analysis, proposal, etc.)" },
26
- category: { type: "string", enum: ["document", "communication", "response", "data", "notification"] },
27
- requires_attachment: { type: "boolean" },
28
- requires_delivery: { type: "boolean" },
29
- },
30
- required: ["primary", "category", "requires_attachment", "requires_delivery"],
31
- },
32
- format: {
33
- type: "object",
34
- properties: {
35
- primary: { type: "string", enum: ["document", "email", "chat", "file", "api_response"] },
36
- file_format: { type: "string", enum: ["docx", "pdf", "html", "markdown", "xlsx"] },
37
- structure: { type: "string", enum: ["narrative", "bullet_points", "sections", "table", "mixed"] },
38
- },
39
- required: ["primary"],
40
- },
41
- tone: {
42
- type: "object",
43
- properties: {
44
- formality: { type: "string", enum: ["formal", "professional", "casual", "friendly", "neutral"] },
45
- sentiment: { type: "string", enum: ["positive", "neutral", "cautious", "urgent"] },
46
- voice: { type: "string", enum: ["active", "passive", "mixed"] },
47
- },
48
- required: ["formality", "sentiment", "voice"],
49
- },
50
- style: {
51
- type: "object",
52
- properties: {
53
- approach: { type: "string", enum: ["analytical", "persuasive", "informative", "instructional", "conversational"] },
54
- detail_level: { type: "string", enum: ["high_level", "balanced", "detailed", "exhaustive"] },
55
- use_examples: { type: "boolean" },
56
- use_citations: { type: "boolean" },
57
- },
58
- required: ["approach", "detail_level", "use_examples", "use_citations"],
59
- },
60
- audience: {
61
- type: "object",
62
- properties: {
63
- type: { type: "string", enum: ["internal", "external", "client", "executive", "technical", "general"] },
64
- familiarity: { type: "string", enum: ["expert", "familiar", "novice", "unknown"] },
65
- relationship: { type: "string", description: "Relationship to recipient (client, colleague, manager, etc.)" },
66
- },
67
- required: ["type", "familiarity"],
68
- },
69
- purpose: {
70
- type: "object",
71
- properties: {
72
- primary: { type: "string", enum: ["inform", "persuade", "summarize", "analyze", "recommend", "request", "confirm"] },
73
- secondary: { type: "array", items: { type: "string" } },
74
- action_required: { type: "boolean" },
75
- decision_support: { type: "boolean" },
76
- },
77
- required: ["primary", "action_required", "decision_support"],
78
- },
79
- length: { type: "string", enum: ["brief", "standard", "detailed", "comprehensive"] },
80
- formatting_requirements: { type: "array", items: { type: "string" } },
81
- reasoning: { type: "string", description: "Brief explanation of why these attributes were chosen" },
82
- },
83
- required: ["output_type", "format", "tone", "style", "audience", "purpose", "length"],
84
- },
85
- };
86
- /**
87
- * Default output semantics when LLM extraction is not available.
88
- * Falls back to reasonable defaults for professional document generation.
89
- */
90
- export const DEFAULT_OUTPUT_SEMANTICS = {
91
- output_type: {
92
- primary: "document",
93
- category: "document",
94
- requires_attachment: false,
95
- requires_delivery: false,
96
- },
97
- format: {
98
- primary: "document",
99
- file_format: "docx",
100
- structure: "sections",
101
- },
102
- tone: {
103
- formality: "professional",
104
- sentiment: "neutral",
105
- voice: "active",
106
- },
107
- style: {
108
- approach: "informative",
109
- detail_level: "balanced",
110
- use_examples: false,
111
- use_citations: true,
112
- },
113
- audience: {
114
- type: "general",
115
- familiarity: "familiar",
116
- },
117
- purpose: {
118
- primary: "inform",
119
- action_required: false,
120
- decision_support: false,
121
- },
122
- length: "standard",
123
- extraction_confidence: 50, // Default confidence when using fallback
124
- };
125
- export function detectInputType(input) {
126
- if (typeof input === "string") {
127
- // Check if it's JSON
128
- try {
129
- const parsed = JSON.parse(input);
130
- return detectInputType(parsed);
131
- }
132
- catch {
133
- return "natural_language";
134
- }
135
- }
136
- if (typeof input === "object" && input !== null) {
137
- const obj = input;
138
- // Full spec: has nodes array with proper structure
139
- if (Array.isArray(obj.nodes) && obj.nodes.length > 0) {
140
- const firstNode = obj.nodes[0];
141
- if (firstNode.id && firstNode.actionType) {
142
- return "full_spec";
143
- }
144
- }
145
- // Existing workflow: has actions array (workflow_def structure)
146
- if (Array.isArray(obj.actions)) {
147
- return "existing_workflow";
148
- }
149
- // Partial spec: has some structure but not full nodes
150
- if (obj.intents || obj.tools || obj.data_sources) {
151
- return "partial_spec";
152
- }
153
- }
154
- return "natural_language";
155
- }
156
- // ─────────────────────────────────────────────────────────────────────────────
157
- // Natural Language Parsing
158
- // ─────────────────────────────────────────────────────────────────────────────
159
- // INTENT_PATTERNS and TOOL_PATTERNS removed - were only used by parseNaturalLanguage()
160
- // which violated LLM-driven architecture. Agent should understand intent naturally.
161
- // ─────────────────────────────────────────────────────────────────────────────
162
- // Action Chain Patterns - End-to-End Semantic Flows
163
- // ─────────────────────────────────────────────────────────────────────────────
164
- /**
165
- * Known action chains that require specific wiring
166
- */
167
- const ACTION_CHAIN_PATTERNS = [
168
- {
169
- id: "llm_template_to_doc",
170
- name: "LLM Templating → Document Generation",
171
- description: "Use LLM to generate structured content with template prompt, then convert to document. RECOMMENDED for dynamic, context-dependent content.",
172
- steps: [
173
- {
174
- action_type: "search",
175
- purpose: "Gather context from knowledge base",
176
- input_from: "trigger.user_query",
177
- output_to: "content_generator",
178
- config: { output: "search_results", output_type: "WELL_KNOWN_TYPE_SEARCH_RESULT" },
179
- },
180
- {
181
- action_type: "call_llm",
182
- purpose: "Generate structured content with LLM templating",
183
- input_from: "search_results via named_inputs",
184
- output_to: "document_generator",
185
- config: {
186
- // LLM TEMPLATING: Use structured prompt with section headers
187
- // The LLM determines appropriate sections based on content type
188
- prompt_guidance: "Include structured sections with ## headers. Let LLM determine appropriate structure based on content type and user intent.",
189
- temperature: "0.3-0.5 for consistent formatting",
190
- use_named_inputs_for_data: true,
191
- },
192
- },
193
- {
194
- action_type: "generate_document",
195
- purpose: "Convert markdown to document format",
196
- input_from: "call_llm.response_with_sources",
197
- output_to: "email_or_output",
198
- config: { output: "document_link", output_type: "WELL_KNOWN_TYPE_DOCUMENT" },
199
- },
200
- ],
201
- },
202
- {
203
- id: "doc_to_email",
204
- name: "Document Generation → Email Delivery",
205
- description: "Generate a document and send it as an email attachment",
206
- steps: [
207
- {
208
- action_type: "generate_document",
209
- purpose: "Create the document",
210
- input_from: "content_source", // search_results, llm output, etc.
211
- output_to: "email_attachment",
212
- config: { output: "document_link", output_type: "WELL_KNOWN_TYPE_DOCUMENT" },
213
- },
214
- {
215
- action_type: "send_email_agent",
216
- purpose: "Send email with document attached",
217
- input_from: "document_link",
218
- output_to: "result",
219
- config: {
220
- // CRITICAL: document_link is DOCUMENT type, NOT TEXT_WITH_SOURCES
221
- // Must use named_inputs for attachment, not attachment_links
222
- attachment_binding: "named_inputs",
223
- attachment_name: "document_attachment",
224
- },
225
- },
226
- ],
227
- },
228
- {
229
- id: "entity_to_scoped_search",
230
- name: "Entity Extraction → Scoped Search",
231
- description: "Extract entities (client, advisor) and use them to scope searches",
232
- steps: [
233
- {
234
- action_type: "entity_extraction_with_documents",
235
- purpose: "Extract structured entities from conversation",
236
- input_from: "trigger.chat_conversation",
237
- output_to: "json_mapper",
238
- },
239
- {
240
- action_type: "json_mapper",
241
- purpose: "Transform entities into usable variables",
242
- input_from: "extracted_entities",
243
- output_to: "search_query_modifier",
244
- },
245
- {
246
- action_type: "search",
247
- purpose: "Search scoped to extracted entity",
248
- input_from: "scoped_query",
249
- output_to: "response",
250
- },
251
- ],
252
- },
253
- {
254
- id: "actor_identification",
255
- name: "Actor Identification Flow",
256
- description: "Identify caller type and route/validate accordingly",
257
- steps: [
258
- {
259
- action_type: "text_categorizer",
260
- purpose: "Classify actor type (client, advisor, unknown)",
261
- input_from: "trigger.chat_conversation",
262
- output_to: "conditional_routing",
263
- },
264
- {
265
- action_type: "call_llm",
266
- purpose: "For unknown actors: ask for identification",
267
- input_from: "unknown_actor_branch",
268
- output_to: "validation",
269
- config: { condition: "actor == unknown" },
270
- },
271
- ],
272
- },
273
- {
274
- id: "search_combine_respond",
275
- name: "Multi-Source Search → Combine → Respond",
276
- description: "Search multiple sources, combine results, generate response",
277
- steps: [
278
- {
279
- action_type: "search",
280
- purpose: "Search knowledge base",
281
- input_from: "trigger.user_query",
282
- output_to: "combiner",
283
- config: { output: "search_results", output_type: "WELL_KNOWN_TYPE_SEARCH_RESULT" },
284
- },
285
- {
286
- action_type: "live_web_search",
287
- purpose: "Search web for real-time data",
288
- input_from: "trigger.user_query",
289
- output_to: "combiner",
290
- config: { output: "search_results", output_type: "WELL_KNOWN_TYPE_SEARCH_RESULT" },
291
- },
292
- {
293
- action_type: "combine_search_results",
294
- purpose: "Merge results from multiple sources",
295
- input_from: "both_search_results",
296
- output_to: "response",
297
- config: { output: "combined_results", output_type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES" },
298
- },
299
- {
300
- action_type: "respond_with_sources",
301
- purpose: "Generate response with citations",
302
- // CRITICAL: respond_with_sources.search_results expects SEARCH_RESULT, not TEXT_WITH_SOURCES
303
- // Route original search.search_results, not combined_results
304
- input_from: "search.search_results",
305
- output_to: "result",
306
- config: { use_original_search_for_search_results: true },
307
- },
308
- ],
309
- },
310
- {
311
- id: "content_generation_to_doc",
312
- name: "Content Generation → Document",
313
- description: "Generate rich content and create downloadable document",
314
- steps: [
315
- {
316
- action_type: "personalized_content_generator",
317
- purpose: "Generate rich HTML content",
318
- input_from: "search_results",
319
- output_to: "document_generator",
320
- config: { output: "generated_content", output_type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES" },
321
- },
322
- {
323
- action_type: "generate_document",
324
- purpose: "Create downloadable document",
325
- input_from: "generated_content",
326
- output_to: "delivery",
327
- config: { output: "document_link", output_type: "WELL_KNOWN_TYPE_DOCUMENT" },
328
- },
329
- ],
330
- },
331
- {
332
- id: "extract_validate_action",
333
- name: "Extract Required Inputs → Validate → Action",
334
- description: "Extract required data, validate completeness, ask if missing, confirm before action",
335
- steps: [
336
- {
337
- action_type: "entity_extraction",
338
- purpose: "Extract required fields from conversation",
339
- input_from: "trigger.chat_conversation",
340
- output_to: "validator",
341
- config: {
342
- // CRITICAL: Define extraction schema with required fields
343
- extraction_schema: {
344
- email_address: { type: "string", required: true },
345
- recipient_name: { type: "string", required: false },
346
- subject: { type: "string", required: false },
347
- },
348
- },
349
- },
350
- {
351
- action_type: "text_categorizer",
352
- purpose: "Check if all required inputs are present",
353
- input_from: "extracted_entities",
354
- output_to: "conditional_routing",
355
- config: {
356
- categories: ["has_all_required", "missing_required"],
357
- // Route based on presence of required fields
358
- },
359
- },
360
- {
361
- action_type: "call_llm",
362
- purpose: "Ask for missing required inputs",
363
- input_from: "missing_required_branch",
364
- output_to: "workflow_output",
365
- config: {
366
- condition: "category == missing_required",
367
- prompt: "Ask user for the missing required information",
368
- },
369
- },
370
- {
371
- action_type: "hitl",
372
- purpose: "Confirm before executing action with side effects",
373
- input_from: "has_all_required_branch",
374
- output_to: "action_execution",
375
- config: { confirmation_message: "Confirm action before proceeding" },
376
- },
377
- {
378
- action_type: "send_email_agent",
379
- purpose: "Execute the action only after validation and confirmation",
380
- input_from: "hitl_success",
381
- output_to: "result",
382
- config: {
383
- // CRITICAL: email_to must come from entity_extraction, NOT from summarized text
384
- email_to_source: "entity_extraction.email_address",
385
- runIf: "HITL Success",
386
- },
387
- },
388
- ],
389
- },
390
- {
391
- id: "email_with_validation",
392
- name: "Email Sending with Proper Validation",
393
- description: "Send email with extracted recipient, validation, and HITL confirmation",
394
- steps: [
395
- {
396
- action_type: "entity_extraction",
397
- purpose: "Extract email recipient from conversation",
398
- input_from: "trigger.chat_conversation",
399
- output_to: "validation",
400
- config: {
401
- // Extract structured data, NOT summarized text
402
- output: "extracted_entities",
403
- output_type: "WELL_KNOWN_TYPE_JSON",
404
- },
405
- },
406
- {
407
- action_type: "hitl",
408
- purpose: "Confirm recipient and content before sending",
409
- input_from: "extracted_entities",
410
- output_to: "conditional",
411
- config: {
412
- // REQUIRED for any action with side effects
413
- display_extracted_data: true,
414
- },
415
- },
416
- {
417
- action_type: "send_email_agent",
418
- purpose: "Send email after confirmation",
419
- input_from: "hitl.success",
420
- output_to: "result",
421
- config: {
422
- // CRITICAL: email_to must be EMAIL ADDRESS from extraction
423
- // NOT: summarized_conversation (text)
424
- // NOT: search_results (sources)
425
- // NOT: response_with_sources (generated text)
426
- email_to_source: "entity_extraction.email_address",
427
- runIf: "HITL Success",
428
- },
429
- },
430
- ],
431
- },
432
- ];
433
- /**
434
- * Intent routing type compatibility - determines how outputs wire to inputs during intent processing.
435
- *
436
- * NOTE: This is SEPARATE from knowledge.ts TYPE_COMPATIBILITY which is user-facing documentation.
437
- * This focuses on the "can_connect_to" and "use_named_inputs_for" routing logic.
438
- *
439
- * Canonical type documentation: src/sdk/knowledge.ts → TYPE_COMPATIBILITY
440
- */
441
- export const INTENT_TYPE_ROUTING = {
442
- WELL_KNOWN_TYPE_CHAT_CONVERSATION: {
443
- can_connect_to: ["conversation"],
444
- use_named_inputs_for: [],
445
- },
446
- WELL_KNOWN_TYPE_TEXT_WITH_SOURCES: {
447
- can_connect_to: ["query", "instructions", "context", "text"],
448
- use_named_inputs_for: ["search_results", "attachment_links"],
449
- },
450
- WELL_KNOWN_TYPE_SEARCH_RESULT: {
451
- can_connect_to: ["search_results"],
452
- use_named_inputs_for: ["text", "query"],
453
- },
454
- WELL_KNOWN_TYPE_DOCUMENT: {
455
- can_connect_to: [], // Documents can't directly connect to typed inputs
456
- use_named_inputs_for: ["attachment_links", "text", "query", "content"], // Always use named_inputs
457
- },
458
- };
459
- /**
460
- * Detect action chains in the input text
461
- */
462
- export function detectActionChains(text) {
463
- const detected = [];
464
- const lowerText = text.toLowerCase();
465
- // Document + Email chain
466
- if ((lowerText.includes("document") || lowerText.includes("brief") || lowerText.includes("report")) &&
467
- (lowerText.includes("email") || lowerText.includes("send"))) {
468
- detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "doc_to_email"));
469
- }
470
- // Entity extraction + scoped search
471
- if ((lowerText.includes("client") || lowerText.includes("advisor") || lowerText.includes("user")) &&
472
- (lowerText.includes("scope") || lowerText.includes("filter") || lowerText.includes("their"))) {
473
- detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "entity_to_scoped_search"));
474
- }
475
- // Actor identification
476
- if ((lowerText.includes("identify") || lowerText.includes("who is")) &&
477
- (lowerText.includes("caller") || lowerText.includes("client") || lowerText.includes("advisor"))) {
478
- detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "actor_identification"));
479
- }
480
- // Multi-source search
481
- if ((lowerText.includes("kb") || lowerText.includes("knowledge")) &&
482
- (lowerText.includes("web") || lowerText.includes("live") || lowerText.includes("real-time"))) {
483
- detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "search_combine_respond"));
484
- }
485
- // ═══════════════════════════════════════════════════════════════════════════
486
- // FALLBACK: String-based detection for document generation
487
- //
488
- // NOTE: This is a FALLBACK approach. For accurate semantic understanding,
489
- // use LLM extraction via generateOutputSemanticsPrompt() which extracts:
490
- // - output_type (document, email, report, etc.)
491
- // - format, tone, style, audience, purpose, length
492
- //
493
- // The functions below provide defaults when LLM extraction is unavailable.
494
- // ═══════════════════════════════════════════════════════════════════════════
495
- const hasGenerationIntent = lowerText.includes("generate") || lowerText.includes("create") ||
496
- lowerText.includes("prepare") || lowerText.includes("produce") || lowerText.includes("draft");
497
- const hasDocumentOutput = lowerText.includes("document") || lowerText.includes("doc") ||
498
- lowerText.includes("file") || lowerText.includes("download") || lowerText.includes("pdf") ||
499
- lowerText.includes("attachment") || lowerText.includes(".docx");
500
- const hasContentSynthesis = lowerText.includes("content") || lowerText.includes("html") ||
501
- lowerText.includes("markdown") || lowerText.includes("formatted");
502
- if (hasGenerationIntent && (hasDocumentOutput || hasContentSynthesis)) {
503
- // LLM templating is the recommended default for dynamic content
504
- detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "llm_template_to_doc"));
505
- }
506
- // Email sending (without document attachment) - requires proper validation
507
- if ((lowerText.includes("send") || lowerText.includes("email") || lowerText.includes("mail")) &&
508
- !lowerText.includes("document") && !lowerText.includes("attach") && !lowerText.includes("brief")) {
509
- detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "email_with_validation"));
510
- }
511
- // Extract → Validate → Action pattern (when action has side effects)
512
- if ((lowerText.includes("send") || lowerText.includes("create") || lowerText.includes("update") || lowerText.includes("book")) &&
513
- (lowerText.includes("confirm") || lowerText.includes("validate") || lowerText.includes("required") || lowerText.includes("check"))) {
514
- detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "extract_validate_action"));
515
- }
516
- return detected.filter(Boolean);
517
- }
518
- // PERSONA_TYPE_PATTERNS removed - no longer used after parseNaturalLanguage() removal
519
- /**
520
- * REMOVED: parseNaturalLanguage() - regex-based NL parsing violates LLM-driven architecture
521
- *
522
- * This function was removed because it violates the core principle:
523
- * "THE AGENT (LLM) DOES THE THINKING. THE MCP PROVIDES CONTEXT AND EXECUTES."
524
- *
525
- * Natural language input should be handled by:
526
- * 1. Intent Architect (runIntentArchitect) - analyzes complexity and returns LLM prompts
527
- * 2. generateWorkflow() - takes WorkflowIntent and returns LLM prompts for complex cases
528
- * 3. Direct structured input (WorkflowSpec) from the Agent
529
- *
530
- * For natural language input, create a minimal WorkflowIntent and let Intent Architect
531
- * or generateWorkflow() handle the complexity analysis.
532
- *
533
- * @see src/mcp/AGENTS.md for the LLM-driven architecture guidelines
534
- */
535
- function createMinimalIntentFromText(text, personaType = "chat") {
536
- // Create minimal intent - no regex parsing
537
- // Intent Architect and generateWorkflow() will handle complexity analysis
538
- // Extract a name from the text (first sentence or first N words)
539
- const sentences = text.split(/[.!?]/);
540
- const name = sentences[0].trim().slice(0, 50) || "AI Employee";
541
- return {
542
- name,
543
- description: text,
544
- persona_type: personaType,
545
- };
546
- }
547
- /**
548
- * Detect entities that should be extracted from the conversation
549
- */
550
- function detectEntities(text) {
551
- const entities = [];
552
- const lowerText = text.toLowerCase();
553
- // Client entity
554
- if (lowerText.includes("client")) {
555
- entities.push({
556
- name: "client",
557
- type: "person",
558
- extract_from: "conversation",
559
- use_for: ["scoped_search", "email_recipient", "document_context"],
560
- });
561
- }
562
- // Advisor entity
563
- if (lowerText.includes("advisor")) {
564
- entities.push({
565
- name: "advisor",
566
- type: "person",
567
- extract_from: "conversation",
568
- use_for: ["validation", "cc_recipient"],
569
- });
570
- }
571
- // Ticker/stock symbol
572
- if (/ticker|stock|symbol|security/i.test(text)) {
573
- entities.push({
574
- name: "ticker",
575
- type: "identifier",
576
- extract_from: "conversation",
577
- use_for: ["scoped_search", "document_title"],
578
- });
579
- }
580
- // Topic/focus area
581
- if (/focus|topic|about|regarding/i.test(text)) {
582
- entities.push({
583
- name: "focus_area",
584
- type: "topic",
585
- extract_from: "conversation",
586
- use_for: ["scoped_search", "document_content"],
587
- });
588
- }
589
- return entities;
590
- }
591
- /**
592
- * Detect how results should be delivered
593
- */
594
- function detectDeliveryConfig(text) {
595
- const lowerText = text.toLowerCase();
596
- const methods = [];
597
- // Email delivery
598
- if (lowerText.includes("email") || lowerText.includes("send")) {
599
- const emailConfig = {
600
- recipient_source: "extracted",
601
- subject_source: "generated",
602
- body_source: "llm_generated",
603
- include_attachments: false,
604
- };
605
- // Check if document should be attached
606
- if ((lowerText.includes("attach") || lowerText.includes("document") || lowerText.includes("brief")) &&
607
- lowerText.includes("email")) {
608
- emailConfig.include_attachments = true;
609
- emailConfig.attachment_source = "generate_document.document_link";
610
- }
611
- methods.push({ type: "email", config: emailConfig });
612
- }
613
- // Document delivery (download/generate)
614
- if (lowerText.includes("document") ||
615
- lowerText.includes("brief") ||
616
- lowerText.includes("report") ||
617
- lowerText.includes("download")) {
618
- const docConfig = {
619
- document_type: lowerText.includes("brief")
620
- ? "brief"
621
- : lowerText.includes("report")
622
- ? "report"
623
- : "summary",
624
- title_source: "extracted",
625
- content_source: "combined",
626
- };
627
- methods.push({ type: "document", config: docConfig });
628
- }
629
- // Determine if confirmation is needed
630
- const requiresConfirmation = lowerText.includes("confirm") || lowerText.includes("approve");
631
- if (methods.length === 0) {
632
- return undefined;
633
- }
634
- return {
635
- method: methods.length > 1 ? "multiple" : methods[0].type,
636
- methods: methods.length > 1 ? methods : undefined,
637
- requires_confirmation: requiresConfirmation || methods.some((m) => m.type === "email"),
638
- };
639
- }
640
- // ─────────────────────────────────────────────────────────────────────────────
641
- // Partial Spec Parsing
642
- // ─────────────────────────────────────────────────────────────────────────────
643
- export function parsePartialSpec(spec) {
644
- return {
645
- name: String(spec.name ?? "AI Employee"),
646
- description: String(spec.description ?? ""),
647
- persona_type: spec.persona_type ?? "chat",
648
- intents: spec.intents,
649
- tools: spec.tools,
650
- data_sources: spec.data_sources,
651
- constraints: spec.constraints,
652
- };
653
- }
654
- // ─────────────────────────────────────────────────────────────────────────────
655
- // Validation
656
- // ─────────────────────────────────────────────────────────────────────────────
657
- export function validateIntent(intent) {
658
- const missing = [];
659
- const questions = [];
660
- const suggestions = [];
661
- // Check name/description
662
- if (!intent.name || intent.name.length < 3) {
663
- missing.push("name");
664
- questions.push("What should this AI Employee be called?");
665
- }
666
- if (!intent.description || intent.description.length < 10) {
667
- missing.push("description");
668
- questions.push("What should this AI Employee do?");
669
- }
670
- // Check if we have any capability defined
671
- const hasCapability = (intent.intents?.length ?? 0) > 0 ||
672
- (intent.tools?.length ?? 0) > 0 ||
673
- (intent.data_sources?.length ?? 0) > 0;
674
- if (!hasCapability) {
675
- missing.push("capabilities");
676
- questions.push("What capabilities should this AI have? (e.g., search KB, create tickets, route by intent)");
677
- }
678
- // Intent-specific validation
679
- if (intent.intents && intent.intents.length > 0) {
680
- // Check for Fallback
681
- const hasFallback = intent.intents.some((i) => i.name.toLowerCase() === "fallback");
682
- if (!hasFallback) {
683
- suggestions.push("Consider adding a 'Fallback' intent for unmatched queries");
684
- }
685
- // Check tool intents have tool config
686
- for (const i of intent.intents) {
687
- if (i.handler === "tool" && !i.tool_config && (intent.tools?.length ?? 0) === 0) {
688
- missing.push(`tool for intent "${i.name}"`);
689
- questions.push(`What tool should handle the "${i.name}" intent? (e.g., ServiceNow, Salesforce)`);
690
- }
691
- }
692
- }
693
- // Tool validation
694
- if (intent.tools && intent.tools.length > 0 && !intent.constraints?.require_hitl) {
695
- suggestions.push("External tools detected - consider enabling HITL for safety");
696
- }
697
- // Voice-specific validation
698
- if (intent.persona_type === "voice") {
699
- if (!intent.voice_config?.welcome_message) {
700
- suggestions.push("Voice AI should have a welcome message");
701
- }
702
- if (!intent.voice_config?.hangup_instructions) {
703
- suggestions.push("Voice AI should have hangup instructions");
704
- }
705
- }
706
- // ═══════════════════════════════════════════════════════════════════════════
707
- // ACTION CHAIN VALIDATION - Critical for end-to-end flows
708
- // ═══════════════════════════════════════════════════════════════════════════
709
- if (intent.action_chains && intent.action_chains.length > 0) {
710
- for (const chain of intent.action_chains) {
711
- // Validate doc_to_email chain
712
- if (chain.id === "doc_to_email") {
713
- // Check if we have email config
714
- if (!intent.delivery_config?.methods?.some((m) => m.type === "email")) {
715
- questions.push("Who should receive the email? (client, advisor, specific email)");
716
- missing.push("email_recipient");
717
- }
718
- // Check for document config
719
- if (!intent.delivery_config?.methods?.some((m) => m.type === "document")) {
720
- questions.push("What type of document should be generated? (brief, report, summary)");
721
- missing.push("document_type");
722
- }
723
- // CRITICAL: Remind about proper wiring
724
- suggestions.push("Document → Email chain detected. IMPORTANT: document_link output is DOCUMENT type. " +
725
- "Use named_inputs (not attachment_links) to attach documents to emails.");
726
- }
727
- // Validate entity_to_scoped_search chain
728
- if (chain.id === "entity_to_scoped_search") {
729
- if (!intent.entities || intent.entities.length === 0) {
730
- questions.push("What entities should be extracted? (client name, advisor, ticker symbol)");
731
- missing.push("entities_to_extract");
732
- }
733
- }
734
- // Validate actor_identification chain
735
- if (chain.id === "actor_identification") {
736
- questions.push("How should unknown actors be validated? (phone number, PIN, name lookup)");
737
- suggestions.push("Actor identification detected. Consider adding validation for unknown callers.");
738
- }
739
- }
740
- }
741
- // ═══════════════════════════════════════════════════════════════════════════
742
- // ENTITY VALIDATION
743
- // ═══════════════════════════════════════════════════════════════════════════
744
- if (intent.entities && intent.entities.length > 0) {
745
- for (const entity of intent.entities) {
746
- // Check if entity usage is clear
747
- if (!entity.use_for || entity.use_for.length === 0) {
748
- questions.push(`What should the extracted "${entity.name}" be used for?`);
749
- }
750
- // Email recipient validation
751
- if (entity.use_for?.includes("email_recipient")) {
752
- suggestions.push(`Ensure ${entity.name} entity extraction includes email address field for email delivery.`);
753
- }
754
- }
755
- }
756
- // ═══════════════════════════════════════════════════════════════════════════
757
- // DELIVERY VALIDATION
758
- // ═══════════════════════════════════════════════════════════════════════════
759
- if (intent.delivery_config) {
760
- // Email delivery checks
761
- const emailMethod = intent.delivery_config.methods?.find((m) => m.type === "email");
762
- if (emailMethod) {
763
- const emailConfig = emailMethod.config;
764
- if (emailConfig?.include_attachments && !emailConfig.attachment_source) {
765
- questions.push("What should be attached to the email? (generated document, search results)");
766
- missing.push("email_attachment_source");
767
- }
768
- if (emailConfig?.recipient_source === "static" && !emailConfig.static_recipient) {
769
- questions.push("What email address should receive the message?");
770
- missing.push("email_recipient_address");
771
- }
772
- if (emailConfig?.recipient_source === "extracted" && (!intent.entities || intent.entities.length === 0)) {
773
- questions.push("How should the recipient email be determined? (extract from conversation, user specifies)");
774
- missing.push("recipient_extraction_config");
775
- }
776
- // Confirm send
777
- if (!intent.delivery_config.requires_confirmation) {
778
- suggestions.push("Email delivery detected. Consider requiring confirmation before sending.");
779
- }
780
- }
781
- // Document delivery checks
782
- const docMethod = intent.delivery_config.methods?.find((m) => m.type === "document");
783
- if (docMethod) {
784
- const docConfig = docMethod.config;
785
- if (!docConfig?.document_type) {
786
- questions.push("What type of document should be generated? (brief, report, analysis)");
787
- missing.push("document_type");
788
- }
789
- if (!docConfig?.content_source) {
790
- questions.push("Where should document content come from? (search results, generated, template)");
791
- missing.push("document_content_source");
792
- }
793
- }
794
- }
795
- // Calculate confidence
796
- let confidence;
797
- if (missing.length === 0 && questions.length === 0) {
798
- confidence = "high";
799
- }
800
- else if (missing.length <= 1) {
801
- confidence = "medium";
802
- }
803
- else {
804
- confidence = "low";
805
- }
806
- return {
807
- complete: missing.length === 0,
808
- confidence,
809
- missing,
810
- questions,
811
- suggestions,
812
- };
813
- }
814
- // ─────────────────────────────────────────────────────────────────────────────
815
- // Comprehensive Intent Confidence Analysis
816
- // ─────────────────────────────────────────────────────────────────────────────
817
- /**
818
- * Calculate comprehensive confidence in our understanding of the user's intent.
819
- * This is the primary driver for deciding whether to proceed or ask questions.
820
- *
821
- * @param intent - The parsed workflow intent
822
- * @param originalText - The original user input (for context analysis)
823
- * @returns Detailed confidence analysis with recommended action
824
- */
825
- export function calculateIntentConfidence(intent, originalText) {
826
- const understood = [];
827
- const uncertain = [];
828
- const blockers = [];
829
- const questions = [];
830
- // ═══════════════════════════════════════════════════════════════════════════
831
- // 1. GOAL UNDERSTANDING - Why does the user want this?
832
- // ═══════════════════════════════════════════════════════════════════════════
833
- const goalSignals = [];
834
- let goalScore = 0;
835
- // Check if we have clear intent definitions
836
- if (intent.intents && intent.intents.length > 0) {
837
- goalScore += 30;
838
- goalSignals.push(`${intent.intents.length} intent(s) identified`);
839
- // Check intent clarity
840
- const hasDescriptions = intent.intents.every(i => i.description && i.description.length > 10);
841
- if (hasDescriptions) {
842
- goalScore += 20;
843
- goalSignals.push("All intents have clear descriptions");
844
- }
845
- else {
846
- goalSignals.push("Some intents lack clear descriptions");
847
- uncertain.push("Purpose of some intents unclear");
848
- }
849
- // Check if handlers are specified
850
- const hasHandlers = intent.intents.every(i => i.handler);
851
- if (hasHandlers) {
852
- goalScore += 15;
853
- goalSignals.push("All intents have defined handlers");
854
- }
855
- }
856
- else {
857
- goalSignals.push("No explicit intents defined");
858
- uncertain.push("Primary goal unclear");
859
- }
860
- // Check for recognized patterns (shows we understand the "shape" of what they want)
861
- if (intent.action_chains && intent.action_chains.length > 0) {
862
- goalScore += 25;
863
- goalSignals.push(`Recognized pattern: ${intent.action_chains.map(c => c.name).join(", ")}`);
864
- understood.push(`Workflow pattern: ${intent.action_chains[0].name}`);
865
- }
866
- // Check for persona type (basic understanding)
867
- if (intent.persona_type) {
868
- goalScore += 10;
869
- goalSignals.push(`Persona type: ${intent.persona_type}`);
870
- understood.push(`Type: ${intent.persona_type} workflow`);
871
- }
872
- const goalUnderstanding = {
873
- score: Math.min(goalScore, 100),
874
- level: goalScore >= 70 ? "high" : goalScore >= 40 ? "medium" : goalScore > 0 ? "low" : "unknown",
875
- reason: goalScore >= 70
876
- ? "Clear understanding of user's objective"
877
- : goalScore >= 40
878
- ? "Partial understanding of objective - some aspects unclear"
879
- : "Limited understanding of why user wants this workflow",
880
- signals: goalSignals,
881
- };
882
- if (goalScore < 40) {
883
- questions.push({
884
- id: "goal_clarification",
885
- question: "What is the main goal you're trying to achieve with this workflow?",
886
- category: "goal_understanding",
887
- priority: "critical",
888
- context: "Understanding the 'why' helps us design the right solution",
889
- options: ["Answer questions", "Generate content", "Process documents", "Send communications", "Search & analyze data"],
890
- });
891
- }
892
- // ═══════════════════════════════════════════════════════════════════════════
893
- // 2. INPUT REQUIREMENTS - What data is needed?
894
- // ═══════════════════════════════════════════════════════════════════════════
895
- const inputSignals = [];
896
- let inputScore = 0;
897
- // Check for entity definitions
898
- if (intent.entities && intent.entities.length > 0) {
899
- inputScore += 40;
900
- inputSignals.push(`${intent.entities.length} entity type(s) identified: ${intent.entities.map(e => e.type).join(", ")}`);
901
- understood.push(`Required entities: ${intent.entities.map(e => e.type).join(", ")}`);
902
- // Check if entities have clear use_for
903
- const entitiesWithUse = intent.entities.filter(e => e.use_for && e.use_for.length > 0);
904
- if (entitiesWithUse.length === intent.entities.length) {
905
- inputScore += 20;
906
- inputSignals.push("All entities have defined usage");
907
- }
908
- else {
909
- uncertain.push("How some entities will be used");
910
- }
911
- }
912
- else {
913
- // Try to infer from action chains
914
- if (intent.action_chains?.some(c => c.id === "doc_to_email" || c.id === "email_with_validation")) {
915
- inputSignals.push("Email workflow detected - will need recipient email");
916
- inputScore += 20;
917
- uncertain.push("Email recipient source not specified");
918
- questions.push({
919
- id: "email_recipient_source",
920
- question: "Where will the recipient email address come from?",
921
- category: "input_requirements",
922
- priority: "critical",
923
- context: "Email workflows require a valid email address - need to know the source",
924
- options: ["Extract from conversation", "User provides it", "From CRM/database", "Fixed recipient"],
925
- });
926
- }
927
- }
928
- // Check for data sources
929
- if (intent.data_sources && intent.data_sources.length > 0) {
930
- inputScore += 30;
931
- inputSignals.push(`Data sources specified: ${intent.data_sources.join(", ")}`);
932
- understood.push(`Data sources: ${intent.data_sources.join(", ")}`);
933
- }
934
- else if (intent.intents?.some(i => i.handler === "search")) {
935
- inputSignals.push("Search intent but no data sources specified");
936
- uncertain.push("What data sources to search");
937
- }
938
- const inputRequirements = {
939
- score: Math.min(inputScore, 100),
940
- level: inputScore >= 70 ? "high" : inputScore >= 40 ? "medium" : inputScore > 0 ? "low" : "unknown",
941
- reason: inputScore >= 70
942
- ? "Clear understanding of required inputs and data"
943
- : inputScore >= 40
944
- ? "Some inputs identified but gaps remain"
945
- : "Unclear what data/inputs are needed",
946
- signals: inputSignals,
947
- };
948
- // ═══════════════════════════════════════════════════════════════════════════
949
- // 3. OUTPUT EXPECTATIONS - What should the result look like?
950
- // ═══════════════════════════════════════════════════════════════════════════
951
- const outputSignals = [];
952
- let outputScore = 0;
953
- // Check for delivery config
954
- if (intent.delivery_config) {
955
- outputScore += 40;
956
- outputSignals.push(`Delivery method: ${intent.delivery_config.method}`);
957
- understood.push(`Delivery: ${intent.delivery_config.method}`);
958
- if (intent.delivery_config.requires_confirmation) {
959
- outputScore += 10;
960
- outputSignals.push("Confirmation required before delivery");
961
- }
962
- }
963
- else {
964
- outputSignals.push("Delivery method not specified");
965
- uncertain.push("How results should be delivered");
966
- }
967
- // Check intent handlers for output hints
968
- const outputHandlers = intent.intents?.filter(i => i.handler === "document" ||
969
- i.handler === "email" ||
970
- i.document_config ||
971
- i.email_config) ?? [];
972
- if (outputHandlers.length > 0) {
973
- outputScore += 30;
974
- outputSignals.push(`Output-producing handlers: ${outputHandlers.length}`);
975
- }
976
- // Check for specific output configurations
977
- if (intent.intents?.some(i => i.document_config?.document_type)) {
978
- outputScore += 20;
979
- outputSignals.push("Document type specified");
980
- understood.push("Document generation required");
981
- }
982
- const outputExpectations = {
983
- score: Math.min(outputScore, 100),
984
- level: outputScore >= 70 ? "high" : outputScore >= 40 ? "medium" : outputScore > 0 ? "low" : "unknown",
985
- reason: outputScore >= 70
986
- ? "Clear understanding of expected outputs"
987
- : outputScore >= 40
988
- ? "Partial understanding of expected outputs"
989
- : "Unclear what the workflow should produce",
990
- signals: outputSignals,
991
- };
992
- if (outputScore < 40) {
993
- questions.push({
994
- id: "output_format",
995
- question: "What should the output of this workflow look like?",
996
- category: "output_expectations",
997
- priority: "important",
998
- context: "Understanding the expected output helps design the right response format",
999
- options: ["Text response in chat", "Generated document", "Email to someone", "Data update", "Multiple outputs"],
1000
- });
1001
- }
1002
- // ═══════════════════════════════════════════════════════════════════════════
1003
- // 4. PATTERN RECOGNITION - Do we recognize this workflow type?
1004
- // ═══════════════════════════════════════════════════════════════════════════
1005
- const patternSignals = [];
1006
- let patternScore = 0;
1007
- if (intent.action_chains && intent.action_chains.length > 0) {
1008
- patternScore += 60;
1009
- for (const chain of intent.action_chains) {
1010
- patternSignals.push(`Matched pattern: ${chain.name}`);
1011
- understood.push(`Pattern: ${chain.description}`);
1012
- }
1013
- }
1014
- // Check for common patterns even without explicit action chains
1015
- const hasSearch = intent.intents?.some(i => i.handler === "search");
1016
- const hasLlm = intent.intents?.some(i => i.handler === "llm");
1017
- const hasDocument = intent.intents?.some(i => i.handler === "document" || i.document_config);
1018
- const hasEmail = intent.intents?.some(i => i.handler === "email" || i.email_config);
1019
- if (hasSearch && hasLlm && !intent.action_chains?.length) {
1020
- patternScore += 30;
1021
- patternSignals.push("Search + LLM pattern (RAG-style)");
1022
- }
1023
- if (hasDocument && hasEmail && !intent.action_chains?.some(c => c.id === "doc_to_email")) {
1024
- patternSignals.push("Document + Email detected but chain not matched");
1025
- uncertain.push("Document-to-email flow not fully specified");
1026
- }
1027
- const patternRecognition = {
1028
- score: Math.min(patternScore, 100),
1029
- level: patternScore >= 60 ? "high" : patternScore >= 30 ? "medium" : patternScore > 0 ? "low" : "unknown",
1030
- reason: patternScore >= 60
1031
- ? "Recognized workflow pattern with known best practices"
1032
- : patternScore >= 30
1033
- ? "Partial pattern match - may need customization"
1034
- : "No recognized pattern - custom workflow needed",
1035
- signals: patternSignals,
1036
- };
1037
- // ═══════════════════════════════════════════════════════════════════════════
1038
- // 5. DATA COMPLETENESS - Are required fields specified?
1039
- // ═══════════════════════════════════════════════════════════════════════════
1040
- const dataSignals = [];
1041
- let dataScore = 50; // Start at 50, deduct for missing critical data
1042
- // Check action chain requirements
1043
- if (intent.action_chains) {
1044
- for (const chain of intent.action_chains) {
1045
- if (chain.id === "doc_to_email" || chain.id === "email_with_validation") {
1046
- // Email requires recipient - check entities by name or use_for
1047
- const hasEmailEntity = intent.entities?.some(e => e.name?.toLowerCase().includes("email") ||
1048
- e.name?.toLowerCase().includes("recipient") ||
1049
- e.use_for?.some(u => u.includes("email") || u.includes("recipient")));
1050
- if (!hasEmailEntity) {
1051
- dataScore -= 30;
1052
- dataSignals.push("Email recipient not specified");
1053
- blockers.push({
1054
- category: "data_completeness",
1055
- what: "Email recipient",
1056
- why: "Cannot send email without knowing who to send it to",
1057
- impact: "blocking",
1058
- });
1059
- questions.push({
1060
- id: "email_recipient",
1061
- question: "Who should receive the email?",
1062
- category: "data_completeness",
1063
- priority: "critical",
1064
- context: "Email recipient is required to send the email",
1065
- });
1066
- }
1067
- else {
1068
- dataSignals.push("Email recipient source identified");
1069
- }
1070
- }
1071
- if (chain.id === "doc_to_email") {
1072
- // Document type should be specified
1073
- if (!intent.intents?.some(i => i.document_config?.document_type)) {
1074
- dataScore -= 15;
1075
- dataSignals.push("Document type not specified");
1076
- uncertain.push("What type of document to generate");
1077
- }
1078
- }
1079
- }
1080
- }
1081
- // Check for incomplete entity definitions (entities without clear usage)
1082
- const incompleteEntities = intent.entities?.filter(e => !e.use_for?.length) ?? [];
1083
- if (incompleteEntities.length > 0) {
1084
- dataScore -= 10;
1085
- dataSignals.push(`${incompleteEntities.length} entities with incomplete definitions`);
1086
- }
1087
- dataScore = Math.max(0, dataScore);
1088
- const dataCompleteness = {
1089
- score: dataScore,
1090
- level: dataScore >= 70 ? "high" : dataScore >= 40 ? "medium" : dataScore > 0 ? "low" : "unknown",
1091
- reason: dataScore >= 70
1092
- ? "Required data and fields are specified"
1093
- : dataScore >= 40
1094
- ? "Some required data missing but workflow may still work"
1095
- : "Critical data missing - cannot proceed",
1096
- signals: dataSignals,
1097
- };
1098
- // ═══════════════════════════════════════════════════════════════════════════
1099
- // 6. CONSTRAINTS CLARITY - Do we understand rules and constraints?
1100
- // ═══════════════════════════════════════════════════════════════════════════
1101
- const constraintSignals = [];
1102
- let constraintScore = 50; // Start at 50 (neutral if no constraints)
1103
- // Check for confirmation requirements
1104
- if (intent.delivery_config?.requires_confirmation) {
1105
- constraintScore += 20;
1106
- constraintSignals.push("Confirmation requirement specified");
1107
- understood.push("Requires confirmation before action");
1108
- }
1109
- // Check for HITL patterns in action chains
1110
- const needsHitl = intent.action_chains?.some(c => c.id === "email_with_validation" ||
1111
- c.id === "extract_validate_action" ||
1112
- c.steps?.some(s => s.action_type === "hitl"));
1113
- if (needsHitl) {
1114
- constraintScore += 15;
1115
- constraintSignals.push("HITL requirement recognized");
1116
- understood.push("Human approval needed before side effects");
1117
- }
1118
- else if (intent.action_chains?.some(c => c.id.includes("email"))) {
1119
- // Email without HITL - potential issue
1120
- constraintSignals.push("Email action without explicit HITL requirement");
1121
- uncertain.push("Whether confirmation is needed before sending");
1122
- }
1123
- const constraintsClarity = {
1124
- score: Math.min(constraintScore, 100),
1125
- level: constraintScore >= 70 ? "high" : constraintScore >= 40 ? "medium" : constraintScore > 0 ? "low" : "unknown",
1126
- reason: constraintScore >= 70
1127
- ? "Constraints and rules are clear"
1128
- : constraintScore >= 40
1129
- ? "Some constraints understood but may need defaults"
1130
- : "Unclear what rules or constraints apply",
1131
- signals: constraintSignals,
1132
- };
1133
- // ═══════════════════════════════════════════════════════════════════════════
1134
- // CALCULATE OVERALL CONFIDENCE
1135
- // ═══════════════════════════════════════════════════════════════════════════
1136
- // Weighted average with goal understanding being most important
1137
- const weights = {
1138
- goal_understanding: 0.25,
1139
- input_requirements: 0.20,
1140
- output_expectations: 0.15,
1141
- pattern_recognition: 0.15,
1142
- data_completeness: 0.15,
1143
- constraints_clarity: 0.10,
1144
- };
1145
- const overall = Math.round(goalUnderstanding.score * weights.goal_understanding +
1146
- inputRequirements.score * weights.input_requirements +
1147
- outputExpectations.score * weights.output_expectations +
1148
- patternRecognition.score * weights.pattern_recognition +
1149
- dataCompleteness.score * weights.data_completeness +
1150
- constraintsClarity.score * weights.constraints_clarity);
1151
- // Determine level and recommendation
1152
- let level;
1153
- let recommendation;
1154
- if (blockers.length > 0) {
1155
- level = "insufficient";
1156
- recommendation = "clarify_critical";
1157
- }
1158
- else if (overall >= 70) {
1159
- level = "high";
1160
- recommendation = questions.length > 0 ? "clarify_recommended" : "proceed";
1161
- }
1162
- else if (overall >= 45) {
1163
- level = "medium";
1164
- recommendation = questions.some(q => q.priority === "critical") ? "clarify_critical" : "clarify_recommended";
1165
- }
1166
- else {
1167
- level = "low";
1168
- recommendation = "insufficient_info";
1169
- }
1170
- // Sort questions by priority
1171
- questions.sort((a, b) => {
1172
- const priorityOrder = { critical: 0, important: 1, nice_to_have: 2 };
1173
- return priorityOrder[a.priority] - priorityOrder[b.priority];
1174
- });
1175
- return {
1176
- overall,
1177
- level,
1178
- breakdown: {
1179
- goal_understanding: goalUnderstanding,
1180
- input_requirements: inputRequirements,
1181
- output_expectations: outputExpectations,
1182
- pattern_recognition: patternRecognition,
1183
- data_completeness: dataCompleteness,
1184
- constraints_clarity: constraintsClarity,
1185
- },
1186
- understood,
1187
- uncertain,
1188
- blockers,
1189
- recommendation,
1190
- clarification_questions: questions,
1191
- };
1192
- }
1193
- /**
1194
- * Get a human-readable summary of intent confidence
1195
- */
1196
- export function summarizeIntentConfidence(confidence) {
1197
- const lines = [];
1198
- lines.push(`## Intent Understanding: ${confidence.level.toUpperCase()} (${confidence.overall}%)`);
1199
- lines.push("");
1200
- // What we understood
1201
- if (confidence.understood.length > 0) {
1202
- lines.push("### ✅ Understood:");
1203
- for (const item of confidence.understood) {
1204
- lines.push(`- ${item}`);
1205
- }
1206
- lines.push("");
1207
- }
1208
- // What's uncertain
1209
- if (confidence.uncertain.length > 0) {
1210
- lines.push("### ⚠️ Uncertain:");
1211
- for (const item of confidence.uncertain) {
1212
- lines.push(`- ${item}`);
1213
- }
1214
- lines.push("");
1215
- }
1216
- // Blockers
1217
- if (confidence.blockers.length > 0) {
1218
- lines.push("### 🚫 Blocking Issues:");
1219
- for (const blocker of confidence.blockers) {
1220
- lines.push(`- **${blocker.what}**: ${blocker.why}`);
1221
- }
1222
- lines.push("");
1223
- }
1224
- // Recommendation
1225
- lines.push(`### Recommendation: ${confidence.recommendation.replace(/_/g, " ").toUpperCase()}`);
1226
- if (confidence.clarification_questions.length > 0) {
1227
- lines.push("");
1228
- lines.push("### Questions to Clarify:");
1229
- for (const q of confidence.clarification_questions) {
1230
- const priority = q.priority === "critical" ? "🔴" : q.priority === "important" ? "🟡" : "🟢";
1231
- lines.push(`${priority} ${q.question}`);
1232
- if (q.options) {
1233
- lines.push(` Options: ${q.options.join(", ")}`);
1234
- }
1235
- }
1236
- }
1237
- return lines.join("\n");
1238
- }
1239
- // ─────────────────────────────────────────────────────────────────────────────
1240
- // Intent to Spec Conversion
1241
- // ─────────────────────────────────────────────────────────────────────────────
1242
- /**
1243
- * Generate takeActionInstructions from tools for Voice AI.
1244
- *
1245
- * This populates the "Define what actions your AI Employee can perform" section
1246
- * in the Voice persona config. Format follows the Ema Voice API spec.
1247
- */
1248
- function generateTakeActionInstructions(tools) {
1249
- if (!tools || tools.length === 0) {
1250
- return "";
1251
- }
1252
- const cases = tools.map((tool, index) => {
1253
- const caseNum = index + 1;
1254
- const toolName = tool.display_name ?? tool.action ?? `Action ${caseNum}`;
1255
- const triggerCondition = tool.trigger_condition ?? tool.description ?? "Perform this action when requested";
1256
- // Extract required parameters from tool schema if available
1257
- const requiredParams = tool.required_inputs ?? [];
1258
- const paramsObj = requiredParams.length > 0
1259
- ? `{ ${requiredParams.map(p => `"${p}": ""`).join(", ")} }`
1260
- : "{ }";
1261
- return `</Case ${caseNum}>
1262
- ${toolName}
1263
-
1264
- Trigger When: ${triggerCondition}
1265
-
1266
- Intent for tool call: "User requests ${toolName.toLowerCase()}"
1267
-
1268
- Required parameters: ${paramsObj}
1269
- </Case ${caseNum}>`;
1270
- });
1271
- return cases.join("\n\n");
1272
- }
1273
- export function intentToSpec(intent) {
1274
- const nodes = [];
1275
- const resultMappings = [];
1276
- // 1. Add trigger - Voice and Chat both use chat_trigger, Dashboard uses document_trigger
1277
- const triggerId = "trigger";
1278
- // Note: voice_trigger doesn't exist in API - Voice AI uses chat_trigger with voiceSettings in proto_config
1279
- const triggerType = intent.persona_type === "dashboard" ? "document_trigger" : "chat_trigger";
1280
- nodes.push({
1281
- id: triggerId,
1282
- actionType: triggerType,
1283
- displayName: "Trigger",
1284
- });
1285
- // 2. Add categorizer if multiple intents
1286
- let categorizerId;
1287
- if (intent.intents && intent.intents.length > 1) {
1288
- categorizerId = "categorizer";
1289
- const categories = intent.intents.map((i) => ({
1290
- name: i.name,
1291
- description: i.description,
1292
- examples: i.examples,
1293
- }));
1294
- // Add Fallback if not present
1295
- if (!categories.some((c) => c.name.toLowerCase() === "fallback")) {
1296
- categories.push({
1297
- name: "Fallback",
1298
- description: "Query doesn't match other categories",
1299
- });
1300
- }
1301
- nodes.push({
1302
- id: categorizerId,
1303
- actionType: "chat_categorizer",
1304
- displayName: "Intent Classifier",
1305
- inputs: {
1306
- conversation: {
1307
- type: "action_output",
1308
- actionName: triggerId,
1309
- output: "chat_conversation",
1310
- },
1311
- },
1312
- categories,
1313
- });
1314
- }
1315
- // 3. Add search if data sources include KB
1316
- let searchId;
1317
- if (intent.data_sources?.some((ds) => ds.type === "knowledge_base" || ds.type === "combined")) {
1318
- searchId = "search";
1319
- nodes.push({
1320
- id: searchId,
1321
- actionType: "search",
1322
- displayName: "Knowledge Search",
1323
- inputs: {
1324
- query: {
1325
- type: "action_output",
1326
- actionName: triggerId,
1327
- output: "user_query",
1328
- },
1329
- // REQUIRED: Widget binding to fileUpload data sources (uses multiBinding.elements format)
1330
- datastore_configs: {
1331
- type: "widget_config_array",
1332
- widgetNames: ["fileUpload"],
1333
- },
1334
- },
1335
- });
1336
- }
1337
- // 4. Add web search if enabled
1338
- let webSearchId;
1339
- if (intent.data_sources?.some((ds) => ds.type === "web_search" || ds.type === "combined")) {
1340
- webSearchId = "web_search";
1341
- nodes.push({
1342
- id: webSearchId,
1343
- actionType: "live_web_search",
1344
- displayName: "Web Search",
1345
- inputs: {
1346
- query: {
1347
- type: "action_output",
1348
- actionName: triggerId,
1349
- output: "user_query",
1350
- },
1351
- },
1352
- });
1353
- }
1354
- // 5. Add tool caller if tools defined
1355
- // DISABLED: external_action_caller format causes 500 errors
1356
- // TODO: Fix tool format and re-enable - tools are stored in intent for future use
1357
- const toolCallerId = undefined;
1358
- // if (intent.tools && intent.tools.length > 0) {
1359
- // toolCallerId = "tool_caller";
1360
- // nodes.push({...});
1361
- // }
1362
- // 6. Add HITL if required
1363
- let hitlId;
1364
- if (intent.constraints?.require_hitl && toolCallerId) {
1365
- hitlId = "hitl";
1366
- nodes.push({
1367
- id: hitlId,
1368
- actionType: "general_hitl",
1369
- displayName: "Human Approval",
1370
- inputs: {
1371
- query: {
1372
- type: "action_output",
1373
- actionName: toolCallerId,
1374
- output: "tool_execution_result",
1375
- },
1376
- },
1377
- });
1378
- }
1379
- // 7. Add response node
1380
- const respondId = "respond";
1381
- const respondInputs = {
1382
- query: {
1383
- type: "action_output",
1384
- actionName: triggerId,
1385
- output: "user_query",
1386
- },
1387
- // REQUIRED: Widget binding to fusionModel for LLM selection
1388
- model_config: {
1389
- type: "widget_config",
1390
- widgetName: "fusionModel",
1391
- },
1392
- };
1393
- if (searchId) {
1394
- // call_llm uses "named_inputs" not "search_results"
1395
- // (respond_with_sources doesn't exist in API - mapped to call_llm)
1396
- respondInputs.named_inputs = {
1397
- type: "action_output",
1398
- actionName: searchId,
1399
- output: "search_results",
1400
- };
1401
- }
1402
- nodes.push({
1403
- id: respondId,
1404
- // respond_with_sources doesn't exist in API - always use call_llm
1405
- actionType: "call_llm",
1406
- displayName: "Response",
1407
- inputs: respondInputs,
1408
- });
1409
- resultMappings.push({
1410
- nodeId: respondId,
1411
- output: searchId ? "response_with_sources" : "response_with_sources",
1412
- });
1413
- // NOTE: Action chains (email_with_validation, doc_to_email, etc.) are NOT processed here.
1414
- // Complex workflows with action chains should be generated via LLM using:
1415
- // 1. generateWorkflowPrompt(intent, availableActions) - builds LLM prompt
1416
- // 2. Pass to Auto Builder or external LLM
1417
- // 3. Parse the LLM response as WorkflowSpec
1418
- // This keeps intentToSpec simple (basic RAG workflow) and lets LLM handle complexity.
1419
- // Build voiceConfig from intent.voice_config
1420
- const voiceConfig = intent.persona_type === "voice" ? {
1421
- welcomeMessage: intent.voice_config?.welcome_message,
1422
- identityAndPurpose: intent.voice_config?.identity ?? intent.description,
1423
- hangupInstructions: intent.voice_config?.hangup_instructions,
1424
- transferInstructions: intent.voice_config?.transfer_instructions,
1425
- waitMessage: intent.voice_config?.wait_message,
1426
- // Generate takeActionInstructions from tools if not explicitly provided
1427
- takeActionInstructions: intent.voice_config?.take_action_instructions ??
1428
- generateTakeActionInstructions(intent.tools),
1429
- } : undefined;
1430
- return {
1431
- name: intent.name,
1432
- description: intent.description,
1433
- personaType: intent.persona_type,
1434
- nodes,
1435
- resultMappings,
1436
- ...(voiceConfig && { voiceConfig }),
1437
- ...(intent.persona_type === "chat" && { chatConfig: { name: intent.name } }),
1438
- };
1439
- }
1440
- /**
1441
- * Parse input and extract workflow intent.
1442
- *
1443
- * Note: The "natural_language" path uses deprecated regex-based parsing.
1444
- * For new code, prefer providing structured input (partial_spec or full_spec)
1445
- * directly from the Agent, which understands user intent naturally.
1446
- */
1447
- export function parseInput(input) {
1448
- const inputType = detectInputType(input);
1449
- let intent;
1450
- switch (inputType) {
1451
- case "natural_language":
1452
- // Create minimal intent - no regex parsing
1453
- // Intent Architect and generateWorkflow() will handle complexity
1454
- const text = String(input);
1455
- // Detect persona type from args if available, otherwise default to chat
1456
- let personaType = "chat";
1457
- // Simple detection - can be overridden by args.type in handler
1458
- if (/voice|call|phone/i.test(text)) {
1459
- personaType = "voice";
1460
- }
1461
- else if (/document|batch|upload/i.test(text)) {
1462
- personaType = "dashboard";
1463
- }
1464
- intent = createMinimalIntentFromText(text, personaType);
1465
- break;
1466
- case "partial_spec":
1467
- intent = parsePartialSpec(input);
1468
- break;
1469
- case "full_spec":
1470
- // Full spec: extract intent from the spec
1471
- const spec = input;
1472
- intent = {
1473
- name: spec.name,
1474
- description: spec.description,
1475
- persona_type: spec.personaType,
1476
- };
1477
- break;
1478
- case "existing_workflow":
1479
- // Would need to reverse-engineer from workflow_def
1480
- // For now, return minimal intent
1481
- intent = {
1482
- name: "Existing Workflow",
1483
- description: "Imported from existing workflow_def",
1484
- persona_type: "chat",
1485
- };
1486
- break;
1487
- }
1488
- const validation = validateIntent(intent);
1489
- return { intent, input_type: inputType, validation };
1490
- }
1491
- // ─────────────────────────────────────────────────────────────────────────────
1492
- // LLM-Based Semantic Extraction
1493
- // ─────────────────────────────────────────────────────────────────────────────
1494
- /**
1495
- * Generate a prompt for LLM-based output semantics extraction.
1496
- * This should be called by tools that have access to an LLM.
1497
- *
1498
- * @param userInput - The user's natural language request
1499
- * @returns Prompt and schema for LLM extraction
1500
- */
1501
- export function generateOutputSemanticsPrompt(userInput) {
1502
- const systemPrompt = `You are an expert at understanding user requests and extracting semantic attributes about desired outputs.
1503
-
1504
- Given a user's request, extract the following:
1505
- - OUTPUT TYPE: What kind of output are they asking for? (document, email, report, brief, analysis, etc.)
1506
- - FORMAT: How should it be formatted? (document, email, chat response, file type)
1507
- - TONE: What tone is appropriate? (formal, professional, casual, friendly)
1508
- - STYLE: What writing style? (analytical, persuasive, informative, instructional)
1509
- - AUDIENCE: Who is this for? (client, internal, executive, technical)
1510
- - PURPOSE: What is the goal? (inform, persuade, summarize, analyze, recommend)
1511
- - LENGTH: How detailed? (brief, standard, detailed, comprehensive)
1512
-
1513
- Be precise but infer reasonable defaults when not explicitly stated.
1514
- Consider context clues - "send to client" implies external/client audience, "quick summary" implies brief length.`;
1515
- const userPrompt = `Analyze this user request and extract the output semantic attributes:
1516
-
1517
- "${userInput}"
1518
-
1519
- Extract the attributes using the provided schema. Include your reasoning.`;
1520
- return {
1521
- system_prompt: systemPrompt,
1522
- user_prompt: userPrompt,
1523
- schema: OUTPUT_SEMANTICS_EXTRACTION_SCHEMA,
1524
- };
1525
- }
1526
- /**
1527
- * Apply LLM-extracted semantics to an existing intent.
1528
- * Call this after getting LLM extraction results.
1529
- *
1530
- * @param intent - The current workflow intent
1531
- * @param extracted - The LLM-extracted output semantics
1532
- * @returns Updated intent with semantics applied
1533
- */
1534
- export function applyExtractedSemantics(intent, extracted) {
1535
- return {
1536
- ...intent,
1537
- output_semantics: extracted,
1538
- // Also update delivery config based on extracted info
1539
- delivery_config: {
1540
- ...intent.delivery_config,
1541
- method: extracted.output_type.category === "communication" ? "email" :
1542
- extracted.output_type.category === "document" ? "document" :
1543
- intent.delivery_config?.method ?? "response",
1544
- requires_confirmation: extracted.output_type.requires_delivery ||
1545
- extracted.purpose.action_required ||
1546
- intent.delivery_config?.requires_confirmation,
1547
- },
1548
- };
1549
- }
1550
- /**
1551
- * Generate LLM instructions based on extracted semantics.
1552
- * Use these instructions in call_llm prompts for consistent output.
1553
- *
1554
- * @param semantics - The extracted output semantics
1555
- * @returns Instructions to include in LLM prompts
1556
- */
1557
- export function generateContentInstructions(semantics) {
1558
- const instructions = [];
1559
- // Tone instructions
1560
- instructions.push(`TONE: Write in a ${semantics.tone.formality} tone with ${semantics.tone.voice} voice.`);
1561
- if (semantics.tone.sentiment === "urgent") {
1562
- instructions.push("Convey urgency appropriately.");
1563
- }
1564
- // Style instructions
1565
- instructions.push(`STYLE: Use ${semantics.style.approach} approach with ${semantics.style.detail_level} detail.`);
1566
- if (semantics.style.use_citations) {
1567
- instructions.push("Include citations to source documents.");
1568
- }
1569
- if (semantics.style.use_examples) {
1570
- instructions.push("Include relevant examples where appropriate.");
1571
- }
1572
- // Audience instructions
1573
- instructions.push(`AUDIENCE: Writing for ${semantics.audience.type} audience with ${semantics.audience.familiarity} familiarity.`);
1574
- if (semantics.audience.relationship) {
1575
- instructions.push(`The recipient is: ${semantics.audience.relationship}.`);
1576
- }
1577
- // Format instructions
1578
- instructions.push(`FORMAT: Structure content as ${semantics.format.structure ?? "sections"} format.`);
1579
- // Length instructions
1580
- const lengthGuidance = {
1581
- brief: "Keep it concise - 1-2 paragraphs or bullet points.",
1582
- standard: "Provide balanced coverage - 3-5 paragraphs with key sections.",
1583
- detailed: "Be thorough - cover all aspects with supporting details.",
1584
- comprehensive: "Be exhaustive - include all relevant information with deep analysis.",
1585
- };
1586
- instructions.push(`LENGTH: ${lengthGuidance[semantics.length]}`);
1587
- // Purpose instructions
1588
- instructions.push(`PURPOSE: Primary goal is to ${semantics.purpose.primary}.`);
1589
- if (semantics.purpose.action_required) {
1590
- instructions.push("Include clear call-to-action or next steps.");
1591
- }
1592
- if (semantics.purpose.decision_support) {
1593
- instructions.push("Present information to support decision-making.");
1594
- }
1595
- // Formatting requirements
1596
- if (semantics.formatting_requirements && semantics.formatting_requirements.length > 0) {
1597
- instructions.push(`SPECIFIC REQUIREMENTS: ${semantics.formatting_requirements.join(", ")}`);
1598
- }
1599
- return instructions.join("\n");
1600
- }
1601
- /**
1602
- * Determine if intent requires LLM-driven generation or can use simple intentToSpec.
1603
- *
1604
- * Simple (use intentToSpec):
1605
- * - Basic Q&A with search
1606
- * - Single intent without delivery
1607
- *
1608
- * Complex (use LLM):
1609
- * - Action chains (email, document generation)
1610
- * - Multiple intents with routing
1611
- * - HITL requirements
1612
- * - External tool calls
1613
- */
1614
- export function needsLLMGeneration(intent) {
1615
- // Check for action chains
1616
- if (intent.action_chains && intent.action_chains.length > 0) {
1617
- return true;
1618
- }
1619
- // Check for email/document delivery
1620
- if (intent.delivery_config?.methods?.some(m => m.type === "email" || m.type === "document")) {
1621
- return true;
1622
- }
1623
- // Check for HITL requirements
1624
- if (intent.constraints?.require_hitl) {
1625
- return true;
1626
- }
1627
- // Check for multiple intents (needs categorizer with complex routing)
1628
- if (intent.intents && intent.intents.length > 2) {
1629
- return true;
1630
- }
1631
- // Check for tools/external actions
1632
- if (intent.tools && intent.tools.length > 0) {
1633
- return true;
1634
- }
1635
- return false;
1636
- }
1637
- /**
1638
- * Generate a workflow spec or LLM prompt based on intent complexity.
1639
- *
1640
- * For simple intents: returns spec directly
1641
- * For complex intents: returns LLM prompt with full context
1642
- *
1643
- * @param intent - Parsed workflow intent
1644
- * @param availableActions - Actions from ListActions API (optional)
1645
- * @returns Generation result with either spec or LLM prompt
1646
- */
1647
- export function generateWorkflow(intent, availableActions) {
1648
- const complexity = {
1649
- has_action_chains: !!(intent.action_chains && intent.action_chains.length > 0),
1650
- has_email: !!(intent.delivery_config?.methods?.some(m => m.type === "email")),
1651
- has_hitl: !!intent.constraints?.require_hitl,
1652
- has_multi_intent: !!(intent.intents && intent.intents.length > 2),
1653
- chain_ids: intent.action_chains?.map(c => c.id) ?? [],
1654
- };
1655
- // Simple workflow - use intentToSpec
1656
- if (!needsLLMGeneration(intent)) {
1657
- return {
1658
- needs_llm: false,
1659
- spec: intentToSpec(intent),
1660
- reason: "Simple workflow (basic RAG pattern) - no LLM needed",
1661
- complexity,
1662
- };
1663
- }
1664
- // Complex workflow - generate LLM prompt
1665
- const systemPrompt = buildWorkflowGenerationSystemPrompt(availableActions);
1666
- const userPrompt = buildWorkflowGenerationUserPrompt(intent);
1667
- return {
1668
- needs_llm: true,
1669
- llm_prompt: {
1670
- system: systemPrompt,
1671
- user: userPrompt,
1672
- },
1673
- reason: `Complex workflow requires LLM: ${complexity.chain_ids.length > 0 ? `chains: ${complexity.chain_ids.join(', ')}` : ''} ${complexity.has_email ? 'email' : ''} ${complexity.has_hitl ? 'HITL' : ''}`.trim(),
1674
- complexity,
1675
- };
1676
- }
1677
- /**
1678
- * Build system prompt for LLM workflow generation.
1679
- */
1680
- function buildWorkflowGenerationSystemPrompt(availableActions) {
1681
- const actionList = availableActions
1682
- ? availableActions.map(a => `- ${a.name}: ${a.description} (inputs: ${a.inputs.join(', ')})`).join('\n')
1683
- : 'Use standard Ema actions: chat_trigger, search, call_llm, entity_extraction, general_hitl, send_email, chat_categorizer';
1684
- return `You are an expert workflow designer for the Ema AI platform.
1685
-
1686
- Your task is to generate a WorkflowSpec JSON that implements the user's requirements.
1687
-
1688
- ## Available Actions
1689
- ${actionList}
1690
-
1691
- ## WorkflowSpec Schema
1692
- \`\`\`typescript
1693
- interface WorkflowSpec {
1694
- name: string;
1695
- description: string;
1696
- personaType: "voice" | "chat" | "dashboard";
1697
- nodes: Node[];
1698
- resultMappings: { nodeId: string; output: string }[];
1699
- }
1700
-
1701
- interface Node {
1702
- id: string; // Unique identifier (snake_case)
1703
- actionType: string; // Action name from available actions
1704
- displayName: string; // Human-readable name
1705
- inputs?: Record<string, InputBinding>;
1706
- runIf?: { actionName: string; output: string; enumValue: string }; // Conditional execution
1707
- categories?: Category[]; // For categorizer nodes
1708
- }
1709
-
1710
- interface InputBinding {
1711
- type: "action_output" | "inline_string" | "widget_config";
1712
- actionName?: string; // For action_output
1713
- output?: string; // For action_output
1714
- value?: string; // For inline_string
1715
- widgetName?: string; // For widget_config
1716
- }
1717
- \`\`\`
1718
-
1719
- ## Critical Rules
1720
- 1. ALWAYS start with a trigger node (chat_trigger for voice/chat, document_trigger for dashboard)
1721
- 2. Wire data correctly: each input must reference a valid upstream output
1722
- 3. For EMAIL: extract recipient via entity_extraction FIRST, add HITL before send_email
1723
- 4. Use runIf for conditional execution (after HITL approval)
1724
- 5. All email recipients must come from entity_extraction, NOT from LLM-generated text
1725
-
1726
- ## Example: Email with HITL
1727
- \`\`\`json
1728
- {
1729
- "nodes": [
1730
- { "id": "trigger", "actionType": "chat_trigger", "displayName": "Trigger" },
1731
- { "id": "extract", "actionType": "entity_extraction", "displayName": "Extract Email",
1732
- "inputs": { "conversation": { "type": "action_output", "actionName": "trigger", "output": "chat_conversation" } } },
1733
- { "id": "respond", "actionType": "call_llm", "displayName": "Generate Response",
1734
- "inputs": { "query": { "type": "action_output", "actionName": "trigger", "output": "user_query" } } },
1735
- { "id": "hitl", "actionType": "general_hitl", "displayName": "Approve Email",
1736
- "inputs": { "query": { "type": "action_output", "actionName": "respond", "output": "response" } } },
1737
- { "id": "send_email", "actionType": "send_email", "displayName": "Send Email",
1738
- "inputs": {
1739
- "email_to": { "type": "action_output", "actionName": "extract", "output": "email_address" },
1740
- "email_body": { "type": "action_output", "actionName": "respond", "output": "response" }
1741
- },
1742
- "runIf": { "actionName": "hitl", "output": "hitl_status", "enumValue": "HITL Success" } }
1743
- ],
1744
- "resultMappings": [{ "nodeId": "send_email", "output": "email_sent" }]
1745
- }
1746
- \`\`\`
1747
-
1748
- Respond with ONLY valid JSON - no markdown, no explanation.`;
1749
- }
1750
- /**
1751
- * Build user prompt with intent details.
1752
- */
1753
- function buildWorkflowGenerationUserPrompt(intent) {
1754
- const sections = [];
1755
- sections.push(`## User Request\n${intent.description}`);
1756
- sections.push(`## Persona Type\n${intent.persona_type ?? 'chat'}`);
1757
- if (intent.action_chains && intent.action_chains.length > 0) {
1758
- sections.push(`## Detected Patterns\n${intent.action_chains.map(c => `- ${c.name}: ${c.description}`).join('\n')}`);
1759
- }
1760
- if (intent.intents && intent.intents.length > 0) {
1761
- sections.push(`## User Intents\n${intent.intents.map(i => `- ${i.name}: ${i.description}`).join('\n')}`);
1762
- }
1763
- if (intent.entities && intent.entities.length > 0) {
1764
- sections.push(`## Entities to Extract\n${intent.entities.map(e => `- ${e.name}: ${e.type}`).join('\n')}`);
1765
- }
1766
- if (intent.data_sources && intent.data_sources.length > 0) {
1767
- sections.push(`## Data Sources\n${intent.data_sources.map(d => `- ${d.type}${d.instructions ? `: ${d.instructions}` : ''}`).join('\n')}`);
1768
- }
1769
- if (intent.constraints) {
1770
- const constraints = [];
1771
- if (intent.constraints.require_hitl)
1772
- constraints.push('- Require human approval before side effects');
1773
- if (intent.constraints.require_validation)
1774
- constraints.push('- Require validation before actions');
1775
- if (intent.constraints.enable_web_search)
1776
- constraints.push('- Enable web search');
1777
- if (constraints.length > 0) {
1778
- sections.push(`## Constraints\n${constraints.join('\n')}`);
1779
- }
1780
- }
1781
- sections.push('\nGenerate a WorkflowSpec JSON that implements this workflow.');
1782
- return sections.join('\n\n');
1783
- }
1784
- /**
1785
- * Parse LLM response into WorkflowSpec.
1786
- * Handles markdown code blocks and validates structure.
1787
- */
1788
- export function parseWorkflowSpecFromLLM(llmResponse) {
1789
- try {
1790
- // Extract JSON from markdown code block if present
1791
- let json = llmResponse;
1792
- const codeBlockMatch = llmResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
1793
- if (codeBlockMatch) {
1794
- json = codeBlockMatch[1];
1795
- }
1796
- const parsed = JSON.parse(json.trim());
1797
- // Basic validation
1798
- if (!parsed.nodes || !Array.isArray(parsed.nodes)) {
1799
- return null;
1800
- }
1801
- return parsed;
1802
- }
1803
- catch {
1804
- return null;
1805
- }
1806
- }