diff --git a/backend/autometabuilder/workflow/plugins/README.md b/backend/autometabuilder/workflow/plugins/README.md new file mode 100644 index 0000000..97acfc2 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/README.md @@ -0,0 +1,822 @@ +# Workflow Plugins Documentation + +This document describes all available workflow plugins for building declarative n8n-style workflows. + +## Categories + +- [Core Plugins](#core-plugins) - AI and context management +- [Tool Plugins](#tool-plugins) - File system and SDLC operations +- [Logic Plugins](#logic-plugins) - Boolean logic and comparisons +- [List Plugins](#list-plugins) - Collection operations +- [Dictionary Plugins](#dictionary-plugins) - Object/map operations +- [String Plugins](#string-plugins) - Text manipulation +- [Math Plugins](#math-plugins) - Arithmetic operations +- [Conversion Plugins](#conversion-plugins) - Type conversions +- [Control Flow Plugins](#control-flow-plugins) - Branching and switching +- [Variable Plugins](#variable-plugins) - State management +- [Backend Plugins](#backend-plugins) - System initialization +- [Utility Plugins](#utility-plugins) - General utilities + +--- + +## Core Plugins + +### `core.load_context` +Load SDLC context (roadmap, issues, PRs) from GitHub. + +**Outputs:** +- `context` - String containing SDLC context + +### `core.seed_messages` +Initialize empty message array for AI conversation. + +**Outputs:** +- `messages` - Empty array + +### `core.append_context_message` +Add context to messages array. + +**Inputs:** +- `messages` - Message array +- `context` - Context text + +**Outputs:** +- `messages` - Updated array + +### `core.append_user_instruction` +Add user instruction to messages. + +**Inputs:** +- `messages` - Message array + +**Outputs:** +- `messages` - Updated array + +### `core.ai_request` +Make AI request with messages. + +**Inputs:** +- `messages` - Message array + +**Outputs:** +- `response` - AI response message +- `has_tool_calls` - Boolean +- `tool_calls_count` - Number + +### `core.run_tool_calls` +Execute tool calls from AI response. + +**Inputs:** +- `response` - AI response message + +**Outputs:** +- `tool_results` - Array of results + +### `core.append_tool_results` +Add tool results to messages. + +**Inputs:** +- `messages` - Message array +- `tool_results` - Tool results array + +**Outputs:** +- `messages` - Updated array + +--- + +## Tool Plugins + +### `tools.list_files` +List files in directory. + +**Inputs:** +- `path` - Directory path + +**Outputs:** +- `files` - Array of file paths + +### `tools.read_file` +Read file contents. + +**Inputs:** +- `path` - File path + +**Outputs:** +- `content` - File content + +### `tools.run_tests` +Execute test suite. + +**Outputs:** +- `success` - Boolean +- `output` - Test output + +### `tools.run_lint` +Run linter. + +**Outputs:** +- `success` - Boolean +- `output` - Lint output + +### `tools.create_branch` +Create Git branch. + +**Inputs:** +- `branch_name` - Branch name + +**Outputs:** +- `success` - Boolean + +### `tools.create_pull_request` +Create GitHub pull request. + +**Inputs:** +- `title` - PR title +- `body` - PR description + +**Outputs:** +- `pr_number` - PR number + +--- + +## Logic Plugins + +### `logic.and` +Logical AND operation. + +**Inputs:** +- `values` - Array of boolean values + +**Outputs:** +- `result` - Boolean (all values are true) + +### `logic.or` +Logical OR operation. + +**Inputs:** +- `values` - Array of boolean values + +**Outputs:** +- `result` - Boolean (any value is true) + +### `logic.xor` +Logical XOR operation. + +**Inputs:** +- `a` - First boolean +- `b` - Second boolean + +**Outputs:** +- `result` - Boolean (exactly one is true) + +### `logic.equals` +Equality comparison. + +**Inputs:** +- `a` - First value +- `b` - Second value + +**Outputs:** +- `result` - Boolean (a == b) + +### `logic.gt` +Greater than comparison. + +**Inputs:** +- `a` - First value +- `b` - Second value + +**Outputs:** +- `result` - Boolean (a > b) + +### `logic.lt` +Less than comparison. + +**Inputs:** +- `a` - First value +- `b` - Second value + +**Outputs:** +- `result` - Boolean (a < b) + +### `logic.gte` +Greater than or equal comparison. + +**Inputs:** +- `a` - First value +- `b` - Second value + +**Outputs:** +- `result` - Boolean (a >= b) + +### `logic.lte` +Less than or equal comparison. + +**Inputs:** +- `a` - First value +- `b` - Second value + +**Outputs:** +- `result` - Boolean (a <= b) + +### `logic.in` +Membership test. + +**Inputs:** +- `value` - Value to find +- `collection` - Array or string + +**Outputs:** +- `result` - Boolean (value in collection) + +--- + +## List Plugins + +### `list.find` +Find first item matching condition. + +**Inputs:** +- `items` - Array of objects +- `key` - Property name +- `value` - Value to match + +**Outputs:** +- `result` - Found item or null +- `found` - Boolean + +### `list.some` +Check if any item matches. + +**Inputs:** +- `items` - Array +- `key` - Optional property name +- `value` - Optional value to match + +**Outputs:** +- `result` - Boolean + +### `list.every` +Check if all items match. + +**Inputs:** +- `items` - Array +- `key` - Optional property name +- `value` - Optional value to match + +**Outputs:** +- `result` - Boolean + +### `list.concat` +Concatenate multiple lists. + +**Inputs:** +- `lists` - Array of arrays + +**Outputs:** +- `result` - Concatenated array + +### `list.slice` +Extract slice from list. + +**Inputs:** +- `items` - Array +- `start` - Start index (default: 0) +- `end` - End index (optional) + +**Outputs:** +- `result` - Sliced array + +### `list.sort` +Sort list. + +**Inputs:** +- `items` - Array +- `key` - Optional sort key +- `reverse` - Boolean (default: false) + +**Outputs:** +- `result` - Sorted array + +### `list.length` +Get list length. + +**Inputs:** +- `items` - Array + +**Outputs:** +- `result` - Number (length) + +--- + +## Dictionary Plugins + +### `dict.get` +Get value from dictionary. + +**Inputs:** +- `object` - Dictionary +- `key` - Key name +- `default` - Default value (optional) + +**Outputs:** +- `result` - Value +- `found` - Boolean + +### `dict.set` +Set value in dictionary. + +**Inputs:** +- `object` - Dictionary +- `key` - Key name +- `value` - Value to set + +**Outputs:** +- `result` - Updated dictionary + +### `dict.merge` +Merge multiple dictionaries. + +**Inputs:** +- `objects` - Array of dictionaries + +**Outputs:** +- `result` - Merged dictionary + +### `dict.keys` +Get dictionary keys. + +**Inputs:** +- `object` - Dictionary + +**Outputs:** +- `result` - Array of keys + +### `dict.values` +Get dictionary values. + +**Inputs:** +- `object` - Dictionary + +**Outputs:** +- `result` - Array of values + +### `dict.items` +Get dictionary items as [key, value] pairs. + +**Inputs:** +- `object` - Dictionary + +**Outputs:** +- `result` - Array of [key, value] arrays + +--- + +## String Plugins + +### `string.concat` +Concatenate strings. + +**Inputs:** +- `strings` - Array of strings +- `separator` - Separator string (default: "") + +**Outputs:** +- `result` - Concatenated string + +### `string.split` +Split string. + +**Inputs:** +- `text` - Input string +- `separator` - Split separator (default: " ") +- `max_splits` - Max splits (optional) + +**Outputs:** +- `result` - Array of strings + +### `string.replace` +Replace occurrences in string. + +**Inputs:** +- `text` - Input string +- `old` - String to replace +- `new` - Replacement string +- `count` - Max replacements (default: -1 for all) + +**Outputs:** +- `result` - Modified string + +### `string.trim` +Trim whitespace. + +**Inputs:** +- `text` - Input string +- `mode` - "both", "start", or "end" (default: "both") + +**Outputs:** +- `result` - Trimmed string + +### `string.upper` +Convert to uppercase. + +**Inputs:** +- `text` - Input string + +**Outputs:** +- `result` - Uppercase string + +### `string.lower` +Convert to lowercase. + +**Inputs:** +- `text` - Input string + +**Outputs:** +- `result` - Lowercase string + +### `string.format` +Format string with variables. + +**Inputs:** +- `template` - Template string with {placeholders} +- `variables` - Dictionary of variables + +**Outputs:** +- `result` - Formatted string + +### `string.length` +Get string length. + +**Inputs:** +- `text` - Input string + +**Outputs:** +- `result` - Number (length) + +--- + +## Math Plugins + +### `math.add` +Add numbers. + +**Inputs:** +- `numbers` - Array of numbers + +**Outputs:** +- `result` - Sum + +### `math.subtract` +Subtract numbers. + +**Inputs:** +- `a` - Minuend +- `b` - Subtrahend + +**Outputs:** +- `result` - Difference (a - b) + +### `math.multiply` +Multiply numbers. + +**Inputs:** +- `numbers` - Array of numbers + +**Outputs:** +- `result` - Product + +### `math.divide` +Divide numbers. + +**Inputs:** +- `a` - Dividend +- `b` - Divisor + +**Outputs:** +- `result` - Quotient (a / b) + +### `math.modulo` +Modulo operation. + +**Inputs:** +- `a` - Dividend +- `b` - Divisor + +**Outputs:** +- `result` - Remainder (a % b) + +### `math.power` +Power operation. + +**Inputs:** +- `a` - Base +- `b` - Exponent + +**Outputs:** +- `result` - a^b + +### `math.min` +Find minimum value. + +**Inputs:** +- `numbers` - Array of numbers + +**Outputs:** +- `result` - Minimum value + +### `math.max` +Find maximum value. + +**Inputs:** +- `numbers` - Array of numbers + +**Outputs:** +- `result` - Maximum value + +### `math.abs` +Absolute value. + +**Inputs:** +- `value` - Number + +**Outputs:** +- `result` - |value| + +### `math.round` +Round number. + +**Inputs:** +- `value` - Number +- `precision` - Decimal places (default: 0) + +**Outputs:** +- `result` - Rounded number + +--- + +## Conversion Plugins + +### `convert.to_string` +Convert to string. + +**Inputs:** +- `value` - Any value + +**Outputs:** +- `result` - String + +### `convert.to_number` +Convert to number. + +**Inputs:** +- `value` - String or number +- `default` - Default value (default: 0) + +**Outputs:** +- `result` - Number + +### `convert.to_boolean` +Convert to boolean. + +**Inputs:** +- `value` - Any value + +**Outputs:** +- `result` - Boolean + +### `convert.to_list` +Convert to list. + +**Inputs:** +- `value` - Any value + +**Outputs:** +- `result` - Array + +### `convert.to_dict` +Convert to dictionary. + +**Inputs:** +- `value` - List of [key, value] pairs or dict + +**Outputs:** +- `result` - Dictionary + +### `convert.parse_json` +Parse JSON string. + +**Inputs:** +- `text` - JSON string + +**Outputs:** +- `result` - Parsed object + +### `convert.to_json` +Convert to JSON string. + +**Inputs:** +- `value` - Any value +- `indent` - Indentation (optional) + +**Outputs:** +- `result` - JSON string + +--- + +## Control Flow Plugins + +### `control.switch` +Switch/case statement. + +**Inputs:** +- `value` - Value to match +- `cases` - Dictionary of case values +- `default` - Default value (optional) + +**Outputs:** +- `result` - Matched case value +- `matched` - Boolean + +### `utils.branch_condition` +Branch based on condition. + +**Inputs:** +- `condition` - Boolean + +**Outputs:** +- Routes to output 0 (true) or 1 (false) + +--- + +## Variable Plugins + +### `var.get` +Get variable from workflow store. + +**Inputs:** +- `key` - Variable name +- `default` - Default value (optional) + +**Outputs:** +- `result` - Variable value +- `exists` - Boolean + +### `var.set` +Set variable in workflow store. + +**Inputs:** +- `key` - Variable name +- `value` - Value to set + +**Outputs:** +- `result` - Set value +- `key` - Variable name + +### `var.delete` +Delete variable from workflow store. + +**Inputs:** +- `key` - Variable name + +**Outputs:** +- `result` - Boolean (success) +- `deleted` - Boolean + +### `var.exists` +Check if variable exists. + +**Inputs:** +- `key` - Variable name + +**Outputs:** +- `result` - Boolean + +--- + +## Backend Plugins + +### `backend.create_github` +Initialize GitHub client. + +**Outputs:** +- `result` - GitHub client +- `initialized` - Boolean + +### `backend.create_openai` +Initialize OpenAI client. + +**Outputs:** +- `result` - OpenAI client +- `initialized` - Boolean + +### `backend.load_metadata` +Load metadata.json. + +**Outputs:** +- `result` - Metadata dictionary + +### `backend.load_messages` +Load translation messages. + +**Outputs:** +- `result` - Messages dictionary + +### `backend.load_tools` +Load tool definitions. + +**Outputs:** +- `result` - Tools array + +### `backend.load_prompt` +Load prompt.yml. + +**Outputs:** +- `result` - Prompt dictionary + +### `backend.build_tool_map` +Build tool registry map. + +**Outputs:** +- `result` - Tool map dictionary + +### `backend.load_plugins` +Load and register plugins. + +**Outputs:** +- `result` - Boolean (success) + +--- + +## Utility Plugins + +### `utils.filter_list` +Filter list by condition. + +**Inputs:** +- `items` - Array +- `mode` - Filter mode +- `pattern` - Pattern/condition + +**Outputs:** +- `result` - Filtered array + +### `utils.map_list` +Map/transform list items. + +**Inputs:** +- `items` - Array +- `transform` - Transformation + +**Outputs:** +- `result` - Transformed array + +### `utils.reduce_list` +Reduce list to single value. + +**Inputs:** +- `items` - Array +- `separator` - Join separator + +**Outputs:** +- `result` - Reduced value + +### `utils.not` +Logical NOT operation. + +**Inputs:** +- `value` - Boolean value + +**Outputs:** +- `result` - Negated boolean + +--- + +## Variable Binding + +All plugins support variable binding using `$variable_name` syntax in inputs. Variables are stored in the workflow runtime store and can be accessed across nodes. + +Example: +```json +{ + "parameters": { + "text": "$user_input", + "template": "Hello {name}!", + "variables": { + "name": "$user_name" + } + } +} +``` + +## Error Handling + +Plugins may return an `error` field in their output when an error occurs. Check for this field to handle errors gracefully in your workflow. + +Example: +```json +{ + "result": null, + "error": "Division by zero" +} +``` diff --git a/backend/tests/test_workflow_plugins.py b/backend/tests/test_workflow_plugins.py new file mode 100644 index 0000000..d4ab99e --- /dev/null +++ b/backend/tests/test_workflow_plugins.py @@ -0,0 +1,319 @@ +"""Test new workflow plugins for software development primitives.""" +from autometabuilder.workflow.plugin_registry import PluginRegistry, load_plugin_map +from autometabuilder.workflow.runtime import WorkflowRuntime +import logging + + +class MockLogger: + """Mock logger for testing.""" + def info(self, *args, **kwargs): + pass + + def debug(self, *args, **kwargs): + pass + + def error(self, *args, **kwargs): + pass + + +def create_test_runtime(): + """Create a test runtime with empty context.""" + logger = MockLogger() + return WorkflowRuntime(context={}, store={}, tool_runner=None, logger=logger) + + +def test_plugin_map_loads_all_new_plugins(): + """Test that plugin map includes all new plugins.""" + plugin_map = load_plugin_map() + + # Test logic plugins + assert "logic.and" in plugin_map + assert "logic.or" in plugin_map + assert "logic.xor" in plugin_map + assert "logic.equals" in plugin_map + assert "logic.gt" in plugin_map + assert "logic.lt" in plugin_map + + # Test list plugins + assert "list.find" in plugin_map + assert "list.some" in plugin_map + assert "list.every" in plugin_map + assert "list.concat" in plugin_map + assert "list.slice" in plugin_map + assert "list.sort" in plugin_map + assert "list.length" in plugin_map + + # Test dict plugins + assert "dict.get" in plugin_map + assert "dict.set" in plugin_map + assert "dict.merge" in plugin_map + + # Test string plugins + assert "string.concat" in plugin_map + assert "string.split" in plugin_map + assert "string.upper" in plugin_map + assert "string.lower" in plugin_map + + # Test math plugins + assert "math.add" in plugin_map + assert "math.subtract" in plugin_map + assert "math.multiply" in plugin_map + assert "math.divide" in plugin_map + + # Test conversion plugins + assert "convert.to_string" in plugin_map + assert "convert.to_number" in plugin_map + assert "convert.parse_json" in plugin_map + assert "convert.to_json" in plugin_map + + # Test control flow plugins + assert "control.switch" in plugin_map + + # Test variable plugins + assert "var.get" in plugin_map + assert "var.set" in plugin_map + + # Test backend plugins + assert "backend.load_metadata" in plugin_map + assert "backend.load_messages" in plugin_map + + +def test_logic_and_plugin(): + """Test logic.and plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("logic.and") + assert plugin is not None + + result = plugin(runtime, {"values": [True, True, True]}) + assert result["result"] is True + + result = plugin(runtime, {"values": [True, False, True]}) + assert result["result"] is False + + +def test_logic_or_plugin(): + """Test logic.or plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("logic.or") + assert plugin is not None + + result = plugin(runtime, {"values": [False, False, True]}) + assert result["result"] is True + + result = plugin(runtime, {"values": [False, False, False]}) + assert result["result"] is False + + +def test_logic_equals_plugin(): + """Test logic.equals plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("logic.equals") + assert plugin is not None + + result = plugin(runtime, {"a": 5, "b": 5}) + assert result["result"] is True + + result = plugin(runtime, {"a": 5, "b": 10}) + assert result["result"] is False + + +def test_math_add_plugin(): + """Test math.add plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("math.add") + assert plugin is not None + + result = plugin(runtime, {"numbers": [1, 2, 3, 4, 5]}) + assert result["result"] == 15 + + +def test_math_multiply_plugin(): + """Test math.multiply plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("math.multiply") + assert plugin is not None + + result = plugin(runtime, {"numbers": [2, 3, 4]}) + assert result["result"] == 24 + + +def test_string_concat_plugin(): + """Test string.concat plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("string.concat") + assert plugin is not None + + result = plugin(runtime, {"strings": ["Hello", "World"], "separator": " "}) + assert result["result"] == "Hello World" + + +def test_string_upper_plugin(): + """Test string.upper plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("string.upper") + assert plugin is not None + + result = plugin(runtime, {"text": "hello"}) + assert result["result"] == "HELLO" + + +def test_list_length_plugin(): + """Test list.length plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("list.length") + assert plugin is not None + + result = plugin(runtime, {"items": [1, 2, 3, 4, 5]}) + assert result["result"] == 5 + + +def test_list_concat_plugin(): + """Test list.concat plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("list.concat") + assert plugin is not None + + result = plugin(runtime, {"lists": [[1, 2], [3, 4], [5, 6]]}) + assert result["result"] == [1, 2, 3, 4, 5, 6] + + +def test_dict_get_plugin(): + """Test dict.get plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("dict.get") + assert plugin is not None + + result = plugin(runtime, {"object": {"name": "John", "age": 30}, "key": "name"}) + assert result["result"] == "John" + assert result["found"] is True + + result = plugin(runtime, {"object": {"name": "John"}, "key": "missing", "default": "N/A"}) + assert result["result"] == "N/A" + assert result["found"] is False + + +def test_dict_set_plugin(): + """Test dict.set plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("dict.set") + assert plugin is not None + + result = plugin(runtime, {"object": {"a": 1}, "key": "b", "value": 2}) + assert result["result"] == {"a": 1, "b": 2} + + +def test_var_get_set_plugin(): + """Test var.get and var.set plugins.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + set_plugin = registry.get("var.set") + get_plugin = registry.get("var.get") + + assert set_plugin is not None + assert get_plugin is not None + + # Set a variable + set_result = set_plugin(runtime, {"key": "test_var", "value": 42}) + assert set_result["result"] == 42 + + # Get the variable + get_result = get_plugin(runtime, {"key": "test_var"}) + assert get_result["result"] == 42 + assert get_result["exists"] is True + + +def test_convert_to_json_and_parse(): + """Test JSON conversion plugins.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + to_json = registry.get("convert.to_json") + parse_json = registry.get("convert.parse_json") + + assert to_json is not None + assert parse_json is not None + + # Convert to JSON + data = {"name": "Test", "value": 123} + json_result = to_json(runtime, {"value": data}) + json_str = json_result["result"] + + # Parse JSON back + parse_result = parse_json(runtime, {"text": json_str}) + assert parse_result["result"] == data + + +def test_convert_to_number(): + """Test number conversion.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("convert.to_number") + assert plugin is not None + + result = plugin(runtime, {"value": "42"}) + assert result["result"] == 42 + + result = plugin(runtime, {"value": "3.14"}) + assert result["result"] == 3.14 + + +def test_control_switch(): + """Test control.switch plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("control.switch") + assert plugin is not None + + cases = { + "option1": "Result 1", + "option2": "Result 2", + "option3": "Result 3" + } + + result = plugin(runtime, {"value": "option2", "cases": cases}) + assert result["result"] == "Result 2" + assert result["matched"] is True + + result = plugin(runtime, {"value": "unknown", "cases": cases, "default": "Default"}) + assert result["result"] == "Default" + assert result["matched"] is False