@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
package/dist/mcp/server.js
CHANGED
|
@@ -24,2138 +24,58 @@ export { TOOLKIT_NAME, TOOLKIT_VERSION, TOOLKIT_COMMIT };
|
|
|
24
24
|
import { PromptRegistry, isPromptError } from "./prompts.js";
|
|
25
25
|
import { ResourceRegistry, isResourceError } from "./resources.js";
|
|
26
26
|
import { generateServerInstructions, getContextualTip, TOOL_GUIDANCE } from "./guidance.js";
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
//
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
import { AGENT_CATALOG, WORKFLOW_PATTERNS, QUALIFYING_QUESTIONS, PLATFORM_CONCEPTS, WORKFLOW_EXECUTION_MODEL, COMMON_MISTAKES, DEBUG_CHECKLIST, GUIDANCE_TOPICS, PROJECT_TYPES, getAgentsByCategory, getAgentByName, getWidgetsForPersonaType, checkTypeCompatibility, getQualifyingQuestionsByCategory, getConceptByTerm, suggestAgentsForUseCase, validateWorkflowPrompt,
|
|
35
|
-
// Workflow Data (LLM does analysis with rules from ema://rules/*)
|
|
36
|
-
validateWorkflowConnections, parseWorkflowDef, } from "./knowledge.js";
|
|
37
|
-
// Template fallbacks (generated from protos, used when API unavailable)
|
|
38
|
-
import { getTemplateFallback, getTemplateFieldDocs, VOICE_TEMPLATE_FALLBACK } from "../sdk/generated/template-fallbacks.js";
|
|
39
|
-
// Workflow Compiler (Template-driven)
|
|
40
|
-
import { compileWorkflow, } from "./domain/workflow-generator.js";
|
|
41
|
-
// V2 Tools (4 tools: persona, catalog, sync, env) - NEW DEFAULT
|
|
42
|
-
import { generateTools, } from "./tools.js";
|
|
43
|
-
import { handleEnv, handlePersona,
|
|
44
|
-
// handleWorkflow - removed, now using extracted handler from ./handlers/workflow/index.js
|
|
45
|
-
handleAction,
|
|
46
|
-
// handleData - removed, now using extracted handler from ./handlers/data/index.js
|
|
47
|
-
handleTemplate, handleKnowledge, handleReference, } from "./handlers-consolidated.js";
|
|
48
|
-
// Import extracted handlers
|
|
49
|
-
import { handleWorkflow } from "./handlers/workflow/index.js";
|
|
27
|
+
// V2 Tools
|
|
28
|
+
import { generateTools } from "./tools.js";
|
|
29
|
+
// Handler imports (simple 1-2 line handlers stay inline)
|
|
30
|
+
import { handleEnv } from "./handlers/env/index.js";
|
|
31
|
+
import { handleAction } from "./handlers/action/index.js";
|
|
32
|
+
import { handleTemplate } from "./handlers/template/index.js";
|
|
33
|
+
import { handleReference } from "./handlers/reference/index.js";
|
|
50
34
|
import { handleCatalog } from "./handlers/catalog/index.js";
|
|
51
35
|
import { handleFeedback } from "./handlers/feedback/index.js";
|
|
52
36
|
import { recordTelemetry } from "./handlers/feedback/store.js";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
import {
|
|
58
|
-
|
|
59
|
-
import { summarizeWorkflow } from "./handlers/workflow/fix.js";
|
|
37
|
+
// V2 adapters (extracted from server.ts — each owns its routing/transformation logic)
|
|
38
|
+
import { handlePersonaAdapter } from "./handlers/persona/adapter.js";
|
|
39
|
+
import { handleWorkflowAdapter } from "./handlers/workflow/adapter.js";
|
|
40
|
+
import { handleSyncAdapter } from "./handlers/sync/adapter.js";
|
|
41
|
+
import { handleDemoAdapter } from "./handlers/demo/adapter.js";
|
|
42
|
+
import { handleDebugAdapter } from "./handlers/debug/adapter.js";
|
|
60
43
|
// Start token initialization in background (non-blocking)
|
|
61
44
|
void initializeApiKeyTokens();
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* V2: 5 tools (persona, catalog, workflow, sync, env) - LLM-optimized
|
|
95
|
-
*
|
|
96
|
-
* Why V2:
|
|
97
|
-
* - Minimal tool count optimizes LLM tool selection
|
|
98
|
-
* - Data operations under persona (always persona-scoped)
|
|
99
|
-
* - Catalog consolidates all reference data (actions, templates, etc.)
|
|
100
|
-
* - Clear separation: entity (persona), reference (catalog), operation (sync, workflow)
|
|
101
|
-
*/
|
|
102
|
-
function generateAllTools() {
|
|
103
|
-
const envNames = getAvailableEnvironments().map(e => e.name);
|
|
104
|
-
const defaultEnv = getDefaultEnvName();
|
|
105
|
-
return generateTools(envNames, defaultEnv);
|
|
106
|
-
}
|
|
107
|
-
// Generate tools (called once at module load)
|
|
108
|
-
const TOOLS = generateAllTools();
|
|
109
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
-
// ADDITIONAL TOOLS (special-purpose inline tools)
|
|
111
|
-
// These handle specific operations that don't fit the V2 pattern
|
|
112
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
-
const ADDITIONAL_TOOLS = [
|
|
114
|
-
// NOTE: Tools (persona, catalog, workflow, sync, env) are in ./tools.ts
|
|
115
|
-
//
|
|
116
|
-
// Special-purpose tools here:
|
|
117
|
-
// - compile_workflow - Direct workflow compilation
|
|
118
|
-
// - Demo data tools - RAG document generation/validation
|
|
119
|
-
// - Data source tools - Upload/delete/manage knowledge sources
|
|
120
|
-
// toggle_embedding
|
|
121
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
-
// Workflow Compilation - Template-driven, no biased patterns
|
|
123
|
-
// Read ema://catalog/patterns for pattern references, then construct nodes
|
|
124
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
-
{
|
|
126
|
-
name: "compile_workflow",
|
|
127
|
-
description: `🔧 Compile workflow from node specification. Template-driven - read \`ema://catalog/patterns\` for pattern templates, construct nodes, then compile.
|
|
128
|
-
|
|
129
|
-
**Process**:
|
|
130
|
-
1. Read \`ema://catalog/patterns\` for pattern reference
|
|
131
|
-
2. Read \`ema://catalog/agents\` for available actions
|
|
132
|
-
3. Construct nodes array based on requirements
|
|
133
|
-
4. Call compile_workflow with nodes
|
|
134
|
-
5. Use deploy_workflow to deploy result
|
|
135
|
-
|
|
136
|
-
**Example nodes** (KB search):
|
|
137
|
-
\`\`\`json
|
|
138
|
-
[
|
|
139
|
-
{ "id": "trigger", "action_type": "chat_trigger", "display_name": "Trigger" },
|
|
140
|
-
{ "id": "search", "action_type": "search", "display_name": "Search", "inputs": { "query": { "type": "action_output", "action_name": "trigger", "output": "user_query" } } },
|
|
141
|
-
{ "id": "respond", "action_type": "respond_with_sources", "display_name": "Respond", "inputs": { "search_results": { "type": "action_output", "action_name": "search", "output": "search_results" } } }
|
|
142
|
-
]
|
|
143
|
-
\`\`\``,
|
|
144
|
-
inputSchema: {
|
|
145
|
-
type: "object",
|
|
146
|
-
properties: {
|
|
147
|
-
name: { type: "string", description: "Workflow name" },
|
|
148
|
-
description: { type: "string", description: "Workflow description" },
|
|
149
|
-
persona_type: { type: "string", enum: ["voice", "chat", "dashboard"], description: "AI type" },
|
|
150
|
-
nodes: {
|
|
151
|
-
type: "array",
|
|
152
|
-
description: "Node definitions",
|
|
153
|
-
items: {
|
|
154
|
-
type: "object",
|
|
155
|
-
properties: {
|
|
156
|
-
id: { type: "string", description: "Node ID" },
|
|
157
|
-
action_type: { type: "string", description: "Action type (e.g., chat_trigger, search, respond_with_sources)" },
|
|
158
|
-
display_name: { type: "string", description: "Display name" },
|
|
159
|
-
description: { type: "string", description: "Optional description" },
|
|
160
|
-
inputs: { type: "object", description: "Input bindings (key: input name, value: binding spec)" },
|
|
161
|
-
run_if: {
|
|
162
|
-
type: "object",
|
|
163
|
-
description: "Conditional execution",
|
|
164
|
-
properties: {
|
|
165
|
-
source_action: { type: "string" },
|
|
166
|
-
source_output: { type: "string", description: "Output name to check" },
|
|
167
|
-
operator: { type: "string", enum: ["eq", "neq", "gt", "lt", "gte", "lte"], description: "Comparison operator" },
|
|
168
|
-
value: { type: "string", description: "Value to compare against" },
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
categories: {
|
|
172
|
-
type: "array",
|
|
173
|
-
description: "Categories for categorizer nodes",
|
|
174
|
-
items: {
|
|
175
|
-
type: "object",
|
|
176
|
-
properties: {
|
|
177
|
-
name: { type: "string", description: "Category name (e.g., 'Password Reset', 'Fallback')" },
|
|
178
|
-
description: { type: "string", description: "When this category triggers" },
|
|
179
|
-
examples: { type: "array", items: { type: "string" }, description: "Example phrases" },
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
tools: {
|
|
184
|
-
type: "array",
|
|
185
|
-
description: "External tools for external_action_caller nodes",
|
|
186
|
-
items: {
|
|
187
|
-
type: "object",
|
|
188
|
-
properties: {
|
|
189
|
-
name: { type: "string", description: "Tool name" },
|
|
190
|
-
namespace: { type: "string", description: "Tool namespace" },
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
disable_human_interaction: { type: "boolean", description: "If true, disable HITL for this node" },
|
|
195
|
-
},
|
|
196
|
-
required: ["id", "action_type", "display_name"],
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
result_mappings: {
|
|
200
|
-
type: "array",
|
|
201
|
-
description: "Which node outputs map to WORKFLOW_OUTPUT",
|
|
202
|
-
items: {
|
|
203
|
-
type: "object",
|
|
204
|
-
properties: {
|
|
205
|
-
node_id: { type: "string", description: "Node ID" },
|
|
206
|
-
output: { type: "string", description: "Output name from the node" },
|
|
207
|
-
},
|
|
208
|
-
required: ["node_id", "output"],
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
required: ["name", "description", "persona_type", "nodes", "result_mappings"],
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
216
|
-
// Data Source Management
|
|
217
|
-
// TODO: Add support for 3rd party data sources (Google Drive, SharePoint, Confluence, etc.)
|
|
218
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
219
|
-
{
|
|
220
|
-
name: "upload_data_source",
|
|
221
|
-
description: "Upload a file from the local filesystem to an AI Employee's knowledge base. The file will be available for RAG/search if embedding is enabled. IMPORTANT: Upload data sources BEFORE deploying workflows that reference them.",
|
|
222
|
-
inputSchema: {
|
|
223
|
-
type: "object",
|
|
224
|
-
properties: {
|
|
225
|
-
persona_id: {
|
|
226
|
-
type: "string",
|
|
227
|
-
description: "The AI Employee ID to upload the file to",
|
|
228
|
-
},
|
|
229
|
-
file_path: {
|
|
230
|
-
type: "string",
|
|
231
|
-
description: "Absolute path to the file on the local filesystem",
|
|
232
|
-
},
|
|
233
|
-
tags: {
|
|
234
|
-
type: "string",
|
|
235
|
-
description: "Optional tags for categorizing the file (default: 'fileUpload')",
|
|
236
|
-
},
|
|
237
|
-
env: {
|
|
238
|
-
type: "string",
|
|
239
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
required: ["persona_id", "file_path"],
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
name: "delete_data_source",
|
|
247
|
-
description: "Delete a file from an AI Employee's knowledge base.",
|
|
248
|
-
inputSchema: {
|
|
249
|
-
type: "object",
|
|
250
|
-
properties: {
|
|
251
|
-
persona_id: {
|
|
252
|
-
type: "string",
|
|
253
|
-
description: "The AI Employee ID",
|
|
254
|
-
},
|
|
255
|
-
file_id: {
|
|
256
|
-
type: "string",
|
|
257
|
-
description: "The file ID to delete (from list_data_sources)",
|
|
258
|
-
},
|
|
259
|
-
env: {
|
|
260
|
-
type: "string",
|
|
261
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
required: ["persona_id", "file_id"],
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
name: "list_data_sources",
|
|
269
|
-
description: "List data sources (knowledge base files/documents) configured for an AI Employee, including upload status and file count.",
|
|
270
|
-
inputSchema: {
|
|
271
|
-
type: "object",
|
|
272
|
-
properties: {
|
|
273
|
-
persona_id: {
|
|
274
|
-
type: "string",
|
|
275
|
-
description: "The AI Employee ID to list data sources for",
|
|
276
|
-
},
|
|
277
|
-
env: {
|
|
278
|
-
type: "string",
|
|
279
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
required: ["persona_id"],
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
name: "get_embedding_status",
|
|
287
|
-
description: "Get the embedding/RAG status for an AI Employee's knowledge base.",
|
|
288
|
-
inputSchema: {
|
|
289
|
-
type: "object",
|
|
290
|
-
properties: {
|
|
291
|
-
persona_id: {
|
|
292
|
-
type: "string",
|
|
293
|
-
description: "The AI Employee ID",
|
|
294
|
-
},
|
|
295
|
-
env: {
|
|
296
|
-
type: "string",
|
|
297
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
required: ["persona_id"],
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
name: "toggle_embedding",
|
|
305
|
-
description: "Enable or disable embedding/RAG for an AI Employee's knowledge base. When enabled, uploaded documents are indexed for semantic search.",
|
|
306
|
-
inputSchema: {
|
|
307
|
-
type: "object",
|
|
308
|
-
properties: {
|
|
309
|
-
persona_id: {
|
|
310
|
-
type: "string",
|
|
311
|
-
description: "The AI Employee ID",
|
|
312
|
-
},
|
|
313
|
-
enabled: {
|
|
314
|
-
type: "boolean",
|
|
315
|
-
description: "Whether to enable (true) or disable (false) embedding",
|
|
316
|
-
},
|
|
317
|
-
env: {
|
|
318
|
-
type: "string",
|
|
319
|
-
description: "Target environment. Available: dev, demo, staging. Default: demo",
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
required: ["persona_id", "enabled"],
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
326
|
-
// Unified Workflow Tool
|
|
327
|
-
// Accepts any input: natural language, partial spec, full spec, or persona_id
|
|
328
|
-
// Normalizes to WorkflowIntent → validates → generates → deploys
|
|
329
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
330
|
-
{
|
|
331
|
-
name: "workflow",
|
|
332
|
-
description: `🔧 UNIFIED workflow tool. Accepts ANY input and normalizes it.
|
|
333
|
-
|
|
334
|
-
**Input types** (auto-detected):
|
|
335
|
-
- Natural language: "IT helpdesk that creates ServiceNow tickets"
|
|
336
|
-
- Partial spec: { intents: [...], tools: [...] }
|
|
337
|
-
- Full nodes spec: { nodes: [...], result_mappings: [...] }
|
|
338
|
-
- Existing persona: persona_id to analyze/improve
|
|
339
|
-
|
|
340
|
-
**Process**:
|
|
341
|
-
1. Parse input → WorkflowIntent (normalized representation)
|
|
342
|
-
2. Validate completeness → return questions if incomplete
|
|
343
|
-
3. Generate workflow (local compile or Auto Builder)
|
|
344
|
-
4. Validate output → auto-fix if enabled
|
|
345
|
-
5. Deploy if persona_id provided
|
|
346
|
-
|
|
347
|
-
**Examples**:
|
|
348
|
-
\`\`\`
|
|
349
|
-
workflow("IT helpdesk bot with KB search")
|
|
350
|
-
workflow({ intents: [{name: "Billing", handler: "search"}], tools: [{namespace: "service_now", action: "Create_Ticket"}] })
|
|
351
|
-
workflow(persona_id, mode="improve")
|
|
352
|
-
\`\`\``,
|
|
353
|
-
inputSchema: withEnvParam({
|
|
354
|
-
input: {
|
|
355
|
-
description: "Natural language description, partial spec object, or full nodes spec",
|
|
356
|
-
},
|
|
357
|
-
persona_id: {
|
|
358
|
-
type: "string",
|
|
359
|
-
description: "For deployment OR to analyze/improve existing workflow",
|
|
360
|
-
},
|
|
361
|
-
mode: {
|
|
362
|
-
type: "string",
|
|
363
|
-
enum: ["generate", "improve", "analyze"],
|
|
364
|
-
description: "generate (default): Create new workflow. improve: Fix existing. analyze: Validate only.",
|
|
365
|
-
},
|
|
366
|
-
persona_type: {
|
|
367
|
-
type: "string",
|
|
368
|
-
enum: ["voice", "chat", "dashboard"],
|
|
369
|
-
description: "AI type (default: chat, auto-detected from input)",
|
|
370
|
-
},
|
|
371
|
-
use_autobuilder: {
|
|
372
|
-
type: "boolean",
|
|
373
|
-
description: "Force Auto Builder for generation (default: auto-decide based on complexity)",
|
|
374
|
-
},
|
|
375
|
-
auto_deploy: {
|
|
376
|
-
type: "boolean",
|
|
377
|
-
description: "Deploy immediately (default: false - returns preview)",
|
|
378
|
-
},
|
|
379
|
-
auto_fix: {
|
|
380
|
-
type: "boolean",
|
|
381
|
-
description: "Auto-fix detected issues (default: true)",
|
|
382
|
-
},
|
|
383
|
-
}, []),
|
|
384
|
-
},
|
|
385
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
386
|
-
// Demo Data Management
|
|
387
|
-
// Tools for consolidating, transforming, and preparing mock data for RAG
|
|
388
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
389
|
-
{
|
|
390
|
-
name: "consolidate_demo_data",
|
|
391
|
-
description: `Transform normalized JSON source files into RAG-optimized Markdown documents for Ema knowledge bases.
|
|
392
|
-
|
|
393
|
-
This tool pre-joins related data (like customers + orders + tickets) into denormalized entity documents that work well with semantic search.
|
|
394
|
-
|
|
395
|
-
**Why this matters**: RAG systems can't do SQL-style joins at query time. Data relationships must be explicit in the document content.
|
|
396
|
-
|
|
397
|
-
**Supported patterns**:
|
|
398
|
-
- Entity consolidation (customer with all their orders, tickets, contacts)
|
|
399
|
-
- Product catalogs with cross-references
|
|
400
|
-
- Scenario documents for demos
|
|
401
|
-
|
|
402
|
-
**Output format**: Markdown files with embedded tables, metadata comments, and narrative summaries optimized for Ema's search agents.`,
|
|
403
|
-
inputSchema: {
|
|
404
|
-
type: "object",
|
|
405
|
-
properties: {
|
|
406
|
-
source_dir: {
|
|
407
|
-
type: "string",
|
|
408
|
-
description: "Path to directory containing source JSON files (e.g., './data/source')",
|
|
409
|
-
},
|
|
410
|
-
output_dir: {
|
|
411
|
-
type: "string",
|
|
412
|
-
description: "Path to output directory for generated Markdown files (e.g., './data/knowledge-base')",
|
|
413
|
-
},
|
|
414
|
-
entity_type: {
|
|
415
|
-
type: "string",
|
|
416
|
-
enum: ["customer", "product", "employee", "scenario", "custom"],
|
|
417
|
-
description: "Type of entity being consolidated. Determines document structure.",
|
|
418
|
-
},
|
|
419
|
-
primary_file: {
|
|
420
|
-
type: "string",
|
|
421
|
-
description: "Name of the primary JSON file (e.g., 'customers.json')",
|
|
422
|
-
},
|
|
423
|
-
joins: {
|
|
424
|
-
type: "array",
|
|
425
|
-
items: {
|
|
426
|
-
type: "object",
|
|
427
|
-
properties: {
|
|
428
|
-
file: { type: "string", description: "JSON file to join (e.g., 'orders.json')" },
|
|
429
|
-
on: { type: "string", description: "Foreign key field (e.g., 'customerId')" },
|
|
430
|
-
as: { type: "string", description: "Name for the joined data (e.g., 'orders')" },
|
|
431
|
-
},
|
|
432
|
-
},
|
|
433
|
-
description: "Array of files to join with the primary file",
|
|
434
|
-
},
|
|
435
|
-
id_field: {
|
|
436
|
-
type: "string",
|
|
437
|
-
description: "Field name for the entity ID (default: 'id')",
|
|
438
|
-
},
|
|
439
|
-
name_field: {
|
|
440
|
-
type: "string",
|
|
441
|
-
description: "Field name for the entity name (default: 'name')",
|
|
442
|
-
},
|
|
443
|
-
tags: {
|
|
444
|
-
type: "string",
|
|
445
|
-
description: "Comma-separated tags to include in document metadata",
|
|
446
|
-
},
|
|
447
|
-
},
|
|
448
|
-
required: ["source_dir", "output_dir", "entity_type", "primary_file"],
|
|
449
|
-
},
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
name: "generate_demo_document",
|
|
453
|
-
description: `Generate a single RAG-optimized Markdown document from provided JSON data.
|
|
454
|
-
|
|
455
|
-
Use this for:
|
|
456
|
-
- Creating individual entity documents programmatically
|
|
457
|
-
- Testing document formats before batch consolidation
|
|
458
|
-
- Custom document generation with specific data
|
|
459
|
-
|
|
460
|
-
The output follows Ema's knowledge base best practices with metadata comments, tables, and narrative context.`,
|
|
461
|
-
inputSchema: {
|
|
462
|
-
type: "object",
|
|
463
|
-
properties: {
|
|
464
|
-
entity_type: {
|
|
465
|
-
type: "string",
|
|
466
|
-
enum: ["customer", "product", "employee", "scenario", "reference"],
|
|
467
|
-
description: "Type of document to generate",
|
|
468
|
-
},
|
|
469
|
-
data: {
|
|
470
|
-
type: "object",
|
|
471
|
-
description: "The entity data as a JSON object",
|
|
472
|
-
},
|
|
473
|
-
related_data: {
|
|
474
|
-
type: "object",
|
|
475
|
-
description: "Related data to include (e.g., { orders: [...], tickets: [...] })",
|
|
476
|
-
},
|
|
477
|
-
output_path: {
|
|
478
|
-
type: "string",
|
|
479
|
-
description: "Optional: Path to save the generated document. If not provided, returns the content.",
|
|
480
|
-
},
|
|
481
|
-
tags: {
|
|
482
|
-
type: "string",
|
|
483
|
-
description: "Comma-separated tags for metadata",
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
required: ["entity_type", "data"],
|
|
487
|
-
},
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
name: "validate_demo_document",
|
|
491
|
-
description: `Validate a Markdown document for RAG optimization and Ema compatibility.
|
|
492
|
-
|
|
493
|
-
Checks for:
|
|
494
|
-
- Required metadata comments (ema_entity, ema_id, ema_tags)
|
|
495
|
-
- Table formatting
|
|
496
|
-
- Narrative context presence
|
|
497
|
-
- Cross-reference consistency
|
|
498
|
-
- Filename conventions`,
|
|
499
|
-
inputSchema: {
|
|
500
|
-
type: "object",
|
|
501
|
-
properties: {
|
|
502
|
-
file_path: {
|
|
503
|
-
type: "string",
|
|
504
|
-
description: "Path to the Markdown file to validate",
|
|
505
|
-
},
|
|
506
|
-
content: {
|
|
507
|
-
type: "string",
|
|
508
|
-
description: "Alternatively, provide the document content directly",
|
|
509
|
-
},
|
|
510
|
-
},
|
|
511
|
-
required: [],
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
name: "get_demo_data_template",
|
|
516
|
-
description: `Get a template for demo data documents based on entity type.
|
|
517
|
-
|
|
518
|
-
Returns:
|
|
519
|
-
- Source JSON schema (what fields to include)
|
|
520
|
-
- Output Markdown template
|
|
521
|
-
- Best practices for the entity type
|
|
522
|
-
- Example data`,
|
|
523
|
-
inputSchema: {
|
|
524
|
-
type: "object",
|
|
525
|
-
properties: {
|
|
526
|
-
entity_type: {
|
|
527
|
-
type: "string",
|
|
528
|
-
enum: ["customer", "product", "employee", "scenario", "reference"],
|
|
529
|
-
description: "Type of template to get",
|
|
530
|
-
},
|
|
531
|
-
include_example: {
|
|
532
|
-
type: "boolean",
|
|
533
|
-
description: "Include example data (default: true)",
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
required: ["entity_type"],
|
|
537
|
-
},
|
|
538
|
-
},
|
|
539
|
-
];
|
|
540
|
-
const toolHandlers = {
|
|
541
|
-
// Environment Management
|
|
542
|
-
list_environments: async () => {
|
|
543
|
-
const envs = getAvailableEnvironments();
|
|
544
|
-
const defaultEnv = getDefaultEnvName();
|
|
545
|
-
return {
|
|
546
|
-
default_environment: defaultEnv,
|
|
547
|
-
environments: envs.map((e) => ({
|
|
548
|
-
name: e.name,
|
|
549
|
-
url: e.baseUrl,
|
|
550
|
-
is_default: e.name === defaultEnv,
|
|
551
|
-
})),
|
|
552
|
-
};
|
|
553
|
-
},
|
|
554
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
555
|
-
// AI Employee Handlers (Consolidated)
|
|
556
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
557
|
-
get_persona: async (args) => {
|
|
558
|
-
const client = createClient(args.env);
|
|
559
|
-
const identifier = String(args.identifier);
|
|
560
|
-
const includeWorkflow = args.include_workflow === true;
|
|
561
|
-
const includeFingerprint = args.include_fingerprint === true;
|
|
562
|
-
// Auto-detect: UUIDs are 36 chars with dashes, otherwise it's a name
|
|
563
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
564
|
-
let persona;
|
|
565
|
-
if (isUUID) {
|
|
566
|
-
// Fetch by ID - use full fetch if workflow needed, otherwise list
|
|
567
|
-
if (includeWorkflow || includeFingerprint) {
|
|
568
|
-
persona = (await client.getPersonaById(identifier)) ?? undefined;
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
const personas = await client.getPersonasForTenant();
|
|
572
|
-
persona = personas.find((p) => p.id === identifier);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
else {
|
|
576
|
-
// Fetch by name - always need to list first to find ID
|
|
577
|
-
const personas = await client.getPersonasForTenant();
|
|
578
|
-
persona = personas.find((p) => p.name === identifier);
|
|
579
|
-
// If found and need workflow, fetch full details
|
|
580
|
-
if (persona && (includeWorkflow || includeFingerprint)) {
|
|
581
|
-
persona = (await client.getPersonaById(persona.id)) ?? undefined;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
if (!persona) {
|
|
585
|
-
throw new Error(`AI Employee not found: ${identifier} (searched by ${isUUID ? "ID" : "name"})`);
|
|
586
|
-
}
|
|
587
|
-
// Build response
|
|
588
|
-
const result = {
|
|
589
|
-
environment: client["env"].name,
|
|
590
|
-
ai_employee: persona,
|
|
591
|
-
};
|
|
592
|
-
// Add fingerprint if requested
|
|
593
|
-
if (includeFingerprint) {
|
|
594
|
-
result.fingerprint = fingerprintPersona(persona);
|
|
595
|
-
}
|
|
596
|
-
return result;
|
|
597
|
-
},
|
|
598
|
-
find_personas: async (args) => {
|
|
599
|
-
const client = createClient(args.env);
|
|
600
|
-
let personas = await client.getPersonasForTenant();
|
|
601
|
-
// Apply filters
|
|
602
|
-
if (args.query) {
|
|
603
|
-
const q = String(args.query).toLowerCase();
|
|
604
|
-
personas = personas.filter((p) => p.name?.toLowerCase().includes(q));
|
|
605
|
-
}
|
|
606
|
-
if (args.status) {
|
|
607
|
-
const f = String(args.status).toLowerCase();
|
|
608
|
-
personas = personas.filter((p) => p.status?.toLowerCase() === f);
|
|
609
|
-
}
|
|
610
|
-
if (args.trigger_type) {
|
|
611
|
-
const f = String(args.trigger_type).toLowerCase();
|
|
612
|
-
personas = personas.filter((p) => p.trigger_type?.toLowerCase() === f);
|
|
613
|
-
}
|
|
614
|
-
if (args.access_level) {
|
|
615
|
-
const f = String(args.access_level).toLowerCase();
|
|
616
|
-
personas = personas.filter((p) => p.access_level?.toLowerCase() === f);
|
|
617
|
-
}
|
|
618
|
-
if (typeof args.has_workflow === "boolean") {
|
|
619
|
-
personas = personas.filter((p) => args.has_workflow ? !!p.workflow_id : !p.workflow_id);
|
|
620
|
-
}
|
|
621
|
-
if (typeof args.embedding_enabled === "boolean") {
|
|
622
|
-
personas = personas.filter((p) => p.embedding_enabled === args.embedding_enabled);
|
|
623
|
-
}
|
|
624
|
-
const limit = typeof args.limit === "number" ? args.limit : 50;
|
|
625
|
-
personas = personas.slice(0, limit);
|
|
626
|
-
return {
|
|
627
|
-
environment: client["env"].name,
|
|
628
|
-
count: personas.length,
|
|
629
|
-
ai_employees: personas.map((p) => ({
|
|
630
|
-
id: p.id,
|
|
631
|
-
name: p.name,
|
|
632
|
-
description: p.description,
|
|
633
|
-
status: p.status,
|
|
634
|
-
template_id: p.template_id ?? p.templateId,
|
|
635
|
-
workflow_id: p.workflow_id,
|
|
636
|
-
trigger_type: p.trigger_type,
|
|
637
|
-
access_level: p.access_level,
|
|
638
|
-
embedding_enabled: p.embedding_enabled,
|
|
639
|
-
})),
|
|
640
|
-
};
|
|
641
|
-
},
|
|
642
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
643
|
-
// AI Employee CRUD Handlers
|
|
644
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
645
|
-
create_ai_employee: async (args) => {
|
|
646
|
-
const client = createClient(args.env);
|
|
647
|
-
// Determine template_id: explicit > dynamic lookup based on persona_type
|
|
648
|
-
let templateId = args.template_id ? String(args.template_id) : undefined;
|
|
649
|
-
const sourcePersonaId = args.source_persona_id ? String(args.source_persona_id) : undefined;
|
|
650
|
-
// If no template_id or source_persona_id, use dynamic template lookup
|
|
651
|
-
if (!templateId && !sourcePersonaId) {
|
|
652
|
-
const personaType = args.persona_type ? String(args.persona_type).toLowerCase() : null;
|
|
653
|
-
if (personaType) {
|
|
654
|
-
// Dynamic template lookup - templates are tenant-specific
|
|
655
|
-
const templates = await client.getPersonaTemplates();
|
|
656
|
-
const matchingTemplate = templates.find(t => normalizeTriggerType(t.trigger_type) === personaType);
|
|
657
|
-
if (matchingTemplate) {
|
|
658
|
-
templateId = matchingTemplate.id;
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
const availableTypes = [...new Set(templates.map(t => normalizeTriggerType(t.trigger_type)).filter(Boolean))];
|
|
662
|
-
throw new Error(`No template found for type "${personaType}". Available types: ${availableTypes.join(", ") || "none"}`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
throw new Error("Must provide template_id, source_persona_id, or persona_type");
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
const req = {
|
|
670
|
-
name: String(args.name),
|
|
671
|
-
description: args.description ? String(args.description) : undefined,
|
|
672
|
-
template_id: templateId,
|
|
673
|
-
source_persona_id: sourcePersonaId,
|
|
674
|
-
// Note: trigger_type is determined by template, not passed separately
|
|
675
|
-
};
|
|
676
|
-
const result = await client.createAiEmployee(req);
|
|
677
|
-
return {
|
|
678
|
-
environment: client["env"].name,
|
|
679
|
-
success: true,
|
|
680
|
-
persona_id: result.persona_id ?? result.id,
|
|
681
|
-
status: result.status ?? "created",
|
|
682
|
-
template_used: templateId,
|
|
683
|
-
};
|
|
684
|
-
},
|
|
685
|
-
update_ai_employee: async (args) => {
|
|
686
|
-
const client = createClient(args.env);
|
|
687
|
-
const personaId = String(args.persona_id);
|
|
688
|
-
// Use getPersonaById for complete data (including full proto_config)
|
|
689
|
-
const existing = await client.getPersonaById(personaId);
|
|
690
|
-
if (!existing)
|
|
691
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
692
|
-
// Merge proto_config using SDK function (deep merges widgets by name)
|
|
693
|
-
const mergedProtoConfig = mergeProtoConfig(existing.proto_config, args.proto_config);
|
|
694
|
-
// Check if user is trying to pass workflow - redirect them to deploy_workflow
|
|
695
|
-
if (args.workflow) {
|
|
696
|
-
return {
|
|
697
|
-
environment: client["env"].name,
|
|
698
|
-
success: false,
|
|
699
|
-
error: "workflow_parameter_deprecated",
|
|
700
|
-
message: "The 'workflow' parameter has been removed from update_ai_employee. " +
|
|
701
|
-
"Use workflow(mode='get' → mode='deploy') instead (it provides validation and strict stale-state protection).",
|
|
702
|
-
suggestion: {
|
|
703
|
-
tool: "workflow",
|
|
704
|
-
parameters: {
|
|
705
|
-
mode: "deploy",
|
|
706
|
-
persona_id: personaId,
|
|
707
|
-
base_fingerprint: "<fingerprint_from_workflow_get>",
|
|
708
|
-
workflow_def: "your_workflow_here",
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
// CRITICAL: The Ema API requires workflow to be sent along with proto_config
|
|
714
|
-
// for proto_config changes to persist. Without workflow, updates silently fail.
|
|
715
|
-
// GET returns 'workflow_def', UPDATE expects 'workflow' field.
|
|
716
|
-
const existingWorkflow = (existing.workflow_def ?? existing.workflow);
|
|
717
|
-
const req = {
|
|
718
|
-
persona_id: personaId,
|
|
719
|
-
name: args.name ? String(args.name) : undefined,
|
|
720
|
-
description: args.description ? String(args.description) : undefined,
|
|
721
|
-
proto_config: mergedProtoConfig,
|
|
722
|
-
workflow: existingWorkflow, // Required for proto_config to persist
|
|
723
|
-
embedding_enabled: typeof args.embedding_enabled === "boolean" ? args.embedding_enabled : undefined,
|
|
724
|
-
enabled_by_user: typeof args.enabled_by_user === "boolean" ? args.enabled_by_user : undefined,
|
|
725
|
-
};
|
|
726
|
-
await client.updateAiEmployee(req);
|
|
727
|
-
return {
|
|
728
|
-
environment: client["env"].name,
|
|
729
|
-
success: true,
|
|
730
|
-
persona_id: personaId,
|
|
731
|
-
persona_name: existing.name,
|
|
732
|
-
updated_fields: {
|
|
733
|
-
name: !!args.name,
|
|
734
|
-
description: !!args.description,
|
|
735
|
-
proto_config: !!args.proto_config,
|
|
736
|
-
embedding_enabled: typeof args.embedding_enabled === "boolean",
|
|
737
|
-
enabled_by_user: typeof args.enabled_by_user === "boolean",
|
|
738
|
-
},
|
|
739
|
-
note: "For workflow changes, use deploy_workflow which provides validation and auto-fix.",
|
|
740
|
-
};
|
|
741
|
-
},
|
|
742
|
-
deploy_workflow: async (args) => {
|
|
743
|
-
const client = createClient(args.env);
|
|
744
|
-
const personaId = String(args.persona_id);
|
|
745
|
-
const validateFirst = args.validate_first !== false; // default true
|
|
746
|
-
const autoFix = args.auto_fix === true; // default false
|
|
747
|
-
const force = args.force === true;
|
|
748
|
-
const baseFingerprint = args.base_fingerprint;
|
|
749
|
-
// Get existing persona with full details
|
|
750
|
-
const persona = await client.getPersonaById(personaId);
|
|
751
|
-
if (!persona)
|
|
752
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
753
|
-
// STRICT: stale-state protection + required snapshot (same safety as workflow tool)
|
|
754
|
-
const currentFp = fingerprintPersona(persona);
|
|
755
|
-
if (!force && !baseFingerprint) {
|
|
756
|
-
return {
|
|
757
|
-
environment: client["env"].name,
|
|
758
|
-
success: false,
|
|
759
|
-
error: "base_fingerprint_required",
|
|
760
|
-
persona_id: personaId,
|
|
761
|
-
current_fingerprint: currentFp,
|
|
762
|
-
message: "base_fingerprint is required for deploy_workflow (stale-state protection). " +
|
|
763
|
-
"Re-fetch the latest state and retry.",
|
|
764
|
-
hint: "Prefer: workflow(mode='get') → workflow(mode='deploy', base_fingerprint='<fingerprint>', ...). " +
|
|
765
|
-
"Use force=true only for emergency overrides.",
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
if (!force && baseFingerprint && baseFingerprint !== currentFp) {
|
|
769
|
-
return {
|
|
770
|
-
environment: client["env"].name,
|
|
771
|
-
success: false,
|
|
772
|
-
error: "fingerprint_mismatch",
|
|
773
|
-
persona_id: personaId,
|
|
774
|
-
base_fingerprint: baseFingerprint,
|
|
775
|
-
current_fingerprint: currentFp,
|
|
776
|
-
message: "Persona changed since you last fetched it (fingerprint mismatch).",
|
|
777
|
-
hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. " +
|
|
778
|
-
"Use force=true only if you intend to overwrite out-of-band changes.",
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
// Required pre-deploy snapshot (local)
|
|
782
|
-
try {
|
|
783
|
-
const storage = createVersionStorage(process.cwd());
|
|
784
|
-
const engine = createVersionPolicyEngine(storage);
|
|
785
|
-
const snap = engine.forceCreateVersion(persona, {
|
|
786
|
-
environment: client["env"].name,
|
|
787
|
-
tenant_id: client["env"].name,
|
|
788
|
-
message: "Pre-deploy snapshot (deploy_workflow)",
|
|
789
|
-
created_by: "mcp-toolkit",
|
|
790
|
-
});
|
|
791
|
-
if (!snap.created || !snap.version) {
|
|
792
|
-
if (!force) {
|
|
793
|
-
return {
|
|
794
|
-
environment: client["env"].name,
|
|
795
|
-
success: false,
|
|
796
|
-
error: "pre_deploy_snapshot_failed",
|
|
797
|
-
persona_id: personaId,
|
|
798
|
-
details: snap.reason,
|
|
799
|
-
hint: "Fix local snapshot storage or retry with force=true for emergency override.",
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
catch (e) {
|
|
805
|
-
if (!force) {
|
|
806
|
-
return {
|
|
807
|
-
environment: client["env"].name,
|
|
808
|
-
success: false,
|
|
809
|
-
error: "pre_deploy_snapshot_failed",
|
|
810
|
-
persona_id: personaId,
|
|
811
|
-
details: e instanceof Error ? e.message : String(e),
|
|
812
|
-
hint: "Fix local snapshot storage or retry with force=true for emergency override.",
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
let workflowDef = args.workflow_def;
|
|
817
|
-
const protoConfig = args.proto_config;
|
|
818
|
-
if (!workflowDef && !protoConfig) {
|
|
819
|
-
throw new Error("At least one of workflow_def or proto_config must be provided");
|
|
820
|
-
}
|
|
821
|
-
// Sanitize workflow_def to prevent server-side crashes
|
|
822
|
-
if (workflowDef) {
|
|
823
|
-
// Fix enumTypes - remove entries with empty/missing names (causes server panic)
|
|
824
|
-
// EnumType structure in proto: { name: { name: { name: "string", namespaces: [] } }, options: [...] }
|
|
825
|
-
// The backend calls FlattenNamespacedName(enum.Name.Name) which panics if Name.Name is nil
|
|
826
|
-
const enumTypes = workflowDef.enumTypes;
|
|
827
|
-
if (Array.isArray(enumTypes)) {
|
|
828
|
-
const validEnumTypes = enumTypes.filter(e => {
|
|
829
|
-
// Navigate the nested structure: e.name.name.name
|
|
830
|
-
const outerName = e.name;
|
|
831
|
-
const innerName = outerName?.name;
|
|
832
|
-
const actualName = innerName?.name;
|
|
833
|
-
// Must have the full structure with a non-empty string name
|
|
834
|
-
return typeof actualName === "string" && actualName.trim().length > 0;
|
|
835
|
-
});
|
|
836
|
-
if (validEnumTypes.length > 0) {
|
|
837
|
-
workflowDef.enumTypes = validEnumTypes;
|
|
838
|
-
}
|
|
839
|
-
else {
|
|
840
|
-
// Remove empty enumTypes entirely
|
|
841
|
-
delete workflowDef.enumTypes;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
// Ensure all actions have a 'name' field (node identifier)
|
|
845
|
-
const actions = workflowDef.actions;
|
|
846
|
-
if (Array.isArray(actions)) {
|
|
847
|
-
for (const action of actions) {
|
|
848
|
-
// If action has 'actionName' but not 'name', fix it
|
|
849
|
-
if (!action.name && action.actionName) {
|
|
850
|
-
action.name = action.actionName;
|
|
851
|
-
}
|
|
852
|
-
// Ensure name is a non-empty string
|
|
853
|
-
if (!action.name || (typeof action.name === "string" && action.name.trim().length === 0)) {
|
|
854
|
-
// Try to derive from action type
|
|
855
|
-
const actionType = action.action;
|
|
856
|
-
if (actionType?.name?.name) {
|
|
857
|
-
action.name = `${actionType.name.name}_${actions.indexOf(action)}`;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
// CRITICAL: Normalize action structure to include required empty fields
|
|
861
|
-
// The backend expects these fields to exist (even if empty) or it may 500
|
|
862
|
-
if (action.typeArguments === undefined) {
|
|
863
|
-
action.typeArguments = {};
|
|
864
|
-
}
|
|
865
|
-
if (action.tools === undefined) {
|
|
866
|
-
action.tools = [];
|
|
867
|
-
}
|
|
868
|
-
if (action.disableHumanInteraction === undefined) {
|
|
869
|
-
action.disableHumanInteraction = false;
|
|
870
|
-
}
|
|
871
|
-
// Ensure displaySettings exists and has required structure
|
|
872
|
-
if (!action.displaySettings) {
|
|
873
|
-
action.displaySettings = {
|
|
874
|
-
displayName: String(action.name || ""),
|
|
875
|
-
coordinates: { x: 0, y: 0 },
|
|
876
|
-
description: "",
|
|
877
|
-
showConfig: 0,
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
else {
|
|
881
|
-
const ds = action.displaySettings;
|
|
882
|
-
if (ds.description === undefined)
|
|
883
|
-
ds.description = "";
|
|
884
|
-
if (ds.showConfig === undefined)
|
|
885
|
-
ds.showConfig = 0;
|
|
886
|
-
}
|
|
887
|
-
// Ensure inputs exists
|
|
888
|
-
if (action.inputs === undefined) {
|
|
889
|
-
action.inputs = {};
|
|
890
|
-
}
|
|
891
|
-
// Normalize runIf operator enum: backend proto expects numeric values (e.g. 1),
|
|
892
|
-
// but some JSON payloads use string enums (e.g. "OPERATOR_EQ") which can 500.
|
|
893
|
-
const runIf = action.runIf;
|
|
894
|
-
if (runIf && typeof runIf === "object") {
|
|
895
|
-
const op = runIf.operator;
|
|
896
|
-
if (typeof op === "string") {
|
|
897
|
-
const opMap = {
|
|
898
|
-
OPERATOR_EQ: 1,
|
|
899
|
-
OPERATOR_NEQ: 2,
|
|
900
|
-
OPERATOR_GT: 3,
|
|
901
|
-
OPERATOR_GTE: 4,
|
|
902
|
-
OPERATOR_LT: 5,
|
|
903
|
-
OPERATOR_LTE: 6,
|
|
904
|
-
OPERATOR_IN: 7,
|
|
905
|
-
OPERATOR_NOT_IN: 8,
|
|
906
|
-
};
|
|
907
|
-
if (opMap[op] !== undefined)
|
|
908
|
-
runIf.operator = opMap[op];
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
// Get existing workflow info
|
|
915
|
-
const existingWorkflow = persona.workflow_def;
|
|
916
|
-
const existingWorkflowId = persona.workflow_id;
|
|
917
|
-
// Copy missing top-level workflow fields from existing workflow.
|
|
918
|
-
// Some backends are strict about presence of these keys.
|
|
919
|
-
if (workflowDef && existingWorkflow) {
|
|
920
|
-
const copyIfMissing = (k) => {
|
|
921
|
-
if (workflowDef[k] === undefined && existingWorkflow[k] !== undefined) {
|
|
922
|
-
workflowDef[k] = JSON.parse(JSON.stringify(existingWorkflow[k]));
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
copyIfMissing("workflowInputs");
|
|
926
|
-
copyIfMissing("namedResults");
|
|
927
|
-
copyIfMissing("displayName");
|
|
928
|
-
copyIfMissing("description");
|
|
929
|
-
copyIfMissing("namedResultsEditable");
|
|
930
|
-
copyIfMissing("namedResultsEnabled");
|
|
931
|
-
copyIfMissing("edges");
|
|
932
|
-
}
|
|
933
|
-
// Determine deployment strategy
|
|
934
|
-
const hasExistingWorkflow = !!existingWorkflowId;
|
|
935
|
-
let deploymentMethod = hasExistingWorkflow ? "direct_api" : "autobuilder";
|
|
936
|
-
// === WORKFLOW ID TRANSFORMATION ===
|
|
937
|
-
// If workflow was generated for a different persona, transform IDs to target persona
|
|
938
|
-
// (No brownfield merging - we do full workflow replacement)
|
|
939
|
-
if (workflowDef && hasExistingWorkflow && existingWorkflow) {
|
|
940
|
-
// Extract source persona ID from incoming workflow (if any)
|
|
941
|
-
const incomingWfName = workflowDef.workflowName;
|
|
942
|
-
const sourcePersonaId = incomingWfName?.name?.namespaces?.[2]; // Usually at index 2: ["ema", "templates", "<persona_id>"]
|
|
943
|
-
if (sourcePersonaId && sourcePersonaId !== personaId) {
|
|
944
|
-
// Workflow was generated for a different persona - transform IDs
|
|
945
|
-
workflowDef = transformWorkflowForTarget(workflowDef, sourcePersonaId, personaId);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
else if (workflowDef && hasExistingWorkflow && !existingWorkflow) {
|
|
949
|
-
// Has workflow_id but no workflow_def (edge case) - construct workflowName from workflow_id
|
|
950
|
-
// workflow_id format: "ema.templates.<persona_id>.default" or similar
|
|
951
|
-
const parts = existingWorkflowId.split(".");
|
|
952
|
-
if (parts.length >= 3) {
|
|
953
|
-
workflowDef.workflowName = {
|
|
954
|
-
name: {
|
|
955
|
-
namespaces: parts.slice(0, -1),
|
|
956
|
-
name: parts[parts.length - 1],
|
|
957
|
-
},
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
// Track fixes applied
|
|
962
|
-
const appliedFixes = [];
|
|
963
|
-
let fixAttempted = false;
|
|
964
|
-
// Validation is now done by backend when workflow is deployed
|
|
965
|
-
// LLM should use ema://rules/* for pre-validation guidance
|
|
966
|
-
const validationResults = { valid: true, issues: [] };
|
|
967
|
-
if (validateFirst && workflowDef) {
|
|
968
|
-
// DEPRECATED: MCP no longer pre-validates workflows
|
|
969
|
-
// Backend validation happens on deploy
|
|
970
|
-
// LLM uses ema://rules/anti-patterns for analysis
|
|
971
|
-
console.warn("[DEPRECATED] validateFirst is deprecated - backend validates on deploy");
|
|
972
|
-
if (autoFix) {
|
|
973
|
-
// Auto-fix is removed - return guidance for LLM
|
|
974
|
-
return {
|
|
975
|
-
environment: client["env"].name,
|
|
976
|
-
success: false,
|
|
977
|
-
persona_id: personaId,
|
|
978
|
-
persona_name: persona.name,
|
|
979
|
-
_deprecation_notice: "autoFix is deprecated. Use LLM analysis with ema://rules/* instead.",
|
|
980
|
-
_guidance: [
|
|
981
|
-
"1. Fetch ema://rules/anti-patterns",
|
|
982
|
-
"2. Analyze workflow against rules",
|
|
983
|
-
"3. Make structured modifications",
|
|
984
|
-
"4. Deploy via workflow(mode='deploy')",
|
|
985
|
-
],
|
|
986
|
-
applied_fixes: [],
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
// If validation failed and not forcing, return the issues
|
|
991
|
-
if (!validationResults.valid) {
|
|
992
|
-
return {
|
|
993
|
-
environment: client["env"].name,
|
|
994
|
-
success: false,
|
|
995
|
-
persona_id: personaId,
|
|
996
|
-
persona_name: persona.name,
|
|
997
|
-
validation_failed: true,
|
|
998
|
-
issues: validationResults.issues,
|
|
999
|
-
auto_fix_attempted: fixAttempted,
|
|
1000
|
-
fixes_applied: appliedFixes.filter(f => f.applied),
|
|
1001
|
-
fixes_failed: appliedFixes.filter(f => !f.applied),
|
|
1002
|
-
remaining_issues: validationResults.issues.length,
|
|
1003
|
-
hint: autoFix
|
|
1004
|
-
? "Some issues could not be auto-fixed. Review the remaining issues and fix manually, or set validate_first=false to skip validation (not recommended)"
|
|
1005
|
-
: "Fix the issues above, enable auto_fix=true for automatic fixes, or set validate_first=false to skip validation (not recommended)",
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
// Merge proto_config using SDK function (deep merges widgets by name)
|
|
1009
|
-
const mergedProtoConfig = mergeProtoConfig(persona.proto_config, protoConfig);
|
|
1010
|
-
// Auto-fix malformed HITL runIf patterns BEFORE deploy (H1: production-grade fix)
|
|
1011
|
-
// Pattern: "hitl_status_HITL Success" should be output="hitl_status", enumValue="HITL Success"
|
|
1012
|
-
let hitlFixCount = 0;
|
|
1013
|
-
if (workflowDef) {
|
|
1014
|
-
const wfActions = workflowDef.actions ?? [];
|
|
1015
|
-
for (const action of wfActions) {
|
|
1016
|
-
const runIfAction = action.runIf;
|
|
1017
|
-
if (!runIfAction)
|
|
1018
|
-
continue;
|
|
1019
|
-
const lhsAction = runIfAction.lhs;
|
|
1020
|
-
const rhsAction = runIfAction.rhs;
|
|
1021
|
-
if (lhsAction?.actionOutput && rhsAction?.inline) {
|
|
1022
|
-
const actionOutputField = lhsAction.actionOutput;
|
|
1023
|
-
const inlineRhsField = rhsAction.inline;
|
|
1024
|
-
const outputStr = String(actionOutputField.output ?? "");
|
|
1025
|
-
// Detect malformed HITL patterns: "hitl_status_HITL Success" or "hitl_status HITL Success"
|
|
1026
|
-
const hitlPatternMatch = outputStr.match(/^hitl_status[_\s]?(HITL[ _]?(?:Success|Failure))$/i);
|
|
1027
|
-
if (hitlPatternMatch) {
|
|
1028
|
-
// Extract and normalize the enum value
|
|
1029
|
-
let correctedEnumVal = hitlPatternMatch[1].replace(/_/g, " ");
|
|
1030
|
-
if (correctedEnumVal.toLowerCase().includes("success")) {
|
|
1031
|
-
correctedEnumVal = "HITL Success";
|
|
1032
|
-
}
|
|
1033
|
-
else if (correctedEnumVal.toLowerCase().includes("failure")) {
|
|
1034
|
-
correctedEnumVal = "HITL Failure";
|
|
1035
|
-
}
|
|
1036
|
-
// Apply the fix
|
|
1037
|
-
actionOutputField.output = "hitl_status";
|
|
1038
|
-
inlineRhsField.enumValue = correctedEnumVal;
|
|
1039
|
-
hitlFixCount++;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
// Build update request
|
|
1045
|
-
const req = {
|
|
1046
|
-
persona_id: personaId,
|
|
1047
|
-
proto_config: mergedProtoConfig,
|
|
1048
|
-
workflow: workflowDef,
|
|
1049
|
-
};
|
|
1050
|
-
// Deployment attempt with automatic fallback
|
|
1051
|
-
let deployedVia = "direct_api";
|
|
1052
|
-
let autobuilderResult;
|
|
1053
|
-
if (deploymentMethod === "direct_api") {
|
|
1054
|
-
try {
|
|
1055
|
-
await client.updateAiEmployee(req);
|
|
1056
|
-
}
|
|
1057
|
-
catch (err) {
|
|
1058
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1059
|
-
// If direct API fails due to "no existing workflow", try Auto Builder
|
|
1060
|
-
if (errorMessage.includes("Cannot set persona workflow without existing workflow") && workflowDef) {
|
|
1061
|
-
deploymentMethod = "autobuilder";
|
|
1062
|
-
}
|
|
1063
|
-
else if (errorMessage.includes("Workflow name does not match")) {
|
|
1064
|
-
// This shouldn't happen with our name sync, but handle gracefully
|
|
1065
|
-
throw new Error(`Workflow deployment failed: The workflow structure may be incompatible. ` +
|
|
1066
|
-
`Please use the Ema UI Auto Builder to make changes to this persona's workflow. ` +
|
|
1067
|
-
`(Technical: ${errorMessage})`);
|
|
1068
|
-
}
|
|
1069
|
-
else if ((errorMessage.toLowerCase().includes("internal server error") || errorMessage.includes("500")) && workflowDef) {
|
|
1070
|
-
// 500 error - attempt Autobuilder fallback
|
|
1071
|
-
deploymentMethod = "autobuilder";
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
// Other API errors - surface clearly
|
|
1075
|
-
throw new Error(`Workflow deployment failed: ${errorMessage}`);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
// Auto Builder fallback for personas without existing workflows
|
|
1080
|
-
if (deploymentMethod === "autobuilder" && workflowDef) {
|
|
1081
|
-
try {
|
|
1082
|
-
// Generate a prompt that asks the Auto Builder to deploy this specific workflow
|
|
1083
|
-
const workflowSummary = summarizeWorkflow(workflowDef);
|
|
1084
|
-
const prompt = `Deploy this workflow to the persona. The workflow has the following structure:\n\n${workflowSummary}\n\nPlease create and save this workflow.`;
|
|
1085
|
-
// Use the iterate workflow method which handles Auto Builder discovery
|
|
1086
|
-
autobuilderResult = await client.iterateWorkflow(personaId, prompt, { newConversation: true });
|
|
1087
|
-
deployedVia = "autobuilder";
|
|
1088
|
-
// Also update proto_config if provided (Auto Builder may not handle this)
|
|
1089
|
-
if (protoConfig) {
|
|
1090
|
-
await client.updateAiEmployee({
|
|
1091
|
-
persona_id: personaId,
|
|
1092
|
-
proto_config: mergedProtoConfig,
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
catch (autoErr) {
|
|
1097
|
-
const autoErrorMessage = autoErr instanceof Error ? autoErr.message : String(autoErr);
|
|
1098
|
-
// If Auto Builder also fails, provide clear guidance
|
|
1099
|
-
if (autoErrorMessage.includes("No Autobuilder persona found")) {
|
|
1100
|
-
throw new Error(`Cannot deploy workflow: This persona has no existing workflow, and the Ema Auto Builder is not available in this tenant. ` +
|
|
1101
|
-
`Please contact your Ema administrator to enable the Auto Builder, or create a new persona from a workflow template.`);
|
|
1102
|
-
}
|
|
1103
|
-
throw new Error(`Workflow deployment via Auto Builder failed: ${autoErrorMessage}`);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
// Build success note
|
|
1107
|
-
let successNote = "Workflow deployed successfully. Test in the Ema simulator to verify behavior.";
|
|
1108
|
-
if (deployedVia === "autobuilder") {
|
|
1109
|
-
successNote = "Workflow deployed via Ema Auto Builder. Test in the Ema simulator to verify behavior.";
|
|
1110
|
-
}
|
|
1111
|
-
else if (fixAttempted && appliedFixes.some(f => f.applied)) {
|
|
1112
|
-
successNote = `Workflow deployed successfully with ${appliedFixes.filter(f => f.applied).length} auto-fix(es) applied. Test in the Ema simulator to verify behavior.`;
|
|
1113
|
-
}
|
|
1114
|
-
else if (!workflowDef) {
|
|
1115
|
-
successNote = "Proto config updated successfully.";
|
|
1116
|
-
}
|
|
1117
|
-
return {
|
|
1118
|
-
environment: client["env"].name,
|
|
1119
|
-
success: true,
|
|
1120
|
-
persona_id: personaId,
|
|
1121
|
-
persona_name: persona.name,
|
|
1122
|
-
deployed: {
|
|
1123
|
-
workflow_def: !!workflowDef,
|
|
1124
|
-
proto_config: !!protoConfig,
|
|
1125
|
-
},
|
|
1126
|
-
deployment_method: deployedVia,
|
|
1127
|
-
validation_passed: validationResults.valid,
|
|
1128
|
-
auto_fix_applied: fixAttempted && appliedFixes.some(f => f.applied),
|
|
1129
|
-
fixes_applied: appliedFixes.filter(f => f.applied),
|
|
1130
|
-
autobuilder_response: autobuilderResult?.response,
|
|
1131
|
-
note: successNote,
|
|
1132
|
-
};
|
|
1133
|
-
},
|
|
1134
|
-
optimize_workflow: async (args) => {
|
|
1135
|
-
const client = createClient(args.env);
|
|
1136
|
-
const identifier = args.identifier ? String(args.identifier) : undefined;
|
|
1137
|
-
const targetPersonaId = args.persona_id ? String(args.persona_id) : undefined;
|
|
1138
|
-
const prompt = args.prompt ? String(args.prompt) : undefined;
|
|
1139
|
-
const personaType = args.type ?? "chat";
|
|
1140
|
-
const preview = args.preview === true;
|
|
1141
|
-
// Validate inputs
|
|
1142
|
-
if (!identifier && !targetPersonaId && !prompt) {
|
|
1143
|
-
throw new Error('Provide either: identifier (to fix existing), or persona_id + prompt (to enhance existing)');
|
|
1144
|
-
}
|
|
1145
|
-
let persona = null;
|
|
1146
|
-
let workflowDef;
|
|
1147
|
-
let personaId;
|
|
1148
|
-
let enhancementPrompt = prompt; // Store prompt for enhancement logging
|
|
1149
|
-
// === ALWAYS START BY FETCHING EXISTING WORKFLOW ===
|
|
1150
|
-
// Brownfield: fix existing + apply enhancements from prompt
|
|
1151
|
-
// The prompt describes what to ADD or CHANGE, not a complete replacement
|
|
1152
|
-
const lookupId = identifier ?? targetPersonaId;
|
|
1153
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(lookupId);
|
|
1154
|
-
if (isUUID) {
|
|
1155
|
-
persona = await client.getPersonaById(lookupId);
|
|
1156
|
-
}
|
|
1157
|
-
else {
|
|
1158
|
-
// Search by name
|
|
1159
|
-
const personas = await client.getPersonasForTenant();
|
|
1160
|
-
const match = personas.find((p) => p.name?.toLowerCase() === lookupId.toLowerCase() ||
|
|
1161
|
-
p.name?.toLowerCase().includes(lookupId.toLowerCase()));
|
|
1162
|
-
if (match) {
|
|
1163
|
-
persona = await client.getPersonaById(match.id);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
if (!persona) {
|
|
1167
|
-
throw new Error(`AI Employee "${lookupId}" not found. Check the name or ID.`);
|
|
1168
|
-
}
|
|
1169
|
-
personaId = persona.id;
|
|
1170
|
-
workflowDef = persona.workflow_def;
|
|
1171
|
-
if (!workflowDef) {
|
|
1172
|
-
return {
|
|
1173
|
-
success: false,
|
|
1174
|
-
persona: persona.name,
|
|
1175
|
-
status: "⚠️ No Workflow",
|
|
1176
|
-
message: "This AI Employee has no workflow. Use prompt parameter to generate one: optimize_workflow(persona_id=\"...\", prompt=\"description of what it should do\")",
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
// DEPRECATED: MCP no longer pre-analyzes workflows
|
|
1180
|
-
// LLM should use ema://rules/* for analysis
|
|
1181
|
-
console.warn("[DEPRECATED] optimize_workflow tool is deprecated - use LLM analysis with ema://rules/*");
|
|
1182
|
-
const nodes = parseWorkflowDef(workflowDef);
|
|
1183
|
-
return {
|
|
1184
|
-
success: true,
|
|
1185
|
-
persona: persona?.name ?? "Unknown",
|
|
1186
|
-
persona_id: personaId,
|
|
1187
|
-
status: "DEPRECATED - use LLM analysis",
|
|
1188
|
-
node_count: nodes.length,
|
|
1189
|
-
workflow_def: workflowDef,
|
|
1190
|
-
_deprecation_notice: {
|
|
1191
|
-
message: "optimize_workflow is deprecated. MCP does not pre-compute issues.",
|
|
1192
|
-
new_workflow: [
|
|
1193
|
-
"1. Fetch rules: ema://rules/anti-patterns, ema://rules/optimizations",
|
|
1194
|
-
"2. Apply rules to find issues (LLM does this, not MCP)",
|
|
1195
|
-
"3. Make structured modifications based on your analysis",
|
|
1196
|
-
"4. Deploy via workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
1197
|
-
],
|
|
1198
|
-
},
|
|
1199
|
-
};
|
|
1200
|
-
},
|
|
1201
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1202
|
-
// Action Handlers (Consolidated)
|
|
1203
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1204
|
-
get_workflow_action: async (args) => {
|
|
1205
|
-
const client = createClient(args.env);
|
|
1206
|
-
const identifier = String(args.identifier);
|
|
1207
|
-
const actions = await client.listActions();
|
|
1208
|
-
// Try ID first, then name
|
|
1209
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
1210
|
-
let action = isUUID
|
|
1211
|
-
? actions.find((a) => a.id === identifier)
|
|
1212
|
-
: actions.find((a) => a.name?.toLowerCase() === identifier.toLowerCase());
|
|
1213
|
-
if (!action) {
|
|
1214
|
-
throw new Error(`Action not found: ${identifier} (searched by ${isUUID ? "ID" : "name"})`);
|
|
1215
|
-
}
|
|
1216
|
-
return { environment: client["env"].name, action };
|
|
1217
|
-
},
|
|
1218
|
-
find_workflow_actions: async (args) => {
|
|
1219
|
-
const client = createClient(args.env);
|
|
1220
|
-
// Handle list_categories request
|
|
1221
|
-
if (args.list_categories === true) {
|
|
1222
|
-
const actions = await client.listActions();
|
|
1223
|
-
const categoryMap = new Map();
|
|
1224
|
-
for (const a of actions) {
|
|
1225
|
-
const cat = a.category ?? "uncategorized";
|
|
1226
|
-
categoryMap.set(cat, (categoryMap.get(cat) ?? 0) + 1);
|
|
1227
|
-
}
|
|
1228
|
-
return {
|
|
1229
|
-
environment: client["env"].name,
|
|
1230
|
-
categories: Array.from(categoryMap.entries()).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count),
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
// Handle persona/workflow scope
|
|
1234
|
-
if (args.persona_id) {
|
|
1235
|
-
const personaId = String(args.persona_id);
|
|
1236
|
-
const personas = await client.getPersonasForTenant();
|
|
1237
|
-
const persona = personas.find((p) => p.id === personaId);
|
|
1238
|
-
if (!persona)
|
|
1239
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1240
|
-
if (!persona.workflow_id) {
|
|
1241
|
-
return {
|
|
1242
|
-
environment: client["env"].name,
|
|
1243
|
-
persona_id: personaId,
|
|
1244
|
-
persona_name: persona.name,
|
|
1245
|
-
error: "AI Employee has no workflow",
|
|
1246
|
-
actions: [],
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
const actionIds = await client.listActionsFromWorkflow(persona.workflow_id);
|
|
1250
|
-
const allActions = await client.listActions();
|
|
1251
|
-
const actionIdSet = new Set(actionIds);
|
|
1252
|
-
const actions = allActions.filter(a => actionIdSet.has(a.id));
|
|
1253
|
-
return {
|
|
1254
|
-
environment: client["env"].name,
|
|
1255
|
-
persona_id: personaId,
|
|
1256
|
-
persona_name: persona.name,
|
|
1257
|
-
workflow_id: persona.workflow_id,
|
|
1258
|
-
count: actions.length,
|
|
1259
|
-
actions: actions.map((a) => ({
|
|
1260
|
-
id: a.id, name: a.name, description: a.description,
|
|
1261
|
-
category: a.category, inputs: a.inputs, outputs: a.outputs,
|
|
1262
|
-
})),
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1265
|
-
if (args.workflow_id) {
|
|
1266
|
-
const workflowId = String(args.workflow_id);
|
|
1267
|
-
const actionIds = await client.listActionsFromWorkflow(workflowId);
|
|
1268
|
-
const allActions = await client.listActions();
|
|
1269
|
-
const actionIdSet = new Set(actionIds);
|
|
1270
|
-
const actions = allActions.filter(a => actionIdSet.has(a.id));
|
|
1271
|
-
return {
|
|
1272
|
-
environment: client["env"].name,
|
|
1273
|
-
workflow_id: workflowId,
|
|
1274
|
-
count: actions.length,
|
|
1275
|
-
actions: actions.map((a) => ({
|
|
1276
|
-
id: a.id, name: a.name, description: a.description,
|
|
1277
|
-
category: a.category, inputs: a.inputs, outputs: a.outputs,
|
|
1278
|
-
})),
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
// Default: search all actions
|
|
1282
|
-
let actions = await client.listActions();
|
|
1283
|
-
if (args.query) {
|
|
1284
|
-
const q = String(args.query).toLowerCase();
|
|
1285
|
-
actions = actions.filter((a) => a.name?.toLowerCase().includes(q));
|
|
1286
|
-
}
|
|
1287
|
-
if (args.category) {
|
|
1288
|
-
const f = String(args.category).toLowerCase();
|
|
1289
|
-
actions = actions.filter((a) => a.category?.toLowerCase() === f);
|
|
1290
|
-
}
|
|
1291
|
-
if (typeof args.enabled === "boolean") {
|
|
1292
|
-
actions = actions.filter((a) => a.enabled === args.enabled);
|
|
1293
|
-
}
|
|
1294
|
-
const limit = typeof args.limit === "number" ? args.limit : 100;
|
|
1295
|
-
actions = actions.slice(0, limit);
|
|
1296
|
-
return {
|
|
1297
|
-
environment: client["env"].name,
|
|
1298
|
-
count: actions.length,
|
|
1299
|
-
actions: actions.map((a) => ({
|
|
1300
|
-
id: a.id, name: a.name, description: a.description,
|
|
1301
|
-
category: a.category, enabled: a.enabled, tags: a.tags,
|
|
1302
|
-
})),
|
|
1303
|
-
};
|
|
1304
|
-
},
|
|
1305
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1306
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1307
|
-
// Diagnostics & Comparison
|
|
1308
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1309
|
-
compare_ai_employees: async (args) => {
|
|
1310
|
-
const env1 = args.env_1 ?? getDefaultEnvName();
|
|
1311
|
-
const env2 = args.env_2 ?? env1;
|
|
1312
|
-
const id1 = String(args.persona_id_1);
|
|
1313
|
-
const id2 = String(args.persona_id_2);
|
|
1314
|
-
const client1 = createClient(env1);
|
|
1315
|
-
const client2 = env1 === env2 ? client1 : createClient(env2);
|
|
1316
|
-
const [p1, p2] = await Promise.all([
|
|
1317
|
-
client1.getPersonaById(id1),
|
|
1318
|
-
client2.getPersonaById(id2),
|
|
1319
|
-
]);
|
|
1320
|
-
if (!p1)
|
|
1321
|
-
throw new Error(`AI Employee not found: ${id1} in ${env1}`);
|
|
1322
|
-
if (!p2)
|
|
1323
|
-
throw new Error(`AI Employee not found: ${id2} in ${env2}`);
|
|
1324
|
-
const fp1 = fingerprintPersona(p1);
|
|
1325
|
-
const fp2 = fingerprintPersona(p2);
|
|
1326
|
-
const compareFields = ["name", "description", "status", "trigger_type", "access_level", "embedding_enabled", "template_id", "workflow_id"];
|
|
1327
|
-
const differences = [];
|
|
1328
|
-
for (const field of compareFields) {
|
|
1329
|
-
const val1 = p1[field] ?? p1[field === "template_id" ? "templateId" : field];
|
|
1330
|
-
const val2 = p2[field] ?? p2[field === "template_id" ? "templateId" : field];
|
|
1331
|
-
if (JSON.stringify(val1) !== JSON.stringify(val2)) {
|
|
1332
|
-
differences.push({ field, value_1: val1, value_2: val2 });
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
if (JSON.stringify(p1.proto_config ?? {}) !== JSON.stringify(p2.proto_config ?? {})) {
|
|
1336
|
-
differences.push({ field: "proto_config", value_1: "(differs)", value_2: "(differs)" });
|
|
1337
|
-
}
|
|
1338
|
-
if (JSON.stringify(p1.welcome_messages ?? {}) !== JSON.stringify(p2.welcome_messages ?? {})) {
|
|
1339
|
-
differences.push({ field: "welcome_messages", value_1: "(differs)", value_2: "(differs)" });
|
|
1340
|
-
}
|
|
1341
|
-
return {
|
|
1342
|
-
persona_1: { id: id1, env: env1, name: p1.name, fingerprint: fp1 },
|
|
1343
|
-
persona_2: { id: id2, env: env2, name: p2.name, fingerprint: fp2 },
|
|
1344
|
-
fingerprints_match: fp1 === fp2,
|
|
1345
|
-
difference_count: differences.length,
|
|
1346
|
-
differences,
|
|
1347
|
-
};
|
|
1348
|
-
},
|
|
1349
|
-
list_ai_employee_templates: async (args) => {
|
|
1350
|
-
const client = createClient(args.env);
|
|
1351
|
-
const personas = await client.getPersonasForTenant();
|
|
1352
|
-
const templateMap = new Map();
|
|
1353
|
-
for (const p of personas) {
|
|
1354
|
-
const templateId = p.template_id ?? p.templateId ?? "unknown";
|
|
1355
|
-
const existing = templateMap.get(templateId) ?? { count: 0, names: [] };
|
|
1356
|
-
existing.count++;
|
|
1357
|
-
if (p.name && existing.names.length < 3)
|
|
1358
|
-
existing.names.push(p.name);
|
|
1359
|
-
templateMap.set(templateId, existing);
|
|
1360
|
-
}
|
|
1361
|
-
const templates = Array.from(templateMap.entries())
|
|
1362
|
-
.map(([template_id, data]) => ({ template_id, usage_count: data.count, examples: data.names }))
|
|
1363
|
-
.sort((a, b) => b.usage_count - a.usage_count);
|
|
1364
|
-
return {
|
|
1365
|
-
environment: client["env"].name,
|
|
1366
|
-
total_ai_employees: personas.length,
|
|
1367
|
-
template_count: templates.length,
|
|
1368
|
-
templates,
|
|
1369
|
-
};
|
|
1370
|
-
},
|
|
1371
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1372
|
-
// CONSOLIDATED SYNC HANDLERS
|
|
1373
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1374
|
-
sync: async (args) => {
|
|
1375
|
-
const targetEnv = String(args.target_env);
|
|
1376
|
-
const sourceEnv = args.source_env ? String(args.source_env) : getDefaultEnvName();
|
|
1377
|
-
const dryRun = args.dry_run === true;
|
|
1378
|
-
const includeStatus = args.include_status === true;
|
|
1379
|
-
const scope = args.scope === "all" ? "all" : "one";
|
|
1380
|
-
const identifier = args.identifier ? String(args.identifier) : undefined;
|
|
1381
|
-
// Sync all tagged personas
|
|
1382
|
-
if (scope === "all" || !identifier) {
|
|
1383
|
-
const sdk = getSyncSDK();
|
|
1384
|
-
if (sdk) {
|
|
1385
|
-
try {
|
|
1386
|
-
const result = await sdk.runSync();
|
|
1387
|
-
return { success: true, mode: "config", ...result };
|
|
1388
|
-
}
|
|
1389
|
-
finally {
|
|
1390
|
-
sdk.close();
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
// Config-less mode
|
|
1394
|
-
try {
|
|
1395
|
-
const result = await directSyncAll({ targetEnv, dryRun });
|
|
1396
|
-
return { success: true, mode: "tags", ...result };
|
|
1397
|
-
}
|
|
1398
|
-
catch (e) {
|
|
1399
|
-
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
// Sync single persona
|
|
1403
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
|
1404
|
-
const behavior = resolveSyncBehavior({
|
|
1405
|
-
personaName: isUUID ? undefined : identifier,
|
|
1406
|
-
targetEnv,
|
|
1407
|
-
overrides: {
|
|
1408
|
-
dry_run: dryRun ? true : undefined,
|
|
1409
|
-
sync_status: includeStatus ? true : undefined,
|
|
1410
|
-
},
|
|
1411
|
-
});
|
|
1412
|
-
try {
|
|
1413
|
-
const result = isUUID
|
|
1414
|
-
? await directSyncPersonaById({
|
|
1415
|
-
personaId: identifier,
|
|
1416
|
-
sourceEnv,
|
|
1417
|
-
targetEnv,
|
|
1418
|
-
dryRun: behavior.dry_run,
|
|
1419
|
-
syncStatus: behavior.sync_status,
|
|
1420
|
-
})
|
|
1421
|
-
: await directSyncPersona({
|
|
1422
|
-
name: identifier,
|
|
1423
|
-
sourceEnv,
|
|
1424
|
-
targetEnv,
|
|
1425
|
-
dryRun: behavior.dry_run,
|
|
1426
|
-
syncStatus: behavior.sync_status,
|
|
1427
|
-
});
|
|
1428
|
-
return { ...result, resolved_behavior: behavior };
|
|
1429
|
-
}
|
|
1430
|
-
catch (e) {
|
|
1431
|
-
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
1432
|
-
}
|
|
1433
|
-
},
|
|
1434
|
-
sync_info: async (args) => {
|
|
1435
|
-
const client = args.env ? createClient(args.env) : undefined;
|
|
1436
|
-
// Check if persona is synced
|
|
1437
|
-
if (args.persona_id) {
|
|
1438
|
-
if (!client)
|
|
1439
|
-
throw new Error("env required when checking persona sync status");
|
|
1440
|
-
const personaId = String(args.persona_id);
|
|
1441
|
-
const personas = await client.getPersonasForTenant();
|
|
1442
|
-
const persona = personas.find((p) => p.id === personaId);
|
|
1443
|
-
if (!persona)
|
|
1444
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1445
|
-
const meta = client.getSyncMetadata(persona);
|
|
1446
|
-
return {
|
|
1447
|
-
environment: client["env"].name,
|
|
1448
|
-
persona_id: personaId,
|
|
1449
|
-
persona_name: persona.name,
|
|
1450
|
-
is_synced: !!meta,
|
|
1451
|
-
sync_metadata: meta,
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
// Check by persona name
|
|
1455
|
-
if (args.persona_name) {
|
|
1456
|
-
const sdk = getSyncSDK();
|
|
1457
|
-
if (!sdk)
|
|
1458
|
-
return { error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG." };
|
|
1459
|
-
try {
|
|
1460
|
-
const persona = await sdk.getMasterPersonaByName(String(args.persona_name));
|
|
1461
|
-
if (!persona)
|
|
1462
|
-
return { error: `Persona not found: ${args.persona_name}` };
|
|
1463
|
-
return await sdk.getPersonaSyncStatus(persona.id);
|
|
1464
|
-
}
|
|
1465
|
-
finally {
|
|
1466
|
-
sdk.close();
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
// List all synced personas
|
|
1470
|
-
if (args.list_synced === true) {
|
|
1471
|
-
if (!client)
|
|
1472
|
-
throw new Error("env required when listing synced personas");
|
|
1473
|
-
const personas = await client.getPersonasForTenant();
|
|
1474
|
-
const masterEnvFilter = args.master_env ? String(args.master_env).toLowerCase() : undefined;
|
|
1475
|
-
const synced = [];
|
|
1476
|
-
for (const p of personas) {
|
|
1477
|
-
const meta = client.getSyncMetadata(p);
|
|
1478
|
-
if (meta) {
|
|
1479
|
-
if (masterEnvFilter && meta.master_env.toLowerCase() !== masterEnvFilter)
|
|
1480
|
-
continue;
|
|
1481
|
-
synced.push({ persona_id: p.id, persona_name: p.name, sync_metadata: meta });
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
return { environment: client["env"].name, count: synced.length, synced_personas: synced };
|
|
1485
|
-
}
|
|
1486
|
-
// Default: return overall sync config/status
|
|
1487
|
-
const sdk = getSyncSDK();
|
|
1488
|
-
const options = args.include_options === true ? loadSyncOptions() : undefined;
|
|
1489
|
-
if (!sdk) {
|
|
1490
|
-
return {
|
|
1491
|
-
configured: false,
|
|
1492
|
-
error: "No sync config found. Set EMA_AGENT_SYNC_CONFIG.",
|
|
1493
|
-
options,
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
try {
|
|
1497
|
-
const master = sdk.getMasterEnvironment();
|
|
1498
|
-
const envs = sdk.getEnvironments();
|
|
1499
|
-
const personas = await sdk.listMasterPersonas();
|
|
1500
|
-
return {
|
|
1501
|
-
configured: true,
|
|
1502
|
-
master_environment: { name: master.name, url: master.baseUrl },
|
|
1503
|
-
target_environments: envs.filter((e) => !e.isMaster).map((e) => ({ name: e.name, url: e.baseUrl })),
|
|
1504
|
-
total_personas: personas.length,
|
|
1505
|
-
options,
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
finally {
|
|
1509
|
-
sdk.close();
|
|
1510
|
-
}
|
|
1511
|
-
},
|
|
1512
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1513
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1514
|
-
// Auto Builder Knowledge Handlers
|
|
1515
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1516
|
-
list_auto_builder_agents: async (args) => {
|
|
1517
|
-
const category = args.category;
|
|
1518
|
-
const agents = category ? getAgentsByCategory(category) : AGENT_CATALOG;
|
|
1519
|
-
return {
|
|
1520
|
-
count: agents.length,
|
|
1521
|
-
category: category ?? "all",
|
|
1522
|
-
agents: agents.map(a => ({
|
|
1523
|
-
action_name: a.actionName,
|
|
1524
|
-
display_name: a.displayName,
|
|
1525
|
-
category: a.category,
|
|
1526
|
-
description: a.description,
|
|
1527
|
-
when_to_use: a.whenToUse,
|
|
1528
|
-
inputs: a.inputs.map(i => i.name),
|
|
1529
|
-
outputs: a.outputs.map(o => o.name),
|
|
1530
|
-
has_critical_rules: !!a.criticalRules?.length,
|
|
1531
|
-
})),
|
|
1532
|
-
};
|
|
1533
|
-
},
|
|
1534
|
-
get_auto_builder_agent: async (args) => {
|
|
1535
|
-
const actionName = String(args.action_name);
|
|
1536
|
-
const agent = getAgentByName(actionName);
|
|
1537
|
-
if (!agent) {
|
|
1538
|
-
const available = AGENT_CATALOG.slice(0, 15).map(a => a.actionName);
|
|
1539
|
-
return {
|
|
1540
|
-
error: `Agent not found: ${actionName}`,
|
|
1541
|
-
available_examples: available,
|
|
1542
|
-
hint: "Use list_auto_builder_agents to see all available agents",
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
return {
|
|
1546
|
-
action_name: agent.actionName,
|
|
1547
|
-
display_name: agent.displayName,
|
|
1548
|
-
category: agent.category,
|
|
1549
|
-
description: agent.description,
|
|
1550
|
-
inputs: agent.inputs,
|
|
1551
|
-
outputs: agent.outputs,
|
|
1552
|
-
critical_rules: agent.criticalRules ?? [],
|
|
1553
|
-
when_to_use: agent.whenToUse,
|
|
1554
|
-
when_not_to_use: agent.whenNotToUse,
|
|
1555
|
-
example: agent.example,
|
|
1556
|
-
};
|
|
1557
|
-
},
|
|
1558
|
-
suggest_agents_for_use_case: async (args) => {
|
|
1559
|
-
const useCase = String(args.use_case);
|
|
1560
|
-
const suggestions = suggestAgentsForUseCase(useCase);
|
|
1561
|
-
return {
|
|
1562
|
-
use_case: useCase,
|
|
1563
|
-
suggested_agent_count: suggestions.length,
|
|
1564
|
-
suggested_agents: suggestions.map(a => ({
|
|
1565
|
-
action_name: a.actionName,
|
|
1566
|
-
display_name: a.displayName,
|
|
1567
|
-
category: a.category,
|
|
1568
|
-
why: a.whenToUse,
|
|
1569
|
-
inputs: a.inputs.map(i => `${i.name} (${i.type})`),
|
|
1570
|
-
outputs: a.outputs.map(o => `${o.name} (${o.type})`),
|
|
1571
|
-
})),
|
|
1572
|
-
suggested_flow: suggestions.map(a => a.actionName).join(" → "),
|
|
1573
|
-
next_steps: [
|
|
1574
|
-
"Use get_auto_builder_agent to get detailed info on each agent",
|
|
1575
|
-
"Use get_workflow_pattern for a complete template if a pattern matches",
|
|
1576
|
-
"Use get_qualifying_questions to ensure you have all required information",
|
|
1577
|
-
],
|
|
1578
|
-
};
|
|
1579
|
-
},
|
|
1580
|
-
get_workflow_pattern: async (args) => {
|
|
1581
|
-
const patternName = String(args.pattern_name);
|
|
1582
|
-
const pattern = WORKFLOW_PATTERNS.find(p => p.name === patternName);
|
|
1583
|
-
if (!pattern) {
|
|
1584
|
-
return {
|
|
1585
|
-
error: `Pattern not found: ${patternName}`,
|
|
1586
|
-
available: WORKFLOW_PATTERNS.map(p => ({ name: p.name, description: p.description })),
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1589
|
-
return {
|
|
1590
|
-
name: pattern.name,
|
|
1591
|
-
persona_type: pattern.personaType,
|
|
1592
|
-
description: pattern.description,
|
|
1593
|
-
use_case: pattern.useCase,
|
|
1594
|
-
nodes: pattern.nodes,
|
|
1595
|
-
connections: pattern.connections,
|
|
1596
|
-
anti_patterns: pattern.antiPatterns ?? [],
|
|
1597
|
-
implementation_notes: [
|
|
1598
|
-
"Replace * with actual category/handler names",
|
|
1599
|
-
"All paths must lead to WORKFLOW_OUTPUT",
|
|
1600
|
-
"Include Fallback category for categorizers",
|
|
1601
|
-
"Check type compatibility for all connections",
|
|
1602
|
-
],
|
|
1603
|
-
};
|
|
1604
|
-
},
|
|
1605
|
-
list_workflow_patterns: async (args) => {
|
|
1606
|
-
const personaType = args.persona_type;
|
|
1607
|
-
const patterns = personaType
|
|
1608
|
-
? WORKFLOW_PATTERNS.filter(p => p.personaType === personaType)
|
|
1609
|
-
: WORKFLOW_PATTERNS;
|
|
1610
|
-
return {
|
|
1611
|
-
count: patterns.length,
|
|
1612
|
-
persona_type_filter: personaType ?? "all",
|
|
1613
|
-
patterns: patterns.map(p => ({
|
|
1614
|
-
name: p.name,
|
|
1615
|
-
persona_type: p.personaType,
|
|
1616
|
-
description: p.description,
|
|
1617
|
-
use_case: p.useCase,
|
|
1618
|
-
node_count: p.nodes.length,
|
|
1619
|
-
})),
|
|
1620
|
-
};
|
|
1621
|
-
},
|
|
1622
|
-
check_type_compatibility: async (args) => {
|
|
1623
|
-
const sourceType = String(args.source_type);
|
|
1624
|
-
const targetType = String(args.target_type);
|
|
1625
|
-
const compat = checkTypeCompatibility(sourceType, targetType);
|
|
1626
|
-
if (!compat) {
|
|
1627
|
-
return {
|
|
1628
|
-
source_type: sourceType,
|
|
1629
|
-
target_type: targetType,
|
|
1630
|
-
compatible: false,
|
|
1631
|
-
note: "No explicit compatibility rule found - likely incompatible",
|
|
1632
|
-
recommendation: "Use an intermediate node to convert types, or check if target accepts WELL_KNOWN_TYPE_ANY",
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
return {
|
|
1636
|
-
source_type: sourceType,
|
|
1637
|
-
target_type: targetType,
|
|
1638
|
-
compatible: compat.compatible,
|
|
1639
|
-
note: compat.note,
|
|
1640
|
-
recommendation: compat.compatible
|
|
1641
|
-
? "These types are compatible for direct connection"
|
|
1642
|
-
: `Incompatible. ${compat.note || "Use an intermediate node to convert types."}`,
|
|
1643
|
-
};
|
|
1644
|
-
},
|
|
1645
|
-
get_widget_reference: async (args) => {
|
|
1646
|
-
const personaType = String(args.persona_type);
|
|
1647
|
-
const widgets = getWidgetsForPersonaType(personaType);
|
|
1648
|
-
const projectType = PROJECT_TYPES[personaType];
|
|
1649
|
-
return {
|
|
1650
|
-
persona_type: personaType,
|
|
1651
|
-
project_type: projectType,
|
|
1652
|
-
widget_count: widgets.length,
|
|
1653
|
-
widgets: widgets.map(w => ({
|
|
1654
|
-
id: w.id,
|
|
1655
|
-
name: w.name,
|
|
1656
|
-
description: w.description,
|
|
1657
|
-
fields: w.fields,
|
|
1658
|
-
})),
|
|
1659
|
-
note: `Project type ${projectType} is used in proto_config for ${personaType} AI Employees`,
|
|
1660
|
-
};
|
|
1661
|
-
},
|
|
1662
|
-
get_qualifying_questions: async (args) => {
|
|
1663
|
-
const category = args.category;
|
|
1664
|
-
const requiredOnly = args.required_only === true;
|
|
1665
|
-
let questions = category
|
|
1666
|
-
? getQualifyingQuestionsByCategory(category)
|
|
1667
|
-
: QUALIFYING_QUESTIONS;
|
|
1668
|
-
if (requiredOnly) {
|
|
1669
|
-
questions = questions.filter(q => q.required);
|
|
1670
|
-
}
|
|
1671
|
-
const grouped = questions.reduce((acc, q) => {
|
|
1672
|
-
if (!acc[q.category])
|
|
1673
|
-
acc[q.category] = [];
|
|
1674
|
-
acc[q.category].push({ question: q.question, why_it_matters: q.whyItMatters, required: q.required });
|
|
1675
|
-
return acc;
|
|
1676
|
-
}, {});
|
|
1677
|
-
return {
|
|
1678
|
-
total_questions: questions.length,
|
|
1679
|
-
categories: Object.keys(grouped),
|
|
1680
|
-
questions_by_category: grouped,
|
|
1681
|
-
minimum_required: [
|
|
1682
|
-
"AI Type (Voice/Chat/Dashboard)",
|
|
1683
|
-
"2-3 intent categories + Fallback",
|
|
1684
|
-
"1 primary data source or action",
|
|
1685
|
-
"Success output format",
|
|
1686
|
-
],
|
|
1687
|
-
questioning_rounds: {
|
|
1688
|
-
round_1: "Core Context: AI type, trigger, main intents, data sources",
|
|
1689
|
-
round_2: "Workflow Details: Actions, validations, outputs, approvals",
|
|
1690
|
-
round_3: "Voice/Chat Specifics: Welcome message, hangup conditions (if applicable)",
|
|
1691
|
-
},
|
|
1692
|
-
};
|
|
1693
|
-
},
|
|
1694
|
-
get_voice_persona_template: async () => {
|
|
1695
|
-
// Use generated fallback from proto definitions
|
|
1696
|
-
// In production, prefer API templates via client.getPersonaTemplates()
|
|
1697
|
-
const template = getTemplateFallback("voice");
|
|
1698
|
-
return {
|
|
1699
|
-
template: template || VOICE_TEMPLATE_FALLBACK,
|
|
1700
|
-
field_docs: getTemplateFieldDocs("voice"),
|
|
1701
|
-
required_fields: ["conversationSettings.welcomeMessage", "conversationSettings.identityAndPurpose", "conversationSettings.takeActionInstructions", "conversationSettings.hangupInstructions"],
|
|
1702
|
-
optional_fields: ["conversationSettings.transferCallInstructions", "conversationSettings.speechCharacteristics", "conversationSettings.systemPrompt", "conversationSettings.formFillingInstructions", "conversationSettings.waitMessage"],
|
|
1703
|
-
project_type: PROJECT_TYPES.voice,
|
|
1704
|
-
widget_ids: {
|
|
1705
|
-
voiceSettings: 38,
|
|
1706
|
-
conversationSettings: 39,
|
|
1707
|
-
vadSettings: 43,
|
|
1708
|
-
dataStorageSettings: 42,
|
|
1709
|
-
},
|
|
1710
|
-
_source: "generated_fallback",
|
|
1711
|
-
_note: "This template is auto-generated from proto definitions. For live templates, use client.getPersonaTemplates().",
|
|
1712
|
-
};
|
|
1713
|
-
},
|
|
1714
|
-
validate_workflow_prompt: async (args) => {
|
|
1715
|
-
const prompt = String(args.prompt);
|
|
1716
|
-
const result = validateWorkflowPrompt(prompt);
|
|
1717
|
-
return {
|
|
1718
|
-
valid: result.valid,
|
|
1719
|
-
issue_count: result.issues.length,
|
|
1720
|
-
warning_count: result.warnings.length,
|
|
1721
|
-
issues: result.issues,
|
|
1722
|
-
warnings: result.warnings,
|
|
1723
|
-
recommendations: result.issues.length > 0 ? [
|
|
1724
|
-
"Add Fallback category to all categorizers",
|
|
1725
|
-
"Ensure HITL nodes have both success and failure paths",
|
|
1726
|
-
"Map all response nodes to WORKFLOW_OUTPUT",
|
|
1727
|
-
"Specify persona type (Voice AI, Chat AI, Dashboard AI)",
|
|
1728
|
-
"Check type compatibility for all connections",
|
|
1729
|
-
] : ["Prompt structure looks valid - verify type compatibility after generation"],
|
|
1730
|
-
};
|
|
1731
|
-
},
|
|
1732
|
-
get_auto_builder_guidance: async (args) => {
|
|
1733
|
-
const topic = String(args.topic);
|
|
1734
|
-
const guidance = GUIDANCE_TOPICS[topic];
|
|
1735
|
-
if (!guidance) {
|
|
1736
|
-
return {
|
|
1737
|
-
error: `Topic not found: ${topic}`,
|
|
1738
|
-
available_topics: Object.keys(GUIDANCE_TOPICS).map(k => ({
|
|
1739
|
-
topic: k,
|
|
1740
|
-
title: GUIDANCE_TOPICS[k].title,
|
|
1741
|
-
})),
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
return guidance;
|
|
1745
|
-
},
|
|
1746
|
-
get_platform_concept: async (args) => {
|
|
1747
|
-
const term = String(args.term);
|
|
1748
|
-
const concept = getConceptByTerm(term);
|
|
1749
|
-
if (!concept) {
|
|
1750
|
-
return {
|
|
1751
|
-
error: `Concept not found: ${term}`,
|
|
1752
|
-
available_concepts: PLATFORM_CONCEPTS.map(c => c.term),
|
|
1753
|
-
hint: "Try searching for aliases like 'Persona' (AI Employee) or 'Action' (Agent)",
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
return {
|
|
1757
|
-
term: concept.term,
|
|
1758
|
-
definition: concept.definition,
|
|
1759
|
-
aliases: concept.aliases ?? [],
|
|
1760
|
-
related_terms: concept.relatedTerms ?? [],
|
|
1761
|
-
examples: concept.examples ?? [],
|
|
1762
|
-
common_confusions: concept.commonConfusions,
|
|
1763
|
-
};
|
|
1764
|
-
},
|
|
1765
|
-
list_platform_concepts: async () => {
|
|
1766
|
-
return {
|
|
1767
|
-
count: PLATFORM_CONCEPTS.length,
|
|
1768
|
-
concepts: PLATFORM_CONCEPTS.map(c => ({
|
|
1769
|
-
term: c.term,
|
|
1770
|
-
definition: c.definition,
|
|
1771
|
-
aliases: c.aliases ?? [],
|
|
1772
|
-
})),
|
|
1773
|
-
key_relationships: [
|
|
1774
|
-
"AI Employee CONTAINS Workflow (processing logic) + Persona (conversational behavior)",
|
|
1775
|
-
"Workflow is made up of Agents/Actions connected by Edges",
|
|
1776
|
-
"Agents use Connectors to interact with external systems",
|
|
1777
|
-
"HITL = Human-in-the-Loop approval/verification step",
|
|
1778
|
-
],
|
|
1779
|
-
};
|
|
1780
|
-
},
|
|
1781
|
-
get_common_mistakes: async () => {
|
|
1782
|
-
return {
|
|
1783
|
-
count: COMMON_MISTAKES.length,
|
|
1784
|
-
mistakes: COMMON_MISTAKES,
|
|
1785
|
-
top_3_critical: [
|
|
1786
|
-
COMMON_MISTAKES.find(m => m.mistake.includes("Fallback")),
|
|
1787
|
-
COMMON_MISTAKES.find(m => m.mistake.includes("HITL")),
|
|
1788
|
-
COMMON_MISTAKES.find(m => m.mistake.includes("duplicate")),
|
|
1789
|
-
].filter(Boolean),
|
|
1790
|
-
};
|
|
1791
|
-
},
|
|
1792
|
-
get_debug_checklist: async () => {
|
|
1793
|
-
return {
|
|
1794
|
-
total_steps: DEBUG_CHECKLIST.length,
|
|
1795
|
-
checklist: DEBUG_CHECKLIST,
|
|
1796
|
-
quick_checks: [
|
|
1797
|
-
"Is the AI Employee status 'active' or 'ready'?",
|
|
1798
|
-
"Does the categorizer have all category edges including Fallback?",
|
|
1799
|
-
"Do all paths lead to WORKFLOW_OUTPUT?",
|
|
1800
|
-
"Are all connections type-compatible?",
|
|
1801
|
-
],
|
|
1802
|
-
};
|
|
1803
|
-
},
|
|
1804
|
-
get_workflow_execution_model: async () => {
|
|
1805
|
-
return {
|
|
1806
|
-
...WORKFLOW_EXECUTION_MODEL,
|
|
1807
|
-
summary: "Each user message triggers a NEW workflow execution. Use chat_conversation to detect previous actions and avoid duplicates.",
|
|
1808
|
-
};
|
|
1809
|
-
},
|
|
1810
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1811
|
-
// Workflow Review & Audit Handlers
|
|
1812
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1813
|
-
analyze_workflow: async (args) => {
|
|
1814
|
-
// DEPRECATED: MCP no longer pre-analyzes workflows
|
|
1815
|
-
// LLM should use ema://rules/* for analysis
|
|
1816
|
-
const client = createClient(args.env);
|
|
1817
|
-
const personaId = String(args.persona_id);
|
|
1818
|
-
const persona = await client.getPersonaById(personaId);
|
|
1819
|
-
if (!persona)
|
|
1820
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1821
|
-
const nodes = persona.workflow_def ? parseWorkflowDef(persona.workflow_def) : [];
|
|
1822
|
-
const connections = persona.workflow_def ? validateWorkflowConnections(persona.workflow_def) : [];
|
|
1823
|
-
return {
|
|
1824
|
-
environment: client["env"].name,
|
|
1825
|
-
persona_id: personaId,
|
|
1826
|
-
persona_name: persona.name,
|
|
1827
|
-
status: "DEPRECATED - use LLM analysis",
|
|
1828
|
-
node_count: nodes.length,
|
|
1829
|
-
workflow_def: persona.workflow_def,
|
|
1830
|
-
connections: connections.map(c => ({
|
|
1831
|
-
edge: c.edge_id,
|
|
1832
|
-
source_type: c.source_type,
|
|
1833
|
-
target_type: c.target_type,
|
|
1834
|
-
compatible: c.compatible,
|
|
1835
|
-
})),
|
|
1836
|
-
_deprecation_notice: {
|
|
1837
|
-
message: "analyze_workflow is deprecated. MCP does not pre-compute issues.",
|
|
1838
|
-
new_workflow: [
|
|
1839
|
-
"1. Fetch rules: ema://rules/anti-patterns",
|
|
1840
|
-
"2. Apply rules to find issues (LLM does this, not MCP)",
|
|
1841
|
-
"3. Report your findings",
|
|
1842
|
-
],
|
|
1843
|
-
},
|
|
1844
|
-
};
|
|
1845
|
-
},
|
|
1846
|
-
detect_workflow_issues: async (args) => {
|
|
1847
|
-
// DEPRECATED: MCP no longer detects workflow issues
|
|
1848
|
-
// LLM should use ema://rules/* for analysis
|
|
1849
|
-
const workflowDef = args.workflow_def;
|
|
1850
|
-
if (!workflowDef || typeof workflowDef !== "object") {
|
|
1851
|
-
return {
|
|
1852
|
-
error: "Invalid workflow_def - must be an object",
|
|
1853
|
-
hint: "Get workflow_def from get_ai_employee_full(persona_id).ai_employee.workflow_def",
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
const nodes = parseWorkflowDef(workflowDef);
|
|
1857
|
-
return {
|
|
1858
|
-
status: "DEPRECATED - use LLM analysis",
|
|
1859
|
-
node_count: nodes.length,
|
|
1860
|
-
nodes: nodes.map(n => ({ id: n.id, action: n.action_name })),
|
|
1861
|
-
_deprecation_notice: {
|
|
1862
|
-
message: "detect_workflow_issues is deprecated. MCP does not pre-compute issues.",
|
|
1863
|
-
new_workflow: [
|
|
1864
|
-
"1. Fetch rules: ema://rules/anti-patterns",
|
|
1865
|
-
"2. Apply rules to this workflow (LLM does this)",
|
|
1866
|
-
"3. Report issues YOU find",
|
|
1867
|
-
],
|
|
1868
|
-
},
|
|
1869
|
-
};
|
|
1870
|
-
},
|
|
1871
|
-
validate_workflow_connections: async (args) => {
|
|
1872
|
-
const workflowDef = args.workflow_def;
|
|
1873
|
-
if (!workflowDef || typeof workflowDef !== "object") {
|
|
1874
|
-
return {
|
|
1875
|
-
error: "Invalid workflow_def - must be an object",
|
|
1876
|
-
hint: "Get workflow_def from get_ai_employee_full(persona_id).ai_employee.workflow_def",
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
const validations = validateWorkflowConnections(workflowDef);
|
|
1880
|
-
const compatible = validations.filter(v => v.compatible);
|
|
1881
|
-
const incompatible = validations.filter(v => !v.compatible);
|
|
1882
|
-
return {
|
|
1883
|
-
total_edges: validations.length,
|
|
1884
|
-
compatible_count: compatible.length,
|
|
1885
|
-
incompatible_count: incompatible.length,
|
|
1886
|
-
all_valid: incompatible.length === 0,
|
|
1887
|
-
validations: validations.map(v => ({
|
|
1888
|
-
edge: v.edge_id,
|
|
1889
|
-
source_type: v.source_type,
|
|
1890
|
-
target_type: v.target_type,
|
|
1891
|
-
compatible: v.compatible,
|
|
1892
|
-
note: v.note,
|
|
1893
|
-
})),
|
|
1894
|
-
incompatible_edges: incompatible.map(v => ({
|
|
1895
|
-
edge: v.edge_id,
|
|
1896
|
-
source_type: v.source_type,
|
|
1897
|
-
target_type: v.target_type,
|
|
1898
|
-
note: v.note,
|
|
1899
|
-
fix_hint: v.source_type === "WELL_KNOWN_TYPE_CHAT_CONVERSATION" && v.target_type === "WELL_KNOWN_TYPE_TEXT_WITH_SOURCES"
|
|
1900
|
-
? "Insert conversation_to_search_query between source and target"
|
|
1901
|
-
: "Use named_inputs (accepts ANY type) or add intermediate conversion node",
|
|
1902
|
-
})),
|
|
1903
|
-
};
|
|
1904
|
-
},
|
|
1905
|
-
suggest_workflow_fixes: async (_args) => {
|
|
1906
|
-
// DEPRECATED: MCP no longer suggests fixes
|
|
1907
|
-
// LLM should reason about fixes using ema://rules/*
|
|
1908
|
-
return {
|
|
1909
|
-
status: "DEPRECATED - use LLM reasoning",
|
|
1910
|
-
_deprecation_notice: {
|
|
1911
|
-
message: "suggest_workflow_fixes is deprecated. LLM should propose fixes.",
|
|
1912
|
-
new_workflow: [
|
|
1913
|
-
"1. Get workflow: workflow(mode='get', persona_id='...')",
|
|
1914
|
-
"2. Analyze with ema://rules/anti-patterns",
|
|
1915
|
-
"3. LLM modifies workflow_def to fix issues",
|
|
1916
|
-
"4. Deploy: workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
1917
|
-
],
|
|
1918
|
-
},
|
|
1919
|
-
};
|
|
1920
|
-
},
|
|
1921
|
-
compare_workflow_versions: async (args) => {
|
|
1922
|
-
// DEPRECATED: MCP no longer pre-analyzes for comparison
|
|
1923
|
-
// LLM should compare workflows directly
|
|
1924
|
-
const client = createClient(args.env);
|
|
1925
|
-
const idBefore = String(args.persona_id_before);
|
|
1926
|
-
const idAfter = String(args.persona_id_after);
|
|
1927
|
-
const [personaBefore, personaAfter] = await Promise.all([
|
|
1928
|
-
client.getPersonaById(idBefore),
|
|
1929
|
-
client.getPersonaById(idAfter),
|
|
1930
|
-
]);
|
|
1931
|
-
if (!personaBefore)
|
|
1932
|
-
throw new Error(`AI Employee not found (before): ${idBefore}`);
|
|
1933
|
-
if (!personaAfter)
|
|
1934
|
-
throw new Error(`AI Employee not found (after): ${idAfter}`);
|
|
1935
|
-
const nodesBefore = personaBefore.workflow_def ? parseWorkflowDef(personaBefore.workflow_def) : [];
|
|
1936
|
-
const nodesAfter = personaAfter.workflow_def ? parseWorkflowDef(personaAfter.workflow_def) : [];
|
|
1937
|
-
// Compare fingerprints
|
|
1938
|
-
const fpBefore = personaBefore.workflow_def ? fingerprintPersona(personaBefore) : null;
|
|
1939
|
-
const fpAfter = personaAfter.workflow_def ? fingerprintPersona(personaAfter) : null;
|
|
1940
|
-
return {
|
|
1941
|
-
environment: client["env"].name,
|
|
1942
|
-
status: "DEPRECATED - LLM should compare",
|
|
1943
|
-
before: {
|
|
1944
|
-
persona_id: idBefore,
|
|
1945
|
-
name: personaBefore.name,
|
|
1946
|
-
fingerprint: fpBefore,
|
|
1947
|
-
has_workflow: !!personaBefore.workflow_def,
|
|
1948
|
-
node_count: nodesBefore.length,
|
|
1949
|
-
workflow_def: personaBefore.workflow_def,
|
|
1950
|
-
},
|
|
1951
|
-
after: {
|
|
1952
|
-
persona_id: idAfter,
|
|
1953
|
-
name: personaAfter.name,
|
|
1954
|
-
fingerprint: fpAfter,
|
|
1955
|
-
has_workflow: !!personaAfter.workflow_def,
|
|
1956
|
-
node_count: nodesAfter.length,
|
|
1957
|
-
workflow_def: personaAfter.workflow_def,
|
|
1958
|
-
},
|
|
1959
|
-
comparison: {
|
|
1960
|
-
fingerprints_match: fpBefore === fpAfter,
|
|
1961
|
-
node_count_change: nodesAfter.length - nodesBefore.length,
|
|
1962
|
-
},
|
|
1963
|
-
_deprecation_notice: {
|
|
1964
|
-
message: "compare_workflow_versions is deprecated. LLM should compare workflows directly.",
|
|
1965
|
-
guidance: "Compare the workflow_def objects returned above. Use ema://rules/anti-patterns to check each.",
|
|
1966
|
-
},
|
|
1967
|
-
};
|
|
1968
|
-
},
|
|
1969
|
-
get_workflow_metrics: async (args) => {
|
|
1970
|
-
const client = createClient(args.env);
|
|
1971
|
-
const personaId = String(args.persona_id);
|
|
1972
|
-
const persona = await client.getPersonaById(personaId);
|
|
1973
|
-
if (!persona)
|
|
1974
|
-
throw new Error(`AI Employee not found: ${personaId}`);
|
|
1975
|
-
if (!persona.workflow_def) {
|
|
1976
|
-
return {
|
|
1977
|
-
environment: client["env"].name,
|
|
1978
|
-
persona_id: personaId,
|
|
1979
|
-
persona_name: persona.name,
|
|
1980
|
-
error: "AI Employee has no workflow_def",
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
|
-
const nodes = parseWorkflowDef(persona.workflow_def);
|
|
1984
|
-
const connections = validateWorkflowConnections(persona.workflow_def);
|
|
1985
|
-
// Calculate basic metrics
|
|
1986
|
-
const totalEdges = nodes.reduce((sum, n) => sum + (n.incoming_edges?.length ?? 0), 0);
|
|
1987
|
-
const avgEdgesPerNode = nodes.length > 0
|
|
1988
|
-
? (totalEdges / nodes.length).toFixed(2)
|
|
1989
|
-
: 0;
|
|
1990
|
-
// Check for categorizers and HITL
|
|
1991
|
-
const categorizerCount = nodes.filter(n => n.action_name?.includes("categorizer")).length;
|
|
1992
|
-
const hitlCount = nodes.filter(n => n.action_name?.includes("hitl") || n.id?.includes("hitl")).length;
|
|
1993
|
-
const hasTrigger = nodes.some(n => n.action_name === "trigger" || n.id === "trigger");
|
|
1994
|
-
return {
|
|
1995
|
-
environment: client["env"].name,
|
|
1996
|
-
persona_id: personaId,
|
|
1997
|
-
persona_name: persona.name,
|
|
1998
|
-
structure: {
|
|
1999
|
-
total_nodes: nodes.length,
|
|
2000
|
-
total_edges: totalEdges,
|
|
2001
|
-
has_trigger: hasTrigger,
|
|
2002
|
-
connection_count: connections.length,
|
|
2003
|
-
},
|
|
2004
|
-
routing: {
|
|
2005
|
-
categorizers_count: categorizerCount,
|
|
2006
|
-
hitl_nodes_count: hitlCount,
|
|
2007
|
-
has_parallel_branches: categorizerCount > 0,
|
|
2008
|
-
},
|
|
2009
|
-
complexity: {
|
|
2010
|
-
avg_edges_per_node: avgEdgesPerNode,
|
|
2011
|
-
complexity_rating: nodes.length <= 5 ? "simple"
|
|
2012
|
-
: nodes.length <= 15 ? "moderate"
|
|
2013
|
-
: "complex",
|
|
2014
|
-
},
|
|
2015
|
-
_note: "Use ema://rules/anti-patterns for quality analysis. MCP no longer pre-computes issues.",
|
|
2016
|
-
};
|
|
2017
|
-
},
|
|
2018
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2019
|
-
// Workflow Compilation (Template-driven)
|
|
2020
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2021
|
-
compile_workflow: async (args) => {
|
|
2022
|
-
const name = String(args.name);
|
|
2023
|
-
const description = String(args.description);
|
|
2024
|
-
const personaType = String(args.persona_type);
|
|
2025
|
-
const rawNodes = args.nodes;
|
|
2026
|
-
const rawResultMappings = args.result_mappings;
|
|
2027
|
-
if (!["voice", "chat", "dashboard"].includes(personaType)) {
|
|
2028
|
-
throw new Error(`Invalid persona_type: ${personaType}. Must be one of: voice, chat, dashboard`);
|
|
2029
|
-
}
|
|
2030
|
-
if (!rawNodes || rawNodes.length === 0) {
|
|
2031
|
-
throw new Error("At least one node is required");
|
|
2032
|
-
}
|
|
2033
|
-
if (!rawResultMappings || rawResultMappings.length === 0) {
|
|
2034
|
-
throw new Error("At least one result_mapping is required to connect outputs to WORKFLOW_OUTPUT");
|
|
2035
|
-
}
|
|
2036
|
-
// Convert raw input spec to internal Node format
|
|
2037
|
-
const nodes = rawNodes.map((rawNode) => {
|
|
2038
|
-
const node = {
|
|
2039
|
-
id: rawNode.id,
|
|
2040
|
-
actionType: rawNode.action_type,
|
|
2041
|
-
displayName: rawNode.display_name,
|
|
2042
|
-
description: rawNode.description,
|
|
2043
|
-
disableHitl: rawNode.disable_hitl,
|
|
2044
|
-
};
|
|
2045
|
-
// Convert inputs
|
|
2046
|
-
if (rawNode.inputs) {
|
|
2047
|
-
node.inputs = {};
|
|
2048
|
-
for (const [key, rawBinding] of Object.entries(rawNode.inputs)) {
|
|
2049
|
-
const binding = {
|
|
2050
|
-
type: rawBinding.type,
|
|
2051
|
-
actionName: rawBinding.action_name,
|
|
2052
|
-
output: rawBinding.output,
|
|
2053
|
-
value: rawBinding.value,
|
|
2054
|
-
widgetName: rawBinding.widget_name,
|
|
2055
|
-
};
|
|
2056
|
-
node.inputs[key] = binding;
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
// Convert run_if condition
|
|
2060
|
-
if (rawNode.run_if) {
|
|
2061
|
-
node.runIf = {
|
|
2062
|
-
sourceAction: rawNode.run_if.source_action,
|
|
2063
|
-
sourceOutput: rawNode.run_if.source_output,
|
|
2064
|
-
operator: rawNode.run_if.operator,
|
|
2065
|
-
value: rawNode.run_if.value,
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
// Convert categories
|
|
2069
|
-
if (rawNode.categories) {
|
|
2070
|
-
node.categories = rawNode.categories.map((cat) => ({
|
|
2071
|
-
name: cat.name,
|
|
2072
|
-
description: cat.description,
|
|
2073
|
-
examples: cat.examples,
|
|
2074
|
-
}));
|
|
2075
|
-
}
|
|
2076
|
-
// Convert tools
|
|
2077
|
-
if (rawNode.tools) {
|
|
2078
|
-
node.tools = rawNode.tools.map((tool) => ({
|
|
2079
|
-
name: tool.name,
|
|
2080
|
-
namespace: tool.namespace,
|
|
2081
|
-
}));
|
|
2082
|
-
}
|
|
2083
|
-
return node;
|
|
2084
|
-
});
|
|
2085
|
-
// Convert result mappings
|
|
2086
|
-
const resultMappings = rawResultMappings.map((rm) => ({
|
|
2087
|
-
nodeId: rm.node_id,
|
|
2088
|
-
output: rm.output,
|
|
2089
|
-
}));
|
|
2090
|
-
// Build the WorkflowSpec
|
|
2091
|
-
const spec = {
|
|
2092
|
-
name,
|
|
2093
|
-
description,
|
|
2094
|
-
personaType,
|
|
2095
|
-
nodes,
|
|
2096
|
-
resultMappings,
|
|
2097
|
-
};
|
|
2098
|
-
// Compile the workflow
|
|
2099
|
-
const result = compileWorkflow(spec);
|
|
2100
|
-
return {
|
|
2101
|
-
success: true,
|
|
2102
|
-
message: `Compiled workflow for ${personaType} AI Employee "${name}" with ${nodes.length} nodes`,
|
|
2103
|
-
workflow_def: result.workflow_def,
|
|
2104
|
-
proto_config: result.proto_config,
|
|
2105
|
-
nodes_compiled: nodes.map((n) => ({ id: n.id, action: n.actionType, display_name: n.displayName })),
|
|
2106
|
-
result_mappings: resultMappings,
|
|
2107
|
-
usage: 'Deploy with: workflow(mode="deploy", persona_id="<persona_id>", workflow_def=<workflow_def>, proto_config=<proto_config>)',
|
|
2108
|
-
_tip: 'Stale-state protection is strict: call workflow(mode="get") immediately before deploy and pass base_fingerprint from the response. For large payloads, use workflow_def_path.',
|
|
2109
|
-
};
|
|
2110
|
-
},
|
|
2111
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2112
|
-
// Data Source / Embedding Management
|
|
2113
|
-
// Delegates to extracted handler in ./handlers/data/index.js
|
|
2114
|
-
// TODO: Add support for 3rd party data sources (Google Drive, SharePoint, Confluence, etc.)
|
|
2115
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2116
|
-
upload_data_source: async (args) => {
|
|
2117
|
-
const client = createClient(args.env);
|
|
2118
|
-
const fs = await import("fs/promises");
|
|
2119
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2120
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "upload", path: String(args.file_path) } }, client, (path) => fs.readFile(path));
|
|
2121
|
-
},
|
|
2122
|
-
delete_data_source: async (args) => {
|
|
2123
|
-
const client = createClient(args.env);
|
|
2124
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2125
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "delete", file_id: String(args.file_id) } }, client);
|
|
2126
|
-
},
|
|
2127
|
-
list_data_sources: async (args) => {
|
|
2128
|
-
const client = createClient(args.env);
|
|
2129
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2130
|
-
return handleData({ persona_id: String(args.persona_id), method: "list" }, client);
|
|
2131
|
-
},
|
|
2132
|
-
get_embedding_status: async (args) => {
|
|
2133
|
-
const client = createClient(args.env);
|
|
2134
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2135
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "embedding" } }, client);
|
|
2136
|
-
},
|
|
2137
|
-
toggle_embedding: async (args) => {
|
|
2138
|
-
const client = createClient(args.env);
|
|
2139
|
-
const { handleData } = await import("./handlers/data/index.js");
|
|
2140
|
-
return handleData({ persona_id: String(args.persona_id), data: { method: "embedding", enabled: Boolean(args.enabled) } }, client);
|
|
2141
|
-
},
|
|
2142
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2143
|
-
// Demo Data Management Handlers
|
|
2144
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2145
|
-
consolidate_demo_data: async (args) => {
|
|
2146
|
-
return handleConsolidateDemoData(args);
|
|
2147
|
-
},
|
|
2148
|
-
generate_demo_document: async (args) => {
|
|
2149
|
-
return handleGenerateDemoDocument(args);
|
|
2150
|
-
},
|
|
2151
|
-
validate_demo_document: async (args) => {
|
|
2152
|
-
return handleValidateDemoDocument(args);
|
|
2153
|
-
},
|
|
2154
|
-
get_demo_data_template: async (args) => {
|
|
2155
|
-
return handleGetDemoDataTemplate(args);
|
|
2156
|
-
},
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
// Tool Definitions
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
//
|
|
49
|
+
// V2 TOOLS (7 tools) - LLM-optimized minimal interface
|
|
50
|
+
// - env, persona, catalog, workflow, sync, toolkit_feedback, debug
|
|
51
|
+
// - Defined in: ./tools.ts
|
|
52
|
+
//
|
|
53
|
+
// NAMING CONVENTION:
|
|
54
|
+
// - Tool names are defined as BASE NAMES (e.g., "persona")
|
|
55
|
+
// - MCP clients prefix with "mcp_{server}_" (e.g., "mcp_ema_persona")
|
|
56
|
+
//
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Generate all available tools
|
|
60
|
+
*
|
|
61
|
+
* V2: 7 tools (persona, catalog, workflow, sync, env, toolkit_feedback, debug) - LLM-optimized
|
|
62
|
+
*
|
|
63
|
+
* Why V2:
|
|
64
|
+
* - Minimal tool count optimizes LLM tool selection
|
|
65
|
+
* - Data operations under persona (always persona-scoped)
|
|
66
|
+
* - Catalog consolidates all reference data (actions, templates, etc.)
|
|
67
|
+
* - Clear separation: entity (persona), reference (catalog), operation (sync, workflow)
|
|
68
|
+
*/
|
|
69
|
+
function generateAllTools() {
|
|
70
|
+
const envNames = getAvailableEnvironments().map(e => e.name);
|
|
71
|
+
const defaultEnv = getDefaultEnvName();
|
|
72
|
+
return generateTools(envNames, defaultEnv);
|
|
73
|
+
}
|
|
74
|
+
// Generate tools (called once at module load)
|
|
75
|
+
const TOOLS = generateAllTools();
|
|
76
|
+
const toolHandlers = {
|
|
2157
77
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2158
|
-
//
|
|
78
|
+
// V2 TOOLS — LLM-optimized interface
|
|
2159
79
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2160
80
|
env: async () => {
|
|
2161
81
|
return handleEnv({}, () => getAvailableEnvironments().map(e => ({
|
|
@@ -2164,224 +84,7 @@ const toolHandlers = {
|
|
|
2164
84
|
})), { name: TOOLKIT_NAME, version: TOOLKIT_VERSION, commit: TOOLKIT_COMMIT });
|
|
2165
85
|
},
|
|
2166
86
|
persona: async (args) => {
|
|
2167
|
-
|
|
2168
|
-
const client = createClient(targetEnv);
|
|
2169
|
-
// Build version context for version management modes
|
|
2170
|
-
const versionContext = {
|
|
2171
|
-
// Store versions in the caller's workspace, not the toolkit install dir.
|
|
2172
|
-
// This must be writable under typical MCP usage (e.g. Cursor workspace).
|
|
2173
|
-
workspaceRoot: process.cwd(),
|
|
2174
|
-
environment: targetEnv,
|
|
2175
|
-
tenant_id: targetEnv, // Use env name as tenant identifier
|
|
2176
|
-
};
|
|
2177
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2178
|
-
// V2 Parameter Transformation
|
|
2179
|
-
// Convert v2 structure to v1 mode-based structure for handler compatibility
|
|
2180
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2181
|
-
const transformedArgs = { ...args };
|
|
2182
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2183
|
-
// Explicit Method API (takes priority over flag-based args)
|
|
2184
|
-
// persona(method="create|get|list|update|delete|sanitize|analyze|compare|clone")
|
|
2185
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2186
|
-
if (args.method) {
|
|
2187
|
-
const method = String(args.method);
|
|
2188
|
-
// Map explicit methods to internal modes
|
|
2189
|
-
const methodToMode = {
|
|
2190
|
-
create: "create",
|
|
2191
|
-
get: "get",
|
|
2192
|
-
list: "list",
|
|
2193
|
-
update: "update",
|
|
2194
|
-
delete: "delete",
|
|
2195
|
-
sanitize: "sanitize",
|
|
2196
|
-
analyze: "analyze",
|
|
2197
|
-
compare: "compare",
|
|
2198
|
-
schema: "schema", // get persona input schema (dashboard columns)
|
|
2199
|
-
clone: "create", // clone is create with from
|
|
2200
|
-
snapshot: "version_create",
|
|
2201
|
-
history: "version_list",
|
|
2202
|
-
restore: "version_restore",
|
|
2203
|
-
};
|
|
2204
|
-
const mode = methodToMode[method];
|
|
2205
|
-
if (!mode) {
|
|
2206
|
-
return { error: `Unknown method: ${method}`, valid_methods: Object.keys(methodToMode) };
|
|
2207
|
-
}
|
|
2208
|
-
transformedArgs.mode = mode;
|
|
2209
|
-
delete transformedArgs.method;
|
|
2210
|
-
// Handle action composition if present
|
|
2211
|
-
if (args.actions && Array.isArray(args.actions)) {
|
|
2212
|
-
// Store actions for post-processing after main operation
|
|
2213
|
-
transformedArgs._actions = args.actions;
|
|
2214
|
-
delete transformedArgs.actions;
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2218
|
-
// Flag-based API (legacy compatibility)
|
|
2219
|
-
// ONLY applies when explicit `method` parameter was NOT provided
|
|
2220
|
-
// When method is explicit, flags like sanitize=true are passed through
|
|
2221
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2222
|
-
// Guard: Skip flag-based API if explicit method was provided
|
|
2223
|
-
const skipFlagBasedApi = !!args.method;
|
|
2224
|
-
// Create: persona(create={name, type, from, input})
|
|
2225
|
-
if (!skipFlagBasedApi && args.create && typeof args.create === "object") {
|
|
2226
|
-
const create = args.create;
|
|
2227
|
-
transformedArgs.mode = "create";
|
|
2228
|
-
transformedArgs.name = create.name;
|
|
2229
|
-
transformedArgs.type = create.type;
|
|
2230
|
-
transformedArgs.from = create.from;
|
|
2231
|
-
transformedArgs.input = create.input;
|
|
2232
|
-
transformedArgs.preview = create.preview ?? true;
|
|
2233
|
-
delete transformedArgs.create;
|
|
2234
|
-
}
|
|
2235
|
-
// Update: persona(id, update={config, input, workflow_spec})
|
|
2236
|
-
else if (!skipFlagBasedApi && args.update && typeof args.update === "object") {
|
|
2237
|
-
const update = args.update;
|
|
2238
|
-
transformedArgs.mode = "update";
|
|
2239
|
-
transformedArgs.proto_config = update.config;
|
|
2240
|
-
transformedArgs.input = update.input;
|
|
2241
|
-
transformedArgs.workflow_spec = update.workflow_spec;
|
|
2242
|
-
transformedArgs.preview = update.preview ?? false; // Default to deploy, not preview
|
|
2243
|
-
delete transformedArgs.update;
|
|
2244
|
-
}
|
|
2245
|
-
// Delete: persona(id, delete=true)
|
|
2246
|
-
else if (!skipFlagBasedApi && args.delete === true) {
|
|
2247
|
-
transformedArgs.mode = "delete";
|
|
2248
|
-
delete transformedArgs.delete;
|
|
2249
|
-
}
|
|
2250
|
-
// Analyze: persona(id, analyze=true, fix=true)
|
|
2251
|
-
else if (!skipFlagBasedApi && args.analyze === true) {
|
|
2252
|
-
transformedArgs.mode = "analyze";
|
|
2253
|
-
// fix is already in args
|
|
2254
|
-
delete transformedArgs.analyze;
|
|
2255
|
-
}
|
|
2256
|
-
// Sanitize: persona(id, sanitize=true)
|
|
2257
|
-
// ONLY converts to mode="sanitize" when method is NOT explicit
|
|
2258
|
-
// When method="create" + sanitize=true, sanitize is a FLAG for post-creation sanitization
|
|
2259
|
-
else if (!skipFlagBasedApi && args.sanitize === true) {
|
|
2260
|
-
transformedArgs.mode = "sanitize";
|
|
2261
|
-
delete transformedArgs.sanitize;
|
|
2262
|
-
}
|
|
2263
|
-
// Snapshot: persona(id, snapshot="message")
|
|
2264
|
-
else if (!skipFlagBasedApi && typeof args.snapshot === "string") {
|
|
2265
|
-
transformedArgs.mode = "version_create";
|
|
2266
|
-
transformedArgs.message = args.snapshot;
|
|
2267
|
-
delete transformedArgs.snapshot;
|
|
2268
|
-
}
|
|
2269
|
-
// History: persona(id, history=true)
|
|
2270
|
-
else if (!skipFlagBasedApi && args.history === true) {
|
|
2271
|
-
transformedArgs.mode = "version_list";
|
|
2272
|
-
delete transformedArgs.history;
|
|
2273
|
-
}
|
|
2274
|
-
// Restore: persona(id, restore="v3")
|
|
2275
|
-
else if (!skipFlagBasedApi && typeof args.restore === "string") {
|
|
2276
|
-
transformedArgs.mode = "version_restore";
|
|
2277
|
-
transformedArgs.version = args.restore;
|
|
2278
|
-
delete transformedArgs.restore;
|
|
2279
|
-
}
|
|
2280
|
-
// Compare: persona(id, compare="other-id")
|
|
2281
|
-
else if (!skipFlagBasedApi && typeof args.compare === "string") {
|
|
2282
|
-
transformedArgs.mode = "compare";
|
|
2283
|
-
transformedArgs.compare_to = args.compare;
|
|
2284
|
-
delete transformedArgs.compare;
|
|
2285
|
-
}
|
|
2286
|
-
// Data operations: persona(id, data={method:"...", ...})
|
|
2287
|
-
// Supports both explicit method format and legacy flag format
|
|
2288
|
-
else if (args.data && typeof args.data === "object") {
|
|
2289
|
-
const data = args.data;
|
|
2290
|
-
const personaId = args.id;
|
|
2291
|
-
const fs = await import("fs/promises");
|
|
2292
|
-
// EXPLICIT METHOD FORMAT (preferred): data={method:"list/stats/upload/copy/replicate/delete/search/refresh/regenerate/replace"}
|
|
2293
|
-
if (typeof data.method === "string") {
|
|
2294
|
-
// Import the new data handler - pass data object as part of args
|
|
2295
|
-
const { handleData: handleDataNew } = await import("./handlers/data/index.js");
|
|
2296
|
-
return handleDataNew({ persona_id: personaId, env: args.env, data }, client);
|
|
2297
|
-
}
|
|
2298
|
-
// LEGACY FLAG FORMAT (backwards compatibility) - use extracted handler
|
|
2299
|
-
const { handleData: handleDataExtracted } = await import("./handlers/data/index.js");
|
|
2300
|
-
const readFileFn = (path) => fs.readFile(path);
|
|
2301
|
-
if (data.list === true) {
|
|
2302
|
-
return handleDataExtracted({ method: "list", persona_id: personaId, env: args.env }, client, readFileFn);
|
|
2303
|
-
}
|
|
2304
|
-
if (typeof data.upload === "string") {
|
|
2305
|
-
return handleDataExtracted({ method: "upload", persona_id: personaId, data: { path: data.upload }, env: args.env }, client, readFileFn);
|
|
2306
|
-
}
|
|
2307
|
-
if (typeof data.delete === "string") {
|
|
2308
|
-
return handleDataExtracted({ method: "delete", persona_id: personaId, data: { file_id: data.delete }, env: args.env }, client, readFileFn);
|
|
2309
|
-
}
|
|
2310
|
-
if (typeof data.generate === "string" || data.template) {
|
|
2311
|
-
return handleDataExtracted({
|
|
2312
|
-
method: "generate",
|
|
2313
|
-
persona_id: personaId,
|
|
2314
|
-
data: {
|
|
2315
|
-
input: data.generate,
|
|
2316
|
-
from: data.template,
|
|
2317
|
-
count: data.count,
|
|
2318
|
-
},
|
|
2319
|
-
env: args.env
|
|
2320
|
-
}, client, readFileFn);
|
|
2321
|
-
}
|
|
2322
|
-
if (typeof data.embed === "boolean") {
|
|
2323
|
-
return handleDataExtracted({ method: "embedding", persona_id: personaId, data: { enabled: data.embed }, env: args.env }, client, readFileFn);
|
|
2324
|
-
}
|
|
2325
|
-
if (typeof data.search === "string") {
|
|
2326
|
-
return handleKnowledge({ mode: "search", persona_id: personaId, query: data.search, env: args.env }, client, (path) => fs.readFile(path));
|
|
2327
|
-
}
|
|
2328
|
-
return {
|
|
2329
|
-
error: "Unknown data operation",
|
|
2330
|
-
hint: "Use data={method:'list'} format (explicit method)",
|
|
2331
|
-
available_methods: ["list", "stats", "upload", "copy", "replicate", "delete", "search", "refresh", "regenerate", "replace"],
|
|
2332
|
-
legacy_flags: ["list", "upload", "delete", "generate", "embed", "search"],
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
// Get: persona(id) with no mutation flags
|
|
2336
|
-
else if (args.id && !transformedArgs.mode) {
|
|
2337
|
-
transformedArgs.mode = "get";
|
|
2338
|
-
}
|
|
2339
|
-
// List: persona() or persona(type, status, query) with no id
|
|
2340
|
-
else if (!args.id && !transformedArgs.mode) {
|
|
2341
|
-
transformedArgs.mode = "list";
|
|
2342
|
-
}
|
|
2343
|
-
// Templates are tenant-specific - don't use hardcoded IDs
|
|
2344
|
-
// The handler will use dynamic template lookup from API
|
|
2345
|
-
const result = await handlePersona(transformedArgs, client, () => undefined, // Dynamic lookup in handler
|
|
2346
|
-
(env) => createClient(env), versionContext);
|
|
2347
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2348
|
-
// Action Composition Post-Processing
|
|
2349
|
-
// Execute actions array after main operation completes
|
|
2350
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2351
|
-
const actions = transformedArgs._actions;
|
|
2352
|
-
if (actions && actions.length > 0) {
|
|
2353
|
-
// Get the target persona ID from result
|
|
2354
|
-
const resultObj = result;
|
|
2355
|
-
const targetId = resultObj.id ??
|
|
2356
|
-
resultObj.persona_id;
|
|
2357
|
-
const sourceId = args.from;
|
|
2358
|
-
if (!targetId) {
|
|
2359
|
-
return {
|
|
2360
|
-
...(typeof result === "object" && result !== null ? result : {}),
|
|
2361
|
-
_actions_error: "No persona ID available for action execution",
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
// Import and execute actions
|
|
2365
|
-
const actionExecutor = await import("./handlers/action-executor.js");
|
|
2366
|
-
const context = {
|
|
2367
|
-
source: sourceId,
|
|
2368
|
-
target: targetId,
|
|
2369
|
-
env: targetEnv,
|
|
2370
|
-
originalArgs: args,
|
|
2371
|
-
};
|
|
2372
|
-
const actionsResult = await actionExecutor.executeActions(actions, context, client);
|
|
2373
|
-
return {
|
|
2374
|
-
...(typeof result === "object" && result !== null ? result : {}),
|
|
2375
|
-
_actions: actionsResult,
|
|
2376
|
-
};
|
|
2377
|
-
}
|
|
2378
|
-
return result;
|
|
2379
|
-
},
|
|
2380
|
-
// Consolidated workflow handler (replaces legacy inline handler)
|
|
2381
|
-
workflow: async (args) => {
|
|
2382
|
-
const client = createClient(args.env);
|
|
2383
|
-
// Templates are tenant-specific - dynamic lookup in handler
|
|
2384
|
-
return handleWorkflow(args, client, () => undefined);
|
|
87
|
+
return handlePersonaAdapter(args, createClient, getDefaultEnvName);
|
|
2385
88
|
},
|
|
2386
89
|
action: async (args) => {
|
|
2387
90
|
const client = createClient(args.env);
|
|
@@ -2390,12 +93,6 @@ const toolHandlers = {
|
|
|
2390
93
|
template: async (args) => {
|
|
2391
94
|
return handleTemplate(args);
|
|
2392
95
|
},
|
|
2393
|
-
knowledge: async (args) => {
|
|
2394
|
-
const client = createClient(args.env);
|
|
2395
|
-
const fs = await import("fs/promises");
|
|
2396
|
-
return handleKnowledge(args, client, (path) => fs.readFile(path));
|
|
2397
|
-
},
|
|
2398
|
-
// v2: data is an alias for knowledge with simplified interface
|
|
2399
96
|
data: async (args) => {
|
|
2400
97
|
const client = createClient(args.env);
|
|
2401
98
|
const fs = await import("fs/promises");
|
|
@@ -2419,464 +116,30 @@ const toolHandlers = {
|
|
|
2419
116
|
client,
|
|
2420
117
|
});
|
|
2421
118
|
},
|
|
2422
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2423
|
-
// V2 TOOLS (4 tools: persona, catalog, sync, env)
|
|
2424
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2425
119
|
// catalog: Consolidated reference data (actions, templates, widgets, voices, patterns, concepts)
|
|
2426
120
|
catalog: async (args) => {
|
|
2427
121
|
const client = createClient(args.env);
|
|
2428
122
|
return handleCatalog(args, client);
|
|
2429
123
|
},
|
|
2430
|
-
// Note: 'sync' handler already exists above - keeping it for full functionality
|
|
2431
|
-
// Note: demo is handled via a consolidated adapter below
|
|
2432
124
|
// toolkit_feedback: Agent feedback collection and analysis
|
|
2433
125
|
toolkit_feedback: async (args) => {
|
|
2434
126
|
return handleFeedback(args);
|
|
2435
127
|
},
|
|
128
|
+
// V2 adapters — routing + transformation extracted to handler files
|
|
129
|
+
workflow: async (args) => {
|
|
130
|
+
return handleWorkflowAdapter(args, createClient, getDefaultEnvName);
|
|
131
|
+
},
|
|
132
|
+
sync: async (args) => {
|
|
133
|
+
return handleSyncAdapter(args, createClient, getDefaultEnvName, getSyncSDK);
|
|
134
|
+
},
|
|
135
|
+
demo: async (args) => {
|
|
136
|
+
return handleDemoAdapter(args, createClient);
|
|
137
|
+
},
|
|
138
|
+
debug: async (args) => {
|
|
139
|
+
return handleDebugAdapter(args, createClient, getDefaultEnvName);
|
|
140
|
+
},
|
|
2436
141
|
};
|
|
2437
142
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2438
|
-
// V2 Tool Adapters (contract ↔ implementation)
|
|
2439
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2440
|
-
//
|
|
2441
|
-
// The tool schemas in tools.ts are the public MCP contract.
|
|
2442
|
-
// This file still contains proven handlers that we reuse
|
|
2443
|
-
// (e.g. deploy_workflow, optimize_workflow, compile_workflow, etc.).
|
|
2444
|
-
//
|
|
2445
|
-
// These adapters ensure the V2 tool surface behaves as documented,
|
|
2446
|
-
// while preserving the existing internal implementations.
|
|
2447
|
-
const legacyWorkflowTool = toolHandlers.workflow;
|
|
2448
|
-
const legacyDeployWorkflow = toolHandlers.deploy_workflow;
|
|
2449
|
-
const legacyOptimizeWorkflow = toolHandlers.optimize_workflow;
|
|
2450
|
-
const legacyCompareWorkflowVersions = toolHandlers.compare_workflow_versions;
|
|
2451
|
-
const legacyCompileWorkflow = toolHandlers.compile_workflow;
|
|
2452
|
-
const legacyDetectWorkflowIssues = toolHandlers.detect_workflow_issues;
|
|
2453
|
-
const legacyValidateWorkflowConnections = toolHandlers.validate_workflow_connections;
|
|
2454
|
-
const legacySuggestWorkflowFixes = toolHandlers.suggest_workflow_fixes;
|
|
2455
|
-
const legacySyncRun = toolHandlers.sync;
|
|
2456
|
-
const legacySyncInfo = toolHandlers.sync_info;
|
|
2457
|
-
const legacyConsolidateDemoData = toolHandlers.consolidate_demo_data;
|
|
2458
|
-
const legacyGenerateDemoDocument = toolHandlers.generate_demo_document;
|
|
2459
|
-
const legacyValidateDemoDocument = toolHandlers.validate_demo_document;
|
|
2460
|
-
const legacyGetDemoDataTemplate = toolHandlers.get_demo_data_template;
|
|
2461
|
-
// Workflow tool: MCP provides data (get) and executes (deploy). LLM does all thinking.
|
|
2462
|
-
toolHandlers.workflow = async (args) => {
|
|
2463
|
-
const normalizedArgs = { ...(args ?? {}) };
|
|
2464
|
-
const personaId = normalizedArgs.persona_id ? String(normalizedArgs.persona_id) : undefined;
|
|
2465
|
-
let workflowDef = normalizedArgs.workflow_def;
|
|
2466
|
-
const workflowDefPath = normalizedArgs.workflow_def_path;
|
|
2467
|
-
const mode = normalizedArgs.mode ? String(normalizedArgs.mode) : undefined;
|
|
2468
|
-
const baseFingerprint = normalizedArgs.base_fingerprint;
|
|
2469
|
-
const force = normalizedArgs.force;
|
|
2470
|
-
// For deploy: resolve workflow_def from file when workflow_def_path is provided (large payloads)
|
|
2471
|
-
// Also handle the common agent mistake: passing {"workflow_def_path": "/path"} as workflow_def
|
|
2472
|
-
// (happens when the agent's tool schema doesn't expose workflow_def_path as a separate param)
|
|
2473
|
-
let effectivePath = workflowDefPath;
|
|
2474
|
-
if (mode === "deploy" && workflowDef && !effectivePath) {
|
|
2475
|
-
const keys = Object.keys(workflowDef);
|
|
2476
|
-
if (keys.length === 1 && keys[0] === "workflow_def_path" && typeof workflowDef.workflow_def_path === "string") {
|
|
2477
|
-
effectivePath = workflowDef.workflow_def_path;
|
|
2478
|
-
workflowDef = undefined;
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
if (mode === "deploy" && !workflowDef && effectivePath) {
|
|
2482
|
-
try {
|
|
2483
|
-
// Guardrails: reduce risk of accidental secret exfiltration
|
|
2484
|
-
const path = await import("path");
|
|
2485
|
-
if (!path.isAbsolute(effectivePath)) {
|
|
2486
|
-
return { error: "workflow_def_path must be an absolute path on the MCP server host", path: effectivePath };
|
|
2487
|
-
}
|
|
2488
|
-
if (!effectivePath.toLowerCase().endsWith(".json")) {
|
|
2489
|
-
return { error: "workflow_def_path must point to a .json file", path: effectivePath };
|
|
2490
|
-
}
|
|
2491
|
-
const fs = await import("fs/promises");
|
|
2492
|
-
const stat = await fs.stat(effectivePath);
|
|
2493
|
-
const MAX_WORKFLOW_DEF_BYTES = 1024 * 1024; // 1MB
|
|
2494
|
-
if (stat.size > MAX_WORKFLOW_DEF_BYTES) {
|
|
2495
|
-
return { error: `workflow_def_path file too large (${stat.size} bytes)`, max_bytes: MAX_WORKFLOW_DEF_BYTES, path: effectivePath };
|
|
2496
|
-
}
|
|
2497
|
-
const raw = await fs.readFile(effectivePath, "utf-8");
|
|
2498
|
-
const parsed = JSON.parse(raw);
|
|
2499
|
-
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2500
|
-
workflowDef = parsed;
|
|
2501
|
-
}
|
|
2502
|
-
else {
|
|
2503
|
-
return { error: "workflow_def_path must point to a JSON object", path: effectivePath };
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
catch (err) {
|
|
2507
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2508
|
-
return { error: `Failed to read workflow_def from path: ${msg}`, path: effectivePath };
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
// Route to handleWorkflow for get/deploy (the only public modes)
|
|
2512
|
-
const client = createClient(normalizedArgs.env);
|
|
2513
|
-
switch (mode) {
|
|
2514
|
-
case "get": {
|
|
2515
|
-
// Return workflow data for LLM to analyze/modify
|
|
2516
|
-
return handleWorkflow({
|
|
2517
|
-
mode: "get",
|
|
2518
|
-
persona_id: personaId,
|
|
2519
|
-
env: normalizedArgs.env,
|
|
2520
|
-
}, client, () => undefined);
|
|
2521
|
-
}
|
|
2522
|
-
case "validate": {
|
|
2523
|
-
// Static validation with path enumeration
|
|
2524
|
-
return handleWorkflow({
|
|
2525
|
-
mode: "validate",
|
|
2526
|
-
persona_id: personaId,
|
|
2527
|
-
workflow_def: normalizedArgs.workflow_def,
|
|
2528
|
-
workflow_spec: normalizedArgs.workflow_spec,
|
|
2529
|
-
validation_type: normalizedArgs.validation_type,
|
|
2530
|
-
max_paths: normalizedArgs.max_paths,
|
|
2531
|
-
timeout_ms: normalizedArgs.timeout_ms,
|
|
2532
|
-
env: normalizedArgs.env,
|
|
2533
|
-
}, client, () => undefined);
|
|
2534
|
-
}
|
|
2535
|
-
case "deploy": {
|
|
2536
|
-
if (!personaId) {
|
|
2537
|
-
return { error: 'persona_id is required for workflow(mode="deploy")' };
|
|
2538
|
-
}
|
|
2539
|
-
// Pre-deploy snapshot + stale-state protection (out-of-band changes)
|
|
2540
|
-
const targetEnv = normalizedArgs.env ?? getDefaultEnvName();
|
|
2541
|
-
let versionCreated;
|
|
2542
|
-
try {
|
|
2543
|
-
const personaBefore = await client.getPersonaById(personaId);
|
|
2544
|
-
if (!personaBefore) {
|
|
2545
|
-
return { error: `Persona not found: ${personaId}` };
|
|
2546
|
-
}
|
|
2547
|
-
const currentFp = fingerprintPersona(personaBefore);
|
|
2548
|
-
if (!force && !baseFingerprint) {
|
|
2549
|
-
return {
|
|
2550
|
-
error: "base_fingerprint is required for workflow deploy (stale-state protection)",
|
|
2551
|
-
persona_id: personaId,
|
|
2552
|
-
current_fingerprint: currentFp,
|
|
2553
|
-
hint: "Run workflow(mode='get', persona_id='...') immediately before deploying and pass fingerprint as base_fingerprint. Use force=true only for emergency overrides.",
|
|
2554
|
-
};
|
|
2555
|
-
}
|
|
2556
|
-
if (!force && baseFingerprint && baseFingerprint !== currentFp) {
|
|
2557
|
-
return {
|
|
2558
|
-
error: "Persona changed since you last fetched it (fingerprint mismatch)",
|
|
2559
|
-
persona_id: personaId,
|
|
2560
|
-
base_fingerprint: baseFingerprint,
|
|
2561
|
-
current_fingerprint: currentFp,
|
|
2562
|
-
hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. Use force=true only if you intend to overwrite out-of-band changes.",
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
const storage = createVersionStorage(process.cwd());
|
|
2566
|
-
const engine = createVersionPolicyEngine(storage);
|
|
2567
|
-
const snap = engine.forceCreateVersion(personaBefore, {
|
|
2568
|
-
environment: targetEnv,
|
|
2569
|
-
tenant_id: targetEnv,
|
|
2570
|
-
message: "Pre-deploy snapshot",
|
|
2571
|
-
created_by: "mcp-toolkit",
|
|
2572
|
-
});
|
|
2573
|
-
if (!snap.created || !snap.version) {
|
|
2574
|
-
if (!force) {
|
|
2575
|
-
return {
|
|
2576
|
-
error: "Failed to create pre-deploy snapshot (required before deploy)",
|
|
2577
|
-
persona_id: personaId,
|
|
2578
|
-
details: snap.reason,
|
|
2579
|
-
hint: "Fix snapshotting (workspace storage) or retry with force=true for emergency override.",
|
|
2580
|
-
};
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
else {
|
|
2584
|
-
versionCreated = { id: snap.version.id, version_name: snap.version.version_name };
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
catch {
|
|
2588
|
-
// Snapshotting is required unless force=true
|
|
2589
|
-
if (!force) {
|
|
2590
|
-
return {
|
|
2591
|
-
error: "Failed to create pre-deploy snapshot (required before deploy)",
|
|
2592
|
-
persona_id: personaId,
|
|
2593
|
-
hint: "Retry after fixing local workspace write access, or use force=true for emergency override.",
|
|
2594
|
-
};
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
// Route to handleWorkflow deploy
|
|
2598
|
-
const deployResult = await handleWorkflow({
|
|
2599
|
-
mode: "deploy",
|
|
2600
|
-
persona_id: personaId,
|
|
2601
|
-
workflow_def: workflowDef,
|
|
2602
|
-
proto_config: normalizedArgs.proto_config,
|
|
2603
|
-
env: normalizedArgs.env,
|
|
2604
|
-
force: force,
|
|
2605
|
-
strict_validation: normalizedArgs.strict_validation,
|
|
2606
|
-
}, client, () => undefined);
|
|
2607
|
-
// Add version info to result if created
|
|
2608
|
-
if (versionCreated && deployResult && typeof deployResult === "object") {
|
|
2609
|
-
deployResult.version_snapshot = versionCreated;
|
|
2610
|
-
}
|
|
2611
|
-
return deployResult;
|
|
2612
|
-
}
|
|
2613
|
-
// REMOVED modes - LLM does these
|
|
2614
|
-
case "analyze":
|
|
2615
|
-
case "compare":
|
|
2616
|
-
case "compile":
|
|
2617
|
-
case "optimize":
|
|
2618
|
-
case "generate": {
|
|
2619
|
-
return {
|
|
2620
|
-
error: `Mode "${mode}" removed - LLM does this thinking`,
|
|
2621
|
-
hint: "Use workflow(mode='get') to fetch data, then analyze/generate in your reasoning. Deploy with workflow(mode='deploy').",
|
|
2622
|
-
valid_modes: ["get", "validate", "deploy"],
|
|
2623
|
-
};
|
|
2624
|
-
}
|
|
2625
|
-
default: {
|
|
2626
|
-
// No mode or unknown mode - require explicit mode
|
|
2627
|
-
return {
|
|
2628
|
-
error: `Mode required. Valid modes: get, validate, deploy`,
|
|
2629
|
-
hint: "workflow(mode='get') returns data for LLM. workflow(mode='validate') validates specs. workflow(mode='deploy') executes LLM's workflow_def.",
|
|
2630
|
-
example: `workflow(mode="get", persona_id="...")`,
|
|
2631
|
-
};
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
};
|
|
2635
|
-
// Unify sync modes: run | status | config
|
|
2636
|
-
toolHandlers.sync = async (args) => {
|
|
2637
|
-
const normalizedArgs = { ...(args ?? {}) };
|
|
2638
|
-
// Tool definition uses "method" (preview/execute/status), but legacy callers use "mode" (run/status/config)
|
|
2639
|
-
const rawMethod = normalizedArgs.method ? String(normalizedArgs.method) : normalizedArgs.mode ? String(normalizedArgs.mode) : "run";
|
|
2640
|
-
// Map tool method values to internal mode values
|
|
2641
|
-
const mode = rawMethod === "preview" ? "run" : rawMethod === "execute" ? "run" : rawMethod;
|
|
2642
|
-
// "preview" implies dry_run
|
|
2643
|
-
if (rawMethod === "preview") {
|
|
2644
|
-
normalizedArgs.dry_run = true;
|
|
2645
|
-
}
|
|
2646
|
-
// Support both old and new arg names
|
|
2647
|
-
const target = (normalizedArgs.target ?? normalizedArgs.target_env);
|
|
2648
|
-
const source = (normalizedArgs.source ?? normalizedArgs.source_env);
|
|
2649
|
-
const id = normalizedArgs.id;
|
|
2650
|
-
const identifier = normalizedArgs.identifier; // deprecated alias
|
|
2651
|
-
const idOrIdentifier = id ?? identifier;
|
|
2652
|
-
if (mode === "config") {
|
|
2653
|
-
return legacySyncInfo({ include_options: true });
|
|
2654
|
-
}
|
|
2655
|
-
if (mode === "status") {
|
|
2656
|
-
const env = normalizedArgs.env;
|
|
2657
|
-
if (normalizedArgs.list_synced === true) {
|
|
2658
|
-
if (!env)
|
|
2659
|
-
throw new Error('env is required for sync(mode="status", list_synced=true)');
|
|
2660
|
-
return legacySyncInfo({ list_synced: true, master_env: normalizedArgs.master_env, env });
|
|
2661
|
-
}
|
|
2662
|
-
if (idOrIdentifier) {
|
|
2663
|
-
if (!env)
|
|
2664
|
-
throw new Error('env is required for sync(mode="status", id="...")');
|
|
2665
|
-
const identifierToResolve = String(idOrIdentifier);
|
|
2666
|
-
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifierToResolve);
|
|
2667
|
-
if (isUUID) {
|
|
2668
|
-
return legacySyncInfo({ persona_id: identifierToResolve, env });
|
|
2669
|
-
}
|
|
2670
|
-
// Name lookup: resolve to ID in env, then reuse persona_id path
|
|
2671
|
-
const client = createClient(env);
|
|
2672
|
-
const personas = await client.getPersonasForTenant();
|
|
2673
|
-
const match = personas.find((p) => p.name === identifierToResolve);
|
|
2674
|
-
if (!match)
|
|
2675
|
-
throw new Error(`AI Employee not found by name in ${env}: ${identifierToResolve}`);
|
|
2676
|
-
return legacySyncInfo({ persona_id: match.id, env });
|
|
2677
|
-
}
|
|
2678
|
-
// Default: overall sync status/config summary
|
|
2679
|
-
return legacySyncInfo({ include_options: normalizedArgs.include_options === true });
|
|
2680
|
-
}
|
|
2681
|
-
// mode === "run" (default)
|
|
2682
|
-
if (!target) {
|
|
2683
|
-
throw new Error('target (or target_env) is required for sync(mode="run")');
|
|
2684
|
-
}
|
|
2685
|
-
return legacySyncRun({
|
|
2686
|
-
identifier: idOrIdentifier,
|
|
2687
|
-
target_env: target,
|
|
2688
|
-
source_env: source,
|
|
2689
|
-
scope: normalizedArgs.scope,
|
|
2690
|
-
dry_run: normalizedArgs.dry_run,
|
|
2691
|
-
include_status: normalizedArgs.include_status,
|
|
2692
|
-
});
|
|
2693
|
-
};
|
|
2694
|
-
// Consolidated demo tool: kit | validate_kit | scenarios | consolidate | generate | validate | template
|
|
2695
|
-
toolHandlers.demo = async (args) => {
|
|
2696
|
-
const normalizedArgs = { ...(args ?? {}) };
|
|
2697
|
-
const mode = normalizedArgs.mode ? String(normalizedArgs.mode) : "template";
|
|
2698
|
-
// Deprecation warning - added to all responses
|
|
2699
|
-
const deprecationWarning = {
|
|
2700
|
-
_deprecation: {
|
|
2701
|
-
message: "The 'demo' tool is deprecated. Please use 'data' and 'persona' tools instead.",
|
|
2702
|
-
migration: {
|
|
2703
|
-
"demo(mode='kit')": "persona(from='demo-sales-sdr', include_data=true)",
|
|
2704
|
-
"demo(mode='generate')": "data(mode='generate', from='customer', count=5)",
|
|
2705
|
-
"demo(mode='scenarios')": "data(mode='templates')",
|
|
2706
|
-
"demo(mode='template')": "data(mode='templates', template='customer')",
|
|
2707
|
-
},
|
|
2708
|
-
},
|
|
2709
|
-
};
|
|
2710
|
-
switch (mode) {
|
|
2711
|
-
case "kit": {
|
|
2712
|
-
// Generate complete demo kit for a persona
|
|
2713
|
-
const personaId = String(normalizedArgs.persona_id ?? "");
|
|
2714
|
-
const scenarioId = String(normalizedArgs.scenario ?? "sales-sdr");
|
|
2715
|
-
if (!personaId) {
|
|
2716
|
-
throw new Error('demo(mode="kit") requires: persona_id');
|
|
2717
|
-
}
|
|
2718
|
-
// Import demo generator
|
|
2719
|
-
const { generateDemoKit, DEMO_SCENARIOS, generateDemoScriptMarkdown, validateDemoKit } = await import("./demo-generator.js");
|
|
2720
|
-
// Get scenario
|
|
2721
|
-
const scenario = DEMO_SCENARIOS[scenarioId];
|
|
2722
|
-
if (!scenario) {
|
|
2723
|
-
throw new Error(`Unknown scenario: ${scenarioId}. Available: ${Object.keys(DEMO_SCENARIOS).join(", ")}`);
|
|
2724
|
-
}
|
|
2725
|
-
// Get persona and workflow
|
|
2726
|
-
const client = await createClient(normalizedArgs.env);
|
|
2727
|
-
const persona = await client.getPersonaById(personaId);
|
|
2728
|
-
if (!persona) {
|
|
2729
|
-
throw new Error(`Persona not found: ${personaId}`);
|
|
2730
|
-
}
|
|
2731
|
-
const workflowDef = persona.workflow_def || {};
|
|
2732
|
-
const customQA = normalizedArgs.custom_qa;
|
|
2733
|
-
// Generate kit
|
|
2734
|
-
const kit = generateDemoKit(personaId, persona.name || personaId, workflowDef, scenario, customQA);
|
|
2735
|
-
// Generate markdown script
|
|
2736
|
-
const demoScript = generateDemoScriptMarkdown(kit);
|
|
2737
|
-
// Validate
|
|
2738
|
-
const validation = validateDemoKit(kit);
|
|
2739
|
-
return {
|
|
2740
|
-
success: true,
|
|
2741
|
-
persona_id: personaId,
|
|
2742
|
-
persona_name: persona.name,
|
|
2743
|
-
scenario: scenarioId,
|
|
2744
|
-
kit_summary: {
|
|
2745
|
-
kb_documents: kit.kb_documents.length,
|
|
2746
|
-
demo_questions: kit.demo_script.length,
|
|
2747
|
-
fixed_responses: kit.fixed_responses.length,
|
|
2748
|
-
validation_queries: kit.validation_queries.length,
|
|
2749
|
-
},
|
|
2750
|
-
validation,
|
|
2751
|
-
demo_script_preview: demoScript.slice(0, 2000) + (demoScript.length > 2000 ? "\n\n... (truncated)" : ""),
|
|
2752
|
-
kit,
|
|
2753
|
-
instructions: [
|
|
2754
|
-
"1. Upload KB documents to the persona's knowledge base",
|
|
2755
|
-
"2. Review the demo script and practice the questions",
|
|
2756
|
-
"3. Optionally apply fixed_responses for guaranteed fallbacks",
|
|
2757
|
-
"4. Run validation queries to verify demo readiness",
|
|
2758
|
-
"5. Conduct the demo with confidence!",
|
|
2759
|
-
],
|
|
2760
|
-
};
|
|
2761
|
-
}
|
|
2762
|
-
case "validate_kit": {
|
|
2763
|
-
// Validate a persona's demo readiness
|
|
2764
|
-
const personaId = String(normalizedArgs.persona_id ?? "");
|
|
2765
|
-
if (!personaId) {
|
|
2766
|
-
throw new Error('demo(mode="validate_kit") requires: persona_id');
|
|
2767
|
-
}
|
|
2768
|
-
const { analyzeWorkflowForDemo, DEMO_SCENARIOS } = await import("./demo-generator.js");
|
|
2769
|
-
const client = await createClient(normalizedArgs.env);
|
|
2770
|
-
const persona = await client.getPersonaById(personaId);
|
|
2771
|
-
if (!persona) {
|
|
2772
|
-
throw new Error(`Persona not found: ${personaId}`);
|
|
2773
|
-
}
|
|
2774
|
-
const analysis = analyzeWorkflowForDemo(persona.workflow_def || {});
|
|
2775
|
-
// Check data sources
|
|
2776
|
-
const dataSourcesResult = await client.listDataSourceFiles(personaId);
|
|
2777
|
-
const dataSources = dataSourcesResult.files || [];
|
|
2778
|
-
const hasKnowledgeBase = dataSources.length > 0;
|
|
2779
|
-
const issues = [];
|
|
2780
|
-
if (!hasKnowledgeBase) {
|
|
2781
|
-
issues.push("No knowledge base documents uploaded - RAG search will fail");
|
|
2782
|
-
}
|
|
2783
|
-
if (analysis.intents.length === 0) {
|
|
2784
|
-
issues.push("No categorizer intents detected - workflow may not route correctly");
|
|
2785
|
-
}
|
|
2786
|
-
if (!analysis.has_search) {
|
|
2787
|
-
issues.push("No search nodes detected - cannot retrieve KB data");
|
|
2788
|
-
}
|
|
2789
|
-
// Suggest best scenario
|
|
2790
|
-
let suggestedScenario = "sales-sdr";
|
|
2791
|
-
for (const [id, scenario] of Object.entries(DEMO_SCENARIOS)) {
|
|
2792
|
-
const intentOverlap = scenario.intents.filter(i => analysis.intents.some(ai => ai.toLowerCase().includes(i.name.toLowerCase()))).length;
|
|
2793
|
-
if (intentOverlap > 0) {
|
|
2794
|
-
suggestedScenario = id;
|
|
2795
|
-
break;
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
return {
|
|
2799
|
-
persona_id: personaId,
|
|
2800
|
-
persona_name: persona.name,
|
|
2801
|
-
ready: issues.length === 0,
|
|
2802
|
-
issues,
|
|
2803
|
-
workflow_analysis: analysis,
|
|
2804
|
-
knowledge_base: {
|
|
2805
|
-
has_documents: hasKnowledgeBase,
|
|
2806
|
-
document_count: dataSources.length,
|
|
2807
|
-
},
|
|
2808
|
-
suggested_scenario: suggestedScenario,
|
|
2809
|
-
next_steps: issues.length > 0
|
|
2810
|
-
? issues.map((issue, i) => `${i + 1}. Fix: ${issue}`)
|
|
2811
|
-
: [`Generate demo kit: demo(mode="kit", persona_id="${personaId}", scenario="${suggestedScenario}")`],
|
|
2812
|
-
};
|
|
2813
|
-
}
|
|
2814
|
-
case "scenarios": {
|
|
2815
|
-
// List available demo scenarios
|
|
2816
|
-
const { DEMO_SCENARIOS } = await import("./demo-generator.js");
|
|
2817
|
-
return {
|
|
2818
|
-
scenarios: Object.entries(DEMO_SCENARIOS).map(([id, scenario]) => ({
|
|
2819
|
-
id,
|
|
2820
|
-
name: scenario.name,
|
|
2821
|
-
description: scenario.description,
|
|
2822
|
-
persona_types: scenario.persona_types,
|
|
2823
|
-
tags: scenario.tags,
|
|
2824
|
-
intent_count: scenario.intents.length,
|
|
2825
|
-
qa_count: scenario.qa_pairs.length,
|
|
2826
|
-
entity_types: scenario.entities.map(e => e.type),
|
|
2827
|
-
})),
|
|
2828
|
-
usage: 'demo(mode="kit", persona_id="...", scenario="<scenario_id>")',
|
|
2829
|
-
};
|
|
2830
|
-
}
|
|
2831
|
-
case "consolidate": {
|
|
2832
|
-
const source = String(normalizedArgs.source ?? "");
|
|
2833
|
-
const output = String(normalizedArgs.output ?? "");
|
|
2834
|
-
const entity = String(normalizedArgs.entity ?? "");
|
|
2835
|
-
if (!source || !output || !entity) {
|
|
2836
|
-
throw new Error('demo(mode="consolidate") requires: source, output, entity');
|
|
2837
|
-
}
|
|
2838
|
-
return legacyConsolidateDemoData({
|
|
2839
|
-
source_dir: source,
|
|
2840
|
-
output_dir: output,
|
|
2841
|
-
entity_type: entity,
|
|
2842
|
-
primary_file: normalizedArgs.primary ?? `${entity}s.json`,
|
|
2843
|
-
joins: normalizedArgs.joins ?? [],
|
|
2844
|
-
tags: normalizedArgs.tags,
|
|
2845
|
-
});
|
|
2846
|
-
}
|
|
2847
|
-
case "generate": {
|
|
2848
|
-
const entity = String(normalizedArgs.entity ?? "");
|
|
2849
|
-
if (!entity)
|
|
2850
|
-
throw new Error('demo(mode="generate") requires: entity');
|
|
2851
|
-
return legacyGenerateDemoDocument({
|
|
2852
|
-
entity_type: entity,
|
|
2853
|
-
data: normalizedArgs.data ?? {},
|
|
2854
|
-
related_data: normalizedArgs.related ?? {},
|
|
2855
|
-
output_path: normalizedArgs.output,
|
|
2856
|
-
tags: normalizedArgs.tags,
|
|
2857
|
-
});
|
|
2858
|
-
}
|
|
2859
|
-
case "validate": {
|
|
2860
|
-
return legacyValidateDemoDocument({
|
|
2861
|
-
file_path: normalizedArgs.file,
|
|
2862
|
-
content: normalizedArgs.content,
|
|
2863
|
-
});
|
|
2864
|
-
}
|
|
2865
|
-
case "template": {
|
|
2866
|
-
const entity = String(normalizedArgs.entity ?? "");
|
|
2867
|
-
if (!entity)
|
|
2868
|
-
throw new Error('demo(mode="template") requires: entity');
|
|
2869
|
-
return legacyGetDemoDataTemplate({
|
|
2870
|
-
entity_type: entity,
|
|
2871
|
-
include_example: normalizedArgs.include_example,
|
|
2872
|
-
});
|
|
2873
|
-
}
|
|
2874
|
-
default:
|
|
2875
|
-
throw new Error(`Unknown demo mode: ${mode}`);
|
|
2876
|
-
}
|
|
2877
|
-
};
|
|
2878
|
-
// generateEntityDocument moved to handlers/demo/index.js
|
|
2879
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2880
143
|
// Helpers
|
|
2881
144
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2882
145
|
/**
|
|
@@ -2885,24 +148,8 @@ toolHandlers.demo = async (args) => {
|
|
|
2885
148
|
*/
|
|
2886
149
|
function determineOperation(toolName, args) {
|
|
2887
150
|
if (toolName === "persona") {
|
|
2888
|
-
if (args.
|
|
2889
|
-
return
|
|
2890
|
-
if (args.update)
|
|
2891
|
-
return "update";
|
|
2892
|
-
if (args.create)
|
|
2893
|
-
return "create";
|
|
2894
|
-
if (args.delete)
|
|
2895
|
-
return "delete";
|
|
2896
|
-
if (args.snapshot)
|
|
2897
|
-
return "snapshot";
|
|
2898
|
-
if (args.history)
|
|
2899
|
-
return "history";
|
|
2900
|
-
if (args.restore)
|
|
2901
|
-
return "restore";
|
|
2902
|
-
if (args.compare)
|
|
2903
|
-
return "compare";
|
|
2904
|
-
if (args.sanitize)
|
|
2905
|
-
return "sanitize";
|
|
151
|
+
if (args.method)
|
|
152
|
+
return String(args.method);
|
|
2906
153
|
if (args.data)
|
|
2907
154
|
return "data";
|
|
2908
155
|
if (args.id)
|
|
@@ -2910,24 +157,21 @@ function determineOperation(toolName, args) {
|
|
|
2910
157
|
return "list";
|
|
2911
158
|
}
|
|
2912
159
|
if (toolName === "catalog") {
|
|
2913
|
-
if (args.
|
|
2914
|
-
return
|
|
2915
|
-
if (args.for)
|
|
2916
|
-
return "recommend"; // V2 schema uses 'for' parameter
|
|
2917
|
-
if (args.query)
|
|
2918
|
-
return "search";
|
|
160
|
+
if (args.method)
|
|
161
|
+
return String(args.method);
|
|
2919
162
|
return "list";
|
|
2920
163
|
}
|
|
2921
164
|
if (toolName === "sync") {
|
|
2922
|
-
if (args.
|
|
2923
|
-
return
|
|
2924
|
-
if (args.status)
|
|
2925
|
-
return "status";
|
|
165
|
+
if (args.method)
|
|
166
|
+
return String(args.method);
|
|
2926
167
|
return "preview";
|
|
2927
168
|
}
|
|
2928
169
|
if (toolName === "toolkit_feedback") {
|
|
2929
170
|
return args.method ? String(args.method) : "submit";
|
|
2930
171
|
}
|
|
172
|
+
if (toolName === "debug") {
|
|
173
|
+
return args.method ? String(args.method) : "conversations";
|
|
174
|
+
}
|
|
2931
175
|
return toolName;
|
|
2932
176
|
}
|
|
2933
177
|
// ─────────────────────────────────────────────────────────────────────────────
|