From e4ac69588a7d1338571deeb1d8f2a80e7f9dbb5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 18:01:08 +0000 Subject: [PATCH] Migrate web/ to data/, update imports in plugins Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- backend/autometabuilder/data/__init__.py | 30 ++++++ backend/autometabuilder/data/env.py | 29 ++++++ backend/autometabuilder/data/json_utils.py | 14 +++ backend/autometabuilder/data/logs.py | 11 +++ backend/autometabuilder/data/messages_io.py | 46 +++++++++ backend/autometabuilder/data/metadata.py | 27 +++++ backend/autometabuilder/data/navigation.py | 14 +++ .../data/navigation_items.json | 32 ++++++ .../autometabuilder/data/package_loader.py | 74 ++++++++++++++ backend/autometabuilder/data/paths.py | 7 ++ backend/autometabuilder/data/prompt.py | 36 +++++++ .../autometabuilder/data/routes/context.py | 62 ++++++++++++ .../autometabuilder/data/routes/navigation.py | 39 ++++++++ backend/autometabuilder/data/routes/prompt.py | 30 ++++++ backend/autometabuilder/data/routes/run.py | 19 ++++ .../autometabuilder/data/routes/settings.py | 16 +++ .../data/routes/translations.py | 47 +++++++++ backend/autometabuilder/data/run_state.py | 64 ++++++++++++ backend/autometabuilder/data/server.py | 25 +++++ backend/autometabuilder/data/translations.py | 99 +++++++++++++++++++ backend/autometabuilder/data/ui_assets.json | 43 ++++++++ backend/autometabuilder/data/workflow.py | 53 ++++++++++ .../autometabuilder/data/workflow_graph.py | 89 +++++++++++++++++ .../web_build_prompt_yaml.py | 2 +- .../web_create_translation.py | 2 +- .../web_delete_translation.py | 2 +- .../web/web_get_env_vars/web_get_env_vars.py | 2 +- .../web_get_navigation_items.py | 2 +- .../web_get_prompt_content.py | 2 +- .../web_get_recent_logs.py | 2 +- .../web_get_ui_messages.py | 2 +- .../web_get_workflow_content.py | 2 +- .../web_list_translations.py | 2 +- .../web_load_messages/web_load_messages.py | 2 +- .../web_load_translation.py | 2 +- .../web_load_workflow_packages.py | 2 +- .../web_persist_env_vars.py | 2 +- .../web/web_read_json/web_read_json.py | 2 +- .../web_summarize_workflow_packages.py | 2 +- .../web_update_translation.py | 2 +- .../web_write_messages_dir.py | 2 +- .../web/web_write_prompt/web_write_prompt.py | 2 +- .../web_write_workflow/web_write_workflow.py | 2 +- 43 files changed, 926 insertions(+), 20 deletions(-) create mode 100644 backend/autometabuilder/data/__init__.py create mode 100644 backend/autometabuilder/data/env.py create mode 100644 backend/autometabuilder/data/json_utils.py create mode 100644 backend/autometabuilder/data/logs.py create mode 100644 backend/autometabuilder/data/messages_io.py create mode 100644 backend/autometabuilder/data/metadata.py create mode 100644 backend/autometabuilder/data/navigation.py create mode 100644 backend/autometabuilder/data/navigation_items.json create mode 100644 backend/autometabuilder/data/package_loader.py create mode 100644 backend/autometabuilder/data/paths.py create mode 100644 backend/autometabuilder/data/prompt.py create mode 100644 backend/autometabuilder/data/routes/context.py create mode 100644 backend/autometabuilder/data/routes/navigation.py create mode 100644 backend/autometabuilder/data/routes/prompt.py create mode 100644 backend/autometabuilder/data/routes/run.py create mode 100644 backend/autometabuilder/data/routes/settings.py create mode 100644 backend/autometabuilder/data/routes/translations.py create mode 100644 backend/autometabuilder/data/run_state.py create mode 100644 backend/autometabuilder/data/server.py create mode 100644 backend/autometabuilder/data/translations.py create mode 100644 backend/autometabuilder/data/ui_assets.json create mode 100644 backend/autometabuilder/data/workflow.py create mode 100644 backend/autometabuilder/data/workflow_graph.py diff --git a/backend/autometabuilder/data/__init__.py b/backend/autometabuilder/data/__init__.py new file mode 100644 index 0000000..b9fa019 --- /dev/null +++ b/backend/autometabuilder/data/__init__.py @@ -0,0 +1,30 @@ +"""Web module: Flask HTTP server and REST API backend. + +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. +""" diff --git a/backend/autometabuilder/data/env.py b/backend/autometabuilder/data/env.py new file mode 100644 index 0000000..f61f80e --- /dev/null +++ b/backend/autometabuilder/data/env.py @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..30e532a --- /dev/null +++ b/backend/autometabuilder/data/json_utils.py @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..c4e9787 --- /dev/null +++ b/backend/autometabuilder/data/logs.py @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..f46e8e3 --- /dev/null +++ b/backend/autometabuilder/data/messages_io.py @@ -0,0 +1,46 @@ +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 new file mode 100644 index 0000000..a8dc698 --- /dev/null +++ b/backend/autometabuilder/data/metadata.py @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..965ab79 --- /dev/null +++ b/backend/autometabuilder/data/navigation.py @@ -0,0 +1,14 @@ +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/navigation_items.json b/backend/autometabuilder/data/navigation_items.json new file mode 100644 index 0000000..df8865c --- /dev/null +++ b/backend/autometabuilder/data/navigation_items.json @@ -0,0 +1,32 @@ +[ + { + "section": "dashboard", + "icon": "speedometer2", + "label_key": "ui.nav.dashboard", + "default_label": "Dashboard" + }, + { + "section": "workflow", + "icon": "diagram-3", + "label_key": "ui.nav.workflow", + "default_label": "Workflow" + }, + { + "section": "prompt", + "icon": "file-text", + "label_key": "ui.nav.prompt", + "default_label": "Prompt" + }, + { + "section": "settings", + "icon": "gear", + "label_key": "ui.nav.settings", + "default_label": "Settings" + }, + { + "section": "translations", + "icon": "translate", + "label_key": "ui.nav.translations", + "default_label": "Translations" + } +] diff --git a/backend/autometabuilder/data/package_loader.py b/backend/autometabuilder/data/package_loader.py new file mode 100644 index 0000000..0e5dc12 --- /dev/null +++ b/backend/autometabuilder/data/package_loader.py @@ -0,0 +1,74 @@ +"""Load workflow packages from npm-style package directories.""" +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any, Dict, List + +from .json_utils import read_json + +logger = logging.getLogger(__name__) + + +def load_package(package_dir: Path) -> Dict[str, Any] | None: + """Load a single workflow package.""" + package_json = package_dir / "package.json" + if not package_json.exists(): + logger.warning("Package %s missing package.json", package_dir.name) + return None + + # Read package.json + pkg_data = read_json(package_json) + if not isinstance(pkg_data, dict): + logger.warning("Invalid package.json in %s", package_dir.name) + return None + + # Read workflow file + workflow_file = pkg_data.get("main", "workflow.json") + workflow_path = package_dir / workflow_file + + if not workflow_path.exists(): + logger.warning("Workflow file %s not found in %s", workflow_file, package_dir.name) + return None + + workflow_data = read_json(workflow_path) + if not isinstance(workflow_data, dict): + logger.warning("Invalid workflow in %s", package_dir.name) + return None + + # Combine package metadata with workflow + metadata = pkg_data.get("metadata", {}) + + return { + "id": pkg_data.get("name", package_dir.name), + "name": pkg_data.get("name", package_dir.name), + "version": pkg_data.get("version", "1.0.0"), + "description": pkg_data.get("description", ""), + "author": pkg_data.get("author", ""), + "license": pkg_data.get("license", ""), + "keywords": pkg_data.get("keywords", []), + "label": metadata.get("label", package_dir.name), + "tags": metadata.get("tags", []), + "icon": metadata.get("icon", "workflow"), + "category": metadata.get("category", "templates"), + "workflow": workflow_data, + } + + +def load_all_packages(packages_dir: Path) -> List[Dict[str, Any]]: + """Load all workflow packages from directory.""" + if not packages_dir.exists(): + logger.warning("Packages directory not found: %s", packages_dir) + return [] + + packages = [] + for item in sorted(packages_dir.iterdir()): + if not item.is_dir(): + continue + + package = load_package(item) + if package: + packages.append(package) + + logger.debug("Loaded %d workflow packages", len(packages)) + return packages diff --git a/backend/autometabuilder/data/paths.py b/backend/autometabuilder/data/paths.py new file mode 100644 index 0000000..b9f2963 --- /dev/null +++ b/backend/autometabuilder/data/paths.py @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..19a5de1 --- /dev/null +++ b/backend/autometabuilder/data/prompt.py @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000..090315e --- /dev/null +++ b/backend/autometabuilder/data/routes/context.py @@ -0,0 +1,62 @@ +"""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 new file mode 100644 index 0000000..89481f2 --- /dev/null +++ b/backend/autometabuilder/data/routes/navigation.py @@ -0,0 +1,39 @@ +"""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 new file mode 100644 index 0000000..53563d5 --- /dev/null +++ b/backend/autometabuilder/data/routes/prompt.py @@ -0,0 +1,30 @@ +"""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 new file mode 100644 index 0000000..968edc8 --- /dev/null +++ b/backend/autometabuilder/data/routes/run.py @@ -0,0 +1,19 @@ +"""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 new file mode 100644 index 0000000..4f344c9 --- /dev/null +++ b/backend/autometabuilder/data/routes/settings.py @@ -0,0 +1,16 @@ +"""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 new file mode 100644 index 0000000..9304c73 --- /dev/null +++ b/backend/autometabuilder/data/routes/translations.py @@ -0,0 +1,47 @@ +"""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/run_state.py b/backend/autometabuilder/data/run_state.py new file mode 100644 index 0000000..dc4e0ae --- /dev/null +++ b/backend/autometabuilder/data/run_state.py @@ -0,0 +1,64 @@ +"""Run state helpers for long-lived bot executions.""" +from __future__ import annotations + +import os +import subprocess +import sys +import threading +import time +from typing import Dict + +from ..roadmap_utils import is_mvp_reached + +bot_process = None +mock_running = False +current_run_config: Dict[str, object] = {} + + +def _reset_run_state() -> None: + global bot_process, current_run_config + bot_process = None + current_run_config = {} + + +def run_bot_task(mode: str, iterations: int, yolo: bool, stop_at_mvp: bool) -> None: + global bot_process, mock_running, current_run_config + current_run_config = { + "mode": mode, + "iterations": iterations, + "yolo": yolo, + "stop_at_mvp": stop_at_mvp, + } + + if os.environ.get("MOCK_WEB_UI") == "true": + mock_running = True + time.sleep(5) + mock_running = False + _reset_run_state() + return + + try: + cmd = [sys.executable, "-m", "autometabuilder.main"] + if yolo: + cmd.append("--yolo") + if mode == "once": + cmd.append("--once") + if mode == "iterations" and iterations > 1: + for _ in range(iterations): + if stop_at_mvp and is_mvp_reached(): + break + bot_process = subprocess.Popen(cmd + ["--once"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + bot_process.wait() + else: + bot_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + bot_process.wait() + finally: + _reset_run_state() + + +def start_bot(mode: str = "once", iterations: int = 1, yolo: bool = True, stop_at_mvp: bool = False) -> bool: + if bot_process is not None or mock_running: + return False + thread = threading.Thread(target=run_bot_task, args=(mode, iterations, yolo, stop_at_mvp), daemon=True) + thread.start() + return True diff --git a/backend/autometabuilder/data/server.py b/backend/autometabuilder/data/server.py new file mode 100644 index 0000000..d4c7fc2 --- /dev/null +++ b/backend/autometabuilder/data/server.py @@ -0,0 +1,25 @@ +"""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 new file mode 100644 index 0000000..4440d74 --- /dev/null +++ b/backend/autometabuilder/data/translations.py @@ -0,0 +1,99 @@ +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/ui_assets.json b/backend/autometabuilder/data/ui_assets.json new file mode 100644 index 0000000..fd7869f --- /dev/null +++ b/backend/autometabuilder/data/ui_assets.json @@ -0,0 +1,43 @@ +{ + "core_styles": [ + "/static/css/main.css", + "/static/css/workflow_palette.css" + ], + "core_scripts": [ + "/static/js/app_context.js", + "/static/js/plugin_registry.js", + "/static/js/services/toast.js", + "/static/js/plugins/theme_manager.js", + "/static/js/plugins/navigation_manager.js", + "/static/js/plugins/choices_manager.js", + "/static/js/plugins/workflow_toggle.js", + "/static/js/plugins/form_validator.js", + "/static/js/plugins/status_poller.js" + ], + "workflow_scripts": [ + "/static/js/workflow/workflow_utils.js", + "/static/js/workflow/workflow_state.js", + "/static/js/workflow/workflow_mutations.js", + "/static/js/workflow/workflow_palette.js", + "/static/js/workflow/workflow_plugin_options.js", + "/static/js/workflow/workflow_field_renderer.js", + "/static/js/workflow/workflow_node_template.js", + "/static/js/workflow/workflow_node_events.js", + "/static/js/workflow/workflow_loop_renderer.js", + "/static/js/workflow/workflow_node_renderer.js", + "/static/js/workflow/workflow_canvas_renderer.js", + "/static/js/workflow/workflow_builder.js" + ], + "page_scripts": [ + "/static/js/plugins/navigation_loader.js", + "/static/js/plugins/workflow_builder.js", + "/static/js/plugins/workflow_palette.js", + "/static/js/plugins/workflow_templates.js", + "/static/js/plugins/run_mode_toggle.js", + "/static/js/plugins/prompt_builder.js", + "/static/js/plugins/translation_editor_base.js", + "/static/js/plugins/translation_editor_render.js", + "/static/js/plugins/translation_editor_actions.js", + "/static/js/plugins/translation_editor_network.js" + ] +} diff --git a/backend/autometabuilder/data/workflow.py b/backend/autometabuilder/data/workflow.py new file mode 100644 index 0000000..e407172 --- /dev/null +++ b/backend/autometabuilder/data/workflow.py @@ -0,0 +1,53 @@ +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/data/workflow_graph.py b/backend/autometabuilder/data/workflow_graph.py new file mode 100644 index 0000000..2408fb2 --- /dev/null +++ b/backend/autometabuilder/data/workflow_graph.py @@ -0,0 +1,89 @@ +"""Build a node/edge view of n8n workflows for visualization.""" +from __future__ import annotations + +import json +import logging +from typing import Any, Dict, Iterable, List + +from autometabuilder.data import get_workflow_content, load_metadata + +logger = logging.getLogger(__name__) + + +def _parse_workflow_definition() -> Dict[str, Any]: + payload = get_workflow_content() + if not payload: + return {"name": "Empty", "nodes": [], "connections": {}} + try: + parsed = json.loads(payload) + except json.JSONDecodeError as exc: + logger.warning("Invalid workflow JSON: %s", exc) + return {"name": "Invalid", "nodes": [], "connections": {}} + return parsed if isinstance(parsed, dict) else {"name": "Invalid", "nodes": [], "connections": {}} + + +def _gather_n8n_nodes( + nodes: Iterable[Dict[str, Any]], + plugin_map: Dict[str, Any] +) -> List[Dict[str, Any]]: + """Extract nodes from n8n format.""" + collected = [] + for node in nodes: + node_id = node.get("id", node.get("name", f"node-{len(collected)}")) + node_type = node.get("type", "unknown") + metadata = plugin_map.get(node_type, {}) + + collected.append({ + "id": node_id, + "name": node.get("name", node_id), + "type": node_type, + "label_key": metadata.get("label"), + "parent": None, + "position": node.get("position", [0, 0]), + }) + return collected + + +def _build_n8n_edges( + connections: Dict[str, Any], + nodes: List[Dict[str, Any]] +) -> List[Dict[str, str]]: + """Build edges from n8n connections.""" + # Build name to ID mapping + name_to_id = {node["name"]: node["id"] for node in nodes} + + edges = [] + for source_name, outputs in connections.items(): + source_id = name_to_id.get(source_name, source_name) + + for output_type, indices in outputs.items(): + for index, targets in indices.items(): + for target in targets: + target_name = target["node"] + target_id = name_to_id.get(target_name, target_name) + + edges.append({ + "from": source_id, + "to": target_id, + "type": target.get("type", "main"), + "output_index": index, + "input_index": target.get("index", 0), + }) + return edges + + +def build_workflow_graph() -> Dict[str, Any]: + """Build workflow graph from n8n format (breaking change: legacy format removed).""" + definition = _parse_workflow_definition() + plugin_map = load_metadata().get("workflow_plugins", {}) + + # Only support n8n format now + nodes = _gather_n8n_nodes(definition.get("nodes", []), plugin_map) + edges = _build_n8n_edges(definition.get("connections", {}), nodes) + + logger.debug("Built workflow graph with %d nodes and %d edges", len(nodes), len(edges)) + return { + "nodes": nodes, + "edges": edges, + "count": {"nodes": len(nodes), "edges": len(edges)}, + } 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 3383e18..9486d8c 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,5 @@ """Workflow plugin: build prompt YAML.""" -from ....web.data.prompt import build_prompt_yaml +from ....data.prompt import build_prompt_yaml def run(_runtime, inputs): 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 9233182..bf557c2 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,5 @@ """Workflow plugin: create translation.""" -from ....web.data.translations import create_translation +from ....data.translations import create_translation def run(_runtime, inputs): 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 76e7eb0..e2a856a 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,5 @@ """Workflow plugin: delete translation.""" -from ....web.data.translations import delete_translation +from ....data.translations import delete_translation def run(_runtime, inputs): 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 10f6c9e..e4d24b0 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,5 +1,5 @@ """Workflow plugin: get environment variables.""" -from ....web.data.env import get_env_vars +from ....data.env import get_env_vars def run(_runtime, _inputs): 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 b663727..b701411 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,5 +1,5 @@ """Workflow plugin: get navigation items.""" -from ....web.data.navigation import get_navigation_items +from ....data.navigation import get_navigation_items def run(_runtime, _inputs): 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 5cdf72f..9d4ac87 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,5 +1,5 @@ """Workflow plugin: get prompt content.""" -from ....web.data.prompt import get_prompt_content +from ....data.prompt import get_prompt_content def run(_runtime, _inputs): 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 979902b..984fa02 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,5 +1,5 @@ """Workflow plugin: get recent logs.""" -from ....web.data.logs import get_recent_logs +from ....data.logs import get_recent_logs def run(_runtime, inputs): 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 8e761a1..7a9299b 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,5 @@ """Workflow plugin: get UI messages.""" -from ....web.data.translations import get_ui_messages +from ....data.translations import get_ui_messages def run(_runtime, inputs): 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 9b53ade..fa06f5f 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,5 +1,5 @@ """Workflow plugin: get workflow content.""" -from ....web.data.workflow import get_workflow_content +from ....data.workflow import get_workflow_content def run(_runtime, _inputs): 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 2bdc80f..6168cfb 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,5 +1,5 @@ """Workflow plugin: list translations.""" -from ....web.data.translations import list_translations +from ....data.translations import list_translations def run(_runtime, _inputs): 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 83e8993..c9f96b0 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.""" from pathlib import Path -from ....web.data.messages_io import load_messages +from ....data.messages_io import load_messages def run(_runtime, inputs): 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 99088ac..56e5b02 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,5 +1,5 @@ """Workflow plugin: load translation.""" -from ....web.data.translations import load_translation +from ....data.translations import load_translation def run(_runtime, inputs): 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 b6e1d9b..0633df3 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,5 +1,5 @@ """Workflow plugin: load workflow packages.""" -from ....web.data.workflow import load_workflow_packages +from ....data.workflow import load_workflow_packages def run(_runtime, _inputs): 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 22c5886..444a023 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,5 +1,5 @@ """Workflow plugin: persist environment variables.""" -from ....web.data.env import persist_env_vars +from ....data.env import persist_env_vars def run(_runtime, inputs): 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 8ca1bdf..4d05f8a 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.""" from pathlib import Path -from ....web.data.json_utils import read_json +from ....data.json_utils import read_json def run(_runtime, inputs): 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 4f6e4a3..f70ac53 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,5 +1,5 @@ """Workflow plugin: summarize workflow packages.""" -from ....web.data.workflow import summarize_workflow_packages +from ....data.workflow import summarize_workflow_packages def run(_runtime, inputs): 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 5c995a9..c7ba225 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,5 @@ """Workflow plugin: update translation.""" -from ....web.data.translations import update_translation +from ....data.translations import update_translation def run(_runtime, inputs): 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 4d02855..3eb9518 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.""" from pathlib import Path -from ....web.data.messages_io import write_messages_dir +from ....data.messages_io import write_messages_dir def run(_runtime, inputs): 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 12f56e6..8e7fd9a 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,5 +1,5 @@ """Workflow plugin: write prompt.""" -from ....web.data.prompt import write_prompt +from ....data.prompt import write_prompt def run(_runtime, inputs): 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 a9dba0c..b65b813 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,5 +1,5 @@ """Workflow plugin: write workflow.""" -from ....web.data.workflow import write_workflow +from ....data.workflow import write_workflow def run(_runtime, inputs):