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