ema-mcp-toolkit 0.2.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.
Files changed (49) hide show
  1. package/README.md +338 -0
  2. package/config.example.yaml +32 -0
  3. package/dist/cli/index.js +333 -0
  4. package/dist/config.js +136 -0
  5. package/dist/emaClient.js +398 -0
  6. package/dist/index.js +109 -0
  7. package/dist/mcp/handlers-consolidated.js +851 -0
  8. package/dist/mcp/index.js +15 -0
  9. package/dist/mcp/prompts.js +1753 -0
  10. package/dist/mcp/resources.js +624 -0
  11. package/dist/mcp/server.js +4585 -0
  12. package/dist/mcp/tools-consolidated.js +590 -0
  13. package/dist/mcp/tools-legacy.js +736 -0
  14. package/dist/models.js +8 -0
  15. package/dist/scheduler.js +21 -0
  16. package/dist/sdk/client.js +788 -0
  17. package/dist/sdk/config.js +136 -0
  18. package/dist/sdk/contracts.js +429 -0
  19. package/dist/sdk/generation-schema.js +189 -0
  20. package/dist/sdk/index.js +39 -0
  21. package/dist/sdk/knowledge.js +2780 -0
  22. package/dist/sdk/models.js +8 -0
  23. package/dist/sdk/state.js +88 -0
  24. package/dist/sdk/sync-options.js +216 -0
  25. package/dist/sdk/sync.js +220 -0
  26. package/dist/sdk/validation-rules.js +355 -0
  27. package/dist/sdk/workflow-generator.js +291 -0
  28. package/dist/sdk/workflow-intent.js +1585 -0
  29. package/dist/state.js +88 -0
  30. package/dist/sync.js +416 -0
  31. package/dist/syncOptions.js +216 -0
  32. package/dist/ui.js +334 -0
  33. package/docs/advisor-comms-assistant-fixes.md +175 -0
  34. package/docs/api-contracts.md +216 -0
  35. package/docs/auto-builder-analysis.md +271 -0
  36. package/docs/data-architecture.md +166 -0
  37. package/docs/ema-auto-builder-guide.html +394 -0
  38. package/docs/ema-user-guide.md +1121 -0
  39. package/docs/mcp-tools-guide.md +149 -0
  40. package/docs/naming-conventions.md +218 -0
  41. package/docs/tool-consolidation-proposal.md +427 -0
  42. package/package.json +95 -0
  43. package/resources/templates/chat-ai/README.md +119 -0
  44. package/resources/templates/chat-ai/persona-config.json +111 -0
  45. package/resources/templates/dashboard-ai/README.md +156 -0
  46. package/resources/templates/dashboard-ai/persona-config.json +180 -0
  47. package/resources/templates/voice-ai/README.md +123 -0
  48. package/resources/templates/voice-ai/persona-config.json +74 -0
  49. package/resources/templates/voice-ai/workflow-prompt.md +120 -0
