@ema.co/mcp-toolkit 2026.1.25 → 2026.1.26-4
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/README.md +10 -2
- package/dist/mcp/handlers/action/index.js +3 -18
- package/dist/mcp/handlers/data/index.js +385 -41
- package/dist/mcp/handlers/data/templates.js +107 -0
- package/dist/mcp/handlers/deprecation.js +50 -0
- package/dist/mcp/handlers/env/index.js +8 -4
- package/dist/mcp/handlers/knowledge/index.js +44 -237
- package/dist/mcp/handlers/persona/create.js +47 -18
- package/dist/mcp/handlers/persona/index.js +14 -11
- package/dist/mcp/handlers/persona/update.js +4 -2
- package/dist/mcp/handlers/persona/version.js +234 -0
- package/dist/mcp/handlers/sync/index.js +3 -18
- package/dist/mcp/handlers/template/index.js +75 -10
- package/dist/mcp/handlers/workflow/analyze.js +171 -0
- package/dist/mcp/handlers/workflow/compare.js +70 -0
- package/dist/mcp/handlers/workflow/deploy.js +73 -0
- package/dist/mcp/handlers/workflow/generate.js +350 -0
- package/dist/mcp/handlers/workflow/index.js +294 -0
- package/dist/mcp/handlers/workflow/modify.js +456 -0
- package/dist/mcp/handlers/workflow/optimize.js +136 -0
- package/dist/mcp/handlers/workflow/types.js +4 -0
- package/dist/mcp/handlers/workflow/utils.js +30 -0
- package/dist/mcp/handlers-consolidated.js +73 -2696
- package/dist/mcp/prompts.js +83 -43
- package/dist/mcp/resources.js +382 -57
- package/dist/mcp/server.js +199 -391
- package/dist/mcp/{tools-v2.js → tools.js} +20 -54
- package/dist/mcp/workflow-operations.js +2 -2
- package/dist/sdk/client-adapter.js +267 -32
- package/dist/sdk/client.js +45 -16
- package/dist/sdk/ema-client.js +183 -0
- package/dist/sdk/generated/deprecated-actions.js +171 -0
- package/dist/sdk/generated/template-fallbacks.js +123 -0
- package/dist/sdk/guidance.js +65 -11
- package/dist/sdk/index.js +3 -1
- package/dist/sdk/knowledge.js +139 -86
- package/dist/sdk/workflow-intent.js +27 -0
- package/dist/sdk/workflow-transformer.js +0 -342
- package/docs/mcp-tools-guide.md +37 -45
- package/package.json +10 -4
- package/dist/mcp/handlers/persona/analyze.js +0 -275
- package/dist/mcp/handlers/persona/compare.js +0 -32
- package/dist/mcp/tools-consolidated.js +0 -875
- package/dist/mcp/tools-legacy.js +0 -736
- package/docs/CODEBASE-ANALYSIS-2026-01-23.md +0 -936
- package/docs/CODEBASE-ANALYSIS-PRIORITIZED.md +0 -774
- package/docs/api-contracts.md +0 -216
- package/docs/auto-builder-analysis.md +0 -271
- package/docs/blog/mcp-tool-design-lessons.md +0 -309
- package/docs/data-architecture.md +0 -166
- package/docs/demos/ap-invoice-generation.md +0 -347
- package/docs/demos/ap-invoice-processing.md +0 -271
- package/docs/ema-auto-builder-guide.html +0 -394
- package/docs/lessons-learned.md +0 -209
- package/docs/llm-native-workflow-design.md +0 -252
- package/docs/local-generation.md +0 -508
- package/docs/mcp-flow-diagram.md +0 -135
- package/docs/migration/action-composition-migration.md +0 -270
- package/docs/naming-conventions.md +0 -278
- package/docs/proposals/HANDOFF-tool-restructure.md +0 -526
- package/docs/proposals/action-composition.md +0 -490
- package/docs/proposals/explicit-method-restructure.md +0 -328
- package/docs/proposals/mcp-tool-restructure-2026-01.md +0 -366
- package/docs/proposals/self-contained-guidance.md +0 -427
- package/docs/proto-sdk-generation.md +0 -242
- package/docs/release-impact.md +0 -102
- package/docs/release-process.md +0 -157
- package/docs/staging.RULE.md +0 -142
- package/docs/test-persona-creation.md +0 -196
- package/docs/tool-consolidation-v2.md +0 -225
- package/docs/tool-response-standards.md +0 -256
- package/resources/demo-kits/README.md +0 -175
- package/resources/demo-kits/finance-ap/manifest.json +0 -150
- package/resources/demo-kits/tags.json +0 -91
- package/resources/docs/getting-started.md +0 -97
- package/resources/templates/auto-builder-rules.md +0 -224
- package/resources/templates/chat-ai/README.md +0 -119
- package/resources/templates/chat-ai/persona-config.json +0 -111
- package/resources/templates/dashboard-ai/README.md +0 -156
- package/resources/templates/dashboard-ai/persona-config.json +0 -180
- package/resources/templates/demo-scenarios/README.md +0 -63
- package/resources/templates/demo-scenarios/test-published-package.md +0 -116
- package/resources/templates/document-gen-ai/README.md +0 -132
- package/resources/templates/document-gen-ai/persona-config.json +0 -316
- package/resources/templates/voice-ai/README.md +0 -123
- package/resources/templates/voice-ai/persona-config.json +0 -74
- package/resources/templates/voice-ai/workflow-prompt.md +0 -121
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Each handler dispatches based on mode/flags following Unix CLI patterns.
|
|
5
5
|
*/
|
|
6
6
|
// Import shared utilities from handlers module
|
|
7
|
-
import {
|
|
7
|
+
import { resolvePersona, } from "./handlers/index.js";
|
|
8
8
|
// Import extracted persona mode handlers
|
|
9
|
-
import { hasExtractedHandler, getPersonaModeHandler, } from "./handlers/persona/index.js";
|
|
9
|
+
import { hasExtractedHandler, getPersonaModeHandler, handleVersion, } from "./handlers/persona/index.js";
|
|
10
10
|
// Import extracted handlers (Phase 1 modularization)
|
|
11
11
|
import { handleEnv as handleEnvExtracted } from "./handlers/env/index.js";
|
|
12
12
|
import { handleTemplate as handleTemplateExtracted } from "./handlers/template/index.js";
|
|
@@ -14,156 +14,38 @@ import { handleAction as handleActionExtracted } from "./handlers/action/index.j
|
|
|
14
14
|
import { handleReference as handleReferenceExtracted } from "./handlers/reference/index.js";
|
|
15
15
|
import { handleKnowledge as handleKnowledgeExtracted } from "./handlers/knowledge/index.js";
|
|
16
16
|
import { handleSync as handleSyncExtracted } from "./handlers/sync/index.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
import { ensureActionRegistry } from "../sdk/action-registry.js";
|
|
22
|
-
import { parseInput, intentToSpec, generateWorkflow } from "../sdk/workflow-intent.js";
|
|
23
|
-
import { ensureSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM } from "../sdk/workflow-validator.js";
|
|
24
|
-
import { runIntentArchitect } from "../sdk/intent-architect.js";
|
|
25
|
-
import { analyzeExecutionFlow, generateASCIIFlow } from "../sdk/workflow-execution-analyzer.js";
|
|
26
|
-
import { autoFixWorkflow } from "../sdk/workflow-fixer.js";
|
|
27
|
-
import { SanitizationSession, detectWithPatterns, } from "../sdk/sanitizer.js";
|
|
17
|
+
// Import extracted workflow handler (Phase 2 modularization)
|
|
18
|
+
// Note: Imported as "Extracted" to avoid conflict with local definition during transition
|
|
19
|
+
// The local handleWorkflow will be removed once all tests pass
|
|
20
|
+
import { handleWorkflow as handleWorkflowExtracted } from "./handlers/workflow/index.js";
|
|
28
21
|
// Args type imported from handlers/types.ts
|
|
29
22
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
-
// Generation Templates
|
|
23
|
+
// Generation Templates - MOVED to handlers/data/templates.ts
|
|
24
|
+
// Import via: import { GENERATION_TEMPLATES } from "./handlers/data/templates.js"
|
|
31
25
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
-
/**
|
|
33
|
-
* Built-in templates for document generation.
|
|
34
|
-
* Used by data(mode="generate", from="...").
|
|
35
|
-
*/
|
|
36
|
-
export const GENERATION_TEMPLATES = {
|
|
37
|
-
// Reference data
|
|
38
|
-
"countries": {
|
|
39
|
-
description: "ISO 3166-1 countries with code and name",
|
|
40
|
-
category: "reference",
|
|
41
|
-
prompt: "Generate a JSON array of {count} ISO 3166-1 countries with fields: code (2-letter), name. Format: {format}",
|
|
42
|
-
},
|
|
43
|
-
"industries": {
|
|
44
|
-
description: "NAICS industry classifications",
|
|
45
|
-
category: "reference",
|
|
46
|
-
prompt: "Generate a JSON array of {count} NAICS industry classifications with fields: code, name, description. Format: {format}",
|
|
47
|
-
},
|
|
48
|
-
"currencies": {
|
|
49
|
-
description: "ISO 4217 currencies",
|
|
50
|
-
category: "reference",
|
|
51
|
-
prompt: "Generate a JSON array of {count} ISO 4217 currencies with fields: code (3-letter), name, symbol. Format: {format}",
|
|
52
|
-
},
|
|
53
|
-
// Demo kits
|
|
54
|
-
"demo-sales-sdr": {
|
|
55
|
-
description: "Sales SDR demo kit with prospects, products, and Q&A",
|
|
56
|
-
category: "demo",
|
|
57
|
-
prompt: `Generate a comprehensive sales demo kit in {format} format containing:
|
|
58
|
-
1. {count} realistic B2B prospect profiles (company name, contact, role, pain points, budget)
|
|
59
|
-
2. 3-5 product entries with features and pricing
|
|
60
|
-
3. 10 common sales objections with responses
|
|
61
|
-
4. Sample discovery call script`,
|
|
62
|
-
},
|
|
63
|
-
"demo-support": {
|
|
64
|
-
description: "Customer support demo kit with customers, FAQs, and tickets",
|
|
65
|
-
category: "demo",
|
|
66
|
-
prompt: `Generate a customer support demo kit in {format} format containing:
|
|
67
|
-
1. {count} customer profiles (name, company, subscription tier, history)
|
|
68
|
-
2. 10 frequently asked questions with detailed answers
|
|
69
|
-
3. 5 sample support tickets with resolutions
|
|
70
|
-
4. Escalation guidelines`,
|
|
71
|
-
},
|
|
72
|
-
"demo-hr": {
|
|
73
|
-
description: "HR assistant demo kit with employees, policies, and benefits",
|
|
74
|
-
category: "demo",
|
|
75
|
-
prompt: `Generate an HR demo kit in {format} format containing:
|
|
76
|
-
1. {count} employee profiles (name, department, role, start date)
|
|
77
|
-
2. 5 company policies (PTO, remote work, expenses)
|
|
78
|
-
3. 3 benefits descriptions (health, 401k, equity)
|
|
79
|
-
4. Onboarding checklist`,
|
|
80
|
-
},
|
|
81
|
-
// Entity templates
|
|
82
|
-
"customer": {
|
|
83
|
-
description: "B2B customer profile",
|
|
84
|
-
category: "entity",
|
|
85
|
-
prompt: `Generate {count} realistic B2B customer profiles in {format} format with fields:
|
|
86
|
-
- company_name, industry, size (employees), annual_revenue
|
|
87
|
-
- primary_contact (name, title, email, phone)
|
|
88
|
-
- pain_points (array of 2-3 business challenges)
|
|
89
|
-
- current_solutions (what they use today)
|
|
90
|
-
- budget_range, decision_timeline`,
|
|
91
|
-
},
|
|
92
|
-
"product": {
|
|
93
|
-
description: "Product catalog entry with pricing",
|
|
94
|
-
category: "entity",
|
|
95
|
-
prompt: `Generate {count} product catalog entries in {format} format with fields:
|
|
96
|
-
- name, sku, category
|
|
97
|
-
- description (2-3 sentences)
|
|
98
|
-
- features (array of 3-5 key features)
|
|
99
|
-
- pricing (base_price, currency, billing_period)
|
|
100
|
-
- availability (in_stock, lead_time)`,
|
|
101
|
-
},
|
|
102
|
-
"faq": {
|
|
103
|
-
description: "FAQ entries",
|
|
104
|
-
category: "entity",
|
|
105
|
-
prompt: `Generate {count} FAQ entries in {format} format with fields:
|
|
106
|
-
- question (clear, specific question)
|
|
107
|
-
- answer (comprehensive answer, 2-4 sentences)
|
|
108
|
-
- category (e.g., billing, technical, getting-started)
|
|
109
|
-
- related_questions (array of 2-3 related question titles)`,
|
|
110
|
-
},
|
|
111
|
-
"employee": {
|
|
112
|
-
description: "Employee profile for HR systems",
|
|
113
|
-
category: "entity",
|
|
114
|
-
prompt: `Generate {count} employee profiles in {format} format with fields:
|
|
115
|
-
- name, email, employee_id
|
|
116
|
-
- department, title, manager
|
|
117
|
-
- start_date, location (office or remote)
|
|
118
|
-
- skills (array), certifications (array)`,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
26
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
27
|
// Widget Validation Helpers
|
|
123
28
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
29
|
// isValidWidget and sanitizeWidgets imported from ../sdk/proto-config.js
|
|
125
30
|
// validateWidgetsForApi imported from handlers/utils.ts
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
-
const DEPRECATED_PARAMS = {
|
|
130
|
-
identifier: { newName: "id", removeVersion: "2.0.0" },
|
|
131
|
-
clone_from: { newName: "from", removeVersion: "2.0.0" },
|
|
132
|
-
template_id: { newName: "from", removeVersion: "2.0.0" },
|
|
133
|
-
clone_data: { newName: "include_data", removeVersion: "2.0.0" },
|
|
134
|
-
};
|
|
135
|
-
/**
|
|
136
|
-
* Check for deprecated params and collect warnings.
|
|
137
|
-
* Returns array of warning messages for deprecated params that were used.
|
|
138
|
-
*/
|
|
139
|
-
function checkDeprecatedParams(args) {
|
|
140
|
-
const warnings = [];
|
|
141
|
-
for (const [oldName, { newName, removeVersion }] of Object.entries(DEPRECATED_PARAMS)) {
|
|
142
|
-
if (args[oldName] !== undefined) {
|
|
143
|
-
warnings.push(`'${oldName}' is deprecated, use '${newName}' instead (will be removed in v${removeVersion})`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return warnings;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Add deprecation warnings to response if any deprecated params were used.
|
|
150
|
-
*/
|
|
151
|
-
function addDeprecationWarnings(result, warnings) {
|
|
152
|
-
if (warnings.length > 0) {
|
|
153
|
-
return { ...result, _deprecation_warnings: warnings };
|
|
154
|
-
}
|
|
155
|
-
return result;
|
|
156
|
-
}
|
|
31
|
+
// Deprecation tracking - single source of truth
|
|
32
|
+
import { checkDeprecatedParams, addDeprecationWarnings } from "./handlers/deprecation.js";
|
|
157
33
|
// Template utilities imported from handlers/utils.ts:
|
|
158
34
|
// - normalizeTriggerType
|
|
159
35
|
// - getTemplates
|
|
160
36
|
// - clearTemplateCache
|
|
161
37
|
// - getPersonaTypeFromTemplate
|
|
162
38
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
163
|
-
//
|
|
39
|
+
// Extracted Handler Exports
|
|
164
40
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
165
|
-
//
|
|
41
|
+
// These handlers are fully extracted to handlers/*/ directories.
|
|
42
|
+
// Re-exported here for backwards compatibility with server.ts imports.
|
|
166
43
|
export const handleEnv = handleEnvExtracted;
|
|
44
|
+
export const handleAction = handleActionExtracted;
|
|
45
|
+
export const handleTemplate = handleTemplateExtracted;
|
|
46
|
+
export const handleKnowledge = handleKnowledgeExtracted;
|
|
47
|
+
export const handleReference = handleReferenceExtracted;
|
|
48
|
+
export const handleSync = handleSyncExtracted;
|
|
167
49
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
168
50
|
// PERSONA Handler
|
|
169
51
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -243,7 +125,9 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
243
125
|
const fs = await import("fs/promises");
|
|
244
126
|
return fs.readFile(path);
|
|
245
127
|
};
|
|
246
|
-
|
|
128
|
+
// Use extracted handler
|
|
129
|
+
const { handleData: handleDataExtracted } = await import("./handlers/data/index.js");
|
|
130
|
+
return handleDataExtracted(dataArgs, client, readFile);
|
|
247
131
|
}
|
|
248
132
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
249
133
|
// ROUTING: Workflow modifications
|
|
@@ -253,11 +137,18 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
253
137
|
const workflowSpec = args.workflow_spec;
|
|
254
138
|
const input = args.input;
|
|
255
139
|
// ───────────────────────────────────────────────────────────────────────────
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
140
|
+
// LLM-DRIVEN ARCHITECTURE: Route workflow modifications to handleWorkflow
|
|
141
|
+
//
|
|
142
|
+
// handleWorkflow accepts STRUCTURED OPERATIONS from the Agent.
|
|
143
|
+
// It does NOT parse natural language - that's the Agent's job.
|
|
144
|
+
//
|
|
145
|
+
// Route to handleWorkflow when:
|
|
146
|
+
// - mode === "modify" (explicit modify mode)
|
|
147
|
+
// - mode === "update" with `operations` (Agent built structured ops)
|
|
148
|
+
// - mode === "update" with `input` (legacy - returns context for Agent)
|
|
259
149
|
// ───────────────────────────────────────────────────────────────────────────
|
|
260
|
-
|
|
150
|
+
const operations = args.operations;
|
|
151
|
+
if ((mode === "modify" || (mode === "update" && (input || operations))) && idOrName) {
|
|
261
152
|
const workflowArgs = {
|
|
262
153
|
...args,
|
|
263
154
|
persona_id: idOrName,
|
|
@@ -265,14 +156,13 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
265
156
|
};
|
|
266
157
|
delete workflowArgs.id;
|
|
267
158
|
delete workflowArgs.identifier;
|
|
268
|
-
return
|
|
159
|
+
return handleWorkflowExtracted(workflowArgs, client, getTemplateId);
|
|
269
160
|
}
|
|
270
|
-
// mode="update" without input → modular handleUpdate (config/workflow_spec changes only)
|
|
161
|
+
// mode="update" without input/operations → modular handleUpdate (config/workflow_spec changes only)
|
|
271
162
|
if (mode === "update" && idOrName) {
|
|
272
163
|
return getPersonaModeHandler("update")(args, client);
|
|
273
164
|
}
|
|
274
|
-
// Workflow operations (optimize, etc.) →
|
|
275
|
-
// TODO: Deprecate handleWorkflow and move remaining logic to modular handlers
|
|
165
|
+
// Workflow operations (optimize, etc.) → handleWorkflowExtracted (extracted handler)
|
|
276
166
|
if (optimize || (idOrName && workflowDef && mode !== "update")) {
|
|
277
167
|
// Map persona args to workflow args
|
|
278
168
|
const workflowArgs = {
|
|
@@ -281,7 +171,7 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
281
171
|
};
|
|
282
172
|
delete workflowArgs.id;
|
|
283
173
|
delete workflowArgs.identifier;
|
|
284
|
-
return
|
|
174
|
+
return handleWorkflowExtracted(workflowArgs, client, getTemplateId);
|
|
285
175
|
}
|
|
286
176
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
287
177
|
// Standard persona operations (get, list, compare, version management)
|
|
@@ -310,8 +200,8 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
310
200
|
return {
|
|
311
201
|
error: "Explicit method required",
|
|
312
202
|
message: "You provided a persona id/name. What operation would you like to perform?",
|
|
313
|
-
valid_methods: ["get", "update", "delete", "
|
|
314
|
-
hint: "
|
|
203
|
+
valid_methods: ["get", "update", "delete", "sanitize", "snapshot", "history", "restore"],
|
|
204
|
+
hint: "LLM does analysis/comparison. Use method='get' to fetch data, then reason about it.",
|
|
315
205
|
example: `persona(method="get", id="${idOrName}")`,
|
|
316
206
|
};
|
|
317
207
|
}
|
|
@@ -397,230 +287,35 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
397
287
|
return { error: `Failed to get schema: ${error instanceof Error ? error.message : String(error)}` };
|
|
398
288
|
}
|
|
399
289
|
}
|
|
400
|
-
//
|
|
401
|
-
case "compare":
|
|
290
|
+
// Clone and create - handled by extracted handlers
|
|
402
291
|
case "clone":
|
|
403
|
-
case "create":
|
|
404
|
-
case "analyze": {
|
|
292
|
+
case "create": {
|
|
405
293
|
// These modes have been extracted to handlers/persona/*.ts
|
|
406
294
|
// - create.ts: handleCreate (~410 lines)
|
|
407
|
-
// - analyze.ts: handleAnalyze (~195 lines)
|
|
408
295
|
return { error: `Mode "${effectiveMode}" should be handled by extracted handler` };
|
|
409
296
|
}
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
case "snapshot": // Alias for version_create
|
|
414
|
-
case "version_create": {
|
|
415
|
-
if (!idOrName) {
|
|
416
|
-
return { error: "id required for version_create mode" };
|
|
417
|
-
}
|
|
418
|
-
if (!versionContext) {
|
|
419
|
-
return { error: "Version tracking not configured. Provide workspaceRoot in context." };
|
|
420
|
-
}
|
|
421
|
-
const persona = await resolvePersona(client, idOrName);
|
|
422
|
-
if (!persona) {
|
|
423
|
-
return { error: `Persona not found: ${idOrName}` };
|
|
424
|
-
}
|
|
425
|
-
// Fetch full persona with workflow
|
|
426
|
-
const fullPersona = await client.getPersonaById(persona.id);
|
|
427
|
-
if (!fullPersona) {
|
|
428
|
-
return { error: `Could not fetch full persona: ${persona.id}` };
|
|
429
|
-
}
|
|
430
|
-
const storage = createVersionStorage(versionContext.workspaceRoot);
|
|
431
|
-
const engine = createVersionPolicyEngine(storage);
|
|
432
|
-
const result = engine.forceCreateVersion(fullPersona, {
|
|
433
|
-
environment: versionContext.environment,
|
|
434
|
-
tenant_id: versionContext.tenant_id,
|
|
435
|
-
message: args.message,
|
|
436
|
-
created_by: "mcp-toolkit",
|
|
437
|
-
});
|
|
438
|
-
if (!result.created || !result.version) {
|
|
439
|
-
return { error: result.reason };
|
|
440
|
-
}
|
|
441
|
-
return {
|
|
442
|
-
success: true,
|
|
443
|
-
version: {
|
|
444
|
-
id: result.version.id,
|
|
445
|
-
version_number: result.version.version_number,
|
|
446
|
-
version_name: result.version.version_name,
|
|
447
|
-
content_hash: result.version.content_hash,
|
|
448
|
-
created_at: result.version.created_at,
|
|
449
|
-
message: result.version.message,
|
|
450
|
-
},
|
|
451
|
-
changes_from_parent: result.changes_from_parent,
|
|
452
|
-
versions_pruned: result.versions_pruned,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
case "history": // Alias for version_list
|
|
456
|
-
case "version_list": {
|
|
457
|
-
if (!idOrName) {
|
|
458
|
-
return { error: "id required for history mode" };
|
|
459
|
-
}
|
|
460
|
-
if (!versionContext) {
|
|
461
|
-
return { error: "Version tracking not configured. Provide workspaceRoot in context." };
|
|
462
|
-
}
|
|
463
|
-
const persona = await resolvePersona(client, idOrName);
|
|
464
|
-
if (!persona) {
|
|
465
|
-
return { error: `Persona not found: ${idOrName}` };
|
|
466
|
-
}
|
|
467
|
-
const storage = createVersionStorage(versionContext.workspaceRoot);
|
|
468
|
-
const engine = createVersionPolicyEngine(storage);
|
|
469
|
-
const versions = engine.listVersions(persona.id, {
|
|
470
|
-
limit: args.limit,
|
|
471
|
-
});
|
|
472
|
-
return {
|
|
473
|
-
persona_id: persona.id,
|
|
474
|
-
persona_name: persona.name,
|
|
475
|
-
versions,
|
|
476
|
-
count: versions.length,
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
case "version_get": {
|
|
480
|
-
if (!idOrName) {
|
|
481
|
-
return { error: "id required for version_get mode" };
|
|
482
|
-
}
|
|
483
|
-
if (!versionContext) {
|
|
484
|
-
return { error: "Version tracking not configured. Provide workspaceRoot in context." };
|
|
485
|
-
}
|
|
486
|
-
const persona = await resolvePersona(client, idOrName);
|
|
487
|
-
if (!persona) {
|
|
488
|
-
return { error: `Persona not found: ${idOrName}` };
|
|
489
|
-
}
|
|
490
|
-
const versionId = args.version ?? "latest";
|
|
491
|
-
const storage = createVersionStorage(versionContext.workspaceRoot);
|
|
492
|
-
const engine = createVersionPolicyEngine(storage);
|
|
493
|
-
const version = engine.getVersion(persona.id, versionId);
|
|
494
|
-
if (!version) {
|
|
495
|
-
return { error: `Version not found: ${versionId}` };
|
|
496
|
-
}
|
|
497
|
-
return {
|
|
498
|
-
persona_id: persona.id,
|
|
499
|
-
persona_name: persona.name,
|
|
500
|
-
version: {
|
|
501
|
-
id: version.id,
|
|
502
|
-
version_number: version.version_number,
|
|
503
|
-
version_name: version.version_name,
|
|
504
|
-
content_hash: version.content_hash,
|
|
505
|
-
created_at: version.created_at,
|
|
506
|
-
created_by: version.created_by,
|
|
507
|
-
trigger: version.trigger,
|
|
508
|
-
message: version.message,
|
|
509
|
-
changes_summary: version.changes_summary,
|
|
510
|
-
},
|
|
511
|
-
snapshot: {
|
|
512
|
-
display_name: version.snapshot.display_name,
|
|
513
|
-
description: version.snapshot.description,
|
|
514
|
-
workflow_id: version.snapshot.workflow_id,
|
|
515
|
-
trigger_type: version.snapshot.trigger_type,
|
|
516
|
-
embedding_enabled: version.snapshot.embedding_enabled,
|
|
517
|
-
has_workflow: !!version.snapshot.workflow_definition,
|
|
518
|
-
has_proto_config: !!version.snapshot.proto_config,
|
|
519
|
-
},
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
case "version_compare": {
|
|
523
|
-
if (!idOrName) {
|
|
524
|
-
return { error: "id required for version_compare mode" };
|
|
525
|
-
}
|
|
526
|
-
if (!versionContext) {
|
|
527
|
-
return { error: "Version tracking not configured. Provide workspaceRoot in context." };
|
|
528
|
-
}
|
|
529
|
-
const v1 = args.v1;
|
|
530
|
-
const v2 = args.v2;
|
|
531
|
-
if (!v1 || !v2) {
|
|
532
|
-
return { error: "v1 and v2 required for version_compare mode" };
|
|
533
|
-
}
|
|
534
|
-
const persona = await resolvePersona(client, idOrName);
|
|
535
|
-
if (!persona) {
|
|
536
|
-
return { error: `Persona not found: ${idOrName}` };
|
|
537
|
-
}
|
|
538
|
-
const storage = createVersionStorage(versionContext.workspaceRoot);
|
|
539
|
-
const engine = createVersionPolicyEngine(storage);
|
|
540
|
-
const result = engine.compareVersions(persona.id, v1, v2);
|
|
541
|
-
if (!result.success) {
|
|
542
|
-
return { error: result.error };
|
|
543
|
-
}
|
|
544
|
-
return {
|
|
545
|
-
persona_id: persona.id,
|
|
546
|
-
persona_name: persona.name,
|
|
547
|
-
comparison: {
|
|
548
|
-
v1: result.diff?.v1,
|
|
549
|
-
v2: result.diff?.v2,
|
|
550
|
-
identical: result.diff?.identical,
|
|
551
|
-
changed_fields: result.diff?.changed_fields,
|
|
552
|
-
workflow_diff: result.diff?.workflow_diff,
|
|
553
|
-
},
|
|
554
|
-
summary: result.summary,
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
case "restore": // Alias for version_restore
|
|
558
|
-
case "version_restore": {
|
|
559
|
-
if (!idOrName) {
|
|
560
|
-
return { error: "id required for restore mode" };
|
|
561
|
-
}
|
|
562
|
-
if (!versionContext) {
|
|
563
|
-
return { error: "Version tracking not configured. Provide workspaceRoot in context." };
|
|
564
|
-
}
|
|
565
|
-
const versionId = args.version;
|
|
566
|
-
if (!versionId) {
|
|
567
|
-
return { error: "version required for version_restore mode" };
|
|
568
|
-
}
|
|
569
|
-
const persona = await resolvePersona(client, idOrName);
|
|
570
|
-
if (!persona) {
|
|
571
|
-
return { error: `Persona not found: ${idOrName}` };
|
|
572
|
-
}
|
|
573
|
-
const storage = createVersionStorage(versionContext.workspaceRoot);
|
|
574
|
-
const engine = createVersionPolicyEngine(storage);
|
|
575
|
-
const restoreData = engine.getRestoreData(persona.id, versionId);
|
|
576
|
-
if (!restoreData.success || !restoreData.restore_payload) {
|
|
577
|
-
return { error: restoreData.error ?? "Failed to get restore data" };
|
|
578
|
-
}
|
|
579
|
-
// Create a version snapshot before restoring (audit trail)
|
|
580
|
-
const fullPersona = await client.getPersonaById(persona.id);
|
|
581
|
-
if (fullPersona) {
|
|
582
|
-
engine.forceCreateVersion(fullPersona, {
|
|
583
|
-
environment: versionContext.environment,
|
|
584
|
-
tenant_id: versionContext.tenant_id,
|
|
585
|
-
message: `Before restore to ${restoreData.version?.version_name}`,
|
|
586
|
-
created_by: "mcp-toolkit",
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
// Apply the restore
|
|
590
|
-
const payload = restoreData.restore_payload;
|
|
591
|
-
await client.updateAiEmployee({
|
|
592
|
-
persona_id: payload.persona_id,
|
|
593
|
-
name: payload.name,
|
|
594
|
-
description: payload.description,
|
|
595
|
-
proto_config: payload.proto_config,
|
|
596
|
-
workflow: payload.workflow ?? undefined,
|
|
597
|
-
welcome_messages: payload.welcome_messages ?? undefined,
|
|
598
|
-
embedding_enabled: payload.embedding_enabled ?? undefined,
|
|
599
|
-
});
|
|
600
|
-
// Create post-restore version
|
|
601
|
-
const restoredPersona = await client.getPersonaById(persona.id);
|
|
602
|
-
if (restoredPersona) {
|
|
603
|
-
engine.forceCreateVersion(restoredPersona, {
|
|
604
|
-
environment: versionContext.environment,
|
|
605
|
-
tenant_id: versionContext.tenant_id,
|
|
606
|
-
message: `Restored to ${restoreData.version?.version_name}`,
|
|
607
|
-
created_by: "mcp-toolkit",
|
|
608
|
-
});
|
|
609
|
-
}
|
|
297
|
+
// Analyze and compare - LLM does this, not MCP
|
|
298
|
+
case "analyze":
|
|
299
|
+
case "compare": {
|
|
610
300
|
return {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
version_id: restoreData.version?.id,
|
|
615
|
-
version_name: restoreData.version?.version_name,
|
|
616
|
-
version_number: restoreData.version?.version_number,
|
|
617
|
-
},
|
|
618
|
-
message: `Persona restored to ${restoreData.version?.version_name}`,
|
|
301
|
+
error: `Method "${effectiveMode}" removed - LLM does analysis/comparison`,
|
|
302
|
+
hint: "Use method='get' to fetch persona data, then do your own analysis/comparison.",
|
|
303
|
+
example: `persona(method="get", id="...", include_workflow=true)`,
|
|
619
304
|
};
|
|
620
305
|
}
|
|
306
|
+
// ─────────────── Version Management Modes ───────────────
|
|
307
|
+
// Extracted to handlers/persona/version.ts
|
|
308
|
+
case "snapshot":
|
|
309
|
+
case "history":
|
|
310
|
+
case "restore":
|
|
311
|
+
case "version_create":
|
|
312
|
+
case "version_list":
|
|
313
|
+
case "version_get":
|
|
314
|
+
case "version_compare":
|
|
315
|
+
case "version_restore":
|
|
621
316
|
case "version_policy": {
|
|
622
317
|
if (!idOrName) {
|
|
623
|
-
return { error:
|
|
318
|
+
return { error: `id required for ${effectiveMode} mode` };
|
|
624
319
|
}
|
|
625
320
|
if (!versionContext) {
|
|
626
321
|
return { error: "Version tracking not configured. Provide workspaceRoot in context." };
|
|
@@ -629,2345 +324,27 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
629
324
|
if (!persona) {
|
|
630
325
|
return { error: `Persona not found: ${idOrName}` };
|
|
631
326
|
}
|
|
632
|
-
|
|
633
|
-
const engine = createVersionPolicyEngine(storage);
|
|
634
|
-
// Check if we're updating or just getting
|
|
635
|
-
const hasUpdates = args.auto_on_deploy !== undefined ||
|
|
636
|
-
args.auto_on_sync !== undefined ||
|
|
637
|
-
args.max_versions !== undefined;
|
|
638
|
-
if (hasUpdates) {
|
|
639
|
-
const updated = engine.updatePolicy(persona.id, {
|
|
640
|
-
auto_version_on_deploy: args.auto_on_deploy,
|
|
641
|
-
auto_version_on_sync: args.auto_on_sync,
|
|
642
|
-
max_versions: args.max_versions,
|
|
643
|
-
});
|
|
644
|
-
return {
|
|
645
|
-
success: true,
|
|
646
|
-
persona_id: persona.id,
|
|
647
|
-
persona_name: persona.name,
|
|
648
|
-
policy: updated,
|
|
649
|
-
message: "Policy updated",
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
// Just get current policy
|
|
653
|
-
const policy = engine.getPolicy(persona.id);
|
|
654
|
-
return {
|
|
655
|
-
persona_id: persona.id,
|
|
656
|
-
persona_name: persona.name,
|
|
657
|
-
policy,
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
default:
|
|
661
|
-
return { error: `Unknown mode: ${effectiveMode}` };
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
665
|
-
// WORKFLOW Handler - Unified greenfield/brownfield operations
|
|
666
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
667
|
-
export async function handleWorkflow(args, client, getTemplateId) {
|
|
668
|
-
const personaId = args.persona_id;
|
|
669
|
-
const workflowDef = args.workflow_def;
|
|
670
|
-
const input = args.input;
|
|
671
|
-
const optimize = args.optimize;
|
|
672
|
-
const compareTo = args.compare_to;
|
|
673
|
-
// Default to preview=true for safety (no accidental deploys)
|
|
674
|
-
const preview = args.preview !== false;
|
|
675
|
-
// Smart mode detection - user doesn't need to specify
|
|
676
|
-
// Priority: extraction_schema > compare > optimize > brownfield > greenfield > analyze
|
|
677
|
-
let effectiveMode;
|
|
678
|
-
if (args.mode === "compile") {
|
|
679
|
-
// Explicit compile mode (for advanced users)
|
|
680
|
-
effectiveMode = "compile";
|
|
681
|
-
}
|
|
682
|
-
else if (compareTo) {
|
|
683
|
-
// Compare two workflows
|
|
684
|
-
effectiveMode = "compare";
|
|
685
|
-
}
|
|
686
|
-
else if (optimize && personaId) {
|
|
687
|
-
// Auto-fix issues
|
|
688
|
-
effectiveMode = "optimize";
|
|
689
|
-
}
|
|
690
|
-
else if (personaId && workflowDef) {
|
|
691
|
-
// DEPLOY: Persona exists + workflow_def provided → deploy directly (no input needed)
|
|
692
|
-
effectiveMode = "deploy";
|
|
693
|
-
}
|
|
694
|
-
else if (personaId && input) {
|
|
695
|
-
// MODIFY: Persona exists + input provided → generate new workflow from input
|
|
696
|
-
effectiveMode = "modify"; // New unified mode for all brownfield changes
|
|
697
|
-
}
|
|
698
|
-
else if (input && !personaId) {
|
|
699
|
-
// GREENFIELD: Create new workflow
|
|
700
|
-
effectiveMode = "generate";
|
|
701
|
-
}
|
|
702
|
-
else if (personaId || workflowDef || args.include) {
|
|
703
|
-
// Just analyzing
|
|
704
|
-
effectiveMode = "analyze";
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
effectiveMode = "analyze";
|
|
708
|
-
}
|
|
709
|
-
// Legacy mode support (backwards compatibility)
|
|
710
|
-
const legacyMode = args.mode;
|
|
711
|
-
if (legacyMode === "extend")
|
|
712
|
-
effectiveMode = "modify";
|
|
713
|
-
if (legacyMode === "deploy") {
|
|
714
|
-
// If explicit deploy mode, use it (works with or without input)
|
|
715
|
-
effectiveMode = "deploy";
|
|
716
|
-
}
|
|
717
|
-
switch (effectiveMode) {
|
|
718
|
-
case "generate": {
|
|
719
|
-
if (!input) {
|
|
720
|
-
return { error: "input required for generate mode" };
|
|
721
|
-
}
|
|
722
|
-
// Load action registry for API-driven action versions/namespaces
|
|
723
|
-
const actionRegistry = await ensureActionRegistry(client);
|
|
724
|
-
const parseResult = parseInput(input);
|
|
725
|
-
if (!parseResult.validation.complete) {
|
|
726
|
-
return {
|
|
727
|
-
status: "incomplete",
|
|
728
|
-
input_type: parseResult.input_type,
|
|
729
|
-
missing: parseResult.validation.missing,
|
|
730
|
-
questions: parseResult.validation.questions,
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
// Override persona_type from args.type if provided (args.type takes precedence)
|
|
734
|
-
if (args.type) {
|
|
735
|
-
parseResult.intent.persona_type = args.type;
|
|
736
|
-
}
|
|
737
|
-
// Load schema registry for API-driven validation (graceful degradation if unavailable)
|
|
738
|
-
let schemaRegistry;
|
|
739
|
-
try {
|
|
740
|
-
schemaRegistry = await ensureSchemaRegistry(client);
|
|
741
|
-
}
|
|
742
|
-
catch {
|
|
743
|
-
// Schema registry unavailable - skip API validation
|
|
744
|
-
schemaRegistry = null;
|
|
745
|
-
}
|
|
746
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
747
|
-
// PERSONA WIDGET CONTEXT: If persona_id provided, fetch available widgets
|
|
748
|
-
// Workflows reference widgets by name - agent needs to know what's available
|
|
749
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
750
|
-
let personaWidgets = [];
|
|
751
|
-
if (personaId) {
|
|
752
|
-
try {
|
|
753
|
-
const persona = await client.getPersonaById(personaId);
|
|
754
|
-
const protoConfig = persona?.proto_config;
|
|
755
|
-
if (protoConfig?.widgets) {
|
|
756
|
-
personaWidgets = protoConfig.widgets
|
|
757
|
-
.filter(w => typeof w.name === "string" && w.name.trim().length > 0)
|
|
758
|
-
.map(w => ({
|
|
759
|
-
name: w.name,
|
|
760
|
-
type: String(w.type ?? "unknown"),
|
|
761
|
-
title: w.title,
|
|
762
|
-
}));
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
catch {
|
|
766
|
-
// Persona not found or no widgets - continue without context
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
770
|
-
// PROGRESSIVE ENHANCEMENT: Use Intent Architect for moderate/complex requests
|
|
771
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
772
|
-
//
|
|
773
|
-
// Uses the canonical runIntentArchitect() entrypoint which:
|
|
774
|
-
// - Detects complexity with typed scores (not enums)
|
|
775
|
-
// - Returns qualification questions or LLM prompt
|
|
776
|
-
// - Supports iterative refinement with previous_answers
|
|
777
|
-
//
|
|
778
|
-
// Configure rollout via max_complexity option (default: allow all levels)
|
|
779
|
-
const maxComplexity = args.max_complexity || undefined;
|
|
780
|
-
const inputStr = input; // Already validated above
|
|
781
|
-
const architectResult = runIntentArchitect(inputStr, {
|
|
782
|
-
persona_type: parseResult.intent.persona_type,
|
|
783
|
-
available_integrations: schemaRegistry?.getAllActions().slice(0, 20).map(a => a.displayName || a.name),
|
|
784
|
-
}, { max_complexity: maxComplexity });
|
|
785
|
-
// For moderate/complex: return Intent Architect result with questions or prompt
|
|
786
|
-
if (!architectResult.strategy.can_proceed) {
|
|
787
|
-
let availableActions = [];
|
|
788
|
-
let availableTemplates = [];
|
|
789
|
-
// Enhance prompt with action catalog and widget context
|
|
790
|
-
let enhancedPrompt = architectResult.prompt_package;
|
|
791
|
-
if (enhancedPrompt) {
|
|
792
|
-
let systemAdditions = "";
|
|
793
|
-
// Add action catalog if available
|
|
794
|
-
if (schemaRegistry) {
|
|
795
|
-
const actionCatalog = generateActionCatalogForLLM(schemaRegistry);
|
|
796
|
-
systemAdditions += "\n\n## Available Actions\n" + actionCatalog;
|
|
797
|
-
availableActions = schemaRegistry.getAllActions().map(a => a.name);
|
|
798
|
-
availableTemplates = schemaRegistry.getAllTemplates().map(t => ({ id: t.id, name: t.name, type: t.type }));
|
|
799
|
-
}
|
|
800
|
-
// Add persona widget context if available
|
|
801
|
-
if (personaWidgets.length > 0) {
|
|
802
|
-
systemAdditions += "\n\n## Available Persona Widgets\n";
|
|
803
|
-
systemAdditions += "Use these exact widget names in workflow bindings (widgetName field):\n";
|
|
804
|
-
for (const w of personaWidgets) {
|
|
805
|
-
systemAdditions += `- \`${w.name}\` (${w.type})${w.title ? ` - "${w.title}"` : ""}\n`;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
if (systemAdditions) {
|
|
809
|
-
enhancedPrompt = {
|
|
810
|
-
system: enhancedPrompt.system + systemAdditions,
|
|
811
|
-
user: enhancedPrompt.user,
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
return {
|
|
816
|
-
status: "needs_intent_architect",
|
|
817
|
-
// Persona widget context (for workflow bindings)
|
|
818
|
-
available_widgets: personaWidgets.length > 0 ? personaWidgets : undefined,
|
|
819
|
-
// Typed assessment (new)
|
|
820
|
-
assessment: architectResult.assessment,
|
|
821
|
-
// Strategy decision (new)
|
|
822
|
-
strategy: architectResult.strategy,
|
|
823
|
-
// Qualification questions (new)
|
|
824
|
-
questions: architectResult.questions,
|
|
825
|
-
// LLM prompt (if full architect)
|
|
826
|
-
llm_prompt: enhancedPrompt,
|
|
827
|
-
// Hint for what to do next
|
|
828
|
-
hint: architectResult.strategy.next_step,
|
|
829
|
-
// Legacy fields for backward compatibility
|
|
830
|
-
reason: architectResult.legacy?.signals.reason,
|
|
831
|
-
complexity: architectResult.legacy?.complexity,
|
|
832
|
-
approach: architectResult.strategy.approach,
|
|
833
|
-
gates_to_ask: architectResult.strategy.gates_to_ask,
|
|
834
|
-
// Also provide basic spec as fallback
|
|
835
|
-
fallback_spec: intentToSpec(parseResult.intent),
|
|
836
|
-
// Include catalogs for reference (if available)
|
|
837
|
-
available_actions: availableActions,
|
|
838
|
-
available_templates: availableTemplates,
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
// SIMPLE complexity: Check if intent requires LLM-driven generation (legacy path)
|
|
842
|
-
const genResult = generateWorkflow(parseResult.intent);
|
|
843
|
-
if (genResult.needs_llm) {
|
|
844
|
-
// Complex workflow detected by legacy system - return prompt for LLM to generate
|
|
845
|
-
// This is a fallback for cases the new complexity analyzer might miss
|
|
846
|
-
let enhancedPrompt = genResult.llm_prompt;
|
|
847
|
-
let availableActions = [];
|
|
848
|
-
let availableTemplates = [];
|
|
849
|
-
if (enhancedPrompt) {
|
|
850
|
-
let systemAdditions = "";
|
|
851
|
-
if (schemaRegistry) {
|
|
852
|
-
const actionCatalog = generateActionCatalogForLLM(schemaRegistry);
|
|
853
|
-
systemAdditions += "\n\n" + actionCatalog;
|
|
854
|
-
availableActions = schemaRegistry.getAllActions().map(a => a.name);
|
|
855
|
-
availableTemplates = schemaRegistry.getAllTemplates().map(t => ({ id: t.id, name: t.name, type: t.type }));
|
|
856
|
-
}
|
|
857
|
-
// Add persona widget context if available
|
|
858
|
-
if (personaWidgets.length > 0) {
|
|
859
|
-
systemAdditions += "\n\n## Available Persona Widgets\n";
|
|
860
|
-
systemAdditions += "Use these exact widget names in workflow bindings (widgetName field):\n";
|
|
861
|
-
for (const w of personaWidgets) {
|
|
862
|
-
systemAdditions += `- \`${w.name}\` (${w.type})${w.title ? ` - "${w.title}"` : ""}\n`;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
if (systemAdditions) {
|
|
866
|
-
enhancedPrompt = {
|
|
867
|
-
system: enhancedPrompt.system + systemAdditions,
|
|
868
|
-
user: enhancedPrompt.user,
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
return {
|
|
873
|
-
status: "needs_llm_generation",
|
|
874
|
-
reason: genResult.reason,
|
|
875
|
-
complexity: genResult.complexity,
|
|
876
|
-
llm_prompt: enhancedPrompt,
|
|
877
|
-
hint: "Send llm_prompt to an LLM, then call persona(workflow_def=<parsed_response>) to deploy.",
|
|
878
|
-
// Also provide basic spec as fallback (without complex chains)
|
|
879
|
-
fallback_spec: intentToSpec(parseResult.intent),
|
|
880
|
-
// Include catalogs for reference (if available)
|
|
881
|
-
available_actions: availableActions,
|
|
882
|
-
available_templates: availableTemplates,
|
|
883
|
-
// Persona widget context (for workflow bindings)
|
|
884
|
-
available_widgets: personaWidgets.length > 0 ? personaWidgets : undefined,
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
// Simple workflow - use generated spec directly
|
|
888
|
-
const spec = genResult.spec;
|
|
889
|
-
// Override spec name with provided name if given (args.name takes precedence over parsed name)
|
|
890
|
-
if (args.name) {
|
|
891
|
-
spec.name = args.name;
|
|
892
|
-
}
|
|
893
|
-
if (args.description) {
|
|
894
|
-
spec.description = args.description;
|
|
895
|
-
}
|
|
896
|
-
// Validate spec against API schemas BEFORE compiling (if schema registry available)
|
|
897
|
-
let specValidation = null;
|
|
898
|
-
if (schemaRegistry) {
|
|
899
|
-
specValidation = validateWorkflowSpec(spec, schemaRegistry);
|
|
900
|
-
if (!specValidation.valid) {
|
|
901
|
-
return {
|
|
902
|
-
status: "validation_failed",
|
|
903
|
-
errors: specValidation.errors,
|
|
904
|
-
warnings: specValidation.warnings,
|
|
905
|
-
action_coverage: specValidation.action_coverage,
|
|
906
|
-
hint: "Fix the validation errors and try again. Unknown actions may need to be checked against ListActions.",
|
|
907
|
-
spec_attempted: spec,
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
const compiled = compileWorkflow(spec, { registry: actionRegistry });
|
|
912
|
-
// Validate the generated workflow (structural checks)
|
|
913
|
-
const issues = detectWorkflowIssues(compiled.workflow_def);
|
|
914
|
-
const result = {
|
|
915
|
-
mode: "generate",
|
|
916
|
-
status: preview ? "preview" : "deployed",
|
|
917
|
-
workflow_def: compiled.workflow_def,
|
|
918
|
-
proto_config: compiled.proto_config,
|
|
919
|
-
validation: parseResult.validation,
|
|
920
|
-
};
|
|
921
|
-
// Include API validation results if available
|
|
922
|
-
if (specValidation) {
|
|
923
|
-
result.api_validation = {
|
|
924
|
-
valid: specValidation.valid,
|
|
925
|
-
warnings: specValidation.warnings,
|
|
926
|
-
action_coverage: specValidation.action_coverage,
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
if (issues.length > 0) {
|
|
930
|
-
result.issues = issues;
|
|
931
|
-
}
|
|
932
|
-
// If preview=false and persona_id provided, deploy
|
|
933
|
-
if (!preview && personaId) {
|
|
934
|
-
const persona = await client.getPersonaById(personaId);
|
|
935
|
-
if (!persona) {
|
|
936
|
-
return { error: `Persona not found: ${personaId}` };
|
|
937
|
-
}
|
|
938
|
-
await client.updateAiEmployee({
|
|
939
|
-
persona_id: personaId,
|
|
940
|
-
workflow: compiled.workflow_def,
|
|
941
|
-
proto_config: args.proto_config || compiled.proto_config || persona.proto_config,
|
|
942
|
-
});
|
|
943
|
-
result.deployed_to = { persona_id: personaId, persona_name: persona.name };
|
|
944
|
-
}
|
|
945
|
-
else if (!preview && !personaId) {
|
|
946
|
-
// GREENFIELD: Create new persona from template, then deploy workflow + proto_config
|
|
947
|
-
const personaName = args.name;
|
|
948
|
-
if (personaName) {
|
|
949
|
-
const personaType = args.type || "chat";
|
|
950
|
-
// Dynamic template lookup - templates are tenant-specific
|
|
951
|
-
// Match by normalized trigger_type label
|
|
952
|
-
const templates = await getTemplates(client);
|
|
953
|
-
const matchingTemplate = templates.find(t => normalizeTriggerType(t.trigger_type) === personaType.toLowerCase());
|
|
954
|
-
// Try getTemplateId callback, then registry, then dynamic lookup
|
|
955
|
-
const templateFromRegistry = actionRegistry.getTemplateForType(personaType);
|
|
956
|
-
const templateId = getTemplateId?.(personaType) || templateFromRegistry?.id || matchingTemplate?.id;
|
|
957
|
-
if (!templateId) {
|
|
958
|
-
const availableTypes = [...new Set(templates.map(t => normalizeTriggerType(t.trigger_type)).filter(Boolean))];
|
|
959
|
-
return {
|
|
960
|
-
error: `No template found for type "${personaType}".`,
|
|
961
|
-
available_types: availableTypes,
|
|
962
|
-
hint: "Provide template_id directly or use one of the available types.",
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
// Step 1: Create the persona from template
|
|
966
|
-
const createResult = await client.createAiEmployee({
|
|
967
|
-
name: personaName,
|
|
968
|
-
description: args.description,
|
|
969
|
-
template_id: templateId,
|
|
970
|
-
});
|
|
971
|
-
const newPersonaId = createResult.persona_id ?? createResult.id;
|
|
972
|
-
if (!newPersonaId) {
|
|
973
|
-
return { error: "Failed to create persona: no ID returned" };
|
|
974
|
-
}
|
|
975
|
-
// Step 2: Fetch the newly created persona to get template's valid structure
|
|
976
|
-
const newPersona = await client.getPersonaById(newPersonaId);
|
|
977
|
-
if (!newPersona) {
|
|
978
|
-
return { error: `Failed to fetch newly created persona: ${newPersonaId}` };
|
|
979
|
-
}
|
|
980
|
-
// Step 3: Merge proto_config - keep template structure, update widget values
|
|
981
|
-
const existingProtoConfig = (newPersona.proto_config ?? {});
|
|
982
|
-
const generatedProtoConfig = args.proto_config || compiled.proto_config || {};
|
|
983
|
-
// Merge widgets by name - template widgets provide structure, generated values override
|
|
984
|
-
const existingWidgets = (existingProtoConfig.widgets ?? []);
|
|
985
|
-
const generatedWidgets = (generatedProtoConfig.widgets ?? []);
|
|
986
|
-
const widgetMap = new Map();
|
|
987
|
-
// Start with template widgets (valid structure)
|
|
988
|
-
for (const w of existingWidgets) {
|
|
989
|
-
if (typeof w.name === "string" && w.name.trim().length > 0) {
|
|
990
|
-
widgetMap.set(w.name, w);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
// Merge generated widget VALUES into template structure
|
|
994
|
-
for (const genWidget of generatedWidgets) {
|
|
995
|
-
const widgetName = genWidget.name;
|
|
996
|
-
if (typeof widgetName === "string" && widgetName.trim().length > 0) {
|
|
997
|
-
const existing = widgetMap.get(widgetName);
|
|
998
|
-
if (existing) {
|
|
999
|
-
// Merge: keep template structure, update config values
|
|
1000
|
-
const merged = { ...existing };
|
|
1001
|
-
// The widget config is stored under the widget name (e.g., conversationSettings)
|
|
1002
|
-
if (genWidget[widgetName]) {
|
|
1003
|
-
merged[widgetName] = { ...(existing[widgetName] || {}), ...genWidget[widgetName] };
|
|
1004
|
-
}
|
|
1005
|
-
widgetMap.set(widgetName, merged);
|
|
1006
|
-
}
|
|
1007
|
-
else {
|
|
1008
|
-
// New widget not in template - add as-is
|
|
1009
|
-
widgetMap.set(widgetName, genWidget);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
const mergedProtoConfig = {
|
|
1014
|
-
...existingProtoConfig,
|
|
1015
|
-
widgets: Array.from(widgetMap.values()),
|
|
1016
|
-
};
|
|
1017
|
-
// Step 4: Deploy BOTH workflow AND proto_config for ALL persona types
|
|
1018
|
-
// Voice AI IS driven by workflow (not just identityAndPurpose)
|
|
1019
|
-
const existingWorkflow = newPersona.workflow_def;
|
|
1020
|
-
const existingWfName = existingWorkflow?.workflowName;
|
|
1021
|
-
// Clone our compiled workflow and set the namespace from the existing workflow
|
|
1022
|
-
const workflowForDeploy = JSON.parse(JSON.stringify(compiled.workflow_def));
|
|
1023
|
-
const wfName = workflowForDeploy.workflowName;
|
|
1024
|
-
if (wfName?.name && existingWfName?.name) {
|
|
1025
|
-
// Copy the exact namespace from the template workflow
|
|
1026
|
-
wfName.name.namespaces = existingWfName.name.namespaces;
|
|
1027
|
-
wfName.name.name = existingWfName.name.name; // template_id
|
|
1028
|
-
}
|
|
1029
|
-
// Fix results format - use "<actionName>.<outputName>" keys
|
|
1030
|
-
const compiledResults = workflowForDeploy.results;
|
|
1031
|
-
if (compiledResults) {
|
|
1032
|
-
const newResults = {};
|
|
1033
|
-
for (const [, value] of Object.entries(compiledResults)) {
|
|
1034
|
-
if (value.actionName && value.outputName) {
|
|
1035
|
-
const key = `${value.actionName}.${value.outputName}`;
|
|
1036
|
-
newResults[key] = value;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
workflowForDeploy.results = newResults;
|
|
1040
|
-
}
|
|
1041
|
-
try {
|
|
1042
|
-
await client.updateAiEmployee({
|
|
1043
|
-
persona_id: newPersonaId,
|
|
1044
|
-
workflow: workflowForDeploy,
|
|
1045
|
-
proto_config: mergedProtoConfig,
|
|
1046
|
-
}, { verbose: true });
|
|
1047
|
-
}
|
|
1048
|
-
catch (deployError) {
|
|
1049
|
-
// Workflow deploy failed - still set proto_config
|
|
1050
|
-
const errMsg = deployError instanceof Error ? deployError.message : String(deployError);
|
|
1051
|
-
await client.updateAiEmployee({
|
|
1052
|
-
persona_id: newPersonaId,
|
|
1053
|
-
proto_config: mergedProtoConfig,
|
|
1054
|
-
});
|
|
1055
|
-
result.workflow_deploy_error = errMsg;
|
|
1056
|
-
result.workflow_attempted = workflowForDeploy;
|
|
1057
|
-
result.status = "partial";
|
|
1058
|
-
result.hint = "Persona created, config set, but workflow deploy failed. Check workflow_attempted for details.";
|
|
1059
|
-
result.deployed_to = {
|
|
1060
|
-
persona_id: newPersonaId,
|
|
1061
|
-
persona_name: personaName,
|
|
1062
|
-
created: true,
|
|
1063
|
-
workflow_deployed: false,
|
|
1064
|
-
};
|
|
1065
|
-
return result;
|
|
1066
|
-
}
|
|
1067
|
-
// Success - workflow deployed for all persona types
|
|
1068
|
-
result.deployed_to = {
|
|
1069
|
-
persona_id: newPersonaId,
|
|
1070
|
-
persona_name: personaName,
|
|
1071
|
-
created: true
|
|
1072
|
-
};
|
|
1073
|
-
result.status = "deployed";
|
|
1074
|
-
result.next_steps = [
|
|
1075
|
-
`Persona "${personaName}" created with template workflow.`,
|
|
1076
|
-
`To customize the workflow, use: workflow(persona_id="${newPersonaId}", input="add search node", preview=false)`,
|
|
1077
|
-
];
|
|
1078
|
-
}
|
|
1079
|
-
else {
|
|
1080
|
-
result.hint = "Provide name to create new persona, or persona_id to deploy to existing persona";
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
if (preview) {
|
|
1084
|
-
result.next_steps = [
|
|
1085
|
-
"Review the generated workflow_def",
|
|
1086
|
-
personaId
|
|
1087
|
-
? `Deploy with: workflow(mode="generate", input=..., persona_id="${personaId}", preview=false)`
|
|
1088
|
-
: "Create persona first: persona(mode='create', name='...', type='...')",
|
|
1089
|
-
];
|
|
1090
|
-
}
|
|
1091
|
-
return result;
|
|
1092
|
-
}
|
|
1093
|
-
case "modify":
|
|
1094
|
-
case "extend": {
|
|
1095
|
-
// BROWNFIELD: Intelligently modify existing workflow
|
|
1096
|
-
// Handles: add, remove, replace, consolidate, rewire - all in one
|
|
1097
|
-
if (!personaId) {
|
|
1098
|
-
return { error: "persona_id required for workflow modifications" };
|
|
1099
|
-
}
|
|
1100
|
-
if (!input) {
|
|
1101
|
-
return { error: "input required. Describe what you want to change." };
|
|
1102
|
-
}
|
|
1103
|
-
const persona = await client.getPersonaById(personaId);
|
|
1104
|
-
if (!persona) {
|
|
1105
|
-
return { error: `Persona not found: ${personaId}` };
|
|
1106
|
-
}
|
|
1107
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1108
|
-
// CONFIG-ONLY DETECTION: Route to proto_config update instead of workflow
|
|
1109
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1110
|
-
const lowerInput = input.toLowerCase();
|
|
1111
|
-
const isConfigOnlyUpdate = ((lowerInput.includes("voice settings") || lowerInput.includes("voice config")) ||
|
|
1112
|
-
(lowerInput.includes("welcome message") && !lowerInput.includes("add") && !lowerInput.includes("node")) ||
|
|
1113
|
-
(lowerInput.includes("identity") && lowerInput.includes("purpose")) ||
|
|
1114
|
-
(lowerInput.includes("speech characteristics")) ||
|
|
1115
|
-
(lowerInput.includes("hangup instructions") || lowerInput.includes("hangup")) ||
|
|
1116
|
-
(lowerInput.includes("update") && (lowerInput.includes("persona settings") ||
|
|
1117
|
-
lowerInput.includes("conversation settings") ||
|
|
1118
|
-
lowerInput.includes("proto_config") ||
|
|
1119
|
-
lowerInput.includes("chat settings"))));
|
|
1120
|
-
if (isConfigOnlyUpdate) {
|
|
1121
|
-
// This is a config update, not a workflow modification
|
|
1122
|
-
// Parse the config values from the input and update proto_config only
|
|
1123
|
-
const existingProtoConfig = (persona.proto_config ?? {});
|
|
1124
|
-
const existingWidgets = (existingProtoConfig.widgets ?? []);
|
|
1125
|
-
// Find and update the conversationSettings widget
|
|
1126
|
-
const widgetMap = new Map();
|
|
1127
|
-
for (const w of existingWidgets) {
|
|
1128
|
-
if (typeof w.name === "string" && w.name.trim().length > 0) {
|
|
1129
|
-
widgetMap.set(w.name, { ...w });
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
// Parse voice config updates from input
|
|
1133
|
-
const configUpdates = {};
|
|
1134
|
-
// Extract welcome message
|
|
1135
|
-
const welcomeMatch = input.match(/welcome\s*message[:\s]*["']?([^"'\n]+(?:\n(?![0-9]+\.).*)*?)(?:["']|(?=\n\n|\n[0-9]+\.))/i);
|
|
1136
|
-
if (welcomeMatch) {
|
|
1137
|
-
configUpdates.welcomeMessage = welcomeMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1138
|
-
}
|
|
1139
|
-
// Extract identity and purpose
|
|
1140
|
-
const identityMatch = input.match(/identity\s*(?:and\s*)?purpose[:\s]*["']?(.+?)(?:["']|(?=\n\n[0-9]+\.|$))/is);
|
|
1141
|
-
if (identityMatch) {
|
|
1142
|
-
configUpdates.identityAndPurpose = identityMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1143
|
-
}
|
|
1144
|
-
// Extract speech characteristics
|
|
1145
|
-
const speechMatch = input.match(/speech\s*characteristics?[:\s]*["']?([^"'\n]+)["']?/i);
|
|
1146
|
-
if (speechMatch) {
|
|
1147
|
-
configUpdates.speechCharacteristics = speechMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1148
|
-
}
|
|
1149
|
-
// Extract hangup instructions
|
|
1150
|
-
const hangupMatch = input.match(/hangup\s*instructions?[:\s]*["']?(.+?)(?:["']|$)/is);
|
|
1151
|
-
if (hangupMatch) {
|
|
1152
|
-
configUpdates.hangupInstructions = hangupMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1153
|
-
}
|
|
1154
|
-
// Update conversationSettings widget
|
|
1155
|
-
const convWidget = widgetMap.get("conversationSettings") || {
|
|
1156
|
-
name: "conversationSettings",
|
|
1157
|
-
type: 39,
|
|
1158
|
-
conversationSettings: {},
|
|
1159
|
-
};
|
|
1160
|
-
const convSettings = (convWidget.conversationSettings ?? {});
|
|
1161
|
-
// Merge config updates
|
|
1162
|
-
for (const [key, value] of Object.entries(configUpdates)) {
|
|
1163
|
-
convSettings[key] = value;
|
|
1164
|
-
}
|
|
1165
|
-
convWidget.conversationSettings = convSettings;
|
|
1166
|
-
widgetMap.set("conversationSettings", convWidget);
|
|
1167
|
-
const mergedProtoConfig = {
|
|
1168
|
-
...existingProtoConfig,
|
|
1169
|
-
widgets: Array.from(widgetMap.values()),
|
|
1170
|
-
};
|
|
1171
|
-
if (!preview) {
|
|
1172
|
-
// Deploy config update only - preserve existing workflow
|
|
1173
|
-
await client.updateAiEmployee({
|
|
1174
|
-
persona_id: personaId,
|
|
1175
|
-
proto_config: mergedProtoConfig,
|
|
1176
|
-
workflow: persona.workflow_def,
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
return {
|
|
1180
|
-
mode: "config_update",
|
|
1181
|
-
status: preview ? "preview" : "deployed",
|
|
1182
|
-
persona_id: personaId,
|
|
1183
|
-
persona_name: persona.name,
|
|
1184
|
-
config_updates: configUpdates,
|
|
1185
|
-
note: "Updated persona configuration only. Workflow unchanged.",
|
|
1186
|
-
hint: "To modify the workflow itself, describe workflow changes like 'add search node' or 'add HITL before email'.",
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
const existingWorkflow = persona.workflow_def;
|
|
1190
|
-
if (!existingWorkflow) {
|
|
1191
|
-
return {
|
|
1192
|
-
error: `Persona "${persona.name}" has no workflow to modify`,
|
|
1193
|
-
hint: "Provide just input= to create a new workflow (greenfield)",
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
// Intelligently analyze what the user wants to do
|
|
1197
|
-
const changeAnalysis = analyzeModificationRequest(input, existingWorkflow);
|
|
1198
|
-
// Apply the changes to the workflow
|
|
1199
|
-
const modifyResult = applyWorkflowModifications(existingWorkflow, changeAnalysis);
|
|
1200
|
-
// Validate the result
|
|
1201
|
-
const issues = detectWorkflowIssues(modifyResult.workflow);
|
|
1202
|
-
const fixes = issues.length > 0 ? suggestWorkflowFixes(issues) : [];
|
|
1203
|
-
const result = {
|
|
1204
|
-
mode: "modify",
|
|
1205
|
-
status: preview ? "preview" : "deploying",
|
|
1206
|
-
persona_id: personaId,
|
|
1207
|
-
persona_name: persona.name,
|
|
1208
|
-
// What we understood from the request
|
|
1209
|
-
analysis: {
|
|
1210
|
-
understood_intent: changeAnalysis.intent,
|
|
1211
|
-
operations: changeAnalysis.operations.map(op => ({
|
|
1212
|
-
type: op.type,
|
|
1213
|
-
description: op.description,
|
|
1214
|
-
})),
|
|
1215
|
-
},
|
|
1216
|
-
// What we did
|
|
1217
|
-
changes_applied: modifyResult.changesApplied,
|
|
1218
|
-
nodes_added: modifyResult.nodesAdded,
|
|
1219
|
-
nodes_removed: modifyResult.nodesRemoved,
|
|
1220
|
-
nodes_modified: modifyResult.nodesModified,
|
|
1221
|
-
connections_changed: modifyResult.connectionsChanged,
|
|
1222
|
-
// The result
|
|
1223
|
-
modified_workflow: modifyResult.workflow,
|
|
1224
|
-
};
|
|
1225
|
-
// Add contextual tip when no changes were made (likely a content change request)
|
|
1226
|
-
if (changeAnalysis.operations.length === 0 || modifyResult.changesApplied.length === 0) {
|
|
1227
|
-
result._tip = "No structural changes detected. If you're trying to change DATA inside nodes " +
|
|
1228
|
-
"(fixed_response content, call_llm prompts), use this approach instead: " +
|
|
1229
|
-
"1) persona(id='...', include_workflow=true) to get workflow_def, " +
|
|
1230
|
-
"2) Edit the node's inputs.data.stringValue in the JSON, " +
|
|
1231
|
-
"3) workflow(mode='deploy', persona_id='...', workflow_def={edited})";
|
|
1232
|
-
result._content_change_hint = {
|
|
1233
|
-
step1: `persona(id="${personaId}", include_workflow=true)`,
|
|
1234
|
-
step2: "Edit workflow_def.actions[N].inputs.data.stringValue",
|
|
1235
|
-
step3: `workflow(mode="deploy", persona_id="${personaId}", workflow_def={...})`,
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
if (issues.length > 0) {
|
|
1239
|
-
result.remaining_issues = issues.length;
|
|
1240
|
-
result.issues = issues.slice(0, 5); // Show first 5
|
|
1241
|
-
result.suggested_fixes = fixes.slice(0, 5);
|
|
1242
|
-
}
|
|
1243
|
-
// Deploy if preview=false
|
|
1244
|
-
if (!preview) {
|
|
1245
|
-
const sanitizedWorkflow = sanitizeWorkflowForDeploy(modifyResult.workflow);
|
|
1246
|
-
try {
|
|
1247
|
-
await client.updateAiEmployee({
|
|
1248
|
-
persona_id: personaId,
|
|
1249
|
-
workflow: sanitizedWorkflow,
|
|
1250
|
-
proto_config: args.proto_config || persona.proto_config,
|
|
1251
|
-
});
|
|
1252
|
-
result.status = "deployed";
|
|
1253
|
-
result.deployed = true;
|
|
1254
|
-
result.modified_workflow = sanitizedWorkflow;
|
|
1255
|
-
}
|
|
1256
|
-
catch (err) {
|
|
1257
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1258
|
-
result.deployed = false;
|
|
1259
|
-
result.deploy_error = errorMessage;
|
|
1260
|
-
result.status = "preview_only";
|
|
1261
|
-
result.hint = `Deployment failed: ${errorMessage}. The modified_workflow is ready - you may need to deploy via UI for complex structural changes.`;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
else {
|
|
1265
|
-
result.next_steps = [
|
|
1266
|
-
"Review the changes_applied and modified_workflow",
|
|
1267
|
-
issues.length > 0 ? `Address ${issues.length} remaining issues` : null,
|
|
1268
|
-
`Deploy with: workflow(persona_id="${personaId}", input="...", preview=false)`,
|
|
1269
|
-
].filter(Boolean);
|
|
1270
|
-
}
|
|
1271
|
-
return result;
|
|
1272
|
-
}
|
|
1273
|
-
case "analyze": {
|
|
1274
|
-
let workflow = workflowDef;
|
|
1275
|
-
let persona = null;
|
|
1276
|
-
if (personaId && !workflow) {
|
|
1277
|
-
persona = await client.getPersonaById(personaId);
|
|
1278
|
-
if (!persona) {
|
|
1279
|
-
return { error: `Persona not found: ${personaId}` };
|
|
1280
|
-
}
|
|
1281
|
-
workflow = persona.workflow_def;
|
|
1282
|
-
}
|
|
1283
|
-
if (!workflow) {
|
|
1284
|
-
return { error: "No workflow to analyze. Provide workflow_def or persona_id." };
|
|
1285
|
-
}
|
|
1286
|
-
// Determine what to include (now supports execution_flow!)
|
|
1287
|
-
const include = args.include || ["issues", "connections", "fixes", "metrics"];
|
|
1288
|
-
const result = {
|
|
1289
|
-
mode: "analyze",
|
|
1290
|
-
persona_id: personaId,
|
|
1291
|
-
persona_name: persona?.name,
|
|
1292
|
-
environment: "demo",
|
|
1293
|
-
};
|
|
1294
|
-
if (include.includes("issues") || include.includes("fixes")) {
|
|
1295
|
-
const issues = detectWorkflowIssues(workflow);
|
|
1296
|
-
if (include.includes("issues")) {
|
|
1297
|
-
result.issues = issues;
|
|
1298
|
-
}
|
|
1299
|
-
if (include.includes("fixes")) {
|
|
1300
|
-
result.fixes = suggestWorkflowFixes(issues);
|
|
1301
|
-
}
|
|
1302
|
-
result.issue_summary = {
|
|
1303
|
-
total: issues.length,
|
|
1304
|
-
critical: issues.filter((i) => i.severity === "critical").length,
|
|
1305
|
-
warning: issues.filter((i) => i.severity === "warning").length,
|
|
1306
|
-
info: issues.filter((i) => i.severity === "info").length,
|
|
1307
|
-
};
|
|
1308
|
-
result.validation_passed = issues.filter((i) => i.severity === "critical").length === 0;
|
|
1309
|
-
}
|
|
1310
|
-
if (include.includes("connections")) {
|
|
1311
|
-
result.connections = validateWorkflowConnections(workflow);
|
|
1312
|
-
}
|
|
1313
|
-
if (include.includes("metrics")) {
|
|
1314
|
-
result.metrics = calculateMetrics(workflow);
|
|
1315
|
-
}
|
|
1316
|
-
// NEW: Execution flow analysis - detects loops, multiple responders, redundant classifiers
|
|
1317
|
-
if (include.includes("execution_flow")) {
|
|
1318
|
-
const execAnalysis = analyzeExecutionFlow(workflow);
|
|
1319
|
-
result.execution_flow = {
|
|
1320
|
-
summary: execAnalysis.summary,
|
|
1321
|
-
loops: execAnalysis.loops,
|
|
1322
|
-
multiple_responder_issues: execAnalysis.multipleResponderIssues,
|
|
1323
|
-
redundant_classifiers: execAnalysis.redundantClassifiers,
|
|
1324
|
-
data_flow_issues: execAnalysis.dataFlowIssues,
|
|
1325
|
-
dead_code_paths: execAnalysis.deadCodePaths,
|
|
1326
|
-
};
|
|
1327
|
-
// Include ASCII visualization if requested
|
|
1328
|
-
if (args.visualize) {
|
|
1329
|
-
result.execution_flow_ascii = generateASCIIFlow(execAnalysis);
|
|
1330
|
-
}
|
|
1331
|
-
// Add specific warnings for triple response risk
|
|
1332
|
-
if (execAnalysis.summary.mayRepeatResponses) {
|
|
1333
|
-
result.triple_response_warning = "⚠️ This workflow may cause duplicate/triple responses due to ungated parallel responders";
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
if (persona) {
|
|
1337
|
-
result.persona = { id: persona.id, name: persona.name };
|
|
1338
|
-
}
|
|
1339
|
-
// Always provide optimization guidance
|
|
1340
|
-
const issues = result.issues;
|
|
1341
|
-
const criticalCount = issues?.filter(i => i.severity === "critical").length ?? 0;
|
|
1342
|
-
const warningCount = issues?.filter(i => i.severity === "warning").length ?? 0;
|
|
1343
|
-
// AUTO-FIX: If fix=true, apply fixes and deploy
|
|
1344
|
-
if (args.fix && personaId && persona && workflow) {
|
|
1345
|
-
const fixResult = autoFixWorkflow(workflow);
|
|
1346
|
-
result.fixes_applied = fixResult.fixesApplied;
|
|
1347
|
-
result.fixes_warnings = fixResult.warnings;
|
|
1348
|
-
if (fixResult.fixesApplied.length > 0 && fixResult.success) {
|
|
1349
|
-
// Deploy the fixed workflow
|
|
1350
|
-
try {
|
|
1351
|
-
const fixedWorkflow = fixResult.workflowDef;
|
|
1352
|
-
await client.updateAiEmployee({
|
|
1353
|
-
persona_id: personaId,
|
|
1354
|
-
workflow: fixedWorkflow,
|
|
1355
|
-
proto_config: persona.proto_config,
|
|
1356
|
-
});
|
|
1357
|
-
result.fix_status = "deployed";
|
|
1358
|
-
result.fixes_deployed = fixResult.fixesApplied.length;
|
|
1359
|
-
result.status = `✅ Applied ${fixResult.fixesApplied.length} fixes and deployed`;
|
|
1360
|
-
}
|
|
1361
|
-
catch (deployErr) {
|
|
1362
|
-
result.fix_status = "failed";
|
|
1363
|
-
result.fix_error = deployErr instanceof Error ? deployErr.message : String(deployErr);
|
|
1364
|
-
result.status = `⚠️ Fixes applied but deploy failed: ${result.fix_error}`;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
else if (fixResult.fixesApplied.length === 0) {
|
|
1368
|
-
result.fix_status = "no_fixes_needed";
|
|
1369
|
-
result.status = "✅ No auto-fixable issues found";
|
|
1370
|
-
}
|
|
1371
|
-
else {
|
|
1372
|
-
result.fix_status = "failed";
|
|
1373
|
-
result.status = "⚠️ Fix generation failed";
|
|
1374
|
-
}
|
|
1375
|
-
return result;
|
|
1376
|
-
}
|
|
1377
|
-
if (criticalCount > 0 || warningCount > 0) {
|
|
1378
|
-
result.optimization_suggestion = `This workflow has ${criticalCount} critical and ${warningCount} warning issues. ` +
|
|
1379
|
-
`Use workflow(persona_id="${personaId ?? 'ID'}", optimize=true) to auto-fix.`;
|
|
1380
|
-
}
|
|
1381
|
-
else {
|
|
1382
|
-
result.status = "✅ Workflow is healthy - no issues detected";
|
|
1383
|
-
}
|
|
1384
|
-
return result;
|
|
1385
|
-
}
|
|
1386
|
-
case "deploy": {
|
|
1387
|
-
// DEPLOY: Direct workflow deployment to existing persona (no input required)
|
|
1388
|
-
if (!personaId) {
|
|
1389
|
-
return { error: "persona_id required for deploy mode" };
|
|
1390
|
-
}
|
|
1391
|
-
if (!workflowDef) {
|
|
1392
|
-
return { error: "workflow_def required for deploy mode" };
|
|
1393
|
-
}
|
|
1394
|
-
const persona = await client.getPersonaById(personaId);
|
|
1395
|
-
if (!persona) {
|
|
1396
|
-
return { error: `Persona not found: ${personaId}` };
|
|
1397
|
-
}
|
|
1398
|
-
// Sanitize workflow before deployment
|
|
1399
|
-
const sanitizedWorkflow = sanitizeWorkflowForDeploy(workflowDef);
|
|
1400
|
-
// Determine proto_config to use: provided > existing
|
|
1401
|
-
const existingProtoConfig = persona.proto_config;
|
|
1402
|
-
const providedProtoConfig = args.proto_config;
|
|
1403
|
-
const protoConfigToUse = providedProtoConfig || existingProtoConfig || {};
|
|
1404
|
-
// Check if this is a voice persona and warn if critical settings are missing
|
|
1405
|
-
const projectSettings = protoConfigToUse.projectSettings;
|
|
1406
|
-
const isVoice = projectSettings?.projectType === 5;
|
|
1407
|
-
const warnings = [];
|
|
1408
|
-
if (isVoice && !providedProtoConfig) {
|
|
1409
|
-
// Check if existing proto_config has conversationSettings populated
|
|
1410
|
-
const widgets = (protoConfigToUse.widgets ?? []);
|
|
1411
|
-
const convSettings = widgets.find(w => w.name === "conversationSettings");
|
|
1412
|
-
const convConfig = convSettings?.conversationSettings;
|
|
1413
|
-
if (!convConfig?.welcomeMessage || !convConfig?.identityAndPurpose) {
|
|
1414
|
-
warnings.push("Voice persona detected but proto_config not provided. Voice settings (welcomeMessage, identityAndPurpose, etc.) may be empty or generic. " +
|
|
1415
|
-
"If you generated the workflow with workflow(input=...), pass the generated proto_config along with workflow_def to preserve voice settings.");
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
// Deploy workflow (always deploy, preview is not applicable for explicit deploy mode)
|
|
1419
|
-
try {
|
|
1420
|
-
await client.updateAiEmployee({
|
|
1421
|
-
persona_id: personaId,
|
|
1422
|
-
workflow: sanitizedWorkflow,
|
|
1423
|
-
proto_config: protoConfigToUse,
|
|
1424
|
-
});
|
|
1425
|
-
const result = {
|
|
1426
|
-
mode: "deploy",
|
|
1427
|
-
status: "deployed",
|
|
1428
|
-
persona_id: personaId,
|
|
1429
|
-
persona_name: persona.name,
|
|
1430
|
-
workflow_deployed: true,
|
|
1431
|
-
};
|
|
1432
|
-
if (warnings.length > 0) {
|
|
1433
|
-
result.warnings = warnings;
|
|
1434
|
-
}
|
|
1435
|
-
return result;
|
|
1436
|
-
}
|
|
1437
|
-
catch (err) {
|
|
1438
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1439
|
-
return {
|
|
1440
|
-
mode: "deploy",
|
|
1441
|
-
status: "failed",
|
|
1442
|
-
persona_id: personaId,
|
|
1443
|
-
persona_name: persona.name,
|
|
1444
|
-
error: errorMessage,
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
case "compare": {
|
|
1449
|
-
if (!personaId || !args.compare_to) {
|
|
1450
|
-
return { error: "persona_id and compare_to required for compare mode" };
|
|
1451
|
-
}
|
|
1452
|
-
const persona1 = await client.getPersonaById(personaId);
|
|
1453
|
-
const persona2 = await client.getPersonaById(args.compare_to);
|
|
1454
|
-
if (!persona1 || !persona2) {
|
|
1455
|
-
return { error: "One or both personas not found" };
|
|
1456
|
-
}
|
|
1457
|
-
const workflow1 = persona1.workflow_def;
|
|
1458
|
-
const workflow2 = persona2.workflow_def;
|
|
1459
|
-
return {
|
|
1460
|
-
persona_1: { id: persona1.id, name: persona1.name },
|
|
1461
|
-
persona_2: { id: persona2.id, name: persona2.name },
|
|
1462
|
-
differences: compareWorkflows(workflow1, workflow2),
|
|
1463
|
-
};
|
|
1464
|
-
}
|
|
1465
|
-
case "compile": {
|
|
1466
|
-
const nodes = args.nodes;
|
|
1467
|
-
const resultMappings = args.result_mappings;
|
|
1468
|
-
if (!nodes) {
|
|
1469
|
-
return { error: "nodes required for compile mode" };
|
|
1470
|
-
}
|
|
1471
|
-
// Load action registry for API-driven action versions/namespaces
|
|
1472
|
-
const compileRegistry = await ensureActionRegistry(client);
|
|
1473
|
-
const spec = {
|
|
1474
|
-
name: args.name || "Compiled Workflow",
|
|
1475
|
-
description: args.description || "Generated workflow",
|
|
1476
|
-
persona_type: args.type || "chat",
|
|
1477
|
-
nodes: nodes,
|
|
1478
|
-
result_mappings: resultMappings || [],
|
|
1479
|
-
};
|
|
1480
|
-
const compiled = compileWorkflow(spec, { registry: compileRegistry });
|
|
1481
|
-
return {
|
|
1482
|
-
workflow_def: compiled.workflow_def,
|
|
1483
|
-
proto_config: compiled.proto_config,
|
|
1484
|
-
registry_loaded: compileRegistry.isLoaded(),
|
|
1485
|
-
};
|
|
1486
|
-
}
|
|
1487
|
-
case "optimize": {
|
|
1488
|
-
// FIX issues in existing workflow (not extend - that's mode="extend")
|
|
1489
|
-
if (!personaId) {
|
|
1490
|
-
return { error: "persona_id required for optimize mode" };
|
|
1491
|
-
}
|
|
1492
|
-
const persona = await client.getPersonaById(personaId);
|
|
1493
|
-
if (!persona) {
|
|
1494
|
-
return { error: `Persona not found: ${personaId}` };
|
|
1495
|
-
}
|
|
1496
|
-
const existingWorkflow = persona.workflow_def;
|
|
1497
|
-
if (!existingWorkflow) {
|
|
1498
|
-
return {
|
|
1499
|
-
error: `Persona "${persona.name}" has no workflow to optimize`,
|
|
1500
|
-
hint: "Use mode='generate' to create a workflow first",
|
|
1501
|
-
};
|
|
1502
|
-
}
|
|
1503
|
-
// Analyze and detect issues
|
|
1504
|
-
const issues = detectWorkflowIssues(existingWorkflow);
|
|
1505
|
-
const fixes = suggestWorkflowFixes(issues);
|
|
1506
|
-
if (issues.length === 0) {
|
|
1507
|
-
return {
|
|
1508
|
-
mode: "optimize",
|
|
1509
|
-
status: "✅ No issues found",
|
|
1510
|
-
persona_id: personaId,
|
|
1511
|
-
persona_name: persona.name,
|
|
1512
|
-
workflow_healthy: true,
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
|
-
// Apply fixes
|
|
1516
|
-
const fixedWorkflow = applySimpleFixes(existingWorkflow, issues);
|
|
1517
|
-
const result = {
|
|
1518
|
-
mode: "optimize",
|
|
1519
|
-
status: preview ? "preview" : "deployed",
|
|
1520
|
-
persona_id: personaId,
|
|
1521
|
-
persona_name: persona.name,
|
|
1522
|
-
issues_found: issues.length,
|
|
1523
|
-
issues: issues.map(i => ({
|
|
1524
|
-
type: i.type,
|
|
1525
|
-
severity: i.severity,
|
|
1526
|
-
reason: i.reason,
|
|
1527
|
-
})),
|
|
1528
|
-
suggested_fixes: fixes,
|
|
1529
|
-
fixed_workflow: fixedWorkflow,
|
|
1530
|
-
};
|
|
1531
|
-
// If preview=false, deploy the fixed workflow
|
|
1532
|
-
if (!preview) {
|
|
1533
|
-
await client.updateAiEmployee({
|
|
1534
|
-
persona_id: personaId,
|
|
1535
|
-
workflow: fixedWorkflow,
|
|
1536
|
-
proto_config: args.proto_config || persona.proto_config,
|
|
1537
|
-
});
|
|
1538
|
-
result.deployed = true;
|
|
1539
|
-
}
|
|
1540
|
-
else {
|
|
1541
|
-
result.next_steps = [
|
|
1542
|
-
"Review the suggested fixes and fixed_workflow",
|
|
1543
|
-
`Deploy with: workflow(mode="optimize", persona_id="${personaId}", preview=false)`,
|
|
1544
|
-
];
|
|
1545
|
-
}
|
|
1546
|
-
return result;
|
|
327
|
+
return handleVersion(effectiveMode, args, client, persona, versionContext);
|
|
1547
328
|
}
|
|
1548
329
|
default:
|
|
1549
330
|
return { error: `Unknown mode: ${effectiveMode}` };
|
|
1550
331
|
}
|
|
1551
332
|
}
|
|
1552
333
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1553
|
-
//
|
|
1554
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1555
|
-
// Delegate to extracted handler
|
|
1556
|
-
export const handleAction = handleActionExtracted;
|
|
1557
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1558
|
-
// TEMPLATE Handler
|
|
1559
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1560
|
-
// Delegate to extracted handler
|
|
1561
|
-
export const handleTemplate = handleTemplateExtracted;
|
|
1562
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1563
|
-
// KNOWLEDGE Handler
|
|
1564
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1565
|
-
// Delegate to extracted handler
|
|
1566
|
-
export const handleKnowledge = handleKnowledgeExtracted;
|
|
1567
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1568
|
-
// REFERENCE Handler (v2 unified: envs, actions, templates, patterns, concepts, guidance)
|
|
1569
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1570
|
-
// Delegate to extracted handler
|
|
1571
|
-
export const handleReference = handleReferenceExtracted;
|
|
334
|
+
// WORKFLOW Handler - REMOVED
|
|
1572
335
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1573
|
-
//
|
|
336
|
+
// The handleWorkflow function has been fully extracted to handlers/workflow/
|
|
337
|
+
// Import via: import { handleWorkflow } from "./handlers/workflow/index.js"
|
|
338
|
+
//
|
|
339
|
+
// Modular structure:
|
|
340
|
+
// - handlers/workflow/analyze.ts
|
|
341
|
+
// - handlers/workflow/compare.ts
|
|
342
|
+
// - handlers/workflow/compile.ts
|
|
343
|
+
// - handlers/workflow/deploy.ts
|
|
344
|
+
// - handlers/workflow/generate.ts
|
|
345
|
+
// - handlers/workflow/modify.ts
|
|
346
|
+
// - handlers/workflow/optimize.ts
|
|
347
|
+
// - handlers/workflow/index.ts (router)
|
|
1574
348
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1575
|
-
export
|
|
1576
|
-
|
|
1577
|
-
const personaId = args.persona_id;
|
|
1578
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1579
|
-
// Clarification when mode is omitted
|
|
1580
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1581
|
-
if (!mode) {
|
|
1582
|
-
// Try to infer from params if unambiguous
|
|
1583
|
-
if (args.file && !args.file_id) {
|
|
1584
|
-
// Has file path but no file_id → likely wants upload
|
|
1585
|
-
return {
|
|
1586
|
-
status: "clarification_needed",
|
|
1587
|
-
message: "What operation would you like to perform?",
|
|
1588
|
-
inferred: "upload",
|
|
1589
|
-
hint: "You provided a file path. Did you mean mode='upload'?",
|
|
1590
|
-
options: ["upload", "list", "generate", "embedding"],
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
if (args.file_id && args.input) {
|
|
1594
|
-
// Has file_id and input → likely wants modify
|
|
1595
|
-
return {
|
|
1596
|
-
status: "clarification_needed",
|
|
1597
|
-
message: "What operation would you like to perform?",
|
|
1598
|
-
inferred: "modify",
|
|
1599
|
-
hint: "You provided file_id and input. Did you mean mode='modify'?",
|
|
1600
|
-
options: ["modify", "delete", "get"],
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
if (args.input || args.from) {
|
|
1604
|
-
// Has input or from → likely wants generate
|
|
1605
|
-
return {
|
|
1606
|
-
status: "clarification_needed",
|
|
1607
|
-
message: "What operation would you like to perform?",
|
|
1608
|
-
inferred: "generate",
|
|
1609
|
-
hint: "You provided input/from. Did you mean mode='generate'?",
|
|
1610
|
-
options: ["generate", "list"],
|
|
1611
|
-
};
|
|
1612
|
-
}
|
|
1613
|
-
if (args.template) {
|
|
1614
|
-
// Wants a specific template
|
|
1615
|
-
return {
|
|
1616
|
-
status: "clarification_needed",
|
|
1617
|
-
message: "What operation would you like to perform?",
|
|
1618
|
-
inferred: "templates",
|
|
1619
|
-
hint: "You provided template name. Did you mean mode='templates'?",
|
|
1620
|
-
options: ["templates"],
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
// Default clarification
|
|
1624
|
-
return {
|
|
1625
|
-
status: "clarification_needed",
|
|
1626
|
-
message: "What operation would you like to perform?",
|
|
1627
|
-
options: ["list", "upload", "generate", "modify", "sanitize", "delete", "embedding", "templates"],
|
|
1628
|
-
hint: "Specify mode='...' for direct execution. Example: data(persona_id='abc', mode='list')",
|
|
1629
|
-
};
|
|
1630
|
-
}
|
|
1631
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1632
|
-
// Templates mode - list or get generation templates
|
|
1633
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1634
|
-
if (mode === "templates") {
|
|
1635
|
-
const templateName = args.template;
|
|
1636
|
-
if (templateName) {
|
|
1637
|
-
const template = GENERATION_TEMPLATES[templateName];
|
|
1638
|
-
if (!template) {
|
|
1639
|
-
return {
|
|
1640
|
-
error: `Template not found: ${templateName}`,
|
|
1641
|
-
available: Object.keys(GENERATION_TEMPLATES),
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
return {
|
|
1645
|
-
name: templateName,
|
|
1646
|
-
...template,
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
// List all templates
|
|
1650
|
-
return {
|
|
1651
|
-
templates: Object.entries(GENERATION_TEMPLATES).map(([name, t]) => ({
|
|
1652
|
-
name,
|
|
1653
|
-
description: t.description,
|
|
1654
|
-
category: t.category,
|
|
1655
|
-
})),
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1659
|
-
// Generate mode - create new content via Document Generation API
|
|
1660
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1661
|
-
if (mode === "generate") {
|
|
1662
|
-
if (!personaId) {
|
|
1663
|
-
return { error: "persona_id is required for generate mode" };
|
|
1664
|
-
}
|
|
1665
|
-
const from = args.from;
|
|
1666
|
-
const input = args.input;
|
|
1667
|
-
const count = args.count ?? 1;
|
|
1668
|
-
const format = args.format ?? "markdown";
|
|
1669
|
-
const data = args.data;
|
|
1670
|
-
if (!from && !input) {
|
|
1671
|
-
return {
|
|
1672
|
-
error: "Either 'from' (template name) or 'input' (natural language) is required for generate mode",
|
|
1673
|
-
available_templates: Object.keys(GENERATION_TEMPLATES),
|
|
1674
|
-
};
|
|
1675
|
-
}
|
|
1676
|
-
// Build instructions from template or input
|
|
1677
|
-
let instructions;
|
|
1678
|
-
if (from) {
|
|
1679
|
-
const template = GENERATION_TEMPLATES[from];
|
|
1680
|
-
if (!template) {
|
|
1681
|
-
return {
|
|
1682
|
-
error: `Template not found: ${from}`,
|
|
1683
|
-
available: Object.keys(GENERATION_TEMPLATES),
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
// Build instructions from template
|
|
1687
|
-
instructions = template.prompt
|
|
1688
|
-
.replace("{count}", String(count))
|
|
1689
|
-
.replace("{format}", format);
|
|
1690
|
-
if (data) {
|
|
1691
|
-
// Inject additional context
|
|
1692
|
-
instructions += `\n\nAdditional context: ${JSON.stringify(data)}`;
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
else {
|
|
1696
|
-
instructions = input;
|
|
1697
|
-
if (count > 1) {
|
|
1698
|
-
instructions = `Generate ${count} items: ${instructions}`;
|
|
1699
|
-
}
|
|
1700
|
-
instructions += `\n\nOutput format: ${format}`;
|
|
1701
|
-
}
|
|
1702
|
-
// Call Document Generation API
|
|
1703
|
-
try {
|
|
1704
|
-
const result = await client.generateDocument(personaId, instructions, {
|
|
1705
|
-
timeoutMs: 120_000,
|
|
1706
|
-
pollIntervalMs: 2_000,
|
|
1707
|
-
});
|
|
1708
|
-
return {
|
|
1709
|
-
success: true,
|
|
1710
|
-
document_id: result.document_id,
|
|
1711
|
-
project_id: result.project_id,
|
|
1712
|
-
status: result.status,
|
|
1713
|
-
content: result.content,
|
|
1714
|
-
format,
|
|
1715
|
-
template: from,
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
catch (err) {
|
|
1719
|
-
const error = err;
|
|
1720
|
-
return {
|
|
1721
|
-
error: `Document generation failed: ${error.message}`,
|
|
1722
|
-
template: from,
|
|
1723
|
-
instructions_length: instructions.length,
|
|
1724
|
-
};
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1728
|
-
// Modify mode - modify existing document
|
|
1729
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1730
|
-
if (mode === "modify") {
|
|
1731
|
-
if (!personaId) {
|
|
1732
|
-
return { error: "persona_id is required for modify mode" };
|
|
1733
|
-
}
|
|
1734
|
-
const fileId = args.file_id;
|
|
1735
|
-
const input = args.input;
|
|
1736
|
-
if (!fileId) {
|
|
1737
|
-
return { error: "file_id is required for modify mode" };
|
|
1738
|
-
}
|
|
1739
|
-
if (!input) {
|
|
1740
|
-
return { error: "input (modification instructions) is required for modify mode" };
|
|
1741
|
-
}
|
|
1742
|
-
// For now, regenerate uses the Document Generation API's regenerate endpoint
|
|
1743
|
-
// This requires document_id and project_id - we'd need to track these
|
|
1744
|
-
// For MVP, return guidance
|
|
1745
|
-
return {
|
|
1746
|
-
status: "not_yet_implemented",
|
|
1747
|
-
message: "Document modification via regenerate API requires document_id and project_id from the original generation.",
|
|
1748
|
-
hint: "Use mode='generate' to create new content, or manually edit the file.",
|
|
1749
|
-
file_id: fileId,
|
|
1750
|
-
instructions: input,
|
|
1751
|
-
};
|
|
1752
|
-
}
|
|
1753
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1754
|
-
// Get mode - get specific file details
|
|
1755
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1756
|
-
if (mode === "get") {
|
|
1757
|
-
if (!personaId) {
|
|
1758
|
-
return { error: "persona_id is required for get mode" };
|
|
1759
|
-
}
|
|
1760
|
-
const fileId = args.file_id;
|
|
1761
|
-
if (!fileId) {
|
|
1762
|
-
return { error: "file_id is required for get mode" };
|
|
1763
|
-
}
|
|
1764
|
-
// List files and filter to the specific one
|
|
1765
|
-
const files = await client.listDataSourceFiles(personaId, { limit: 1000 });
|
|
1766
|
-
const file = files.files.find(f => f.id === fileId);
|
|
1767
|
-
if (!file) {
|
|
1768
|
-
return { error: `File not found: ${fileId}` };
|
|
1769
|
-
}
|
|
1770
|
-
return {
|
|
1771
|
-
file,
|
|
1772
|
-
persona_id: personaId,
|
|
1773
|
-
};
|
|
1774
|
-
}
|
|
1775
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1776
|
-
// Sanitize mode - existing implementation
|
|
1777
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1778
|
-
if (mode === "sanitize") {
|
|
1779
|
-
// Data sanitization - needs special handling
|
|
1780
|
-
const personaId = args.persona_id;
|
|
1781
|
-
const includeWorkflow = args.include_workflow;
|
|
1782
|
-
const sanitizeExamples = args.sanitize_examples;
|
|
1783
|
-
const persona = await client.getPersonaById(personaId);
|
|
1784
|
-
if (!persona) {
|
|
1785
|
-
return { error: `Persona not found: ${personaId}` };
|
|
1786
|
-
}
|
|
1787
|
-
// Create sanitization session
|
|
1788
|
-
const session = new SanitizationSession();
|
|
1789
|
-
// Detect and sanitize in proto_config
|
|
1790
|
-
const protoConfig = persona.proto_config;
|
|
1791
|
-
let sanitizedConfig = protoConfig;
|
|
1792
|
-
if (protoConfig) {
|
|
1793
|
-
// Apply pattern-based detection and sanitization
|
|
1794
|
-
const clonedConfig = JSON.parse(JSON.stringify(protoConfig)); // Deep clone
|
|
1795
|
-
// Recursively find and sanitize string values
|
|
1796
|
-
const sanitizeObject = (obj) => {
|
|
1797
|
-
for (const key of Object.keys(obj)) {
|
|
1798
|
-
const val = obj[key];
|
|
1799
|
-
if (typeof val === "string" && val.length > 0) {
|
|
1800
|
-
const detected = detectWithPatterns(val);
|
|
1801
|
-
for (const entity of detected) {
|
|
1802
|
-
const replacement = session.getOrCreateReplacement(entity.value, entity.type);
|
|
1803
|
-
obj[key] = obj[key].split(entity.value).join(replacement);
|
|
1804
|
-
}
|
|
1805
|
-
// Also apply user examples
|
|
1806
|
-
if (sanitizeExamples) {
|
|
1807
|
-
for (const example of sanitizeExamples) {
|
|
1808
|
-
if (obj[key].includes(example)) {
|
|
1809
|
-
const replacement = session.getOrCreateReplacement(example, "unknown");
|
|
1810
|
-
obj[key] = obj[key].split(example).join(replacement);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
else if (typeof val === "object" && val !== null) {
|
|
1816
|
-
sanitizeObject(val);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
};
|
|
1820
|
-
sanitizeObject(clonedConfig);
|
|
1821
|
-
sanitizedConfig = clonedConfig;
|
|
1822
|
-
}
|
|
1823
|
-
// Optionally sanitize workflow
|
|
1824
|
-
let sanitizedWorkflow = persona.workflow_def;
|
|
1825
|
-
if (includeWorkflow && sanitizedWorkflow) {
|
|
1826
|
-
const clonedWorkflow = JSON.parse(JSON.stringify(sanitizedWorkflow));
|
|
1827
|
-
const sanitizeWfObj = (obj) => {
|
|
1828
|
-
for (const key of Object.keys(obj)) {
|
|
1829
|
-
const val = obj[key];
|
|
1830
|
-
if (typeof val === "string" && val.length > 0) {
|
|
1831
|
-
const detected = detectWithPatterns(val);
|
|
1832
|
-
for (const entity of detected) {
|
|
1833
|
-
const replacement = session.getOrCreateReplacement(entity.value, entity.type);
|
|
1834
|
-
obj[key] = obj[key].split(entity.value).join(replacement);
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
else if (typeof val === "object" && val !== null) {
|
|
1838
|
-
sanitizeWfObj(val);
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
};
|
|
1842
|
-
sanitizeWfObj(clonedWorkflow);
|
|
1843
|
-
sanitizedWorkflow = clonedWorkflow;
|
|
1844
|
-
}
|
|
1845
|
-
// Apply updates
|
|
1846
|
-
await client.updateAiEmployee({
|
|
1847
|
-
persona_id: personaId,
|
|
1848
|
-
proto_config: sanitizedConfig ?? {},
|
|
1849
|
-
workflow: includeWorkflow ? sanitizedWorkflow : undefined,
|
|
1850
|
-
});
|
|
1851
|
-
// Convert Map to array for JSON response
|
|
1852
|
-
const mappingsMap = session.getMappings();
|
|
1853
|
-
const mappingsArray = Array.from(mappingsMap.entries()).map(([original, replacement]) => ({
|
|
1854
|
-
original,
|
|
1855
|
-
replacement,
|
|
1856
|
-
}));
|
|
1857
|
-
return {
|
|
1858
|
-
success: true,
|
|
1859
|
-
persona_id: personaId,
|
|
1860
|
-
entities_sanitized: mappingsMap.size,
|
|
1861
|
-
mappings: mappingsArray,
|
|
1862
|
-
workflow_sanitized: !!includeWorkflow,
|
|
1863
|
-
};
|
|
1864
|
-
}
|
|
1865
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1866
|
-
// Embedding mode - status or toggle
|
|
1867
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1868
|
-
if (mode === "embedding") {
|
|
1869
|
-
if (!personaId) {
|
|
1870
|
-
return { error: "persona_id is required for embedding mode" };
|
|
1871
|
-
}
|
|
1872
|
-
const knowledgeArgs = { ...args };
|
|
1873
|
-
if (args.enabled !== undefined) {
|
|
1874
|
-
knowledgeArgs.mode = "toggle";
|
|
1875
|
-
}
|
|
1876
|
-
else {
|
|
1877
|
-
knowledgeArgs.mode = "status";
|
|
1878
|
-
}
|
|
1879
|
-
// Call handleKnowledge without deprecation warning (data is the new interface)
|
|
1880
|
-
return handleKnowledge(knowledgeArgs, client, readFile, { includeDeprecationWarning: false });
|
|
1881
|
-
}
|
|
1882
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1883
|
-
// Replicate mode - copy data by reference (fast, no re-upload)
|
|
1884
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1885
|
-
if (mode === "replicate") {
|
|
1886
|
-
if (!personaId) {
|
|
1887
|
-
return { error: "persona_id is required for replicate mode (this is the TARGET persona)" };
|
|
1888
|
-
}
|
|
1889
|
-
const sourcePersonaId = args.from;
|
|
1890
|
-
if (!sourcePersonaId) {
|
|
1891
|
-
return {
|
|
1892
|
-
error: "from is required for replicate mode (source persona ID)",
|
|
1893
|
-
hint: "Use data(method='replicate', from='source-persona-id') to copy data by reference",
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
// Parse and validate widget mappings if provided
|
|
1897
|
-
const widgetMappingsRaw = args.widget_mappings;
|
|
1898
|
-
let widgetMappings;
|
|
1899
|
-
if (widgetMappingsRaw !== undefined) {
|
|
1900
|
-
// Validate that widget_mappings is an array
|
|
1901
|
-
if (!Array.isArray(widgetMappingsRaw)) {
|
|
1902
|
-
return {
|
|
1903
|
-
error: "widget_mappings must be an array",
|
|
1904
|
-
hint: "Use widget_mappings=[{source_widget: 'kb', target_widget: 'kb'}]",
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
1907
|
-
// Validate each mapping has at least source_widget
|
|
1908
|
-
try {
|
|
1909
|
-
widgetMappings = widgetMappingsRaw.map((m, i) => {
|
|
1910
|
-
if (typeof m !== "object" || m === null) {
|
|
1911
|
-
throw new Error(`widget_mappings[${i}] must be an object`);
|
|
1912
|
-
}
|
|
1913
|
-
const mapping = m;
|
|
1914
|
-
const sourceWidget = mapping.source_widget ?? mapping.sourceWidget;
|
|
1915
|
-
if (!sourceWidget || typeof sourceWidget !== "string") {
|
|
1916
|
-
throw new Error(`widget_mappings[${i}].source_widget is required`);
|
|
1917
|
-
}
|
|
1918
|
-
return {
|
|
1919
|
-
sourceWidget,
|
|
1920
|
-
targetWidget: mapping.target_widget ?? mapping.targetWidget,
|
|
1921
|
-
};
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
catch (validationErr) {
|
|
1925
|
-
return {
|
|
1926
|
-
error: `Invalid widget_mappings: ${validationErr.message}`,
|
|
1927
|
-
hint: "Each mapping must have source_widget (string). target_widget is optional.",
|
|
1928
|
-
};
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
else {
|
|
1932
|
-
// Default: replicate knowledge_base
|
|
1933
|
-
widgetMappings = [{ sourceWidget: "knowledge_base" }];
|
|
1934
|
-
}
|
|
1935
|
-
const shouldWait = args.wait !== false; // Default to true
|
|
1936
|
-
try {
|
|
1937
|
-
// Call replicateData - copies S3 references, not actual files
|
|
1938
|
-
const result = await client.replicateData({ type: "persona", id: sourcePersonaId }, { type: "persona", id: personaId }, widgetMappings);
|
|
1939
|
-
// If wait=true (default), poll for completion
|
|
1940
|
-
if (shouldWait && result.results.length > 0) {
|
|
1941
|
-
const requestIds = result.results.map(r => r.requestId).filter(Boolean);
|
|
1942
|
-
const statuses = [];
|
|
1943
|
-
for (const requestId of requestIds) {
|
|
1944
|
-
try {
|
|
1945
|
-
const status = await client.waitForReplication(requestId, personaId, {
|
|
1946
|
-
timeoutMs: 60_000,
|
|
1947
|
-
pollIntervalMs: 1_000,
|
|
1948
|
-
});
|
|
1949
|
-
statuses.push({ requestId, status: status.status, failedReason: status.failedReason });
|
|
1950
|
-
}
|
|
1951
|
-
catch (pollErr) {
|
|
1952
|
-
statuses.push({ requestId, status: "timeout", failedReason: String(pollErr) });
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
const allCompleted = statuses.every(s => s.status === "completed");
|
|
1956
|
-
const anyFailed = statuses.some(s => s.status === "failed");
|
|
1957
|
-
return {
|
|
1958
|
-
success: allCompleted,
|
|
1959
|
-
source_persona_id: sourcePersonaId,
|
|
1960
|
-
target_persona_id: personaId,
|
|
1961
|
-
replication_results: result.results,
|
|
1962
|
-
final_statuses: statuses,
|
|
1963
|
-
message: allCompleted
|
|
1964
|
-
? "Data replicated successfully (by reference, no files copied)"
|
|
1965
|
-
: anyFailed
|
|
1966
|
-
? "Some replications failed"
|
|
1967
|
-
: "Replication completed with warnings",
|
|
1968
|
-
_tip: "Replication copies S3 references - much faster than downloading and re-uploading files",
|
|
1969
|
-
};
|
|
1970
|
-
}
|
|
1971
|
-
// If wait=false, return immediately with request IDs
|
|
1972
|
-
return {
|
|
1973
|
-
success: true,
|
|
1974
|
-
source_persona_id: sourcePersonaId,
|
|
1975
|
-
target_persona_id: personaId,
|
|
1976
|
-
replication_results: result.results,
|
|
1977
|
-
message: "Replication initiated (async)",
|
|
1978
|
-
_tip: "Use getReplicationStatus with request IDs to check progress",
|
|
1979
|
-
};
|
|
1980
|
-
}
|
|
1981
|
-
catch (err) {
|
|
1982
|
-
const error = err;
|
|
1983
|
-
return {
|
|
1984
|
-
error: `Replication failed: ${error.message}`,
|
|
1985
|
-
source_persona_id: sourcePersonaId,
|
|
1986
|
-
target_persona_id: personaId,
|
|
1987
|
-
hint: "Ensure both personas exist and have compatible data structures",
|
|
1988
|
-
};
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1992
|
-
// Stats mode - get file counts by status
|
|
1993
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1994
|
-
if (mode === "stats") {
|
|
1995
|
-
if (!personaId) {
|
|
1996
|
-
return { error: "persona_id is required for stats mode" };
|
|
1997
|
-
}
|
|
1998
|
-
const widgetName = args.widget_name;
|
|
1999
|
-
try {
|
|
2000
|
-
const aggregates = await client.getDataSourceAggregates(personaId, widgetName);
|
|
2001
|
-
return {
|
|
2002
|
-
persona_id: personaId,
|
|
2003
|
-
widget_name: aggregates.widgetName ?? widgetName ?? "all",
|
|
2004
|
-
total: aggregates.total,
|
|
2005
|
-
pending: aggregates.pending,
|
|
2006
|
-
success: aggregates.success,
|
|
2007
|
-
failed: aggregates.failed,
|
|
2008
|
-
_tip: "Use data(method='list') to see individual files",
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
catch (err) {
|
|
2012
|
-
const error = err;
|
|
2013
|
-
return {
|
|
2014
|
-
error: `Failed to get data stats: ${error.message}`,
|
|
2015
|
-
persona_id: personaId,
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2020
|
-
// Remaining modes (list, upload, delete) - delegate to knowledge handler
|
|
2021
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2022
|
-
if (!personaId && mode !== "templates") {
|
|
2023
|
-
return { error: "persona_id is required for this operation" };
|
|
2024
|
-
}
|
|
2025
|
-
// Call handleKnowledge without deprecation warning (data is the new interface)
|
|
2026
|
-
return handleKnowledge(args, client, readFile, { includeDeprecationWarning: false });
|
|
2027
|
-
}
|
|
2028
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2029
|
-
// SYNC Handler
|
|
2030
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2031
|
-
// Delegate to extracted handler
|
|
2032
|
-
export const handleSync = handleSyncExtracted;
|
|
2033
|
-
/**
|
|
2034
|
-
* Analyze a natural language modification request and determine what operations are needed.
|
|
2035
|
-
* This is the "smart" part - understanding user intent.
|
|
2036
|
-
*/
|
|
2037
|
-
function analyzeModificationRequest(input, existingWorkflow) {
|
|
2038
|
-
const lowerInput = input.toLowerCase();
|
|
2039
|
-
const operations = [];
|
|
2040
|
-
let intent = "modify workflow";
|
|
2041
|
-
const actions = (existingWorkflow.actions || []);
|
|
2042
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2043
|
-
// CONSOLIDATION: "consolidate X agents into one", "merge", "combine"
|
|
2044
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2045
|
-
if (lowerInput.includes("consolidat") || lowerInput.includes("merge") ||
|
|
2046
|
-
lowerInput.includes("combine") || lowerInput.includes("into one") ||
|
|
2047
|
-
lowerInput.includes("single agent") || lowerInput.includes("unified")) {
|
|
2048
|
-
intent = "consolidate multiple nodes into one";
|
|
2049
|
-
// Find nodes to consolidate - look for patterns like "8 custom agents", "the agents"
|
|
2050
|
-
const customAgents = actions.filter(a => {
|
|
2051
|
-
const actionType = getActionType(a);
|
|
2052
|
-
return actionType === "custom_agent" || actionType.includes("custom");
|
|
2053
|
-
});
|
|
2054
|
-
if (customAgents.length > 1) {
|
|
2055
|
-
operations.push({
|
|
2056
|
-
type: "consolidate",
|
|
2057
|
-
description: `Consolidate ${customAgents.length} custom_agent nodes into a single unified agent`,
|
|
2058
|
-
target_nodes: customAgents.map(a => String(a.name || "")),
|
|
2059
|
-
});
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2063
|
-
// REMOVAL: "remove", "delete", "clean up orphan"
|
|
2064
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2065
|
-
if (lowerInput.includes("remove") || lowerInput.includes("delete") ||
|
|
2066
|
-
lowerInput.includes("clean up") || lowerInput.includes("orphan")) {
|
|
2067
|
-
intent = "remove nodes";
|
|
2068
|
-
// Check if removing orphans specifically
|
|
2069
|
-
if (lowerInput.includes("orphan")) {
|
|
2070
|
-
// Find orphan nodes by checking reachability
|
|
2071
|
-
const orphanNodes = findOrphanNodes(existingWorkflow);
|
|
2072
|
-
if (orphanNodes.length > 0) {
|
|
2073
|
-
operations.push({
|
|
2074
|
-
type: "remove",
|
|
2075
|
-
description: `Remove ${orphanNodes.length} orphan nodes`,
|
|
2076
|
-
target_nodes: orphanNodes,
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
// Check for specific node names mentioned
|
|
2081
|
-
// Strategy 1: Look for "remove the node NODENAME" pattern
|
|
2082
|
-
const explicitNodeMatch = input.match(/remove\s+(?:the\s+)?node\s+["']?([a-zA-Z0-9_]+)["']?/i);
|
|
2083
|
-
if (explicitNodeMatch) {
|
|
2084
|
-
const nodeName = explicitNodeMatch[1];
|
|
2085
|
-
const matchingNodes = actions.filter(a => String(a.name || "").toLowerCase() === nodeName.toLowerCase() ||
|
|
2086
|
-
String(a.name || "").toLowerCase().includes(nodeName.toLowerCase()));
|
|
2087
|
-
if (matchingNodes.length > 0) {
|
|
2088
|
-
operations.push({
|
|
2089
|
-
type: "remove",
|
|
2090
|
-
description: `Remove node(s) matching "${nodeName}"`,
|
|
2091
|
-
target_nodes: matchingNodes.map(a => String(a.name || "")),
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
// Strategy 2: Check if any actual node ID appears in the input text
|
|
2096
|
-
if (operations.length === 0) {
|
|
2097
|
-
const nodeNames = actions.map(a => String(a.name || "")).filter(n => n.length > 0);
|
|
2098
|
-
const foundNodes = nodeNames.filter(nodeName => input.toLowerCase().includes(nodeName.toLowerCase()));
|
|
2099
|
-
if (foundNodes.length > 0) {
|
|
2100
|
-
operations.push({
|
|
2101
|
-
type: "remove",
|
|
2102
|
-
description: `Remove node(s): ${foundNodes.join(", ")}`,
|
|
2103
|
-
target_nodes: foundNodes,
|
|
2104
|
-
});
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
// Strategy 3: Fallback - simple "remove X" pattern (for short names)
|
|
2108
|
-
if (operations.length === 0) {
|
|
2109
|
-
const simpleMatch = input.match(/remove\s+(?:the\s+)?(\w+)/i);
|
|
2110
|
-
if (simpleMatch && simpleMatch[1].toLowerCase() !== "node") {
|
|
2111
|
-
const nodeName = simpleMatch[1];
|
|
2112
|
-
const matchingNodes = actions.filter(a => String(a.name || "").toLowerCase().includes(nodeName.toLowerCase()));
|
|
2113
|
-
if (matchingNodes.length > 0) {
|
|
2114
|
-
operations.push({
|
|
2115
|
-
type: "remove",
|
|
2116
|
-
description: `Remove node(s) matching "${nodeName}"`,
|
|
2117
|
-
target_nodes: matchingNodes.map(a => String(a.name || "")),
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2124
|
-
// REPLACEMENT: "replace X with Y", "swap", "use X instead of Y"
|
|
2125
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2126
|
-
if (lowerInput.includes("replace") || lowerInput.includes("swap") ||
|
|
2127
|
-
lowerInput.includes("instead of") || lowerInput.includes("use entity_extraction")) {
|
|
2128
|
-
intent = "replace nodes";
|
|
2129
|
-
// Parse "replace X with Y" pattern
|
|
2130
|
-
const replaceMatch = input.match(/replace\s+(?:the\s+)?(.+?)\s+with\s+(.+)/i);
|
|
2131
|
-
if (replaceMatch) {
|
|
2132
|
-
operations.push({
|
|
2133
|
-
type: "replace",
|
|
2134
|
-
description: `Replace ${replaceMatch[1]} with ${replaceMatch[2]}`,
|
|
2135
|
-
source: replaceMatch[1],
|
|
2136
|
-
target: replaceMatch[2],
|
|
2137
|
-
});
|
|
2138
|
-
}
|
|
2139
|
-
// Special case: "use entity_extraction" suggests replacing LLM-based extraction
|
|
2140
|
-
if (lowerInput.includes("entity_extraction")) {
|
|
2141
|
-
const llmNodes = actions.filter(a => {
|
|
2142
|
-
const actionType = getActionType(a);
|
|
2143
|
-
const displayName = a.displaySettings?.displayName || "";
|
|
2144
|
-
return (actionType === "call_llm" &&
|
|
2145
|
-
(displayName.toLowerCase().includes("email") ||
|
|
2146
|
-
displayName.toLowerCase().includes("extract")));
|
|
2147
|
-
});
|
|
2148
|
-
if (llmNodes.length > 0) {
|
|
2149
|
-
operations.push({
|
|
2150
|
-
type: "replace",
|
|
2151
|
-
description: `Replace ${llmNodes.length} LLM extraction nodes with entity_extraction`,
|
|
2152
|
-
target_nodes: llmNodes.map(a => String(a.name || "")),
|
|
2153
|
-
});
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2158
|
-
// HITL: "add hitl", "add approval", "confirm before"
|
|
2159
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2160
|
-
if (lowerInput.includes("hitl") || lowerInput.includes("approval") ||
|
|
2161
|
-
lowerInput.includes("confirm") || lowerInput.includes("human")) {
|
|
2162
|
-
intent = "add human-in-the-loop";
|
|
2163
|
-
// Find the action to add HITL before
|
|
2164
|
-
const beforeMatch = input.match(/before\s+(\w+)/i);
|
|
2165
|
-
const targetAction = beforeMatch ? beforeMatch[1] : "email";
|
|
2166
|
-
// Find email/send nodes
|
|
2167
|
-
const emailNodes = actions.filter(a => {
|
|
2168
|
-
const actionType = getActionType(a);
|
|
2169
|
-
const nodeName = String(a.name || "").toLowerCase();
|
|
2170
|
-
return actionType.includes("email") || nodeName.includes("email") ||
|
|
2171
|
-
nodeName.includes("send") || nodeName.includes(targetAction.toLowerCase());
|
|
2172
|
-
});
|
|
2173
|
-
operations.push({
|
|
2174
|
-
type: "add",
|
|
2175
|
-
description: `Add HITL approval before ${emailNodes.length > 0 ? emailNodes[0].name : targetAction}`,
|
|
2176
|
-
new_node: {
|
|
2177
|
-
type: "general_hitl",
|
|
2178
|
-
name: `hitl_approval_${Date.now()}`,
|
|
2179
|
-
before: emailNodes.length > 0 ? String(emailNodes[0].name) : targetAction,
|
|
2180
|
-
},
|
|
2181
|
-
});
|
|
2182
|
-
}
|
|
2183
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2184
|
-
// WIRING: "connect", "wire", "link X to Y"
|
|
2185
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2186
|
-
if (lowerInput.includes("connect") || lowerInput.includes("wire") ||
|
|
2187
|
-
lowerInput.includes("link")) {
|
|
2188
|
-
intent = "rewire connections";
|
|
2189
|
-
// Parse "connect X to Y" pattern
|
|
2190
|
-
const connectMatch = input.match(/(?:connect|wire|link)\s+(\w+)\s+to\s+(\w+)/i);
|
|
2191
|
-
if (connectMatch) {
|
|
2192
|
-
operations.push({
|
|
2193
|
-
type: "rewire",
|
|
2194
|
-
description: `Connect ${connectMatch[1]} to ${connectMatch[2]}`,
|
|
2195
|
-
source: connectMatch[1],
|
|
2196
|
-
target: connectMatch[2],
|
|
2197
|
-
});
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2201
|
-
// ADD: "add", "include", "new"
|
|
2202
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
2203
|
-
if (operations.length === 0 && (lowerInput.includes("add") || lowerInput.includes("new") || lowerInput.includes("include"))) {
|
|
2204
|
-
intent = "add new capabilities";
|
|
2205
|
-
// Use existing parseExtensionInput for additive changes
|
|
2206
|
-
const addChanges = parseExtensionInput(input);
|
|
2207
|
-
for (const change of addChanges) {
|
|
2208
|
-
operations.push({
|
|
2209
|
-
type: "add",
|
|
2210
|
-
description: change.description,
|
|
2211
|
-
config: change.details,
|
|
2212
|
-
});
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
// Determine confidence based on how specific the operations are
|
|
2216
|
-
const confidence = operations.length > 0 && operations.every(op => op.target_nodes?.length || op.new_node) ? "high" :
|
|
2217
|
-
operations.length > 0 ? "medium" : "low";
|
|
2218
|
-
return { intent, operations, confidence };
|
|
2219
|
-
}
|
|
2220
|
-
/**
|
|
2221
|
-
* Find orphan nodes (not reachable from trigger).
|
|
2222
|
-
*/
|
|
2223
|
-
function findOrphanNodes(workflow) {
|
|
2224
|
-
const actions = (workflow.actions || []);
|
|
2225
|
-
const orphans = [];
|
|
2226
|
-
// Build a set of all referenced nodes (nodes that receive input from other nodes)
|
|
2227
|
-
const referencedNodes = new Set();
|
|
2228
|
-
referencedNodes.add("trigger"); // Trigger is always reachable
|
|
2229
|
-
// Find all nodes that are referenced in actionOutput bindings
|
|
2230
|
-
for (const action of actions) {
|
|
2231
|
-
const inputs = (action.inputs || {});
|
|
2232
|
-
for (const inputValue of Object.values(inputs)) {
|
|
2233
|
-
const binding = inputValue;
|
|
2234
|
-
if (binding?.actionOutput) {
|
|
2235
|
-
const actionOutput = binding.actionOutput;
|
|
2236
|
-
referencedNodes.add(String(actionOutput.actionName || ""));
|
|
2237
|
-
}
|
|
2238
|
-
// Check multiBinding
|
|
2239
|
-
if (binding?.multiBinding) {
|
|
2240
|
-
const multi = binding.multiBinding;
|
|
2241
|
-
const elements = (multi.elements || []);
|
|
2242
|
-
for (const elem of elements) {
|
|
2243
|
-
if (elem.namedBinding) {
|
|
2244
|
-
const named = elem.namedBinding;
|
|
2245
|
-
const value = named.value;
|
|
2246
|
-
if (value?.actionOutput) {
|
|
2247
|
-
const ao = value.actionOutput;
|
|
2248
|
-
referencedNodes.add(String(ao.actionName || ""));
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
// Find nodes that don't receive input from trigger chain
|
|
2256
|
-
// This is a simplified check - proper reachability would need graph traversal
|
|
2257
|
-
for (const action of actions) {
|
|
2258
|
-
const nodeName = String(action.name || "");
|
|
2259
|
-
const hasInputFromTrigger = checkNodeHasPath(nodeName, actions, "trigger", new Set());
|
|
2260
|
-
if (!hasInputFromTrigger && nodeName !== "trigger") {
|
|
2261
|
-
orphans.push(nodeName);
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
return orphans;
|
|
2265
|
-
}
|
|
2266
|
-
/**
|
|
2267
|
-
* Check if a node has a path from source (simple BFS).
|
|
2268
|
-
*/
|
|
2269
|
-
function checkNodeHasPath(nodeName, actions, source, visited) {
|
|
2270
|
-
if (visited.has(nodeName))
|
|
2271
|
-
return false;
|
|
2272
|
-
visited.add(nodeName);
|
|
2273
|
-
const node = actions.find(a => a.name === nodeName);
|
|
2274
|
-
if (!node)
|
|
2275
|
-
return false;
|
|
2276
|
-
const inputs = (node.inputs || {});
|
|
2277
|
-
for (const inputValue of Object.values(inputs)) {
|
|
2278
|
-
const binding = inputValue;
|
|
2279
|
-
if (binding?.actionOutput) {
|
|
2280
|
-
const actionOutput = binding.actionOutput;
|
|
2281
|
-
const sourceNode = String(actionOutput.actionName || "");
|
|
2282
|
-
if (sourceNode === source)
|
|
2283
|
-
return true;
|
|
2284
|
-
if (checkNodeHasPath(sourceNode, actions, source, visited))
|
|
2285
|
-
return true;
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
return false;
|
|
2289
|
-
}
|
|
2290
|
-
/**
|
|
2291
|
-
* Apply modifications to the workflow based on analyzed operations.
|
|
2292
|
-
*/
|
|
2293
|
-
function applyWorkflowModifications(existingWorkflow, analysis) {
|
|
2294
|
-
const workflow = JSON.parse(JSON.stringify(existingWorkflow));
|
|
2295
|
-
const actions = (workflow.actions || []);
|
|
2296
|
-
const enumTypes = (workflow.enumTypes || []);
|
|
2297
|
-
const results = (workflow.results || {});
|
|
2298
|
-
const changesApplied = [];
|
|
2299
|
-
const nodesAdded = [];
|
|
2300
|
-
const nodesRemoved = [];
|
|
2301
|
-
const nodesModified = [];
|
|
2302
|
-
const connectionsChanged = [];
|
|
2303
|
-
for (const op of analysis.operations) {
|
|
2304
|
-
switch (op.type) {
|
|
2305
|
-
case "consolidate": {
|
|
2306
|
-
// Consolidate multiple nodes into one unified agent
|
|
2307
|
-
const targetNodes = op.target_nodes || [];
|
|
2308
|
-
if (targetNodes.length < 2) {
|
|
2309
|
-
changesApplied.push("Consolidation skipped: need 2+ nodes to consolidate");
|
|
2310
|
-
break;
|
|
2311
|
-
}
|
|
2312
|
-
// Get the first node as template
|
|
2313
|
-
const templateIdx = actions.findIndex(a => a.name === targetNodes[0]);
|
|
2314
|
-
if (templateIdx < 0)
|
|
2315
|
-
break;
|
|
2316
|
-
const template = actions[templateIdx];
|
|
2317
|
-
// Create unified agent node
|
|
2318
|
-
const unifiedNode = {
|
|
2319
|
-
name: "unified_analysis_agent",
|
|
2320
|
-
action: template.action,
|
|
2321
|
-
inputs: {
|
|
2322
|
-
...template.inputs,
|
|
2323
|
-
// Update instructions to be dynamic
|
|
2324
|
-
role_instructions: {
|
|
2325
|
-
inline: {
|
|
2326
|
-
wellKnown: {
|
|
2327
|
-
stringValue: "You are a versatile analysis agent. Read your agent definition from the knowledge base based on the current intent type and adopt that role.",
|
|
2328
|
-
},
|
|
2329
|
-
},
|
|
2330
|
-
autoDetectedBinding: false,
|
|
2331
|
-
},
|
|
2332
|
-
task_instructions: {
|
|
2333
|
-
inline: {
|
|
2334
|
-
wellKnown: {
|
|
2335
|
-
stringValue: "Execute the task based on the intent type. The knowledge base contains definitions for each intent with INPUT DATA, ANALYSIS FRAMEWORK, and OUTPUT SCHEMA. Follow those precisely.",
|
|
2336
|
-
},
|
|
2337
|
-
},
|
|
2338
|
-
autoDetectedBinding: false,
|
|
2339
|
-
},
|
|
2340
|
-
},
|
|
2341
|
-
displaySettings: {
|
|
2342
|
-
displayName: "Unified Analysis Agent",
|
|
2343
|
-
description: `Consolidated from ${targetNodes.length} separate agents`,
|
|
2344
|
-
coordinates: template.displaySettings?.coordinates || { x: 2000, y: 1500 },
|
|
2345
|
-
},
|
|
2346
|
-
typeArguments: template.typeArguments || {},
|
|
2347
|
-
tools: template.tools || [],
|
|
2348
|
-
disableHumanInteraction: false,
|
|
2349
|
-
// No runIf - runs for all intents, uses internal routing
|
|
2350
|
-
};
|
|
2351
|
-
// Remove all target nodes
|
|
2352
|
-
for (const nodeName of targetNodes) {
|
|
2353
|
-
const idx = actions.findIndex(a => a.name === nodeName);
|
|
2354
|
-
if (idx >= 0) {
|
|
2355
|
-
actions.splice(idx, 1);
|
|
2356
|
-
nodesRemoved.push(nodeName);
|
|
2357
|
-
// Also remove from results mapping
|
|
2358
|
-
const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
|
|
2359
|
-
if (resultKey) {
|
|
2360
|
-
delete results[resultKey];
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
// Add unified node
|
|
2365
|
-
actions.push(unifiedNode);
|
|
2366
|
-
nodesAdded.push("unified_analysis_agent");
|
|
2367
|
-
// Add to results mapping
|
|
2368
|
-
results["unified_analysis_agent.response_with_sources"] = {
|
|
2369
|
-
actionName: "unified_analysis_agent",
|
|
2370
|
-
outputName: "response_with_sources",
|
|
2371
|
-
};
|
|
2372
|
-
changesApplied.push(`Consolidated ${targetNodes.length} nodes into unified_analysis_agent`);
|
|
2373
|
-
break;
|
|
2374
|
-
}
|
|
2375
|
-
case "remove": {
|
|
2376
|
-
const targetNodes = op.target_nodes || [];
|
|
2377
|
-
for (const nodeName of targetNodes) {
|
|
2378
|
-
const idx = actions.findIndex(a => a.name === nodeName);
|
|
2379
|
-
if (idx >= 0) {
|
|
2380
|
-
actions.splice(idx, 1);
|
|
2381
|
-
nodesRemoved.push(nodeName);
|
|
2382
|
-
// Also remove from results
|
|
2383
|
-
const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
|
|
2384
|
-
if (resultKey) {
|
|
2385
|
-
delete results[resultKey];
|
|
2386
|
-
}
|
|
2387
|
-
changesApplied.push(`Removed node: ${nodeName}`);
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
break;
|
|
2391
|
-
}
|
|
2392
|
-
case "replace": {
|
|
2393
|
-
// For now, just note it - full replacement is complex
|
|
2394
|
-
changesApplied.push(`Replace operation noted: ${op.description}. Manual verification needed.`);
|
|
2395
|
-
break;
|
|
2396
|
-
}
|
|
2397
|
-
case "rewire": {
|
|
2398
|
-
const source = op.source;
|
|
2399
|
-
const target = op.target;
|
|
2400
|
-
if (source && target) {
|
|
2401
|
-
// Find target node and update its inputs
|
|
2402
|
-
const targetNode = actions.find(a => String(a.name || "").toLowerCase().includes(target.toLowerCase()));
|
|
2403
|
-
if (targetNode) {
|
|
2404
|
-
nodesModified.push(String(targetNode.name));
|
|
2405
|
-
connectionsChanged.push(`${source} → ${targetNode.name}`);
|
|
2406
|
-
changesApplied.push(`Rewiring: ${source} → ${targetNode.name}`);
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
break;
|
|
2410
|
-
}
|
|
2411
|
-
case "add": {
|
|
2412
|
-
if (op.new_node) {
|
|
2413
|
-
const newNode = op.new_node;
|
|
2414
|
-
const nodeName = String(newNode.name || `new_node_${Date.now()}`);
|
|
2415
|
-
nodesAdded.push(nodeName);
|
|
2416
|
-
changesApplied.push(`Added node: ${nodeName}`);
|
|
2417
|
-
}
|
|
2418
|
-
else if (op.config) {
|
|
2419
|
-
// Use legacy merge for additive changes
|
|
2420
|
-
const legacyChanges = parseExtensionInput(op.description);
|
|
2421
|
-
const legacyResult = mergeWorkflowChanges(workflow, legacyChanges);
|
|
2422
|
-
changesApplied.push(...legacyResult.changesApplied);
|
|
2423
|
-
}
|
|
2424
|
-
break;
|
|
2425
|
-
}
|
|
2426
|
-
case "modify_config": {
|
|
2427
|
-
changesApplied.push(`Config modification noted: ${op.description}`);
|
|
2428
|
-
break;
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
workflow.actions = actions;
|
|
2433
|
-
workflow.enumTypes = enumTypes;
|
|
2434
|
-
workflow.results = results;
|
|
2435
|
-
return {
|
|
2436
|
-
workflow,
|
|
2437
|
-
changesApplied,
|
|
2438
|
-
nodesAdded,
|
|
2439
|
-
nodesRemoved,
|
|
2440
|
-
nodesModified,
|
|
2441
|
-
connectionsChanged,
|
|
2442
|
-
};
|
|
2443
|
-
}
|
|
2444
|
-
/**
|
|
2445
|
-
* Parse natural language input to detect what changes are being requested.
|
|
2446
|
-
* Handles multiple changes in one input (e.g., "add X, add Y, add Z").
|
|
2447
|
-
*
|
|
2448
|
-
* IMPORTANT: We need to be smart about splitting - don't split enum options!
|
|
2449
|
-
* "add caller_type enum options Advisor and Client" should keep "Advisor and Client" together
|
|
2450
|
-
*/
|
|
2451
|
-
function parseExtensionInput(input) {
|
|
2452
|
-
const changes = [];
|
|
2453
|
-
// First, identify major change boundaries (comma-separated distinct changes)
|
|
2454
|
-
// But be careful not to split within an enum options list
|
|
2455
|
-
const segments = splitIntoChanges(input);
|
|
2456
|
-
for (const segment of segments) {
|
|
2457
|
-
const lowerSegment = segment.toLowerCase();
|
|
2458
|
-
// Detect categorizer/enum changes - parse options carefully
|
|
2459
|
-
if (lowerSegment.includes("categorizer") || lowerSegment.includes("enum") ||
|
|
2460
|
-
lowerSegment.includes("caller_type") || lowerSegment.includes("actor")) {
|
|
2461
|
-
// Extract options: "with Advisor and Client" or "options Advisor/Client" or "Advisor, Client"
|
|
2462
|
-
const optionsMatch = segment.match(/(?:options?|with)\s+(.+?)(?:,\s*add|$)/i);
|
|
2463
|
-
let options = [];
|
|
2464
|
-
if (optionsMatch) {
|
|
2465
|
-
// Split on "and", "/", or "," but preserve the values
|
|
2466
|
-
options = optionsMatch[1]
|
|
2467
|
-
.split(/\s+and\s+|[,/]/i)
|
|
2468
|
-
.map(s => s.trim())
|
|
2469
|
-
.filter(s => s.length > 0 && !s.toLowerCase().includes('add'));
|
|
2470
|
-
}
|
|
2471
|
-
// Also look for target categorizer name
|
|
2472
|
-
const categorizerMatch = segment.match(/(?:to\s+)?(\w+_categorizer|\w+categorizer)/i);
|
|
2473
|
-
const targetCategorizer = categorizerMatch ? categorizerMatch[1] : undefined;
|
|
2474
|
-
changes.push({
|
|
2475
|
-
type: "categorizer",
|
|
2476
|
-
description: `Add enum options: ${options.join(', ')}`,
|
|
2477
|
-
details: {
|
|
2478
|
-
raw: segment,
|
|
2479
|
-
options,
|
|
2480
|
-
target_categorizer: targetCategorizer,
|
|
2481
|
-
},
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
// Detect HITL/approval changes
|
|
2485
|
-
else if (lowerSegment.includes("hitl") || lowerSegment.includes("approval") ||
|
|
2486
|
-
lowerSegment.includes("confirm") || lowerSegment.includes("human")) {
|
|
2487
|
-
// Parse "before email" or "before send_email" or "before send"
|
|
2488
|
-
const beforeMatch = segment.match(/before\s+(\w+(?:_\w+)?)/i);
|
|
2489
|
-
const targetAction = beforeMatch ? beforeMatch[1] : "unspecified";
|
|
2490
|
-
changes.push({
|
|
2491
|
-
type: "hitl",
|
|
2492
|
-
description: `Add human-in-the-loop approval before ${targetAction}`,
|
|
2493
|
-
details: { raw: segment, before_action: targetAction },
|
|
2494
|
-
});
|
|
2495
|
-
}
|
|
2496
|
-
// Detect wiring/connection changes - parse source → target(s)
|
|
2497
|
-
else if (lowerSegment.includes("connect") || lowerSegment.includes("wire") ||
|
|
2498
|
-
lowerSegment.includes("link") || lowerSegment.match(/\w+\s+to\s+\w+\.\w+/)) {
|
|
2499
|
-
// Parse: "connect entity_extraction_1 to send_communications_handler to_email and subject"
|
|
2500
|
-
// Supports multiple target inputs separated by "and"
|
|
2501
|
-
const wiringMatch = segment.match(/(?:connect\s+)?(\w+)(?:\.(\w+))?\s+(?:to|→|->)\s+(\w+)(?:\.|\s+)(.+)/i);
|
|
2502
|
-
if (wiringMatch) {
|
|
2503
|
-
const sourceNode = wiringMatch[1];
|
|
2504
|
-
const sourceOutput = wiringMatch[2] || "extracted_entities";
|
|
2505
|
-
const targetNode = wiringMatch[3];
|
|
2506
|
-
// Parse multiple target inputs: "to_email and subject" or "to_email, subject"
|
|
2507
|
-
const targetInputsRaw = wiringMatch[4];
|
|
2508
|
-
const targetInputs = targetInputsRaw
|
|
2509
|
-
.split(/\s+and\s+|[,]/i)
|
|
2510
|
-
.map(s => s.trim())
|
|
2511
|
-
.filter(s => s.length > 0 && /^\w+$/.test(s));
|
|
2512
|
-
// Create a wiring change for each target input
|
|
2513
|
-
for (const targetInput of targetInputs) {
|
|
2514
|
-
changes.push({
|
|
2515
|
-
type: "wiring",
|
|
2516
|
-
description: `Connect ${sourceNode} to ${targetNode}.${targetInput}`,
|
|
2517
|
-
details: {
|
|
2518
|
-
raw: segment,
|
|
2519
|
-
source_node: sourceNode,
|
|
2520
|
-
source_output: sourceOutput,
|
|
2521
|
-
target_node: targetNode,
|
|
2522
|
-
target_input: targetInput,
|
|
2523
|
-
},
|
|
2524
|
-
});
|
|
2525
|
-
}
|
|
2526
|
-
if (targetInputs.length === 0) {
|
|
2527
|
-
changes.push({
|
|
2528
|
-
type: "wiring",
|
|
2529
|
-
description: `Modify connections: ${segment}`,
|
|
2530
|
-
details: { raw: segment },
|
|
2531
|
-
});
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
else {
|
|
2535
|
-
changes.push({
|
|
2536
|
-
type: "wiring",
|
|
2537
|
-
description: `Modify connections: ${segment}`,
|
|
2538
|
-
details: { raw: segment },
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
// Detect intent changes
|
|
2543
|
-
else if (lowerSegment.includes("intent")) {
|
|
2544
|
-
const intentMatch = segment.match(/(?:add|new)\s+(?:intent:?\s*)?(\w+)/i);
|
|
2545
|
-
const intentName = intentMatch ? intentMatch[1] : "new_intent";
|
|
2546
|
-
changes.push({
|
|
2547
|
-
type: "intent",
|
|
2548
|
-
description: `Add new intent: ${intentName}`,
|
|
2549
|
-
details: { raw: segment, intent_name: intentName },
|
|
2550
|
-
});
|
|
2551
|
-
}
|
|
2552
|
-
// Detect new node/capability additions
|
|
2553
|
-
else if (lowerSegment.includes("add") || lowerSegment.includes("new") || lowerSegment.includes("include")) {
|
|
2554
|
-
changes.push({
|
|
2555
|
-
type: "add_node",
|
|
2556
|
-
description: `Add new capability: ${segment}`,
|
|
2557
|
-
details: { raw: segment },
|
|
2558
|
-
});
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
return changes;
|
|
2562
|
-
}
|
|
2563
|
-
/**
|
|
2564
|
-
* Split input into distinct change requests, being careful not to split
|
|
2565
|
-
* within enum option lists.
|
|
2566
|
-
*/
|
|
2567
|
-
function splitIntoChanges(input) {
|
|
2568
|
-
const changes = [];
|
|
2569
|
-
// Split on ", add", ", connect", ", wire" patterns - these start new change requests
|
|
2570
|
-
// Use a regex that keeps the keyword with the following text
|
|
2571
|
-
const parts = input.split(/,\s*(?=add\s|connect\s|wire\s|link\s)/i);
|
|
2572
|
-
for (const part of parts) {
|
|
2573
|
-
const trimmed = part.trim();
|
|
2574
|
-
// Skip very short fragments that are just keywords
|
|
2575
|
-
if (trimmed.length > 5) {
|
|
2576
|
-
changes.push(trimmed);
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
// If no splits found, return the whole input
|
|
2580
|
-
if (changes.length === 0 && input.trim().length > 0) {
|
|
2581
|
-
changes.push(input.trim());
|
|
2582
|
-
}
|
|
2583
|
-
return changes;
|
|
2584
|
-
}
|
|
2585
|
-
/**
|
|
2586
|
-
* Extract the action type from a workflow action node.
|
|
2587
|
-
* Handles nested structure: action.action.name.name or action.action.name
|
|
2588
|
-
*/
|
|
2589
|
-
function getActionType(action) {
|
|
2590
|
-
const actionObj = action.action;
|
|
2591
|
-
if (!actionObj)
|
|
2592
|
-
return "";
|
|
2593
|
-
const nameObj = actionObj.name;
|
|
2594
|
-
if (!nameObj)
|
|
2595
|
-
return "";
|
|
2596
|
-
// Nested structure: action.name.name
|
|
2597
|
-
if (typeof nameObj === "object" && "name" in nameObj) {
|
|
2598
|
-
return String(nameObj.name || "");
|
|
2599
|
-
}
|
|
2600
|
-
// Direct string
|
|
2601
|
-
if (typeof nameObj === "string")
|
|
2602
|
-
return nameObj;
|
|
2603
|
-
return "";
|
|
2604
|
-
}
|
|
2605
|
-
/**
|
|
2606
|
-
* Merge detected changes into an existing workflow.
|
|
2607
|
-
* Returns the modified workflow and list of new nodes that need configuration.
|
|
2608
|
-
*/
|
|
2609
|
-
function mergeWorkflowChanges(existingWorkflow, changes) {
|
|
2610
|
-
const workflow = JSON.parse(JSON.stringify(existingWorkflow));
|
|
2611
|
-
const actions = (workflow.actions || []);
|
|
2612
|
-
const enumTypes = (workflow.enumTypes || []);
|
|
2613
|
-
const newNodes = [];
|
|
2614
|
-
const changesApplied = [];
|
|
2615
|
-
for (const change of changes) {
|
|
2616
|
-
switch (change.type) {
|
|
2617
|
-
case "categorizer":
|
|
2618
|
-
case "enum_option": {
|
|
2619
|
-
const options = change.details.options || [];
|
|
2620
|
-
const targetCategorizerName = change.details.target_categorizer;
|
|
2621
|
-
if (options.length === 0) {
|
|
2622
|
-
changesApplied.push("No enum options specified");
|
|
2623
|
-
break;
|
|
2624
|
-
}
|
|
2625
|
-
// Find the target categorizer action
|
|
2626
|
-
const categorizer = actions.find(a => {
|
|
2627
|
-
const nodeName = String(a.name || "");
|
|
2628
|
-
const actionType = getActionType(a);
|
|
2629
|
-
// Match by specific name or by type
|
|
2630
|
-
if (targetCategorizerName) {
|
|
2631
|
-
return nodeName.toLowerCase().includes(targetCategorizerName.toLowerCase());
|
|
2632
|
-
}
|
|
2633
|
-
return actionType.includes("categorizer");
|
|
2634
|
-
});
|
|
2635
|
-
if (!categorizer) {
|
|
2636
|
-
changesApplied.push(`No categorizer found to add options: ${options.join(', ')}`);
|
|
2637
|
-
break;
|
|
2638
|
-
}
|
|
2639
|
-
const categorizerName = String(categorizer.name || "");
|
|
2640
|
-
// Find the corresponding enumType for this categorizer
|
|
2641
|
-
// EnumType structure: { name: { name: { name: "EnumName", namespaces: [] } }, options: [...] }
|
|
2642
|
-
let targetEnum = enumTypes.find(e => {
|
|
2643
|
-
const enumName = extractEnumTypeName(e);
|
|
2644
|
-
// Match by categorizer name or by checking typeArguments reference
|
|
2645
|
-
return enumName && (enumName.toLowerCase().includes(categorizerName.toLowerCase()) ||
|
|
2646
|
-
categorizerName.toLowerCase().includes(enumName.toLowerCase().replace(/_categories$/i, '')));
|
|
2647
|
-
});
|
|
2648
|
-
// If no matching enum found, try to find any enum (for simple workflows)
|
|
2649
|
-
if (!targetEnum && enumTypes.length === 1) {
|
|
2650
|
-
targetEnum = enumTypes[0];
|
|
2651
|
-
}
|
|
2652
|
-
if (targetEnum) {
|
|
2653
|
-
// Add options to enumTypes[].options[]
|
|
2654
|
-
const enumOptions = (targetEnum.options || []);
|
|
2655
|
-
for (const opt of options) {
|
|
2656
|
-
const optLower = opt.toLowerCase();
|
|
2657
|
-
const exists = enumOptions.some(o => (o.name || "").toLowerCase() === optLower);
|
|
2658
|
-
if (!exists) {
|
|
2659
|
-
enumOptions.push({
|
|
2660
|
-
name: opt,
|
|
2661
|
-
description: `Added via MCP extend`
|
|
2662
|
-
});
|
|
2663
|
-
changesApplied.push(`Added "${opt}" to enumType.options`);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
targetEnum.options = enumOptions;
|
|
2667
|
-
}
|
|
2668
|
-
else {
|
|
2669
|
-
// No enumType found - note it for manual action
|
|
2670
|
-
changesApplied.push(`Enum options noted but no enumType found for ${categorizerName}: ${options.join(', ')}. Create enumType manually.`);
|
|
2671
|
-
}
|
|
2672
|
-
break;
|
|
2673
|
-
}
|
|
2674
|
-
case "hitl": {
|
|
2675
|
-
const targetAction = change.details.before_action;
|
|
2676
|
-
const hitlNodeName = `hitl_${targetAction || "approval"}_${Date.now()}`;
|
|
2677
|
-
// Find the target action to insert HITL before
|
|
2678
|
-
const targetNode = actions.find(a => {
|
|
2679
|
-
const nodeName = String(a.name || "").toLowerCase();
|
|
2680
|
-
return nodeName.includes(targetAction.toLowerCase());
|
|
2681
|
-
});
|
|
2682
|
-
newNodes.push({
|
|
2683
|
-
name: hitlNodeName,
|
|
2684
|
-
type: "human_in_the_loop",
|
|
2685
|
-
recommended_agent: "general_hitl",
|
|
2686
|
-
});
|
|
2687
|
-
if (targetNode) {
|
|
2688
|
-
changesApplied.push(`HITL node "${hitlNodeName}" to be inserted before "${targetNode.name}"`);
|
|
2689
|
-
}
|
|
2690
|
-
else {
|
|
2691
|
-
changesApplied.push(`HITL node "${hitlNodeName}" identified (target "${targetAction}" not found - wire manually)`);
|
|
2692
|
-
}
|
|
2693
|
-
break;
|
|
2694
|
-
}
|
|
2695
|
-
case "wiring": {
|
|
2696
|
-
const sourceNode = change.details.source_node;
|
|
2697
|
-
const sourceOutput = change.details.source_output;
|
|
2698
|
-
const targetNode = change.details.target_node;
|
|
2699
|
-
const targetInput = change.details.target_input;
|
|
2700
|
-
if (sourceNode && targetNode && targetInput) {
|
|
2701
|
-
// Find the target action and update its input
|
|
2702
|
-
const target = actions.find(a => {
|
|
2703
|
-
const nodeName = String(a.name || "").toLowerCase();
|
|
2704
|
-
return nodeName.includes(targetNode.toLowerCase());
|
|
2705
|
-
});
|
|
2706
|
-
if (target && target.inputs) {
|
|
2707
|
-
const inputs = target.inputs;
|
|
2708
|
-
// Find the input field to update (may be nested as to_email, email_to, subject, etc.)
|
|
2709
|
-
const inputKey = Object.keys(inputs).find(k => k.toLowerCase().includes(targetInput.toLowerCase()) ||
|
|
2710
|
-
targetInput.toLowerCase().includes(k.toLowerCase()));
|
|
2711
|
-
if (inputKey) {
|
|
2712
|
-
// Update the actionOutput reference
|
|
2713
|
-
inputs[inputKey] = {
|
|
2714
|
-
actionOutput: {
|
|
2715
|
-
actionName: sourceNode,
|
|
2716
|
-
output: sourceOutput || "extracted_entities",
|
|
2717
|
-
},
|
|
2718
|
-
autoDetectedBinding: false,
|
|
2719
|
-
};
|
|
2720
|
-
changesApplied.push(`Wired ${sourceNode}.${sourceOutput || "output"} → ${target.name}.${inputKey}`);
|
|
2721
|
-
}
|
|
2722
|
-
else {
|
|
2723
|
-
changesApplied.push(`Input "${targetInput}" not found on "${target.name}" - wire manually`);
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
else {
|
|
2727
|
-
changesApplied.push(`Target node "${targetNode}" not found - wire manually`);
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
else {
|
|
2731
|
-
changesApplied.push(`Wiring change noted: ${change.description} (incomplete - specify source and target)`);
|
|
2732
|
-
}
|
|
2733
|
-
break;
|
|
2734
|
-
}
|
|
2735
|
-
case "intent": {
|
|
2736
|
-
const intentName = change.details.intent_name;
|
|
2737
|
-
// Add to enumType options (same as categorizer case)
|
|
2738
|
-
const categorizer = actions.find(a => getActionType(a).includes("categorizer"));
|
|
2739
|
-
if (categorizer && enumTypes.length > 0) {
|
|
2740
|
-
const targetEnum = enumTypes[0]; // Use first enum for intents
|
|
2741
|
-
const enumOptions = (targetEnum.options || []);
|
|
2742
|
-
const exists = enumOptions.some(o => (o.name || "").toLowerCase() === intentName.toLowerCase());
|
|
2743
|
-
if (!exists) {
|
|
2744
|
-
enumOptions.push({ name: intentName });
|
|
2745
|
-
targetEnum.options = enumOptions;
|
|
2746
|
-
changesApplied.push(`Added intent "${intentName}" to enumType.options`);
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
else {
|
|
2750
|
-
changesApplied.push(`Intent "${intentName}" noted - add categorizer with enumType first`);
|
|
2751
|
-
}
|
|
2752
|
-
break;
|
|
2753
|
-
}
|
|
2754
|
-
case "add_node": {
|
|
2755
|
-
const raw = change.details.raw.toLowerCase();
|
|
2756
|
-
let nodeType = "unknown";
|
|
2757
|
-
let recommended = "call_llm";
|
|
2758
|
-
if (raw.includes("search") || raw.includes("kb") || raw.includes("knowledge")) {
|
|
2759
|
-
nodeType = "search";
|
|
2760
|
-
recommended = "search";
|
|
2761
|
-
}
|
|
2762
|
-
else if (raw.includes("email")) {
|
|
2763
|
-
nodeType = "send_email";
|
|
2764
|
-
recommended = "send_email_agent";
|
|
2765
|
-
}
|
|
2766
|
-
else if (raw.includes("ticket")) {
|
|
2767
|
-
nodeType = "external_action";
|
|
2768
|
-
recommended = "external_action_caller";
|
|
2769
|
-
}
|
|
2770
|
-
else if (raw.includes("extract")) {
|
|
2771
|
-
nodeType = "entity_extraction";
|
|
2772
|
-
recommended = "entity_extraction_with_documents";
|
|
2773
|
-
}
|
|
2774
|
-
newNodes.push({
|
|
2775
|
-
name: `${nodeType}_${Date.now()}`,
|
|
2776
|
-
type: nodeType,
|
|
2777
|
-
recommended_agent: recommended,
|
|
2778
|
-
});
|
|
2779
|
-
changesApplied.push(`New node identified: ${nodeType}`);
|
|
2780
|
-
break;
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
workflow.actions = actions;
|
|
2785
|
-
workflow.enumTypes = enumTypes;
|
|
2786
|
-
return { workflow, newNodes, changesApplied };
|
|
2787
|
-
}
|
|
2788
|
-
/**
|
|
2789
|
-
* Extract the name from an enumType's nested name structure.
|
|
2790
|
-
* Structure: { name: { name: { name: "ActualName", namespaces: [] } } }
|
|
2791
|
-
*/
|
|
2792
|
-
function extractEnumTypeName(enumType) {
|
|
2793
|
-
try {
|
|
2794
|
-
const outerName = enumType.name;
|
|
2795
|
-
if (!outerName)
|
|
2796
|
-
return null;
|
|
2797
|
-
const innerName = outerName.name;
|
|
2798
|
-
if (typeof innerName === "string")
|
|
2799
|
-
return innerName;
|
|
2800
|
-
if (!innerName)
|
|
2801
|
-
return null;
|
|
2802
|
-
const actualName = innerName.name;
|
|
2803
|
-
if (typeof actualName === "string")
|
|
2804
|
-
return actualName;
|
|
2805
|
-
return null;
|
|
2806
|
-
}
|
|
2807
|
-
catch {
|
|
2808
|
-
return null;
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
/**
|
|
2812
|
-
* Sanitize workflow before deployment to prevent API errors.
|
|
2813
|
-
* Same logic as deploy_workflow uses.
|
|
2814
|
-
*/
|
|
2815
|
-
function sanitizeWorkflowForDeploy(workflow) {
|
|
2816
|
-
const sanitized = JSON.parse(JSON.stringify(workflow));
|
|
2817
|
-
// Fix enumTypes - remove entries with empty/missing names (causes server panic)
|
|
2818
|
-
// EnumType structure: { name: { name: { name: "string", namespaces: [] } }, options: [...] }
|
|
2819
|
-
const enumTypes = sanitized.enumTypes;
|
|
2820
|
-
if (Array.isArray(enumTypes)) {
|
|
2821
|
-
const validEnumTypes = enumTypes.filter(e => {
|
|
2822
|
-
const outerName = e.name;
|
|
2823
|
-
const innerName = outerName?.name;
|
|
2824
|
-
const actualName = innerName?.name;
|
|
2825
|
-
return typeof actualName === "string" && actualName.trim().length > 0;
|
|
2826
|
-
});
|
|
2827
|
-
if (validEnumTypes.length > 0) {
|
|
2828
|
-
sanitized.enumTypes = validEnumTypes;
|
|
2829
|
-
}
|
|
2830
|
-
else {
|
|
2831
|
-
delete sanitized.enumTypes;
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
// Ensure all actions have a 'name' field (node identifier)
|
|
2835
|
-
const actions = sanitized.actions;
|
|
2836
|
-
if (Array.isArray(actions)) {
|
|
2837
|
-
for (const action of actions) {
|
|
2838
|
-
// If action has 'actionName' but not 'name', fix it
|
|
2839
|
-
if (!action.name && action.actionName) {
|
|
2840
|
-
action.name = action.actionName;
|
|
2841
|
-
}
|
|
2842
|
-
// Ensure name is a non-empty string
|
|
2843
|
-
if (typeof action.name !== "string" || !action.name.trim()) {
|
|
2844
|
-
// Generate a name from action type if possible
|
|
2845
|
-
const actionType = getActionType(action);
|
|
2846
|
-
if (actionType) {
|
|
2847
|
-
action.name = `${actionType}_${Date.now()}`;
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
return sanitized;
|
|
2853
|
-
}
|
|
2854
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2855
|
-
// Helper Functions
|
|
2856
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2857
|
-
// resolvePersona imported from handlers/utils.ts
|
|
2858
|
-
function comparePersonas(p1, p2) {
|
|
2859
|
-
const diffs = {};
|
|
2860
|
-
if (p1.name !== p2.name)
|
|
2861
|
-
diffs.name = { from: p1.name, to: p2.name };
|
|
2862
|
-
if (p1.description !== p2.description)
|
|
2863
|
-
diffs.description = "different";
|
|
2864
|
-
if (p1.status !== p2.status)
|
|
2865
|
-
diffs.status = { from: p1.status, to: p2.status };
|
|
2866
|
-
if (p1.template_id !== p2.template_id)
|
|
2867
|
-
diffs.template_id = { from: p1.template_id, to: p2.template_id };
|
|
2868
|
-
// Compare workflows
|
|
2869
|
-
const w1 = p1.workflow_def;
|
|
2870
|
-
const w2 = p2.workflow_def;
|
|
2871
|
-
if (JSON.stringify(w1) !== JSON.stringify(w2)) {
|
|
2872
|
-
diffs.workflow = "different";
|
|
2873
|
-
}
|
|
2874
|
-
return diffs;
|
|
2875
|
-
}
|
|
2876
|
-
function compareWorkflows(w1, w2) {
|
|
2877
|
-
if (!w1 && !w2)
|
|
2878
|
-
return { equal: true };
|
|
2879
|
-
if (!w1 || !w2)
|
|
2880
|
-
return { equal: false, reason: "one workflow missing" };
|
|
2881
|
-
const actions1 = w1.actions || [];
|
|
2882
|
-
const actions2 = w2.actions || [];
|
|
2883
|
-
return {
|
|
2884
|
-
equal: JSON.stringify(w1) === JSON.stringify(w2),
|
|
2885
|
-
node_count: { before: actions1.length, after: actions2.length },
|
|
2886
|
-
};
|
|
2887
|
-
}
|
|
2888
|
-
function calculateMetrics(workflow) {
|
|
2889
|
-
const actions = workflow.actions || [];
|
|
2890
|
-
// Helper to get action type from nested structure
|
|
2891
|
-
const getType = (a) => {
|
|
2892
|
-
const action = a.action;
|
|
2893
|
-
if (!action?.name)
|
|
2894
|
-
return "";
|
|
2895
|
-
const nameObj = action.name;
|
|
2896
|
-
if (typeof nameObj === "string")
|
|
2897
|
-
return nameObj;
|
|
2898
|
-
if (typeof nameObj === "object" && "name" in nameObj)
|
|
2899
|
-
return String(nameObj.name || "");
|
|
2900
|
-
return "";
|
|
2901
|
-
};
|
|
2902
|
-
return {
|
|
2903
|
-
node_count: actions.length,
|
|
2904
|
-
has_categorizer: actions.some(a => getType(a).includes("categorizer")),
|
|
2905
|
-
has_hitl: actions.some(a => getType(a) === "general_hitl"),
|
|
2906
|
-
has_external: actions.some(a => getType(a) === "external_action_caller"),
|
|
2907
|
-
};
|
|
2908
|
-
}
|
|
2909
|
-
/**
|
|
2910
|
-
* Apply simple workflow fixes for auto_fixable issues
|
|
2911
|
-
* Handles orphan removal with cascading dependency cleanup
|
|
2912
|
-
*/
|
|
2913
|
-
function applySimpleFixes(workflow, issues) {
|
|
2914
|
-
// Deep clone to avoid mutating original
|
|
2915
|
-
const fixed = JSON.parse(JSON.stringify(workflow));
|
|
2916
|
-
// Get nodes array from workflow (handle different structures)
|
|
2917
|
-
const getNodes = (w) => {
|
|
2918
|
-
if (Array.isArray(w.nodes))
|
|
2919
|
-
return w.nodes;
|
|
2920
|
-
const wd = w.workflow_def;
|
|
2921
|
-
if (wd && Array.isArray(wd.nodes))
|
|
2922
|
-
return wd.nodes;
|
|
2923
|
-
return undefined;
|
|
2924
|
-
};
|
|
2925
|
-
const nodes = getNodes(fixed);
|
|
2926
|
-
if (!nodes)
|
|
2927
|
-
return fixed;
|
|
2928
|
-
// Collect all orphan node IDs to remove
|
|
2929
|
-
const nodesToRemove = new Set();
|
|
2930
|
-
for (const issue of issues) {
|
|
2931
|
-
if (issue.type === "orphan" && issue.auto_fixable && issue.node) {
|
|
2932
|
-
nodesToRemove.add(issue.node);
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
if (nodesToRemove.size === 0)
|
|
2936
|
-
return fixed;
|
|
2937
|
-
// Step 1: Remove orphan nodes
|
|
2938
|
-
const filteredNodes = nodes.filter(n => {
|
|
2939
|
-
const nodeId = n.id;
|
|
2940
|
-
return nodeId && !nodesToRemove.has(nodeId);
|
|
2941
|
-
});
|
|
2942
|
-
// Step 2: Clean up dangling references in remaining nodes
|
|
2943
|
-
for (const node of filteredNodes) {
|
|
2944
|
-
const incomingEdges = node.incoming_edges;
|
|
2945
|
-
if (incomingEdges && Array.isArray(incomingEdges)) {
|
|
2946
|
-
// Filter out edges that reference removed nodes
|
|
2947
|
-
node.incoming_edges = incomingEdges.filter(edge => {
|
|
2948
|
-
const sourceNodeId = edge.source_node_id;
|
|
2949
|
-
return sourceNodeId && !nodesToRemove.has(sourceNodeId);
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
2952
|
-
// Also clean up inputBindings if they exist (different workflow format)
|
|
2953
|
-
const inputBindings = node.inputBindings;
|
|
2954
|
-
if (inputBindings && Array.isArray(inputBindings)) {
|
|
2955
|
-
node.inputBindings = inputBindings.filter(binding => {
|
|
2956
|
-
const actionOutput = binding.actionOutput;
|
|
2957
|
-
const actionName = actionOutput?.actionName;
|
|
2958
|
-
return !actionName || !nodesToRemove.has(actionName);
|
|
2959
|
-
});
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
// Update nodes in the workflow
|
|
2963
|
-
if (Array.isArray(fixed.nodes)) {
|
|
2964
|
-
fixed.nodes = filteredNodes;
|
|
2965
|
-
}
|
|
2966
|
-
else {
|
|
2967
|
-
const wd = fixed.workflow_def;
|
|
2968
|
-
if (wd) {
|
|
2969
|
-
wd.nodes = filteredNodes;
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
return fixed;
|
|
2973
|
-
}
|
|
349
|
+
// Re-export the extracted handler for backwards compatibility
|
|
350
|
+
export { handleWorkflow } from "./handlers/workflow/index.js";
|