@ema.co/mcp-toolkit 1.5.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/README.md +2 -2
- package/dist/mcp/handlers-consolidated.js +773 -25
- package/dist/mcp/resources.js +124 -0
- package/dist/mcp/server.js +13 -205
- package/dist/mcp/tools-consolidated.js +163 -103
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/action-schema-parser.js +379 -0
- package/dist/sdk/client.js +757 -90
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +59 -2
- package/dist/sdk/intent-architect.js +883 -0
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/sanitizer.js +1121 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +88 -34
- package/dist/sdk/workflow-intent.js +237 -24
- package/dist/sdk/workflow-optimizer.js +665 -0
- package/dist/sdk/workflow-tracer.js +648 -0
- package/dist/sdk/workflow-transformer.js +10 -0
- package/dist/sdk/workflow-validator.js +609 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +196 -204
- package/docs/release-process.md +153 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +8 -2
- package/resources/action-schema.json +5678 -0
- package/resources/config/gates.json +88 -0
- package/resources/config/gates.schema.json +77 -0
- package/resources/templates/auto-builder-rules.md +222 -0
- package/resources/templates/demo-scenarios/test-published-package.md +116 -0
|
@@ -8,8 +8,12 @@ import { createVersionStorage } from "../sdk/version-storage.js";
|
|
|
8
8
|
import { createVersionPolicyEngine } from "../sdk/version-policy.js";
|
|
9
9
|
import { AGENT_CATALOG, WORKFLOW_PATTERNS, QUALIFYING_QUESTIONS, PLATFORM_CONCEPTS, WORKFLOW_EXECUTION_MODEL, COMMON_MISTAKES, DEBUG_CHECKLIST, GUIDANCE_TOPICS, VOICE_PERSONA_TEMPLATE, getAgentByName, getWidgetsForPersonaType, checkTypeCompatibility, getQualifyingQuestionsByCategory, getRequiredQualifyingQuestions, getConceptByTerm, suggestAgentsForUseCase, validateWorkflowPrompt, detectWorkflowIssues, validateWorkflowConnections, suggestWorkflowFixes, } from "../sdk/knowledge.js";
|
|
10
10
|
import { compileWorkflow } from "../sdk/workflow-generator.js";
|
|
11
|
-
import {
|
|
11
|
+
import { ensureActionRegistry } from "../sdk/action-registry.js";
|
|
12
|
+
import { parseInput, intentToSpec, generateWorkflow } from "../sdk/workflow-intent.js";
|
|
13
|
+
import { ensureSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM } from "../sdk/workflow-validator.js";
|
|
14
|
+
import { runIntentArchitect } from "../sdk/intent-architect.js";
|
|
12
15
|
import { analyzeExecutionFlow, generateASCIIFlow } from "../sdk/workflow-execution-analyzer.js";
|
|
16
|
+
import { SanitizationSession, sanitizePersona, buildConfirmationPrompt, detectWithPatterns, } from "../sdk/sanitizer.js";
|
|
13
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
18
|
// Widget Validation Helpers
|
|
15
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -43,7 +47,7 @@ function validateWidgetsForApi(widgets) {
|
|
|
43
47
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
48
|
// ENV Handler
|
|
45
49
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
-
export async function handleEnv(_args, getEnvironments) {
|
|
50
|
+
export async function handleEnv(_args, getEnvironments, toolkit) {
|
|
47
51
|
const envs = getEnvironments();
|
|
48
52
|
return {
|
|
49
53
|
environments: envs.map(e => ({
|
|
@@ -51,6 +55,7 @@ export async function handleEnv(_args, getEnvironments) {
|
|
|
51
55
|
default: e.isDefault,
|
|
52
56
|
})),
|
|
53
57
|
count: envs.length,
|
|
58
|
+
toolkit: toolkit ?? { name: "unknown", version: "unknown" },
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -61,13 +66,41 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
61
66
|
const identifier = args.identifier; // deprecated alias
|
|
62
67
|
const idOrName = id ?? identifier;
|
|
63
68
|
const mode = args.mode;
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
// UNIFIED HANDLING: Route workflow operations to handleWorkflow
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
const input = args.input;
|
|
73
|
+
const optimize = args.optimize;
|
|
74
|
+
const workflowDef = args.workflow_def ?? args.workflow;
|
|
75
|
+
// If input is provided → this is a create/modify workflow operation
|
|
76
|
+
// If optimize is provided → this is an optimize operation
|
|
77
|
+
// If id + workflowDef provided → this is a deploy operation
|
|
78
|
+
// Route these to handleWorkflow
|
|
79
|
+
if (input || optimize || (idOrName && workflowDef)) {
|
|
80
|
+
// Map persona args to workflow args
|
|
81
|
+
const workflowArgs = {
|
|
82
|
+
...args,
|
|
83
|
+
persona_id: idOrName, // workflow uses persona_id not id
|
|
84
|
+
};
|
|
85
|
+
delete workflowArgs.id;
|
|
86
|
+
delete workflowArgs.identifier;
|
|
87
|
+
return handleWorkflow(workflowArgs, client, getTemplateId);
|
|
88
|
+
}
|
|
89
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
90
|
+
// Standard persona operations (get, list, compare, version management)
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
64
92
|
// Determine effective mode
|
|
93
|
+
// BUG FIX: clone_from and name+type now properly route to "create" mode
|
|
65
94
|
let effectiveMode = mode;
|
|
66
95
|
if (!effectiveMode) {
|
|
67
96
|
if (args.templates)
|
|
68
97
|
effectiveMode = "templates";
|
|
69
98
|
else if (args.all || args.query || args.status || args.trigger_type)
|
|
70
99
|
effectiveMode = "list";
|
|
100
|
+
else if (args.sanitize && idOrName)
|
|
101
|
+
effectiveMode = "sanitize"; // Standalone sanitize mode
|
|
102
|
+
else if (args.clone_from || (args.name && (args.type || args.template_id)))
|
|
103
|
+
effectiveMode = "create";
|
|
71
104
|
else if (idOrName)
|
|
72
105
|
effectiveMode = "get";
|
|
73
106
|
else
|
|
@@ -151,18 +184,185 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
151
184
|
if (!templateId && args.type) {
|
|
152
185
|
templateId = getTemplateId(args.type);
|
|
153
186
|
}
|
|
187
|
+
// Check if cloning from dashboard persona
|
|
188
|
+
const cloneFrom = args.clone_from;
|
|
189
|
+
const cloneData = args.clone_data;
|
|
190
|
+
let sourcePersona = null;
|
|
191
|
+
let sourcePersonaType;
|
|
192
|
+
let sourceDashboardId;
|
|
193
|
+
if (cloneFrom && cloneData) {
|
|
194
|
+
try {
|
|
195
|
+
sourcePersona = await resolvePersona(client, cloneFrom);
|
|
196
|
+
if (sourcePersona) {
|
|
197
|
+
const protoConfig = sourcePersona.proto_config;
|
|
198
|
+
const projectSettings = protoConfig?.projectSettings;
|
|
199
|
+
const projectType = projectSettings?.projectType;
|
|
200
|
+
sourcePersonaType = projectType === 5 ? "voice" : projectType === 4 ? "chat" : projectType === 2 ? "dashboard" : undefined;
|
|
201
|
+
sourceDashboardId = sourcePersona.workflow_dashboard_id;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Ignore errors in source persona detection
|
|
206
|
+
}
|
|
207
|
+
}
|
|
154
208
|
const result = await client.createAiEmployee({
|
|
155
209
|
name,
|
|
156
210
|
description: args.description,
|
|
157
211
|
template_id: templateId,
|
|
158
|
-
source_persona_id:
|
|
159
|
-
clone_data:
|
|
212
|
+
source_persona_id: cloneFrom,
|
|
213
|
+
clone_data: cloneData,
|
|
160
214
|
});
|
|
161
|
-
|
|
215
|
+
const newPersonaId = result.persona_id ?? result.id;
|
|
216
|
+
// Automatically clone dashboard rows if source is a dashboard persona
|
|
217
|
+
let dashboardCloneResult;
|
|
218
|
+
if (sourcePersonaType === "dashboard" && sourceDashboardId && newPersonaId && cloneData) {
|
|
219
|
+
try {
|
|
220
|
+
// Dashboard operations require the persona to be enabled
|
|
221
|
+
// Auto-enable the persona temporarily for cloning
|
|
222
|
+
const newPersona = await client.getPersonaById(newPersonaId);
|
|
223
|
+
const newPersonaProtoConfig = newPersona?.proto_config;
|
|
224
|
+
// Enable the persona so we can add dashboard rows
|
|
225
|
+
await client.updateAiEmployee({
|
|
226
|
+
persona_id: newPersonaId,
|
|
227
|
+
proto_config: newPersonaProtoConfig ?? {},
|
|
228
|
+
enabled_by_user: true,
|
|
229
|
+
});
|
|
230
|
+
// Get source dashboard rows
|
|
231
|
+
const sourceRows = await client.getDashboardRows(sourceDashboardId, sourcePersona.id);
|
|
232
|
+
if (sourceRows.rows.length > 0) {
|
|
233
|
+
// Identify input columns
|
|
234
|
+
const inputColumns = sourceRows.schema.columns.filter(c => c.isInput);
|
|
235
|
+
// Clone each row
|
|
236
|
+
const results = [];
|
|
237
|
+
const shouldSanitize = args.sanitize;
|
|
238
|
+
const sanitizeExamples = args.sanitize_examples;
|
|
239
|
+
// Create sanitization session if needed
|
|
240
|
+
let sanitizationSession;
|
|
241
|
+
if (shouldSanitize) {
|
|
242
|
+
sanitizationSession = new SanitizationSession();
|
|
243
|
+
}
|
|
244
|
+
for (const row of sourceRows.rows) {
|
|
245
|
+
try {
|
|
246
|
+
// Build inputs from row's input column values
|
|
247
|
+
const inputs = [];
|
|
248
|
+
for (const inputCol of inputColumns) {
|
|
249
|
+
const colValue = row.columnValues.find(cv => cv.columnId === inputCol.columnId);
|
|
250
|
+
if (!colValue)
|
|
251
|
+
continue;
|
|
252
|
+
// Handle different column types
|
|
253
|
+
if (inputCol.columnType === "COLUMN_TYPE_DOCUMENT") {
|
|
254
|
+
// Skip document columns for now
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
else if (inputCol.columnType === "COLUMN_TYPE_STRING") {
|
|
258
|
+
let value = colValue.value.stringValue ?? "";
|
|
259
|
+
// Sanitize if enabled
|
|
260
|
+
if (sanitizationSession && value) {
|
|
261
|
+
const detected = detectWithPatterns(value);
|
|
262
|
+
for (const entity of detected) {
|
|
263
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
264
|
+
value = value.split(entity.value).join(replacement);
|
|
265
|
+
}
|
|
266
|
+
if (sanitizeExamples) {
|
|
267
|
+
for (const example of sanitizeExamples) {
|
|
268
|
+
if (value.includes(example)) {
|
|
269
|
+
const replacement = sanitizationSession.getOrCreateReplacement(example, "unknown");
|
|
270
|
+
value = value.split(example).join(replacement);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
inputs.push({ name: inputCol.name, string_value: value });
|
|
276
|
+
}
|
|
277
|
+
else if (inputCol.columnType === "COLUMN_TYPE_ARRAY") {
|
|
278
|
+
const arrayVals = colValue.value.arrayValue?.arrayValues ?? [];
|
|
279
|
+
if (arrayVals.length > 0) {
|
|
280
|
+
let value = arrayVals[0].stringValue ?? "";
|
|
281
|
+
if (sanitizationSession && value) {
|
|
282
|
+
const detected = detectWithPatterns(value);
|
|
283
|
+
for (const entity of detected) {
|
|
284
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
285
|
+
value = value.split(entity.value).join(replacement);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
inputs.push({ name: inputCol.name, string_value: value });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (inputs.length === 0) {
|
|
293
|
+
results.push({ source_row_id: row.id, status: "skipped", error: "No clonable input values" });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Upload the row to target dashboard
|
|
297
|
+
const uploadResult = await client.uploadAndRunDashboardRow(newPersonaId, inputs);
|
|
298
|
+
results.push({ source_row_id: row.id, target_row_id: uploadResult.row_id, status: "cloned" });
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
results.push({ source_row_id: row.id, status: "error", error: err instanceof Error ? err.message : String(err) });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const clonedCount = results.filter(r => r.status === "cloned").length;
|
|
305
|
+
dashboardCloneResult = {
|
|
306
|
+
source_rows: sourceRows.rows.length,
|
|
307
|
+
cloned_rows: clonedCount,
|
|
308
|
+
skipped_rows: results.filter(r => r.status === "skipped").length,
|
|
309
|
+
error_rows: results.filter(r => r.status === "error").length,
|
|
310
|
+
sanitization_applied: !!shouldSanitize,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
dashboardCloneResult = { source_rows: 0, cloned_rows: 0, message: "Source dashboard was empty" };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
dashboardCloneResult = { error: `Dashboard clone failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Handle sanitization for non-dashboard personas
|
|
322
|
+
const shouldSanitize = args.sanitize;
|
|
323
|
+
if (shouldSanitize && newPersonaId && sourcePersonaType !== "dashboard") {
|
|
324
|
+
const sanitizeResult = await sanitizePersonaById(client, newPersonaId, {
|
|
325
|
+
examples: args.sanitize_examples,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
success: true,
|
|
329
|
+
persona_id: newPersonaId,
|
|
330
|
+
name,
|
|
331
|
+
sanitization: sanitizeResult,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const createResult = {
|
|
162
335
|
success: true,
|
|
163
|
-
persona_id:
|
|
336
|
+
persona_id: newPersonaId,
|
|
164
337
|
name,
|
|
165
338
|
};
|
|
339
|
+
if (sourcePersonaType) {
|
|
340
|
+
createResult.source_persona_type = sourcePersonaType;
|
|
341
|
+
}
|
|
342
|
+
if (dashboardCloneResult) {
|
|
343
|
+
createResult.dashboard_data_clone = dashboardCloneResult;
|
|
344
|
+
}
|
|
345
|
+
return createResult;
|
|
346
|
+
}
|
|
347
|
+
case "sanitize": {
|
|
348
|
+
// Standalone sanitize mode: sanitize an existing persona
|
|
349
|
+
if (!idOrName) {
|
|
350
|
+
return { error: "id required for sanitize mode" };
|
|
351
|
+
}
|
|
352
|
+
const persona = await resolvePersona(client, idOrName);
|
|
353
|
+
if (!persona) {
|
|
354
|
+
return { error: `Persona not found: ${idOrName}` };
|
|
355
|
+
}
|
|
356
|
+
const sanitizeResult = await sanitizePersonaById(client, persona.id, {
|
|
357
|
+
examples: args.sanitize_examples,
|
|
358
|
+
preview: args.preview,
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
mode: "sanitize",
|
|
362
|
+
persona_id: persona.id,
|
|
363
|
+
persona_name: persona.name,
|
|
364
|
+
...sanitizeResult,
|
|
365
|
+
};
|
|
166
366
|
}
|
|
167
367
|
case "update": {
|
|
168
368
|
if (!idOrName) {
|
|
@@ -752,6 +952,8 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
752
952
|
if (!input) {
|
|
753
953
|
return { error: "input required for generate mode" };
|
|
754
954
|
}
|
|
955
|
+
// Load action registry for API-driven action versions/namespaces
|
|
956
|
+
const actionRegistry = await ensureActionRegistry(client);
|
|
755
957
|
const parseResult = parseInput(input);
|
|
756
958
|
if (!parseResult.validation.complete) {
|
|
757
959
|
return {
|
|
@@ -761,10 +963,130 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
761
963
|
questions: parseResult.validation.questions,
|
|
762
964
|
};
|
|
763
965
|
}
|
|
764
|
-
//
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
966
|
+
// Override persona_type from args.type if provided (args.type takes precedence)
|
|
967
|
+
if (args.type) {
|
|
968
|
+
parseResult.intent.persona_type = args.type;
|
|
969
|
+
}
|
|
970
|
+
// Load schema registry for API-driven validation (graceful degradation if unavailable)
|
|
971
|
+
let schemaRegistry;
|
|
972
|
+
try {
|
|
973
|
+
schemaRegistry = await ensureSchemaRegistry(client);
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
// Schema registry unavailable - skip API validation
|
|
977
|
+
schemaRegistry = null;
|
|
978
|
+
}
|
|
979
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
980
|
+
// PROGRESSIVE ENHANCEMENT: Use Intent Architect for moderate/complex requests
|
|
981
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
982
|
+
//
|
|
983
|
+
// Uses the canonical runIntentArchitect() entrypoint which:
|
|
984
|
+
// - Detects complexity with typed scores (not enums)
|
|
985
|
+
// - Returns qualification questions or LLM prompt
|
|
986
|
+
// - Supports iterative refinement with previous_answers
|
|
987
|
+
//
|
|
988
|
+
// Configure rollout via max_complexity option (default: allow all levels)
|
|
989
|
+
const maxComplexity = args.max_complexity || undefined;
|
|
990
|
+
const inputStr = input; // Already validated above
|
|
991
|
+
const architectResult = runIntentArchitect(inputStr, {
|
|
992
|
+
persona_type: parseResult.intent.persona_type,
|
|
993
|
+
available_integrations: schemaRegistry?.getAllActions().slice(0, 20).map(a => a.displayName || a.name),
|
|
994
|
+
}, { max_complexity: maxComplexity });
|
|
995
|
+
// For moderate/complex: return Intent Architect result with questions or prompt
|
|
996
|
+
if (!architectResult.strategy.can_proceed) {
|
|
997
|
+
let availableActions = [];
|
|
998
|
+
let availableTemplates = [];
|
|
999
|
+
// Enhance prompt with action catalog if available
|
|
1000
|
+
let enhancedPrompt = architectResult.prompt_package;
|
|
1001
|
+
if (schemaRegistry && enhancedPrompt) {
|
|
1002
|
+
const actionCatalog = generateActionCatalogForLLM(schemaRegistry);
|
|
1003
|
+
enhancedPrompt = {
|
|
1004
|
+
system: enhancedPrompt.system + "\n\n## Available Actions\n" + actionCatalog,
|
|
1005
|
+
user: enhancedPrompt.user,
|
|
1006
|
+
};
|
|
1007
|
+
availableActions = schemaRegistry.getAllActions().map(a => a.name);
|
|
1008
|
+
availableTemplates = schemaRegistry.getAllTemplates().map(t => ({ id: t.id, name: t.name, type: t.type }));
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
status: "needs_intent_architect",
|
|
1012
|
+
// Typed assessment (new)
|
|
1013
|
+
assessment: architectResult.assessment,
|
|
1014
|
+
// Strategy decision (new)
|
|
1015
|
+
strategy: architectResult.strategy,
|
|
1016
|
+
// Qualification questions (new)
|
|
1017
|
+
questions: architectResult.questions,
|
|
1018
|
+
// LLM prompt (if full architect)
|
|
1019
|
+
llm_prompt: enhancedPrompt,
|
|
1020
|
+
// Hint for what to do next
|
|
1021
|
+
hint: architectResult.strategy.next_step,
|
|
1022
|
+
// Legacy fields for backward compatibility
|
|
1023
|
+
reason: architectResult.legacy?.signals.reason,
|
|
1024
|
+
complexity: architectResult.legacy?.complexity,
|
|
1025
|
+
approach: architectResult.strategy.approach,
|
|
1026
|
+
gates_to_ask: architectResult.strategy.gates_to_ask,
|
|
1027
|
+
// Also provide basic spec as fallback
|
|
1028
|
+
fallback_spec: intentToSpec(parseResult.intent),
|
|
1029
|
+
// Include catalogs for reference (if available)
|
|
1030
|
+
available_actions: availableActions,
|
|
1031
|
+
available_templates: availableTemplates,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
// SIMPLE complexity: Check if intent requires LLM-driven generation (legacy path)
|
|
1035
|
+
const genResult = generateWorkflow(parseResult.intent);
|
|
1036
|
+
if (genResult.needs_llm) {
|
|
1037
|
+
// Complex workflow detected by legacy system - return prompt for LLM to generate
|
|
1038
|
+
// This is a fallback for cases the new complexity analyzer might miss
|
|
1039
|
+
let enhancedPrompt = genResult.llm_prompt;
|
|
1040
|
+
let availableActions = [];
|
|
1041
|
+
let availableTemplates = [];
|
|
1042
|
+
if (schemaRegistry) {
|
|
1043
|
+
const actionCatalog = generateActionCatalogForLLM(schemaRegistry);
|
|
1044
|
+
enhancedPrompt = genResult.llm_prompt ? {
|
|
1045
|
+
system: genResult.llm_prompt.system + "\n\n" + actionCatalog,
|
|
1046
|
+
user: genResult.llm_prompt.user,
|
|
1047
|
+
} : undefined;
|
|
1048
|
+
availableActions = schemaRegistry.getAllActions().map(a => a.name);
|
|
1049
|
+
availableTemplates = schemaRegistry.getAllTemplates().map(t => ({ id: t.id, name: t.name, type: t.type }));
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
status: "needs_llm_generation",
|
|
1053
|
+
reason: genResult.reason,
|
|
1054
|
+
complexity: genResult.complexity,
|
|
1055
|
+
llm_prompt: enhancedPrompt,
|
|
1056
|
+
hint: "Send llm_prompt to an LLM, then call persona(workflow_def=<parsed_response>) to deploy.",
|
|
1057
|
+
// Also provide basic spec as fallback (without complex chains)
|
|
1058
|
+
fallback_spec: intentToSpec(parseResult.intent),
|
|
1059
|
+
// Include catalogs for reference (if available)
|
|
1060
|
+
available_actions: availableActions,
|
|
1061
|
+
available_templates: availableTemplates,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
// Simple workflow - use generated spec directly
|
|
1065
|
+
const spec = genResult.spec;
|
|
1066
|
+
// Override spec name with provided name if given (args.name takes precedence over parsed name)
|
|
1067
|
+
if (args.name) {
|
|
1068
|
+
spec.name = args.name;
|
|
1069
|
+
}
|
|
1070
|
+
if (args.description) {
|
|
1071
|
+
spec.description = args.description;
|
|
1072
|
+
}
|
|
1073
|
+
// Validate spec against API schemas BEFORE compiling (if schema registry available)
|
|
1074
|
+
let specValidation = null;
|
|
1075
|
+
if (schemaRegistry) {
|
|
1076
|
+
specValidation = validateWorkflowSpec(spec, schemaRegistry);
|
|
1077
|
+
if (!specValidation.valid) {
|
|
1078
|
+
return {
|
|
1079
|
+
status: "validation_failed",
|
|
1080
|
+
errors: specValidation.errors,
|
|
1081
|
+
warnings: specValidation.warnings,
|
|
1082
|
+
action_coverage: specValidation.action_coverage,
|
|
1083
|
+
hint: "Fix the validation errors and try again. Unknown actions may need to be checked against ListActions.",
|
|
1084
|
+
spec_attempted: spec,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const compiled = compileWorkflow(spec, { registry: actionRegistry });
|
|
1089
|
+
// Validate the generated workflow (structural checks)
|
|
768
1090
|
const issues = detectWorkflowIssues(compiled.workflow_def);
|
|
769
1091
|
const result = {
|
|
770
1092
|
mode: "generate",
|
|
@@ -773,6 +1095,14 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
773
1095
|
proto_config: compiled.proto_config,
|
|
774
1096
|
validation: parseResult.validation,
|
|
775
1097
|
};
|
|
1098
|
+
// Include API validation results if available
|
|
1099
|
+
if (specValidation) {
|
|
1100
|
+
result.api_validation = {
|
|
1101
|
+
valid: specValidation.valid,
|
|
1102
|
+
warnings: specValidation.warnings,
|
|
1103
|
+
action_coverage: specValidation.action_coverage,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
776
1106
|
if (issues.length > 0) {
|
|
777
1107
|
result.issues = issues;
|
|
778
1108
|
}
|
|
@@ -790,18 +1120,19 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
790
1120
|
result.deployed_to = { persona_id: personaId, persona_name: persona.name };
|
|
791
1121
|
}
|
|
792
1122
|
else if (!preview && !personaId) {
|
|
793
|
-
// GREENFIELD: Create new persona from template, then
|
|
794
|
-
// The template provides a valid workflow structure - we don't replace it,
|
|
795
|
-
// we just configure the persona settings (voice config, welcome message, etc.)
|
|
1123
|
+
// GREENFIELD: Create new persona from template, then deploy workflow + proto_config
|
|
796
1124
|
const personaName = args.name;
|
|
797
1125
|
if (personaName) {
|
|
798
1126
|
const personaType = args.type || "chat";
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1127
|
+
// Try API-driven template lookup first, fallback to hardcoded IDs
|
|
1128
|
+
const FALLBACK_TEMPLATES = {
|
|
1129
|
+
voice: "00000000-0000-0000-0000-00000000001e", // Voice AI template (fallback)
|
|
1130
|
+
chat: "00000000-0000-0000-0000-000000000004", // Chat AI template (fallback)
|
|
1131
|
+
dashboard: "00000000-0000-0000-0000-000000000002", // Dashboard AI template (fallback)
|
|
803
1132
|
};
|
|
804
|
-
|
|
1133
|
+
// Use registry to find template by type
|
|
1134
|
+
const templateFromRegistry = actionRegistry.getTemplateForType(personaType);
|
|
1135
|
+
const templateId = getTemplateId?.(personaType) || templateFromRegistry?.id || FALLBACK_TEMPLATES[personaType];
|
|
805
1136
|
if (!templateId) {
|
|
806
1137
|
return {
|
|
807
1138
|
error: `No template found for type "${personaType}". Provide template_id or use type: voice, chat, or dashboard.`
|
|
@@ -859,12 +1190,57 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
859
1190
|
...existingProtoConfig,
|
|
860
1191
|
widgets: Array.from(widgetMap.values()),
|
|
861
1192
|
};
|
|
862
|
-
// Step 4:
|
|
863
|
-
//
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1193
|
+
// Step 4: Deploy BOTH workflow AND proto_config for ALL persona types
|
|
1194
|
+
// Voice AI IS driven by workflow (not just identityAndPurpose)
|
|
1195
|
+
const existingWorkflow = newPersona.workflow_def;
|
|
1196
|
+
const existingWfName = existingWorkflow?.workflowName;
|
|
1197
|
+
// Clone our compiled workflow and set the namespace from the existing workflow
|
|
1198
|
+
const workflowForDeploy = JSON.parse(JSON.stringify(compiled.workflow_def));
|
|
1199
|
+
const wfName = workflowForDeploy.workflowName;
|
|
1200
|
+
if (wfName?.name && existingWfName?.name) {
|
|
1201
|
+
// Copy the exact namespace from the template workflow
|
|
1202
|
+
wfName.name.namespaces = existingWfName.name.namespaces;
|
|
1203
|
+
wfName.name.name = existingWfName.name.name; // template_id
|
|
1204
|
+
}
|
|
1205
|
+
// Fix results format - use "<actionName>.<outputName>" keys
|
|
1206
|
+
const compiledResults = workflowForDeploy.results;
|
|
1207
|
+
if (compiledResults) {
|
|
1208
|
+
const newResults = {};
|
|
1209
|
+
for (const [, value] of Object.entries(compiledResults)) {
|
|
1210
|
+
if (value.actionName && value.outputName) {
|
|
1211
|
+
const key = `${value.actionName}.${value.outputName}`;
|
|
1212
|
+
newResults[key] = value;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
workflowForDeploy.results = newResults;
|
|
1216
|
+
}
|
|
1217
|
+
try {
|
|
1218
|
+
await client.updateAiEmployee({
|
|
1219
|
+
persona_id: newPersonaId,
|
|
1220
|
+
workflow: workflowForDeploy,
|
|
1221
|
+
proto_config: mergedProtoConfig,
|
|
1222
|
+
}, { verbose: true });
|
|
1223
|
+
}
|
|
1224
|
+
catch (deployError) {
|
|
1225
|
+
// Workflow deploy failed - still set proto_config
|
|
1226
|
+
const errMsg = deployError instanceof Error ? deployError.message : String(deployError);
|
|
1227
|
+
await client.updateAiEmployee({
|
|
1228
|
+
persona_id: newPersonaId,
|
|
1229
|
+
proto_config: mergedProtoConfig,
|
|
1230
|
+
});
|
|
1231
|
+
result.workflow_deploy_error = errMsg;
|
|
1232
|
+
result.workflow_attempted = workflowForDeploy;
|
|
1233
|
+
result.status = "partial";
|
|
1234
|
+
result.hint = "Persona created, config set, but workflow deploy failed. Check workflow_attempted for details.";
|
|
1235
|
+
result.deployed_to = {
|
|
1236
|
+
persona_id: newPersonaId,
|
|
1237
|
+
persona_name: personaName,
|
|
1238
|
+
created: true,
|
|
1239
|
+
workflow_deployed: false,
|
|
1240
|
+
};
|
|
1241
|
+
return result;
|
|
1242
|
+
}
|
|
1243
|
+
// Success - workflow deployed for all persona types
|
|
868
1244
|
result.deployed_to = {
|
|
869
1245
|
persona_id: newPersonaId,
|
|
870
1246
|
persona_name: personaName,
|
|
@@ -904,6 +1280,88 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
904
1280
|
if (!persona) {
|
|
905
1281
|
return { error: `Persona not found: ${personaId}` };
|
|
906
1282
|
}
|
|
1283
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1284
|
+
// CONFIG-ONLY DETECTION: Route to proto_config update instead of workflow
|
|
1285
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1286
|
+
const lowerInput = input.toLowerCase();
|
|
1287
|
+
const isConfigOnlyUpdate = ((lowerInput.includes("voice settings") || lowerInput.includes("voice config")) ||
|
|
1288
|
+
(lowerInput.includes("welcome message") && !lowerInput.includes("add") && !lowerInput.includes("node")) ||
|
|
1289
|
+
(lowerInput.includes("identity") && lowerInput.includes("purpose")) ||
|
|
1290
|
+
(lowerInput.includes("speech characteristics")) ||
|
|
1291
|
+
(lowerInput.includes("hangup instructions") || lowerInput.includes("hangup")) ||
|
|
1292
|
+
(lowerInput.includes("update") && (lowerInput.includes("persona settings") ||
|
|
1293
|
+
lowerInput.includes("conversation settings") ||
|
|
1294
|
+
lowerInput.includes("proto_config") ||
|
|
1295
|
+
lowerInput.includes("chat settings"))));
|
|
1296
|
+
if (isConfigOnlyUpdate) {
|
|
1297
|
+
// This is a config update, not a workflow modification
|
|
1298
|
+
// Parse the config values from the input and update proto_config only
|
|
1299
|
+
const existingProtoConfig = (persona.proto_config ?? {});
|
|
1300
|
+
const existingWidgets = (existingProtoConfig.widgets ?? []);
|
|
1301
|
+
// Find and update the conversationSettings widget
|
|
1302
|
+
const widgetMap = new Map();
|
|
1303
|
+
for (const w of existingWidgets) {
|
|
1304
|
+
if (typeof w.name === "string" && w.name.trim().length > 0) {
|
|
1305
|
+
widgetMap.set(w.name, { ...w });
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
// Parse voice config updates from input
|
|
1309
|
+
const configUpdates = {};
|
|
1310
|
+
// Extract welcome message
|
|
1311
|
+
const welcomeMatch = input.match(/welcome\s*message[:\s]*["']?([^"'\n]+(?:\n(?![0-9]+\.).*)*?)(?:["']|(?=\n\n|\n[0-9]+\.))/i);
|
|
1312
|
+
if (welcomeMatch) {
|
|
1313
|
+
configUpdates.welcomeMessage = welcomeMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1314
|
+
}
|
|
1315
|
+
// Extract identity and purpose
|
|
1316
|
+
const identityMatch = input.match(/identity\s*(?:and\s*)?purpose[:\s]*["']?(.+?)(?:["']|(?=\n\n[0-9]+\.|$))/is);
|
|
1317
|
+
if (identityMatch) {
|
|
1318
|
+
configUpdates.identityAndPurpose = identityMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1319
|
+
}
|
|
1320
|
+
// Extract speech characteristics
|
|
1321
|
+
const speechMatch = input.match(/speech\s*characteristics?[:\s]*["']?([^"'\n]+)["']?/i);
|
|
1322
|
+
if (speechMatch) {
|
|
1323
|
+
configUpdates.speechCharacteristics = speechMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1324
|
+
}
|
|
1325
|
+
// Extract hangup instructions
|
|
1326
|
+
const hangupMatch = input.match(/hangup\s*instructions?[:\s]*["']?(.+?)(?:["']|$)/is);
|
|
1327
|
+
if (hangupMatch) {
|
|
1328
|
+
configUpdates.hangupInstructions = hangupMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
1329
|
+
}
|
|
1330
|
+
// Update conversationSettings widget
|
|
1331
|
+
const convWidget = widgetMap.get("conversationSettings") || {
|
|
1332
|
+
name: "conversationSettings",
|
|
1333
|
+
type: 39,
|
|
1334
|
+
conversationSettings: {},
|
|
1335
|
+
};
|
|
1336
|
+
const convSettings = (convWidget.conversationSettings ?? {});
|
|
1337
|
+
// Merge config updates
|
|
1338
|
+
for (const [key, value] of Object.entries(configUpdates)) {
|
|
1339
|
+
convSettings[key] = value;
|
|
1340
|
+
}
|
|
1341
|
+
convWidget.conversationSettings = convSettings;
|
|
1342
|
+
widgetMap.set("conversationSettings", convWidget);
|
|
1343
|
+
const mergedProtoConfig = {
|
|
1344
|
+
...existingProtoConfig,
|
|
1345
|
+
widgets: Array.from(widgetMap.values()),
|
|
1346
|
+
};
|
|
1347
|
+
if (!preview) {
|
|
1348
|
+
// Deploy config update only - preserve existing workflow
|
|
1349
|
+
await client.updateAiEmployee({
|
|
1350
|
+
persona_id: personaId,
|
|
1351
|
+
proto_config: mergedProtoConfig,
|
|
1352
|
+
workflow: persona.workflow_def,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
mode: "config_update",
|
|
1357
|
+
status: preview ? "preview" : "deployed",
|
|
1358
|
+
persona_id: personaId,
|
|
1359
|
+
persona_name: persona.name,
|
|
1360
|
+
config_updates: configUpdates,
|
|
1361
|
+
note: "Updated persona configuration only. Workflow unchanged.",
|
|
1362
|
+
hint: "To modify the workflow itself, describe workflow changes like 'add search node' or 'add HITL before email'.",
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
907
1365
|
const existingWorkflow = persona.workflow_def;
|
|
908
1366
|
if (!existingWorkflow) {
|
|
909
1367
|
return {
|
|
@@ -1139,6 +1597,8 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
1139
1597
|
if (!nodes) {
|
|
1140
1598
|
return { error: "nodes required for compile mode" };
|
|
1141
1599
|
}
|
|
1600
|
+
// Load action registry for API-driven action versions/namespaces
|
|
1601
|
+
const compileRegistry = await ensureActionRegistry(client);
|
|
1142
1602
|
const spec = {
|
|
1143
1603
|
name: args.name || "Compiled Workflow",
|
|
1144
1604
|
description: args.description || "Generated workflow",
|
|
@@ -1146,10 +1606,11 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
1146
1606
|
nodes: nodes,
|
|
1147
1607
|
result_mappings: resultMappings || [],
|
|
1148
1608
|
};
|
|
1149
|
-
const compiled = compileWorkflow(spec);
|
|
1609
|
+
const compiled = compileWorkflow(spec, { registry: compileRegistry });
|
|
1150
1610
|
return {
|
|
1151
1611
|
workflow_def: compiled.workflow_def,
|
|
1152
1612
|
proto_config: compiled.proto_config,
|
|
1613
|
+
registry_loaded: compileRegistry.isLoaded(),
|
|
1153
1614
|
};
|
|
1154
1615
|
}
|
|
1155
1616
|
case "optimize": {
|
|
@@ -1541,6 +2002,205 @@ export async function handleKnowledge(args, client, readFile) {
|
|
|
1541
2002
|
message: `Data source '${widgetName}' attached to node '${nodeName}'`,
|
|
1542
2003
|
};
|
|
1543
2004
|
}
|
|
2005
|
+
case "dashboard_rows": {
|
|
2006
|
+
// Get rows from a dashboard persona
|
|
2007
|
+
const persona = await client.getPersonaById(personaId);
|
|
2008
|
+
if (!persona) {
|
|
2009
|
+
return { error: `Persona not found: ${personaId}` };
|
|
2010
|
+
}
|
|
2011
|
+
const dashboardId = persona.workflow_dashboard_id;
|
|
2012
|
+
if (!dashboardId) {
|
|
2013
|
+
return {
|
|
2014
|
+
error: "This persona has no dashboard. Use dashboard_rows only for Dashboard-type AI Employees.",
|
|
2015
|
+
hint: "Check that the persona has trigger_type=DASHBOARD",
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
const limit = args.limit ?? 100;
|
|
2019
|
+
const result = await client.getDashboardRows(dashboardId, personaId, { limit });
|
|
2020
|
+
// Return schema and rows in a structured format
|
|
2021
|
+
return {
|
|
2022
|
+
persona_id: personaId,
|
|
2023
|
+
dashboard_id: dashboardId,
|
|
2024
|
+
dashboard_name: result.dashboardName,
|
|
2025
|
+
total_rows: result.totalCount,
|
|
2026
|
+
schema: {
|
|
2027
|
+
input_columns: result.schema.columns.filter(c => c.isInput),
|
|
2028
|
+
output_columns: result.schema.columns.filter(c => !c.isInput),
|
|
2029
|
+
},
|
|
2030
|
+
rows: result.rows.map(row => ({
|
|
2031
|
+
id: row.id,
|
|
2032
|
+
state: row.state,
|
|
2033
|
+
created_at: row.dashboardRowMetadata.createdAt,
|
|
2034
|
+
input_values: row.columnValues
|
|
2035
|
+
.filter(cv => result.schema.columns.find(c => c.columnId === cv.columnId)?.isInput)
|
|
2036
|
+
.map(cv => ({
|
|
2037
|
+
column: result.schema.columns.find(c => c.columnId === cv.columnId)?.name ?? cv.columnId,
|
|
2038
|
+
value: cv.value.stringValue ?? cv.value.documentCellValue?.documentValues?.[0]?.name ?? cv.value.arrayValue?.arrayValues?.[0]?.stringValue ?? "(complex)",
|
|
2039
|
+
})),
|
|
2040
|
+
})),
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
case "dashboard_clone": {
|
|
2044
|
+
const sourcePersonaId = args.source_persona_id;
|
|
2045
|
+
if (!sourcePersonaId) {
|
|
2046
|
+
return { error: "source_persona_id required for dashboard_clone" };
|
|
2047
|
+
}
|
|
2048
|
+
// Get source and target personas
|
|
2049
|
+
const [sourcePersona, targetPersona] = await Promise.all([
|
|
2050
|
+
client.getPersonaById(sourcePersonaId),
|
|
2051
|
+
client.getPersonaById(personaId),
|
|
2052
|
+
]);
|
|
2053
|
+
if (!sourcePersona) {
|
|
2054
|
+
return { error: `Source persona not found: ${sourcePersonaId}` };
|
|
2055
|
+
}
|
|
2056
|
+
if (!targetPersona) {
|
|
2057
|
+
return { error: `Target persona not found: ${personaId}` };
|
|
2058
|
+
}
|
|
2059
|
+
const sourceDashboardId = sourcePersona.workflow_dashboard_id;
|
|
2060
|
+
if (!sourceDashboardId) {
|
|
2061
|
+
return { error: "Source persona has no dashboard" };
|
|
2062
|
+
}
|
|
2063
|
+
// Dashboard operations require the persona to be enabled
|
|
2064
|
+
// Auto-enable the target persona for cloning
|
|
2065
|
+
const targetProtoConfig = targetPersona.proto_config;
|
|
2066
|
+
await client.updateAiEmployee({
|
|
2067
|
+
persona_id: personaId,
|
|
2068
|
+
proto_config: targetProtoConfig ?? {},
|
|
2069
|
+
enabled_by_user: true,
|
|
2070
|
+
});
|
|
2071
|
+
// Get source dashboard data
|
|
2072
|
+
const sourceRows = await client.getDashboardRows(sourceDashboardId, sourcePersonaId);
|
|
2073
|
+
if (sourceRows.rows.length === 0) {
|
|
2074
|
+
return {
|
|
2075
|
+
success: true,
|
|
2076
|
+
message: "No rows to clone - source dashboard is empty",
|
|
2077
|
+
source_rows: 0,
|
|
2078
|
+
cloned_rows: 0,
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
// Identify input columns
|
|
2082
|
+
const inputColumns = sourceRows.schema.columns.filter(c => c.isInput);
|
|
2083
|
+
// Clone each row
|
|
2084
|
+
const results = [];
|
|
2085
|
+
const sanitize = args.sanitize;
|
|
2086
|
+
const sanitizeExamples = args.sanitize_examples;
|
|
2087
|
+
// Create sanitization session if needed
|
|
2088
|
+
let sanitizationSession;
|
|
2089
|
+
if (sanitize) {
|
|
2090
|
+
sanitizationSession = new SanitizationSession();
|
|
2091
|
+
}
|
|
2092
|
+
for (const row of sourceRows.rows) {
|
|
2093
|
+
try {
|
|
2094
|
+
// Build inputs from row's input column values
|
|
2095
|
+
const inputs = [];
|
|
2096
|
+
for (const inputCol of inputColumns) {
|
|
2097
|
+
const colValue = row.columnValues.find(cv => cv.columnId === inputCol.columnId);
|
|
2098
|
+
if (!colValue)
|
|
2099
|
+
continue;
|
|
2100
|
+
// Handle different column types
|
|
2101
|
+
if (inputCol.columnType === "COLUMN_TYPE_DOCUMENT") {
|
|
2102
|
+
// Document columns - we need to fetch the actual file content
|
|
2103
|
+
const docs = colValue.value.documentCellValue?.documentValues ?? [];
|
|
2104
|
+
if (docs.length > 0) {
|
|
2105
|
+
// For documents, we'd need to download from source and re-upload
|
|
2106
|
+
// For now, add as placeholder - requires file content fetching
|
|
2107
|
+
results.push({
|
|
2108
|
+
source_row_id: row.id,
|
|
2109
|
+
status: "skipped",
|
|
2110
|
+
error: "Document cloning requires file content transfer (not yet implemented)",
|
|
2111
|
+
});
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
else if (inputCol.columnType === "COLUMN_TYPE_STRING") {
|
|
2116
|
+
let value = colValue.value.stringValue ?? "";
|
|
2117
|
+
// Sanitize if enabled
|
|
2118
|
+
if (sanitizationSession && value) {
|
|
2119
|
+
// Simple pattern-based sanitization for known types
|
|
2120
|
+
const detected = detectWithPatterns(value);
|
|
2121
|
+
for (const entity of detected) {
|
|
2122
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
2123
|
+
value = value.split(entity.value).join(replacement);
|
|
2124
|
+
}
|
|
2125
|
+
// Also apply any user-provided examples
|
|
2126
|
+
if (sanitizeExamples) {
|
|
2127
|
+
for (const example of sanitizeExamples) {
|
|
2128
|
+
if (value.includes(example)) {
|
|
2129
|
+
const replacement = sanitizationSession.getOrCreateReplacement(example, "unknown");
|
|
2130
|
+
value = value.split(example).join(replacement);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
inputs.push({
|
|
2136
|
+
name: inputCol.name,
|
|
2137
|
+
string_value: value,
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
else if (inputCol.columnType === "COLUMN_TYPE_ARRAY") {
|
|
2141
|
+
// Array values - take first value as string for simplicity
|
|
2142
|
+
const arrayVals = colValue.value.arrayValue?.arrayValues ?? [];
|
|
2143
|
+
if (arrayVals.length > 0) {
|
|
2144
|
+
let value = arrayVals[0].stringValue ?? "";
|
|
2145
|
+
if (sanitizationSession && value) {
|
|
2146
|
+
const detected = detectWithPatterns(value);
|
|
2147
|
+
for (const entity of detected) {
|
|
2148
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
2149
|
+
value = value.split(entity.value).join(replacement);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
inputs.push({
|
|
2153
|
+
name: inputCol.name,
|
|
2154
|
+
string_value: value,
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
if (inputs.length === 0) {
|
|
2160
|
+
results.push({
|
|
2161
|
+
source_row_id: row.id,
|
|
2162
|
+
status: "skipped",
|
|
2163
|
+
error: "No clonable input values found",
|
|
2164
|
+
});
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
// Upload the row to target dashboard
|
|
2168
|
+
const uploadResult = await client.uploadAndRunDashboardRow(personaId, inputs);
|
|
2169
|
+
results.push({
|
|
2170
|
+
source_row_id: row.id,
|
|
2171
|
+
target_row_id: uploadResult.row_id,
|
|
2172
|
+
status: "cloned",
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
catch (err) {
|
|
2176
|
+
results.push({
|
|
2177
|
+
source_row_id: row.id,
|
|
2178
|
+
status: "error",
|
|
2179
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
const clonedCount = results.filter(r => r.status === "cloned").length;
|
|
2184
|
+
const skippedCount = results.filter(r => r.status === "skipped").length;
|
|
2185
|
+
const errorCount = results.filter(r => r.status === "error").length;
|
|
2186
|
+
return {
|
|
2187
|
+
success: true,
|
|
2188
|
+
source_persona_id: sourcePersonaId,
|
|
2189
|
+
target_persona_id: personaId,
|
|
2190
|
+
source_rows: sourceRows.rows.length,
|
|
2191
|
+
cloned_rows: clonedCount,
|
|
2192
|
+
skipped_rows: skippedCount,
|
|
2193
|
+
error_rows: errorCount,
|
|
2194
|
+
sanitization_applied: !!sanitize,
|
|
2195
|
+
details: results,
|
|
2196
|
+
notes: [
|
|
2197
|
+
"Dashboard clone creates NEW rows in the target dashboard",
|
|
2198
|
+
"Workflows will re-run on the new rows to generate output columns",
|
|
2199
|
+
"Document columns require manual re-upload (file content transfer not implemented)",
|
|
2200
|
+
sanitize ? "Sanitization applied to string/array values" : null,
|
|
2201
|
+
].filter(Boolean),
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
1544
2204
|
default:
|
|
1545
2205
|
return { error: `Unknown mode: ${mode}` };
|
|
1546
2206
|
}
|
|
@@ -2502,6 +3162,94 @@ function sanitizeWorkflowForDeploy(workflow) {
|
|
|
2502
3162
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2503
3163
|
// Helper Functions
|
|
2504
3164
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3165
|
+
/**
|
|
3166
|
+
* Sanitize a persona's content (PII, company names, etc.)
|
|
3167
|
+
*
|
|
3168
|
+
* Uses two-pass approach:
|
|
3169
|
+
* 1. Detection pass: Find sensitive entities in all text fields
|
|
3170
|
+
* 2. Alignment pass: Apply ALL mappings for cross-field consistency
|
|
3171
|
+
*
|
|
3172
|
+
* Supports typed examples: strings (type inferred) or objects with explicit type.
|
|
3173
|
+
*/
|
|
3174
|
+
async function sanitizePersonaById(client, personaId, options = {}) {
|
|
3175
|
+
try {
|
|
3176
|
+
// Fetch full persona
|
|
3177
|
+
const persona = await client.getPersonaById(personaId);
|
|
3178
|
+
if (!persona) {
|
|
3179
|
+
return { success: false, preview: true, replacements_applied: 0, items_needing_review: 0, error: "Persona not found" };
|
|
3180
|
+
}
|
|
3181
|
+
// Build persona object for sanitization - include ALL text-containing fields
|
|
3182
|
+
const personaData = {
|
|
3183
|
+
name: persona.name,
|
|
3184
|
+
description: persona.description,
|
|
3185
|
+
proto_config: persona.proto_config,
|
|
3186
|
+
workflow_def: persona.workflow_def,
|
|
3187
|
+
};
|
|
3188
|
+
// Create sanitization session with default policy
|
|
3189
|
+
const session = new SanitizationSession();
|
|
3190
|
+
// Normalize examples to TypedExample format
|
|
3191
|
+
const typedExamples = options.examples?.map(ex => {
|
|
3192
|
+
if (typeof ex === "string")
|
|
3193
|
+
return ex;
|
|
3194
|
+
return {
|
|
3195
|
+
value: ex.value,
|
|
3196
|
+
type: ex.type,
|
|
3197
|
+
replacement: ex.replacement,
|
|
3198
|
+
};
|
|
3199
|
+
});
|
|
3200
|
+
// Run sanitization with two-pass approach
|
|
3201
|
+
const sanitizationOptions = {
|
|
3202
|
+
examples: typedExamples,
|
|
3203
|
+
auto_replace_threshold: "high", // Only auto-replace high confidence
|
|
3204
|
+
};
|
|
3205
|
+
const result = sanitizePersona(personaData, session, sanitizationOptions);
|
|
3206
|
+
// If preview mode or there are items needing review, return preview
|
|
3207
|
+
const isPreview = options.preview !== false;
|
|
3208
|
+
if (isPreview || result.summary.items_needing_review > 0) {
|
|
3209
|
+
// Build confirmation prompt with all replacements and items needing review
|
|
3210
|
+
const allReplacements = result.results.flatMap(r => r.result.replacements);
|
|
3211
|
+
const allNeedsReview = result.results.flatMap(r => r.result.needs_review);
|
|
3212
|
+
const confirmation = buildConfirmationPrompt(allReplacements, allNeedsReview);
|
|
3213
|
+
return {
|
|
3214
|
+
success: true,
|
|
3215
|
+
preview: true,
|
|
3216
|
+
replacements_applied: 0,
|
|
3217
|
+
items_needing_review: allNeedsReview.length,
|
|
3218
|
+
by_class: result.summary.by_class,
|
|
3219
|
+
by_type: result.summary.by_type,
|
|
3220
|
+
confirmation,
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
// Apply sanitization by updating the persona
|
|
3224
|
+
const sanitizedProtoConfig = result.sanitized.proto_config;
|
|
3225
|
+
const sanitizedWorkflow = result.sanitized.workflow_def;
|
|
3226
|
+
const sanitizedDescription = result.sanitized.description;
|
|
3227
|
+
// Update the persona with sanitized data
|
|
3228
|
+
await client.updateAiEmployee({
|
|
3229
|
+
persona_id: personaId,
|
|
3230
|
+
description: sanitizedDescription ?? persona.description,
|
|
3231
|
+
proto_config: (sanitizedProtoConfig ?? persona.proto_config ?? {}),
|
|
3232
|
+
workflow: sanitizedWorkflow ?? persona.workflow_def,
|
|
3233
|
+
});
|
|
3234
|
+
return {
|
|
3235
|
+
success: true,
|
|
3236
|
+
preview: false,
|
|
3237
|
+
by_class: result.summary.by_class,
|
|
3238
|
+
by_type: result.summary.by_type,
|
|
3239
|
+
replacements_applied: result.summary.total_replacements,
|
|
3240
|
+
items_needing_review: result.summary.items_needing_review,
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
catch (error) {
|
|
3244
|
+
return {
|
|
3245
|
+
success: false,
|
|
3246
|
+
preview: true,
|
|
3247
|
+
replacements_applied: 0,
|
|
3248
|
+
items_needing_review: 0,
|
|
3249
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
2505
3253
|
async function resolvePersona(client, identifier) {
|
|
2506
3254
|
// Try as UUID first
|
|
2507
3255
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|