@ema.co/mcp-toolkit 1.4.3 → 1.5.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.
- package/dist/mcp/handlers-consolidated.js +647 -85
- package/dist/mcp/tools-consolidated.js +73 -42
- package/dist/sdk/index.js +4 -0
- package/dist/sdk/knowledge.js +934 -0
- package/dist/sdk/workflow-execution-analyzer.js +412 -0
- package/dist/sdk/workflow-fixer.js +272 -0
- package/dist/sdk/workflow-transformer.js +600 -0
- package/docs/llm-native-workflow-design.md +252 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ 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
11
|
import { parseInput, intentToSpec } from "../sdk/workflow-intent.js";
|
|
12
|
+
import { analyzeExecutionFlow, generateASCIIFlow } from "../sdk/workflow-execution-analyzer.js";
|
|
12
13
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
14
|
// Widget Validation Helpers
|
|
14
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -541,46 +542,205 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
|
|
|
541
542
|
}
|
|
542
543
|
}
|
|
543
544
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
544
|
-
// WORKFLOW Handler
|
|
545
|
+
// WORKFLOW Handler - Unified greenfield/brownfield operations
|
|
545
546
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
546
547
|
export async function handleWorkflow(args, client) {
|
|
547
|
-
const mode = args.mode;
|
|
548
548
|
const personaId = args.persona_id;
|
|
549
549
|
const workflowDef = args.workflow_def;
|
|
550
550
|
const input = args.input;
|
|
551
|
+
const optimize = args.optimize;
|
|
552
|
+
const compareTo = args.compare_to;
|
|
551
553
|
// Default to preview=true for safety (no accidental deploys)
|
|
552
554
|
const preview = args.preview !== false;
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
// - workflow_def only → analyze
|
|
559
|
-
let effectiveMode = mode;
|
|
560
|
-
if (!effectiveMode) {
|
|
561
|
-
if (args.include)
|
|
562
|
-
effectiveMode = "analyze";
|
|
563
|
-
else if (workflowDef && !personaId)
|
|
564
|
-
effectiveMode = "analyze";
|
|
565
|
-
else if (personaId && input)
|
|
566
|
-
effectiveMode = "extend"; // BROWNFIELD: existing persona + new requirements
|
|
567
|
-
else if (personaId && !input)
|
|
568
|
-
effectiveMode = "analyze";
|
|
569
|
-
else if (input)
|
|
570
|
-
effectiveMode = "generate"; // GREENFIELD: new requirements only
|
|
571
|
-
else
|
|
572
|
-
effectiveMode = "analyze";
|
|
555
|
+
// Smart mode detection - user doesn't need to specify
|
|
556
|
+
// Priority: extraction_schema > compare > optimize > brownfield > greenfield > analyze
|
|
557
|
+
let effectiveMode;
|
|
558
|
+
if (args.mode === "extraction_schema" || args.metadata_file || args.metadata_json) {
|
|
559
|
+
effectiveMode = "extraction_schema";
|
|
573
560
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
561
|
+
else if (args.mode === "compile") {
|
|
562
|
+
// Explicit compile mode (for advanced users)
|
|
563
|
+
effectiveMode = "compile";
|
|
564
|
+
}
|
|
565
|
+
else if (compareTo) {
|
|
566
|
+
// Compare two workflows
|
|
567
|
+
effectiveMode = "compare";
|
|
568
|
+
}
|
|
569
|
+
else if (optimize && personaId) {
|
|
570
|
+
// Auto-fix issues
|
|
571
|
+
effectiveMode = "optimize";
|
|
572
|
+
}
|
|
573
|
+
else if (personaId && input) {
|
|
574
|
+
// BROWNFIELD: Modify existing workflow
|
|
575
|
+
effectiveMode = "modify"; // New unified mode for all brownfield changes
|
|
582
576
|
}
|
|
577
|
+
else if (input && !personaId) {
|
|
578
|
+
// GREENFIELD: Create new workflow
|
|
579
|
+
effectiveMode = "generate";
|
|
580
|
+
}
|
|
581
|
+
else if (personaId || workflowDef || args.include) {
|
|
582
|
+
// Just analyzing
|
|
583
|
+
effectiveMode = "analyze";
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
effectiveMode = "analyze";
|
|
587
|
+
}
|
|
588
|
+
// Legacy mode support (backwards compatibility)
|
|
589
|
+
const legacyMode = args.mode;
|
|
590
|
+
if (legacyMode === "extend")
|
|
591
|
+
effectiveMode = "modify";
|
|
592
|
+
if (legacyMode === "deploy" && personaId && input)
|
|
593
|
+
effectiveMode = "modify";
|
|
583
594
|
switch (effectiveMode) {
|
|
595
|
+
case "extraction_schema": {
|
|
596
|
+
// Generate extraction schema from JSON metadata (fully dynamic, no hardcoding)
|
|
597
|
+
const metadataFile = args.metadata_file;
|
|
598
|
+
const metadataJson = args.metadata_json;
|
|
599
|
+
if (!metadataFile && !metadataJson) {
|
|
600
|
+
return { error: "metadata_file or metadata_json required for extraction_schema mode" };
|
|
601
|
+
}
|
|
602
|
+
let metadata;
|
|
603
|
+
if (metadataJson) {
|
|
604
|
+
metadata = metadataJson;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// Read from file
|
|
608
|
+
const fs = await import("fs/promises");
|
|
609
|
+
const content = await fs.readFile(metadataFile, "utf8");
|
|
610
|
+
metadata = JSON.parse(content);
|
|
611
|
+
}
|
|
612
|
+
// Dynamically generate extraction schema (no hardcoding)
|
|
613
|
+
const extractionFields = [];
|
|
614
|
+
const jsonMapperFields = {};
|
|
615
|
+
// Parse entities section dynamically
|
|
616
|
+
const entities = metadata.entities;
|
|
617
|
+
if (entities) {
|
|
618
|
+
// Clients
|
|
619
|
+
if (entities.clients) {
|
|
620
|
+
const clientIds = Object.keys(entities.clients);
|
|
621
|
+
const firstClient = Object.values(entities.clients)[0];
|
|
622
|
+
extractionFields.push({
|
|
623
|
+
name: "client_id",
|
|
624
|
+
type: "string",
|
|
625
|
+
description: "Client identifier",
|
|
626
|
+
examples: clientIds.slice(0, 5),
|
|
627
|
+
required: false,
|
|
628
|
+
});
|
|
629
|
+
jsonMapperFields["client_id"] = "client_id";
|
|
630
|
+
extractionFields.push({
|
|
631
|
+
name: "client_name",
|
|
632
|
+
type: "string",
|
|
633
|
+
description: "Client full name",
|
|
634
|
+
examples: firstClient ? [String(firstClient.name || "")].filter(Boolean) : [],
|
|
635
|
+
required: false,
|
|
636
|
+
});
|
|
637
|
+
jsonMapperFields["client_name"] = "client_name";
|
|
638
|
+
// Dynamically extract all other client fields
|
|
639
|
+
if (firstClient) {
|
|
640
|
+
Object.keys(firstClient).forEach(key => {
|
|
641
|
+
if (key !== "name" && key !== "file") {
|
|
642
|
+
const value = firstClient[key];
|
|
643
|
+
const fieldType = typeof value === "boolean" ? "boolean" :
|
|
644
|
+
typeof value === "number" ? "number" : "string";
|
|
645
|
+
extractionFields.push({
|
|
646
|
+
name: `client_${key}`,
|
|
647
|
+
type: fieldType,
|
|
648
|
+
description: `Client ${key} (from metadata)`,
|
|
649
|
+
required: false,
|
|
650
|
+
});
|
|
651
|
+
jsonMapperFields[`client_${key}`] = `client_${key}`;
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// Advisors
|
|
657
|
+
if (entities.advisors) {
|
|
658
|
+
const advisorIds = Object.keys(entities.advisors);
|
|
659
|
+
extractionFields.push({
|
|
660
|
+
name: "advisor_id",
|
|
661
|
+
type: "string",
|
|
662
|
+
description: "Advisor identifier",
|
|
663
|
+
examples: advisorIds.slice(0, 5),
|
|
664
|
+
required: false,
|
|
665
|
+
});
|
|
666
|
+
jsonMapperFields["advisor_id"] = "advisor_id";
|
|
667
|
+
const advisorEmails = Object.values(entities.advisors)
|
|
668
|
+
.map((a) => a.email)
|
|
669
|
+
.filter(Boolean)
|
|
670
|
+
.slice(0, 5);
|
|
671
|
+
if (advisorEmails.length > 0) {
|
|
672
|
+
extractionFields.push({
|
|
673
|
+
name: "advisor_email",
|
|
674
|
+
type: "string",
|
|
675
|
+
description: "Advisor email address",
|
|
676
|
+
examples: advisorEmails,
|
|
677
|
+
required: false,
|
|
678
|
+
});
|
|
679
|
+
jsonMapperFields["advisor_email"] = "advisor_email";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// Stocks/Tickers
|
|
683
|
+
if (entities.stocks) {
|
|
684
|
+
const tickers = Object.keys(entities.stocks);
|
|
685
|
+
extractionFields.push({
|
|
686
|
+
name: "ticker_symbols",
|
|
687
|
+
type: "array",
|
|
688
|
+
description: "Stock ticker symbols",
|
|
689
|
+
examples: tickers.slice(0, 10),
|
|
690
|
+
required: false,
|
|
691
|
+
items: { type: "string" },
|
|
692
|
+
});
|
|
693
|
+
jsonMapperFields["ticker_symbols"] = "ticker_symbols";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Parse intents section
|
|
697
|
+
const intents = metadata.intents;
|
|
698
|
+
if (intents) {
|
|
699
|
+
const intentNames = Object.keys(intents);
|
|
700
|
+
extractionFields.push({
|
|
701
|
+
name: "intent",
|
|
702
|
+
type: "string",
|
|
703
|
+
description: "Detected intent from conversation",
|
|
704
|
+
examples: intentNames,
|
|
705
|
+
required: false,
|
|
706
|
+
});
|
|
707
|
+
jsonMapperFields["intent"] = "intent";
|
|
708
|
+
}
|
|
709
|
+
// Format extraction schema
|
|
710
|
+
const extractionSchema = {};
|
|
711
|
+
extractionFields.forEach(field => {
|
|
712
|
+
if (field.type === "array") {
|
|
713
|
+
extractionSchema[field.name] = {
|
|
714
|
+
type: "array",
|
|
715
|
+
items: field.items || { type: "string" },
|
|
716
|
+
description: field.description,
|
|
717
|
+
examples: field.examples,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
extractionSchema[field.name] = {
|
|
722
|
+
type: field.type,
|
|
723
|
+
description: field.description,
|
|
724
|
+
examples: field.examples,
|
|
725
|
+
required: field.required || false,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
// Generate fixed response template
|
|
730
|
+
const fixedResponseVars = Object.keys(jsonMapperFields);
|
|
731
|
+
const fixedResponseTemplate = fixedResponseVars.length > 0
|
|
732
|
+
? `Metadata Context:\n${fixedResponseVars.map(v => `- ${v}: {{${v}}}`).join("\n")}`
|
|
733
|
+
: "No metadata variables available";
|
|
734
|
+
return {
|
|
735
|
+
mode: "extraction_schema",
|
|
736
|
+
metadata_source: metadataFile || "inline_json",
|
|
737
|
+
extraction_columns: extractionSchema,
|
|
738
|
+
json_mapper_fields: jsonMapperFields,
|
|
739
|
+
fixed_response_template: fixedResponseTemplate,
|
|
740
|
+
fields_generated: extractionFields.length,
|
|
741
|
+
note: "This schema is dynamically generated from your metadata - no hardcoding. Use extraction_columns to configure entity_extraction_with_documents node.",
|
|
742
|
+
};
|
|
743
|
+
}
|
|
584
744
|
case "generate": {
|
|
585
745
|
if (!input) {
|
|
586
746
|
return { error: "input required for generate mode" };
|
|
@@ -635,13 +795,15 @@ export async function handleWorkflow(args, client) {
|
|
|
635
795
|
}
|
|
636
796
|
return result;
|
|
637
797
|
}
|
|
798
|
+
case "modify":
|
|
638
799
|
case "extend": {
|
|
639
|
-
// BROWNFIELD:
|
|
800
|
+
// BROWNFIELD: Intelligently modify existing workflow
|
|
801
|
+
// Handles: add, remove, replace, consolidate, rewire - all in one
|
|
640
802
|
if (!personaId) {
|
|
641
|
-
return { error: "persona_id required for
|
|
803
|
+
return { error: "persona_id required for workflow modifications" };
|
|
642
804
|
}
|
|
643
805
|
if (!input) {
|
|
644
|
-
return { error: "input required
|
|
806
|
+
return { error: "input required. Describe what you want to change." };
|
|
645
807
|
}
|
|
646
808
|
const persona = await client.getPersonaById(personaId);
|
|
647
809
|
if (!persona) {
|
|
@@ -650,35 +812,47 @@ export async function handleWorkflow(args, client) {
|
|
|
650
812
|
const existingWorkflow = persona.workflow_def;
|
|
651
813
|
if (!existingWorkflow) {
|
|
652
814
|
return {
|
|
653
|
-
error: `Persona "${persona.name}" has no workflow to
|
|
654
|
-
hint: "
|
|
815
|
+
error: `Persona "${persona.name}" has no workflow to modify`,
|
|
816
|
+
hint: "Provide just input= to create a new workflow (greenfield)",
|
|
655
817
|
};
|
|
656
818
|
}
|
|
657
|
-
//
|
|
658
|
-
const
|
|
659
|
-
//
|
|
660
|
-
const
|
|
661
|
-
// Validate the
|
|
662
|
-
const issues = detectWorkflowIssues(
|
|
819
|
+
// Intelligently analyze what the user wants to do
|
|
820
|
+
const changeAnalysis = analyzeModificationRequest(input, existingWorkflow);
|
|
821
|
+
// Apply the changes to the workflow
|
|
822
|
+
const modifyResult = applyWorkflowModifications(existingWorkflow, changeAnalysis);
|
|
823
|
+
// Validate the result
|
|
824
|
+
const issues = detectWorkflowIssues(modifyResult.workflow);
|
|
663
825
|
const fixes = issues.length > 0 ? suggestWorkflowFixes(issues) : [];
|
|
664
826
|
const result = {
|
|
665
|
-
mode: "
|
|
666
|
-
status: preview ? "preview" : "deploying",
|
|
827
|
+
mode: "modify",
|
|
828
|
+
status: preview ? "preview" : "deploying",
|
|
667
829
|
persona_id: personaId,
|
|
668
830
|
persona_name: persona.name,
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
831
|
+
// What we understood from the request
|
|
832
|
+
analysis: {
|
|
833
|
+
understood_intent: changeAnalysis.intent,
|
|
834
|
+
operations: changeAnalysis.operations.map(op => ({
|
|
835
|
+
type: op.type,
|
|
836
|
+
description: op.description,
|
|
837
|
+
})),
|
|
838
|
+
},
|
|
839
|
+
// What we did
|
|
840
|
+
changes_applied: modifyResult.changesApplied,
|
|
841
|
+
nodes_added: modifyResult.nodesAdded,
|
|
842
|
+
nodes_removed: modifyResult.nodesRemoved,
|
|
843
|
+
nodes_modified: modifyResult.nodesModified,
|
|
844
|
+
connections_changed: modifyResult.connectionsChanged,
|
|
845
|
+
// The result
|
|
846
|
+
modified_workflow: modifyResult.workflow,
|
|
673
847
|
};
|
|
674
848
|
if (issues.length > 0) {
|
|
675
|
-
result.
|
|
676
|
-
result.
|
|
849
|
+
result.remaining_issues = issues.length;
|
|
850
|
+
result.issues = issues.slice(0, 5); // Show first 5
|
|
851
|
+
result.suggested_fixes = fixes.slice(0, 5);
|
|
677
852
|
}
|
|
678
|
-
//
|
|
853
|
+
// Deploy if preview=false
|
|
679
854
|
if (!preview) {
|
|
680
|
-
|
|
681
|
-
const sanitizedWorkflow = sanitizeWorkflowForDeploy(mergeResult.workflow);
|
|
855
|
+
const sanitizedWorkflow = sanitizeWorkflowForDeploy(modifyResult.workflow);
|
|
682
856
|
try {
|
|
683
857
|
await client.updateAiEmployee({
|
|
684
858
|
persona_id: personaId,
|
|
@@ -687,42 +861,21 @@ export async function handleWorkflow(args, client) {
|
|
|
687
861
|
});
|
|
688
862
|
result.status = "deployed";
|
|
689
863
|
result.deployed = true;
|
|
690
|
-
result.
|
|
864
|
+
result.modified_workflow = sanitizedWorkflow;
|
|
691
865
|
}
|
|
692
866
|
catch (err) {
|
|
693
867
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
694
|
-
// Check if this is a validation error or server error
|
|
695
|
-
const isServerError = errorMessage.includes("Internal Server Error") || errorMessage.includes("500");
|
|
696
868
|
result.deployed = false;
|
|
697
869
|
result.deploy_error = errorMessage;
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
result.status = "preview_only";
|
|
701
|
-
result.hint = "Direct API deployment failed (common for complex changes). The merged workflow is correct - deploy it using deploy_workflow which can use Autobuilder as fallback.";
|
|
702
|
-
result.deploy_command = {
|
|
703
|
-
tool: "deploy_workflow",
|
|
704
|
-
args: {
|
|
705
|
-
persona_id: personaId,
|
|
706
|
-
workflow_def: "Use merged_workflow from this response",
|
|
707
|
-
validate_first: true,
|
|
708
|
-
auto_fix: true,
|
|
709
|
-
},
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
else {
|
|
713
|
-
// Other error - provide the message
|
|
714
|
-
result.hint = `Deployment failed: ${errorMessage}. Review the merged_workflow for issues.`;
|
|
715
|
-
}
|
|
870
|
+
result.status = "preview_only";
|
|
871
|
+
result.hint = `Deployment failed: ${errorMessage}. The modified_workflow is ready - you may need to deploy via UI for complex structural changes.`;
|
|
716
872
|
}
|
|
717
873
|
}
|
|
718
874
|
else {
|
|
719
875
|
result.next_steps = [
|
|
720
|
-
"Review
|
|
721
|
-
issues.length > 0 ?
|
|
722
|
-
|
|
723
|
-
? `Use action(suggest='...') to get agent recommendations for: ${mergeResult.newNodes.map(n => n.name).join(', ')}`
|
|
724
|
-
: null,
|
|
725
|
-
`Deploy with: workflow(mode="extend", persona_id="${personaId}", input="...", preview=false)`,
|
|
876
|
+
"Review the changes_applied and modified_workflow",
|
|
877
|
+
issues.length > 0 ? `Address ${issues.length} remaining issues` : null,
|
|
878
|
+
`Deploy with: workflow(persona_id="${personaId}", input="...", preview=false)`,
|
|
726
879
|
].filter(Boolean);
|
|
727
880
|
}
|
|
728
881
|
return result;
|
|
@@ -740,9 +893,14 @@ export async function handleWorkflow(args, client) {
|
|
|
740
893
|
if (!workflow) {
|
|
741
894
|
return { error: "No workflow to analyze. Provide workflow_def or persona_id." };
|
|
742
895
|
}
|
|
743
|
-
// Determine what to include
|
|
896
|
+
// Determine what to include (now supports execution_flow!)
|
|
744
897
|
const include = args.include || ["issues", "connections", "fixes", "metrics"];
|
|
745
|
-
const result = {
|
|
898
|
+
const result = {
|
|
899
|
+
mode: "analyze",
|
|
900
|
+
persona_id: personaId,
|
|
901
|
+
persona_name: persona?.name,
|
|
902
|
+
environment: "demo",
|
|
903
|
+
};
|
|
746
904
|
if (include.includes("issues") || include.includes("fixes")) {
|
|
747
905
|
const issues = detectWorkflowIssues(workflow);
|
|
748
906
|
if (include.includes("issues")) {
|
|
@@ -751,13 +909,13 @@ export async function handleWorkflow(args, client) {
|
|
|
751
909
|
if (include.includes("fixes")) {
|
|
752
910
|
result.fixes = suggestWorkflowFixes(issues);
|
|
753
911
|
}
|
|
754
|
-
// Always include summary even if only fixes requested
|
|
755
912
|
result.issue_summary = {
|
|
756
913
|
total: issues.length,
|
|
757
914
|
critical: issues.filter((i) => i.severity === "critical").length,
|
|
758
|
-
|
|
915
|
+
warning: issues.filter((i) => i.severity === "warning").length,
|
|
759
916
|
info: issues.filter((i) => i.severity === "info").length,
|
|
760
917
|
};
|
|
918
|
+
result.validation_passed = issues.filter((i) => i.severity === "critical").length === 0;
|
|
761
919
|
}
|
|
762
920
|
if (include.includes("connections")) {
|
|
763
921
|
result.connections = validateWorkflowConnections(workflow);
|
|
@@ -765,6 +923,26 @@ export async function handleWorkflow(args, client) {
|
|
|
765
923
|
if (include.includes("metrics")) {
|
|
766
924
|
result.metrics = calculateMetrics(workflow);
|
|
767
925
|
}
|
|
926
|
+
// NEW: Execution flow analysis - detects loops, multiple responders, redundant classifiers
|
|
927
|
+
if (include.includes("execution_flow")) {
|
|
928
|
+
const execAnalysis = analyzeExecutionFlow(workflow);
|
|
929
|
+
result.execution_flow = {
|
|
930
|
+
summary: execAnalysis.summary,
|
|
931
|
+
loops: execAnalysis.loops,
|
|
932
|
+
multiple_responder_issues: execAnalysis.multipleResponderIssues,
|
|
933
|
+
redundant_classifiers: execAnalysis.redundantClassifiers,
|
|
934
|
+
data_flow_issues: execAnalysis.dataFlowIssues,
|
|
935
|
+
dead_code_paths: execAnalysis.deadCodePaths,
|
|
936
|
+
};
|
|
937
|
+
// Include ASCII visualization if requested
|
|
938
|
+
if (args.visualize) {
|
|
939
|
+
result.execution_flow_ascii = generateASCIIFlow(execAnalysis);
|
|
940
|
+
}
|
|
941
|
+
// Add specific warnings for triple response risk
|
|
942
|
+
if (execAnalysis.summary.mayRepeatResponses) {
|
|
943
|
+
result.triple_response_warning = "⚠️ This workflow may cause duplicate/triple responses due to ungated parallel responders";
|
|
944
|
+
}
|
|
945
|
+
}
|
|
768
946
|
if (persona) {
|
|
769
947
|
result.persona = { id: persona.id, name: persona.name };
|
|
770
948
|
}
|
|
@@ -774,8 +952,7 @@ export async function handleWorkflow(args, client) {
|
|
|
774
952
|
const warningCount = issues?.filter(i => i.severity === "warning").length ?? 0;
|
|
775
953
|
if (criticalCount > 0 || warningCount > 0) {
|
|
776
954
|
result.optimization_suggestion = `This workflow has ${criticalCount} critical and ${warningCount} warning issues. ` +
|
|
777
|
-
`Use workflow(
|
|
778
|
-
`or workflow(mode="deploy", persona_id="${personaId ?? 'ID'}", auto_fix=true) to fix and deploy.`;
|
|
955
|
+
`Use workflow(persona_id="${personaId ?? 'ID'}", optimize=true) to auto-fix.`;
|
|
779
956
|
}
|
|
780
957
|
else {
|
|
781
958
|
result.status = "✅ Workflow is healthy - no issues detected";
|
|
@@ -1134,10 +1311,13 @@ export async function handleKnowledge(args, client, readFile) {
|
|
|
1134
1311
|
if (!persona) {
|
|
1135
1312
|
return { error: `Persona not found: ${personaId}` };
|
|
1136
1313
|
}
|
|
1314
|
+
// IMPORTANT: The Ema API requires workflow to be sent along with proto_config
|
|
1315
|
+
// for proto_config changes to persist. This matches what the UI does.
|
|
1137
1316
|
await client.updateAiEmployee({
|
|
1138
1317
|
persona_id: personaId,
|
|
1139
1318
|
embedding_enabled: args.enabled,
|
|
1140
1319
|
proto_config: persona.proto_config,
|
|
1320
|
+
workflow: persona.workflow_def,
|
|
1141
1321
|
});
|
|
1142
1322
|
return {
|
|
1143
1323
|
success: true,
|
|
@@ -1372,6 +1552,388 @@ export async function handleSync(args, createClient, getSyncOptions) {
|
|
|
1372
1552
|
return { error: `Unknown mode: ${mode}` };
|
|
1373
1553
|
}
|
|
1374
1554
|
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Analyze a natural language modification request and determine what operations are needed.
|
|
1557
|
+
* This is the "smart" part - understanding user intent.
|
|
1558
|
+
*/
|
|
1559
|
+
function analyzeModificationRequest(input, existingWorkflow) {
|
|
1560
|
+
const lowerInput = input.toLowerCase();
|
|
1561
|
+
const operations = [];
|
|
1562
|
+
let intent = "modify workflow";
|
|
1563
|
+
const actions = (existingWorkflow.actions || []);
|
|
1564
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1565
|
+
// CONSOLIDATION: "consolidate X agents into one", "merge", "combine"
|
|
1566
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1567
|
+
if (lowerInput.includes("consolidat") || lowerInput.includes("merge") ||
|
|
1568
|
+
lowerInput.includes("combine") || lowerInput.includes("into one") ||
|
|
1569
|
+
lowerInput.includes("single agent") || lowerInput.includes("unified")) {
|
|
1570
|
+
intent = "consolidate multiple nodes into one";
|
|
1571
|
+
// Find nodes to consolidate - look for patterns like "8 custom agents", "the agents"
|
|
1572
|
+
const customAgents = actions.filter(a => {
|
|
1573
|
+
const actionType = getActionType(a);
|
|
1574
|
+
return actionType === "custom_agent" || actionType.includes("custom");
|
|
1575
|
+
});
|
|
1576
|
+
if (customAgents.length > 1) {
|
|
1577
|
+
operations.push({
|
|
1578
|
+
type: "consolidate",
|
|
1579
|
+
description: `Consolidate ${customAgents.length} custom_agent nodes into a single unified agent`,
|
|
1580
|
+
target_nodes: customAgents.map(a => String(a.name || "")),
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1585
|
+
// REMOVAL: "remove", "delete", "clean up orphan"
|
|
1586
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1587
|
+
if (lowerInput.includes("remove") || lowerInput.includes("delete") ||
|
|
1588
|
+
lowerInput.includes("clean up") || lowerInput.includes("orphan")) {
|
|
1589
|
+
intent = "remove nodes";
|
|
1590
|
+
// Check if removing orphans specifically
|
|
1591
|
+
if (lowerInput.includes("orphan")) {
|
|
1592
|
+
// Find orphan nodes by checking reachability
|
|
1593
|
+
const orphanNodes = findOrphanNodes(existingWorkflow);
|
|
1594
|
+
if (orphanNodes.length > 0) {
|
|
1595
|
+
operations.push({
|
|
1596
|
+
type: "remove",
|
|
1597
|
+
description: `Remove ${orphanNodes.length} orphan nodes`,
|
|
1598
|
+
target_nodes: orphanNodes,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
// Check for specific node names mentioned
|
|
1603
|
+
const nodeNameMatch = input.match(/remove\s+(?:the\s+)?(\w+)/i);
|
|
1604
|
+
if (nodeNameMatch) {
|
|
1605
|
+
const nodeName = nodeNameMatch[1];
|
|
1606
|
+
const matchingNodes = actions.filter(a => String(a.name || "").toLowerCase().includes(nodeName.toLowerCase()));
|
|
1607
|
+
if (matchingNodes.length > 0) {
|
|
1608
|
+
operations.push({
|
|
1609
|
+
type: "remove",
|
|
1610
|
+
description: `Remove node(s) matching "${nodeName}"`,
|
|
1611
|
+
target_nodes: matchingNodes.map(a => String(a.name || "")),
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1617
|
+
// REPLACEMENT: "replace X with Y", "swap", "use X instead of Y"
|
|
1618
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1619
|
+
if (lowerInput.includes("replace") || lowerInput.includes("swap") ||
|
|
1620
|
+
lowerInput.includes("instead of") || lowerInput.includes("use entity_extraction")) {
|
|
1621
|
+
intent = "replace nodes";
|
|
1622
|
+
// Parse "replace X with Y" pattern
|
|
1623
|
+
const replaceMatch = input.match(/replace\s+(?:the\s+)?(.+?)\s+with\s+(.+)/i);
|
|
1624
|
+
if (replaceMatch) {
|
|
1625
|
+
operations.push({
|
|
1626
|
+
type: "replace",
|
|
1627
|
+
description: `Replace ${replaceMatch[1]} with ${replaceMatch[2]}`,
|
|
1628
|
+
source: replaceMatch[1],
|
|
1629
|
+
target: replaceMatch[2],
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
// Special case: "use entity_extraction" suggests replacing LLM-based extraction
|
|
1633
|
+
if (lowerInput.includes("entity_extraction")) {
|
|
1634
|
+
const llmNodes = actions.filter(a => {
|
|
1635
|
+
const actionType = getActionType(a);
|
|
1636
|
+
const displayName = a.displaySettings?.displayName || "";
|
|
1637
|
+
return (actionType === "call_llm" &&
|
|
1638
|
+
(displayName.toLowerCase().includes("email") ||
|
|
1639
|
+
displayName.toLowerCase().includes("extract")));
|
|
1640
|
+
});
|
|
1641
|
+
if (llmNodes.length > 0) {
|
|
1642
|
+
operations.push({
|
|
1643
|
+
type: "replace",
|
|
1644
|
+
description: `Replace ${llmNodes.length} LLM extraction nodes with entity_extraction`,
|
|
1645
|
+
target_nodes: llmNodes.map(a => String(a.name || "")),
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1651
|
+
// HITL: "add hitl", "add approval", "confirm before"
|
|
1652
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1653
|
+
if (lowerInput.includes("hitl") || lowerInput.includes("approval") ||
|
|
1654
|
+
lowerInput.includes("confirm") || lowerInput.includes("human")) {
|
|
1655
|
+
intent = "add human-in-the-loop";
|
|
1656
|
+
// Find the action to add HITL before
|
|
1657
|
+
const beforeMatch = input.match(/before\s+(\w+)/i);
|
|
1658
|
+
const targetAction = beforeMatch ? beforeMatch[1] : "email";
|
|
1659
|
+
// Find email/send nodes
|
|
1660
|
+
const emailNodes = actions.filter(a => {
|
|
1661
|
+
const actionType = getActionType(a);
|
|
1662
|
+
const nodeName = String(a.name || "").toLowerCase();
|
|
1663
|
+
return actionType.includes("email") || nodeName.includes("email") ||
|
|
1664
|
+
nodeName.includes("send") || nodeName.includes(targetAction.toLowerCase());
|
|
1665
|
+
});
|
|
1666
|
+
operations.push({
|
|
1667
|
+
type: "add",
|
|
1668
|
+
description: `Add HITL approval before ${emailNodes.length > 0 ? emailNodes[0].name : targetAction}`,
|
|
1669
|
+
new_node: {
|
|
1670
|
+
type: "general_hitl",
|
|
1671
|
+
name: `hitl_approval_${Date.now()}`,
|
|
1672
|
+
before: emailNodes.length > 0 ? String(emailNodes[0].name) : targetAction,
|
|
1673
|
+
},
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1677
|
+
// WIRING: "connect", "wire", "link X to Y"
|
|
1678
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1679
|
+
if (lowerInput.includes("connect") || lowerInput.includes("wire") ||
|
|
1680
|
+
lowerInput.includes("link")) {
|
|
1681
|
+
intent = "rewire connections";
|
|
1682
|
+
// Parse "connect X to Y" pattern
|
|
1683
|
+
const connectMatch = input.match(/(?:connect|wire|link)\s+(\w+)\s+to\s+(\w+)/i);
|
|
1684
|
+
if (connectMatch) {
|
|
1685
|
+
operations.push({
|
|
1686
|
+
type: "rewire",
|
|
1687
|
+
description: `Connect ${connectMatch[1]} to ${connectMatch[2]}`,
|
|
1688
|
+
source: connectMatch[1],
|
|
1689
|
+
target: connectMatch[2],
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1694
|
+
// ADD: "add", "include", "new"
|
|
1695
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1696
|
+
if (operations.length === 0 && (lowerInput.includes("add") || lowerInput.includes("new") || lowerInput.includes("include"))) {
|
|
1697
|
+
intent = "add new capabilities";
|
|
1698
|
+
// Use existing parseExtensionInput for additive changes
|
|
1699
|
+
const addChanges = parseExtensionInput(input);
|
|
1700
|
+
for (const change of addChanges) {
|
|
1701
|
+
operations.push({
|
|
1702
|
+
type: "add",
|
|
1703
|
+
description: change.description,
|
|
1704
|
+
config: change.details,
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
// Determine confidence based on how specific the operations are
|
|
1709
|
+
const confidence = operations.length > 0 && operations.every(op => op.target_nodes?.length || op.new_node) ? "high" :
|
|
1710
|
+
operations.length > 0 ? "medium" : "low";
|
|
1711
|
+
return { intent, operations, confidence };
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Find orphan nodes (not reachable from trigger).
|
|
1715
|
+
*/
|
|
1716
|
+
function findOrphanNodes(workflow) {
|
|
1717
|
+
const actions = (workflow.actions || []);
|
|
1718
|
+
const orphans = [];
|
|
1719
|
+
// Build a set of all referenced nodes (nodes that receive input from other nodes)
|
|
1720
|
+
const referencedNodes = new Set();
|
|
1721
|
+
referencedNodes.add("trigger"); // Trigger is always reachable
|
|
1722
|
+
// Find all nodes that are referenced in actionOutput bindings
|
|
1723
|
+
for (const action of actions) {
|
|
1724
|
+
const inputs = (action.inputs || {});
|
|
1725
|
+
for (const inputValue of Object.values(inputs)) {
|
|
1726
|
+
const binding = inputValue;
|
|
1727
|
+
if (binding?.actionOutput) {
|
|
1728
|
+
const actionOutput = binding.actionOutput;
|
|
1729
|
+
referencedNodes.add(String(actionOutput.actionName || ""));
|
|
1730
|
+
}
|
|
1731
|
+
// Check multiBinding
|
|
1732
|
+
if (binding?.multiBinding) {
|
|
1733
|
+
const multi = binding.multiBinding;
|
|
1734
|
+
const elements = (multi.elements || []);
|
|
1735
|
+
for (const elem of elements) {
|
|
1736
|
+
if (elem.namedBinding) {
|
|
1737
|
+
const named = elem.namedBinding;
|
|
1738
|
+
const value = named.value;
|
|
1739
|
+
if (value?.actionOutput) {
|
|
1740
|
+
const ao = value.actionOutput;
|
|
1741
|
+
referencedNodes.add(String(ao.actionName || ""));
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
// Find nodes that don't receive input from trigger chain
|
|
1749
|
+
// This is a simplified check - proper reachability would need graph traversal
|
|
1750
|
+
for (const action of actions) {
|
|
1751
|
+
const nodeName = String(action.name || "");
|
|
1752
|
+
const hasInputFromTrigger = checkNodeHasPath(nodeName, actions, "trigger", new Set());
|
|
1753
|
+
if (!hasInputFromTrigger && nodeName !== "trigger") {
|
|
1754
|
+
orphans.push(nodeName);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return orphans;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Check if a node has a path from source (simple BFS).
|
|
1761
|
+
*/
|
|
1762
|
+
function checkNodeHasPath(nodeName, actions, source, visited) {
|
|
1763
|
+
if (visited.has(nodeName))
|
|
1764
|
+
return false;
|
|
1765
|
+
visited.add(nodeName);
|
|
1766
|
+
const node = actions.find(a => a.name === nodeName);
|
|
1767
|
+
if (!node)
|
|
1768
|
+
return false;
|
|
1769
|
+
const inputs = (node.inputs || {});
|
|
1770
|
+
for (const inputValue of Object.values(inputs)) {
|
|
1771
|
+
const binding = inputValue;
|
|
1772
|
+
if (binding?.actionOutput) {
|
|
1773
|
+
const actionOutput = binding.actionOutput;
|
|
1774
|
+
const sourceNode = String(actionOutput.actionName || "");
|
|
1775
|
+
if (sourceNode === source)
|
|
1776
|
+
return true;
|
|
1777
|
+
if (checkNodeHasPath(sourceNode, actions, source, visited))
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Apply modifications to the workflow based on analyzed operations.
|
|
1785
|
+
*/
|
|
1786
|
+
function applyWorkflowModifications(existingWorkflow, analysis) {
|
|
1787
|
+
const workflow = JSON.parse(JSON.stringify(existingWorkflow));
|
|
1788
|
+
const actions = (workflow.actions || []);
|
|
1789
|
+
const enumTypes = (workflow.enumTypes || []);
|
|
1790
|
+
const results = (workflow.results || {});
|
|
1791
|
+
const changesApplied = [];
|
|
1792
|
+
const nodesAdded = [];
|
|
1793
|
+
const nodesRemoved = [];
|
|
1794
|
+
const nodesModified = [];
|
|
1795
|
+
const connectionsChanged = [];
|
|
1796
|
+
for (const op of analysis.operations) {
|
|
1797
|
+
switch (op.type) {
|
|
1798
|
+
case "consolidate": {
|
|
1799
|
+
// Consolidate multiple nodes into one unified agent
|
|
1800
|
+
const targetNodes = op.target_nodes || [];
|
|
1801
|
+
if (targetNodes.length < 2) {
|
|
1802
|
+
changesApplied.push("Consolidation skipped: need 2+ nodes to consolidate");
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
// Get the first node as template
|
|
1806
|
+
const templateIdx = actions.findIndex(a => a.name === targetNodes[0]);
|
|
1807
|
+
if (templateIdx < 0)
|
|
1808
|
+
break;
|
|
1809
|
+
const template = actions[templateIdx];
|
|
1810
|
+
// Create unified agent node
|
|
1811
|
+
const unifiedNode = {
|
|
1812
|
+
name: "unified_analysis_agent",
|
|
1813
|
+
action: template.action,
|
|
1814
|
+
inputs: {
|
|
1815
|
+
...template.inputs,
|
|
1816
|
+
// Update instructions to be dynamic
|
|
1817
|
+
role_instructions: {
|
|
1818
|
+
inline: {
|
|
1819
|
+
wellKnown: {
|
|
1820
|
+
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.",
|
|
1821
|
+
},
|
|
1822
|
+
},
|
|
1823
|
+
autoDetectedBinding: false,
|
|
1824
|
+
},
|
|
1825
|
+
task_instructions: {
|
|
1826
|
+
inline: {
|
|
1827
|
+
wellKnown: {
|
|
1828
|
+
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.",
|
|
1829
|
+
},
|
|
1830
|
+
},
|
|
1831
|
+
autoDetectedBinding: false,
|
|
1832
|
+
},
|
|
1833
|
+
},
|
|
1834
|
+
displaySettings: {
|
|
1835
|
+
displayName: "Unified Analysis Agent",
|
|
1836
|
+
description: `Consolidated from ${targetNodes.length} separate agents`,
|
|
1837
|
+
coordinates: template.displaySettings?.coordinates || { x: 2000, y: 1500 },
|
|
1838
|
+
},
|
|
1839
|
+
typeArguments: template.typeArguments || {},
|
|
1840
|
+
tools: template.tools || [],
|
|
1841
|
+
disableHumanInteraction: false,
|
|
1842
|
+
// No runIf - runs for all intents, uses internal routing
|
|
1843
|
+
};
|
|
1844
|
+
// Remove all target nodes
|
|
1845
|
+
for (const nodeName of targetNodes) {
|
|
1846
|
+
const idx = actions.findIndex(a => a.name === nodeName);
|
|
1847
|
+
if (idx >= 0) {
|
|
1848
|
+
actions.splice(idx, 1);
|
|
1849
|
+
nodesRemoved.push(nodeName);
|
|
1850
|
+
// Also remove from results mapping
|
|
1851
|
+
const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
|
|
1852
|
+
if (resultKey) {
|
|
1853
|
+
delete results[resultKey];
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
// Add unified node
|
|
1858
|
+
actions.push(unifiedNode);
|
|
1859
|
+
nodesAdded.push("unified_analysis_agent");
|
|
1860
|
+
// Add to results mapping
|
|
1861
|
+
results["unified_analysis_agent.response_with_sources"] = {
|
|
1862
|
+
actionName: "unified_analysis_agent",
|
|
1863
|
+
outputName: "response_with_sources",
|
|
1864
|
+
};
|
|
1865
|
+
changesApplied.push(`Consolidated ${targetNodes.length} nodes into unified_analysis_agent`);
|
|
1866
|
+
break;
|
|
1867
|
+
}
|
|
1868
|
+
case "remove": {
|
|
1869
|
+
const targetNodes = op.target_nodes || [];
|
|
1870
|
+
for (const nodeName of targetNodes) {
|
|
1871
|
+
const idx = actions.findIndex(a => a.name === nodeName);
|
|
1872
|
+
if (idx >= 0) {
|
|
1873
|
+
actions.splice(idx, 1);
|
|
1874
|
+
nodesRemoved.push(nodeName);
|
|
1875
|
+
// Also remove from results
|
|
1876
|
+
const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
|
|
1877
|
+
if (resultKey) {
|
|
1878
|
+
delete results[resultKey];
|
|
1879
|
+
}
|
|
1880
|
+
changesApplied.push(`Removed node: ${nodeName}`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
case "replace": {
|
|
1886
|
+
// For now, just note it - full replacement is complex
|
|
1887
|
+
changesApplied.push(`Replace operation noted: ${op.description}. Manual verification needed.`);
|
|
1888
|
+
break;
|
|
1889
|
+
}
|
|
1890
|
+
case "rewire": {
|
|
1891
|
+
const source = op.source;
|
|
1892
|
+
const target = op.target;
|
|
1893
|
+
if (source && target) {
|
|
1894
|
+
// Find target node and update its inputs
|
|
1895
|
+
const targetNode = actions.find(a => String(a.name || "").toLowerCase().includes(target.toLowerCase()));
|
|
1896
|
+
if (targetNode) {
|
|
1897
|
+
nodesModified.push(String(targetNode.name));
|
|
1898
|
+
connectionsChanged.push(`${source} → ${targetNode.name}`);
|
|
1899
|
+
changesApplied.push(`Rewiring: ${source} → ${targetNode.name}`);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
case "add": {
|
|
1905
|
+
if (op.new_node) {
|
|
1906
|
+
const newNode = op.new_node;
|
|
1907
|
+
const nodeName = String(newNode.name || `new_node_${Date.now()}`);
|
|
1908
|
+
nodesAdded.push(nodeName);
|
|
1909
|
+
changesApplied.push(`Added node: ${nodeName}`);
|
|
1910
|
+
}
|
|
1911
|
+
else if (op.config) {
|
|
1912
|
+
// Use legacy merge for additive changes
|
|
1913
|
+
const legacyChanges = parseExtensionInput(op.description);
|
|
1914
|
+
const legacyResult = mergeWorkflowChanges(workflow, legacyChanges);
|
|
1915
|
+
changesApplied.push(...legacyResult.changesApplied);
|
|
1916
|
+
}
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
case "modify_config": {
|
|
1920
|
+
changesApplied.push(`Config modification noted: ${op.description}`);
|
|
1921
|
+
break;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
workflow.actions = actions;
|
|
1926
|
+
workflow.enumTypes = enumTypes;
|
|
1927
|
+
workflow.results = results;
|
|
1928
|
+
return {
|
|
1929
|
+
workflow,
|
|
1930
|
+
changesApplied,
|
|
1931
|
+
nodesAdded,
|
|
1932
|
+
nodesRemoved,
|
|
1933
|
+
nodesModified,
|
|
1934
|
+
connectionsChanged,
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1375
1937
|
/**
|
|
1376
1938
|
* Parse natural language input to detect what changes are being requested.
|
|
1377
1939
|
* Handles multiple changes in one input (e.g., "add X, add Y, add Z").
|