@ema.co/mcp-toolkit 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/mcp/handlers-consolidated.js +608 -9
- package/dist/mcp/resources.js +124 -0
- package/dist/mcp/server.js +12 -2
- package/dist/mcp/tools-consolidated.js +18 -4
- package/dist/sdk/action-schema-parser.js +379 -0
- package/dist/sdk/client.js +735 -0
- package/dist/sdk/index.js +45 -2
- package/dist/sdk/intent-architect.js +883 -0
- package/dist/sdk/sanitizer.js +1121 -0
- package/dist/sdk/workflow-validator.js +221 -3
- package/docs/mcp-tools-guide.md +40 -2
- package/docs/tool-consolidation-v2.md +215 -0
- package/package.json +6 -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
- package/docs/.temp/datasource-attach.har +0 -198369
- package/docs/.temp/grpcweb.gar +0 -1
- package/docs/openapi.json +0 -8000
|
@@ -11,7 +11,9 @@ import { compileWorkflow } from "../sdk/workflow-generator.js";
|
|
|
11
11
|
import { ensureActionRegistry } from "../sdk/action-registry.js";
|
|
12
12
|
import { parseInput, intentToSpec, generateWorkflow } from "../sdk/workflow-intent.js";
|
|
13
13
|
import { ensureSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM } from "../sdk/workflow-validator.js";
|
|
14
|
+
import { runIntentArchitect } from "../sdk/intent-architect.js";
|
|
14
15
|
import { analyzeExecutionFlow, generateASCIIFlow } from "../sdk/workflow-execution-analyzer.js";
|
|
16
|
+
import { SanitizationSession, sanitizePersona, buildConfirmationPrompt, detectWithPatterns, } from "../sdk/sanitizer.js";
|
|
15
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
18
|
// Widget Validation Helpers
|
|
17
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -45,7 +47,7 @@ function validateWidgetsForApi(widgets) {
|
|
|
45
47
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
48
|
// ENV Handler
|
|
47
49
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
-
export async function handleEnv(_args, getEnvironments) {
|
|
50
|
+
export async function handleEnv(_args, getEnvironments, toolkit) {
|
|
49
51
|
const envs = getEnvironments();
|
|
50
52
|
return {
|
|
51
53
|
environments: envs.map(e => ({
|
|
@@ -53,6 +55,7 @@ export async function handleEnv(_args, getEnvironments) {
|
|
|
53
55
|
default: e.isDefault,
|
|
54
56
|
})),
|
|
55
57
|
count: envs.length,
|
|
58
|
+
toolkit: toolkit ?? { name: "unknown", version: "unknown" },
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -87,12 +90,17 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
87
90
|
// Standard persona operations (get, list, compare, version management)
|
|
88
91
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
92
|
// Determine effective mode
|
|
93
|
+
// BUG FIX: clone_from and name+type now properly route to "create" mode
|
|
90
94
|
let effectiveMode = mode;
|
|
91
95
|
if (!effectiveMode) {
|
|
92
96
|
if (args.templates)
|
|
93
97
|
effectiveMode = "templates";
|
|
94
98
|
else if (args.all || args.query || args.status || args.trigger_type)
|
|
95
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";
|
|
96
104
|
else if (idOrName)
|
|
97
105
|
effectiveMode = "get";
|
|
98
106
|
else
|
|
@@ -176,18 +184,267 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
176
184
|
if (!templateId && args.type) {
|
|
177
185
|
templateId = getTemplateId(args.type);
|
|
178
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
|
+
// Use trigger_type (root level) for reliable persona type detection
|
|
198
|
+
// trigger_type: 0=chat, 1=voice, 2=dashboard
|
|
199
|
+
const triggerType = sourcePersona.trigger_type;
|
|
200
|
+
if (triggerType === 1) {
|
|
201
|
+
sourcePersonaType = "voice";
|
|
202
|
+
}
|
|
203
|
+
else if (triggerType === 2) {
|
|
204
|
+
sourcePersonaType = "dashboard";
|
|
205
|
+
}
|
|
206
|
+
else if (triggerType === 0) {
|
|
207
|
+
sourcePersonaType = "chat";
|
|
208
|
+
}
|
|
209
|
+
sourceDashboardId = sourcePersona.workflow_dashboard_id;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Ignore errors in source persona detection
|
|
214
|
+
}
|
|
215
|
+
}
|
|
179
216
|
const result = await client.createAiEmployee({
|
|
180
217
|
name,
|
|
181
218
|
description: args.description,
|
|
182
219
|
template_id: templateId,
|
|
183
|
-
source_persona_id:
|
|
184
|
-
clone_data:
|
|
220
|
+
source_persona_id: cloneFrom,
|
|
221
|
+
clone_data: cloneData,
|
|
185
222
|
});
|
|
186
|
-
|
|
223
|
+
const newPersonaId = result.persona_id ?? result.id;
|
|
224
|
+
// Automatically clone dashboard rows if source is a dashboard persona
|
|
225
|
+
let dashboardCloneResult;
|
|
226
|
+
if (sourcePersonaType === "dashboard" && sourceDashboardId && newPersonaId && cloneData) {
|
|
227
|
+
try {
|
|
228
|
+
// Dashboard operations require the persona to be enabled
|
|
229
|
+
// Auto-enable the persona temporarily for cloning
|
|
230
|
+
const newPersona = await client.getPersonaById(newPersonaId);
|
|
231
|
+
const newPersonaProtoConfig = newPersona?.proto_config;
|
|
232
|
+
// Enable the persona so we can add dashboard rows
|
|
233
|
+
await client.updateAiEmployee({
|
|
234
|
+
persona_id: newPersonaId,
|
|
235
|
+
proto_config: newPersonaProtoConfig ?? {},
|
|
236
|
+
enabled_by_user: true,
|
|
237
|
+
});
|
|
238
|
+
// Get source dashboard rows
|
|
239
|
+
const sourceRows = await client.getDashboardRows(sourceDashboardId, sourcePersona.id);
|
|
240
|
+
if (sourceRows.rows.length > 0) {
|
|
241
|
+
// Identify input columns
|
|
242
|
+
const inputColumns = sourceRows.schema.columns.filter(c => c.isInput);
|
|
243
|
+
// Clone each row
|
|
244
|
+
const results = [];
|
|
245
|
+
const shouldSanitize = args.sanitize;
|
|
246
|
+
const sanitizeExamples = args.sanitize_examples;
|
|
247
|
+
// Create sanitization session if needed
|
|
248
|
+
let sanitizationSession;
|
|
249
|
+
if (shouldSanitize) {
|
|
250
|
+
sanitizationSession = new SanitizationSession();
|
|
251
|
+
}
|
|
252
|
+
// Track document inputs that couldn't be cloned (require manual re-upload)
|
|
253
|
+
const allSkippedDocuments = [];
|
|
254
|
+
for (const row of sourceRows.rows) {
|
|
255
|
+
try {
|
|
256
|
+
// Build inputs from row's input column values
|
|
257
|
+
const inputs = [];
|
|
258
|
+
const rowSkippedDocs = [];
|
|
259
|
+
for (const inputCol of inputColumns) {
|
|
260
|
+
const colValue = row.columnValues.find(cv => cv.columnId === inputCol.columnId);
|
|
261
|
+
if (!colValue)
|
|
262
|
+
continue;
|
|
263
|
+
// Handle different column types
|
|
264
|
+
if (inputCol.columnType === "COLUMN_TYPE_DOCUMENT") {
|
|
265
|
+
// Document inputs can't be auto-cloned (no download API for original files)
|
|
266
|
+
// Track them for manual re-upload notification
|
|
267
|
+
const docValues = colValue.value.documentCellValue?.documentValues ?? [];
|
|
268
|
+
for (const doc of docValues) {
|
|
269
|
+
if (!doc.deleted) {
|
|
270
|
+
rowSkippedDocs.push({
|
|
271
|
+
name: inputCol.name,
|
|
272
|
+
original_filename: doc.name
|
|
273
|
+
});
|
|
274
|
+
allSkippedDocuments.push({
|
|
275
|
+
column: inputCol.name,
|
|
276
|
+
filename: doc.name,
|
|
277
|
+
contentNodeId: doc.contentNodeId,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
else if (inputCol.columnType === "COLUMN_TYPE_STRING") {
|
|
284
|
+
let value = colValue.value.stringValue ?? "";
|
|
285
|
+
// Sanitize if enabled
|
|
286
|
+
if (sanitizationSession && value) {
|
|
287
|
+
const detected = detectWithPatterns(value);
|
|
288
|
+
for (const entity of detected) {
|
|
289
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
290
|
+
value = value.split(entity.value).join(replacement);
|
|
291
|
+
}
|
|
292
|
+
if (sanitizeExamples) {
|
|
293
|
+
for (const example of sanitizeExamples) {
|
|
294
|
+
if (value.includes(example)) {
|
|
295
|
+
const replacement = sanitizationSession.getOrCreateReplacement(example, "unknown");
|
|
296
|
+
value = value.split(example).join(replacement);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
inputs.push({ name: inputCol.name, string_value: value });
|
|
302
|
+
}
|
|
303
|
+
else if (inputCol.columnType === "COLUMN_TYPE_ARRAY") {
|
|
304
|
+
const arrayVals = colValue.value.arrayValue?.arrayValues ?? [];
|
|
305
|
+
if (arrayVals.length > 0) {
|
|
306
|
+
let value = arrayVals[0].stringValue ?? "";
|
|
307
|
+
if (sanitizationSession && value) {
|
|
308
|
+
const detected = detectWithPatterns(value);
|
|
309
|
+
for (const entity of detected) {
|
|
310
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
311
|
+
value = value.split(entity.value).join(replacement);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
inputs.push({ name: inputCol.name, string_value: value });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Note: COLUMN_TYPE_NUMBER and COLUMN_TYPE_BOOLEAN are not currently
|
|
318
|
+
// parsed from the dashboard rows protobuf response, so they're skipped
|
|
319
|
+
}
|
|
320
|
+
// If we have inputs (even if some docs were skipped), try to clone
|
|
321
|
+
if (inputs.length === 0) {
|
|
322
|
+
// Check if we had document-only inputs
|
|
323
|
+
if (rowSkippedDocs.length > 0) {
|
|
324
|
+
results.push({
|
|
325
|
+
source_row_id: row.id,
|
|
326
|
+
status: "skipped",
|
|
327
|
+
error: "Document-only inputs require manual re-upload",
|
|
328
|
+
skipped_documents: rowSkippedDocs,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
results.push({ source_row_id: row.id, status: "skipped", error: "No clonable input values" });
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
// Upload the row to target dashboard
|
|
337
|
+
const uploadResult = await client.uploadAndRunDashboardRow(newPersonaId, inputs);
|
|
338
|
+
const rowResult = {
|
|
339
|
+
source_row_id: row.id,
|
|
340
|
+
target_row_id: uploadResult.row_id,
|
|
341
|
+
status: rowSkippedDocs.length > 0 ? "partial" : "cloned",
|
|
342
|
+
};
|
|
343
|
+
if (rowSkippedDocs.length > 0) {
|
|
344
|
+
rowResult.skipped_documents = rowSkippedDocs;
|
|
345
|
+
}
|
|
346
|
+
results.push(rowResult);
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
results.push({ source_row_id: row.id, status: "error", error: err instanceof Error ? err.message : String(err) });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const clonedCount = results.filter(r => r.status === "cloned" || r.status === "partial").length;
|
|
353
|
+
dashboardCloneResult = {
|
|
354
|
+
source_rows: sourceRows.rows.length,
|
|
355
|
+
cloned_rows: clonedCount,
|
|
356
|
+
partial_rows: results.filter(r => r.status === "partial").length,
|
|
357
|
+
skipped_rows: results.filter(r => r.status === "skipped").length,
|
|
358
|
+
error_rows: results.filter(r => r.status === "error").length,
|
|
359
|
+
sanitization_applied: !!shouldSanitize,
|
|
360
|
+
// Debug info for troubleshooting
|
|
361
|
+
_debug: {
|
|
362
|
+
total_count_from_api: sourceRows.totalCount,
|
|
363
|
+
schema_columns_count: sourceRows.schema.columns.length,
|
|
364
|
+
input_columns_count: inputColumns.length,
|
|
365
|
+
schema_columns: sourceRows.schema.columns.map(c => ({
|
|
366
|
+
id: c.columnId,
|
|
367
|
+
name: c.name,
|
|
368
|
+
type: c.columnType,
|
|
369
|
+
isInput: c.isInput,
|
|
370
|
+
})),
|
|
371
|
+
first_row_id: sourceRows.rows[0]?.id?.substring(0, 100),
|
|
372
|
+
first_row_state: sourceRows.rows[0]?.state,
|
|
373
|
+
first_row_column_values_count: sourceRows.rows[0]?.columnValues?.length,
|
|
374
|
+
results_detail: results,
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
// Include document upload notice if any documents were skipped
|
|
378
|
+
if (allSkippedDocuments.length > 0) {
|
|
379
|
+
dashboardCloneResult.documents_require_manual_upload = {
|
|
380
|
+
count: allSkippedDocuments.length,
|
|
381
|
+
files: allSkippedDocuments.slice(0, 10), // Limit to first 10 for readability
|
|
382
|
+
note: "Document inputs cannot be auto-cloned. Re-upload original files to the cloned dashboard.",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
dashboardCloneResult = { source_rows: 0, cloned_rows: 0, message: "Source dashboard was empty" };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
dashboardCloneResult = { error: `Dashboard clone failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Handle workflow sanitization for ALL persona types (including dashboard)
|
|
395
|
+
// Dashboard rows are sanitized during clone loop above, but workflow still needs sanitization
|
|
396
|
+
const shouldSanitize = args.sanitize;
|
|
397
|
+
if (shouldSanitize && newPersonaId) {
|
|
398
|
+
const sanitizeResult = await sanitizePersonaById(client, newPersonaId, {
|
|
399
|
+
examples: args.sanitize_examples,
|
|
400
|
+
preview: args.preview ?? false, // Use preview arg, default to false (apply)
|
|
401
|
+
});
|
|
402
|
+
const createResult = {
|
|
403
|
+
success: true,
|
|
404
|
+
persona_id: newPersonaId,
|
|
405
|
+
name,
|
|
406
|
+
sanitization: sanitizeResult,
|
|
407
|
+
};
|
|
408
|
+
if (sourcePersonaType) {
|
|
409
|
+
createResult.source_persona_type = sourcePersonaType;
|
|
410
|
+
}
|
|
411
|
+
if (dashboardCloneResult) {
|
|
412
|
+
createResult.dashboard_data_clone = dashboardCloneResult;
|
|
413
|
+
}
|
|
414
|
+
return createResult;
|
|
415
|
+
}
|
|
416
|
+
const createResult = {
|
|
187
417
|
success: true,
|
|
188
|
-
persona_id:
|
|
418
|
+
persona_id: newPersonaId,
|
|
189
419
|
name,
|
|
190
420
|
};
|
|
421
|
+
if (sourcePersonaType) {
|
|
422
|
+
createResult.source_persona_type = sourcePersonaType;
|
|
423
|
+
}
|
|
424
|
+
if (dashboardCloneResult) {
|
|
425
|
+
createResult.dashboard_data_clone = dashboardCloneResult;
|
|
426
|
+
}
|
|
427
|
+
return createResult;
|
|
428
|
+
}
|
|
429
|
+
case "sanitize": {
|
|
430
|
+
// Standalone sanitize mode: sanitize an existing persona
|
|
431
|
+
if (!idOrName) {
|
|
432
|
+
return { error: "id required for sanitize mode" };
|
|
433
|
+
}
|
|
434
|
+
const persona = await resolvePersona(client, idOrName);
|
|
435
|
+
if (!persona) {
|
|
436
|
+
return { error: `Persona not found: ${idOrName}` };
|
|
437
|
+
}
|
|
438
|
+
const sanitizeResult = await sanitizePersonaById(client, persona.id, {
|
|
439
|
+
examples: args.sanitize_examples,
|
|
440
|
+
preview: args.preview,
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
mode: "sanitize",
|
|
444
|
+
persona_id: persona.id,
|
|
445
|
+
persona_name: persona.name,
|
|
446
|
+
...sanitizeResult,
|
|
447
|
+
};
|
|
191
448
|
}
|
|
192
449
|
case "update": {
|
|
193
450
|
if (!idOrName) {
|
|
@@ -792,8 +1049,6 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
792
1049
|
if (args.type) {
|
|
793
1050
|
parseResult.intent.persona_type = args.type;
|
|
794
1051
|
}
|
|
795
|
-
// Check if intent requires LLM-driven generation
|
|
796
|
-
const genResult = generateWorkflow(parseResult.intent);
|
|
797
1052
|
// Load schema registry for API-driven validation (graceful degradation if unavailable)
|
|
798
1053
|
let schemaRegistry;
|
|
799
1054
|
try {
|
|
@@ -803,9 +1058,66 @@ export async function handleWorkflow(args, client, getTemplateId) {
|
|
|
803
1058
|
// Schema registry unavailable - skip API validation
|
|
804
1059
|
schemaRegistry = null;
|
|
805
1060
|
}
|
|
1061
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1062
|
+
// PROGRESSIVE ENHANCEMENT: Use Intent Architect for moderate/complex requests
|
|
1063
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1064
|
+
//
|
|
1065
|
+
// Uses the canonical runIntentArchitect() entrypoint which:
|
|
1066
|
+
// - Detects complexity with typed scores (not enums)
|
|
1067
|
+
// - Returns qualification questions or LLM prompt
|
|
1068
|
+
// - Supports iterative refinement with previous_answers
|
|
1069
|
+
//
|
|
1070
|
+
// Configure rollout via max_complexity option (default: allow all levels)
|
|
1071
|
+
const maxComplexity = args.max_complexity || undefined;
|
|
1072
|
+
const inputStr = input; // Already validated above
|
|
1073
|
+
const architectResult = runIntentArchitect(inputStr, {
|
|
1074
|
+
persona_type: parseResult.intent.persona_type,
|
|
1075
|
+
available_integrations: schemaRegistry?.getAllActions().slice(0, 20).map(a => a.displayName || a.name),
|
|
1076
|
+
}, { max_complexity: maxComplexity });
|
|
1077
|
+
// For moderate/complex: return Intent Architect result with questions or prompt
|
|
1078
|
+
if (!architectResult.strategy.can_proceed) {
|
|
1079
|
+
let availableActions = [];
|
|
1080
|
+
let availableTemplates = [];
|
|
1081
|
+
// Enhance prompt with action catalog if available
|
|
1082
|
+
let enhancedPrompt = architectResult.prompt_package;
|
|
1083
|
+
if (schemaRegistry && enhancedPrompt) {
|
|
1084
|
+
const actionCatalog = generateActionCatalogForLLM(schemaRegistry);
|
|
1085
|
+
enhancedPrompt = {
|
|
1086
|
+
system: enhancedPrompt.system + "\n\n## Available Actions\n" + actionCatalog,
|
|
1087
|
+
user: enhancedPrompt.user,
|
|
1088
|
+
};
|
|
1089
|
+
availableActions = schemaRegistry.getAllActions().map(a => a.name);
|
|
1090
|
+
availableTemplates = schemaRegistry.getAllTemplates().map(t => ({ id: t.id, name: t.name, type: t.type }));
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
status: "needs_intent_architect",
|
|
1094
|
+
// Typed assessment (new)
|
|
1095
|
+
assessment: architectResult.assessment,
|
|
1096
|
+
// Strategy decision (new)
|
|
1097
|
+
strategy: architectResult.strategy,
|
|
1098
|
+
// Qualification questions (new)
|
|
1099
|
+
questions: architectResult.questions,
|
|
1100
|
+
// LLM prompt (if full architect)
|
|
1101
|
+
llm_prompt: enhancedPrompt,
|
|
1102
|
+
// Hint for what to do next
|
|
1103
|
+
hint: architectResult.strategy.next_step,
|
|
1104
|
+
// Legacy fields for backward compatibility
|
|
1105
|
+
reason: architectResult.legacy?.signals.reason,
|
|
1106
|
+
complexity: architectResult.legacy?.complexity,
|
|
1107
|
+
approach: architectResult.strategy.approach,
|
|
1108
|
+
gates_to_ask: architectResult.strategy.gates_to_ask,
|
|
1109
|
+
// Also provide basic spec as fallback
|
|
1110
|
+
fallback_spec: intentToSpec(parseResult.intent),
|
|
1111
|
+
// Include catalogs for reference (if available)
|
|
1112
|
+
available_actions: availableActions,
|
|
1113
|
+
available_templates: availableTemplates,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
// SIMPLE complexity: Check if intent requires LLM-driven generation (legacy path)
|
|
1117
|
+
const genResult = generateWorkflow(parseResult.intent);
|
|
806
1118
|
if (genResult.needs_llm) {
|
|
807
|
-
// Complex workflow - return prompt for LLM to generate
|
|
808
|
-
//
|
|
1119
|
+
// Complex workflow detected by legacy system - return prompt for LLM to generate
|
|
1120
|
+
// This is a fallback for cases the new complexity analyzer might miss
|
|
809
1121
|
let enhancedPrompt = genResult.llm_prompt;
|
|
810
1122
|
let availableActions = [];
|
|
811
1123
|
let availableTemplates = [];
|
|
@@ -1772,6 +2084,205 @@ export async function handleKnowledge(args, client, readFile) {
|
|
|
1772
2084
|
message: `Data source '${widgetName}' attached to node '${nodeName}'`,
|
|
1773
2085
|
};
|
|
1774
2086
|
}
|
|
2087
|
+
case "dashboard_rows": {
|
|
2088
|
+
// Get rows from a dashboard persona
|
|
2089
|
+
const persona = await client.getPersonaById(personaId);
|
|
2090
|
+
if (!persona) {
|
|
2091
|
+
return { error: `Persona not found: ${personaId}` };
|
|
2092
|
+
}
|
|
2093
|
+
const dashboardId = persona.workflow_dashboard_id;
|
|
2094
|
+
if (!dashboardId) {
|
|
2095
|
+
return {
|
|
2096
|
+
error: "This persona has no dashboard. Use dashboard_rows only for Dashboard-type AI Employees.",
|
|
2097
|
+
hint: "Check that the persona has trigger_type=DASHBOARD",
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
const limit = args.limit ?? 100;
|
|
2101
|
+
const result = await client.getDashboardRows(dashboardId, personaId, { limit });
|
|
2102
|
+
// Return schema and rows in a structured format
|
|
2103
|
+
return {
|
|
2104
|
+
persona_id: personaId,
|
|
2105
|
+
dashboard_id: dashboardId,
|
|
2106
|
+
dashboard_name: result.dashboardName,
|
|
2107
|
+
total_rows: result.totalCount,
|
|
2108
|
+
schema: {
|
|
2109
|
+
input_columns: result.schema.columns.filter(c => c.isInput),
|
|
2110
|
+
output_columns: result.schema.columns.filter(c => !c.isInput),
|
|
2111
|
+
},
|
|
2112
|
+
rows: result.rows.map(row => ({
|
|
2113
|
+
id: row.id,
|
|
2114
|
+
state: row.state,
|
|
2115
|
+
created_at: row.dashboardRowMetadata.createdAt,
|
|
2116
|
+
input_values: row.columnValues
|
|
2117
|
+
.filter(cv => result.schema.columns.find(c => c.columnId === cv.columnId)?.isInput)
|
|
2118
|
+
.map(cv => ({
|
|
2119
|
+
column: result.schema.columns.find(c => c.columnId === cv.columnId)?.name ?? cv.columnId,
|
|
2120
|
+
value: cv.value.stringValue ?? cv.value.documentCellValue?.documentValues?.[0]?.name ?? cv.value.arrayValue?.arrayValues?.[0]?.stringValue ?? "(complex)",
|
|
2121
|
+
})),
|
|
2122
|
+
})),
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
case "dashboard_clone": {
|
|
2126
|
+
const sourcePersonaId = args.source_persona_id;
|
|
2127
|
+
if (!sourcePersonaId) {
|
|
2128
|
+
return { error: "source_persona_id required for dashboard_clone" };
|
|
2129
|
+
}
|
|
2130
|
+
// Get source and target personas
|
|
2131
|
+
const [sourcePersona, targetPersona] = await Promise.all([
|
|
2132
|
+
client.getPersonaById(sourcePersonaId),
|
|
2133
|
+
client.getPersonaById(personaId),
|
|
2134
|
+
]);
|
|
2135
|
+
if (!sourcePersona) {
|
|
2136
|
+
return { error: `Source persona not found: ${sourcePersonaId}` };
|
|
2137
|
+
}
|
|
2138
|
+
if (!targetPersona) {
|
|
2139
|
+
return { error: `Target persona not found: ${personaId}` };
|
|
2140
|
+
}
|
|
2141
|
+
const sourceDashboardId = sourcePersona.workflow_dashboard_id;
|
|
2142
|
+
if (!sourceDashboardId) {
|
|
2143
|
+
return { error: "Source persona has no dashboard" };
|
|
2144
|
+
}
|
|
2145
|
+
// Dashboard operations require the persona to be enabled
|
|
2146
|
+
// Auto-enable the target persona for cloning
|
|
2147
|
+
const targetProtoConfig = targetPersona.proto_config;
|
|
2148
|
+
await client.updateAiEmployee({
|
|
2149
|
+
persona_id: personaId,
|
|
2150
|
+
proto_config: targetProtoConfig ?? {},
|
|
2151
|
+
enabled_by_user: true,
|
|
2152
|
+
});
|
|
2153
|
+
// Get source dashboard data
|
|
2154
|
+
const sourceRows = await client.getDashboardRows(sourceDashboardId, sourcePersonaId);
|
|
2155
|
+
if (sourceRows.rows.length === 0) {
|
|
2156
|
+
return {
|
|
2157
|
+
success: true,
|
|
2158
|
+
message: "No rows to clone - source dashboard is empty",
|
|
2159
|
+
source_rows: 0,
|
|
2160
|
+
cloned_rows: 0,
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
// Identify input columns
|
|
2164
|
+
const inputColumns = sourceRows.schema.columns.filter(c => c.isInput);
|
|
2165
|
+
// Clone each row
|
|
2166
|
+
const results = [];
|
|
2167
|
+
const sanitize = args.sanitize;
|
|
2168
|
+
const sanitizeExamples = args.sanitize_examples;
|
|
2169
|
+
// Create sanitization session if needed
|
|
2170
|
+
let sanitizationSession;
|
|
2171
|
+
if (sanitize) {
|
|
2172
|
+
sanitizationSession = new SanitizationSession();
|
|
2173
|
+
}
|
|
2174
|
+
for (const row of sourceRows.rows) {
|
|
2175
|
+
try {
|
|
2176
|
+
// Build inputs from row's input column values
|
|
2177
|
+
const inputs = [];
|
|
2178
|
+
for (const inputCol of inputColumns) {
|
|
2179
|
+
const colValue = row.columnValues.find(cv => cv.columnId === inputCol.columnId);
|
|
2180
|
+
if (!colValue)
|
|
2181
|
+
continue;
|
|
2182
|
+
// Handle different column types
|
|
2183
|
+
if (inputCol.columnType === "COLUMN_TYPE_DOCUMENT") {
|
|
2184
|
+
// Document columns - we need to fetch the actual file content
|
|
2185
|
+
const docs = colValue.value.documentCellValue?.documentValues ?? [];
|
|
2186
|
+
if (docs.length > 0) {
|
|
2187
|
+
// For documents, we'd need to download from source and re-upload
|
|
2188
|
+
// For now, add as placeholder - requires file content fetching
|
|
2189
|
+
results.push({
|
|
2190
|
+
source_row_id: row.id,
|
|
2191
|
+
status: "skipped",
|
|
2192
|
+
error: "Document cloning requires file content transfer (not yet implemented)",
|
|
2193
|
+
});
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
else if (inputCol.columnType === "COLUMN_TYPE_STRING") {
|
|
2198
|
+
let value = colValue.value.stringValue ?? "";
|
|
2199
|
+
// Sanitize if enabled
|
|
2200
|
+
if (sanitizationSession && value) {
|
|
2201
|
+
// Simple pattern-based sanitization for known types
|
|
2202
|
+
const detected = detectWithPatterns(value);
|
|
2203
|
+
for (const entity of detected) {
|
|
2204
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
2205
|
+
value = value.split(entity.value).join(replacement);
|
|
2206
|
+
}
|
|
2207
|
+
// Also apply any user-provided examples
|
|
2208
|
+
if (sanitizeExamples) {
|
|
2209
|
+
for (const example of sanitizeExamples) {
|
|
2210
|
+
if (value.includes(example)) {
|
|
2211
|
+
const replacement = sanitizationSession.getOrCreateReplacement(example, "unknown");
|
|
2212
|
+
value = value.split(example).join(replacement);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
inputs.push({
|
|
2218
|
+
name: inputCol.name,
|
|
2219
|
+
string_value: value,
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
else if (inputCol.columnType === "COLUMN_TYPE_ARRAY") {
|
|
2223
|
+
// Array values - take first value as string for simplicity
|
|
2224
|
+
const arrayVals = colValue.value.arrayValue?.arrayValues ?? [];
|
|
2225
|
+
if (arrayVals.length > 0) {
|
|
2226
|
+
let value = arrayVals[0].stringValue ?? "";
|
|
2227
|
+
if (sanitizationSession && value) {
|
|
2228
|
+
const detected = detectWithPatterns(value);
|
|
2229
|
+
for (const entity of detected) {
|
|
2230
|
+
const replacement = sanitizationSession.getOrCreateReplacement(entity.value, entity.type);
|
|
2231
|
+
value = value.split(entity.value).join(replacement);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
inputs.push({
|
|
2235
|
+
name: inputCol.name,
|
|
2236
|
+
string_value: value,
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (inputs.length === 0) {
|
|
2242
|
+
results.push({
|
|
2243
|
+
source_row_id: row.id,
|
|
2244
|
+
status: "skipped",
|
|
2245
|
+
error: "No clonable input values found",
|
|
2246
|
+
});
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
// Upload the row to target dashboard
|
|
2250
|
+
const uploadResult = await client.uploadAndRunDashboardRow(personaId, inputs);
|
|
2251
|
+
results.push({
|
|
2252
|
+
source_row_id: row.id,
|
|
2253
|
+
target_row_id: uploadResult.row_id,
|
|
2254
|
+
status: "cloned",
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
catch (err) {
|
|
2258
|
+
results.push({
|
|
2259
|
+
source_row_id: row.id,
|
|
2260
|
+
status: "error",
|
|
2261
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
const clonedCount = results.filter(r => r.status === "cloned").length;
|
|
2266
|
+
const skippedCount = results.filter(r => r.status === "skipped").length;
|
|
2267
|
+
const errorCount = results.filter(r => r.status === "error").length;
|
|
2268
|
+
return {
|
|
2269
|
+
success: true,
|
|
2270
|
+
source_persona_id: sourcePersonaId,
|
|
2271
|
+
target_persona_id: personaId,
|
|
2272
|
+
source_rows: sourceRows.rows.length,
|
|
2273
|
+
cloned_rows: clonedCount,
|
|
2274
|
+
skipped_rows: skippedCount,
|
|
2275
|
+
error_rows: errorCount,
|
|
2276
|
+
sanitization_applied: !!sanitize,
|
|
2277
|
+
details: results,
|
|
2278
|
+
notes: [
|
|
2279
|
+
"Dashboard clone creates NEW rows in the target dashboard",
|
|
2280
|
+
"Workflows will re-run on the new rows to generate output columns",
|
|
2281
|
+
"Document columns require manual re-upload (file content transfer not implemented)",
|
|
2282
|
+
sanitize ? "Sanitization applied to string/array values" : null,
|
|
2283
|
+
].filter(Boolean),
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
1775
2286
|
default:
|
|
1776
2287
|
return { error: `Unknown mode: ${mode}` };
|
|
1777
2288
|
}
|
|
@@ -2733,6 +3244,94 @@ function sanitizeWorkflowForDeploy(workflow) {
|
|
|
2733
3244
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2734
3245
|
// Helper Functions
|
|
2735
3246
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3247
|
+
/**
|
|
3248
|
+
* Sanitize a persona's content (PII, company names, etc.)
|
|
3249
|
+
*
|
|
3250
|
+
* Uses two-pass approach:
|
|
3251
|
+
* 1. Detection pass: Find sensitive entities in all text fields
|
|
3252
|
+
* 2. Alignment pass: Apply ALL mappings for cross-field consistency
|
|
3253
|
+
*
|
|
3254
|
+
* Supports typed examples: strings (type inferred) or objects with explicit type.
|
|
3255
|
+
*/
|
|
3256
|
+
async function sanitizePersonaById(client, personaId, options = {}) {
|
|
3257
|
+
try {
|
|
3258
|
+
// Fetch full persona
|
|
3259
|
+
const persona = await client.getPersonaById(personaId);
|
|
3260
|
+
if (!persona) {
|
|
3261
|
+
return { success: false, preview: true, replacements_applied: 0, items_needing_review: 0, error: "Persona not found" };
|
|
3262
|
+
}
|
|
3263
|
+
// Build persona object for sanitization - include ALL text-containing fields
|
|
3264
|
+
const personaData = {
|
|
3265
|
+
name: persona.name,
|
|
3266
|
+
description: persona.description,
|
|
3267
|
+
proto_config: persona.proto_config,
|
|
3268
|
+
workflow_def: persona.workflow_def,
|
|
3269
|
+
};
|
|
3270
|
+
// Create sanitization session with default policy
|
|
3271
|
+
const session = new SanitizationSession();
|
|
3272
|
+
// Normalize examples to TypedExample format
|
|
3273
|
+
const typedExamples = options.examples?.map(ex => {
|
|
3274
|
+
if (typeof ex === "string")
|
|
3275
|
+
return ex;
|
|
3276
|
+
return {
|
|
3277
|
+
value: ex.value,
|
|
3278
|
+
type: ex.type,
|
|
3279
|
+
replacement: ex.replacement,
|
|
3280
|
+
};
|
|
3281
|
+
});
|
|
3282
|
+
// Run sanitization with two-pass approach
|
|
3283
|
+
const sanitizationOptions = {
|
|
3284
|
+
examples: typedExamples,
|
|
3285
|
+
auto_replace_threshold: "high", // Only auto-replace high confidence
|
|
3286
|
+
};
|
|
3287
|
+
const result = sanitizePersona(personaData, session, sanitizationOptions);
|
|
3288
|
+
// If preview mode or there are items needing review, return preview
|
|
3289
|
+
const isPreview = options.preview !== false;
|
|
3290
|
+
if (isPreview || result.summary.items_needing_review > 0) {
|
|
3291
|
+
// Build confirmation prompt with all replacements and items needing review
|
|
3292
|
+
const allReplacements = result.results.flatMap(r => r.result.replacements);
|
|
3293
|
+
const allNeedsReview = result.results.flatMap(r => r.result.needs_review);
|
|
3294
|
+
const confirmation = buildConfirmationPrompt(allReplacements, allNeedsReview);
|
|
3295
|
+
return {
|
|
3296
|
+
success: true,
|
|
3297
|
+
preview: true,
|
|
3298
|
+
replacements_applied: 0,
|
|
3299
|
+
items_needing_review: allNeedsReview.length,
|
|
3300
|
+
by_class: result.summary.by_class,
|
|
3301
|
+
by_type: result.summary.by_type,
|
|
3302
|
+
confirmation,
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
// Apply sanitization by updating the persona
|
|
3306
|
+
const sanitizedProtoConfig = result.sanitized.proto_config;
|
|
3307
|
+
const sanitizedWorkflow = result.sanitized.workflow_def;
|
|
3308
|
+
const sanitizedDescription = result.sanitized.description;
|
|
3309
|
+
// Update the persona with sanitized data
|
|
3310
|
+
await client.updateAiEmployee({
|
|
3311
|
+
persona_id: personaId,
|
|
3312
|
+
description: sanitizedDescription ?? persona.description,
|
|
3313
|
+
proto_config: (sanitizedProtoConfig ?? persona.proto_config ?? {}),
|
|
3314
|
+
workflow: sanitizedWorkflow ?? persona.workflow_def,
|
|
3315
|
+
});
|
|
3316
|
+
return {
|
|
3317
|
+
success: true,
|
|
3318
|
+
preview: false,
|
|
3319
|
+
by_class: result.summary.by_class,
|
|
3320
|
+
by_type: result.summary.by_type,
|
|
3321
|
+
replacements_applied: result.summary.total_replacements,
|
|
3322
|
+
items_needing_review: result.summary.items_needing_review,
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
catch (error) {
|
|
3326
|
+
return {
|
|
3327
|
+
success: false,
|
|
3328
|
+
preview: true,
|
|
3329
|
+
replacements_applied: 0,
|
|
3330
|
+
items_needing_review: 0,
|
|
3331
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
2736
3335
|
async function resolvePersona(client, identifier) {
|
|
2737
3336
|
// Try as UUID first
|
|
2738
3337
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|