@ema.co/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.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/config.example.yaml +32 -0
- package/dist/cli/index.js +333 -0
- package/dist/config.js +136 -0
- package/dist/emaClient.js +398 -0
- package/dist/index.js +109 -0
- package/dist/mcp/handlers-consolidated.js +851 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/prompts.js +1753 -0
- package/dist/mcp/resources.js +624 -0
- package/dist/mcp/server.js +4723 -0
- package/dist/mcp/tools-consolidated.js +590 -0
- package/dist/mcp/tools-legacy.js +736 -0
- package/dist/models.js +8 -0
- package/dist/scheduler.js +21 -0
- package/dist/sdk/client.js +788 -0
- package/dist/sdk/config.js +136 -0
- package/dist/sdk/contracts.js +429 -0
- package/dist/sdk/generation-schema.js +189 -0
- package/dist/sdk/index.js +39 -0
- package/dist/sdk/knowledge.js +2780 -0
- package/dist/sdk/models.js +8 -0
- package/dist/sdk/state.js +88 -0
- package/dist/sdk/sync-options.js +216 -0
- package/dist/sdk/sync.js +220 -0
- package/dist/sdk/validation-rules.js +355 -0
- package/dist/sdk/workflow-generator.js +291 -0
- package/dist/sdk/workflow-intent.js +1585 -0
- package/dist/state.js +88 -0
- package/dist/sync.js +416 -0
- package/dist/syncOptions.js +216 -0
- package/dist/ui.js +334 -0
- package/docs/advisor-comms-assistant-fixes.md +175 -0
- package/docs/api-contracts.md +216 -0
- package/docs/auto-builder-analysis.md +271 -0
- package/docs/data-architecture.md +166 -0
- package/docs/ema-auto-builder-guide.html +394 -0
- package/docs/ema-user-guide.md +1121 -0
- package/docs/mcp-tools-guide.md +149 -0
- package/docs/naming-conventions.md +218 -0
- package/docs/tool-consolidation-proposal.md +427 -0
- package/package.json +98 -0
- package/resources/templates/chat-ai/README.md +119 -0
- package/resources/templates/chat-ai/persona-config.json +111 -0
- package/resources/templates/dashboard-ai/README.md +156 -0
- package/resources/templates/dashboard-ai/persona-config.json +180 -0
- package/resources/templates/voice-ai/README.md +123 -0
- package/resources/templates/voice-ai/persona-config.json +74 -0
- 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
|
+
}
|