diff --git a/.pylint_cache_run/src_1.stats b/.pylint_cache_run/src_1.stats index 9255e58..c211c30 100644 Binary files a/.pylint_cache_run/src_1.stats and b/.pylint_cache_run/src_1.stats differ diff --git a/src/autometabuilder/app_runner.py b/src/autometabuilder/app_runner.py index 45d4725..ca2abcc 100644 --- a/src/autometabuilder/app_runner.py +++ b/src/autometabuilder/app_runner.py @@ -21,6 +21,7 @@ from .workflow_engine_builder import build_workflow_engine def run_app() -> None: + """Run the AutoMetabuilder CLI.""" load_env() configure_logging() logger = logging.getLogger("autometabuilder") @@ -43,23 +44,21 @@ def run_app() -> None: metadata = load_metadata() tools = load_tools(metadata) - registry_entries = load_tool_registry() - tool_map = build_tool_map(gh, registry_entries) + tool_map = build_tool_map(gh, load_tool_registry()) load_plugins(tool_map, tools) - tool_policies = load_tool_policies() - workflow_config = load_workflow_config(metadata) - workflow_context = build_workflow_context( - args, - gh, - msgs, - client, - tools, - tool_map, - prompt, - tool_policies - ) + context_parts = { + "args": args, + "gh": gh, + "msgs": msgs, + "client": client, + "tools": tools, + "tool_map": tool_map, + "prompt": prompt, + "tool_policies": load_tool_policies() + } + workflow_context = build_workflow_context(context_parts) logger.debug("Workflow context ready with %s tools", len(tool_map)) - engine = build_workflow_engine(workflow_config, workflow_context, logger) + engine = build_workflow_engine(load_workflow_config(metadata), workflow_context, logger) engine.execute() diff --git a/src/autometabuilder/callable_loader.py b/src/autometabuilder/callable_loader.py index 683a960..757f244 100644 --- a/src/autometabuilder/callable_loader.py +++ b/src/autometabuilder/callable_loader.py @@ -3,6 +3,7 @@ import importlib def load_callable(path: str): + """Import and return a callable.""" module_path, attr = path.rsplit(".", 1) module = importlib.import_module(module_path) return getattr(module, attr) diff --git a/src/autometabuilder/cli_args.py b/src/autometabuilder/cli_args.py index ed8acce..88e0a42 100644 --- a/src/autometabuilder/cli_args.py +++ b/src/autometabuilder/cli_args.py @@ -3,9 +3,22 @@ import argparse def parse_args(): + """Parse CLI arguments.""" parser = argparse.ArgumentParser(description="AutoMetabuilder: AI-driven SDLC assistant.") - parser.add_argument("--dry-run", action="store_true", help="Do not execute state-modifying tools.") - parser.add_argument("--yolo", action="store_true", help="Execute tools without confirmation.") - parser.add_argument("--once", action="store_true", help="Run a single full iteration (AI -> Tool -> AI).") + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not execute state-modifying tools." + ) + parser.add_argument( + "--yolo", + action="store_true", + help="Execute tools without confirmation." + ) + parser.add_argument( + "--once", + action="store_true", + help="Run a single full iteration (AI -> Tool -> AI)." + ) parser.add_argument("--web", action="store_true", help="Start the Web UI.") return parser.parse_args() diff --git a/src/autometabuilder/context_loader.py b/src/autometabuilder/context_loader.py index e5d3fa5..60a2404 100644 --- a/src/autometabuilder/context_loader.py +++ b/src/autometabuilder/context_loader.py @@ -7,6 +7,7 @@ logger = logging.getLogger("autometabuilder") def get_sdlc_context(gh: GitHubIntegration, msgs: dict) -> str: + """Return SDLC context text from roadmap, issues, and PRs.""" sdlc_context = "" if os.path.exists("ROADMAP.md"): with open("ROADMAP.md", "r", encoding="utf-8") as f: diff --git a/src/autometabuilder/env_loader.py b/src/autometabuilder/env_loader.py index a3cc36b..1427323 100644 --- a/src/autometabuilder/env_loader.py +++ b/src/autometabuilder/env_loader.py @@ -3,4 +3,5 @@ from dotenv import load_dotenv def load_env() -> None: + """Load environment variables.""" load_dotenv() diff --git a/src/autometabuilder/github_service.py b/src/autometabuilder/github_service.py index 5595a6e..e3e5687 100644 --- a/src/autometabuilder/github_service.py +++ b/src/autometabuilder/github_service.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def create_github_integration(token: str, msgs: dict): + """Create GitHub integration if possible.""" if not token: return None try: diff --git a/src/autometabuilder/metadata.json b/src/autometabuilder/metadata.json index a54a0b3..caf1c6b 100644 --- a/src/autometabuilder/metadata.json +++ b/src/autometabuilder/metadata.json @@ -40,10 +40,18 @@ "description": "meta.settings.log_level.description", "type": "select", "options": [ + "TRACE", "DEBUG", "INFO", "WARNING", "ERROR" + ], + "option_labels": [ + "meta.settings.log_level.options.trace", + "meta.settings.log_level.options.debug", + "meta.settings.log_level.options.info", + "meta.settings.log_level.options.warning", + "meta.settings.log_level.options.error" ] }, "APP_LANG": { @@ -56,6 +64,13 @@ "fr", "nl", "pirate" + ], + "option_labels": [ + "meta.settings.app_lang.options.en", + "meta.settings.app_lang.options.es", + "meta.settings.app_lang.options.fr", + "meta.settings.app_lang.options.nl", + "meta.settings.app_lang.options.pirate" ] }, "PROMPT_PATH": { @@ -99,6 +114,7 @@ "WEB_PASSWORD" ], "env_values": [ + "TRACE", "INFO", "DEBUG", "WARNING", diff --git a/src/autometabuilder/metadata_loader.py b/src/autometabuilder/metadata_loader.py index 7ad8056..4766f35 100644 --- a/src/autometabuilder/metadata_loader.py +++ b/src/autometabuilder/metadata_loader.py @@ -4,6 +4,7 @@ import os def load_metadata() -> dict: + """Load metadata.json.""" metadata_path = os.path.join(os.path.dirname(__file__), "metadata.json") with open(metadata_path, "r", encoding="utf-8") as f: return json.load(f) diff --git a/src/autometabuilder/model_resolver.py b/src/autometabuilder/model_resolver.py index 0d10cf6..6839bc1 100644 --- a/src/autometabuilder/model_resolver.py +++ b/src/autometabuilder/model_resolver.py @@ -5,4 +5,5 @@ DEFAULT_MODEL = "openai/gpt-4o" def resolve_model_name(prompt: dict) -> str: + """Resolve model name from env or prompt.""" return os.environ.get("LLM_MODEL", prompt.get("model", DEFAULT_MODEL)) diff --git a/src/autometabuilder/openai_client.py b/src/autometabuilder/openai_client.py index 3a6037f..4a6c3ed 100644 --- a/src/autometabuilder/openai_client.py +++ b/src/autometabuilder/openai_client.py @@ -4,6 +4,7 @@ from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) def get_completion(client, model, messages, tools): + """Request a chat completion with retries.""" return client.chat.completions.create( model=model, messages=messages, diff --git a/src/autometabuilder/openai_factory.py b/src/autometabuilder/openai_factory.py index c0c9e22..22160ed 100644 --- a/src/autometabuilder/openai_factory.py +++ b/src/autometabuilder/openai_factory.py @@ -6,6 +6,7 @@ DEFAULT_ENDPOINT = "https://models.github.ai/inference" def create_openai_client(token: str): + """Create an OpenAI client.""" return OpenAI( base_url=os.environ.get("GITHUB_MODELS_ENDPOINT", DEFAULT_ENDPOINT), api_key=token, diff --git a/src/autometabuilder/plugin_loader.py b/src/autometabuilder/plugin_loader.py index 40cb95a..1b924d6 100644 --- a/src/autometabuilder/plugin_loader.py +++ b/src/autometabuilder/plugin_loader.py @@ -8,6 +8,7 @@ logger = logging.getLogger("autometabuilder") def load_plugins(tool_map: dict, tools: list) -> None: + """Load plugin tools and append metadata.""" plugins_dir = os.path.join(os.path.dirname(__file__), "plugins") if not os.path.exists(plugins_dir): return diff --git a/src/autometabuilder/tool_map_builder.py b/src/autometabuilder/tool_map_builder.py index 2418862..4247fb5 100644 --- a/src/autometabuilder/tool_map_builder.py +++ b/src/autometabuilder/tool_map_builder.py @@ -3,6 +3,7 @@ from .callable_loader import load_callable def build_tool_map(gh, registry_entries: list) -> dict: + """Build tool name to callable map.""" tool_map = {} for entry in registry_entries: name = entry.get("name") diff --git a/src/autometabuilder/tool_policy_loader.py b/src/autometabuilder/tool_policy_loader.py index 6963167..0204348 100644 --- a/src/autometabuilder/tool_policy_loader.py +++ b/src/autometabuilder/tool_policy_loader.py @@ -4,6 +4,7 @@ import os def load_tool_policies() -> dict: + """Load tool policies JSON.""" path = os.path.join(os.path.dirname(__file__), "tool_policies.json") if not os.path.exists(path): return {"modifying_tools": []} diff --git a/src/autometabuilder/tool_registry_loader.py b/src/autometabuilder/tool_registry_loader.py index cd1a356..f9481ea 100644 --- a/src/autometabuilder/tool_registry_loader.py +++ b/src/autometabuilder/tool_registry_loader.py @@ -4,6 +4,7 @@ import os def load_tool_registry() -> list: + """Load tool registry entries.""" path = os.path.join(os.path.dirname(__file__), "tool_registry.json") if not os.path.exists(path): return [] diff --git a/src/autometabuilder/tools/edit_file.py b/src/autometabuilder/tools/edit_file.py index be26088..16bd5dd 100644 --- a/src/autometabuilder/tools/edit_file.py +++ b/src/autometabuilder/tools/edit_file.py @@ -1,6 +1,7 @@ """Edit file content with search/replace.""" def edit_file(path: str, search: str, replace: str) -> str: + """Replace text in a file.""" try: with open(path, "r", encoding="utf-8") as f: content = f.read() diff --git a/src/autometabuilder/tools/list_files.py b/src/autometabuilder/tools/list_files.py index 80b4a59..84caaea 100644 --- a/src/autometabuilder/tools/list_files.py +++ b/src/autometabuilder/tools/list_files.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def list_files(directory: str = ".") -> str: + """Return newline-separated files under a directory.""" files_list = [] for root, _, files in os.walk(directory): if ".git" in root or "__pycache__" in root or ".venv" in root: diff --git a/src/autometabuilder/tools/read_file.py b/src/autometabuilder/tools/read_file.py index ae6f117..145d58e 100644 --- a/src/autometabuilder/tools/read_file.py +++ b/src/autometabuilder/tools/read_file.py @@ -1,6 +1,7 @@ """Read file content.""" def read_file(path: str) -> str: + """Read file content.""" try: with open(path, "r", encoding="utf-8") as f: return f.read() diff --git a/src/autometabuilder/tools/run_docker_task.py b/src/autometabuilder/tools/run_docker_task.py index 8123b28..82092bc 100644 --- a/src/autometabuilder/tools/run_docker_task.py +++ b/src/autometabuilder/tools/run_docker_task.py @@ -4,5 +4,6 @@ from ..docker_utils import run_command_in_docker def run_docker_task(image: str, command: str, workdir: str = "/workspace") -> str: + """Run a command inside Docker.""" volumes = {os.getcwd(): "/workspace"} return run_command_in_docker(image, command, volumes=volumes, workdir=workdir) diff --git a/src/autometabuilder/tools/run_lint.py b/src/autometabuilder/tools/run_lint.py index 7987a9b..f9885c2 100644 --- a/src/autometabuilder/tools/run_lint.py +++ b/src/autometabuilder/tools/run_lint.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def run_lint(path: str = "src") -> str: + """Run pylint on a path.""" logger.info("Running linting in %s...", path) result = subprocess.run(["pylint", path], capture_output=True, text=True, check=False) logger.info(result.stdout) diff --git a/src/autometabuilder/tools/run_tests.py b/src/autometabuilder/tools/run_tests.py index 8788fd8..b56631c 100644 --- a/src/autometabuilder/tools/run_tests.py +++ b/src/autometabuilder/tools/run_tests.py @@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder") def run_tests(path: str = "tests") -> str: + """Run pytest on a path.""" logger.info("Running tests in %s...", path) result = subprocess.run(["pytest", path], capture_output=True, text=True, check=False) logger.info(result.stdout) diff --git a/src/autometabuilder/tools/write_file.py b/src/autometabuilder/tools/write_file.py index 780d40f..8f0bc94 100644 --- a/src/autometabuilder/tools/write_file.py +++ b/src/autometabuilder/tools/write_file.py @@ -1,6 +1,7 @@ """Write file content.""" def write_file(path: str, content: str) -> str: + """Write content to a file.""" try: with open(path, "w", encoding="utf-8") as f: f.write(content) diff --git a/src/autometabuilder/tools_loader.py b/src/autometabuilder/tools_loader.py index 6ccee3a..333fc61 100644 --- a/src/autometabuilder/tools_loader.py +++ b/src/autometabuilder/tools_loader.py @@ -4,6 +4,7 @@ import os def load_tools(metadata: dict) -> list: + """Load tool specs from metadata reference.""" tools_path = os.path.join(os.path.dirname(__file__), metadata.get("tools_path", "tools.json")) with open(tools_path, "r", encoding="utf-8") as f: return json.load(f) diff --git a/src/autometabuilder/web/static/js/plugins/workflow_palette.js b/src/autometabuilder/web/static/js/plugins/workflow_palette.js new file mode 100644 index 0000000..fbf8bf6 --- /dev/null +++ b/src/autometabuilder/web/static/js/plugins/workflow_palette.js @@ -0,0 +1,10 @@ +/** + * AutoMetabuilder - Workflow Palette Plugin + */ +(() => { + const init = async () => { + window.AMBWorkflowPalette?.init?.(); + }; + + window.AMBPlugins?.register('workflow_palette', init); +})(); diff --git a/src/autometabuilder/web/static/js/workflow/workflow_mutations.js b/src/autometabuilder/web/static/js/workflow/workflow_mutations.js index bc5ff7f..c129768 100644 --- a/src/autometabuilder/web/static/js/workflow/workflow_mutations.js +++ b/src/autometabuilder/web/static/js/workflow/workflow_mutations.js @@ -36,14 +36,15 @@ return candidate; }; - const addNode = (targetArray, pluginDefinitions) => { + const addNode = (targetArray, pluginDefinitions, typeOverride = '') => { const types = Object.keys(pluginDefinitions); const defaultType = types[0] || ''; + const nextType = pluginDefinitions[typeOverride] ? typeOverride : defaultType; const node = { - id: generateNodeId(defaultType, targetArray), - type: defaultType, - inputs: buildDefaultFields(pluginDefinitions[defaultType]?.inputs), - outputs: buildDefaultFields(pluginDefinitions[defaultType]?.outputs) + id: generateNodeId(nextType, targetArray), + type: nextType, + inputs: buildDefaultFields(pluginDefinitions[nextType]?.inputs), + outputs: buildDefaultFields(pluginDefinitions[nextType]?.outputs) }; targetArray.push(node); }; diff --git a/src/autometabuilder/web/static/js/workflow/workflow_palette.js b/src/autometabuilder/web/static/js/workflow/workflow_palette.js new file mode 100644 index 0000000..2f16207 --- /dev/null +++ b/src/autometabuilder/web/static/js/workflow/workflow_palette.js @@ -0,0 +1,99 @@ +/** + * AutoMetabuilder - Workflow Palette + */ +(() => { + const state = { + container: null, + searchInput: null, + list: null, + filter: '' + }; + + const groupOrder = ['core', 'tools', 'utils', 'control', 'other']; + const groupLabelKeys = { + core: 'ui.workflow.palette.group.core', + tools: 'ui.workflow.palette.group.tools', + utils: 'ui.workflow.palette.group.utils', + control: 'ui.workflow.palette.group.control', + other: 'ui.workflow.palette.group.other' + }; + + const getGroupKey = (type) => (type || '').split('.')[0] || 'other'; + + const buildEntries = (definitions, t) => Object.entries(definitions || {}).map(([key, def]) => { + const label = t?.(def.label || '', def.label || key) || key; + return { + key, + label, + group: getGroupKey(key), + search: `${key} ${label}`.toLowerCase() + }; + }); + + const render = () => { + const { t, escapeHtml } = window.AMBWorkflowUtils || {}; + const definitions = window.AMBWorkflowState?.state?.pluginDefinitions || {}; + if (!state.list) return; + const term = state.filter.toLowerCase(); + const entries = buildEntries(definitions, t).filter(entry => !term || entry.search.includes(term)); + if (!entries.length) { + state.list.innerHTML = `
${escapeHtml ? escapeHtml(t?.('ui.workflow.palette.empty', 'No matching nodes.')) : t?.('ui.workflow.palette.empty', 'No matching nodes.')}
`; + return; + } + + const grouped = entries.reduce((acc, entry) => { + const group = groupOrder.includes(entry.group) ? entry.group : 'other'; + acc[group] = acc[group] || []; + acc[group].push(entry); + return acc; + }, {}); + + const groupsHtml = groupOrder.filter(group => grouped[group]?.length).map(group => { + const groupLabel = t?.(groupLabelKeys[group], group) || group; + const items = grouped[group].map(entry => { + const safeLabel = escapeHtml ? escapeHtml(entry.label) : entry.label; + const safeKey = escapeHtml ? escapeHtml(entry.key) : entry.key; + return ` + + `; + }).join(''); + return ` +{{ t('ui.workflow.palette.loading', 'Loading nodes...') }}
+