@@ -0,0 +1,1585 @@
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
+ const INTENT_PATTERNS = [
160
+ { pattern: /password\s*reset/i, name: "Password Reset", handler: "llm" },
161
+ { pattern: /billing|payment|invoice/i, name: "Billing", handler: "search" },
162
+ { pattern: /technical|support|help/i, name: "Technical Support", handler: "search" },
163
+ { pattern: /schedule|appointment|book/i, name: "Scheduling", handler: "tool" },
164
+ { pattern: /ticket|incident|request/i, name: "Ticket Creation", handler: "tool" },
165
+ { pattern: /status|check|lookup/i, name: "Status Check", handler: "search" },
166
+ ];
167
+ const TOOL_PATTERNS = [
168
+ { pattern: /servicenow/i, namespace: "service_now", action: "Create_Incident" },
169
+ { pattern: /salesforce/i, namespace: "salesforce", action: "Create_Case" },
170
+ { pattern: /jira/i, namespace: "jira", action: "Create_Issue" },
171
+ { pattern: /slack/i, namespace: "slack", action: "Send_Message" },
172
+ { pattern: /email/i, namespace: "email", action: "Send_Email" },
173
+ { pattern: /calendar|schedule/i, namespace: "calendar", action: "Check_Availability" },
174
+ ];
175
+ // ─────────────────────────────────────────────────────────────────────────────
176
+ // Action Chain Patterns - End-to-End Semantic Flows
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+ /**
179
+ * Known action chains that require specific wiring
180
+ */
181
+ const ACTION_CHAIN_PATTERNS = [
182
+ {
183
+ id: "llm_template_to_doc",
184
+ name: "LLM Templating → Document Generation",
185
+ description: "Use LLM to generate structured content with template prompt, then convert to document. RECOMMENDED for dynamic, context-dependent content.",
186
+ steps: [
187
+ {
188
+ action_type: "search",
189
+ purpose: "Gather context from knowledge base",
190
+ input_from: "trigger.user_query",
191
+ output_to: "content_generator",
192
+ config: { output: "search_results", output_type: "WELL_KNOWN_TYPE_SEARCH_RESULT" },
193
+ },
194
+ {
195
+ action_type: "call_llm",
196
+ purpose: "Generate structured content with LLM templating",
197
+ input_from: "search_results via named_inputs",
198
+ output_to: "document_generator",
199
+ config: {
200
+ // LLM TEMPLATING: Use structured prompt with section headers
201
+ // The LLM determines appropriate sections based on content type
202
+ prompt_guidance: "Include structured sections with ## headers. Let LLM determine appropriate structure based on content type and user intent.",
203
+ temperature: "0.3-0.5 for consistent formatting",
204
+ use_named_inputs_for_data: true,
205
+ },
206
+ },
207
+ {
208
+ action_type: "generate_document",
209
+ purpose: "Convert markdown to document format",
210
+ input_from: "call_llm.response_with_sources",
211
+ output_to: "email_or_output",
212
+ config: { output: "document_link", output_type: "WELL_KNOWN_TYPE_DOCUMENT" },
213
+ },
214
+ ],
215
+ },
216
+ {
217
+ id: "doc_to_email",
218
+ name: "Document Generation → Email Delivery",
219
+ description: "Generate a document and send it as an email attachment",
220
+ steps: [
221
+ {
222
+ action_type: "generate_document",
223
+ purpose: "Create the document",
224
+ input_from: "content_source", // search_results, llm output, etc.
225
+ output_to: "email_attachment",
226
+ config: { output: "document_link", output_type: "WELL_KNOWN_TYPE_DOCUMENT" },
227
+ },
228
+ {
229
+ action_type: "send_email_agent",
230
+ purpose: "Send email with document attached",
231
+ input_from: "document_link",
232
+ output_to: "result",
233
+ config: {
234
+ // CRITICAL: document_link is DOCUMENT type, NOT TEXT_WITH_SOURCES
235
+ // Must use named_inputs for attachment, not attachment_links
236
+ attachment_binding: "named_inputs",
237
+ attachment_name: "document_attachment",
238
+ },
239
+ },
240
+ ],
241
+ },
242
+ {
243
+ id: "entity_to_scoped_search",
244
+ name: "Entity Extraction → Scoped Search",
245
+ description: "Extract entities (client, advisor) and use them to scope searches",
246
+ steps: [
247
+ {
248
+ action_type: "entity_extraction_with_documents",
249
+ purpose: "Extract structured entities from conversation",
250
+ input_from: "trigger.chat_conversation",
251
+ output_to: "json_mapper",
252
+ },
253
+ {
254
+ action_type: "json_mapper",
255
+ purpose: "Transform entities into usable variables",
256
+ input_from: "extracted_entities",
257
+ output_to: "search_query_modifier",
258
+ },
259
+ {
260
+ action_type: "search",
261
+ purpose: "Search scoped to extracted entity",
262
+ input_from: "scoped_query",
263
+ output_to: "response",
264
+ },
265
+ ],
266
+ },
267
+ {
268
+ id: "actor_identification",
269
+ name: "Actor Identification Flow",
270
+ description: "Identify caller type and route/validate accordingly",
271
+ steps: [
272
+ {
273
+ action_type: "text_categorizer",
274
+ purpose: "Classify actor type (client, advisor, unknown)",
275
+ input_from: "trigger.chat_conversation",
276
+ output_to: "conditional_routing",
277
+ },
278
+ {
279
+ action_type: "call_llm",
280
+ purpose: "For unknown actors: ask for identification",
281
+ input_from: "unknown_actor_branch",
282
+ output_to: "validation",
283
+ config: { condition: "actor == unknown" },
284
+ },
285
+ ],
286
+ },
287
+ {
288
+ id: "search_combine_respond",
289
+ name: "Multi-Source Search → Combine → Respond",
290
+ description: "Search multiple sources, combine results, generate response",
291
+ steps: [
292
+ {
293
+ action_type: "search",
294
+ purpose: "Search knowledge base",
295
+ input_from: "trigger.user_query",
296
+ output_to: "combiner",
297
+ config: { output: "search_results", output_type: "WELL_KNOWN_TYPE_SEARCH_RESULT" },
298
+ },
299
+ {
300
+ action_type: "live_web_search",
301
+ purpose: "Search web for real-time data",
302
+ input_from: "trigger.user_query",
303
+ output_to: "combiner",
304
+ config: { output: "search_results", output_type: "WELL_KNOWN_TYPE_SEARCH_RESULT" },
305
+ },
306
+ {
307
+ action_type: "combine_search_results",
308
+ purpose: "Merge results from multiple sources",
309
+ input_from: "both_search_results",
310
+ output_to: "response",
311
+ config: { output: "combined_results", output_type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES" },
312
+ },
313
+ {
314
+ action_type: "respond_with_sources",
315
+ purpose: "Generate response with citations",
316
+ // CRITICAL: respond_with_sources.search_results expects SEARCH_RESULT, not TEXT_WITH_SOURCES
317
+ // Route original search.search_results, not combined_results
318
+ input_from: "search.search_results",
319
+ output_to: "result",
320
+ config: { use_original_search_for_search_results: true },
321
+ },
322
+ ],
323
+ },
324
+ {
325
+ id: "content_generation_to_doc",
326
+ name: "Content Generation → Document",
327
+ description: "Generate rich content and create downloadable document",
328
+ steps: [
329
+ {
330
+ action_type: "personalized_content_generator",
331
+ purpose: "Generate rich HTML content",
332
+ input_from: "search_results",
333
+ output_to: "document_generator",
334
+ config: { output: "generated_content", output_type: "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES" },
335
+ },
336
+ {
337
+ action_type: "generate_document",
338
+ purpose: "Create downloadable document",
339
+ input_from: "generated_content",
340
+ output_to: "delivery",
341
+ config: { output: "document_link", output_type: "WELL_KNOWN_TYPE_DOCUMENT" },
342
+ },
343
+ ],
344
+ },
345
+ {
346
+ id: "extract_validate_action",
347
+ name: "Extract Required Inputs → Validate → Action",
348
+ description: "Extract required data, validate completeness, ask if missing, confirm before action",
349
+ steps: [
350
+ {
351
+ action_type: "entity_extraction",
352
+ purpose: "Extract required fields from conversation",
353
+ input_from: "trigger.chat_conversation",
354
+ output_to: "validator",
355
+ config: {
356
+ // CRITICAL: Define extraction schema with required fields
357
+ extraction_schema: {
358
+ email_address: { type: "string", required: true },
359
+ recipient_name: { type: "string", required: false },
360
+ subject: { type: "string", required: false },
361
+ },
362
+ },
363
+ },
364
+ {
365
+ action_type: "text_categorizer",
366
+ purpose: "Check if all required inputs are present",
367
+ input_from: "extracted_entities",
368
+ output_to: "conditional_routing",
369
+ config: {
370
+ categories: ["has_all_required", "missing_required"],
371
+ // Route based on presence of required fields
372
+ },
373
+ },
374
+ {
375
+ action_type: "call_llm",
376
+ purpose: "Ask for missing required inputs",
377
+ input_from: "missing_required_branch",
378
+ output_to: "workflow_output",
379
+ config: {
380
+ condition: "category == missing_required",
381
+ prompt: "Ask user for the missing required information",
382
+ },
383
+ },
384
+ {
385
+ action_type: "hitl",
386
+ purpose: "Confirm before executing action with side effects",
387
+ input_from: "has_all_required_branch",
388
+ output_to: "action_execution",
389
+ config: { confirmation_message: "Confirm action before proceeding" },
390
+ },
391
+ {
392
+ action_type: "send_email_agent",
393
+ purpose: "Execute the action only after validation and confirmation",
394
+ input_from: "hitl_success",
395
+ output_to: "result",
396
+ config: {
397
+ // CRITICAL: email_to must come from entity_extraction, NOT from summarized text
398
+ email_to_source: "entity_extraction.email_address",
399
+ runIf: "HITL Success",
400
+ },
401
+ },
402
+ ],
403
+ },
404
+ {
405
+ id: "email_with_validation",
406
+ name: "Email Sending with Proper Validation",
407
+ description: "Send email with extracted recipient, validation, and HITL confirmation",
408
+ steps: [
409
+ {
410
+ action_type: "entity_extraction",
411
+ purpose: "Extract email recipient from conversation",
412
+ input_from: "trigger.chat_conversation",
413
+ output_to: "validation",
414
+ config: {
415
+ // Extract structured data, NOT summarized text
416
+ output: "extracted_entities",
417
+ output_type: "WELL_KNOWN_TYPE_JSON",
418
+ },
419
+ },
420
+ {
421
+ action_type: "hitl",
422
+ purpose: "Confirm recipient and content before sending",
423
+ input_from: "extracted_entities",
424
+ output_to: "conditional",
425
+ config: {
426
+ // REQUIRED for any action with side effects
427
+ display_extracted_data: true,
428
+ },
429
+ },
430
+ {
431
+ action_type: "send_email_agent",
432
+ purpose: "Send email after confirmation",
433
+ input_from: "hitl.success",
434
+ output_to: "result",
435
+ config: {
436
+ // CRITICAL: email_to must be EMAIL ADDRESS from extraction
437
+ // NOT: summarized_conversation (text)
438
+ // NOT: search_results (sources)
439
+ // NOT: response_with_sources (generated text)
440
+ email_to_source: "entity_extraction.email_address",
441
+ runIf: "HITL Success",
442
+ },
443
+ },
444
+ ],
445
+ },
446
+ ];
447
+ /**
448
+ * Type compatibility rules - what outputs can connect to what inputs
449
+ */
450
+ export const TYPE_COMPATIBILITY = {
451
+ WELL_KNOWN_TYPE_CHAT_CONVERSATION: {
452
+ can_connect_to: ["conversation"],
453
+ use_named_inputs_for: [],
454
+ },
455
+ WELL_KNOWN_TYPE_TEXT_WITH_SOURCES: {
456
+ can_connect_to: ["query", "instructions", "context", "text"],
457
+ use_named_inputs_for: ["search_results", "attachment_links"],
458
+ },
459
+ WELL_KNOWN_TYPE_SEARCH_RESULT: {
460
+ can_connect_to: ["search_results"],
461
+ use_named_inputs_for: ["text", "query"],
462
+ },
463
+ WELL_KNOWN_TYPE_DOCUMENT: {
464
+ can_connect_to: [], // Documents can't directly connect to typed inputs
465
+ use_named_inputs_for: ["attachment_links", "text", "query", "content"], // Always use named_inputs
466
+ },
467
+ };
468
+ /**
469
+ * Detect action chains in the input text
470
+ */
471
+ export function detectActionChains(text) {
472
+ const detected = [];
473
+ const lowerText = text.toLowerCase();
474
+ // Document + Email chain
475
+ if ((lowerText.includes("document") || lowerText.includes("brief") || lowerText.includes("report")) &&
476
+ (lowerText.includes("email") || lowerText.includes("send"))) {
477
+ detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "doc_to_email"));
478
+ }
479
+ // Entity extraction + scoped search
480
+ if ((lowerText.includes("client") || lowerText.includes("advisor") || lowerText.includes("user")) &&
481
+ (lowerText.includes("scope") || lowerText.includes("filter") || lowerText.includes("their"))) {
482
+ detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "entity_to_scoped_search"));
483
+ }
484
+ // Actor identification
485
+ if ((lowerText.includes("identify") || lowerText.includes("who is")) &&
486
+ (lowerText.includes("caller") || lowerText.includes("client") || lowerText.includes("advisor"))) {
487
+ detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "actor_identification"));
488
+ }
489
+ // Multi-source search
490
+ if ((lowerText.includes("kb") || lowerText.includes("knowledge")) &&
491
+ (lowerText.includes("web") || lowerText.includes("live") || lowerText.includes("real-time"))) {
492
+ detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "search_combine_respond"));
493
+ }
494
+ // ═══════════════════════════════════════════════════════════════════════════
495
+ // FALLBACK: String-based detection for document generation
496
+ //
497
+ // NOTE: This is a FALLBACK approach. For accurate semantic understanding,
498
+ // use LLM extraction via generateOutputSemanticsPrompt() which extracts:
499
+ // - output_type (document, email, report, etc.)
500
+ // - format, tone, style, audience, purpose, length
501
+ //
502
+ // The functions below provide defaults when LLM extraction is unavailable.
503
+ // ═══════════════════════════════════════════════════════════════════════════
504
+ const hasGenerationIntent = lowerText.includes("generate") || lowerText.includes("create") ||
505
+ lowerText.includes("prepare") || lowerText.includes("produce") || lowerText.includes("draft");
506
+ const hasDocumentOutput = lowerText.includes("document") || lowerText.includes("doc") ||
507
+ lowerText.includes("file") || lowerText.includes("download") || lowerText.includes("pdf") ||
508
+ lowerText.includes("attachment") || lowerText.includes(".docx");
509
+ const hasContentSynthesis = lowerText.includes("content") || lowerText.includes("html") ||
510
+ lowerText.includes("markdown") || lowerText.includes("formatted");
511
+ if (hasGenerationIntent && (hasDocumentOutput || hasContentSynthesis)) {
512
+ // LLM templating is the recommended default for dynamic content
513
+ detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "llm_template_to_doc"));
514
+ }
515
+ // Email sending (without document attachment) - requires proper validation
516
+ if ((lowerText.includes("send") || lowerText.includes("email") || lowerText.includes("mail")) &&
517
+ !lowerText.includes("document") && !lowerText.includes("attach") && !lowerText.includes("brief")) {
518
+ detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "email_with_validation"));
519
+ }
520
+ // Extract → Validate → Action pattern (when action has side effects)
521
+ if ((lowerText.includes("send") || lowerText.includes("create") || lowerText.includes("update") || lowerText.includes("book")) &&
522
+ (lowerText.includes("confirm") || lowerText.includes("validate") || lowerText.includes("required") || lowerText.includes("check"))) {
523
+ detected.push(ACTION_CHAIN_PATTERNS.find((c) => c.id === "extract_validate_action"));
524
+ }
525
+ return detected.filter(Boolean);
526
+ }
527
+ const PERSONA_TYPE_PATTERNS = [
528
+ { pattern: /voice|call|phone/i, type: "voice" },
529
+ { pattern: /document|batch|upload/i, type: "dashboard" },
530
+ ];
531
+ export function parseNaturalLanguage(text) {
532
+ // Detect persona type (default: chat)
533
+ let personaType = "chat";
534
+ for (const { pattern, type } of PERSONA_TYPE_PATTERNS) {
535
+ if (pattern.test(text)) {
536
+ personaType = type;
537
+ break;
538
+ }
539
+ }
540
+ // Detect intents
541
+ const intents = [];
542
+ const seenIntents = new Set();
543
+ for (const { pattern, name, handler } of INTENT_PATTERNS) {
544
+ if (pattern.test(text) && !seenIntents.has(name)) {
545
+ seenIntents.add(name);
546
+ intents.push({
547
+ name,
548
+ description: `User requests ${name.toLowerCase()}`,
549
+ handler,
550
+ });
551
+ }
552
+ }
553
+ // Detect tools
554
+ const tools = [];
555
+ for (const { pattern, namespace, action } of TOOL_PATTERNS) {
556
+ if (pattern.test(text)) {
557
+ tools.push({ namespace, action });
558
+ }
559
+ }
560
+ // Detect data sources
561
+ const dataSources = [];
562
+ if (/search|knowledge|faq|document|lookup|kb/i.test(text)) {
563
+ dataSources.push({ type: "knowledge_base" });
564
+ }
565
+ if (/web|internet|online|live/i.test(text)) {
566
+ dataSources.push({ type: "web_search" });
567
+ }
568
+ if (dataSources.length === 0) {
569
+ // Default to KB search for most use cases
570
+ dataSources.push({ type: "knowledge_base" });
571
+ }
572
+ // Detect constraints
573
+ const constraints = {};
574
+ if (/approv|human|review|escalat|hitl/i.test(text)) {
575
+ constraints.require_hitl = true;
576
+ }
577
+ if (/validat|check|verify/i.test(text)) {
578
+ constraints.require_validation = true;
579
+ }
580
+ // Detect action chains - end-to-end semantic flows
581
+ const actionChains = detectActionChains(text);
582
+ // Detect entities to extract
583
+ const entities = detectEntities(text);
584
+ // Detect delivery configuration
585
+ const deliveryConfig = detectDeliveryConfig(text);
586
+ // Extract a name from the text (first sentence or first N words)
587
+ const sentences = text.split(/[.!?]/);
588
+ const name = sentences[0].trim().slice(0, 50) || "AI Employee";
589
+ return {
590
+ name,
591
+ description: text,
592
+ persona_type: personaType,
593
+ intents: intents.length > 0 ? intents : undefined,
594
+ tools: tools.length > 0 ? tools : undefined,
595
+ data_sources: dataSources,
596
+ constraints: Object.keys(constraints).length > 0 ? constraints : undefined,
597
+ action_chains: actionChains.length > 0 ? actionChains : undefined,
598
+ entities: entities.length > 0 ? entities : undefined,
599
+ delivery_config: deliveryConfig,
600
+ };
601
+ }
602
+ /**
603
+ * Detect entities that should be extracted from the conversation
604
+ */
605
+ function detectEntities(text) {
606
+ const entities = [];
607
+ const lowerText = text.toLowerCase();
608
+ // Client entity
609
+ if (lowerText.includes("client")) {
610
+ entities.push({
611
+ name: "client",
612
+ type: "person",
613
+ extract_from: "conversation",
614
+ use_for: ["scoped_search", "email_recipient", "document_context"],
615
+ });
616
+ }
617
+ // Advisor entity
618
+ if (lowerText.includes("advisor")) {
619
+ entities.push({
620
+ name: "advisor",
621
+ type: "person",
622
+ extract_from: "conversation",
623
+ use_for: ["validation", "cc_recipient"],
624
+ });
625
+ }
626
+ // Ticker/stock symbol
627
+ if (/ticker|stock|symbol|security/i.test(text)) {
628
+ entities.push({
629
+ name: "ticker",
630
+ type: "identifier",
631
+ extract_from: "conversation",
632
+ use_for: ["scoped_search", "document_title"],
633
+ });
634
+ }
635
+ // Topic/focus area
636
+ if (/focus|topic|about|regarding/i.test(text)) {
637
+ entities.push({
638
+ name: "focus_area",
639
+ type: "topic",
640
+ extract_from: "conversation",
641
+ use_for: ["scoped_search", "document_content"],
642
+ });
643
+ }
644
+ return entities;
645
+ }
646
+ /**
647
+ * Detect how results should be delivered
648
+ */
649
+ function detectDeliveryConfig(text) {
650
+ const lowerText = text.toLowerCase();
651
+ const methods = [];
652
+ // Email delivery
653
+ if (lowerText.includes("email") || lowerText.includes("send")) {
654
+ const emailConfig = {
655
+ recipient_source: "extracted",
656
+ subject_source: "generated",
657
+ body_source: "llm_generated",
658
+ include_attachments: false,
659
+ };
660
+ // Check if document should be attached
661
+ if ((lowerText.includes("attach") || lowerText.includes("document") || lowerText.includes("brief")) &&
662
+ lowerText.includes("email")) {
663
+ emailConfig.include_attachments = true;
664
+ emailConfig.attachment_source = "generate_document.document_link";
665
+ }
666
+ methods.push({ type: "email", config: emailConfig });
667
+ }
668
+ // Document delivery (download/generate)
669
+ if (lowerText.includes("document") ||
670
+ lowerText.includes("brief") ||
671
+ lowerText.includes("report") ||
672
+ lowerText.includes("download")) {
673
+ const docConfig = {
674
+ document_type: lowerText.includes("brief")
675
+ ? "brief"
676
+ : lowerText.includes("report")
677
+ ? "report"
678
+ : "summary",
679
+ title_source: "extracted",
680
+ content_source: "combined",
681
+ };
682
+ methods.push({ type: "document", config: docConfig });
683
+ }
684
+ // Determine if confirmation is needed
685
+ const requiresConfirmation = lowerText.includes("confirm") || lowerText.includes("approve");
686
+ if (methods.length === 0) {
687
+ return undefined;
688
+ }
689
+ return {
690
+ method: methods.length > 1 ? "multiple" : methods[0].type,
691
+ methods: methods.length > 1 ? methods : undefined,
692
+ requires_confirmation: requiresConfirmation || methods.some((m) => m.type === "email"),
693
+ };
694
+ }
695
+ // ─────────────────────────────────────────────────────────────────────────────
696
+ // Partial Spec Parsing
697
+ // ─────────────────────────────────────────────────────────────────────────────
698
+ export function parsePartialSpec(spec) {
699
+ return {
700
+ name: String(spec.name ?? "AI Employee"),
701
+ description: String(spec.description ?? ""),
702
+ persona_type: spec.persona_type ?? "chat",
703
+ intents: spec.intents,
704
+ tools: spec.tools,
705
+ data_sources: spec.data_sources,
706
+ constraints: spec.constraints,
707
+ };
708
+ }
709
+ // ─────────────────────────────────────────────────────────────────────────────
710
+ // Validation
711
+ // ─────────────────────────────────────────────────────────────────────────────
712
+ export function validateIntent(intent) {
713
+ const missing = [];
714
+ const questions = [];
715
+ const suggestions = [];
716
+ // Check name/description
717
+ if (!intent.name || intent.name.length < 3) {
718
+ missing.push("name");
719
+ questions.push("What should this AI Employee be called?");
720
+ }
721
+ if (!intent.description || intent.description.length < 10) {
722
+ missing.push("description");
723
+ questions.push("What should this AI Employee do?");
724
+ }
725
+ // Check if we have any capability defined
726
+ const hasCapability = (intent.intents?.length ?? 0) > 0 ||
727
+ (intent.tools?.length ?? 0) > 0 ||
728
+ (intent.data_sources?.length ?? 0) > 0;
729
+ if (!hasCapability) {
730
+ missing.push("capabilities");
731
+ questions.push("What capabilities should this AI have? (e.g., search KB, create tickets, route by intent)");
732
+ }
733
+ // Intent-specific validation
734
+ if (intent.intents && intent.intents.length > 0) {
735
+ // Check for Fallback
736
+ const hasFallback = intent.intents.some((i) => i.name.toLowerCase() === "fallback");
737
+ if (!hasFallback) {
738
+ suggestions.push("Consider adding a 'Fallback' intent for unmatched queries");
739
+ }
740
+ // Check tool intents have tool config
741
+ for (const i of intent.intents) {
742
+ if (i.handler === "tool" && !i.tool_config && (intent.tools?.length ?? 0) === 0) {
743
+ missing.push(`tool for intent "${i.name}"`);
744
+ questions.push(`What tool should handle the "${i.name}" intent? (e.g., ServiceNow, Salesforce)`);
745
+ }
746
+ }
747
+ }
748
+ // Tool validation
749
+ if (intent.tools && intent.tools.length > 0 && !intent.constraints?.require_hitl) {
750
+ suggestions.push("External tools detected - consider enabling HITL for safety");
751
+ }
752
+ // Voice-specific validation
753
+ if (intent.persona_type === "voice") {
754
+ if (!intent.voice_config?.welcome_message) {
755
+ suggestions.push("Voice AI should have a welcome message");
756
+ }
757
+ if (!intent.voice_config?.hangup_instructions) {
758
+ suggestions.push("Voice AI should have hangup instructions");
759
+ }
760
+ }
761
+ // ═══════════════════════════════════════════════════════════════════════════
762
+ // ACTION CHAIN VALIDATION - Critical for end-to-end flows
763
+ // ═══════════════════════════════════════════════════════════════════════════
764
+ if (intent.action_chains && intent.action_chains.length > 0) {
765
+ for (const chain of intent.action_chains) {
766
+ // Validate doc_to_email chain
767
+ if (chain.id === "doc_to_email") {
768
+ // Check if we have email config
769
+ if (!intent.delivery_config?.methods?.some((m) => m.type === "email")) {
770
+ questions.push("Who should receive the email? (client, advisor, specific email)");
771
+ missing.push("email_recipient");
772
+ }
773
+ // Check for document config
774
+ if (!intent.delivery_config?.methods?.some((m) => m.type === "document")) {
775
+ questions.push("What type of document should be generated? (brief, report, summary)");
776
+ missing.push("document_type");
777
+ }
778
+ // CRITICAL: Remind about proper wiring
779
+ suggestions.push("Document → Email chain detected. IMPORTANT: document_link output is DOCUMENT type. " +
780
+ "Use named_inputs (not attachment_links) to attach documents to emails.");
781
+ }
782
+ // Validate entity_to_scoped_search chain
783
+ if (chain.id === "entity_to_scoped_search") {
784
+ if (!intent.entities || intent.entities.length === 0) {
785
+ questions.push("What entities should be extracted? (client name, advisor, ticker symbol)");
786
+ missing.push("entities_to_extract");
787
+ }
788
+ }
789
+ // Validate actor_identification chain
790
+ if (chain.id === "actor_identification") {
791
+ questions.push("How should unknown actors be validated? (phone number, PIN, name lookup)");
792
+ suggestions.push("Actor identification detected. Consider adding validation for unknown callers.");
793
+ }
794
+ }
795
+ }
796
+ // ═══════════════════════════════════════════════════════════════════════════
797
+ // ENTITY VALIDATION
798
+ // ═══════════════════════════════════════════════════════════════════════════
799
+ if (intent.entities && intent.entities.length > 0) {
800
+ for (const entity of intent.entities) {
801
+ // Check if entity usage is clear
802
+ if (!entity.use_for || entity.use_for.length === 0) {
803
+ questions.push(`What should the extracted "${entity.name}" be used for?`);
804
+ }
805
+ // Email recipient validation
806
+ if (entity.use_for?.includes("email_recipient")) {
807
+ suggestions.push(`Ensure ${entity.name} entity extraction includes email address field for email delivery.`);
808
+ }
809
+ }
810
+ }
811
+ // ═══════════════════════════════════════════════════════════════════════════
812
+ // DELIVERY VALIDATION
813
+ // ═══════════════════════════════════════════════════════════════════════════
814
+ if (intent.delivery_config) {
815
+ // Email delivery checks
816
+ const emailMethod = intent.delivery_config.methods?.find((m) => m.type === "email");
817
+ if (emailMethod) {
818
+ const emailConfig = emailMethod.config;
819
+ if (emailConfig?.include_attachments && !emailConfig.attachment_source) {
820
+ questions.push("What should be attached to the email? (generated document, search results)");
821
+ missing.push("email_attachment_source");
822
+ }
823
+ if (emailConfig?.recipient_source === "static" && !emailConfig.static_recipient) {
824
+ questions.push("What email address should receive the message?");
825
+ missing.push("email_recipient_address");
826
+ }
827
+ if (emailConfig?.recipient_source === "extracted" && (!intent.entities || intent.entities.length === 0)) {
828
+ questions.push("How should the recipient email be determined? (extract from conversation, user specifies)");
829
+ missing.push("recipient_extraction_config");
830
+ }
831
+ // Confirm send
832
+ if (!intent.delivery_config.requires_confirmation) {
833
+ suggestions.push("Email delivery detected. Consider requiring confirmation before sending.");
834
+ }
835
+ }
836
+ // Document delivery checks
837
+ const docMethod = intent.delivery_config.methods?.find((m) => m.type === "document");
838
+ if (docMethod) {
839
+ const docConfig = docMethod.config;
840
+ if (!docConfig?.document_type) {
841
+ questions.push("What type of document should be generated? (brief, report, analysis)");
842
+ missing.push("document_type");
843
+ }
844
+ if (!docConfig?.content_source) {
845
+ questions.push("Where should document content come from? (search results, generated, template)");
846
+ missing.push("document_content_source");
847
+ }
848
+ }
849
+ }
850
+ // Calculate confidence
851
+ let confidence;
852
+ if (missing.length === 0 && questions.length === 0) {
853
+ confidence = "high";
854
+ }
855
+ else if (missing.length <= 1) {
856
+ confidence = "medium";
857
+ }
858
+ else {
859
+ confidence = "low";
860
+ }
861
+ return {
862
+ complete: missing.length === 0,
863
+ confidence,
864
+ missing,
865
+ questions,
866
+ suggestions,
867
+ };
868
+ }
869
+ // ─────────────────────────────────────────────────────────────────────────────
870
+ // Comprehensive Intent Confidence Analysis
871
+ // ─────────────────────────────────────────────────────────────────────────────
872
+ /**
873
+ * Calculate comprehensive confidence in our understanding of the user's intent.
874
+ * This is the primary driver for deciding whether to proceed or ask questions.
875
+ *
876
+ * @param intent - The parsed workflow intent
877
+ * @param originalText - The original user input (for context analysis)
878
+ * @returns Detailed confidence analysis with recommended action
879
+ */
880
+ export function calculateIntentConfidence(intent, originalText) {
881
+ const understood = [];
882
+ const uncertain = [];
883
+ const blockers = [];
884
+ const questions = [];
885
+ // ═══════════════════════════════════════════════════════════════════════════
886
+ // 1. GOAL UNDERSTANDING - Why does the user want this?
887
+ // ═══════════════════════════════════════════════════════════════════════════
888
+ const goalSignals = [];
889
+ let goalScore = 0;
890
+ // Check if we have clear intent definitions
891
+ if (intent.intents && intent.intents.length > 0) {
892
+ goalScore += 30;
893
+ goalSignals.push(`${intent.intents.length} intent(s) identified`);
894
+ // Check intent clarity
895
+ const hasDescriptions = intent.intents.every(i => i.description && i.description.length > 10);
896
+ if (hasDescriptions) {
897
+ goalScore += 20;
898
+ goalSignals.push("All intents have clear descriptions");
899
+ }
900
+ else {
901
+ goalSignals.push("Some intents lack clear descriptions");
902
+ uncertain.push("Purpose of some intents unclear");
903
+ }
904
+ // Check if handlers are specified
905
+ const hasHandlers = intent.intents.every(i => i.handler);
906
+ if (hasHandlers) {
907
+ goalScore += 15;
908
+ goalSignals.push("All intents have defined handlers");
909
+ }
910
+ }
911
+ else {
912
+ goalSignals.push("No explicit intents defined");
913
+ uncertain.push("Primary goal unclear");
914
+ }
915
+ // Check for recognized patterns (shows we understand the "shape" of what they want)
916
+ if (intent.action_chains && intent.action_chains.length > 0) {
917
+ goalScore += 25;
918
+ goalSignals.push(`Recognized pattern: ${intent.action_chains.map(c => c.name).join(", ")}`);
919
+ understood.push(`Workflow pattern: ${intent.action_chains[0].name}`);
920
+ }
921
+ // Check for persona type (basic understanding)
922
+ if (intent.persona_type) {
923
+ goalScore += 10;
924
+ goalSignals.push(`Persona type: ${intent.persona_type}`);
925
+ understood.push(`Type: ${intent.persona_type} workflow`);
926
+ }
927
+ const goalUnderstanding = {
928
+ score: Math.min(goalScore, 100),
929
+ level: goalScore >= 70 ? "high" : goalScore >= 40 ? "medium" : goalScore > 0 ? "low" : "unknown",
930
+ reason: goalScore >= 70
931
+ ? "Clear understanding of user's objective"
932
+ : goalScore >= 40
933
+ ? "Partial understanding of objective - some aspects unclear"
934
+ : "Limited understanding of why user wants this workflow",
935
+ signals: goalSignals,
936
+ };
937
+ if (goalScore < 40) {
938
+ questions.push({
939
+ id: "goal_clarification",
940
+ question: "What is the main goal you're trying to achieve with this workflow?",
941
+ category: "goal_understanding",
942
+ priority: "critical",
943
+ context: "Understanding the 'why' helps us design the right solution",
944
+ options: ["Answer questions", "Generate content", "Process documents", "Send communications", "Search & analyze data"],
945
+ });
946
+ }
947
+ // ═══════════════════════════════════════════════════════════════════════════
948
+ // 2. INPUT REQUIREMENTS - What data is needed?
949
+ // ═══════════════════════════════════════════════════════════════════════════
950
+ const inputSignals = [];
951
+ let inputScore = 0;
952
+ // Check for entity definitions
953
+ if (intent.entities && intent.entities.length > 0) {
954
+ inputScore += 40;
955
+ inputSignals.push(`${intent.entities.length} entity type(s) identified: ${intent.entities.map(e => e.type).join(", ")}`);
956
+ understood.push(`Required entities: ${intent.entities.map(e => e.type).join(", ")}`);
957
+ // Check if entities have clear use_for
958
+ const entitiesWithUse = intent.entities.filter(e => e.use_for && e.use_for.length > 0);
959
+ if (entitiesWithUse.length === intent.entities.length) {
960
+ inputScore += 20;
961
+ inputSignals.push("All entities have defined usage");
962
+ }
963
+ else {
964
+ uncertain.push("How some entities will be used");
965
+ }
966
+ }
967
+ else {
968
+ // Try to infer from action chains
969
+ if (intent.action_chains?.some(c => c.id === "doc_to_email" || c.id === "email_with_validation")) {
970
+ inputSignals.push("Email workflow detected - will need recipient email");
971
+ inputScore += 20;
972
+ uncertain.push("Email recipient source not specified");
973
+ questions.push({
974
+ id: "email_recipient_source",
975
+ question: "Where will the recipient email address come from?",
976
+ category: "input_requirements",
977
+ priority: "critical",
978
+ context: "Email workflows require a valid email address - need to know the source",
979
+ options: ["Extract from conversation", "User provides it", "From CRM/database", "Fixed recipient"],
980
+ });
981
+ }
982
+ }
983
+ // Check for data sources
984
+ if (intent.data_sources && intent.data_sources.length > 0) {
985
+ inputScore += 30;
986
+ inputSignals.push(`Data sources specified: ${intent.data_sources.join(", ")}`);
987
+ understood.push(`Data sources: ${intent.data_sources.join(", ")}`);
988
+ }
989
+ else if (intent.intents?.some(i => i.handler === "search")) {
990
+ inputSignals.push("Search intent but no data sources specified");
991
+ uncertain.push("What data sources to search");
992
+ }
993
+ const inputRequirements = {
994
+ score: Math.min(inputScore, 100),
995
+ level: inputScore >= 70 ? "high" : inputScore >= 40 ? "medium" : inputScore > 0 ? "low" : "unknown",
996
+ reason: inputScore >= 70
997
+ ? "Clear understanding of required inputs and data"
998
+ : inputScore >= 40
999
+ ? "Some inputs identified but gaps remain"
1000
+ : "Unclear what data/inputs are needed",
1001
+ signals: inputSignals,
1002
+ };
1003
+ // ═══════════════════════════════════════════════════════════════════════════
1004
+ // 3. OUTPUT EXPECTATIONS - What should the result look like?
1005
+ // ═══════════════════════════════════════════════════════════════════════════
1006
+ const outputSignals = [];
1007
+ let outputScore = 0;
1008
+ // Check for delivery config
1009
+ if (intent.delivery_config) {
1010
+ outputScore += 40;
1011
+ outputSignals.push(`Delivery method: ${intent.delivery_config.method}`);
1012
+ understood.push(`Delivery: ${intent.delivery_config.method}`);
1013
+ if (intent.delivery_config.requires_confirmation) {
1014
+ outputScore += 10;
1015
+ outputSignals.push("Confirmation required before delivery");
1016
+ }
1017
+ }
1018
+ else {
1019
+ outputSignals.push("Delivery method not specified");
1020
+ uncertain.push("How results should be delivered");
1021
+ }
1022
+ // Check intent handlers for output hints
1023
+ const outputHandlers = intent.intents?.filter(i => i.handler === "document" ||
1024
+ i.handler === "email" ||
1025
+ i.document_config ||
1026
+ i.email_config) ?? [];
1027
+ if (outputHandlers.length > 0) {
1028
+ outputScore += 30;
1029
+ outputSignals.push(`Output-producing handlers: ${outputHandlers.length}`);
1030
+ }
1031
+ // Check for specific output configurations
1032
+ if (intent.intents?.some(i => i.document_config?.document_type)) {
1033
+ outputScore += 20;
1034
+ outputSignals.push("Document type specified");
1035
+ understood.push("Document generation required");
1036
+ }
1037
+ const outputExpectations = {
1038
+ score: Math.min(outputScore, 100),
1039
+ level: outputScore >= 70 ? "high" : outputScore >= 40 ? "medium" : outputScore > 0 ? "low" : "unknown",
1040
+ reason: outputScore >= 70
1041
+ ? "Clear understanding of expected outputs"
1042
+ : outputScore >= 40
1043
+ ? "Partial understanding of expected outputs"
1044
+ : "Unclear what the workflow should produce",
1045
+ signals: outputSignals,
1046
+ };
1047
+ if (outputScore < 40) {
1048
+ questions.push({
1049
+ id: "output_format",
1050
+ question: "What should the output of this workflow look like?",
1051
+ category: "output_expectations",
1052
+ priority: "important",
1053
+ context: "Understanding the expected output helps design the right response format",
1054
+ options: ["Text response in chat", "Generated document", "Email to someone", "Data update", "Multiple outputs"],
1055
+ });
1056
+ }
1057
+ // ═══════════════════════════════════════════════════════════════════════════
1058
+ // 4. PATTERN RECOGNITION - Do we recognize this workflow type?
1059
+ // ═══════════════════════════════════════════════════════════════════════════
1060
+ const patternSignals = [];
1061
+ let patternScore = 0;
1062
+ if (intent.action_chains && intent.action_chains.length > 0) {
1063
+ patternScore += 60;
1064
+ for (const chain of intent.action_chains) {
1065
+ patternSignals.push(`Matched pattern: ${chain.name}`);
1066
+ understood.push(`Pattern: ${chain.description}`);
1067
+ }
1068
+ }
1069
+ // Check for common patterns even without explicit action chains
1070
+ const hasSearch = intent.intents?.some(i => i.handler === "search");
1071
+ const hasLlm = intent.intents?.some(i => i.handler === "llm");
1072
+ const hasDocument = intent.intents?.some(i => i.handler === "document" || i.document_config);
1073
+ const hasEmail = intent.intents?.some(i => i.handler === "email" || i.email_config);
1074
+ if (hasSearch && hasLlm && !intent.action_chains?.length) {
1075
+ patternScore += 30;
1076
+ patternSignals.push("Search + LLM pattern (RAG-style)");
1077
+ }
1078
+ if (hasDocument && hasEmail && !intent.action_chains?.some(c => c.id === "doc_to_email")) {
1079
+ patternSignals.push("Document + Email detected but chain not matched");
1080
+ uncertain.push("Document-to-email flow not fully specified");
1081
+ }
1082
+ const patternRecognition = {
1083
+ score: Math.min(patternScore, 100),
1084
+ level: patternScore >= 60 ? "high" : patternScore >= 30 ? "medium" : patternScore > 0 ? "low" : "unknown",
1085
+ reason: patternScore >= 60
1086
+ ? "Recognized workflow pattern with known best practices"
1087
+ : patternScore >= 30
1088
+ ? "Partial pattern match - may need customization"
1089
+ : "No recognized pattern - custom workflow needed",
1090
+ signals: patternSignals,
1091
+ };
1092
+ // ═══════════════════════════════════════════════════════════════════════════
1093
+ // 5. DATA COMPLETENESS - Are required fields specified?
1094
+ // ═══════════════════════════════════════════════════════════════════════════
1095
+ const dataSignals = [];
1096
+ let dataScore = 50; // Start at 50, deduct for missing critical data
1097
+ // Check action chain requirements
1098
+ if (intent.action_chains) {
1099
+ for (const chain of intent.action_chains) {
1100
+ if (chain.id === "doc_to_email" || chain.id === "email_with_validation") {
1101
+ // Email requires recipient - check entities by name or use_for
1102
+ const hasEmailEntity = intent.entities?.some(e => e.name?.toLowerCase().includes("email") ||
1103
+ e.name?.toLowerCase().includes("recipient") ||
1104
+ e.use_for?.some(u => u.includes("email") || u.includes("recipient")));
1105
+ if (!hasEmailEntity) {
1106
+ dataScore -= 30;
1107
+ dataSignals.push("Email recipient not specified");
1108
+ blockers.push({
1109
+ category: "data_completeness",
1110
+ what: "Email recipient",
1111
+ why: "Cannot send email without knowing who to send it to",
1112
+ impact: "blocking",
1113
+ });
1114
+ questions.push({
1115
+ id: "email_recipient",
1116
+ question: "Who should receive the email?",
1117
+ category: "data_completeness",
1118
+ priority: "critical",
1119
+ context: "Email recipient is required to send the email",
1120
+ });
1121
+ }
1122
+ else {
1123
+ dataSignals.push("Email recipient source identified");
1124
+ }
1125
+ }
1126
+ if (chain.id === "doc_to_email") {
1127
+ // Document type should be specified
1128
+ if (!intent.intents?.some(i => i.document_config?.document_type)) {
1129
+ dataScore -= 15;
1130
+ dataSignals.push("Document type not specified");
1131
+ uncertain.push("What type of document to generate");
1132
+ }
1133
+ }
1134
+ }
1135
+ }
1136
+ // Check for incomplete entity definitions (entities without clear usage)
1137
+ const incompleteEntities = intent.entities?.filter(e => !e.use_for?.length) ?? [];
1138
+ if (incompleteEntities.length > 0) {
1139
+ dataScore -= 10;
1140
+ dataSignals.push(`${incompleteEntities.length} entities with incomplete definitions`);
1141
+ }
1142
+ dataScore = Math.max(0, dataScore);
1143
+ const dataCompleteness = {
1144
+ score: dataScore,
1145
+ level: dataScore >= 70 ? "high" : dataScore >= 40 ? "medium" : dataScore > 0 ? "low" : "unknown",
1146
+ reason: dataScore >= 70
1147
+ ? "Required data and fields are specified"
1148
+ : dataScore >= 40
1149
+ ? "Some required data missing but workflow may still work"
1150
+ : "Critical data missing - cannot proceed",
1151
+ signals: dataSignals,
1152
+ };
1153
+ // ═══════════════════════════════════════════════════════════════════════════
1154
+ // 6. CONSTRAINTS CLARITY - Do we understand rules and constraints?
1155
+ // ═══════════════════════════════════════════════════════════════════════════
1156
+ const constraintSignals = [];
1157
+ let constraintScore = 50; // Start at 50 (neutral if no constraints)
1158
+ // Check for confirmation requirements
1159
+ if (intent.delivery_config?.requires_confirmation) {
1160
+ constraintScore += 20;
1161
+ constraintSignals.push("Confirmation requirement specified");
1162
+ understood.push("Requires confirmation before action");
1163
+ }
1164
+ // Check for HITL patterns in action chains
1165
+ const needsHitl = intent.action_chains?.some(c => c.id === "email_with_validation" ||
1166
+ c.id === "extract_validate_action" ||
1167
+ c.steps?.some(s => s.action_type === "hitl"));
1168
+ if (needsHitl) {
1169
+ constraintScore += 15;
1170
+ constraintSignals.push("HITL requirement recognized");
1171
+ understood.push("Human approval needed before side effects");
1172
+ }
1173
+ else if (intent.action_chains?.some(c => c.id.includes("email"))) {
1174
+ // Email without HITL - potential issue
1175
+ constraintSignals.push("Email action without explicit HITL requirement");
1176
+ uncertain.push("Whether confirmation is needed before sending");
1177
+ }
1178
+ const constraintsClarity = {
1179
+ score: Math.min(constraintScore, 100),
1180
+ level: constraintScore >= 70 ? "high" : constraintScore >= 40 ? "medium" : constraintScore > 0 ? "low" : "unknown",
1181
+ reason: constraintScore >= 70
1182
+ ? "Constraints and rules are clear"
1183
+ : constraintScore >= 40
1184
+ ? "Some constraints understood but may need defaults"
1185
+ : "Unclear what rules or constraints apply",
1186
+ signals: constraintSignals,
1187
+ };
1188
+ // ═══════════════════════════════════════════════════════════════════════════
1189
+ // CALCULATE OVERALL CONFIDENCE
1190
+ // ═══════════════════════════════════════════════════════════════════════════
1191
+ // Weighted average with goal understanding being most important
1192
+ const weights = {
1193
+ goal_understanding: 0.25,
1194
+ input_requirements: 0.20,
1195
+ output_expectations: 0.15,
1196
+ pattern_recognition: 0.15,
1197
+ data_completeness: 0.15,
1198
+ constraints_clarity: 0.10,
1199
+ };
1200
+ const overall = Math.round(goalUnderstanding.score * weights.goal_understanding +
1201
+ inputRequirements.score * weights.input_requirements +
1202
+ outputExpectations.score * weights.output_expectations +
1203
+ patternRecognition.score * weights.pattern_recognition +
1204
+ dataCompleteness.score * weights.data_completeness +
1205
+ constraintsClarity.score * weights.constraints_clarity);
1206
+ // Determine level and recommendation
1207
+ let level;
1208
+ let recommendation;
1209
+ if (blockers.length > 0) {
1210
+ level = "insufficient";
1211
+ recommendation = "clarify_critical";
1212
+ }
1213
+ else if (overall >= 70) {
1214
+ level = "high";
1215
+ recommendation = questions.length > 0 ? "clarify_recommended" : "proceed";
1216
+ }
1217
+ else if (overall >= 45) {
1218
+ level = "medium";
1219
+ recommendation = questions.some(q => q.priority === "critical") ? "clarify_critical" : "clarify_recommended";
1220
+ }
1221
+ else {
1222
+ level = "low";
1223
+ recommendation = "insufficient_info";
1224
+ }
1225
+ // Sort questions by priority
1226
+ questions.sort((a, b) => {
1227
+ const priorityOrder = { critical: 0, important: 1, nice_to_have: 2 };
1228
+ return priorityOrder[a.priority] - priorityOrder[b.priority];
1229
+ });
1230
+ return {
1231
+ overall,
1232
+ level,
1233
+ breakdown: {
1234
+ goal_understanding: goalUnderstanding,
1235
+ input_requirements: inputRequirements,
1236
+ output_expectations: outputExpectations,
1237
+ pattern_recognition: patternRecognition,
1238
+ data_completeness: dataCompleteness,
1239
+ constraints_clarity: constraintsClarity,
1240
+ },
1241
+ understood,
1242
+ uncertain,
1243
+ blockers,
1244
+ recommendation,
1245
+ clarification_questions: questions,
1246
+ };
1247
+ }
1248
+ /**
1249
+ * Get a human-readable summary of intent confidence
1250
+ */
1251
+ export function summarizeIntentConfidence(confidence) {
1252
+ const lines = [];
1253
+ lines.push(`## Intent Understanding: ${confidence.level.toUpperCase()} (${confidence.overall}%)`);
1254
+ lines.push("");
1255
+ // What we understood
1256
+ if (confidence.understood.length > 0) {
1257
+ lines.push("### ✅ Understood:");
1258
+ for (const item of confidence.understood) {
1259
+ lines.push(`- ${item}`);
1260
+ }
1261
+ lines.push("");
1262
+ }
1263
+ // What's uncertain
1264
+ if (confidence.uncertain.length > 0) {
1265
+ lines.push("### ⚠️ Uncertain:");
1266
+ for (const item of confidence.uncertain) {
1267
+ lines.push(`- ${item}`);
1268
+ }
1269
+ lines.push("");
1270
+ }
1271
+ // Blockers
1272
+ if (confidence.blockers.length > 0) {
1273
+ lines.push("### 🚫 Blocking Issues:");
1274
+ for (const blocker of confidence.blockers) {
1275
+ lines.push(`- **${blocker.what}**: ${blocker.why}`);
1276
+ }
1277
+ lines.push("");
1278
+ }
1279
+ // Recommendation
1280
+ lines.push(`### Recommendation: ${confidence.recommendation.replace(/_/g, " ").toUpperCase()}`);
1281
+ if (confidence.clarification_questions.length > 0) {
1282
+ lines.push("");
1283
+ lines.push("### Questions to Clarify:");
1284
+ for (const q of confidence.clarification_questions) {
1285
+ const priority = q.priority === "critical" ? "🔴" : q.priority === "important" ? "🟡" : "🟢";
1286
+ lines.push(`${priority} ${q.question}`);
1287
+ if (q.options) {
1288
+ lines.push(` Options: ${q.options.join(", ")}`);
1289
+ }
1290
+ }
1291
+ }
1292
+ return lines.join("\n");
1293
+ }
1294
+ // ─────────────────────────────────────────────────────────────────────────────
1295
+ // Intent to Spec Conversion
1296
+ // ─────────────────────────────────────────────────────────────────────────────
1297
+ export function intentToSpec(intent) {
1298
+ const nodes = [];
1299
+ const resultMappings = [];
1300
+ // 1. Add trigger
1301
+ const triggerId = "trigger";
1302
+ const triggerType = intent.persona_type === "dashboard" ? "document_trigger" : "chat_trigger";
1303
+ nodes.push({
1304
+ id: triggerId,
1305
+ actionType: triggerType,
1306
+ displayName: "Trigger",
1307
+ });
1308
+ // 2. Add categorizer if multiple intents
1309
+ let categorizerId;
1310
+ if (intent.intents && intent.intents.length > 1) {
1311
+ categorizerId = "categorizer";
1312
+ const categories = intent.intents.map((i) => ({
1313
+ name: i.name,
1314
+ description: i.description,
1315
+ examples: i.examples,
1316
+ }));
1317
+ // Add Fallback if not present
1318
+ if (!categories.some((c) => c.name.toLowerCase() === "fallback")) {
1319
+ categories.push({
1320
+ name: "Fallback",
1321
+ description: "Query doesn't match other categories",
1322
+ });
1323
+ }
1324
+ nodes.push({
1325
+ id: categorizerId,
1326
+ actionType: "chat_categorizer",
1327
+ displayName: "Intent Classifier",
1328
+ inputs: {
1329
+ conversation: {
1330
+ type: "action_output",
1331
+ actionName: triggerId,
1332
+ output: "chat_conversation",
1333
+ },
1334
+ },
1335
+ categories,
1336
+ });
1337
+ }
1338
+ // 3. Add search if data sources include KB
1339
+ let searchId;
1340
+ if (intent.data_sources?.some((ds) => ds.type === "knowledge_base" || ds.type === "combined")) {
1341
+ searchId = "search";
1342
+ nodes.push({
1343
+ id: searchId,
1344
+ actionType: "search",
1345
+ displayName: "Knowledge Search",
1346
+ inputs: {
1347
+ query: {
1348
+ type: "action_output",
1349
+ actionName: triggerId,
1350
+ output: "user_query",
1351
+ },
1352
+ },
1353
+ });
1354
+ }
1355
+ // 4. Add web search if enabled
1356
+ let webSearchId;
1357
+ if (intent.data_sources?.some((ds) => ds.type === "web_search" || ds.type === "combined")) {
1358
+ webSearchId = "web_search";
1359
+ nodes.push({
1360
+ id: webSearchId,
1361
+ actionType: "live_web_search",
1362
+ displayName: "Web Search",
1363
+ inputs: {
1364
+ query: {
1365
+ type: "action_output",
1366
+ actionName: triggerId,
1367
+ output: "user_query",
1368
+ },
1369
+ },
1370
+ });
1371
+ }
1372
+ // 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
+ }
1393
+ // 6. Add HITL if required
1394
+ let hitlId;
1395
+ if (intent.constraints?.require_hitl && toolCallerId) {
1396
+ hitlId = "hitl";
1397
+ nodes.push({
1398
+ id: hitlId,
1399
+ actionType: "general_hitl",
1400
+ displayName: "Human Approval",
1401
+ inputs: {
1402
+ query: {
1403
+ type: "action_output",
1404
+ actionName: toolCallerId,
1405
+ output: "tool_execution_result",
1406
+ },
1407
+ },
1408
+ });
1409
+ }
1410
+ // 7. Add response node
1411
+ const respondId = "respond";
1412
+ const respondInputs = {
1413
+ query: {
1414
+ type: "action_output",
1415
+ actionName: triggerId,
1416
+ output: "user_query",
1417
+ },
1418
+ };
1419
+ if (searchId) {
1420
+ respondInputs.search_results = {
1421
+ type: "action_output",
1422
+ actionName: searchId,
1423
+ output: "search_results",
1424
+ };
1425
+ }
1426
+ nodes.push({
1427
+ id: respondId,
1428
+ actionType: searchId ? "respond_with_sources" : "call_llm",
1429
+ displayName: "Response",
1430
+ inputs: respondInputs,
1431
+ });
1432
+ resultMappings.push({
1433
+ nodeId: respondId,
1434
+ output: searchId ? "response_with_sources" : "response_with_sources",
1435
+ });
1436
+ return {
1437
+ name: intent.name,
1438
+ description: intent.description,
1439
+ personaType: intent.persona_type,
1440
+ nodes,
1441
+ resultMappings,
1442
+ };
1443
+ }
1444
+ export function parseInput(input) {
1445
+ const inputType = detectInputType(input);
1446
+ let intent;
1447
+ switch (inputType) {
1448
+ case "natural_language":
1449
+ intent = parseNaturalLanguage(String(input));
1450
+ break;
1451
+ case "partial_spec":
1452
+ intent = parsePartialSpec(input);
1453
+ break;
1454
+ case "full_spec":
1455
+ // Full spec: extract intent from the spec
1456
+ const spec = input;
1457
+ intent = {
1458
+ name: spec.name,
1459
+ description: spec.description,
1460
+ persona_type: spec.personaType,
1461
+ };
1462
+ break;
1463
+ case "existing_workflow":
1464
+ // Would need to reverse-engineer from workflow_def
1465
+ // For now, return minimal intent
1466
+ intent = {
1467
+ name: "Existing Workflow",
1468
+ description: "Imported from existing workflow_def",
1469
+ persona_type: "chat",
1470
+ };
1471
+ break;
1472
+ }
1473
+ const validation = validateIntent(intent);
1474
+ return { intent, input_type: inputType, validation };
1475
+ }
1476
+ // ─────────────────────────────────────────────────────────────────────────────
1477
+ // LLM-Based Semantic Extraction
1478
+ // ─────────────────────────────────────────────────────────────────────────────
1479
+ /**
1480
+ * Generate a prompt for LLM-based output semantics extraction.
1481
+ * This should be called by tools that have access to an LLM.
1482
+ *
1483
+ * @param userInput - The user's natural language request
1484
+ * @returns Prompt and schema for LLM extraction
1485
+ */
1486
+ export function generateOutputSemanticsPrompt(userInput) {
1487
+ const systemPrompt = `You are an expert at understanding user requests and extracting semantic attributes about desired outputs.
1488
+
1489
+ Given a user's request, extract the following:
1490
+ - OUTPUT TYPE: What kind of output are they asking for? (document, email, report, brief, analysis, etc.)
1491
+ - FORMAT: How should it be formatted? (document, email, chat response, file type)
1492
+ - TONE: What tone is appropriate? (formal, professional, casual, friendly)
1493
+ - STYLE: What writing style? (analytical, persuasive, informative, instructional)
1494
+ - AUDIENCE: Who is this for? (client, internal, executive, technical)
1495
+ - PURPOSE: What is the goal? (inform, persuade, summarize, analyze, recommend)
1496
+ - LENGTH: How detailed? (brief, standard, detailed, comprehensive)
1497
+
1498
+ Be precise but infer reasonable defaults when not explicitly stated.
1499
+ Consider context clues - "send to client" implies external/client audience, "quick summary" implies brief length.`;
1500
+ const userPrompt = `Analyze this user request and extract the output semantic attributes:
1501
+
1502
+ "${userInput}"
1503
+
1504
+ Extract the attributes using the provided schema. Include your reasoning.`;
1505
+ return {
1506
+ system_prompt: systemPrompt,
1507
+ user_prompt: userPrompt,
1508
+ schema: OUTPUT_SEMANTICS_EXTRACTION_SCHEMA,
1509
+ };
1510
+ }
1511
+ /**
1512
+ * Apply LLM-extracted semantics to an existing intent.
1513
+ * Call this after getting LLM extraction results.
1514
+ *
1515
+ * @param intent - The current workflow intent
1516
+ * @param extracted - The LLM-extracted output semantics
1517
+ * @returns Updated intent with semantics applied
1518
+ */
1519
+ export function applyExtractedSemantics(intent, extracted) {
1520
+ return {
1521
+ ...intent,
1522
+ output_semantics: extracted,
1523
+ // Also update delivery config based on extracted info
1524
+ delivery_config: {
1525
+ ...intent.delivery_config,
1526
+ method: extracted.output_type.category === "communication" ? "email" :
1527
+ extracted.output_type.category === "document" ? "document" :
1528
+ intent.delivery_config?.method ?? "response",
1529
+ requires_confirmation: extracted.output_type.requires_delivery ||
1530
+ extracted.purpose.action_required ||
1531
+ intent.delivery_config?.requires_confirmation,
1532
+ },
1533
+ };
1534
+ }
1535
+ /**
1536
+ * Generate LLM instructions based on extracted semantics.
1537
+ * Use these instructions in call_llm prompts for consistent output.
1538
+ *
1539
+ * @param semantics - The extracted output semantics
1540
+ * @returns Instructions to include in LLM prompts
1541
+ */
1542
+ export function generateContentInstructions(semantics) {
1543
+ const instructions = [];
1544
+ // Tone instructions
1545
+ instructions.push(`TONE: Write in a ${semantics.tone.formality} tone with ${semantics.tone.voice} voice.`);
1546
+ if (semantics.tone.sentiment === "urgent") {
1547
+ instructions.push("Convey urgency appropriately.");
1548
+ }
1549
+ // Style instructions
1550
+ instructions.push(`STYLE: Use ${semantics.style.approach} approach with ${semantics.style.detail_level} detail.`);
1551
+ if (semantics.style.use_citations) {
1552
+ instructions.push("Include citations to source documents.");
1553
+ }
1554
+ if (semantics.style.use_examples) {
1555
+ instructions.push("Include relevant examples where appropriate.");
1556
+ }
1557
+ // Audience instructions
1558
+ instructions.push(`AUDIENCE: Writing for ${semantics.audience.type} audience with ${semantics.audience.familiarity} familiarity.`);
1559
+ if (semantics.audience.relationship) {
1560
+ instructions.push(`The recipient is: ${semantics.audience.relationship}.`);
1561
+ }
1562
+ // Format instructions
1563
+ instructions.push(`FORMAT: Structure content as ${semantics.format.structure ?? "sections"} format.`);
1564
+ // Length instructions
1565
+ const lengthGuidance = {
1566
+ brief: "Keep it concise - 1-2 paragraphs or bullet points.",
1567
+ standard: "Provide balanced coverage - 3-5 paragraphs with key sections.",
1568
+ detailed: "Be thorough - cover all aspects with supporting details.",
1569
+ comprehensive: "Be exhaustive - include all relevant information with deep analysis.",
1570
+ };
1571
+ instructions.push(`LENGTH: ${lengthGuidance[semantics.length]}`);
1572
+ // Purpose instructions
1573
+ instructions.push(`PURPOSE: Primary goal is to ${semantics.purpose.primary}.`);
1574
+ if (semantics.purpose.action_required) {
1575
+ instructions.push("Include clear call-to-action or next steps.");
1576
+ }
1577
+ if (semantics.purpose.decision_support) {
1578
+ instructions.push("Present information to support decision-making.");
1579
+ }
1580
+ // Formatting requirements
1581
+ if (semantics.formatting_requirements && semantics.formatting_requirements.length > 0) {
1582
+ instructions.push(`SPECIFIC REQUIREMENTS: ${semantics.formatting_requirements.join(", ")}`);
1583
+ }
1584
+ return instructions.join("\n");
1585
+ }