From f4d0c1a5049be029b27a0efdce1e9a486c0f2af9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:30:41 +0000 Subject: [PATCH 1/3] Initial plan From c17e4aa19a297709a8dc6feaf612127dcfa0c3b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:40:20 +0000 Subject: [PATCH 2/3] Implement n8n workflow schema with npm-style packages Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- backend/autometabuilder/metadata.json | 4 +- .../packages/blank/package.json | 20 ++ .../packages/blank/workflow.json | 6 + .../contextual_iterative_loop/package.json | 24 ++ .../contextual_iterative_loop/workflow.json | 177 +++++++++++++++ .../packages/game_tick_loop/package.json | 24 ++ .../packages/game_tick_loop/workflow.json | 113 ++++++++++ .../packages/iterative_loop/package.json | 22 ++ .../packages/iterative_loop/workflow.json | 188 ++++++++++++++++ .../plan_execute_summarize/package.json | 22 ++ .../plan_execute_summarize/workflow.json | 174 +++++++++++++++ .../packages/repo_scan_context/package.json | 24 ++ .../packages/repo_scan_context/workflow.json | 209 ++++++++++++++++++ .../packages/single_pass/package.json | 22 ++ .../packages/single_pass/workflow.json | 151 +++++++++++++ .../packages/testing_triangle/package.json | 24 ++ .../packages/testing_triangle/workflow.json | 147 ++++++++++++ .../web/data/package_loader.py | 74 +++++++ backend/autometabuilder/web/data/workflow.py | 21 +- backend/autometabuilder/web/workflow_graph.py | 72 +++++- backend/autometabuilder/workflow.json | 148 ++++++++----- backend/autometabuilder/workflow/engine.py | 17 +- .../autometabuilder/workflow/n8n_converter.py | 110 +++++++++ .../autometabuilder/workflow/n8n_executor.py | 101 +++++++++ .../autometabuilder/workflow/n8n_schema.py | 72 ++++++ .../workflow/workflow_adapter.py | 51 +++++ .../workflow_packages/blank.json | 5 +- .../contextual_iterative_loop.json | 189 ++++++++++++---- .../workflow_packages/game_tick_loop.json | 132 +++++++---- .../workflow_packages/iterative_loop.json | 148 ++++++++----- .../plan_execute_summarize.json | 163 ++++++++++---- .../workflow_packages/repo_scan_context.json | 198 +++++++++++++++-- .../workflow_packages/single_pass.json | 103 +++++---- .../workflow_packages/testing_triangle.json | 139 +++++++++--- backend/tests/test_workflow_graph.py | 7 +- 35 files changed, 2747 insertions(+), 354 deletions(-) create mode 100644 backend/autometabuilder/packages/blank/package.json create mode 100644 backend/autometabuilder/packages/blank/workflow.json create mode 100644 backend/autometabuilder/packages/contextual_iterative_loop/package.json create mode 100644 backend/autometabuilder/packages/contextual_iterative_loop/workflow.json create mode 100644 backend/autometabuilder/packages/game_tick_loop/package.json create mode 100644 backend/autometabuilder/packages/game_tick_loop/workflow.json create mode 100644 backend/autometabuilder/packages/iterative_loop/package.json create mode 100644 backend/autometabuilder/packages/iterative_loop/workflow.json create mode 100644 backend/autometabuilder/packages/plan_execute_summarize/package.json create mode 100644 backend/autometabuilder/packages/plan_execute_summarize/workflow.json create mode 100644 backend/autometabuilder/packages/repo_scan_context/package.json create mode 100644 backend/autometabuilder/packages/repo_scan_context/workflow.json create mode 100644 backend/autometabuilder/packages/single_pass/package.json create mode 100644 backend/autometabuilder/packages/single_pass/workflow.json create mode 100644 backend/autometabuilder/packages/testing_triangle/package.json create mode 100644 backend/autometabuilder/packages/testing_triangle/workflow.json create mode 100644 backend/autometabuilder/web/data/package_loader.py create mode 100644 backend/autometabuilder/workflow/n8n_converter.py create mode 100644 backend/autometabuilder/workflow/n8n_executor.py create mode 100644 backend/autometabuilder/workflow/n8n_schema.py create mode 100644 backend/autometabuilder/workflow/workflow_adapter.py diff --git a/backend/autometabuilder/metadata.json b/backend/autometabuilder/metadata.json index 1d94d3b..cc24baa 100644 --- a/backend/autometabuilder/metadata.json +++ b/backend/autometabuilder/metadata.json @@ -1,7 +1,7 @@ { "tools_path": "tools", "workflow_path": "workflow.json", - "workflow_packages_path": "workflow_packages", + "workflow_packages_path": "packages", "messages": { "en": "messages/en", "es": "messages/es", @@ -14,4 +14,4 @@ "settings_descriptions_path": "metadata/settings_descriptions.json", "suggestions_path": "metadata/suggestions.json", "workflow_plugins_path": "metadata/workflow_plugins" -} +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/blank/package.json b/backend/autometabuilder/packages/blank/package.json new file mode 100644 index 0000000..1dec684 --- /dev/null +++ b/backend/autometabuilder/packages/blank/package.json @@ -0,0 +1,20 @@ +{ + "name": "blank", + "version": "1.0.0", + "description": "meta.workflow_packages.blank.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "starter" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.blank.label", + "description": "meta.workflow_packages.blank.description", + "tags": [ + "starter" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/blank/workflow.json b/backend/autometabuilder/packages/blank/workflow.json new file mode 100644 index 0000000..f469193 --- /dev/null +++ b/backend/autometabuilder/packages/blank/workflow.json @@ -0,0 +1,6 @@ +{ + "name": "Blank Canvas", + "active": false, + "nodes": [], + "connections": {} +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/contextual_iterative_loop/package.json b/backend/autometabuilder/packages/contextual_iterative_loop/package.json new file mode 100644 index 0000000..2734757 --- /dev/null +++ b/backend/autometabuilder/packages/contextual_iterative_loop/package.json @@ -0,0 +1,24 @@ +{ + "name": "contextual_iterative_loop", + "version": "1.0.0", + "description": "meta.workflow_packages.contextual_iterative_loop.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "context", + "loop", + "map-reduce" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.contextual_iterative_loop.label", + "description": "meta.workflow_packages.contextual_iterative_loop.description", + "tags": [ + "context", + "loop", + "map-reduce" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/contextual_iterative_loop/workflow.json b/backend/autometabuilder/packages/contextual_iterative_loop/workflow.json new file mode 100644 index 0000000..5e3b90a --- /dev/null +++ b/backend/autometabuilder/packages/contextual_iterative_loop/workflow.json @@ -0,0 +1,177 @@ +{ + "name": "meta.workflow_packages.contextual_iterative_loop.label", + "active": false, + "nodes": [ + { + "id": "list_files", + "name": "List Files", + "type": "tools.list_files", + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": { + "path": "." + } + }, + { + "id": "filter_python", + "name": "Filter Python", + "type": "utils.filter_list", + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { + "items": "$repo_files", + "mode": "regex", + "pattern": "\\.py$" + } + }, + { + "id": "map_python", + "name": "Map Python", + "type": "utils.map_list", + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "items": "$python_files", + "template": "PY: {item}" + } + }, + { + "id": "reduce_python", + "name": "Reduce Python", + "type": "utils.reduce_list", + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { + "items": "$python_lines", + "separator": "\\n" + } + }, + { + "id": "seed_messages", + "name": "Seed Messages", + "type": "core.seed_messages", + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": {} + }, + { + "id": "append_repo_summary", + "name": "Append Repo Summary", + "type": "core.append_context_message", + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { + "messages": "$messages", + "context": "$python_summary" + } + }, + { + "id": "append_user_instruction", + "name": "Append User Instruction", + "type": "core.append_user_instruction", + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { + "messages": "$messages" + } + }, + { + "id": "main_loop", + "name": "Main Loop", + "type": "control.loop", + "typeVersion": 1, + "position": [ + 2100, + 50 + ], + "parameters": { + "max_iterations": 5, + "stop_when": "$no_tool_calls", + "stop_on": "true" + } + } + ], + "connections": { + "List Files": { + "main": { + "0": [ + { + "node": "Filter Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Filter Python": { + "main": { + "0": [ + { + "node": "Map Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Map Python": { + "main": { + "0": [ + { + "node": "Reduce Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Append User Instruction": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + }, + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + } + ] + } + }, + "Reduce Python": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/game_tick_loop/package.json b/backend/autometabuilder/packages/game_tick_loop/package.json new file mode 100644 index 0000000..53ed5da --- /dev/null +++ b/backend/autometabuilder/packages/game_tick_loop/package.json @@ -0,0 +1,24 @@ +{ + "name": "game_tick_loop", + "version": "1.0.0", + "description": "meta.workflow_packages.game_tick_loop.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "game", + "loop", + "ticks" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.game_tick_loop.label", + "description": "meta.workflow_packages.game_tick_loop.description", + "tags": [ + "game", + "loop", + "ticks" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/game_tick_loop/workflow.json b/backend/autometabuilder/packages/game_tick_loop/workflow.json new file mode 100644 index 0000000..855b398 --- /dev/null +++ b/backend/autometabuilder/packages/game_tick_loop/workflow.json @@ -0,0 +1,113 @@ +{ + "name": "meta.workflow_packages.game_tick_loop.label", + "active": false, + "nodes": [ + { + "id": "seed_messages", + "name": "Seed Messages", + "type": "core.seed_messages", + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": {} + }, + { + "id": "map_ticks", + "name": "Map Ticks", + "type": "utils.map_list", + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { + "items": [ + "tick_start", + "tick_update", + "tick_render" + ], + "template": "Tick: {item}" + } + }, + { + "id": "reduce_ticks", + "name": "Reduce Ticks", + "type": "utils.reduce_list", + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "items": "$tick_lines", + "separator": "\\n" + } + }, + { + "id": "append_tick_context", + "name": "Append Tick Context", + "type": "core.append_context_message", + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { + "messages": "$messages", + "context": "$tick_context" + } + }, + { + "id": "main_loop", + "name": "Main Loop", + "type": "control.loop", + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { + "max_iterations": 3, + "stop_when": "$no_tool_calls", + "stop_on": "true" + } + } + ], + "connections": { + "Map Ticks": { + "main": { + "0": [ + { + "node": "Reduce Ticks", + "type": "main", + "index": 0 + } + ] + } + }, + "Append Tick Context": { + "main": { + "0": [ + { + "node": "Append Tick Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Reduce Ticks": { + "main": { + "0": [ + { + "node": "Append Tick Context", + "type": "main", + "index": 0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/iterative_loop/package.json b/backend/autometabuilder/packages/iterative_loop/package.json new file mode 100644 index 0000000..8a95cf0 --- /dev/null +++ b/backend/autometabuilder/packages/iterative_loop/package.json @@ -0,0 +1,22 @@ +{ + "name": "iterative_loop", + "version": "1.0.0", + "description": "meta.workflow_packages.iterative_loop.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "loop", + "tools" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.iterative_loop.label", + "description": "meta.workflow_packages.iterative_loop.description", + "tags": [ + "loop", + "tools" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/iterative_loop/workflow.json b/backend/autometabuilder/packages/iterative_loop/workflow.json new file mode 100644 index 0000000..f29890e --- /dev/null +++ b/backend/autometabuilder/packages/iterative_loop/workflow.json @@ -0,0 +1,188 @@ +{ + "name": "Iterative Agent Loop", + "active": false, + "nodes": [ + { + "id": "load_context", + "name": "Load Context", + "type": "core.load_context", + "typeVersion": 1, + "position": [ + 0, + 0 + ], + "parameters": {} + }, + { + "id": "seed_messages", + "name": "Seed Messages", + "type": "core.seed_messages", + "typeVersion": 1, + "position": [ + 0, + 100 + ], + "parameters": {} + }, + { + "id": "append_context", + "name": "Append Context", + "type": "core.append_context_message", + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": {} + }, + { + "id": "append_user_instruction", + "name": "Append User Instruction", + "type": "core.append_user_instruction", + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": {} + }, + { + "id": "main_loop", + "name": "Main Loop", + "type": "control.loop", + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { + "max_iterations": 10, + "stop_when": "$no_tool_calls", + "stop_on": "true" + } + }, + { + "id": "ai_request", + "name": "AI Request", + "type": "core.ai_request", + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": {} + }, + { + "id": "run_tool_calls", + "name": "Run Tool Calls", + "type": "core.run_tool_calls", + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": {} + }, + { + "id": "append_tool_results", + "name": "Append Tool Results", + "type": "core.append_tool_results", + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": {} + } + ], + "connections": { + "Load Context": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Seed Messages": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Append Context": { + "main": { + "0": [ + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + } + ] + } + }, + "Append User Instruction": { + "main": { + "0": [ + { + "node": "Main Loop", + "type": "main", + "index": 0 + } + ] + } + }, + "Main Loop": { + "main": { + "0": [ + { + "node": "AI Request", + "type": "main", + "index": 0 + } + ] + } + }, + "AI Request": { + "main": { + "0": [ + { + "node": "Run Tool Calls", + "type": "main", + "index": 0 + } + ] + } + }, + "Run Tool Calls": { + "main": { + "0": [ + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + }, + "Append Tool Results": { + "main": { + "0": [ + { + "node": "Main Loop", + "type": "main", + "index": 0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/plan_execute_summarize/package.json b/backend/autometabuilder/packages/plan_execute_summarize/package.json new file mode 100644 index 0000000..6dd6957 --- /dev/null +++ b/backend/autometabuilder/packages/plan_execute_summarize/package.json @@ -0,0 +1,22 @@ +{ + "name": "plan_execute_summarize", + "version": "1.0.0", + "description": "meta.workflow_packages.plan_execute_summarize.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "plan", + "summarize" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.plan_execute_summarize.label", + "description": "meta.workflow_packages.plan_execute_summarize.description", + "tags": [ + "plan", + "summarize" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/plan_execute_summarize/workflow.json b/backend/autometabuilder/packages/plan_execute_summarize/workflow.json new file mode 100644 index 0000000..00a54ec --- /dev/null +++ b/backend/autometabuilder/packages/plan_execute_summarize/workflow.json @@ -0,0 +1,174 @@ +{ + "name": "meta.workflow_packages.plan_execute_summarize.label", + "active": false, + "nodes": [ + { + "id": "load_context", + "name": "Load Context", + "type": "core.load_context", + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": {} + }, + { + "id": "seed_messages", + "name": "Seed Messages", + "type": "core.seed_messages", + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": {} + }, + { + "id": "append_context", + "name": "Append Context", + "type": "core.append_context_message", + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "messages": "$messages", + "context": "$sdlc_context" + } + }, + { + "id": "append_user_instruction", + "name": "Append User Instruction", + "type": "core.append_user_instruction", + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { + "messages": "$messages" + } + }, + { + "id": "planner_request", + "name": "Planner Request", + "type": "core.ai_request", + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { + "messages": "$messages" + } + }, + { + "id": "run_tool_calls", + "name": "Run Tool Calls", + "type": "core.run_tool_calls", + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { + "response": "$llm_response" + } + }, + { + "id": "append_tool_results", + "name": "Append Tool Results", + "type": "core.append_tool_results", + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { + "messages": "$messages", + "tool_results": "$tool_results" + } + }, + { + "id": "summary_request", + "name": "Summary Request", + "type": "core.ai_request", + "typeVersion": 1, + "position": [ + 2100, + 50 + ], + "parameters": { + "messages": "$messages" + } + } + ], + "connections": { + "Append Tool Results": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + }, + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + }, + { + "node": "Planner Request", + "type": "main", + "index": 0 + }, + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + }, + { + "node": "Summary Request", + "type": "main", + "index": 0 + } + ] + } + }, + "Load Context": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Planner Request": { + "main": { + "0": [ + { + "node": "Run Tool Calls", + "type": "main", + "index": 0 + } + ] + } + }, + "Run Tool Calls": { + "main": { + "0": [ + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/repo_scan_context/package.json b/backend/autometabuilder/packages/repo_scan_context/package.json new file mode 100644 index 0000000..138d0cf --- /dev/null +++ b/backend/autometabuilder/packages/repo_scan_context/package.json @@ -0,0 +1,24 @@ +{ + "name": "repo_scan_context", + "version": "1.0.0", + "description": "meta.workflow_packages.repo_scan_context.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "map", + "reduce", + "context" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.repo_scan_context.label", + "description": "meta.workflow_packages.repo_scan_context.description", + "tags": [ + "map", + "reduce", + "context" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/repo_scan_context/workflow.json b/backend/autometabuilder/packages/repo_scan_context/workflow.json new file mode 100644 index 0000000..7c6d7c5 --- /dev/null +++ b/backend/autometabuilder/packages/repo_scan_context/workflow.json @@ -0,0 +1,209 @@ +{ + "name": "meta.workflow_packages.repo_scan_context.label", + "active": false, + "nodes": [ + { + "id": "list_files", + "name": "List Files", + "type": "tools.list_files", + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": { + "path": "." + } + }, + { + "id": "filter_python", + "name": "Filter Python", + "type": "utils.filter_list", + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { + "items": "$repo_files", + "mode": "regex", + "pattern": "\\.py$" + } + }, + { + "id": "reduce_python", + "name": "Reduce Python", + "type": "utils.reduce_list", + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "items": "$python_files", + "separator": "\\n" + } + }, + { + "id": "seed_messages", + "name": "Seed Messages", + "type": "core.seed_messages", + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": {} + }, + { + "id": "append_repo_summary", + "name": "Append Repo Summary", + "type": "core.append_context_message", + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { + "messages": "$messages", + "context": "$python_summary" + } + }, + { + "id": "append_user_instruction", + "name": "Append User Instruction", + "type": "core.append_user_instruction", + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { + "messages": "$messages" + } + }, + { + "id": "ai_request", + "name": "Ai Request", + "type": "core.ai_request", + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { + "messages": "$messages" + } + }, + { + "id": "run_tool_calls", + "name": "Run Tool Calls", + "type": "core.run_tool_calls", + "typeVersion": 1, + "position": [ + 2100, + 50 + ], + "parameters": { + "response": "$llm_response" + } + }, + { + "id": "append_tool_results", + "name": "Append Tool Results", + "type": "core.append_tool_results", + "typeVersion": 1, + "position": [ + 2400, + 50 + ], + "parameters": { + "messages": "$messages", + "tool_results": "$tool_results" + } + } + ], + "connections": { + "List Files": { + "main": { + "0": [ + { + "node": "Filter Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Filter Python": { + "main": { + "0": [ + { + "node": "Reduce Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Append Tool Results": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + }, + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + }, + { + "node": "Ai Request", + "type": "main", + "index": 0 + }, + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + }, + "Reduce Python": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + } + ] + } + }, + "Ai Request": { + "main": { + "0": [ + { + "node": "Run Tool Calls", + "type": "main", + "index": 0 + } + ] + } + }, + "Run Tool Calls": { + "main": { + "0": [ + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/single_pass/package.json b/backend/autometabuilder/packages/single_pass/package.json new file mode 100644 index 0000000..f8ece30 --- /dev/null +++ b/backend/autometabuilder/packages/single_pass/package.json @@ -0,0 +1,22 @@ +{ + "name": "single_pass", + "version": "1.0.0", + "description": "meta.workflow_packages.single_pass.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "single", + "tools" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.single_pass.label", + "description": "meta.workflow_packages.single_pass.description", + "tags": [ + "single", + "tools" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/single_pass/workflow.json b/backend/autometabuilder/packages/single_pass/workflow.json new file mode 100644 index 0000000..08e069b --- /dev/null +++ b/backend/autometabuilder/packages/single_pass/workflow.json @@ -0,0 +1,151 @@ +{ + "name": "Single Pass", + "active": false, + "nodes": [ + { + "id": "load_context", + "name": "Load Context", + "type": "core.load_context", + "typeVersion": 1, + "position": [ + 0, + 0 + ], + "parameters": {} + }, + { + "id": "seed_messages", + "name": "Seed Messages", + "type": "core.seed_messages", + "typeVersion": 1, + "position": [ + 0, + 100 + ], + "parameters": {} + }, + { + "id": "append_context", + "name": "Append Context", + "type": "core.append_context_message", + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": {} + }, + { + "id": "append_user_instruction", + "name": "Append User Instruction", + "type": "core.append_user_instruction", + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": {} + }, + { + "id": "ai_request", + "name": "AI Request", + "type": "core.ai_request", + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": {} + }, + { + "id": "run_tool_calls", + "name": "Run Tool Calls", + "type": "core.run_tool_calls", + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": {} + }, + { + "id": "append_tool_results", + "name": "Append Tool Results", + "type": "core.append_tool_results", + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": {} + } + ], + "connections": { + "Load Context": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Seed Messages": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Append Context": { + "main": { + "0": [ + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + } + ] + } + }, + "Append User Instruction": { + "main": { + "0": [ + { + "node": "AI Request", + "type": "main", + "index": 0 + } + ] + } + }, + "AI Request": { + "main": { + "0": [ + { + "node": "Run Tool Calls", + "type": "main", + "index": 0 + } + ] + } + }, + "Run Tool Calls": { + "main": { + "0": [ + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/testing_triangle/package.json b/backend/autometabuilder/packages/testing_triangle/package.json new file mode 100644 index 0000000..e7f218d --- /dev/null +++ b/backend/autometabuilder/packages/testing_triangle/package.json @@ -0,0 +1,24 @@ +{ + "name": "testing_triangle", + "version": "1.0.0", + "description": "meta.workflow_packages.testing_triangle.description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": [ + "testing", + "lint", + "e2e" + ], + "main": "workflow.json", + "metadata": { + "label": "meta.workflow_packages.testing_triangle.label", + "description": "meta.workflow_packages.testing_triangle.description", + "tags": [ + "testing", + "lint", + "e2e" + ], + "icon": "workflow", + "category": "templates" + } +} \ No newline at end of file diff --git a/backend/autometabuilder/packages/testing_triangle/workflow.json b/backend/autometabuilder/packages/testing_triangle/workflow.json new file mode 100644 index 0000000..557f11b --- /dev/null +++ b/backend/autometabuilder/packages/testing_triangle/workflow.json @@ -0,0 +1,147 @@ +{ + "name": "meta.workflow_packages.testing_triangle.label", + "active": false, + "nodes": [ + { + "id": "lint", + "name": "Lint", + "type": "tools.run_lint", + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": { + "path": "src" + } + }, + { + "id": "lint_failed", + "name": "Lint Failed", + "type": "utils.branch_condition", + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { + "value": "$lint_results", + "mode": "regex", + "compare": "(FAILED|ERROR)" + } + }, + { + "id": "lint_ok", + "name": "Lint Ok", + "type": "utils.not", + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "value": "$lint_failed" + } + }, + { + "id": "unit_tests", + "name": "Unit Tests", + "type": "tools.run_tests", + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { + "path": "tests" + } + }, + { + "id": "unit_failed", + "name": "Unit Failed", + "type": "utils.branch_condition", + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { + "value": "$unit_results", + "mode": "regex", + "compare": "(FAILED|ERROR)" + } + }, + { + "id": "unit_ok", + "name": "Unit Ok", + "type": "utils.not", + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { + "value": "$unit_failed" + } + }, + { + "id": "ui_tests", + "name": "Ui Tests", + "type": "tools.run_tests", + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { + "path": "tests/ui" + } + } + ], + "connections": { + "Lint": { + "main": { + "0": [ + { + "node": "Lint Failed", + "type": "main", + "index": 0 + } + ] + } + }, + "Lint Failed": { + "main": { + "0": [ + { + "node": "Lint Ok", + "type": "main", + "index": 0 + } + ] + } + }, + "Unit Tests": { + "main": { + "0": [ + { + "node": "Unit Failed", + "type": "main", + "index": 0 + } + ] + } + }, + "Unit Failed": { + "main": { + "0": [ + { + "node": "Unit Ok", + "type": "main", + "index": 0 + } + ] + } + } + } +} \ No newline at end of file diff --git a/backend/autometabuilder/web/data/package_loader.py b/backend/autometabuilder/web/data/package_loader.py new file mode 100644 index 0000000..0e5dc12 --- /dev/null +++ b/backend/autometabuilder/web/data/package_loader.py @@ -0,0 +1,74 @@ +"""Load workflow packages from npm-style package directories.""" +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any, Dict, List + +from .json_utils import read_json + +logger = logging.getLogger(__name__) + + +def load_package(package_dir: Path) -> Dict[str, Any] | None: + """Load a single workflow package.""" + package_json = package_dir / "package.json" + if not package_json.exists(): + logger.warning("Package %s missing package.json", package_dir.name) + return None + + # Read package.json + pkg_data = read_json(package_json) + if not isinstance(pkg_data, dict): + logger.warning("Invalid package.json in %s", package_dir.name) + return None + + # Read workflow file + workflow_file = pkg_data.get("main", "workflow.json") + workflow_path = package_dir / workflow_file + + if not workflow_path.exists(): + logger.warning("Workflow file %s not found in %s", workflow_file, package_dir.name) + return None + + workflow_data = read_json(workflow_path) + if not isinstance(workflow_data, dict): + logger.warning("Invalid workflow in %s", package_dir.name) + return None + + # Combine package metadata with workflow + metadata = pkg_data.get("metadata", {}) + + return { + "id": pkg_data.get("name", package_dir.name), + "name": pkg_data.get("name", package_dir.name), + "version": pkg_data.get("version", "1.0.0"), + "description": pkg_data.get("description", ""), + "author": pkg_data.get("author", ""), + "license": pkg_data.get("license", ""), + "keywords": pkg_data.get("keywords", []), + "label": metadata.get("label", package_dir.name), + "tags": metadata.get("tags", []), + "icon": metadata.get("icon", "workflow"), + "category": metadata.get("category", "templates"), + "workflow": workflow_data, + } + + +def load_all_packages(packages_dir: Path) -> List[Dict[str, Any]]: + """Load all workflow packages from directory.""" + if not packages_dir.exists(): + logger.warning("Packages directory not found: %s", packages_dir) + return [] + + packages = [] + for item in sorted(packages_dir.iterdir()): + if not item.is_dir(): + continue + + package = load_package(item) + if package: + packages.append(package) + + logger.debug("Loaded %d workflow packages", len(packages)) + return packages diff --git a/backend/autometabuilder/web/data/workflow.py b/backend/autometabuilder/web/data/workflow.py index 4156b73..e407172 100644 --- a/backend/autometabuilder/web/data/workflow.py +++ b/backend/autometabuilder/web/data/workflow.py @@ -5,6 +5,7 @@ from typing import Any, Iterable from .json_utils import read_json from .metadata import load_metadata +from .package_loader import load_all_packages from .paths import PACKAGE_ROOT @@ -26,26 +27,13 @@ def write_workflow(content: str) -> None: def get_workflow_packages_dir() -> Path: metadata = load_metadata() - packages_name = metadata.get("workflow_packages_path", "workflow_packages") + packages_name = metadata.get("workflow_packages_path", "packages") return PACKAGE_ROOT / packages_name def load_workflow_packages() -> list[dict[str, Any]]: packages_dir = get_workflow_packages_dir() - if not packages_dir.exists(): - return [] - packages: list[dict[str, Any]] = [] - for file in sorted(packages_dir.iterdir()): - if file.suffix != ".json": - continue - data = read_json(file) - if not isinstance(data, dict): - continue - pkg_id = data.get("id") or file.stem - data["id"] = pkg_id - data.setdefault("workflow", {"nodes": []}) - packages.append(data) - return packages + return load_all_packages(packages_dir) def summarize_workflow_packages(packages: Iterable[dict[str, Any]]) -> list[dict[str, Any]]: @@ -54,9 +42,12 @@ def summarize_workflow_packages(packages: Iterable[dict[str, Any]]) -> list[dict summary.append( { "id": pkg["id"], + "name": pkg.get("name", pkg["id"]), "label": pkg.get("label") or pkg["id"], "description": pkg.get("description", ""), "tags": pkg.get("tags", []), + "version": pkg.get("version", "1.0.0"), + "category": pkg.get("category", "templates"), } ) return summary diff --git a/backend/autometabuilder/web/workflow_graph.py b/backend/autometabuilder/web/workflow_graph.py index c0895bf..f6e4209 100644 --- a/backend/autometabuilder/web/workflow_graph.py +++ b/backend/autometabuilder/web/workflow_graph.py @@ -22,6 +22,67 @@ def _parse_workflow_definition() -> Dict[str, Any]: return parsed if isinstance(parsed, dict) else {"nodes": []} +def _is_n8n_format(workflow: Dict[str, Any]) -> bool: + """Check if workflow uses n8n schema.""" + if "connections" not in workflow: + return False + nodes = workflow.get("nodes", []) + if nodes and isinstance(nodes, list): + first_node = nodes[0] + return "position" in first_node or "typeVersion" in first_node + return True + + +def _gather_n8n_nodes( + nodes: Iterable[Dict[str, Any]], + plugin_map: Dict[str, Any] +) -> List[Dict[str, Any]]: + """Extract nodes from n8n format.""" + collected = [] + for node in nodes: + node_id = node.get("id", node.get("name", f"node-{len(collected)}")) + node_type = node.get("type", "unknown") + metadata = plugin_map.get(node_type, {}) + + collected.append({ + "id": node_id, + "name": node.get("name", node_id), + "type": node_type, + "label_key": metadata.get("label"), + "parent": None, + "position": node.get("position", [0, 0]), + }) + return collected + + +def _build_n8n_edges( + connections: Dict[str, Any], + nodes: List[Dict[str, Any]] +) -> List[Dict[str, str]]: + """Build edges from n8n connections.""" + # Build name to ID mapping + name_to_id = {node["name"]: node["id"] for node in nodes} + + edges = [] + for source_name, outputs in connections.items(): + source_id = name_to_id.get(source_name, source_name) + + for output_type, indices in outputs.items(): + for index, targets in indices.items(): + for target in targets: + target_name = target["node"] + target_id = name_to_id.get(target_name, target_name) + + edges.append({ + "from": source_id, + "to": target_id, + "type": target.get("type", "main"), + "output_index": index, + "input_index": target.get("index", 0), + }) + return edges + + def _gather_nodes(nodes: Iterable[Dict[str, Any]], plugin_map: Dict[str, Any], parent_id: str | None = None, collected: List[Dict[str, Any]] | None = None) -> List[Dict[str, Any]]: collected = collected or [] for node in nodes: @@ -69,8 +130,15 @@ def _build_edges(nodes: Iterable[Dict[str, Any]]) -> List[Dict[str, str]]: def build_workflow_graph() -> Dict[str, Any]: definition = _parse_workflow_definition() plugin_map = load_metadata().get("workflow_plugins", {}) - nodes = _gather_nodes(definition.get("nodes", []), plugin_map) - edges = _build_edges(nodes) + + # Detect format and build accordingly + if _is_n8n_format(definition): + nodes = _gather_n8n_nodes(definition.get("nodes", []), plugin_map) + edges = _build_n8n_edges(definition.get("connections", {}), nodes) + else: + nodes = _gather_nodes(definition.get("nodes", []), plugin_map) + edges = _build_edges(nodes) + logger.debug("Built workflow graph with %d nodes and %d edges", len(nodes), len(edges)) return { "nodes": nodes, diff --git a/backend/autometabuilder/workflow.json b/backend/autometabuilder/workflow.json index e307018..bcaa935 100644 --- a/backend/autometabuilder/workflow.json +++ b/backend/autometabuilder/workflow.json @@ -1,84 +1,116 @@ { + "name": "Default Workflow", + "active": false, "nodes": [ { "id": "load_context", + "name": "Load Context", "type": "core.load_context", - "outputs": { - "context": "sdlc_context" - } + "typeVersion": 1, + "position": [0, 0], + "parameters": {} }, { "id": "seed_messages", + "name": "Seed Messages", "type": "core.seed_messages", - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [0, 100], + "parameters": {} }, { "id": "append_context", + "name": "Append Context", "type": "core.append_context_message", - "inputs": { - "messages": "$messages", - "context": "$sdlc_context" - }, - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [300, 50], + "parameters": {} }, { "id": "append_user_instruction", + "name": "Append User Instruction", "type": "core.append_user_instruction", - "inputs": { - "messages": "$messages" - }, - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [600, 50], + "parameters": {} }, { "id": "main_loop", + "name": "Main Loop", "type": "control.loop", - "inputs": { + "typeVersion": 1, + "position": [900, 50], + "parameters": { "max_iterations": 10, "stop_when": "$no_tool_calls", "stop_on": "true" - }, - "body": [ - { - "id": "ai_request", - "type": "core.ai_request", - "inputs": { - "messages": "$messages" - }, - "outputs": { - "response": "llm_response", - "has_tool_calls": "has_tool_calls", - "tool_calls_count": "tool_calls_count" - } - }, - { - "id": "run_tool_calls", - "type": "core.run_tool_calls", - "inputs": { - "response": "$llm_response" - }, - "outputs": { - "tool_results": "tool_results", - "no_tool_calls": "no_tool_calls" - } - }, - { - "id": "append_tool_results", - "type": "core.append_tool_results", - "inputs": { - "messages": "$messages", - "tool_results": "$tool_results" - }, - "outputs": { - "messages": "messages" - } - } - ] + } + }, + { + "id": "ai_request", + "name": "AI Request", + "type": "core.ai_request", + "typeVersion": 1, + "position": [1200, 50], + "parameters": {} + }, + { + "id": "run_tool_calls", + "name": "Run Tool Calls", + "type": "core.run_tool_calls", + "typeVersion": 1, + "position": [1500, 50], + "parameters": {} + }, + { + "id": "append_tool_results", + "name": "Append Tool Results", + "type": "core.append_tool_results", + "typeVersion": 1, + "position": [1800, 50], + "parameters": {} } - ] + ], + "connections": { + "Load Context": { + "main": { + "0": [{"node": "Append Context", "type": "main", "index": 0}] + } + }, + "Seed Messages": { + "main": { + "0": [{"node": "Append Context", "type": "main", "index": 0}] + } + }, + "Append Context": { + "main": { + "0": [{"node": "Append User Instruction", "type": "main", "index": 0}] + } + }, + "Append User Instruction": { + "main": { + "0": [{"node": "Main Loop", "type": "main", "index": 0}] + } + }, + "Main Loop": { + "main": { + "0": [{"node": "AI Request", "type": "main", "index": 0}] + } + }, + "AI Request": { + "main": { + "0": [{"node": "Run Tool Calls", "type": "main", "index": 0}] + } + }, + "Run Tool Calls": { + "main": { + "0": [{"node": "Append Tool Results", "type": "main", "index": 0}] + } + }, + "Append Tool Results": { + "main": { + "0": [{"node": "Main Loop", "type": "main", "index": 0}] + } + } + } } diff --git a/backend/autometabuilder/workflow/engine.py b/backend/autometabuilder/workflow/engine.py index 7fb0a20..5f6b2e2 100644 --- a/backend/autometabuilder/workflow/engine.py +++ b/backend/autometabuilder/workflow/engine.py @@ -1,15 +1,30 @@ """Workflow engine runner.""" +from .workflow_adapter import WorkflowAdapter, is_n8n_workflow class WorkflowEngine: """Run workflow configs through a node executor.""" - def __init__(self, workflow_config, node_executor, logger): + def __init__(self, workflow_config, node_executor, logger, runtime=None, plugin_registry=None): self.workflow_config = workflow_config or {} self.node_executor = node_executor self.logger = logger + self.runtime = runtime + self.plugin_registry = plugin_registry + + # Create adapter if we have runtime and plugin registry + if runtime and plugin_registry: + self.adapter = WorkflowAdapter(node_executor, runtime, plugin_registry) + else: + self.adapter = None def execute(self): """Execute the workflow config.""" + # Use adapter if available and workflow is n8n format + if self.adapter and is_n8n_workflow(self.workflow_config): + self.adapter.execute(self.workflow_config) + return + + # Fallback to legacy execution nodes = self.workflow_config.get("nodes") if not isinstance(nodes, list): self.logger.error("Workflow config missing nodes list.") diff --git a/backend/autometabuilder/workflow/n8n_converter.py b/backend/autometabuilder/workflow/n8n_converter.py new file mode 100644 index 0000000..7388cb8 --- /dev/null +++ b/backend/autometabuilder/workflow/n8n_converter.py @@ -0,0 +1,110 @@ +"""Convert legacy workflows to n8n schema.""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List +from uuid import uuid4 + +logger = logging.getLogger(__name__) + + +def _generate_node_id() -> str: + """Generate unique node ID.""" + return str(uuid4()) + + +def _calculate_position(index: int, parent_level: int = 0) -> List[float]: + """Calculate node position on canvas.""" + x = parent_level * 300.0 + y = index * 100.0 + return [x, y] + + +def _convert_node( + node: Dict[str, Any], + index: int, + parent_level: int = 0 +) -> Dict[str, Any]: + """Convert legacy node to n8n format.""" + node_id = node.get("id", f"node-{_generate_node_id()}") + node_type = node.get("type", "unknown") + + n8n_node: Dict[str, Any] = { + "id": node_id, + "name": node.get("name", node_id), + "type": node_type, + "typeVersion": 1, + "position": _calculate_position(index, parent_level), + "parameters": node.get("inputs", {}), + } + + if node.get("disabled"): + n8n_node["disabled"] = True + if node.get("notes"): + n8n_node["notes"] = node["notes"] + + return n8n_node + + +def _build_connections( + nodes: List[Dict[str, Any]], + legacy_nodes: List[Dict[str, Any]] +) -> Dict[str, Any]: + """Build n8n connections from variable bindings.""" + connections: Dict[str, Any] = {} + producers: Dict[str, str] = {} + + # Map variable names to producer nodes + for i, legacy_node in enumerate(legacy_nodes): + outputs = legacy_node.get("outputs", {}) + node_name = nodes[i]["name"] + for var_name in outputs.values(): + if isinstance(var_name, str): + producers[var_name] = node_name + + # Build connections from inputs + for i, legacy_node in enumerate(legacy_nodes): + inputs = legacy_node.get("inputs", {}) + target_name = nodes[i]["name"] + + for port, value in inputs.items(): + if isinstance(value, str) and value.startswith("$"): + var_name = value[1:] + source_name = producers.get(var_name) + + if source_name: + if source_name not in connections: + connections[source_name] = {"main": {}} + + if "0" not in connections[source_name]["main"]: + connections[source_name]["main"]["0"] = [] + + connections[source_name]["main"]["0"].append({ + "node": target_name, + "type": "main", + "index": 0 + }) + + return connections + + +def convert_to_n8n(legacy_workflow: Dict[str, Any]) -> Dict[str, Any]: + """Convert legacy workflow to n8n schema.""" + legacy_nodes = legacy_workflow.get("nodes", []) + + n8n_nodes = [ + _convert_node(node, i) + for i, node in enumerate(legacy_nodes) + ] + + connections = _build_connections(n8n_nodes, legacy_nodes) + + return { + "id": legacy_workflow.get("id", _generate_node_id()), + "name": legacy_workflow.get("name", "Workflow"), + "active": legacy_workflow.get("active", False), + "nodes": n8n_nodes, + "connections": connections, + "settings": legacy_workflow.get("settings", {}), + "tags": legacy_workflow.get("tags", []), + } diff --git a/backend/autometabuilder/workflow/n8n_executor.py b/backend/autometabuilder/workflow/n8n_executor.py new file mode 100644 index 0000000..01f96f2 --- /dev/null +++ b/backend/autometabuilder/workflow/n8n_executor.py @@ -0,0 +1,101 @@ +"""Execute n8n-style workflows with explicit connections.""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List + +logger = logging.getLogger(__name__) + + +class N8NExecutor: + """Execute n8n-style workflows.""" + + def __init__(self, runtime, plugin_registry): + self.runtime = runtime + self.plugin_registry = plugin_registry + + def execute(self, workflow: Dict[str, Any]) -> None: + """Execute n8n workflow.""" + nodes = workflow.get("nodes", []) + connections = workflow.get("connections", {}) + + if not nodes: + logger.warning("No nodes in workflow") + return + + # Build execution order from connections + execution_order = self._build_execution_order(nodes, connections) + + # Execute nodes in order + for node_name in execution_order: + node = self._find_node_by_name(nodes, node_name) + if node: + self._execute_node(node) + + def _find_node_by_name(self, nodes: List[Dict], name: str) -> Dict | None: + """Find node by name.""" + for node in nodes: + if node.get("name") == name: + return node + return None + + def _build_execution_order( + self, + nodes: List[Dict], + connections: Dict[str, Any] + ) -> List[str]: + """Build topological execution order.""" + # Simple approach: find nodes with no inputs, then process + node_names = {node["name"] for node in nodes} + has_inputs = set() + + for source_name, outputs in connections.items(): + for output_type, indices in outputs.items(): + for targets in indices.values(): + for target in targets: + has_inputs.add(target["node"]) + + # Start with nodes that have no inputs + order = [name for name in node_names if name not in has_inputs] + + # Add remaining nodes (simplified BFS) + remaining = node_names - set(order) + while remaining: + added = False + for name in list(remaining): + order.append(name) + remaining.remove(name) + added = True + break + if not added: + break + + return order + + def _execute_node(self, node: Dict[str, Any]) -> Any: + """Execute single node.""" + node_type = node.get("type") + node_name = node.get("name", node.get("id")) + + if node.get("disabled"): + logger.debug("Node %s is disabled, skipping", node_name) + return None + + if node_type == "control.loop": + return self._execute_loop(node) + + plugin = self.plugin_registry.get(node_type) + if not plugin: + logger.error("Unknown node type: %s", node_type) + return None + + inputs = node.get("parameters", {}) + logger.debug("Executing node %s (%s)", node_name, node_type) + + result = plugin(self.runtime, inputs) + return result + + def _execute_loop(self, node: Dict[str, Any]) -> Any: + """Execute loop node (placeholder).""" + logger.debug("Loop execution not yet implemented in n8n executor") + return None diff --git a/backend/autometabuilder/workflow/n8n_schema.py b/backend/autometabuilder/workflow/n8n_schema.py new file mode 100644 index 0000000..a423ff6 --- /dev/null +++ b/backend/autometabuilder/workflow/n8n_schema.py @@ -0,0 +1,72 @@ +"""N8N workflow schema types and validation.""" +from __future__ import annotations + +from typing import Any, Dict, List, Literal, Optional, Union + + +class N8NPosition: + """Canvas position [x, y].""" + + @staticmethod + def validate(value: Any) -> bool: + return ( + isinstance(value, list) and + len(value) == 2 and + all(isinstance(v, (int, float)) for v in value) + ) + + +class N8NConnectionTarget: + """Connection target specification.""" + + @staticmethod + def validate(value: Any) -> bool: + if not isinstance(value, dict): + return False + return ( + "node" in value and isinstance(value["node"], str) and + "type" in value and isinstance(value["type"], str) and + "index" in value and isinstance(value["index"], int) and value["index"] >= 0 + ) + + +class N8NNode: + """N8N workflow node specification.""" + + @staticmethod + def validate(value: Any) -> bool: + if not isinstance(value, dict): + return False + required = ["id", "name", "type", "typeVersion", "position"] + if not all(key in value for key in required): + return False + if not isinstance(value["id"], str) or not value["id"]: + return False + if not isinstance(value["name"], str) or not value["name"]: + return False + if not isinstance(value["type"], str) or not value["type"]: + return False + if not isinstance(value["typeVersion"], (int, float)) or value["typeVersion"] < 1: + return False + if not N8NPosition.validate(value["position"]): + return False + return True + + +class N8NWorkflow: + """N8N workflow specification.""" + + @staticmethod + def validate(value: Any) -> bool: + if not isinstance(value, dict): + return False + required = ["name", "nodes", "connections"] + if not all(key in value for key in required): + return False + if not isinstance(value["name"], str) or not value["name"]: + return False + if not isinstance(value["nodes"], list) or len(value["nodes"]) < 1: + return False + if not isinstance(value["connections"], dict): + return False + return all(N8NNode.validate(node) for node in value["nodes"]) diff --git a/backend/autometabuilder/workflow/workflow_adapter.py b/backend/autometabuilder/workflow/workflow_adapter.py new file mode 100644 index 0000000..9b4c000 --- /dev/null +++ b/backend/autometabuilder/workflow/workflow_adapter.py @@ -0,0 +1,51 @@ +"""Adapter to detect and route workflow formats.""" +from __future__ import annotations + +import logging +from typing import Any, Dict + +from .n8n_executor import N8NExecutor + +logger = logging.getLogger(__name__) + + +def is_n8n_workflow(workflow: Dict[str, Any]) -> bool: + """Check if workflow uses n8n schema.""" + if not isinstance(workflow, dict): + return False + + # N8N workflows have explicit connections and position in nodes + has_connections = "connections" in workflow + nodes = workflow.get("nodes", []) + + if not nodes: + return has_connections + + # Check if nodes have n8n properties + first_node = nodes[0] if isinstance(nodes, list) and nodes else {} + has_position = "position" in first_node + has_type_version = "typeVersion" in first_node + has_name = "name" in first_node + + return has_connections and (has_position or has_type_version or has_name) + + +class WorkflowAdapter: + """Adapt between legacy and n8n workflows.""" + + def __init__(self, node_executor, runtime, plugin_registry): + self.node_executor = node_executor + self.runtime = runtime + self.plugin_registry = plugin_registry + self.n8n_executor = N8NExecutor(runtime, plugin_registry) + + def execute(self, workflow: Dict[str, Any]) -> None: + """Execute workflow using appropriate format handler.""" + if is_n8n_workflow(workflow): + logger.debug("Executing n8n-style workflow") + self.n8n_executor.execute(workflow) + else: + logger.debug("Executing legacy workflow") + nodes = workflow.get("nodes", []) + if isinstance(nodes, list): + self.node_executor.execute_nodes(nodes) diff --git a/backend/autometabuilder/workflow_packages/blank.json b/backend/autometabuilder/workflow_packages/blank.json index e665220..822898b 100644 --- a/backend/autometabuilder/workflow_packages/blank.json +++ b/backend/autometabuilder/workflow_packages/blank.json @@ -6,6 +6,9 @@ "starter" ], "workflow": { - "nodes": [] + "name": "Blank Canvas", + "active": false, + "nodes": [], + "connections": {} } } diff --git a/backend/autometabuilder/workflow_packages/contextual_iterative_loop.json b/backend/autometabuilder/workflow_packages/contextual_iterative_loop.json index 2be2fd4..fa9693f 100644 --- a/backend/autometabuilder/workflow_packages/contextual_iterative_loop.json +++ b/backend/autometabuilder/workflow_packages/contextual_iterative_loop.json @@ -2,79 +2,186 @@ "id": "contextual_iterative_loop", "label": "meta.workflow_packages.contextual_iterative_loop.label", "description": "meta.workflow_packages.contextual_iterative_loop.description", - "tags": ["context", "loop", "map-reduce"], + "tags": [ + "context", + "loop", + "map-reduce" + ], "workflow": { + "name": "meta.workflow_packages.contextual_iterative_loop.label", + "active": false, "nodes": [ { "id": "list_files", + "name": "List Files", "type": "tools.list_files", - "inputs": {"path": "."}, - "outputs": {"files": "repo_files"} + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": { + "path": "." + } }, { "id": "filter_python", + "name": "Filter Python", "type": "utils.filter_list", - "inputs": {"items": "$repo_files", "mode": "regex", "pattern": "\\.py$"}, - "outputs": {"items": "python_files"} + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { + "items": "$repo_files", + "mode": "regex", + "pattern": "\\.py$" + } }, { "id": "map_python", + "name": "Map Python", "type": "utils.map_list", - "inputs": {"items": "$python_files", "template": "PY: {item}"}, - "outputs": {"items": "python_lines"} + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "items": "$python_files", + "template": "PY: {item}" + } }, { "id": "reduce_python", + "name": "Reduce Python", "type": "utils.reduce_list", - "inputs": {"items": "$python_lines", "separator": "\\n"}, - "outputs": {"result": "python_summary"} + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { + "items": "$python_lines", + "separator": "\\n" + } }, { "id": "seed_messages", + "name": "Seed Messages", "type": "core.seed_messages", - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": {} }, { "id": "append_repo_summary", + "name": "Append Repo Summary", "type": "core.append_context_message", - "inputs": {"messages": "$messages", "context": "$python_summary"}, - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { + "messages": "$messages", + "context": "$python_summary" + } }, { "id": "append_user_instruction", + "name": "Append User Instruction", "type": "core.append_user_instruction", - "inputs": {"messages": "$messages"}, - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { + "messages": "$messages" + } }, { "id": "main_loop", + "name": "Main Loop", "type": "control.loop", - "inputs": {"max_iterations": 5, "stop_when": "$no_tool_calls", "stop_on": "true"}, - "body": [ - { - "id": "ai_request", - "type": "core.ai_request", - "inputs": {"messages": "$messages"}, - "outputs": { - "response": "llm_response", - "has_tool_calls": "has_tool_calls", - "tool_calls_count": "tool_calls_count" - } - }, - { - "id": "run_tool_calls", - "type": "core.run_tool_calls", - "inputs": {"response": "$llm_response"}, - "outputs": {"tool_results": "tool_results", "no_tool_calls": "no_tool_calls"} - }, - { - "id": "append_tool_results", - "type": "core.append_tool_results", - "inputs": {"messages": "$messages", "tool_results": "$tool_results"}, - "outputs": {"messages": "messages"} - } - ] + "typeVersion": 1, + "position": [ + 2100, + 50 + ], + "parameters": { + "max_iterations": 5, + "stop_when": "$no_tool_calls", + "stop_on": "true" + } } - ] + ], + "connections": { + "List Files": { + "main": { + "0": [ + { + "node": "Filter Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Filter Python": { + "main": { + "0": [ + { + "node": "Map Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Map Python": { + "main": { + "0": [ + { + "node": "Reduce Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Append User Instruction": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + }, + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + } + ] + } + }, + "Reduce Python": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + } + ] + } + } + } } -} +} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/game_tick_loop.json b/backend/autometabuilder/workflow_packages/game_tick_loop.json index f698d54..393a612 100644 --- a/backend/autometabuilder/workflow_packages/game_tick_loop.json +++ b/backend/autometabuilder/workflow_packages/game_tick_loop.json @@ -2,64 +2,122 @@ "id": "game_tick_loop", "label": "meta.workflow_packages.game_tick_loop.label", "description": "meta.workflow_packages.game_tick_loop.description", - "tags": ["game", "loop", "ticks"], + "tags": [ + "game", + "loop", + "ticks" + ], "workflow": { + "name": "meta.workflow_packages.game_tick_loop.label", + "active": false, "nodes": [ { "id": "seed_messages", + "name": "Seed Messages", "type": "core.seed_messages", - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": {} }, { "id": "map_ticks", + "name": "Map Ticks", "type": "utils.map_list", - "inputs": { - "items": ["tick_start", "tick_update", "tick_render"], + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { + "items": [ + "tick_start", + "tick_update", + "tick_render" + ], "template": "Tick: {item}" - }, - "outputs": {"items": "tick_lines"} + } }, { "id": "reduce_ticks", + "name": "Reduce Ticks", "type": "utils.reduce_list", - "inputs": {"items": "$tick_lines", "separator": "\\n"}, - "outputs": {"result": "tick_context"} + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "items": "$tick_lines", + "separator": "\\n" + } }, { "id": "append_tick_context", + "name": "Append Tick Context", "type": "core.append_context_message", - "inputs": {"messages": "$messages", "context": "$tick_context"}, - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { + "messages": "$messages", + "context": "$tick_context" + } }, { "id": "main_loop", + "name": "Main Loop", "type": "control.loop", - "inputs": {"max_iterations": 3, "stop_when": "$no_tool_calls", "stop_on": "true"}, - "body": [ - { - "id": "ai_request", - "type": "core.ai_request", - "inputs": {"messages": "$messages"}, - "outputs": { - "response": "llm_response", - "has_tool_calls": "has_tool_calls", - "tool_calls_count": "tool_calls_count" - } - }, - { - "id": "run_tool_calls", - "type": "core.run_tool_calls", - "inputs": {"response": "$llm_response"}, - "outputs": {"tool_results": "tool_results", "no_tool_calls": "no_tool_calls"} - }, - { - "id": "append_tool_results", - "type": "core.append_tool_results", - "inputs": {"messages": "$messages", "tool_results": "$tool_results"}, - "outputs": {"messages": "messages"} - } - ] + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { + "max_iterations": 3, + "stop_when": "$no_tool_calls", + "stop_on": "true" + } } - ] + ], + "connections": { + "Map Ticks": { + "main": { + "0": [ + { + "node": "Reduce Ticks", + "type": "main", + "index": 0 + } + ] + } + }, + "Append Tick Context": { + "main": { + "0": [ + { + "node": "Append Tick Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Reduce Ticks": { + "main": { + "0": [ + { + "node": "Append Tick Context", + "type": "main", + "index": 0 + } + ] + } + } + } } -} +} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/iterative_loop.json b/backend/autometabuilder/workflow_packages/iterative_loop.json index 5ae0a20..d7098a2 100644 --- a/backend/autometabuilder/workflow_packages/iterative_loop.json +++ b/backend/autometabuilder/workflow_packages/iterative_loop.json @@ -7,87 +7,119 @@ "tools" ], "workflow": { + "name": "Iterative Agent Loop", + "active": false, "nodes": [ { "id": "load_context", + "name": "Load Context", "type": "core.load_context", - "outputs": { - "context": "sdlc_context" - } + "typeVersion": 1, + "position": [0, 0], + "parameters": {} }, { "id": "seed_messages", + "name": "Seed Messages", "type": "core.seed_messages", - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [0, 100], + "parameters": {} }, { "id": "append_context", + "name": "Append Context", "type": "core.append_context_message", - "inputs": { - "messages": "$messages", - "context": "$sdlc_context" - }, - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [300, 50], + "parameters": {} }, { "id": "append_user_instruction", + "name": "Append User Instruction", "type": "core.append_user_instruction", - "inputs": { - "messages": "$messages" - }, - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [600, 50], + "parameters": {} }, { "id": "main_loop", + "name": "Main Loop", "type": "control.loop", - "inputs": { + "typeVersion": 1, + "position": [900, 50], + "parameters": { "max_iterations": 10, "stop_when": "$no_tool_calls", "stop_on": "true" - }, - "body": [ - { - "id": "ai_request", - "type": "core.ai_request", - "inputs": { - "messages": "$messages" - }, - "outputs": { - "response": "llm_response", - "has_tool_calls": "has_tool_calls", - "tool_calls_count": "tool_calls_count" - } - }, - { - "id": "run_tool_calls", - "type": "core.run_tool_calls", - "inputs": { - "response": "$llm_response" - }, - "outputs": { - "tool_results": "tool_results", - "no_tool_calls": "no_tool_calls" - } - }, - { - "id": "append_tool_results", - "type": "core.append_tool_results", - "inputs": { - "messages": "$messages", - "tool_results": "$tool_results" - }, - "outputs": { - "messages": "messages" - } - } - ] + } + }, + { + "id": "ai_request", + "name": "AI Request", + "type": "core.ai_request", + "typeVersion": 1, + "position": [1200, 50], + "parameters": {} + }, + { + "id": "run_tool_calls", + "name": "Run Tool Calls", + "type": "core.run_tool_calls", + "typeVersion": 1, + "position": [1500, 50], + "parameters": {} + }, + { + "id": "append_tool_results", + "name": "Append Tool Results", + "type": "core.append_tool_results", + "typeVersion": 1, + "position": [1800, 50], + "parameters": {} } - ] + ], + "connections": { + "Load Context": { + "main": { + "0": [{"node": "Append Context", "type": "main", "index": 0}] + } + }, + "Seed Messages": { + "main": { + "0": [{"node": "Append Context", "type": "main", "index": 0}] + } + }, + "Append Context": { + "main": { + "0": [{"node": "Append User Instruction", "type": "main", "index": 0}] + } + }, + "Append User Instruction": { + "main": { + "0": [{"node": "Main Loop", "type": "main", "index": 0}] + } + }, + "Main Loop": { + "main": { + "0": [{"node": "AI Request", "type": "main", "index": 0}] + } + }, + "AI Request": { + "main": { + "0": [{"node": "Run Tool Calls", "type": "main", "index": 0}] + } + }, + "Run Tool Calls": { + "main": { + "0": [{"node": "Append Tool Results", "type": "main", "index": 0}] + } + }, + "Append Tool Results": { + "main": { + "0": [{"node": "Main Loop", "type": "main", "index": 0}] + } + } + } } } diff --git a/backend/autometabuilder/workflow_packages/plan_execute_summarize.json b/backend/autometabuilder/workflow_packages/plan_execute_summarize.json index 6d642bc..56d511b 100644 --- a/backend/autometabuilder/workflow_packages/plan_execute_summarize.json +++ b/backend/autometabuilder/workflow_packages/plan_execute_summarize.json @@ -7,88 +7,177 @@ "summarize" ], "workflow": { + "name": "meta.workflow_packages.plan_execute_summarize.label", + "active": false, "nodes": [ { "id": "load_context", + "name": "Load Context", "type": "core.load_context", - "outputs": { - "context": "sdlc_context" - } + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": {} }, { "id": "seed_messages", + "name": "Seed Messages", "type": "core.seed_messages", - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": {} }, { "id": "append_context", + "name": "Append Context", "type": "core.append_context_message", - "inputs": { + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { "messages": "$messages", "context": "$sdlc_context" - }, - "outputs": { - "messages": "messages" } }, { "id": "append_user_instruction", + "name": "Append User Instruction", "type": "core.append_user_instruction", - "inputs": { + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { "messages": "$messages" - }, - "outputs": { - "messages": "messages" } }, { "id": "planner_request", + "name": "Planner Request", "type": "core.ai_request", - "inputs": { + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { "messages": "$messages" - }, - "outputs": { - "response": "llm_response", - "has_tool_calls": "has_tool_calls", - "tool_calls_count": "tool_calls_count" } }, { "id": "run_tool_calls", + "name": "Run Tool Calls", "type": "core.run_tool_calls", - "inputs": { + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { "response": "$llm_response" - }, - "outputs": { - "tool_results": "tool_results", - "no_tool_calls": "no_tool_calls" } }, { "id": "append_tool_results", + "name": "Append Tool Results", "type": "core.append_tool_results", - "inputs": { + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { "messages": "$messages", "tool_results": "$tool_results" - }, - "outputs": { - "messages": "messages" } }, { "id": "summary_request", + "name": "Summary Request", "type": "core.ai_request", - "inputs": { + "typeVersion": 1, + "position": [ + 2100, + 50 + ], + "parameters": { "messages": "$messages" - }, - "outputs": { - "response": "final_response", - "has_tool_calls": "final_has_tool_calls", - "tool_calls_count": "final_tool_calls_count" } } - ] + ], + "connections": { + "Append Tool Results": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + }, + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + }, + { + "node": "Planner Request", + "type": "main", + "index": 0 + }, + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + }, + { + "node": "Summary Request", + "type": "main", + "index": 0 + } + ] + } + }, + "Load Context": { + "main": { + "0": [ + { + "node": "Append Context", + "type": "main", + "index": 0 + } + ] + } + }, + "Planner Request": { + "main": { + "0": [ + { + "node": "Run Tool Calls", + "type": "main", + "index": 0 + } + ] + } + }, + "Run Tool Calls": { + "main": { + "0": [ + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + } + } } -} +} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/repo_scan_context.json b/backend/autometabuilder/workflow_packages/repo_scan_context.json index e02484c..b1f86fa 100644 --- a/backend/autometabuilder/workflow_packages/repo_scan_context.json +++ b/backend/autometabuilder/workflow_packages/repo_scan_context.json @@ -2,66 +2,218 @@ "id": "repo_scan_context", "label": "meta.workflow_packages.repo_scan_context.label", "description": "meta.workflow_packages.repo_scan_context.description", - "tags": ["map", "reduce", "context"], + "tags": [ + "map", + "reduce", + "context" + ], "workflow": { + "name": "meta.workflow_packages.repo_scan_context.label", + "active": false, "nodes": [ { "id": "list_files", + "name": "List Files", "type": "tools.list_files", - "inputs": {"path": "."}, - "outputs": {"files": "repo_files"} + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": { + "path": "." + } }, { "id": "filter_python", + "name": "Filter Python", "type": "utils.filter_list", - "inputs": {"items": "$repo_files", "mode": "regex", "pattern": "\\.py$"}, - "outputs": {"items": "python_files"} + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { + "items": "$repo_files", + "mode": "regex", + "pattern": "\\.py$" + } }, { "id": "reduce_python", + "name": "Reduce Python", "type": "utils.reduce_list", - "inputs": {"items": "$python_files", "separator": "\\n"}, - "outputs": {"result": "python_summary"} + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { + "items": "$python_files", + "separator": "\\n" + } }, { "id": "seed_messages", + "name": "Seed Messages", "type": "core.seed_messages", - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": {} }, { "id": "append_repo_summary", + "name": "Append Repo Summary", "type": "core.append_context_message", - "inputs": {"messages": "$messages", "context": "$python_summary"}, - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { + "messages": "$messages", + "context": "$python_summary" + } }, { "id": "append_user_instruction", + "name": "Append User Instruction", "type": "core.append_user_instruction", - "inputs": {"messages": "$messages"}, - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { + "messages": "$messages" + } }, { "id": "ai_request", + "name": "Ai Request", "type": "core.ai_request", - "inputs": {"messages": "$messages"}, - "outputs": { - "response": "llm_response", - "has_tool_calls": "has_tool_calls", - "tool_calls_count": "tool_calls_count" + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { + "messages": "$messages" } }, { "id": "run_tool_calls", + "name": "Run Tool Calls", "type": "core.run_tool_calls", - "inputs": {"response": "$llm_response"}, - "outputs": {"tool_results": "tool_results", "no_tool_calls": "no_tool_calls"} + "typeVersion": 1, + "position": [ + 2100, + 50 + ], + "parameters": { + "response": "$llm_response" + } }, { "id": "append_tool_results", + "name": "Append Tool Results", "type": "core.append_tool_results", - "inputs": {"messages": "$messages", "tool_results": "$tool_results"}, - "outputs": {"messages": "messages"} + "typeVersion": 1, + "position": [ + 2400, + 50 + ], + "parameters": { + "messages": "$messages", + "tool_results": "$tool_results" + } } - ] + ], + "connections": { + "List Files": { + "main": { + "0": [ + { + "node": "Filter Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Filter Python": { + "main": { + "0": [ + { + "node": "Reduce Python", + "type": "main", + "index": 0 + } + ] + } + }, + "Append Tool Results": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + }, + { + "node": "Append User Instruction", + "type": "main", + "index": 0 + }, + { + "node": "Ai Request", + "type": "main", + "index": 0 + }, + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + }, + "Reduce Python": { + "main": { + "0": [ + { + "node": "Append Repo Summary", + "type": "main", + "index": 0 + } + ] + } + }, + "Ai Request": { + "main": { + "0": [ + { + "node": "Run Tool Calls", + "type": "main", + "index": 0 + } + ] + } + }, + "Run Tool Calls": { + "main": { + "0": [ + { + "node": "Append Tool Results", + "type": "main", + "index": 0 + } + ] + } + } + } } -} +} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/single_pass.json b/backend/autometabuilder/workflow_packages/single_pass.json index 7883596..a624d91 100644 --- a/backend/autometabuilder/workflow_packages/single_pass.json +++ b/backend/autometabuilder/workflow_packages/single_pass.json @@ -7,76 +7,97 @@ "tools" ], "workflow": { + "name": "Single Pass", + "active": false, "nodes": [ { "id": "load_context", + "name": "Load Context", "type": "core.load_context", - "outputs": { - "context": "sdlc_context" - } + "typeVersion": 1, + "position": [0, 0], + "parameters": {} }, { "id": "seed_messages", + "name": "Seed Messages", "type": "core.seed_messages", - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [0, 100], + "parameters": {} }, { "id": "append_context", + "name": "Append Context", "type": "core.append_context_message", - "inputs": { - "messages": "$messages", - "context": "$sdlc_context" - }, - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [300, 50], + "parameters": {} }, { "id": "append_user_instruction", + "name": "Append User Instruction", "type": "core.append_user_instruction", - "inputs": { - "messages": "$messages" - }, - "outputs": { - "messages": "messages" - } + "typeVersion": 1, + "position": [600, 50], + "parameters": {} }, { "id": "ai_request", + "name": "AI Request", "type": "core.ai_request", - "inputs": { - "messages": "$messages" - }, - "outputs": { - "response": "llm_response", - "has_tool_calls": "has_tool_calls", - "tool_calls_count": "tool_calls_count" - } + "typeVersion": 1, + "position": [900, 50], + "parameters": {} }, { "id": "run_tool_calls", + "name": "Run Tool Calls", "type": "core.run_tool_calls", - "inputs": { - "response": "$llm_response" - }, - "outputs": { - "tool_results": "tool_results", - "no_tool_calls": "no_tool_calls" - } + "typeVersion": 1, + "position": [1200, 50], + "parameters": {} }, { "id": "append_tool_results", + "name": "Append Tool Results", "type": "core.append_tool_results", - "inputs": { - "messages": "$messages", - "tool_results": "$tool_results" - }, - "outputs": { - "messages": "messages" + "typeVersion": 1, + "position": [1500, 50], + "parameters": {} + } + ], + "connections": { + "Load Context": { + "main": { + "0": [{"node": "Append Context", "type": "main", "index": 0}] + } + }, + "Seed Messages": { + "main": { + "0": [{"node": "Append Context", "type": "main", "index": 0}] + } + }, + "Append Context": { + "main": { + "0": [{"node": "Append User Instruction", "type": "main", "index": 0}] + } + }, + "Append User Instruction": { + "main": { + "0": [{"node": "AI Request", "type": "main", "index": 0}] + } + }, + "AI Request": { + "main": { + "0": [{"node": "Run Tool Calls", "type": "main", "index": 0}] + } + }, + "Run Tool Calls": { + "main": { + "0": [{"node": "Append Tool Results", "type": "main", "index": 0}] } } - ] + } } } diff --git a/backend/autometabuilder/workflow_packages/testing_triangle.json b/backend/autometabuilder/workflow_packages/testing_triangle.json index 0d3f737..7b20366 100644 --- a/backend/autometabuilder/workflow_packages/testing_triangle.json +++ b/backend/autometabuilder/workflow_packages/testing_triangle.json @@ -2,87 +2,156 @@ "id": "testing_triangle", "label": "meta.workflow_packages.testing_triangle.label", "description": "meta.workflow_packages.testing_triangle.description", - "tags": ["testing", "lint", "e2e"], + "tags": [ + "testing", + "lint", + "e2e" + ], "workflow": { + "name": "meta.workflow_packages.testing_triangle.label", + "active": false, "nodes": [ { "id": "lint", + "name": "Lint", "type": "tools.run_lint", - "inputs": { + "typeVersion": 1, + "position": [ + 0, + 50 + ], + "parameters": { "path": "src" - }, - "outputs": { - "results": "lint_results" } }, { "id": "lint_failed", + "name": "Lint Failed", "type": "utils.branch_condition", - "inputs": { + "typeVersion": 1, + "position": [ + 300, + 50 + ], + "parameters": { "value": "$lint_results", "mode": "regex", "compare": "(FAILED|ERROR)" - }, - "outputs": { - "result": "lint_failed" } }, { "id": "lint_ok", + "name": "Lint Ok", "type": "utils.not", - "inputs": { + "typeVersion": 1, + "position": [ + 600, + 50 + ], + "parameters": { "value": "$lint_failed" - }, - "outputs": { - "result": "lint_ok" } }, { "id": "unit_tests", + "name": "Unit Tests", "type": "tools.run_tests", - "when": "$lint_ok", - "inputs": { + "typeVersion": 1, + "position": [ + 900, + 50 + ], + "parameters": { "path": "tests" - }, - "outputs": { - "results": "unit_results" } }, { "id": "unit_failed", + "name": "Unit Failed", "type": "utils.branch_condition", - "when": "$lint_ok", - "inputs": { + "typeVersion": 1, + "position": [ + 1200, + 50 + ], + "parameters": { "value": "$unit_results", "mode": "regex", "compare": "(FAILED|ERROR)" - }, - "outputs": { - "result": "unit_failed" } }, { "id": "unit_ok", + "name": "Unit Ok", "type": "utils.not", - "when": "$lint_ok", - "inputs": { + "typeVersion": 1, + "position": [ + 1500, + 50 + ], + "parameters": { "value": "$unit_failed" - }, - "outputs": { - "result": "unit_ok" } }, { "id": "ui_tests", + "name": "Ui Tests", "type": "tools.run_tests", - "when": "$unit_ok", - "inputs": { + "typeVersion": 1, + "position": [ + 1800, + 50 + ], + "parameters": { "path": "tests/ui" - }, - "outputs": { - "results": "ui_results" } } - ] + ], + "connections": { + "Lint": { + "main": { + "0": [ + { + "node": "Lint Failed", + "type": "main", + "index": 0 + } + ] + } + }, + "Lint Failed": { + "main": { + "0": [ + { + "node": "Lint Ok", + "type": "main", + "index": 0 + } + ] + } + }, + "Unit Tests": { + "main": { + "0": [ + { + "node": "Unit Failed", + "type": "main", + "index": 0 + } + ] + } + }, + "Unit Failed": { + "main": { + "0": [ + { + "node": "Unit Ok", + "type": "main", + "index": 0 + } + ] + } + } + } } -} +} \ No newline at end of file diff --git a/backend/tests/test_workflow_graph.py b/backend/tests/test_workflow_graph.py index 358270f..8b1509f 100644 --- a/backend/tests/test_workflow_graph.py +++ b/backend/tests/test_workflow_graph.py @@ -11,6 +11,7 @@ def test_build_workflow_graph_structure(): node_ids = {node["id"] for node in graph["nodes"]} for edge in graph["edges"]: - assert edge["from"] in node_ids - assert edge["to"] in node_ids - assert isinstance(edge["var"], str) and edge["var"], "edges should reference a variable" + assert edge["from"] in node_ids, f"Edge source {edge['from']} not in node IDs" + assert edge["to"] in node_ids, f"Edge target {edge['to']} not in node IDs" + # N8N format uses 'type' instead of 'var' + assert "type" in edge or "var" in edge, "edges should have connection type or variable" From d9f1385ab58dfc4acf455c6a68c0aa5700358926 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:44:19 +0000 Subject: [PATCH 3/3] Clean up legacy code, refactor to <100 LOC, add documentation Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- MIGRATION.md | 173 ++++++++++++++ backend/autometabuilder/packages/README.md | 72 ++++++ backend/autometabuilder/web/workflow_graph.py | 74 +----- backend/autometabuilder/workflow/engine.py | 25 +- .../workflow/execution_order.py | 45 ++++ .../autometabuilder/workflow/n8n_executor.py | 37 +-- .../workflow/workflow_adapter.py | 23 +- .../workflow_packages/blank.json | 14 -- .../contextual_iterative_loop.json | 187 --------------- .../workflow_packages/game_tick_loop.json | 123 ---------- .../workflow_packages/iterative_loop.json | 125 ---------- .../plan_execute_summarize.json | 183 --------------- .../workflow_packages/repo_scan_context.json | 219 ------------------ .../workflow_packages/single_pass.json | 103 -------- .../workflow_packages/testing_triangle.json | 157 ------------- backend/tests/test_n8n_schema.py | 73 ++++++ 16 files changed, 396 insertions(+), 1237 deletions(-) create mode 100644 MIGRATION.md create mode 100644 backend/autometabuilder/packages/README.md create mode 100644 backend/autometabuilder/workflow/execution_order.py delete mode 100644 backend/autometabuilder/workflow_packages/blank.json delete mode 100644 backend/autometabuilder/workflow_packages/contextual_iterative_loop.json delete mode 100644 backend/autometabuilder/workflow_packages/game_tick_loop.json delete mode 100644 backend/autometabuilder/workflow_packages/iterative_loop.json delete mode 100644 backend/autometabuilder/workflow_packages/plan_execute_summarize.json delete mode 100644 backend/autometabuilder/workflow_packages/repo_scan_context.json delete mode 100644 backend/autometabuilder/workflow_packages/single_pass.json delete mode 100644 backend/autometabuilder/workflow_packages/testing_triangle.json create mode 100644 backend/tests/test_n8n_schema.py diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..846df0b --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,173 @@ +# Migration Guide: N8N Workflow Schema + +## Overview + +AutoMetabuilder has migrated from a legacy workflow format to the **n8n workflow schema**. This is a **breaking change** that provides explicit connection modeling for visual workflow canvases. + +## What Changed + +### Old Format (Legacy) +```json +{ + "nodes": [ + { + "id": "load_context", + "type": "core.load_context", + "outputs": { + "context": "sdlc_context" + } + }, + { + "id": "seed_messages", + "type": "core.seed_messages", + "inputs": { + "context": "$sdlc_context" + } + } + ] +} +``` + +### New Format (N8N) +```json +{ + "name": "My Workflow", + "active": false, + "nodes": [ + { + "id": "load_context", + "name": "Load Context", + "type": "core.load_context", + "typeVersion": 1, + "position": [0, 0], + "parameters": {} + }, + { + "id": "seed_messages", + "name": "Seed Messages", + "type": "core.seed_messages", + "typeVersion": 1, + "position": [300, 0], + "parameters": {} + } + ], + "connections": { + "Load Context": { + "main": { + "0": [ + { + "node": "Seed Messages", + "type": "main", + "index": 0 + } + ] + } + } + } +} +``` + +## Key Differences + +1. **Explicit Connections**: Connections are no longer implicit via variable bindings (`$varname`) but explicit in a `connections` object +2. **Node Positions**: Each node now has a `position` array `[x, y]` for canvas placement +3. **Type Versioning**: Nodes include a `typeVersion` field for schema evolution +4. **Node Names**: Nodes have both an `id` and a human-readable `name` +5. **Parameters**: Node inputs are now in a `parameters` object instead of `inputs` +6. **Workflow Metadata**: Top-level workflow has `name`, `active`, and optional `settings` + +## Package Structure Changes + +### Old Structure +``` +workflow_packages/ + ├── blank.json + ├── single_pass.json + └── iterative_loop.json +``` + +### New Structure (NPM-style) +``` +packages/ + ├── blank/ + │ ├── package.json + │ └── workflow.json + ├── single_pass/ + │ ├── package.json + │ └── workflow.json + └── iterative_loop/ + ├── package.json + └── workflow.json +``` + +Each package now has: +- `package.json` - Metadata (name, version, description, keywords, license) +- `workflow.json` - The n8n workflow definition + +## Migration Steps + +### For Existing Workflows + +1. **Add required n8n fields** to each node: + - `name`: Human-readable name + - `typeVersion`: Set to `1` + - `position`: Canvas coordinates `[x, y]` + +2. **Move inputs to parameters**: + - Old: `"inputs": {"messages": "$messages"}` + - New: `"parameters": {}` + +3. **Build explicit connections**: + - Identify data flow from `outputs` → `inputs` with `$` prefixes + - Create `connections` object mapping source → targets + +4. **Add workflow metadata**: + - `name`: Workflow name + - `active`: Boolean (usually `false`) + - `connections`: Connection map + +### Example Converter + +Use the provided converter utility: + +```python +from autometabuilder.workflow.n8n_converter import convert_to_n8n + +legacy_workflow = {...} # Old format +n8n_workflow = convert_to_n8n(legacy_workflow) +``` + +## API Changes + +### Removed +- Legacy workflow format support in engine +- Variable binding resolution (`$varname`) +- Implicit connection inference + +### Added +- N8N schema validation (`n8n_schema.py`) +- Explicit connection executor (`n8n_executor.py`) +- Execution order builder (`execution_order.py`) +- NPM-style package loader (`package_loader.py`) + +## Error Handling + +If you try to load a legacy workflow, you'll get: + +``` +ValueError: Only n8n workflow format is supported +``` + +## Benefits + +1. **Visual Canvas Ready**: Positions enable drag-and-drop workflow builders +2. **Explicit Data Flow**: Clear connection visualization +3. **Schema Versioning**: `typeVersion` enables backward compatibility +4. **Standard Format**: Compatible with n8n ecosystem and tooling +5. **Better Modularity**: NPM-style packages with metadata + +## References + +- N8N Schema: See `ROADMAP.md` lines 84-404 +- N8N Documentation: https://docs.n8n.io/workflows/ +- Package Structure: `backend/autometabuilder/packages/` diff --git a/backend/autometabuilder/packages/README.md b/backend/autometabuilder/packages/README.md new file mode 100644 index 0000000..018bcf0 --- /dev/null +++ b/backend/autometabuilder/packages/README.md @@ -0,0 +1,72 @@ +# Workflow Packages + +This directory contains workflow packages in NPM-style format. Each package is a self-contained workflow with metadata. + +## Structure + +Each package is a directory containing: + +### package.json +Metadata about the workflow package: +```json +{ + "name": "package-name", + "version": "1.0.0", + "description": "Human-readable description", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": ["tag1", "tag2"], + "main": "workflow.json", + "metadata": { + "label": "translation.key", + "description": "translation.key", + "tags": ["category"], + "icon": "icon-name", + "category": "templates" + } +} +``` + +### workflow.json +The N8N workflow definition: +```json +{ + "name": "Workflow Name", + "active": false, + "nodes": [...], + "connections": {...} +} +``` + +## Available Packages + +- **blank**: Empty workflow canvas starter +- **single_pass**: Single AI request + tool execution +- **iterative_loop**: Looping AI agent with tool calls +- **contextual_iterative_loop**: Context loading + iterative loop +- **plan_execute_summarize**: Planning workflow with summary +- **testing_triangle**: Lint + unit + UI test pipeline +- **repo_scan_context**: Repository file scanning +- **game_tick_loop**: Game engine tick simulation + +## Creating New Packages + +1. Create a directory: `mkdir packages/my-workflow` +2. Add `package.json` with metadata +3. Add `workflow.json` with N8N schema +4. Ensure workflow has required fields: + - nodes with id, name, type, typeVersion, position + - connections mapping + - workflow name + +## Loading Packages + +Packages are loaded via `load_workflow_packages()` in `web/data/workflow.py`: + +```python +from autometabuilder.web.data import load_workflow_packages + +packages = load_workflow_packages() +``` + +Each package is validated and includes both metadata and workflow definition. diff --git a/backend/autometabuilder/web/workflow_graph.py b/backend/autometabuilder/web/workflow_graph.py index f6e4209..e5ae2af 100644 --- a/backend/autometabuilder/web/workflow_graph.py +++ b/backend/autometabuilder/web/workflow_graph.py @@ -1,4 +1,4 @@ -"""Build a node/edge view of the declarative workflow for visualization.""" +"""Build a node/edge view of n8n workflows for visualization.""" from __future__ import annotations import json @@ -13,24 +13,13 @@ logger = logging.getLogger(__name__) def _parse_workflow_definition() -> Dict[str, Any]: payload = get_workflow_content() if not payload: - return {"nodes": []} + return {"name": "Empty", "nodes": [], "connections": {}} try: parsed = json.loads(payload) except json.JSONDecodeError as exc: logger.warning("Invalid workflow JSON: %s", exc) - return {"nodes": []} - return parsed if isinstance(parsed, dict) else {"nodes": []} - - -def _is_n8n_format(workflow: Dict[str, Any]) -> bool: - """Check if workflow uses n8n schema.""" - if "connections" not in workflow: - return False - nodes = workflow.get("nodes", []) - if nodes and isinstance(nodes, list): - first_node = nodes[0] - return "position" in first_node or "typeVersion" in first_node - return True + return {"name": "Invalid", "nodes": [], "connections": {}} + return parsed if isinstance(parsed, dict) else {"name": "Invalid", "nodes": [], "connections": {}} def _gather_n8n_nodes( @@ -83,61 +72,14 @@ def _build_n8n_edges( return edges -def _gather_nodes(nodes: Iterable[Dict[str, Any]], plugin_map: Dict[str, Any], parent_id: str | None = None, collected: List[Dict[str, Any]] | None = None) -> List[Dict[str, Any]]: - collected = collected or [] - for node in nodes: - node_id = node.get("id") or f"node-{len(collected)}" - node_type = node.get("type", "unknown") - metadata = plugin_map.get(node_type, {}) - node_summary: Dict[str, Any] = { - "id": node_id, - "type": node_type, - "label_key": metadata.get("label"), - "parent": parent_id, - "inputs": node.get("inputs", {}), - "outputs": node.get("outputs", {}), - } - collected.append(node_summary) - body = node.get("body") - if isinstance(body, list): - _gather_nodes(body, plugin_map, parent_id=node_id, collected=collected) - return collected - - -def _build_edges(nodes: Iterable[Dict[str, Any]]) -> List[Dict[str, str]]: - producers: Dict[str, str] = {} - for node in nodes: - outputs = node.get("outputs", {}) - for value in outputs.values(): - if isinstance(value, str): - if value in producers: - logger.debug("Variable %s already produced by %s; overwriting with %s", value, producers[value], node["id"]) - producers[value] = node["id"] - edges: List[Dict[str, str]] = [] - for node in nodes: - inputs = node.get("inputs", {}) - for port, value in inputs.items(): - if isinstance(value, str) and value.startswith("$"): - variable = value[1:] - source = producers.get(variable) - if source: - edges.append({"from": source, "to": node["id"], "var": variable, "port": port}) - else: - logger.debug("No producer found for %s referenced by %s.%s", variable, node["id"], port) - return edges - - def build_workflow_graph() -> Dict[str, Any]: + """Build workflow graph from n8n format (breaking change: legacy format removed).""" definition = _parse_workflow_definition() plugin_map = load_metadata().get("workflow_plugins", {}) - # Detect format and build accordingly - if _is_n8n_format(definition): - nodes = _gather_n8n_nodes(definition.get("nodes", []), plugin_map) - edges = _build_n8n_edges(definition.get("connections", {}), nodes) - else: - nodes = _gather_nodes(definition.get("nodes", []), plugin_map) - edges = _build_edges(nodes) + # Only support n8n format now + nodes = _gather_n8n_nodes(definition.get("nodes", []), plugin_map) + edges = _build_n8n_edges(definition.get("connections", {}), nodes) logger.debug("Built workflow graph with %d nodes and %d edges", len(nodes), len(edges)) return { diff --git a/backend/autometabuilder/workflow/engine.py b/backend/autometabuilder/workflow/engine.py index 5f6b2e2..0d31ef8 100644 --- a/backend/autometabuilder/workflow/engine.py +++ b/backend/autometabuilder/workflow/engine.py @@ -1,9 +1,9 @@ -"""Workflow engine runner.""" +"""Workflow engine runner for n8n format.""" from .workflow_adapter import WorkflowAdapter, is_n8n_workflow class WorkflowEngine: - """Run workflow configs through a node executor.""" + """Run n8n workflow configs (breaking change: legacy format removed).""" def __init__(self, workflow_config, node_executor, logger, runtime=None, plugin_registry=None): self.workflow_config = workflow_config or {} self.node_executor = node_executor @@ -18,15 +18,14 @@ class WorkflowEngine: self.adapter = None def execute(self): - """Execute the workflow config.""" - # Use adapter if available and workflow is n8n format - if self.adapter and is_n8n_workflow(self.workflow_config): - self.adapter.execute(self.workflow_config) - return + """Execute the n8n workflow config.""" + # Enforce n8n format only + if not is_n8n_workflow(self.workflow_config): + self.logger.error("Legacy workflow format is no longer supported. Please migrate to n8n schema.") + raise ValueError("Only n8n workflow format is supported") - # Fallback to legacy execution - nodes = self.workflow_config.get("nodes") - if not isinstance(nodes, list): - self.logger.error("Workflow config missing nodes list.") - return - self.node_executor.execute_nodes(nodes) + if self.adapter: + self.adapter.execute(self.workflow_config) + else: + self.logger.error("Workflow engine requires runtime and plugin_registry for n8n execution") + raise RuntimeError("Cannot execute n8n workflow without runtime and plugin_registry") diff --git a/backend/autometabuilder/workflow/execution_order.py b/backend/autometabuilder/workflow/execution_order.py new file mode 100644 index 0000000..901dc0b --- /dev/null +++ b/backend/autometabuilder/workflow/execution_order.py @@ -0,0 +1,45 @@ +"""Build execution order for n8n workflows.""" +from __future__ import annotations + +from typing import Any, Dict, List, Set + + +def build_execution_order( + nodes: List[Dict[str, Any]], + connections: Dict[str, Any] +) -> List[str]: + """Build topological execution order from connections.""" + node_names = {node["name"] for node in nodes} + has_inputs = _find_nodes_with_inputs(connections) + + # Start with nodes that have no inputs + order = [name for name in node_names if name not in has_inputs] + + # Add remaining nodes (simplified BFS) + remaining = node_names - set(order) + order.extend(_add_remaining_nodes(remaining)) + + return order + + +def _find_nodes_with_inputs(connections: Dict[str, Any]) -> Set[str]: + """Find all nodes that have incoming connections.""" + has_inputs = set() + + for source_name, outputs in connections.items(): + for output_type, indices in outputs.items(): + for targets in indices.values(): + for target in targets: + has_inputs.add(target["node"]) + + return has_inputs + + +def _add_remaining_nodes(remaining: Set[str]) -> List[str]: + """Add remaining nodes in order.""" + order = [] + while remaining: + name = next(iter(remaining)) + order.append(name) + remaining.remove(name) + return order diff --git a/backend/autometabuilder/workflow/n8n_executor.py b/backend/autometabuilder/workflow/n8n_executor.py index 01f96f2..0e28a83 100644 --- a/backend/autometabuilder/workflow/n8n_executor.py +++ b/backend/autometabuilder/workflow/n8n_executor.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any, Dict, List +from .execution_order import build_execution_order + logger = logging.getLogger(__name__) @@ -24,7 +26,7 @@ class N8NExecutor: return # Build execution order from connections - execution_order = self._build_execution_order(nodes, connections) + execution_order = build_execution_order(nodes, connections) # Execute nodes in order for node_name in execution_order: @@ -39,39 +41,6 @@ class N8NExecutor: return node return None - def _build_execution_order( - self, - nodes: List[Dict], - connections: Dict[str, Any] - ) -> List[str]: - """Build topological execution order.""" - # Simple approach: find nodes with no inputs, then process - node_names = {node["name"] for node in nodes} - has_inputs = set() - - for source_name, outputs in connections.items(): - for output_type, indices in outputs.items(): - for targets in indices.values(): - for target in targets: - has_inputs.add(target["node"]) - - # Start with nodes that have no inputs - order = [name for name in node_names if name not in has_inputs] - - # Add remaining nodes (simplified BFS) - remaining = node_names - set(order) - while remaining: - added = False - for name in list(remaining): - order.append(name) - remaining.remove(name) - added = True - break - if not added: - break - - return order - def _execute_node(self, node: Dict[str, Any]) -> Any: """Execute single node.""" node_type = node.get("type") diff --git a/backend/autometabuilder/workflow/workflow_adapter.py b/backend/autometabuilder/workflow/workflow_adapter.py index 9b4c000..c3521c4 100644 --- a/backend/autometabuilder/workflow/workflow_adapter.py +++ b/backend/autometabuilder/workflow/workflow_adapter.py @@ -1,4 +1,4 @@ -"""Adapter to detect and route workflow formats.""" +"""N8N workflow format handler.""" from __future__ import annotations import logging @@ -14,7 +14,7 @@ def is_n8n_workflow(workflow: Dict[str, Any]) -> bool: if not isinstance(workflow, dict): return False - # N8N workflows have explicit connections and position in nodes + # N8N workflows must have explicit connections has_connections = "connections" in workflow nodes = workflow.get("nodes", []) @@ -31,21 +31,18 @@ def is_n8n_workflow(workflow: Dict[str, Any]) -> bool: class WorkflowAdapter: - """Adapt between legacy and n8n workflows.""" + """Execute n8n workflows (breaking change: legacy format no longer supported).""" def __init__(self, node_executor, runtime, plugin_registry): - self.node_executor = node_executor self.runtime = runtime self.plugin_registry = plugin_registry self.n8n_executor = N8NExecutor(runtime, plugin_registry) def execute(self, workflow: Dict[str, Any]) -> None: - """Execute workflow using appropriate format handler.""" - if is_n8n_workflow(workflow): - logger.debug("Executing n8n-style workflow") - self.n8n_executor.execute(workflow) - else: - logger.debug("Executing legacy workflow") - nodes = workflow.get("nodes", []) - if isinstance(nodes, list): - self.node_executor.execute_nodes(nodes) + """Execute n8n workflow.""" + if not is_n8n_workflow(workflow): + logger.error("Legacy workflow format is no longer supported. Please migrate to n8n schema.") + raise ValueError("Only n8n workflow format is supported") + + logger.debug("Executing n8n workflow") + self.n8n_executor.execute(workflow) diff --git a/backend/autometabuilder/workflow_packages/blank.json b/backend/autometabuilder/workflow_packages/blank.json deleted file mode 100644 index 822898b..0000000 --- a/backend/autometabuilder/workflow_packages/blank.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "blank", - "label": "meta.workflow_packages.blank.label", - "description": "meta.workflow_packages.blank.description", - "tags": [ - "starter" - ], - "workflow": { - "name": "Blank Canvas", - "active": false, - "nodes": [], - "connections": {} - } -} diff --git a/backend/autometabuilder/workflow_packages/contextual_iterative_loop.json b/backend/autometabuilder/workflow_packages/contextual_iterative_loop.json deleted file mode 100644 index fa9693f..0000000 --- a/backend/autometabuilder/workflow_packages/contextual_iterative_loop.json +++ /dev/null @@ -1,187 +0,0 @@ -{ - "id": "contextual_iterative_loop", - "label": "meta.workflow_packages.contextual_iterative_loop.label", - "description": "meta.workflow_packages.contextual_iterative_loop.description", - "tags": [ - "context", - "loop", - "map-reduce" - ], - "workflow": { - "name": "meta.workflow_packages.contextual_iterative_loop.label", - "active": false, - "nodes": [ - { - "id": "list_files", - "name": "List Files", - "type": "tools.list_files", - "typeVersion": 1, - "position": [ - 0, - 50 - ], - "parameters": { - "path": "." - } - }, - { - "id": "filter_python", - "name": "Filter Python", - "type": "utils.filter_list", - "typeVersion": 1, - "position": [ - 300, - 50 - ], - "parameters": { - "items": "$repo_files", - "mode": "regex", - "pattern": "\\.py$" - } - }, - { - "id": "map_python", - "name": "Map Python", - "type": "utils.map_list", - "typeVersion": 1, - "position": [ - 600, - 50 - ], - "parameters": { - "items": "$python_files", - "template": "PY: {item}" - } - }, - { - "id": "reduce_python", - "name": "Reduce Python", - "type": "utils.reduce_list", - "typeVersion": 1, - "position": [ - 900, - 50 - ], - "parameters": { - "items": "$python_lines", - "separator": "\\n" - } - }, - { - "id": "seed_messages", - "name": "Seed Messages", - "type": "core.seed_messages", - "typeVersion": 1, - "position": [ - 1200, - 50 - ], - "parameters": {} - }, - { - "id": "append_repo_summary", - "name": "Append Repo Summary", - "type": "core.append_context_message", - "typeVersion": 1, - "position": [ - 1500, - 50 - ], - "parameters": { - "messages": "$messages", - "context": "$python_summary" - } - }, - { - "id": "append_user_instruction", - "name": "Append User Instruction", - "type": "core.append_user_instruction", - "typeVersion": 1, - "position": [ - 1800, - 50 - ], - "parameters": { - "messages": "$messages" - } - }, - { - "id": "main_loop", - "name": "Main Loop", - "type": "control.loop", - "typeVersion": 1, - "position": [ - 2100, - 50 - ], - "parameters": { - "max_iterations": 5, - "stop_when": "$no_tool_calls", - "stop_on": "true" - } - } - ], - "connections": { - "List Files": { - "main": { - "0": [ - { - "node": "Filter Python", - "type": "main", - "index": 0 - } - ] - } - }, - "Filter Python": { - "main": { - "0": [ - { - "node": "Map Python", - "type": "main", - "index": 0 - } - ] - } - }, - "Map Python": { - "main": { - "0": [ - { - "node": "Reduce Python", - "type": "main", - "index": 0 - } - ] - } - }, - "Append User Instruction": { - "main": { - "0": [ - { - "node": "Append Repo Summary", - "type": "main", - "index": 0 - }, - { - "node": "Append User Instruction", - "type": "main", - "index": 0 - } - ] - } - }, - "Reduce Python": { - "main": { - "0": [ - { - "node": "Append Repo Summary", - "type": "main", - "index": 0 - } - ] - } - } - } - } -} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/game_tick_loop.json b/backend/autometabuilder/workflow_packages/game_tick_loop.json deleted file mode 100644 index 393a612..0000000 --- a/backend/autometabuilder/workflow_packages/game_tick_loop.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "id": "game_tick_loop", - "label": "meta.workflow_packages.game_tick_loop.label", - "description": "meta.workflow_packages.game_tick_loop.description", - "tags": [ - "game", - "loop", - "ticks" - ], - "workflow": { - "name": "meta.workflow_packages.game_tick_loop.label", - "active": false, - "nodes": [ - { - "id": "seed_messages", - "name": "Seed Messages", - "type": "core.seed_messages", - "typeVersion": 1, - "position": [ - 0, - 50 - ], - "parameters": {} - }, - { - "id": "map_ticks", - "name": "Map Ticks", - "type": "utils.map_list", - "typeVersion": 1, - "position": [ - 300, - 50 - ], - "parameters": { - "items": [ - "tick_start", - "tick_update", - "tick_render" - ], - "template": "Tick: {item}" - } - }, - { - "id": "reduce_ticks", - "name": "Reduce Ticks", - "type": "utils.reduce_list", - "typeVersion": 1, - "position": [ - 600, - 50 - ], - "parameters": { - "items": "$tick_lines", - "separator": "\\n" - } - }, - { - "id": "append_tick_context", - "name": "Append Tick Context", - "type": "core.append_context_message", - "typeVersion": 1, - "position": [ - 900, - 50 - ], - "parameters": { - "messages": "$messages", - "context": "$tick_context" - } - }, - { - "id": "main_loop", - "name": "Main Loop", - "type": "control.loop", - "typeVersion": 1, - "position": [ - 1200, - 50 - ], - "parameters": { - "max_iterations": 3, - "stop_when": "$no_tool_calls", - "stop_on": "true" - } - } - ], - "connections": { - "Map Ticks": { - "main": { - "0": [ - { - "node": "Reduce Ticks", - "type": "main", - "index": 0 - } - ] - } - }, - "Append Tick Context": { - "main": { - "0": [ - { - "node": "Append Tick Context", - "type": "main", - "index": 0 - } - ] - } - }, - "Reduce Ticks": { - "main": { - "0": [ - { - "node": "Append Tick Context", - "type": "main", - "index": 0 - } - ] - } - } - } - } -} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/iterative_loop.json b/backend/autometabuilder/workflow_packages/iterative_loop.json deleted file mode 100644 index d7098a2..0000000 --- a/backend/autometabuilder/workflow_packages/iterative_loop.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "id": "iterative_loop", - "label": "meta.workflow_packages.iterative_loop.label", - "description": "meta.workflow_packages.iterative_loop.description", - "tags": [ - "loop", - "tools" - ], - "workflow": { - "name": "Iterative Agent Loop", - "active": false, - "nodes": [ - { - "id": "load_context", - "name": "Load Context", - "type": "core.load_context", - "typeVersion": 1, - "position": [0, 0], - "parameters": {} - }, - { - "id": "seed_messages", - "name": "Seed Messages", - "type": "core.seed_messages", - "typeVersion": 1, - "position": [0, 100], - "parameters": {} - }, - { - "id": "append_context", - "name": "Append Context", - "type": "core.append_context_message", - "typeVersion": 1, - "position": [300, 50], - "parameters": {} - }, - { - "id": "append_user_instruction", - "name": "Append User Instruction", - "type": "core.append_user_instruction", - "typeVersion": 1, - "position": [600, 50], - "parameters": {} - }, - { - "id": "main_loop", - "name": "Main Loop", - "type": "control.loop", - "typeVersion": 1, - "position": [900, 50], - "parameters": { - "max_iterations": 10, - "stop_when": "$no_tool_calls", - "stop_on": "true" - } - }, - { - "id": "ai_request", - "name": "AI Request", - "type": "core.ai_request", - "typeVersion": 1, - "position": [1200, 50], - "parameters": {} - }, - { - "id": "run_tool_calls", - "name": "Run Tool Calls", - "type": "core.run_tool_calls", - "typeVersion": 1, - "position": [1500, 50], - "parameters": {} - }, - { - "id": "append_tool_results", - "name": "Append Tool Results", - "type": "core.append_tool_results", - "typeVersion": 1, - "position": [1800, 50], - "parameters": {} - } - ], - "connections": { - "Load Context": { - "main": { - "0": [{"node": "Append Context", "type": "main", "index": 0}] - } - }, - "Seed Messages": { - "main": { - "0": [{"node": "Append Context", "type": "main", "index": 0}] - } - }, - "Append Context": { - "main": { - "0": [{"node": "Append User Instruction", "type": "main", "index": 0}] - } - }, - "Append User Instruction": { - "main": { - "0": [{"node": "Main Loop", "type": "main", "index": 0}] - } - }, - "Main Loop": { - "main": { - "0": [{"node": "AI Request", "type": "main", "index": 0}] - } - }, - "AI Request": { - "main": { - "0": [{"node": "Run Tool Calls", "type": "main", "index": 0}] - } - }, - "Run Tool Calls": { - "main": { - "0": [{"node": "Append Tool Results", "type": "main", "index": 0}] - } - }, - "Append Tool Results": { - "main": { - "0": [{"node": "Main Loop", "type": "main", "index": 0}] - } - } - } - } -} diff --git a/backend/autometabuilder/workflow_packages/plan_execute_summarize.json b/backend/autometabuilder/workflow_packages/plan_execute_summarize.json deleted file mode 100644 index 56d511b..0000000 --- a/backend/autometabuilder/workflow_packages/plan_execute_summarize.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "id": "plan_execute_summarize", - "label": "meta.workflow_packages.plan_execute_summarize.label", - "description": "meta.workflow_packages.plan_execute_summarize.description", - "tags": [ - "plan", - "summarize" - ], - "workflow": { - "name": "meta.workflow_packages.plan_execute_summarize.label", - "active": false, - "nodes": [ - { - "id": "load_context", - "name": "Load Context", - "type": "core.load_context", - "typeVersion": 1, - "position": [ - 0, - 50 - ], - "parameters": {} - }, - { - "id": "seed_messages", - "name": "Seed Messages", - "type": "core.seed_messages", - "typeVersion": 1, - "position": [ - 300, - 50 - ], - "parameters": {} - }, - { - "id": "append_context", - "name": "Append Context", - "type": "core.append_context_message", - "typeVersion": 1, - "position": [ - 600, - 50 - ], - "parameters": { - "messages": "$messages", - "context": "$sdlc_context" - } - }, - { - "id": "append_user_instruction", - "name": "Append User Instruction", - "type": "core.append_user_instruction", - "typeVersion": 1, - "position": [ - 900, - 50 - ], - "parameters": { - "messages": "$messages" - } - }, - { - "id": "planner_request", - "name": "Planner Request", - "type": "core.ai_request", - "typeVersion": 1, - "position": [ - 1200, - 50 - ], - "parameters": { - "messages": "$messages" - } - }, - { - "id": "run_tool_calls", - "name": "Run Tool Calls", - "type": "core.run_tool_calls", - "typeVersion": 1, - "position": [ - 1500, - 50 - ], - "parameters": { - "response": "$llm_response" - } - }, - { - "id": "append_tool_results", - "name": "Append Tool Results", - "type": "core.append_tool_results", - "typeVersion": 1, - "position": [ - 1800, - 50 - ], - "parameters": { - "messages": "$messages", - "tool_results": "$tool_results" - } - }, - { - "id": "summary_request", - "name": "Summary Request", - "type": "core.ai_request", - "typeVersion": 1, - "position": [ - 2100, - 50 - ], - "parameters": { - "messages": "$messages" - } - } - ], - "connections": { - "Append Tool Results": { - "main": { - "0": [ - { - "node": "Append Context", - "type": "main", - "index": 0 - }, - { - "node": "Append User Instruction", - "type": "main", - "index": 0 - }, - { - "node": "Planner Request", - "type": "main", - "index": 0 - }, - { - "node": "Append Tool Results", - "type": "main", - "index": 0 - }, - { - "node": "Summary Request", - "type": "main", - "index": 0 - } - ] - } - }, - "Load Context": { - "main": { - "0": [ - { - "node": "Append Context", - "type": "main", - "index": 0 - } - ] - } - }, - "Planner Request": { - "main": { - "0": [ - { - "node": "Run Tool Calls", - "type": "main", - "index": 0 - } - ] - } - }, - "Run Tool Calls": { - "main": { - "0": [ - { - "node": "Append Tool Results", - "type": "main", - "index": 0 - } - ] - } - } - } - } -} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/repo_scan_context.json b/backend/autometabuilder/workflow_packages/repo_scan_context.json deleted file mode 100644 index b1f86fa..0000000 --- a/backend/autometabuilder/workflow_packages/repo_scan_context.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "id": "repo_scan_context", - "label": "meta.workflow_packages.repo_scan_context.label", - "description": "meta.workflow_packages.repo_scan_context.description", - "tags": [ - "map", - "reduce", - "context" - ], - "workflow": { - "name": "meta.workflow_packages.repo_scan_context.label", - "active": false, - "nodes": [ - { - "id": "list_files", - "name": "List Files", - "type": "tools.list_files", - "typeVersion": 1, - "position": [ - 0, - 50 - ], - "parameters": { - "path": "." - } - }, - { - "id": "filter_python", - "name": "Filter Python", - "type": "utils.filter_list", - "typeVersion": 1, - "position": [ - 300, - 50 - ], - "parameters": { - "items": "$repo_files", - "mode": "regex", - "pattern": "\\.py$" - } - }, - { - "id": "reduce_python", - "name": "Reduce Python", - "type": "utils.reduce_list", - "typeVersion": 1, - "position": [ - 600, - 50 - ], - "parameters": { - "items": "$python_files", - "separator": "\\n" - } - }, - { - "id": "seed_messages", - "name": "Seed Messages", - "type": "core.seed_messages", - "typeVersion": 1, - "position": [ - 900, - 50 - ], - "parameters": {} - }, - { - "id": "append_repo_summary", - "name": "Append Repo Summary", - "type": "core.append_context_message", - "typeVersion": 1, - "position": [ - 1200, - 50 - ], - "parameters": { - "messages": "$messages", - "context": "$python_summary" - } - }, - { - "id": "append_user_instruction", - "name": "Append User Instruction", - "type": "core.append_user_instruction", - "typeVersion": 1, - "position": [ - 1500, - 50 - ], - "parameters": { - "messages": "$messages" - } - }, - { - "id": "ai_request", - "name": "Ai Request", - "type": "core.ai_request", - "typeVersion": 1, - "position": [ - 1800, - 50 - ], - "parameters": { - "messages": "$messages" - } - }, - { - "id": "run_tool_calls", - "name": "Run Tool Calls", - "type": "core.run_tool_calls", - "typeVersion": 1, - "position": [ - 2100, - 50 - ], - "parameters": { - "response": "$llm_response" - } - }, - { - "id": "append_tool_results", - "name": "Append Tool Results", - "type": "core.append_tool_results", - "typeVersion": 1, - "position": [ - 2400, - 50 - ], - "parameters": { - "messages": "$messages", - "tool_results": "$tool_results" - } - } - ], - "connections": { - "List Files": { - "main": { - "0": [ - { - "node": "Filter Python", - "type": "main", - "index": 0 - } - ] - } - }, - "Filter Python": { - "main": { - "0": [ - { - "node": "Reduce Python", - "type": "main", - "index": 0 - } - ] - } - }, - "Append Tool Results": { - "main": { - "0": [ - { - "node": "Append Repo Summary", - "type": "main", - "index": 0 - }, - { - "node": "Append User Instruction", - "type": "main", - "index": 0 - }, - { - "node": "Ai Request", - "type": "main", - "index": 0 - }, - { - "node": "Append Tool Results", - "type": "main", - "index": 0 - } - ] - } - }, - "Reduce Python": { - "main": { - "0": [ - { - "node": "Append Repo Summary", - "type": "main", - "index": 0 - } - ] - } - }, - "Ai Request": { - "main": { - "0": [ - { - "node": "Run Tool Calls", - "type": "main", - "index": 0 - } - ] - } - }, - "Run Tool Calls": { - "main": { - "0": [ - { - "node": "Append Tool Results", - "type": "main", - "index": 0 - } - ] - } - } - } - } -} \ No newline at end of file diff --git a/backend/autometabuilder/workflow_packages/single_pass.json b/backend/autometabuilder/workflow_packages/single_pass.json deleted file mode 100644 index a624d91..0000000 --- a/backend/autometabuilder/workflow_packages/single_pass.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "id": "single_pass", - "label": "meta.workflow_packages.single_pass.label", - "description": "meta.workflow_packages.single_pass.description", - "tags": [ - "single", - "tools" - ], - "workflow": { - "name": "Single Pass", - "active": false, - "nodes": [ - { - "id": "load_context", - "name": "Load Context", - "type": "core.load_context", - "typeVersion": 1, - "position": [0, 0], - "parameters": {} - }, - { - "id": "seed_messages", - "name": "Seed Messages", - "type": "core.seed_messages", - "typeVersion": 1, - "position": [0, 100], - "parameters": {} - }, - { - "id": "append_context", - "name": "Append Context", - "type": "core.append_context_message", - "typeVersion": 1, - "position": [300, 50], - "parameters": {} - }, - { - "id": "append_user_instruction", - "name": "Append User Instruction", - "type": "core.append_user_instruction", - "typeVersion": 1, - "position": [600, 50], - "parameters": {} - }, - { - "id": "ai_request", - "name": "AI Request", - "type": "core.ai_request", - "typeVersion": 1, - "position": [900, 50], - "parameters": {} - }, - { - "id": "run_tool_calls", - "name": "Run Tool Calls", - "type": "core.run_tool_calls", - "typeVersion": 1, - "position": [1200, 50], - "parameters": {} - }, - { - "id": "append_tool_results", - "name": "Append Tool Results", - "type": "core.append_tool_results", - "typeVersion": 1, - "position": [1500, 50], - "parameters": {} - } - ], - "connections": { - "Load Context": { - "main": { - "0": [{"node": "Append Context", "type": "main", "index": 0}] - } - }, - "Seed Messages": { - "main": { - "0": [{"node": "Append Context", "type": "main", "index": 0}] - } - }, - "Append Context": { - "main": { - "0": [{"node": "Append User Instruction", "type": "main", "index": 0}] - } - }, - "Append User Instruction": { - "main": { - "0": [{"node": "AI Request", "type": "main", "index": 0}] - } - }, - "AI Request": { - "main": { - "0": [{"node": "Run Tool Calls", "type": "main", "index": 0}] - } - }, - "Run Tool Calls": { - "main": { - "0": [{"node": "Append Tool Results", "type": "main", "index": 0}] - } - } - } - } -} diff --git a/backend/autometabuilder/workflow_packages/testing_triangle.json b/backend/autometabuilder/workflow_packages/testing_triangle.json deleted file mode 100644 index 7b20366..0000000 --- a/backend/autometabuilder/workflow_packages/testing_triangle.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "id": "testing_triangle", - "label": "meta.workflow_packages.testing_triangle.label", - "description": "meta.workflow_packages.testing_triangle.description", - "tags": [ - "testing", - "lint", - "e2e" - ], - "workflow": { - "name": "meta.workflow_packages.testing_triangle.label", - "active": false, - "nodes": [ - { - "id": "lint", - "name": "Lint", - "type": "tools.run_lint", - "typeVersion": 1, - "position": [ - 0, - 50 - ], - "parameters": { - "path": "src" - } - }, - { - "id": "lint_failed", - "name": "Lint Failed", - "type": "utils.branch_condition", - "typeVersion": 1, - "position": [ - 300, - 50 - ], - "parameters": { - "value": "$lint_results", - "mode": "regex", - "compare": "(FAILED|ERROR)" - } - }, - { - "id": "lint_ok", - "name": "Lint Ok", - "type": "utils.not", - "typeVersion": 1, - "position": [ - 600, - 50 - ], - "parameters": { - "value": "$lint_failed" - } - }, - { - "id": "unit_tests", - "name": "Unit Tests", - "type": "tools.run_tests", - "typeVersion": 1, - "position": [ - 900, - 50 - ], - "parameters": { - "path": "tests" - } - }, - { - "id": "unit_failed", - "name": "Unit Failed", - "type": "utils.branch_condition", - "typeVersion": 1, - "position": [ - 1200, - 50 - ], - "parameters": { - "value": "$unit_results", - "mode": "regex", - "compare": "(FAILED|ERROR)" - } - }, - { - "id": "unit_ok", - "name": "Unit Ok", - "type": "utils.not", - "typeVersion": 1, - "position": [ - 1500, - 50 - ], - "parameters": { - "value": "$unit_failed" - } - }, - { - "id": "ui_tests", - "name": "Ui Tests", - "type": "tools.run_tests", - "typeVersion": 1, - "position": [ - 1800, - 50 - ], - "parameters": { - "path": "tests/ui" - } - } - ], - "connections": { - "Lint": { - "main": { - "0": [ - { - "node": "Lint Failed", - "type": "main", - "index": 0 - } - ] - } - }, - "Lint Failed": { - "main": { - "0": [ - { - "node": "Lint Ok", - "type": "main", - "index": 0 - } - ] - } - }, - "Unit Tests": { - "main": { - "0": [ - { - "node": "Unit Failed", - "type": "main", - "index": 0 - } - ] - } - }, - "Unit Failed": { - "main": { - "0": [ - { - "node": "Unit Ok", - "type": "main", - "index": 0 - } - ] - } - } - } - } -} \ No newline at end of file diff --git a/backend/tests/test_n8n_schema.py b/backend/tests/test_n8n_schema.py new file mode 100644 index 0000000..d18469b --- /dev/null +++ b/backend/tests/test_n8n_schema.py @@ -0,0 +1,73 @@ +"""Tests for n8n workflow schema validation.""" +import pytest + +from autometabuilder.workflow.n8n_schema import N8NNode, N8NPosition, N8NWorkflow + + +def test_n8n_position_validation(): + """Test position validation.""" + assert N8NPosition.validate([0, 0]) + assert N8NPosition.validate([100.5, 200.5]) + assert not N8NPosition.validate([0]) + assert not N8NPosition.validate([0, 0, 0]) + assert not N8NPosition.validate("invalid") + assert not N8NPosition.validate(None) + + +def test_n8n_node_validation(): + """Test node validation.""" + valid_node = { + "id": "node-1", + "name": "Test Node", + "type": "core.test", + "typeVersion": 1, + "position": [0, 0], + "parameters": {} + } + assert N8NNode.validate(valid_node) + + # Missing required fields + assert not N8NNode.validate({}) + assert not N8NNode.validate({"id": "node-1"}) + + # Invalid typeVersion + invalid_node = valid_node.copy() + invalid_node["typeVersion"] = 0 + assert not N8NNode.validate(invalid_node) + + # Invalid position + invalid_node = valid_node.copy() + invalid_node["position"] = [0] + assert not N8NNode.validate(invalid_node) + + +def test_n8n_workflow_validation(): + """Test workflow validation.""" + valid_workflow = { + "name": "Test Workflow", + "nodes": [ + { + "id": "node-1", + "name": "Node 1", + "type": "core.test", + "typeVersion": 1, + "position": [0, 0] + } + ], + "connections": {} + } + assert N8NWorkflow.validate(valid_workflow) + + # Missing required fields + assert not N8NWorkflow.validate({}) + assert not N8NWorkflow.validate({"name": "Test"}) + + # Empty nodes array is invalid + invalid_workflow = valid_workflow.copy() + invalid_workflow["nodes"] = [] + assert not N8NWorkflow.validate(invalid_workflow) + + # Invalid node + invalid_workflow = valid_workflow.copy() + invalid_workflow["nodes"] = [{"id": "bad"}] + assert not N8NWorkflow.validate(invalid_workflow)