mirror of
https://github.com/johndoe6345789/AutoMetabuilder.git
synced 2026-04-24 13:54:59 +00:00
Merge pull request #4 from johndoe6345789/copilot/fix-tech-debt-breaking-changes
Breaking: Migrate to n8n workflow schema with npm-style packages
This commit is contained in:
173
MIGRATION.md
Normal file
173
MIGRATION.md
Normal file
@@ -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/`
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
72
backend/autometabuilder/packages/README.md
Normal file
72
backend/autometabuilder/packages/README.md
Normal file
@@ -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.
|
||||
20
backend/autometabuilder/packages/blank/package.json
Normal file
20
backend/autometabuilder/packages/blank/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
6
backend/autometabuilder/packages/blank/workflow.json
Normal file
6
backend/autometabuilder/packages/blank/workflow.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Blank Canvas",
|
||||
"active": false,
|
||||
"nodes": [],
|
||||
"connections": {}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
backend/autometabuilder/packages/game_tick_loop/package.json
Normal file
24
backend/autometabuilder/packages/game_tick_loop/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
113
backend/autometabuilder/packages/game_tick_loop/workflow.json
Normal file
113
backend/autometabuilder/packages/game_tick_loop/workflow.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
backend/autometabuilder/packages/iterative_loop/package.json
Normal file
22
backend/autometabuilder/packages/iterative_loop/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
188
backend/autometabuilder/packages/iterative_loop/workflow.json
Normal file
188
backend/autometabuilder/packages/iterative_loop/workflow.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
209
backend/autometabuilder/packages/repo_scan_context/workflow.json
Normal file
209
backend/autometabuilder/packages/repo_scan_context/workflow.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
backend/autometabuilder/packages/single_pass/package.json
Normal file
22
backend/autometabuilder/packages/single_pass/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
151
backend/autometabuilder/packages/single_pass/workflow.json
Normal file
151
backend/autometabuilder/packages/single_pass/workflow.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
147
backend/autometabuilder/packages/testing_triangle/workflow.json
Normal file
147
backend/autometabuilder/packages/testing_triangle/workflow.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
backend/autometabuilder/web/data/package_loader.py
Normal file
74
backend/autometabuilder/web/data/package_loader.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,64 +13,74 @@ 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": []}
|
||||
return {"name": "Invalid", "nodes": [], "connections": {}}
|
||||
return parsed if isinstance(parsed, dict) else {"name": "Invalid", "nodes": [], "connections": {}}
|
||||
|
||||
|
||||
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 []
|
||||
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") or f"node-{len(collected)}"
|
||||
node_id = node.get("id", node.get("name", f"node-{len(collected)}"))
|
||||
node_type = node.get("type", "unknown")
|
||||
metadata = plugin_map.get(node_type, {})
|
||||
node_summary: Dict[str, Any] = {
|
||||
|
||||
collected.append({
|
||||
"id": node_id,
|
||||
"name": node.get("name", 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)
|
||||
"parent": None,
|
||||
"position": node.get("position", [0, 0]),
|
||||
})
|
||||
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)
|
||||
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 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", {})
|
||||
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 {
|
||||
"nodes": nodes,
|
||||
|
||||
@@ -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}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
"""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."""
|
||||
def __init__(self, workflow_config, node_executor, logger):
|
||||
"""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
|
||||
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."""
|
||||
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)
|
||||
"""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")
|
||||
|
||||
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")
|
||||
|
||||
45
backend/autometabuilder/workflow/execution_order.py
Normal file
45
backend/autometabuilder/workflow/execution_order.py
Normal file
@@ -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
|
||||
110
backend/autometabuilder/workflow/n8n_converter.py
Normal file
110
backend/autometabuilder/workflow/n8n_converter.py
Normal file
@@ -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", []),
|
||||
}
|
||||
70
backend/autometabuilder/workflow/n8n_executor.py
Normal file
70
backend/autometabuilder/workflow/n8n_executor.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Execute n8n-style workflows with explicit connections."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from .execution_order import build_execution_order
|
||||
|
||||
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 = 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 _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
|
||||
72
backend/autometabuilder/workflow/n8n_schema.py
Normal file
72
backend/autometabuilder/workflow/n8n_schema.py
Normal file
@@ -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"])
|
||||
48
backend/autometabuilder/workflow/workflow_adapter.py
Normal file
48
backend/autometabuilder/workflow/workflow_adapter.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""N8N workflow format handler."""
|
||||
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 must have explicit connections
|
||||
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:
|
||||
"""Execute n8n workflows (breaking change: legacy format no longer supported)."""
|
||||
|
||||
def __init__(self, node_executor, runtime, plugin_registry):
|
||||
self.runtime = runtime
|
||||
self.plugin_registry = plugin_registry
|
||||
self.n8n_executor = N8NExecutor(runtime, plugin_registry)
|
||||
|
||||
def execute(self, workflow: Dict[str, Any]) -> None:
|
||||
"""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)
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "blank",
|
||||
"label": "meta.workflow_packages.blank.label",
|
||||
"description": "meta.workflow_packages.blank.description",
|
||||
"tags": [
|
||||
"starter"
|
||||
],
|
||||
"workflow": {
|
||||
"nodes": []
|
||||
}
|
||||
}
|
||||
@@ -1,80 +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": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "list_files",
|
||||
"type": "tools.list_files",
|
||||
"inputs": {"path": "."},
|
||||
"outputs": {"files": "repo_files"}
|
||||
},
|
||||
{
|
||||
"id": "filter_python",
|
||||
"type": "utils.filter_list",
|
||||
"inputs": {"items": "$repo_files", "mode": "regex", "pattern": "\\.py$"},
|
||||
"outputs": {"items": "python_files"}
|
||||
},
|
||||
{
|
||||
"id": "map_python",
|
||||
"type": "utils.map_list",
|
||||
"inputs": {"items": "$python_files", "template": "PY: {item}"},
|
||||
"outputs": {"items": "python_lines"}
|
||||
},
|
||||
{
|
||||
"id": "reduce_python",
|
||||
"type": "utils.reduce_list",
|
||||
"inputs": {"items": "$python_lines", "separator": "\\n"},
|
||||
"outputs": {"result": "python_summary"}
|
||||
},
|
||||
{
|
||||
"id": "seed_messages",
|
||||
"type": "core.seed_messages",
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"id": "append_repo_summary",
|
||||
"type": "core.append_context_message",
|
||||
"inputs": {"messages": "$messages", "context": "$python_summary"},
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"id": "append_user_instruction",
|
||||
"type": "core.append_user_instruction",
|
||||
"inputs": {"messages": "$messages"},
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"id": "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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,65 +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": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "seed_messages",
|
||||
"type": "core.seed_messages",
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"id": "map_ticks",
|
||||
"type": "utils.map_list",
|
||||
"inputs": {
|
||||
"items": ["tick_start", "tick_update", "tick_render"],
|
||||
"template": "Tick: {item}"
|
||||
},
|
||||
"outputs": {"items": "tick_lines"}
|
||||
},
|
||||
{
|
||||
"id": "reduce_ticks",
|
||||
"type": "utils.reduce_list",
|
||||
"inputs": {"items": "$tick_lines", "separator": "\\n"},
|
||||
"outputs": {"result": "tick_context"}
|
||||
},
|
||||
{
|
||||
"id": "append_tick_context",
|
||||
"type": "core.append_context_message",
|
||||
"inputs": {"messages": "$messages", "context": "$tick_context"},
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"id": "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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"id": "iterative_loop",
|
||||
"label": "meta.workflow_packages.iterative_loop.label",
|
||||
"description": "meta.workflow_packages.iterative_loop.description",
|
||||
"tags": [
|
||||
"loop",
|
||||
"tools"
|
||||
],
|
||||
"workflow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_context",
|
||||
"type": "core.load_context",
|
||||
"outputs": {
|
||||
"context": "sdlc_context"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed_messages",
|
||||
"type": "core.seed_messages",
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "append_context",
|
||||
"type": "core.append_context_message",
|
||||
"inputs": {
|
||||
"messages": "$messages",
|
||||
"context": "$sdlc_context"
|
||||
},
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "append_user_instruction",
|
||||
"type": "core.append_user_instruction",
|
||||
"inputs": {
|
||||
"messages": "$messages"
|
||||
},
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "main_loop",
|
||||
"type": "control.loop",
|
||||
"inputs": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,94 +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": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_context",
|
||||
"type": "core.load_context",
|
||||
"outputs": {
|
||||
"context": "sdlc_context"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed_messages",
|
||||
"type": "core.seed_messages",
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "append_context",
|
||||
"type": "core.append_context_message",
|
||||
"inputs": {
|
||||
"messages": "$messages",
|
||||
"context": "$sdlc_context"
|
||||
},
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "append_user_instruction",
|
||||
"type": "core.append_user_instruction",
|
||||
"inputs": {
|
||||
"messages": "$messages"
|
||||
},
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "planner_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": "summary_request",
|
||||
"type": "core.ai_request",
|
||||
"inputs": {
|
||||
"messages": "$messages"
|
||||
},
|
||||
"outputs": {
|
||||
"response": "final_response",
|
||||
"has_tool_calls": "final_has_tool_calls",
|
||||
"tool_calls_count": "final_tool_calls_count"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,67 +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": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "list_files",
|
||||
"type": "tools.list_files",
|
||||
"inputs": {"path": "."},
|
||||
"outputs": {"files": "repo_files"}
|
||||
},
|
||||
{
|
||||
"id": "filter_python",
|
||||
"type": "utils.filter_list",
|
||||
"inputs": {"items": "$repo_files", "mode": "regex", "pattern": "\\.py$"},
|
||||
"outputs": {"items": "python_files"}
|
||||
},
|
||||
{
|
||||
"id": "reduce_python",
|
||||
"type": "utils.reduce_list",
|
||||
"inputs": {"items": "$python_files", "separator": "\\n"},
|
||||
"outputs": {"result": "python_summary"}
|
||||
},
|
||||
{
|
||||
"id": "seed_messages",
|
||||
"type": "core.seed_messages",
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"id": "append_repo_summary",
|
||||
"type": "core.append_context_message",
|
||||
"inputs": {"messages": "$messages", "context": "$python_summary"},
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"id": "append_user_instruction",
|
||||
"type": "core.append_user_instruction",
|
||||
"inputs": {"messages": "$messages"},
|
||||
"outputs": {"messages": "messages"}
|
||||
},
|
||||
{
|
||||
"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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"id": "single_pass",
|
||||
"label": "meta.workflow_packages.single_pass.label",
|
||||
"description": "meta.workflow_packages.single_pass.description",
|
||||
"tags": [
|
||||
"single",
|
||||
"tools"
|
||||
],
|
||||
"workflow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "load_context",
|
||||
"type": "core.load_context",
|
||||
"outputs": {
|
||||
"context": "sdlc_context"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seed_messages",
|
||||
"type": "core.seed_messages",
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "append_context",
|
||||
"type": "core.append_context_message",
|
||||
"inputs": {
|
||||
"messages": "$messages",
|
||||
"context": "$sdlc_context"
|
||||
},
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "append_user_instruction",
|
||||
"type": "core.append_user_instruction",
|
||||
"inputs": {
|
||||
"messages": "$messages"
|
||||
},
|
||||
"outputs": {
|
||||
"messages": "messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"id": "testing_triangle",
|
||||
"label": "meta.workflow_packages.testing_triangle.label",
|
||||
"description": "meta.workflow_packages.testing_triangle.description",
|
||||
"tags": ["testing", "lint", "e2e"],
|
||||
"workflow": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "lint",
|
||||
"type": "tools.run_lint",
|
||||
"inputs": {
|
||||
"path": "src"
|
||||
},
|
||||
"outputs": {
|
||||
"results": "lint_results"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lint_failed",
|
||||
"type": "utils.branch_condition",
|
||||
"inputs": {
|
||||
"value": "$lint_results",
|
||||
"mode": "regex",
|
||||
"compare": "(FAILED|ERROR)"
|
||||
},
|
||||
"outputs": {
|
||||
"result": "lint_failed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lint_ok",
|
||||
"type": "utils.not",
|
||||
"inputs": {
|
||||
"value": "$lint_failed"
|
||||
},
|
||||
"outputs": {
|
||||
"result": "lint_ok"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "unit_tests",
|
||||
"type": "tools.run_tests",
|
||||
"when": "$lint_ok",
|
||||
"inputs": {
|
||||
"path": "tests"
|
||||
},
|
||||
"outputs": {
|
||||
"results": "unit_results"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "unit_failed",
|
||||
"type": "utils.branch_condition",
|
||||
"when": "$lint_ok",
|
||||
"inputs": {
|
||||
"value": "$unit_results",
|
||||
"mode": "regex",
|
||||
"compare": "(FAILED|ERROR)"
|
||||
},
|
||||
"outputs": {
|
||||
"result": "unit_failed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "unit_ok",
|
||||
"type": "utils.not",
|
||||
"when": "$lint_ok",
|
||||
"inputs": {
|
||||
"value": "$unit_failed"
|
||||
},
|
||||
"outputs": {
|
||||
"result": "unit_ok"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui_tests",
|
||||
"type": "tools.run_tests",
|
||||
"when": "$unit_ok",
|
||||
"inputs": {
|
||||
"path": "tests/ui"
|
||||
},
|
||||
"outputs": {
|
||||
"results": "ui_results"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
73
backend/tests/test_n8n_schema.py
Normal file
73
backend/tests/test_n8n_schema.py
Normal file
@@ -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)
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user