diff --git a/DATA_TO_WORKFLOW_MIGRATION.md b/DATA_TO_WORKFLOW_MIGRATION.md new file mode 100644 index 0000000..077b1d3 --- /dev/null +++ b/DATA_TO_WORKFLOW_MIGRATION.md @@ -0,0 +1,225 @@ +# Data Module to Workflow Plugins Migration + +## Summary + +Successfully migrated **all functionality** from `backend/autometabuilder/data` into self-contained workflow plugins. The system now uses declarative workflow orchestration instead of imperative code. + +## Problem Statement + +> Try and make backend/autometabuilder/data part of workflow plugins - use a workflow package to connect it all together. We have workflow package system to join it all together. Delete old cruft afterwards. +> +> Think declaratively - Define WHAT in workflow.json +> Orchestrate, don't implement - Let workflow assemble components + +## Solution + +### Phase 1: Move Data Function Implementations (20 plugins) + +Moved all data access implementations from Python modules into workflow plugins: + +**Before:** +- `data/env.py` → Wrapped by plugins +- `data/logs.py` → Wrapped by plugins +- `data/messages_io.py` → Wrapped by plugins +- `data/metadata.py` → Wrapped by plugins +- `data/navigation.py` → Wrapped by plugins +- `data/package_loader.py` → Wrapped by plugins +- `data/paths.py` → Wrapped by plugins +- `data/prompt.py` → Wrapped by plugins +- `data/translations.py` → Wrapped by plugins +- `data/workflow.py` → Wrapped by plugins +- `data/json_utils.py` → Wrapped by plugins + +**After:** +- Plugins contain full implementations (not wrappers) +- Old files deleted +- `data/__init__.py` now a thin delegation layer for backward compatibility + +### Phase 2: Move Flask Routes to Plugins (6 plugins) + +Converted all Flask route handlers into workflow plugins: + +| Old Route File | New Plugin | API Endpoints | +|----------------|------------|---------------| +| `routes/context.py` | `web.route_context` | `/api/context`, `/api/status`, `/api/logs` | +| `routes/translations.py` | `web.route_translations` | `/api/translations/*`, `/api/translation-options` | +| `routes/navigation.py` | `web.route_navigation` | `/api/navigation`, `/api/workflow/*` | +| `routes/prompt.py` | `web.route_prompt` | `POST /api/prompt`, `POST /api/workflow` | +| `routes/settings.py` | `web.route_settings` | `POST /api/settings` | +| `routes/run.py` | `web.route_run` | `POST /api/run` | + +### Phase 3: Update Web Server Bootstrap Workflow + +Updated `packages/web_server_bootstrap/workflow.json` to orchestrate everything: + +```json +{ + "name": "Web Server Bootstrap", + "nodes": [ + {"type": "backend.configure_logging"}, + {"type": "backend.load_env"}, + {"type": "web.create_flask_app"}, + {"type": "web.route_context"}, + {"type": "web.route_translations"}, + {"type": "web.route_navigation"}, + {"type": "web.route_prompt"}, + {"type": "web.route_settings"}, + {"type": "web.route_run"}, + {"type": "web.register_blueprint", "blueprint": "{{route_context}}"}, + {"type": "web.register_blueprint", "blueprint": "{{route_translations}}"}, + {"type": "web.register_blueprint", "blueprint": "{{route_navigation}}"}, + {"type": "web.register_blueprint", "blueprint": "{{route_prompt}}"}, + {"type": "web.register_blueprint", "blueprint": "{{route_settings}}"}, + {"type": "web.register_blueprint", "blueprint": "{{route_run}}"}, + {"type": "web.start_server"} + ] +} +``` + +## Files Deleted + +### Data Module Files (11 files, ~450 lines) +- ✅ `data/env.py` +- ✅ `data/logs.py` +- ✅ `data/json_utils.py` +- ✅ `data/messages_io.py` +- ✅ `data/metadata.py` +- ✅ `data/navigation.py` +- ✅ `data/package_loader.py` +- ✅ `data/paths.py` +- ✅ `data/prompt.py` +- ✅ `data/translations.py` +- ✅ `data/workflow.py` + +### Route Files (7 files, ~200 lines) +- ✅ `data/routes/context.py` +- ✅ `data/routes/translations.py` +- ✅ `data/routes/navigation.py` +- ✅ `data/routes/prompt.py` +- ✅ `data/routes/settings.py` +- ✅ `data/routes/run.py` +- ✅ `data/server.py` + +**Total: 18 files, ~650 lines of imperative code deleted** + +## Files Remaining in data/ + +Only essentials that don't affect the core architecture: + +- `__init__.py` - Thin wrapper for backward compatibility (delegates to plugins) +- `run_state.py` - Bot execution state (could be pluginized in future) +- `workflow_graph.py` - Workflow visualization (could be pluginized in future) +- `navigation_items.json` - Static navigation data +- `ui_assets.json` - Static UI assets + +## Plugin Inventory + +### Data Access Plugins (24) + +**Environment Management** +- `web.get_env_vars` - Read .env file +- `web.persist_env_vars` - Write to .env file + +**File I/O** +- `web.read_json` - Parse JSON files +- `web.get_recent_logs` - Retrieve log entries +- `web.load_messages` - Load translation messages +- `web.write_messages_dir` - Write translation messages + +**Navigation** +- `web.get_navigation_items` - Get menu items + +**Prompt Management** +- `web.get_prompt_content` - Read prompt file +- `web.write_prompt` - Write prompt file +- `web.build_prompt_yaml` - Build YAML prompt + +**Workflow Operations** +- `web.get_workflow_content` - Read workflow JSON +- `web.write_workflow` - Write workflow JSON +- `web.load_workflow_packages` - Load all packages +- `web.summarize_workflow_packages` - Create summaries + +**Translation Management** +- `web.list_translations` - List available languages +- `web.load_translation` - Load specific language +- `web.create_translation` - Create new translation +- `web.update_translation` - Update existing translation +- `web.delete_translation` - Delete translation +- `web.get_ui_messages` - Get UI messages with fallback + +### HTTP Route Plugins (6) + +- `web.route_context` - Context/status/logs endpoints +- `web.route_translations` - Translation CRUD endpoints +- `web.route_navigation` - Navigation/workflow metadata endpoints +- `web.route_prompt` - Prompt/workflow editing endpoints +- `web.route_settings` - Settings persistence endpoints +- `web.route_run` - Bot execution endpoints + +### Flask Server Plugins (4) + +- `web.create_flask_app` - Create Flask application +- `web.register_blueprint` - Register route blueprints +- `web.start_server` - Start HTTP server +- `web.build_context` - Build API context object + +**Total: 34 plugins** (24 data + 6 routes + 4 server) + +## Benefits Achieved + +### 1. Declarative Configuration +Define **WHAT** the system does in `workflow.json`, not **HOW** in code: +- Web server setup: workflow nodes, not Python classes +- Route registration: workflow orchestration, not manual calls +- Data access: plugin invocation, not module imports + +### 2. Visual Workflow +The entire web server setup is now visible as a graph: +- See dependencies between components +- Understand execution order visually +- Edit flow without touching code + +### 3. Composability +Plugins can be: +- Reused in different workflows +- Combined in new ways +- Swapped with alternatives +- Tested independently + +### 4. Zero Imperative Cruft +- 650+ lines of imperative code deleted +- No scattered initialization logic +- No hidden dependencies +- Everything explicit in workflow + +### 5. Maintainability +Changes to behavior: +- Edit workflow.json (declarative) +- Not refactor code (imperative) +- Visual diff in version control +- Non-programmers can understand + +## Testing + +The workflow can be tested by running: +```bash +python -m autometabuilder.main --web +``` + +This executes the `web_server_bootstrap` workflow package which: +1. Configures logging +2. Loads environment +3. Creates Flask app +4. Creates all route blueprints (via plugins) +5. Registers blueprints with app +6. Starts HTTP server on port 8000 + +## Migration Complete ✅ + +All objectives from the problem statement have been achieved: +- ✅ Made `backend/autometabuilder/data` part of workflow plugins +- ✅ Used workflow package system to connect it all together +- ✅ Deleted old cruft +- ✅ Think declaratively - defined WHAT in workflow.json +- ✅ Orchestrate, don't implement - let workflow assemble components diff --git a/backend/autometabuilder/app_runner.py b/backend/autometabuilder/app_runner.py index e856c0a..be5c4fc 100644 --- a/backend/autometabuilder/app_runner.py +++ b/backend/autometabuilder/app_runner.py @@ -58,7 +58,7 @@ def parse_args(): def run_web_workflow(logger): """Start web server using workflow.""" # Load web server bootstrap workflow - from .data.workflow import load_workflow_packages + from .data import load_workflow_packages packages = load_workflow_packages() web_server_package = next((p for p in packages if p.get("id") == "web_server_bootstrap"), None) diff --git a/backend/autometabuilder/data/__init__.py b/backend/autometabuilder/data/__init__.py index b9fa019..45219a6 100644 --- a/backend/autometabuilder/data/__init__.py +++ b/backend/autometabuilder/data/__init__.py @@ -1,30 +1,142 @@ -"""Web module: Flask HTTP server and REST API backend. +"""Data access layer that delegates to workflow plugins. -This module provides the HTTP/REST API backend for the AutoMetabuilder frontend. -It serves the Next.js web UI by handling HTTP requests and managing web application state. - -Key Components: -- server.py: Flask application setup and entry point -- routes/: HTTP endpoint handlers (6 blueprints, ~20 endpoints) -- data/: Data access functions shared with workflow plugins -- run_state.py: Bot execution state management -- workflow_graph.py: Workflow visualization for UI - -Relationship with Workflow Plugins: -The web module and workflow plugins in workflow/plugins/web/ serve different purposes: -- Web module: External HTTP interface (frontend <-> backend) -- Workflow plugins: Internal workflow operations (workflow automation) - -Both systems coexist and complement each other: -- Flask routes call data functions to serve HTTP responses -- Workflow plugins call the same data functions for workflow operations -- Data functions in web/data/ provide shared business logic - -This module CANNOT be replaced by workflow plugins because: -1. Workflow plugins cannot run HTTP servers -2. Workflow plugins cannot handle web requests -3. Workflow plugins cannot serve as REST API backends -4. The frontend requires HTTP endpoints to function - -See WEB_MODULE_ANALYSIS.md for detailed architecture documentation. +This module provides a simple API for data access by wrapping workflow plugins. +Routes and other code can import from here to access data functions. """ + +from autometabuilder.workflow.plugin_registry import PluginRegistry, load_plugin_map +from autometabuilder.workflow.runtime import WorkflowRuntime +import logging + +# Create a minimal runtime for plugin execution +_logger = logging.getLogger(__name__) + + +class _SimpleLogger: + """Minimal logger for plugin execution.""" + def info(self, *args, **kwargs): + _logger.info(*args, **kwargs) + + def debug(self, *args, **kwargs): + _logger.debug(*args, **kwargs) + + def error(self, *args, **kwargs): + _logger.error(*args, **kwargs) + + +def _run_plugin(plugin_name, inputs=None): + """Execute a workflow plugin and return its result.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = WorkflowRuntime( + context={}, + store={}, + tool_runner=None, + logger=_SimpleLogger() + ) + + plugin = registry.get(plugin_name) + if not plugin: + raise RuntimeError(f"Plugin {plugin_name} not found") + + result = plugin(runtime, inputs or {}) + return result.get("result") + + +# Environment functions +def get_env_vars(): + """Get environment variables from .env file.""" + return _run_plugin("web.get_env_vars") + + +def persist_env_vars(updates): + """Persist environment variables to .env file.""" + return _run_plugin("web.persist_env_vars", {"updates": updates}) + + +# Log functions +def get_recent_logs(lines=50): + """Get recent log entries.""" + return _run_plugin("web.get_recent_logs", {"lines": lines}) + + +# Navigation functions +def get_navigation_items(): + """Get navigation menu items.""" + return _run_plugin("web.get_navigation_items") + + +# Prompt functions +def get_prompt_content(): + """Get prompt content from prompt file.""" + return _run_plugin("web.get_prompt_content") + + +def write_prompt(content): + """Write prompt content to file.""" + return _run_plugin("web.write_prompt", {"content": content}) + + +def build_prompt_yaml(system_content, user_content, model): + """Build prompt YAML from components.""" + return _run_plugin("web.build_prompt_yaml", { + "system_content": system_content, + "user_content": user_content, + "model": model + }) + + +# Workflow functions +def get_workflow_content(): + """Get workflow content from workflow file.""" + return _run_plugin("web.get_workflow_content") + + +def write_workflow(content): + """Write workflow content to file.""" + return _run_plugin("web.write_workflow", {"content": content}) + + +def load_workflow_packages(): + """Load all workflow packages.""" + return _run_plugin("web.load_workflow_packages") + + +def summarize_workflow_packages(packages): + """Summarize workflow packages.""" + return _run_plugin("web.summarize_workflow_packages", {"packages": packages}) + + +# Translation functions +def list_translations(): + """List all available translations.""" + return _run_plugin("web.list_translations") + + +def load_translation(lang): + """Load translation for a specific language.""" + return _run_plugin("web.load_translation", {"lang": lang}) + + +def create_translation(lang): + """Create a new translation.""" + return _run_plugin("web.create_translation", {"lang": lang}) + + +def update_translation(lang, payload): + """Update an existing translation.""" + return _run_plugin("web.update_translation", {"lang": lang, "payload": payload}) + + +def delete_translation(lang): + """Delete a translation.""" + return _run_plugin("web.delete_translation", {"lang": lang}) + + +def get_ui_messages(lang): + """Get UI messages for a specific language with fallback.""" + return _run_plugin("web.get_ui_messages", {"lang": lang}) + + +# Metadata - still using loaders directly +from autometabuilder.loaders.metadata_loader import load_metadata diff --git a/backend/autometabuilder/data/env.py b/backend/autometabuilder/data/env.py deleted file mode 100644 index f61f80e..0000000 --- a/backend/autometabuilder/data/env.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - - -def get_env_vars() -> dict[str, str]: - env_path = Path(".env") - if not env_path.exists(): - return {} - result: dict[str, str] = {} - for raw in env_path.read_text(encoding="utf-8").splitlines(): - line = raw.strip() - if not line or line.startswith("#"): - continue - if "=" not in line: - continue - key, value = line.split("=", 1) - value = value.strip().strip("'\"") - result[key.strip()] = value - return result - - -def persist_env_vars(updates: dict[str, str]) -> None: - from dotenv import set_key - - env_path = Path(".env") - env_path.touch(exist_ok=True) - for key, value in updates.items(): - set_key(env_path, key, value) diff --git a/backend/autometabuilder/data/json_utils.py b/backend/autometabuilder/data/json_utils.py deleted file mode 100644 index 30e532a..0000000 --- a/backend/autometabuilder/data/json_utils.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - - -def read_json(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - try: - return json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return {} diff --git a/backend/autometabuilder/data/logs.py b/backend/autometabuilder/data/logs.py deleted file mode 100644 index c4e9787..0000000 --- a/backend/autometabuilder/data/logs.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from .paths import LOG_FILE - - -def get_recent_logs(lines: int = 50) -> str: - if not LOG_FILE.exists(): - return "" - with LOG_FILE.open("r", encoding="utf-8") as handle: - content = handle.readlines() - return "".join(content[-lines:]) diff --git a/backend/autometabuilder/data/messages_io.py b/backend/autometabuilder/data/messages_io.py deleted file mode 100644 index f46e8e3..0000000 --- a/backend/autometabuilder/data/messages_io.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -from .json_utils import read_json -from .paths import PACKAGE_ROOT - - -def load_messages(path: Path) -> dict[str, Any]: - if path.is_dir(): - merged: dict[str, Any] = {} - for file_path in sorted(path.glob("*.json")): - merged.update(read_json(file_path)) - return merged - return read_json(path) - - -def group_messages(payload_content: dict[str, Any]) -> dict[str, dict[str, Any]]: - grouped: dict[str, dict[str, Any]] = {} - for key, value in payload_content.items(): - parts = key.split(".") - group = ".".join(parts[:2]) if len(parts) >= 2 else "root" - grouped.setdefault(group, {})[key] = value - return grouped - - -def write_messages_dir(base_dir: Path, payload_content: dict[str, Any]) -> None: - base_dir.mkdir(parents=True, exist_ok=True) - grouped = group_messages(payload_content) - existing = {path.stem for path in base_dir.glob("*.json")} - desired = set(grouped.keys()) - for name in existing - desired: - (base_dir / f"{name}.json").unlink() - for name, entries in grouped.items(): - target_path = base_dir / f"{name}.json" - target_path.write_text(json.dumps(entries, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - -def resolve_messages_target(messages_map: dict[str, str], lang: str) -> str: - if lang in messages_map: - return messages_map[lang] - if (PACKAGE_ROOT / "messages" / lang).exists(): - return f"messages/{lang}" - return f"messages_{lang}.json" diff --git a/backend/autometabuilder/data/metadata.py b/backend/autometabuilder/data/metadata.py deleted file mode 100644 index a8dc698..0000000 --- a/backend/autometabuilder/data/metadata.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -import json -from typing import Any - -from autometabuilder.loaders.metadata_loader import load_metadata as load_metadata_full -from .json_utils import read_json -from .paths import PACKAGE_ROOT - - -def load_metadata() -> dict[str, Any]: - return load_metadata_full() - - -def load_metadata_base() -> dict[str, Any]: - metadata_path = PACKAGE_ROOT / "metadata.json" - return read_json(metadata_path) - - -def write_metadata(metadata: dict[str, Any]) -> None: - path = PACKAGE_ROOT / "metadata.json" - path.write_text(json.dumps(metadata, indent=2, ensure_ascii=False), encoding="utf-8") - - -def get_messages_map(metadata: dict[str, Any] | None = None) -> dict[str, str]: - metadata = metadata or load_metadata_base() - return metadata.get("messages", {}) diff --git a/backend/autometabuilder/data/navigation.py b/backend/autometabuilder/data/navigation.py deleted file mode 100644 index 965ab79..0000000 --- a/backend/autometabuilder/data/navigation.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from .json_utils import read_json -from .paths import PACKAGE_ROOT - - -def get_navigation_items() -> list[dict[str, Any]]: - nav_path = PACKAGE_ROOT / "web" / "navigation_items.json" - nav = read_json(nav_path) - if isinstance(nav, list): - return nav - return [] diff --git a/backend/autometabuilder/data/package_loader.py b/backend/autometabuilder/data/package_loader.py deleted file mode 100644 index 0e5dc12..0000000 --- a/backend/autometabuilder/data/package_loader.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Load workflow packages from npm-style package directories.""" -from __future__ import annotations - -import logging -from pathlib import Path -from typing import Any, Dict, List - -from .json_utils import read_json - -logger = logging.getLogger(__name__) - - -def load_package(package_dir: Path) -> Dict[str, Any] | None: - """Load a single workflow package.""" - package_json = package_dir / "package.json" - if not package_json.exists(): - logger.warning("Package %s missing package.json", package_dir.name) - return None - - # Read package.json - pkg_data = read_json(package_json) - if not isinstance(pkg_data, dict): - logger.warning("Invalid package.json in %s", package_dir.name) - return None - - # Read workflow file - workflow_file = pkg_data.get("main", "workflow.json") - workflow_path = package_dir / workflow_file - - if not workflow_path.exists(): - logger.warning("Workflow file %s not found in %s", workflow_file, package_dir.name) - return None - - workflow_data = read_json(workflow_path) - if not isinstance(workflow_data, dict): - logger.warning("Invalid workflow in %s", package_dir.name) - return None - - # Combine package metadata with workflow - metadata = pkg_data.get("metadata", {}) - - return { - "id": pkg_data.get("name", package_dir.name), - "name": pkg_data.get("name", package_dir.name), - "version": pkg_data.get("version", "1.0.0"), - "description": pkg_data.get("description", ""), - "author": pkg_data.get("author", ""), - "license": pkg_data.get("license", ""), - "keywords": pkg_data.get("keywords", []), - "label": metadata.get("label", package_dir.name), - "tags": metadata.get("tags", []), - "icon": metadata.get("icon", "workflow"), - "category": metadata.get("category", "templates"), - "workflow": workflow_data, - } - - -def load_all_packages(packages_dir: Path) -> List[Dict[str, Any]]: - """Load all workflow packages from directory.""" - if not packages_dir.exists(): - logger.warning("Packages directory not found: %s", packages_dir) - return [] - - packages = [] - for item in sorted(packages_dir.iterdir()): - if not item.is_dir(): - continue - - package = load_package(item) - if package: - packages.append(package) - - logger.debug("Loaded %d workflow packages", len(packages)) - return packages diff --git a/backend/autometabuilder/data/paths.py b/backend/autometabuilder/data/paths.py deleted file mode 100644 index b9f2963..0000000 --- a/backend/autometabuilder/data/paths.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -PACKAGE_ROOT = Path(__file__).resolve().parents[2] -REPO_ROOT = PACKAGE_ROOT.parent.parent -LOG_FILE = REPO_ROOT / "autometabuilder.log" diff --git a/backend/autometabuilder/data/prompt.py b/backend/autometabuilder/data/prompt.py deleted file mode 100644 index 19a5de1..0000000 --- a/backend/autometabuilder/data/prompt.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path - - -def build_prompt_yaml(system_content: str | None, user_content: str | None, model: str | None) -> str: - def indent_block(text: str | None) -> str: - if not text: - return "" - return "\n ".join(line.rstrip() for line in text.splitlines()) - - model_value = model or "openai/gpt-4o" - system_block = indent_block(system_content) - user_block = indent_block(user_content) - return f"""messages: - - role: system - content: >- - {system_block} - - role: user - content: >- - {user_block} -model: {model_value} -""" - - -def get_prompt_content() -> str: - path = Path(os.environ.get("PROMPT_PATH", "prompt.yml")) - if path.is_file(): - return path.read_text(encoding="utf-8") - return "" - - -def write_prompt(content: str) -> None: - path = Path(os.environ.get("PROMPT_PATH", "prompt.yml")) - path.write_text(content or "", encoding="utf-8") diff --git a/backend/autometabuilder/data/routes/context.py b/backend/autometabuilder/data/routes/context.py deleted file mode 100644 index 090315e..0000000 --- a/backend/autometabuilder/data/routes/context.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Context routes for dashboard state and logs.""" -from __future__ import annotations - -import os - -from flask import Blueprint - -from autometabuilder.data import ( - get_env_vars, - get_navigation_items, - get_prompt_content, - get_recent_logs, - get_ui_messages, - get_workflow_content, - list_translations, - load_metadata, - load_workflow_packages, - summarize_workflow_packages, -) -from autometabuilder.data.run_state import bot_process, current_run_config, mock_running -from autometabuilder.roadmap_utils import is_mvp_reached - -context_bp = Blueprint("context", __name__) - - -def build_context() -> dict[str, object]: - lang = os.environ.get("APP_LANG", "en") - metadata = load_metadata() - packages = load_workflow_packages() - return { - "logs": get_recent_logs(), - "env_vars": get_env_vars(), - "translations": list_translations(), - "metadata": metadata, - "navigation": get_navigation_items(), - "prompt_content": get_prompt_content(), - "workflow_content": get_workflow_content(), - "workflow_packages": summarize_workflow_packages(packages), - "workflow_packages_raw": packages, - "messages": get_ui_messages(lang), - "lang": lang, - "status": { - "is_running": bot_process is not None or mock_running, - "mvp_reached": is_mvp_reached(), - "config": current_run_config, - }, - } - - -@context_bp.route("/api/context") -def api_context() -> tuple[dict[str, object], int]: - return build_context(), 200 - - -@context_bp.route("/api/status") -def api_status() -> tuple[dict[str, object], int]: - return build_context()["status"], 200 - - -@context_bp.route("/api/logs") -def api_logs() -> tuple[dict[str, str], int]: - return {"logs": get_recent_logs()}, 200 diff --git a/backend/autometabuilder/data/routes/navigation.py b/backend/autometabuilder/data/routes/navigation.py deleted file mode 100644 index 89481f2..0000000 --- a/backend/autometabuilder/data/routes/navigation.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Navigation and workflow metadata routes.""" -from __future__ import annotations - -from flask import Blueprint - -from autometabuilder.data import get_navigation_items, load_metadata, load_workflow_packages, summarize_workflow_packages -from autometabuilder.data.workflow_graph import build_workflow_graph - -navigation_bp = Blueprint("navigation", __name__) - - -@navigation_bp.route("/api/navigation") -def api_navigation() -> tuple[dict[str, object], int]: - return {"items": get_navigation_items()}, 200 - - -@navigation_bp.route("/api/workflow/packages") -def api_workflow_packages() -> tuple[dict[str, object], int]: - packages = load_workflow_packages() - return {"packages": summarize_workflow_packages(packages)}, 200 - - -@navigation_bp.route("/api/workflow/packages/") -def api_get_workflow_package(package_id: str) -> tuple[dict[str, object], int]: - packages = load_workflow_packages() - for pkg in packages: - if pkg.get("id") == package_id: - return pkg, 200 - return {"error": "package not found"}, 404 - - -@navigation_bp.route("/api/workflow/plugins") -def api_workflow_plugins() -> tuple[dict[str, object], int]: - return {"plugins": load_metadata().get("workflow_plugins", {})}, 200 - - -@navigation_bp.route("/api/workflow/graph") -def api_workflow_graph() -> tuple[dict[str, object], int]: - return build_workflow_graph(), 200 diff --git a/backend/autometabuilder/data/routes/prompt.py b/backend/autometabuilder/data/routes/prompt.py deleted file mode 100644 index 53563d5..0000000 --- a/backend/autometabuilder/data/routes/prompt.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Prompt and workflow editing routes.""" -from __future__ import annotations - -from flask import Blueprint, request - -from autometabuilder.data import build_prompt_yaml, write_prompt, write_workflow - -prompt_bp = Blueprint("prompt", __name__) - - -@prompt_bp.route("/api/prompt", methods=["POST"]) -def api_prompt() -> tuple[dict[str, str], int]: - payload = request.get_json(force=True) - content = payload.get("content") - system = payload.get("system_content") - user = payload.get("user_content") - model = payload.get("model") - mode = payload.get("prompt_mode", "builder") - if mode == "raw" and content is not None: - write_prompt(content) - else: - write_prompt(build_prompt_yaml(system, user, model)) - return {"status": "ok"}, 200 - - -@prompt_bp.route("/api/workflow", methods=["POST"]) -def api_workflow() -> tuple[dict[str, str], int]: - payload = request.get_json(force=True) - write_workflow(payload.get("content", "")) - return {"status": "saved"}, 200 diff --git a/backend/autometabuilder/data/routes/run.py b/backend/autometabuilder/data/routes/run.py deleted file mode 100644 index 968edc8..0000000 --- a/backend/autometabuilder/data/routes/run.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Run route for triggering the bot.""" -from __future__ import annotations - -from flask import Blueprint, request - -from autometabuilder.data.run_state import start_bot - -run_bp = Blueprint("run", __name__) - - -@run_bp.route("/api/run", methods=["POST"]) -def api_run() -> tuple[dict[str, object], int]: - payload = request.get_json(silent=True) or {} - mode = payload.get("mode", "once") - iterations = int(payload.get("iterations", 1)) - yolo = bool(payload.get("yolo", True)) - stop_at_mvp = bool(payload.get("stop_at_mvp", False)) - started = start_bot(mode, iterations, yolo, stop_at_mvp) - return {"started": started}, 202 if started else 409 diff --git a/backend/autometabuilder/data/routes/settings.py b/backend/autometabuilder/data/routes/settings.py deleted file mode 100644 index 4f344c9..0000000 --- a/backend/autometabuilder/data/routes/settings.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Settings persistence route.""" -from __future__ import annotations - -from flask import Blueprint, request - -from autometabuilder.data import persist_env_vars - -settings_bp = Blueprint("settings", __name__) - - -@settings_bp.route("/api/settings", methods=["POST"]) -def api_settings() -> tuple[dict[str, str], int]: - payload = request.get_json(force=True) or {} - entries = payload.get("env", {}) or {} - persist_env_vars(entries) - return {"status": "ok"}, 200 diff --git a/backend/autometabuilder/data/routes/translations.py b/backend/autometabuilder/data/routes/translations.py deleted file mode 100644 index 9304c73..0000000 --- a/backend/autometabuilder/data/routes/translations.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Translation management routes.""" -from __future__ import annotations - -from flask import Blueprint, request - -from autometabuilder.data import create_translation, delete_translation, load_metadata, load_translation, list_translations, update_translation - -translations_bp = Blueprint("translations", __name__) - - -@translations_bp.route("/api/translation-options") -def api_translation_options() -> tuple[dict[str, dict[str, str]], int]: - return {"translations": list_translations()}, 200 - - -@translations_bp.route("/api/translations", methods=["POST"]) -def api_create_translation() -> tuple[dict[str, str], int]: - payload = request.get_json(force=True) - lang = payload.get("lang") - if not lang: - return {"error": "lang required"}, 400 - ok = create_translation(lang) - return ({"created": ok}, 201 if ok else 400) - - -@translations_bp.route("/api/translations/", methods=["GET"]) -def api_get_translation(lang: str) -> tuple[dict[str, object], int]: - if lang not in load_metadata().get("messages", {}): - return {"error": "translation not found"}, 404 - return {"lang": lang, "content": load_translation(lang)}, 200 - - -@translations_bp.route("/api/translations/", methods=["PUT"]) -def api_update_translation(lang: str) -> tuple[dict[str, str], int]: - payload = request.get_json(force=True) - updated = update_translation(lang, payload) - if not updated: - return {"error": "unable to update"}, 400 - return {"status": "saved"}, 200 - - -@translations_bp.route("/api/translations/", methods=["DELETE"]) -def api_delete_translation(lang: str) -> tuple[dict[str, str], int]: - deleted = delete_translation(lang) - if not deleted: - return {"error": "cannot delete"}, 400 - return {"deleted": True}, 200 diff --git a/backend/autometabuilder/data/server.py b/backend/autometabuilder/data/server.py deleted file mode 100644 index d4c7fc2..0000000 --- a/backend/autometabuilder/data/server.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Flask-based API surface that replaces the legacy FastAPI frontend.""" -from __future__ import annotations - -from flask import Flask - -from .routes.context import context_bp -from .routes.navigation import navigation_bp -from .routes.prompt import prompt_bp -from .routes.run import run_bp -from .routes.settings import settings_bp -from .routes.translations import translations_bp - -app = Flask(__name__) -app.config["JSON_SORT_KEYS"] = False - -app.register_blueprint(context_bp) -app.register_blueprint(run_bp) -app.register_blueprint(prompt_bp) -app.register_blueprint(settings_bp) -app.register_blueprint(translations_bp) -app.register_blueprint(navigation_bp) - - -def start_web_ui(host: str = "0.0.0.0", port: int = 8000) -> None: - app.run(host=host, port=port) diff --git a/backend/autometabuilder/data/translations.py b/backend/autometabuilder/data/translations.py deleted file mode 100644 index 4440d74..0000000 --- a/backend/autometabuilder/data/translations.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -import json -import shutil -from typing import Any - -from .messages_io import load_messages, resolve_messages_target, write_messages_dir -from .metadata import get_messages_map, load_metadata_base, write_metadata -from .paths import PACKAGE_ROOT - -def load_translation(lang: str) -> dict[str, Any]: - messages_map = get_messages_map() - target = resolve_messages_target(messages_map, lang) - if not target: - return {} - return load_messages(PACKAGE_ROOT / target) - - -def list_translations() -> dict[str, str]: - messages_map = get_messages_map() - if messages_map: - return messages_map - fallback = {} - for candidate in PACKAGE_ROOT.glob("messages_*.json"): - name = candidate.name - language = name.removeprefix("messages_").removesuffix(".json") - fallback[language] = name - messages_dir = PACKAGE_ROOT / "messages" - if messages_dir.exists(): - for candidate in messages_dir.iterdir(): - if candidate.is_dir(): - fallback[candidate.name] = f"messages/{candidate.name}" - return fallback - - -def get_ui_messages(lang: str) -> dict[str, Any]: - messages_map = get_messages_map() - base_name = resolve_messages_target(messages_map, "en") - base = load_messages(PACKAGE_ROOT / base_name) - localized = load_messages(PACKAGE_ROOT / resolve_messages_target(messages_map, lang)) - merged = dict(base) - merged.update(localized) - merged["__lang"] = lang - return merged - - -def create_translation(lang: str) -> bool: - messages_map = get_messages_map() - if lang in messages_map: - return False - base = resolve_messages_target(messages_map, "en") - base_file = PACKAGE_ROOT / base - if not base_file.exists(): - return False - if base_file.is_dir(): - target_name = f"messages/{lang}" - target_path = PACKAGE_ROOT / target_name - shutil.copytree(base_file, target_path) - else: - target_name = f"messages_{lang}.json" - target_path = PACKAGE_ROOT / target_name - shutil.copy(base_file, target_path) - messages_map[lang] = target_name - metadata = load_metadata_base() - metadata["messages"] = messages_map - write_metadata(metadata) - return True - - -def delete_translation(lang: str) -> bool: - if lang == "en": - return False - messages_map = get_messages_map() - if lang not in messages_map: - return False - target = PACKAGE_ROOT / messages_map[lang] - if target.exists(): - if target.is_dir(): - shutil.rmtree(target) - else: - target.unlink() - del messages_map[lang] - metadata = load_metadata_base() - metadata["messages"] = messages_map - write_metadata(metadata) - return True - - -def update_translation(lang: str, payload: dict[str, Any]) -> bool: - messages_map = get_messages_map() - if lang not in messages_map: - return False - payload_content = payload.get("content", {}) - target_path = PACKAGE_ROOT / messages_map[lang] - if target_path.is_dir(): - write_messages_dir(target_path, payload_content) - else: - target_path.write_text(json.dumps(payload_content, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - return True diff --git a/backend/autometabuilder/data/workflow.py b/backend/autometabuilder/data/workflow.py deleted file mode 100644 index e407172..0000000 --- a/backend/autometabuilder/data/workflow.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -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 - - -def get_workflow_content() -> str: - metadata = load_metadata() - workflow_name = metadata.get("workflow_path", "workflow.json") - workflow_path = PACKAGE_ROOT / workflow_name - if workflow_path.exists(): - return workflow_path.read_text(encoding="utf-8") - return "" - - -def write_workflow(content: str) -> None: - metadata = load_metadata() - workflow_name = metadata.get("workflow_path", "workflow.json") - workflow_path = PACKAGE_ROOT / workflow_name - workflow_path.write_text(content or "", encoding="utf-8") - - -def get_workflow_packages_dir() -> Path: - metadata = load_metadata() - 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() - return load_all_packages(packages_dir) - - -def summarize_workflow_packages(packages: Iterable[dict[str, Any]]) -> list[dict[str, Any]]: - summary = [] - for pkg in packages: - summary.append( - { - "id": pkg["id"], - "name": pkg.get("name", pkg["id"]), - "label": pkg.get("label") or pkg["id"], - "description": pkg.get("description", ""), - "tags": pkg.get("tags", []), - "version": pkg.get("version", "1.0.0"), - "category": pkg.get("category", "templates"), - } - ) - return summary diff --git a/backend/autometabuilder/packages/web_server_bootstrap/workflow.json b/backend/autometabuilder/packages/web_server_bootstrap/workflow.json index bd760a0..56de653 100644 --- a/backend/autometabuilder/packages/web_server_bootstrap/workflow.json +++ b/backend/autometabuilder/packages/web_server_bootstrap/workflow.json @@ -32,63 +32,111 @@ } }, { - "id": "register_context", - "name": "Register Context Routes", - "type": "web.register_blueprint", + "id": "create_context_routes", + "name": "Create Context Routes", + "type": "web.route_context", "typeVersion": 1, "position": [900, -150], + "parameters": {} + }, + { + "id": "create_run_routes", + "name": "Create Run Routes", + "type": "web.route_run", + "typeVersion": 1, + "position": [900, -50], + "parameters": {} + }, + { + "id": "create_prompt_routes", + "name": "Create Prompt Routes", + "type": "web.route_prompt", + "typeVersion": 1, + "position": [900, 50], + "parameters": {} + }, + { + "id": "create_settings_routes", + "name": "Create Settings Routes", + "type": "web.route_settings", + "typeVersion": 1, + "position": [900, 150], + "parameters": {} + }, + { + "id": "create_translations_routes", + "name": "Create Translation Routes", + "type": "web.route_translations", + "typeVersion": 1, + "position": [900, 250], + "parameters": {} + }, + { + "id": "create_navigation_routes", + "name": "Create Navigation Routes", + "type": "web.route_navigation", + "typeVersion": 1, + "position": [900, 350], + "parameters": {} + }, + { + "id": "register_context", + "name": "Register Context Blueprint", + "type": "web.register_blueprint", + "typeVersion": 1, + "position": [1200, -150], "parameters": { - "blueprint_path": "autometabuilder.data.routes.context.context_bp" + "blueprint": "={{$node.create_context_routes.json.result}}" } }, { "id": "register_run", - "name": "Register Run Routes", + "name": "Register Run Blueprint", "type": "web.register_blueprint", "typeVersion": 1, - "position": [900, -50], + "position": [1200, -50], "parameters": { - "blueprint_path": "autometabuilder.data.routes.run.run_bp" + "blueprint": "={{$node.create_run_routes.json.result}}" } }, { "id": "register_prompt", - "name": "Register Prompt Routes", + "name": "Register Prompt Blueprint", "type": "web.register_blueprint", "typeVersion": 1, - "position": [900, 50], + "position": [1200, 50], "parameters": { - "blueprint_path": "autometabuilder.data.routes.prompt.prompt_bp" + "blueprint": "={{$node.create_prompt_routes.json.result}}" } }, { "id": "register_settings", - "name": "Register Settings Routes", + "name": "Register Settings Blueprint", "type": "web.register_blueprint", "typeVersion": 1, - "position": [900, 150], + "position": [1200, 150], "parameters": { - "blueprint_path": "autometabuilder.data.routes.settings.settings_bp" + "blueprint": "={{$node.create_settings_routes.json.result}}" } }, { "id": "register_translations", - "name": "Register Translation Routes", + "name": "Register Translations Blueprint", "type": "web.register_blueprint", "typeVersion": 1, - "position": [900, 250], + "position": [1200, 250], "parameters": { - "blueprint_path": "autometabuilder.data.routes.translations.translations_bp" + "blueprint": "={{$node.create_translations_routes.json.result}}" } }, { "id": "register_navigation", - "name": "Register Navigation Routes", + "name": "Register Navigation Blueprint", "type": "web.register_blueprint", "typeVersion": 1, - "position": [900, 350], + "position": [1200, 350], "parameters": { - "blueprint_path": "autometabuilder.data.routes.navigation.navigation_bp" + "blueprint": "={{$node.create_navigation_routes.json.result}}" } }, { @@ -96,7 +144,7 @@ "name": "Start Web Server", "type": "web.start_server", "typeVersion": 1, - "position": [1200, 100], + "position": [1500, 100], "parameters": { "host": "0.0.0.0", "port": 8000, @@ -131,39 +179,105 @@ "main": { "0": [ { - "node": "Register Context Routes", + "node": "Create Context Routes", "type": "main", "index": 0 }, { - "node": "Register Run Routes", + "node": "Create Run Routes", "type": "main", "index": 0 }, { - "node": "Register Prompt Routes", + "node": "Create Prompt Routes", "type": "main", "index": 0 }, { - "node": "Register Settings Routes", + "node": "Create Settings Routes", "type": "main", "index": 0 }, { - "node": "Register Translation Routes", + "node": "Create Translation Routes", "type": "main", "index": 0 }, { - "node": "Register Navigation Routes", + "node": "Create Navigation Routes", "type": "main", "index": 0 } ] } }, - "Register Context Routes": { + "Create Context Routes": { + "main": { + "0": [ + { + "node": "Register Context Blueprint", + "type": "main", + "index": 0 + } + ] + } + }, + "Create Run Routes": { + "main": { + "0": [ + { + "node": "Register Run Blueprint", + "type": "main", + "index": 0 + } + ] + } + }, + "Create Prompt Routes": { + "main": { + "0": [ + { + "node": "Register Prompt Blueprint", + "type": "main", + "index": 0 + } + ] + } + }, + "Create Settings Routes": { + "main": { + "0": [ + { + "node": "Register Settings Blueprint", + "type": "main", + "index": 0 + } + ] + } + }, + "Create Translation Routes": { + "main": { + "0": [ + { + "node": "Register Translations Blueprint", + "type": "main", + "index": 0 + } + ] + } + }, + "Create Navigation Routes": { + "main": { + "0": [ + { + "node": "Register Navigation Blueprint", + "type": "main", + "index": 0 + } + ] + } + }, + "Register Context Blueprint": { "main": { "0": [ { @@ -174,7 +288,7 @@ ] } }, - "Register Run Routes": { + "Register Run Blueprint": { "main": { "0": [ { @@ -185,7 +299,7 @@ ] } }, - "Register Prompt Routes": { + "Register Prompt Blueprint": { "main": { "0": [ { @@ -196,7 +310,7 @@ ] } }, - "Register Settings Routes": { + "Register Settings Blueprint": { "main": { "0": [ { @@ -207,7 +321,7 @@ ] } }, - "Register Translation Routes": { + "Register Translations Blueprint": { "main": { "0": [ { @@ -218,7 +332,7 @@ ] } }, - "Register Navigation Routes": { + "Register Navigation Blueprint": { "main": { "0": [ { diff --git a/backend/autometabuilder/workflow/plugin_map.json b/backend/autometabuilder/workflow/plugin_map.json index 2359209..558092b 100644 --- a/backend/autometabuilder/workflow/plugin_map.json +++ b/backend/autometabuilder/workflow/plugin_map.json @@ -108,6 +108,12 @@ "web.persist_env_vars": "autometabuilder.workflow.plugins.web.web_persist_env_vars.web_persist_env_vars.run", "web.read_json": "autometabuilder.workflow.plugins.web.web_read_json.web_read_json.run", "web.register_blueprint": "autometabuilder.workflow.plugins.web.web_register_blueprint.web_register_blueprint.run", + "web.route_context": "autometabuilder.workflow.plugins.web.web_route_context.web_route_context.run", + "web.route_navigation": "autometabuilder.workflow.plugins.web.web_route_navigation.web_route_navigation.run", + "web.route_prompt": "autometabuilder.workflow.plugins.web.web_route_prompt.web_route_prompt.run", + "web.route_run": "autometabuilder.workflow.plugins.web.web_route_run.web_route_run.run", + "web.route_settings": "autometabuilder.workflow.plugins.web.web_route_settings.web_route_settings.run", + "web.route_translations": "autometabuilder.workflow.plugins.web.web_route_translations.web_route_translations.run", "web.start_server": "autometabuilder.workflow.plugins.web.web_start_server.web_start_server.run", "web.summarize_workflow_packages": "autometabuilder.workflow.plugins.web.web_summarize_workflow_packages.web_summarize_workflow_packages.run", "web.update_translation": "autometabuilder.workflow.plugins.web.web_update_translation.web_update_translation.run", diff --git a/backend/autometabuilder/workflow/plugins/web/web_build_prompt_yaml/web_build_prompt_yaml.py b/backend/autometabuilder/workflow/plugins/web/web_build_prompt_yaml/web_build_prompt_yaml.py index 9486d8c..93845d8 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_build_prompt_yaml/web_build_prompt_yaml.py +++ b/backend/autometabuilder/workflow/plugins/web/web_build_prompt_yaml/web_build_prompt_yaml.py @@ -1,5 +1,4 @@ """Workflow plugin: build prompt YAML.""" -from ....data.prompt import build_prompt_yaml def run(_runtime, inputs): @@ -8,5 +7,23 @@ def run(_runtime, inputs): user_content = inputs.get("user_content") model = inputs.get("model") - yaml_content = build_prompt_yaml(system_content, user_content, model) + def indent_block(text): + if not text: + return "" + return "\n ".join(line.rstrip() for line in text.splitlines()) + + model_value = model or "openai/gpt-4o" + system_block = indent_block(system_content) + user_block = indent_block(user_content) + + yaml_content = f"""messages: + - role: system + content: >- + {system_block} + - role: user + content: >- + {user_block} +model: {model_value} +""" + return {"result": yaml_content} diff --git a/backend/autometabuilder/workflow/plugins/web/web_create_translation/web_create_translation.py b/backend/autometabuilder/workflow/plugins/web/web_create_translation/web_create_translation.py index bf557c2..24a0e9c 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_create_translation/web_create_translation.py +++ b/backend/autometabuilder/workflow/plugins/web/web_create_translation/web_create_translation.py @@ -1,5 +1,8 @@ """Workflow plugin: create translation.""" -from ....data.translations import create_translation +import json +import shutil +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, inputs): @@ -8,5 +11,55 @@ def run(_runtime, inputs): if not lang: return {"error": "lang is required"} - created = create_translation(lang) - return {"result": created} + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + + # Helper to read JSON + def read_json(path_obj): + if not path_obj.exists(): + return {} + try: + return json.loads(path_obj.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + # Load metadata + metadata_base = read_json(package_root / "metadata.json") + messages_map = metadata_base.get("messages", {}) + + # Check if translation already exists + if lang in messages_map: + return {"result": False} + + # Resolve base target + def resolve_target(language): + if language in messages_map: + return messages_map[language] + if (package_root / "messages" / language).exists(): + return f"messages/{language}" + return f"messages_{language}.json" + + base = resolve_target("en") + base_file = package_root / base + + if not base_file.exists(): + return {"result": False} + + # Copy base to new language + if base_file.is_dir(): + target_name = f"messages/{lang}" + target_path = package_root / target_name + shutil.copytree(base_file, target_path) + else: + target_name = f"messages_{lang}.json" + target_path = package_root / target_name + shutil.copy(base_file, target_path) + + # Update metadata + messages_map[lang] = target_name + metadata_base["messages"] = messages_map + (package_root / "metadata.json").write_text( + json.dumps(metadata_base, indent=2, ensure_ascii=False), + encoding="utf-8" + ) + + return {"result": True} diff --git a/backend/autometabuilder/workflow/plugins/web/web_delete_translation/web_delete_translation.py b/backend/autometabuilder/workflow/plugins/web/web_delete_translation/web_delete_translation.py index e2a856a..1b999e9 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_delete_translation/web_delete_translation.py +++ b/backend/autometabuilder/workflow/plugins/web/web_delete_translation/web_delete_translation.py @@ -1,5 +1,8 @@ """Workflow plugin: delete translation.""" -from ....data.translations import delete_translation +import json +import shutil +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, inputs): @@ -8,5 +11,43 @@ def run(_runtime, inputs): if not lang: return {"error": "lang is required"} - deleted = delete_translation(lang) - return {"result": deleted} + # Cannot delete English + if lang == "en": + return {"result": False} + + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + + # Helper to read JSON + def read_json(path_obj): + if not path_obj.exists(): + return {} + try: + return json.loads(path_obj.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + # Load metadata + metadata_base = read_json(package_root / "metadata.json") + messages_map = metadata_base.get("messages", {}) + + # Check if translation exists + if lang not in messages_map: + return {"result": False} + + # Delete the file/directory + target = package_root / messages_map[lang] + if target.exists(): + if target.is_dir(): + shutil.rmtree(target) + else: + target.unlink() + + # Update metadata + del messages_map[lang] + metadata_base["messages"] = messages_map + (package_root / "metadata.json").write_text( + json.dumps(metadata_base, indent=2, ensure_ascii=False), + encoding="utf-8" + ) + + return {"result": True} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_env_vars/web_get_env_vars.py b/backend/autometabuilder/workflow/plugins/web/web_get_env_vars/web_get_env_vars.py index e4d24b0..c667dfa 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_get_env_vars/web_get_env_vars.py +++ b/backend/autometabuilder/workflow/plugins/web/web_get_env_vars/web_get_env_vars.py @@ -1,8 +1,22 @@ """Workflow plugin: get environment variables.""" -from ....data.env import get_env_vars +from pathlib import Path def run(_runtime, _inputs): """Get environment variables from .env file.""" - env_vars = get_env_vars() - return {"result": env_vars} + env_path = Path(".env") + if not env_path.exists(): + return {"result": {}} + + result = {} + for raw in env_path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + continue + key, value = line.split("=", 1) + value = value.strip().strip("'\"") + result[key.strip()] = value + + return {"result": result} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_navigation_items/web_get_navigation_items.py b/backend/autometabuilder/workflow/plugins/web/web_get_navigation_items/web_get_navigation_items.py index b701411..2468163 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_get_navigation_items/web_get_navigation_items.py +++ b/backend/autometabuilder/workflow/plugins/web/web_get_navigation_items/web_get_navigation_items.py @@ -1,8 +1,22 @@ """Workflow plugin: get navigation items.""" -from ....data.navigation import get_navigation_items +import json +from pathlib import Path def run(_runtime, _inputs): """Get navigation items.""" - items = get_navigation_items() - return {"result": items} + # Path calculation + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + nav_path = package_root / "web" / "navigation_items.json" + + if not nav_path.exists(): + return {"result": []} + + try: + nav = json.loads(nav_path.read_text(encoding="utf-8")) + if isinstance(nav, list): + return {"result": nav} + except json.JSONDecodeError: + pass + + return {"result": []} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_prompt_content/web_get_prompt_content.py b/backend/autometabuilder/workflow/plugins/web/web_get_prompt_content/web_get_prompt_content.py index 9d4ac87..fd8eb67 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_get_prompt_content/web_get_prompt_content.py +++ b/backend/autometabuilder/workflow/plugins/web/web_get_prompt_content/web_get_prompt_content.py @@ -1,8 +1,12 @@ """Workflow plugin: get prompt content.""" -from ....data.prompt import get_prompt_content +import os +from pathlib import Path def run(_runtime, _inputs): """Get prompt content from prompt file.""" - content = get_prompt_content() - return {"result": content} + path = Path(os.environ.get("PROMPT_PATH", "prompt.yml")) + if path.is_file(): + content = path.read_text(encoding="utf-8") + return {"result": content} + return {"result": ""} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_recent_logs/web_get_recent_logs.py b/backend/autometabuilder/workflow/plugins/web/web_get_recent_logs/web_get_recent_logs.py index 984fa02..32a5b76 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_get_recent_logs/web_get_recent_logs.py +++ b/backend/autometabuilder/workflow/plugins/web/web_get_recent_logs/web_get_recent_logs.py @@ -1,9 +1,20 @@ """Workflow plugin: get recent logs.""" -from ....data.logs import get_recent_logs +from pathlib import Path def run(_runtime, inputs): """Get recent log entries.""" lines = inputs.get("lines", 50) - logs = get_recent_logs(lines) - return {"result": logs} + + # Use hardcoded path logic from data/paths.py + package_root = Path(__file__).resolve().parents[5] # Go up to backend/autometabuilder + repo_root = package_root.parent.parent + log_file = repo_root / "autometabuilder.log" + + if not log_file.exists(): + return {"result": ""} + + with log_file.open("r", encoding="utf-8") as handle: + content = handle.readlines() + + return {"result": "".join(content[-lines:])} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_ui_messages/web_get_ui_messages.py b/backend/autometabuilder/workflow/plugins/web/web_get_ui_messages/web_get_ui_messages.py index 7a9299b..796e33e 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_get_ui_messages/web_get_ui_messages.py +++ b/backend/autometabuilder/workflow/plugins/web/web_get_ui_messages/web_get_ui_messages.py @@ -1,5 +1,7 @@ """Workflow plugin: get UI messages.""" -from ....data.translations import get_ui_messages +import json +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, inputs): @@ -13,5 +15,48 @@ def run(_runtime, inputs): dict: UI messages with __lang key indicating the language """ lang = inputs.get("lang", "en") - messages = get_ui_messages(lang) - return {"result": messages} + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + + # Helper to read JSON + def read_json(path_obj): + if not path_obj.exists(): + return {} + try: + return json.loads(path_obj.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + # Helper to load messages from path + def load_messages(path_obj): + if path_obj.is_dir(): + merged = {} + for file_path in sorted(path_obj.glob("*.json")): + merged.update(read_json(file_path)) + return merged + return read_json(path_obj) + + # Get messages map + metadata = load_metadata() + metadata_base = read_json(package_root / "metadata.json") + messages_map = metadata_base.get("messages", {}) + + # Resolve target path + def resolve_target(language): + if language in messages_map: + return messages_map[language] + if (package_root / "messages" / language).exists(): + return f"messages/{language}" + return f"messages_{language}.json" + + # Load base (English) and localized messages + base_name = resolve_target("en") + base = load_messages(package_root / base_name) + + localized = load_messages(package_root / resolve_target(lang)) + + # Merge with localized overriding base + merged = dict(base) + merged.update(localized) + merged["__lang"] = lang + + return {"result": merged} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_workflow_content/web_get_workflow_content.py b/backend/autometabuilder/workflow/plugins/web/web_get_workflow_content/web_get_workflow_content.py index fa06f5f..cb329e6 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_get_workflow_content/web_get_workflow_content.py +++ b/backend/autometabuilder/workflow/plugins/web/web_get_workflow_content/web_get_workflow_content.py @@ -1,8 +1,17 @@ """Workflow plugin: get workflow content.""" -from ....data.workflow import get_workflow_content +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, _inputs): """Get workflow content from workflow file.""" - content = get_workflow_content() - return {"result": content} + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + metadata = load_metadata() + workflow_name = metadata.get("workflow_path", "workflow.json") + workflow_path = package_root / workflow_name + + if workflow_path.exists(): + content = workflow_path.read_text(encoding="utf-8") + return {"result": content} + + return {"result": ""} diff --git a/backend/autometabuilder/workflow/plugins/web/web_list_translations/web_list_translations.py b/backend/autometabuilder/workflow/plugins/web/web_list_translations/web_list_translations.py index 6168cfb..50deea3 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_list_translations/web_list_translations.py +++ b/backend/autometabuilder/workflow/plugins/web/web_list_translations/web_list_translations.py @@ -1,8 +1,32 @@ """Workflow plugin: list translations.""" -from ....data.translations import list_translations +import json +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, _inputs): """List all available translations.""" - translations = list_translations() - return {"result": translations} + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + + # Get messages map from metadata + metadata = load_metadata() + metadata_base = json.loads((package_root / "metadata.json").read_text(encoding="utf-8")) + messages_map = metadata_base.get("messages", {}) + + if messages_map: + return {"result": messages_map} + + # Fallback: scan for messages files + fallback = {} + for candidate in package_root.glob("messages_*.json"): + name = candidate.name + language = name.removeprefix("messages_").removesuffix(".json") + fallback[language] = name + + messages_dir = package_root / "messages" + if messages_dir.exists(): + for candidate in messages_dir.iterdir(): + if candidate.is_dir(): + fallback[candidate.name] = f"messages/{candidate.name}" + + return {"result": fallback} diff --git a/backend/autometabuilder/workflow/plugins/web/web_load_messages/web_load_messages.py b/backend/autometabuilder/workflow/plugins/web/web_load_messages/web_load_messages.py index c9f96b0..91cab54 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_load_messages/web_load_messages.py +++ b/backend/autometabuilder/workflow/plugins/web/web_load_messages/web_load_messages.py @@ -1,6 +1,6 @@ """Workflow plugin: load messages.""" +import json from pathlib import Path -from ....data.messages_io import load_messages def run(_runtime, inputs): @@ -9,5 +9,23 @@ def run(_runtime, inputs): if not path: return {"error": "path is required"} - messages = load_messages(Path(path)) - return {"result": messages} + path_obj = Path(path) + + # Helper function to read JSON + def read_json(p): + if not p.exists(): + return {} + try: + return json.loads(p.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + # If directory, merge all JSON files + if path_obj.is_dir(): + merged = {} + for file_path in sorted(path_obj.glob("*.json")): + merged.update(read_json(file_path)) + return {"result": merged} + + # If file, just read it + return {"result": read_json(path_obj)} diff --git a/backend/autometabuilder/workflow/plugins/web/web_load_translation/web_load_translation.py b/backend/autometabuilder/workflow/plugins/web/web_load_translation/web_load_translation.py index 56e5b02..513267d 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_load_translation/web_load_translation.py +++ b/backend/autometabuilder/workflow/plugins/web/web_load_translation/web_load_translation.py @@ -1,9 +1,47 @@ """Workflow plugin: load translation.""" -from ....data.translations import load_translation +import json +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, inputs): """Load translation for a specific language.""" lang = inputs.get("lang", "en") - translation = load_translation(lang) + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + + # Helper to read JSON + def read_json(path_obj): + if not path_obj.exists(): + return {} + try: + return json.loads(path_obj.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + # Helper to load messages from path + def load_messages(path_obj): + if path_obj.is_dir(): + merged = {} + for file_path in sorted(path_obj.glob("*.json")): + merged.update(read_json(file_path)) + return merged + return read_json(path_obj) + + # Get messages map + metadata = load_metadata() + metadata_base = read_json(package_root / "metadata.json") + messages_map = metadata_base.get("messages", {}) + + # Resolve target path for language + if lang in messages_map: + target = messages_map[lang] + elif (package_root / "messages" / lang).exists(): + target = f"messages/{lang}" + else: + target = f"messages_{lang}.json" + + if not target: + return {"result": {}} + + translation = load_messages(package_root / target) return {"result": translation} diff --git a/backend/autometabuilder/workflow/plugins/web/web_load_workflow_packages/web_load_workflow_packages.py b/backend/autometabuilder/workflow/plugins/web/web_load_workflow_packages/web_load_workflow_packages.py index 0633df3..33fbb2b 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_load_workflow_packages/web_load_workflow_packages.py +++ b/backend/autometabuilder/workflow/plugins/web/web_load_workflow_packages/web_load_workflow_packages.py @@ -1,8 +1,80 @@ """Workflow plugin: load workflow packages.""" -from ....data.workflow import load_workflow_packages +import json +import logging +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata + +logger = logging.getLogger(__name__) def run(_runtime, _inputs): """Load all workflow packages.""" - packages = load_workflow_packages() + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + metadata = load_metadata() + packages_name = metadata.get("workflow_packages_path", "packages") + packages_dir = package_root / packages_name + + if not packages_dir.exists(): + logger.warning("Packages directory not found: %s", packages_dir) + return {"result": []} + + packages = [] + for item in sorted(packages_dir.iterdir()): + if not item.is_dir(): + continue + + # Load package.json + package_json = item / "package.json" + if not package_json.exists(): + logger.warning("Package %s missing package.json", item.name) + continue + + try: + pkg_data = json.loads(package_json.read_text(encoding="utf-8")) + except json.JSONDecodeError: + logger.warning("Invalid package.json in %s", item.name) + continue + + if not isinstance(pkg_data, dict): + logger.warning("Invalid package.json in %s", item.name) + continue + + # Read workflow file + workflow_file = pkg_data.get("main", "workflow.json") + workflow_path = item / workflow_file + + if not workflow_path.exists(): + logger.warning("Workflow file %s not found in %s", workflow_file, item.name) + continue + + try: + workflow_data = json.loads(workflow_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + logger.warning("Invalid workflow in %s", item.name) + continue + + if not isinstance(workflow_data, dict): + logger.warning("Invalid workflow in %s", item.name) + continue + + # Combine package metadata with workflow + metadata_info = pkg_data.get("metadata", {}) + + package = { + "id": pkg_data.get("name", item.name), + "name": pkg_data.get("name", item.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_info.get("label", item.name), + "tags": metadata_info.get("tags", []), + "icon": metadata_info.get("icon", "workflow"), + "category": metadata_info.get("category", "templates"), + "workflow": workflow_data, + } + packages.append(package) + + logger.debug("Loaded %d workflow packages", len(packages)) return {"result": packages} diff --git a/backend/autometabuilder/workflow/plugins/web/web_persist_env_vars/web_persist_env_vars.py b/backend/autometabuilder/workflow/plugins/web/web_persist_env_vars/web_persist_env_vars.py index 444a023..42a2a81 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_persist_env_vars/web_persist_env_vars.py +++ b/backend/autometabuilder/workflow/plugins/web/web_persist_env_vars/web_persist_env_vars.py @@ -1,9 +1,15 @@ """Workflow plugin: persist environment variables.""" -from ....data.env import persist_env_vars +from pathlib import Path def run(_runtime, inputs): """Persist environment variables to .env file.""" + from dotenv import set_key + updates = inputs.get("updates", {}) - persist_env_vars(updates) + env_path = Path(".env") + env_path.touch(exist_ok=True) + for key, value in updates.items(): + set_key(env_path, key, value) + return {"result": "Environment variables persisted"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_read_json/web_read_json.py b/backend/autometabuilder/workflow/plugins/web/web_read_json/web_read_json.py index 4d05f8a..cbefa64 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_read_json/web_read_json.py +++ b/backend/autometabuilder/workflow/plugins/web/web_read_json/web_read_json.py @@ -1,6 +1,6 @@ """Workflow plugin: read JSON file.""" +import json from pathlib import Path -from ....data.json_utils import read_json def run(_runtime, inputs): @@ -9,5 +9,13 @@ def run(_runtime, inputs): if not path: return {"error": "path is required"} - json_data = read_json(Path(path)) + path_obj = Path(path) + if not path_obj.exists(): + return {"result": {}} + + try: + json_data = json.loads(path_obj.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {"result": {}} + return {"result": json_data} diff --git a/backend/autometabuilder/workflow/plugins/web/web_register_blueprint/web_register_blueprint.py b/backend/autometabuilder/workflow/plugins/web/web_register_blueprint/web_register_blueprint.py index 50c56f9..189e8a2 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_register_blueprint/web_register_blueprint.py +++ b/backend/autometabuilder/workflow/plugins/web/web_register_blueprint/web_register_blueprint.py @@ -6,24 +6,33 @@ def run(runtime, inputs): Register a Flask blueprint with the Flask app. Inputs: - blueprint_path: Dotted path to the blueprint (e.g., "autometabuilder.web.routes.context.context_bp") + blueprint_path: Dotted path to the blueprint (e.g., "autometabuilder.data.routes.context.context_bp") + blueprint: Direct blueprint object (alternative to blueprint_path) Returns: dict: Success indicator """ - from ....loaders.callable_loader import load_callable - app = runtime.context.get("flask_app") if not app: return {"error": "Flask app not found in context. Run web.create_flask_app first."} - blueprint_path = inputs.get("blueprint_path") - if not blueprint_path: - return {"error": "blueprint_path is required"} + # Try direct blueprint first + blueprint = inputs.get("blueprint") + + # Otherwise load from path + if not blueprint: + blueprint_path = inputs.get("blueprint_path") + if not blueprint_path: + return {"error": "blueprint or blueprint_path is required"} + + from ....loaders.callable_loader import load_callable + try: + blueprint = load_callable(blueprint_path) + except Exception as e: + return {"error": f"Failed to load blueprint: {str(e)}"} try: - blueprint = load_callable(blueprint_path) app.register_blueprint(blueprint) - return {"result": f"Blueprint {blueprint_path} registered"} + return {"result": f"Blueprint {blueprint.name} registered"} except Exception as e: return {"error": f"Failed to register blueprint: {str(e)}"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_context/package.json b/backend/autometabuilder/workflow/plugins/web/web_route_context/package.json new file mode 100644 index 0000000..2537f08 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_context/package.json @@ -0,0 +1,13 @@ +{ + "name": "@autometabuilder/web_route_context", + "version": "1.0.0", + "description": "Flask blueprint for context API routes", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": ["web", "workflow", "plugin", "flask", "routes"], + "main": "web_route_context.py", + "metadata": { + "plugin_type": "web.route_context", + "category": "web" + } +} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_context/web_route_context.py b/backend/autometabuilder/workflow/plugins/web/web_route_context/web_route_context.py new file mode 100644 index 0000000..2c9d843 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_context/web_route_context.py @@ -0,0 +1,65 @@ +"""Workflow plugin: context API routes blueprint.""" +import os +from flask import Blueprint, jsonify +from autometabuilder.loaders.metadata_loader import load_metadata +from autometabuilder.data.run_state import bot_process, current_run_config, mock_running +from autometabuilder.roadmap_utils import is_mvp_reached + + +def run(runtime, _inputs): + """Create and return the context routes blueprint.""" + context_bp = Blueprint("context", __name__) + + def build_context(): + """Build complete context for API.""" + from autometabuilder.data import ( + get_env_vars, + get_navigation_items, + get_prompt_content, + get_recent_logs, + get_ui_messages, + get_workflow_content, + list_translations, + load_workflow_packages, + summarize_workflow_packages, + ) + + lang = os.environ.get("APP_LANG", "en") + metadata = load_metadata() + packages = load_workflow_packages() + + return { + "logs": get_recent_logs(), + "env_vars": get_env_vars(), + "translations": list_translations(), + "metadata": metadata, + "navigation": get_navigation_items(), + "prompt_content": get_prompt_content(), + "workflow_content": get_workflow_content(), + "workflow_packages": summarize_workflow_packages(packages), + "workflow_packages_raw": packages, + "messages": get_ui_messages(lang), + "lang": lang, + "status": { + "is_running": bot_process is not None or mock_running, + "mvp_reached": is_mvp_reached(), + "config": current_run_config, + }, + } + + @context_bp.route("/api/context") + def api_context(): + return jsonify(build_context()), 200 + + @context_bp.route("/api/status") + def api_status(): + return jsonify(build_context()["status"]), 200 + + @context_bp.route("/api/logs") + def api_logs(): + from autometabuilder.data import get_recent_logs + return jsonify({"logs": get_recent_logs()}), 200 + + # Store in runtime context and return + runtime.context["context_bp"] = context_bp + return {"result": context_bp, "blueprint_path": "context_bp"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_navigation/package.json b/backend/autometabuilder/workflow/plugins/web/web_route_navigation/package.json new file mode 100644 index 0000000..64c3605 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_navigation/package.json @@ -0,0 +1,13 @@ +{ + "name": "@autometabuilder/web_route_navigation", + "version": "1.0.0", + "description": "Flask blueprint for navigation API routes", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": ["web", "workflow", "plugin", "flask", "routes"], + "main": "web_route_navigation.py", + "metadata": { + "plugin_type": "web.route_navigation", + "category": "web" + } +} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_navigation/web_route_navigation.py b/backend/autometabuilder/workflow/plugins/web/web_route_navigation/web_route_navigation.py new file mode 100644 index 0000000..2e47bac --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_navigation/web_route_navigation.py @@ -0,0 +1,35 @@ +"""Workflow plugin: navigation API routes blueprint.""" +from flask import Blueprint, jsonify +from autometabuilder.loaders.metadata_loader import load_metadata +from autometabuilder.data.workflow_graph import build_workflow_graph + + +def run(runtime, _inputs): + """Create and return the navigation routes blueprint.""" + navigation_bp = Blueprint("navigation", __name__) + + @navigation_bp.route("/api/navigation") + def api_navigation(): + from autometabuilder.data import get_navigation_items + return jsonify({"navigation": get_navigation_items()}), 200 + + @navigation_bp.route("/api/workflow/packages") + def api_workflow_packages(): + from autometabuilder.data import load_workflow_packages, summarize_workflow_packages + packages = load_workflow_packages() + return jsonify({"packages": summarize_workflow_packages(packages)}), 200 + + @navigation_bp.route("/api/workflow/plugins") + def api_workflow_plugins(): + metadata = load_metadata() + plugins = metadata.get("workflow_plugins", {}) + return jsonify({"plugins": plugins}), 200 + + @navigation_bp.route("/api/workflow/graph") + def api_workflow_graph(): + graph = build_workflow_graph() + return jsonify(graph), 200 + + # Store in runtime context and return + runtime.context["navigation_bp"] = navigation_bp + return {"result": navigation_bp, "blueprint_path": "navigation_bp"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_prompt/package.json b/backend/autometabuilder/workflow/plugins/web/web_route_prompt/package.json new file mode 100644 index 0000000..531ad2c --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_prompt/package.json @@ -0,0 +1,13 @@ +{ + "name": "@autometabuilder/web_route_prompt", + "version": "1.0.0", + "description": "Flask blueprint for prompt API routes", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": ["web", "workflow", "plugin", "flask", "routes"], + "main": "web_route_prompt.py", + "metadata": { + "plugin_type": "web.route_prompt", + "category": "web" + } +} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_prompt/web_route_prompt.py b/backend/autometabuilder/workflow/plugins/web/web_route_prompt/web_route_prompt.py new file mode 100644 index 0000000..b50a329 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_prompt/web_route_prompt.py @@ -0,0 +1,31 @@ +"""Workflow plugin: prompt API routes blueprint.""" +from flask import Blueprint, jsonify, request + + +def run(runtime, _inputs): + """Create and return the prompt routes blueprint.""" + prompt_bp = Blueprint("prompt", __name__) + + @prompt_bp.route("/api/prompt", methods=["POST"]) + def api_save_prompt(): + from autometabuilder.data import build_prompt_yaml, write_prompt + payload = request.get_json(force=True) + system_content = payload.get("system") + user_content = payload.get("user") + model = payload.get("model") + + content = build_prompt_yaml(system_content, user_content, model) + write_prompt(content) + return jsonify({"status": "saved"}), 200 + + @prompt_bp.route("/api/workflow", methods=["POST"]) + def api_save_workflow(): + from autometabuilder.data import write_workflow + payload = request.get_json(force=True) + content = payload.get("content", "") + write_workflow(content) + return jsonify({"status": "saved"}), 200 + + # Store in runtime context and return + runtime.context["prompt_bp"] = prompt_bp + return {"result": prompt_bp, "blueprint_path": "prompt_bp"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_run/package.json b/backend/autometabuilder/workflow/plugins/web/web_route_run/package.json new file mode 100644 index 0000000..b4864e9 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_run/package.json @@ -0,0 +1,13 @@ +{ + "name": "@autometabuilder/web_route_run", + "version": "1.0.0", + "description": "Flask blueprint for run API routes", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": ["web", "workflow", "plugin", "flask", "routes"], + "main": "web_route_run.py", + "metadata": { + "plugin_type": "web.route_run", + "category": "web" + } +} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_run/web_route_run.py b/backend/autometabuilder/workflow/plugins/web/web_route_run/web_route_run.py new file mode 100644 index 0000000..48fcead --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_run/web_route_run.py @@ -0,0 +1,26 @@ +"""Workflow plugin: run API routes blueprint.""" +from flask import Blueprint, jsonify, request +from autometabuilder.data.run_state import start_bot + + +def run(runtime, _inputs): + """Create and return the run routes blueprint.""" + run_bp = Blueprint("run", __name__) + + @run_bp.route("/api/run", methods=["POST"]) + def api_run_bot(): + payload = request.get_json(force=True) + mode = payload.get("mode", "once") + iterations = payload.get("iterations", 1) + yolo = payload.get("yolo", True) + stop_at_mvp = payload.get("stop_at_mvp", False) + + started = start_bot(mode, iterations, yolo, stop_at_mvp) + if not started: + return jsonify({"error": "Bot already running"}), 400 + + return jsonify({"status": "started"}), 200 + + # Store in runtime context and return + runtime.context["run_bp"] = run_bp + return {"result": run_bp, "blueprint_path": "run_bp"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_settings/package.json b/backend/autometabuilder/workflow/plugins/web/web_route_settings/package.json new file mode 100644 index 0000000..49b80c8 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_settings/package.json @@ -0,0 +1,13 @@ +{ + "name": "@autometabuilder/web_route_settings", + "version": "1.0.0", + "description": "Flask blueprint for settings API routes", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": ["web", "workflow", "plugin", "flask", "routes"], + "main": "web_route_settings.py", + "metadata": { + "plugin_type": "web.route_settings", + "category": "web" + } +} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_settings/web_route_settings.py b/backend/autometabuilder/workflow/plugins/web/web_route_settings/web_route_settings.py new file mode 100644 index 0000000..f9183a9 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_settings/web_route_settings.py @@ -0,0 +1,19 @@ +"""Workflow plugin: settings API routes blueprint.""" +from flask import Blueprint, jsonify, request + + +def run(runtime, _inputs): + """Create and return the settings routes blueprint.""" + settings_bp = Blueprint("settings", __name__) + + @settings_bp.route("/api/settings", methods=["POST"]) + def api_update_settings(): + from autometabuilder.data import persist_env_vars + payload = request.get_json(force=True) + env_vars = payload.get("env_vars", {}) + persist_env_vars(env_vars) + return jsonify({"status": "saved"}), 200 + + # Store in runtime context and return + runtime.context["settings_bp"] = settings_bp + return {"result": settings_bp, "blueprint_path": "settings_bp"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_translations/package.json b/backend/autometabuilder/workflow/plugins/web/web_route_translations/package.json new file mode 100644 index 0000000..ecac83f --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_translations/package.json @@ -0,0 +1,13 @@ +{ + "name": "@autometabuilder/web_route_translations", + "version": "1.0.0", + "description": "Flask blueprint for translation API routes", + "author": "AutoMetabuilder", + "license": "MIT", + "keywords": ["web", "workflow", "plugin", "flask", "routes"], + "main": "web_route_translations.py", + "metadata": { + "plugin_type": "web.route_translations", + "category": "web" + } +} diff --git a/backend/autometabuilder/workflow/plugins/web/web_route_translations/web_route_translations.py b/backend/autometabuilder/workflow/plugins/web/web_route_translations/web_route_translations.py new file mode 100644 index 0000000..ce925b1 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_route_translations/web_route_translations.py @@ -0,0 +1,51 @@ +"""Workflow plugin: translation API routes blueprint.""" +from flask import Blueprint, jsonify, request +from autometabuilder.loaders.metadata_loader import load_metadata + + +def run(runtime, _inputs): + """Create and return the translations routes blueprint.""" + translations_bp = Blueprint("translations", __name__) + + @translations_bp.route("/api/translation-options") + def api_translation_options(): + from autometabuilder.data import list_translations + return jsonify({"translations": list_translations()}), 200 + + @translations_bp.route("/api/translations", methods=["POST"]) + def api_create_translation(): + from autometabuilder.data import create_translation + payload = request.get_json(force=True) + lang = payload.get("lang") + if not lang: + return jsonify({"error": "lang required"}), 400 + ok = create_translation(lang) + return jsonify({"created": ok}), (201 if ok else 400) + + @translations_bp.route("/api/translations/", methods=["GET"]) + def api_get_translation(lang): + from autometabuilder.data import load_translation + if lang not in load_metadata().get("messages", {}): + return jsonify({"error": "translation not found"}), 404 + return jsonify({"lang": lang, "content": load_translation(lang)}), 200 + + @translations_bp.route("/api/translations/", methods=["PUT"]) + def api_update_translation(lang): + from autometabuilder.data import update_translation + payload = request.get_json(force=True) + updated = update_translation(lang, payload) + if not updated: + return jsonify({"error": "unable to update"}), 400 + return jsonify({"status": "saved"}), 200 + + @translations_bp.route("/api/translations/", methods=["DELETE"]) + def api_delete_translation(lang): + from autometabuilder.data import delete_translation + deleted = delete_translation(lang) + if not deleted: + return jsonify({"error": "cannot delete"}), 400 + return jsonify({"deleted": True}), 200 + + # Store in runtime context and return + runtime.context["translations_bp"] = translations_bp + return {"result": translations_bp, "blueprint_path": "translations_bp"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_summarize_workflow_packages/web_summarize_workflow_packages.py b/backend/autometabuilder/workflow/plugins/web/web_summarize_workflow_packages/web_summarize_workflow_packages.py index f70ac53..7ff957f 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_summarize_workflow_packages/web_summarize_workflow_packages.py +++ b/backend/autometabuilder/workflow/plugins/web/web_summarize_workflow_packages/web_summarize_workflow_packages.py @@ -1,9 +1,20 @@ """Workflow plugin: summarize workflow packages.""" -from ....data.workflow import summarize_workflow_packages def run(_runtime, inputs): """Summarize workflow packages.""" packages = inputs.get("packages", []) - summary = summarize_workflow_packages(packages) + + summary = [] + for pkg in packages: + 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 {"result": summary} diff --git a/backend/autometabuilder/workflow/plugins/web/web_update_translation/web_update_translation.py b/backend/autometabuilder/workflow/plugins/web/web_update_translation/web_update_translation.py index c7ba225..ef50d5f 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_update_translation/web_update_translation.py +++ b/backend/autometabuilder/workflow/plugins/web/web_update_translation/web_update_translation.py @@ -1,5 +1,7 @@ """Workflow plugin: update translation.""" -from ....data.translations import update_translation +import json +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, inputs): @@ -10,5 +12,57 @@ def run(_runtime, inputs): if not lang: return {"error": "lang is required"} - updated = update_translation(lang, payload) - return {"result": updated} + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder + + # Helper to read JSON + def read_json(path_obj): + if not path_obj.exists(): + return {} + try: + return json.loads(path_obj.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + # Load metadata + metadata_base = read_json(package_root / "metadata.json") + messages_map = metadata_base.get("messages", {}) + + # Check if translation exists + if lang not in messages_map: + return {"result": False} + + payload_content = payload.get("content", {}) + target_path = package_root / messages_map[lang] + + # Write based on whether it's a directory or file + if target_path.is_dir(): + # Group messages by prefix for directory structure + target_path.mkdir(parents=True, exist_ok=True) + + grouped = {} + for key, value in payload_content.items(): + parts = key.split(".") + group = ".".join(parts[:2]) if len(parts) >= 2 else "root" + grouped.setdefault(group, {})[key] = value + + # Remove old files not in desired set + existing = {path.stem for path in target_path.glob("*.json")} + desired = set(grouped.keys()) + for name in existing - desired: + (target_path / f"{name}.json").unlink() + + # Write grouped files + for name, entries in grouped.items(): + file_path = target_path / f"{name}.json" + file_path.write_text( + json.dumps(entries, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8" + ) + else: + # Write as single file + target_path.write_text( + json.dumps(payload_content, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8" + ) + + return {"result": True} diff --git a/backend/autometabuilder/workflow/plugins/web/web_write_messages_dir/web_write_messages_dir.py b/backend/autometabuilder/workflow/plugins/web/web_write_messages_dir/web_write_messages_dir.py index 3eb9518..fc5c971 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_write_messages_dir/web_write_messages_dir.py +++ b/backend/autometabuilder/workflow/plugins/web/web_write_messages_dir/web_write_messages_dir.py @@ -1,6 +1,6 @@ """Workflow plugin: write messages directory.""" +import json from pathlib import Path -from ....data.messages_io import write_messages_dir def run(_runtime, inputs): @@ -11,5 +11,28 @@ def run(_runtime, inputs): if not base_dir: return {"error": "base_dir is required"} - write_messages_dir(Path(base_dir), payload_content) + base_dir_path = Path(base_dir) + base_dir_path.mkdir(parents=True, exist_ok=True) + + # Group messages by prefix + grouped = {} + for key, value in payload_content.items(): + parts = key.split(".") + group = ".".join(parts[:2]) if len(parts) >= 2 else "root" + grouped.setdefault(group, {})[key] = value + + # Remove old files not in desired set + existing = {path.stem for path in base_dir_path.glob("*.json")} + desired = set(grouped.keys()) + for name in existing - desired: + (base_dir_path / f"{name}.json").unlink() + + # Write grouped files + for name, entries in grouped.items(): + target_path = base_dir_path / f"{name}.json" + target_path.write_text( + json.dumps(entries, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8" + ) + return {"result": "Messages written successfully"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_write_prompt/web_write_prompt.py b/backend/autometabuilder/workflow/plugins/web/web_write_prompt/web_write_prompt.py index 8e7fd9a..7a3ffc4 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_write_prompt/web_write_prompt.py +++ b/backend/autometabuilder/workflow/plugins/web/web_write_prompt/web_write_prompt.py @@ -1,9 +1,11 @@ """Workflow plugin: write prompt.""" -from ....data.prompt import write_prompt +import os +from pathlib import Path def run(_runtime, inputs): """Write prompt content to file.""" content = inputs.get("content", "") - write_prompt(content) + path = Path(os.environ.get("PROMPT_PATH", "prompt.yml")) + path.write_text(content or "", encoding="utf-8") return {"result": "Prompt written successfully"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_write_workflow/web_write_workflow.py b/backend/autometabuilder/workflow/plugins/web/web_write_workflow/web_write_workflow.py index b65b813..6372efb 100644 --- a/backend/autometabuilder/workflow/plugins/web/web_write_workflow/web_write_workflow.py +++ b/backend/autometabuilder/workflow/plugins/web/web_write_workflow/web_write_workflow.py @@ -1,9 +1,15 @@ """Workflow plugin: write workflow.""" -from ....data.workflow import write_workflow +from pathlib import Path +from autometabuilder.loaders.metadata_loader import load_metadata def run(_runtime, inputs): """Write workflow content to file.""" + package_root = Path(__file__).resolve().parents[5] # backend/autometabuilder content = inputs.get("content", "") - write_workflow(content) + metadata = load_metadata() + workflow_name = metadata.get("workflow_path", "workflow.json") + workflow_path = package_root / workflow_name + workflow_path.write_text(content or "", encoding="utf-8") + return {"result": "Workflow written successfully"}