mirror of
https://github.com/johndoe6345789/AutoMetabuilder.git
synced 2026-04-24 22:04:58 +00:00
Introduce AutoMetabuilder core components and workflow packages:
- Implement core components: CLI argument parsing, environment loading, GitHub service creation, and logging configuration. - Add support for OpenAI client setup and model resolution. - Develop SDLC context loader from GitHub and repository files. - Implement workflow context and engine builders. - Introduce major workflow packages: `game_tick_loop` and `contextual_iterative_loop`. - Update localization files with new package descriptions and labels. - Streamline web navigation by loading items from a dedicated JSON file.
This commit is contained in:
Binary file not shown.
@@ -21,6 +21,7 @@ from .workflow_engine_builder import build_workflow_engine
|
||||
|
||||
|
||||
def run_app() -> None:
|
||||
"""Run the AutoMetabuilder CLI."""
|
||||
load_env()
|
||||
configure_logging()
|
||||
logger = logging.getLogger("autometabuilder")
|
||||
@@ -43,23 +44,21 @@ def run_app() -> None:
|
||||
metadata = load_metadata()
|
||||
tools = load_tools(metadata)
|
||||
|
||||
registry_entries = load_tool_registry()
|
||||
tool_map = build_tool_map(gh, registry_entries)
|
||||
tool_map = build_tool_map(gh, load_tool_registry())
|
||||
load_plugins(tool_map, tools)
|
||||
|
||||
tool_policies = load_tool_policies()
|
||||
workflow_config = load_workflow_config(metadata)
|
||||
workflow_context = build_workflow_context(
|
||||
args,
|
||||
gh,
|
||||
msgs,
|
||||
client,
|
||||
tools,
|
||||
tool_map,
|
||||
prompt,
|
||||
tool_policies
|
||||
)
|
||||
context_parts = {
|
||||
"args": args,
|
||||
"gh": gh,
|
||||
"msgs": msgs,
|
||||
"client": client,
|
||||
"tools": tools,
|
||||
"tool_map": tool_map,
|
||||
"prompt": prompt,
|
||||
"tool_policies": load_tool_policies()
|
||||
}
|
||||
workflow_context = build_workflow_context(context_parts)
|
||||
logger.debug("Workflow context ready with %s tools", len(tool_map))
|
||||
|
||||
engine = build_workflow_engine(workflow_config, workflow_context, logger)
|
||||
engine = build_workflow_engine(load_workflow_config(metadata), workflow_context, logger)
|
||||
engine.execute()
|
||||
|
||||
@@ -3,6 +3,7 @@ import importlib
|
||||
|
||||
|
||||
def load_callable(path: str):
|
||||
"""Import and return a callable."""
|
||||
module_path, attr = path.rsplit(".", 1)
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, attr)
|
||||
|
||||
@@ -3,9 +3,22 @@ import argparse
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse CLI arguments."""
|
||||
parser = argparse.ArgumentParser(description="AutoMetabuilder: AI-driven SDLC assistant.")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Do not execute state-modifying tools.")
|
||||
parser.add_argument("--yolo", action="store_true", help="Execute tools without confirmation.")
|
||||
parser.add_argument("--once", action="store_true", help="Run a single full iteration (AI -> Tool -> AI).")
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Do not execute state-modifying tools."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yolo",
|
||||
action="store_true",
|
||||
help="Execute tools without confirmation."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--once",
|
||||
action="store_true",
|
||||
help="Run a single full iteration (AI -> Tool -> AI)."
|
||||
)
|
||||
parser.add_argument("--web", action="store_true", help="Start the Web UI.")
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -7,6 +7,7 @@ logger = logging.getLogger("autometabuilder")
|
||||
|
||||
|
||||
def get_sdlc_context(gh: GitHubIntegration, msgs: dict) -> str:
|
||||
"""Return SDLC context text from roadmap, issues, and PRs."""
|
||||
sdlc_context = ""
|
||||
if os.path.exists("ROADMAP.md"):
|
||||
with open("ROADMAP.md", "r", encoding="utf-8") as f:
|
||||
|
||||
@@ -3,4 +3,5 @@ from dotenv import load_dotenv
|
||||
|
||||
|
||||
def load_env() -> None:
|
||||
"""Load environment variables."""
|
||||
load_dotenv()
|
||||
|
||||
@@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder")
|
||||
|
||||
|
||||
def create_github_integration(token: str, msgs: dict):
|
||||
"""Create GitHub integration if possible."""
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
|
||||
@@ -40,10 +40,18 @@
|
||||
"description": "meta.settings.log_level.description",
|
||||
"type": "select",
|
||||
"options": [
|
||||
"TRACE",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"WARNING",
|
||||
"ERROR"
|
||||
],
|
||||
"option_labels": [
|
||||
"meta.settings.log_level.options.trace",
|
||||
"meta.settings.log_level.options.debug",
|
||||
"meta.settings.log_level.options.info",
|
||||
"meta.settings.log_level.options.warning",
|
||||
"meta.settings.log_level.options.error"
|
||||
]
|
||||
},
|
||||
"APP_LANG": {
|
||||
@@ -56,6 +64,13 @@
|
||||
"fr",
|
||||
"nl",
|
||||
"pirate"
|
||||
],
|
||||
"option_labels": [
|
||||
"meta.settings.app_lang.options.en",
|
||||
"meta.settings.app_lang.options.es",
|
||||
"meta.settings.app_lang.options.fr",
|
||||
"meta.settings.app_lang.options.nl",
|
||||
"meta.settings.app_lang.options.pirate"
|
||||
]
|
||||
},
|
||||
"PROMPT_PATH": {
|
||||
@@ -99,6 +114,7 @@
|
||||
"WEB_PASSWORD"
|
||||
],
|
||||
"env_values": [
|
||||
"TRACE",
|
||||
"INFO",
|
||||
"DEBUG",
|
||||
"WARNING",
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
|
||||
def load_metadata() -> dict:
|
||||
"""Load metadata.json."""
|
||||
metadata_path = os.path.join(os.path.dirname(__file__), "metadata.json")
|
||||
with open(metadata_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
@@ -5,4 +5,5 @@ DEFAULT_MODEL = "openai/gpt-4o"
|
||||
|
||||
|
||||
def resolve_model_name(prompt: dict) -> str:
|
||||
"""Resolve model name from env or prompt."""
|
||||
return os.environ.get("LLM_MODEL", prompt.get("model", DEFAULT_MODEL))
|
||||
|
||||
@@ -4,6 +4,7 @@ from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
|
||||
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
|
||||
def get_completion(client, model, messages, tools):
|
||||
"""Request a chat completion with retries."""
|
||||
return client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
|
||||
@@ -6,6 +6,7 @@ DEFAULT_ENDPOINT = "https://models.github.ai/inference"
|
||||
|
||||
|
||||
def create_openai_client(token: str):
|
||||
"""Create an OpenAI client."""
|
||||
return OpenAI(
|
||||
base_url=os.environ.get("GITHUB_MODELS_ENDPOINT", DEFAULT_ENDPOINT),
|
||||
api_key=token,
|
||||
|
||||
@@ -8,6 +8,7 @@ logger = logging.getLogger("autometabuilder")
|
||||
|
||||
|
||||
def load_plugins(tool_map: dict, tools: list) -> None:
|
||||
"""Load plugin tools and append metadata."""
|
||||
plugins_dir = os.path.join(os.path.dirname(__file__), "plugins")
|
||||
if not os.path.exists(plugins_dir):
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ from .callable_loader import load_callable
|
||||
|
||||
|
||||
def build_tool_map(gh, registry_entries: list) -> dict:
|
||||
"""Build tool name to callable map."""
|
||||
tool_map = {}
|
||||
for entry in registry_entries:
|
||||
name = entry.get("name")
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
|
||||
def load_tool_policies() -> dict:
|
||||
"""Load tool policies JSON."""
|
||||
path = os.path.join(os.path.dirname(__file__), "tool_policies.json")
|
||||
if not os.path.exists(path):
|
||||
return {"modifying_tools": []}
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
|
||||
def load_tool_registry() -> list:
|
||||
"""Load tool registry entries."""
|
||||
path = os.path.join(os.path.dirname(__file__), "tool_registry.json")
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Edit file content with search/replace."""
|
||||
|
||||
def edit_file(path: str, search: str, replace: str) -> str:
|
||||
"""Replace text in a file."""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
@@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder")
|
||||
|
||||
|
||||
def list_files(directory: str = ".") -> str:
|
||||
"""Return newline-separated files under a directory."""
|
||||
files_list = []
|
||||
for root, _, files in os.walk(directory):
|
||||
if ".git" in root or "__pycache__" in root or ".venv" in root:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Read file content."""
|
||||
|
||||
def read_file(path: str) -> str:
|
||||
"""Read file content."""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
@@ -4,5 +4,6 @@ from ..docker_utils import run_command_in_docker
|
||||
|
||||
|
||||
def run_docker_task(image: str, command: str, workdir: str = "/workspace") -> str:
|
||||
"""Run a command inside Docker."""
|
||||
volumes = {os.getcwd(): "/workspace"}
|
||||
return run_command_in_docker(image, command, volumes=volumes, workdir=workdir)
|
||||
|
||||
@@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder")
|
||||
|
||||
|
||||
def run_lint(path: str = "src") -> str:
|
||||
"""Run pylint on a path."""
|
||||
logger.info("Running linting in %s...", path)
|
||||
result = subprocess.run(["pylint", path], capture_output=True, text=True, check=False)
|
||||
logger.info(result.stdout)
|
||||
|
||||
@@ -6,6 +6,7 @@ logger = logging.getLogger("autometabuilder")
|
||||
|
||||
|
||||
def run_tests(path: str = "tests") -> str:
|
||||
"""Run pytest on a path."""
|
||||
logger.info("Running tests in %s...", path)
|
||||
result = subprocess.run(["pytest", path], capture_output=True, text=True, check=False)
|
||||
logger.info(result.stdout)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Write file content."""
|
||||
|
||||
def write_file(path: str, content: str) -> str:
|
||||
"""Write content to a file."""
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
|
||||
def load_tools(metadata: dict) -> list:
|
||||
"""Load tool specs from metadata reference."""
|
||||
tools_path = os.path.join(os.path.dirname(__file__), metadata.get("tools_path", "tools.json"))
|
||||
with open(tools_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* AutoMetabuilder - Workflow Palette Plugin
|
||||
*/
|
||||
(() => {
|
||||
const init = async () => {
|
||||
window.AMBWorkflowPalette?.init?.();
|
||||
};
|
||||
|
||||
window.AMBPlugins?.register('workflow_palette', init);
|
||||
})();
|
||||
@@ -36,14 +36,15 @@
|
||||
return candidate;
|
||||
};
|
||||
|
||||
const addNode = (targetArray, pluginDefinitions) => {
|
||||
const addNode = (targetArray, pluginDefinitions, typeOverride = '') => {
|
||||
const types = Object.keys(pluginDefinitions);
|
||||
const defaultType = types[0] || '';
|
||||
const nextType = pluginDefinitions[typeOverride] ? typeOverride : defaultType;
|
||||
const node = {
|
||||
id: generateNodeId(defaultType, targetArray),
|
||||
type: defaultType,
|
||||
inputs: buildDefaultFields(pluginDefinitions[defaultType]?.inputs),
|
||||
outputs: buildDefaultFields(pluginDefinitions[defaultType]?.outputs)
|
||||
id: generateNodeId(nextType, targetArray),
|
||||
type: nextType,
|
||||
inputs: buildDefaultFields(pluginDefinitions[nextType]?.inputs),
|
||||
outputs: buildDefaultFields(pluginDefinitions[nextType]?.outputs)
|
||||
};
|
||||
targetArray.push(node);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* AutoMetabuilder - Workflow Palette
|
||||
*/
|
||||
(() => {
|
||||
const state = {
|
||||
container: null,
|
||||
searchInput: null,
|
||||
list: null,
|
||||
filter: ''
|
||||
};
|
||||
|
||||
const groupOrder = ['core', 'tools', 'utils', 'control', 'other'];
|
||||
const groupLabelKeys = {
|
||||
core: 'ui.workflow.palette.group.core',
|
||||
tools: 'ui.workflow.palette.group.tools',
|
||||
utils: 'ui.workflow.palette.group.utils',
|
||||
control: 'ui.workflow.palette.group.control',
|
||||
other: 'ui.workflow.palette.group.other'
|
||||
};
|
||||
|
||||
const getGroupKey = (type) => (type || '').split('.')[0] || 'other';
|
||||
|
||||
const buildEntries = (definitions, t) => Object.entries(definitions || {}).map(([key, def]) => {
|
||||
const label = t?.(def.label || '', def.label || key) || key;
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
group: getGroupKey(key),
|
||||
search: `${key} ${label}`.toLowerCase()
|
||||
};
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
const { t, escapeHtml } = window.AMBWorkflowUtils || {};
|
||||
const definitions = window.AMBWorkflowState?.state?.pluginDefinitions || {};
|
||||
if (!state.list) return;
|
||||
const term = state.filter.toLowerCase();
|
||||
const entries = buildEntries(definitions, t).filter(entry => !term || entry.search.includes(term));
|
||||
if (!entries.length) {
|
||||
state.list.innerHTML = `<p class="text-muted small mb-0">${escapeHtml ? escapeHtml(t?.('ui.workflow.palette.empty', 'No matching nodes.')) : t?.('ui.workflow.palette.empty', 'No matching nodes.')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = entries.reduce((acc, entry) => {
|
||||
const group = groupOrder.includes(entry.group) ? entry.group : 'other';
|
||||
acc[group] = acc[group] || [];
|
||||
acc[group].push(entry);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const groupsHtml = groupOrder.filter(group => grouped[group]?.length).map(group => {
|
||||
const groupLabel = t?.(groupLabelKeys[group], group) || group;
|
||||
const items = grouped[group].map(entry => {
|
||||
const safeLabel = escapeHtml ? escapeHtml(entry.label) : entry.label;
|
||||
const safeKey = escapeHtml ? escapeHtml(entry.key) : entry.key;
|
||||
return `
|
||||
<button type="button" class="amb-workflow-palette-item" data-node-type="${safeKey}">
|
||||
<span class="amb-workflow-palette-title">${safeLabel}</span>
|
||||
<span class="amb-workflow-palette-subtitle">${safeKey}</span>
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
return `
|
||||
<div class="amb-workflow-palette-group">
|
||||
<div class="amb-workflow-palette-heading">${escapeHtml ? escapeHtml(groupLabel) : groupLabel}</div>
|
||||
<div class="amb-workflow-palette-items">${items}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
state.list.innerHTML = groupsHtml;
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
state.container = document.getElementById('workflow-palette');
|
||||
state.searchInput = document.getElementById('workflow-palette-search');
|
||||
state.list = document.getElementById('workflow-palette-list');
|
||||
if (!state.container || !state.searchInput || !state.list) return;
|
||||
|
||||
state.searchInput.addEventListener('input', (event) => {
|
||||
state.filter = event.target.value || '';
|
||||
render();
|
||||
});
|
||||
|
||||
state.list.addEventListener('click', (event) => {
|
||||
const target = event.target.closest('.amb-workflow-palette-item');
|
||||
if (!target) return;
|
||||
const nodeType = target.dataset.nodeType;
|
||||
const workflowState = window.AMBWorkflowState?.state;
|
||||
if (!workflowState || !nodeType) return;
|
||||
window.AMBWorkflowMutations?.addNode(workflowState.workflow.nodes, workflowState.pluginDefinitions, nodeType);
|
||||
window.AMBWorkflowCanvasRenderer?.render?.();
|
||||
});
|
||||
|
||||
render();
|
||||
};
|
||||
|
||||
window.AMBWorkflowPalette = { init, render };
|
||||
})();
|
||||
@@ -15,7 +15,10 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js@10.2.0/public/assets/styles/choices.min.css">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
{% set core_styles = ui_assets.get('core_styles', ['/static/css/main.css']) if ui_assets else ['/static/css/main.css'] %}
|
||||
{% for style in core_styles %}
|
||||
<link rel="stylesheet" href="{{ style }}">
|
||||
{% endfor %}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
{% set desc_label = desc.get('label', key) %}
|
||||
{% set desc_text = desc.get('description', '') %}
|
||||
{% set desc_placeholder = desc.get('placeholder', '') %}
|
||||
{% set option_labels = desc.get('option_labels', []) %}
|
||||
<div class="amb-form-group">
|
||||
<label class="amb-form-label">{{ t(desc_label, desc_label) }}</label>
|
||||
{% if desc_text %}
|
||||
@@ -20,7 +21,8 @@
|
||||
<select name="env_{{ key }}" class="form-select" data-choices>
|
||||
<option value="">{{ t('ui.common.select_placeholder', 'Select...') }}</option>
|
||||
{% for opt in desc.get('options', []) %}
|
||||
<option value="{{ opt }}" {% if env_vars.get(key) == opt %}selected{% endif %}>{{ opt }}</option>
|
||||
{% set opt_label = option_labels[loop.index0] if option_labels|length > loop.index0 else opt %}
|
||||
<option value="{{ opt }}" {% if env_vars.get(key) == opt %}selected{% endif %}>{{ t(opt_label, opt_label) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{# Workflow palette card macro. #}
|
||||
{% macro workflow_palette() -%}
|
||||
<div class="amb-card amb-workflow-palette" id="workflow-palette">
|
||||
<div class="amb-card-header">
|
||||
<h5><i class="bi bi-grid"></i> {{ t('ui.workflow.palette.title', 'Node Palette') }}</h5>
|
||||
</div>
|
||||
<div class="amb-card-body">
|
||||
<label class="form-label small text-muted" for="workflow-palette-search">{{ t('ui.workflow.palette.search_label', 'Find nodes') }}</label>
|
||||
<input type="search" id="workflow-palette-search" class="form-control form-control-sm"
|
||||
placeholder="{{ t('ui.workflow.palette.search_placeholder', 'Search nodes or keywords...') }}">
|
||||
<div id="workflow-palette-list" class="amb-workflow-palette-list">
|
||||
<p class="text-muted small mb-0">{{ t('ui.workflow.palette.loading', 'Loading nodes...') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
@@ -1,6 +1,7 @@
|
||||
{# Workflow section macro. #}
|
||||
{% from "components/atoms/section_header.html" import section_header %}
|
||||
{% from "components/organisms/workflow_templates.html" import workflow_templates with context %}
|
||||
{% from "components/organisms/workflow_palette.html" import workflow_palette with context %}
|
||||
|
||||
{% macro workflow_section() -%}
|
||||
<section id="workflow" class="amb-section">
|
||||
@@ -8,29 +9,36 @@
|
||||
|
||||
{{ workflow_templates() }}
|
||||
|
||||
<div class="amb-card">
|
||||
<div class="amb-card-header">
|
||||
<h5><i class="bi bi-diagram-3"></i> {{ t('ui.workflow.card.title', 'Tasks & Steps') }}</h5>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-workflow-toggle>
|
||||
<i class="bi bi-code-slash"></i> {{ t('ui.workflow.toggle_json', 'Toggle JSON') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">
|
||||
{{ workflow_palette() }}
|
||||
</div>
|
||||
<div class="amb-card-body">
|
||||
<div id="workflow-builder" class="mb-3">
|
||||
<button type="button" class="btn btn-primary" data-workflow-fallback>
|
||||
<i class="bi bi-plus-lg"></i> {{ t('ui.workflow.add_node', 'Add Node') }}
|
||||
</button>
|
||||
</div>
|
||||
<form action="/workflow" method="post" id="workflow-form">
|
||||
<textarea id="workflow-content" name="content" class="form-control amb-code-editor d-none" rows="15">{{ workflow_content }}</textarea>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-save"></i> {{ t('ui.workflow.save', 'Save Workflow') }}
|
||||
</button>
|
||||
<div class="col-lg-8">
|
||||
<div class="amb-card">
|
||||
<div class="amb-card-header">
|
||||
<h5><i class="bi bi-diagram-3"></i> {{ t('ui.workflow.card.title', 'Tasks & Steps') }}</h5>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-workflow-toggle>
|
||||
<i class="bi bi-code-slash"></i> {{ t('ui.workflow.toggle_json', 'Toggle JSON') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="amb-card-body">
|
||||
<div id="workflow-builder" class="mb-3">
|
||||
<button type="button" class="btn btn-primary" data-workflow-fallback>
|
||||
<i class="bi bi-plus-lg"></i> {{ t('ui.workflow.add_node', 'Add Node') }}
|
||||
</button>
|
||||
</div>
|
||||
<form action="/workflow" method="post" id="workflow-form">
|
||||
<textarea id="workflow-content" name="content" class="form-control amb-code-editor d-none" rows="15">{{ workflow_content }}</textarea>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-save"></i> {{ t('ui.workflow.save', 'Save Workflow') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"core_styles": [
|
||||
"/static/css/main.css",
|
||||
"/static/css/workflow_palette.css"
|
||||
],
|
||||
"core_scripts": [
|
||||
"/static/js/app_context.js",
|
||||
"/static/js/plugin_registry.js",
|
||||
@@ -14,6 +18,7 @@
|
||||
"/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",
|
||||
@@ -26,6 +31,7 @@
|
||||
"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",
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
|
||||
class WorkflowEngine:
|
||||
"""Run workflow configs through a node executor."""
|
||||
def __init__(self, workflow_config, node_executor, logger):
|
||||
self.workflow_config = workflow_config or {}
|
||||
self.node_executor = node_executor
|
||||
self.logger = logger
|
||||
|
||||
def execute(self):
|
||||
"""Execute the workflow config."""
|
||||
nodes = self.workflow_config.get("nodes")
|
||||
if not isinstance(nodes, list):
|
||||
self.logger.error("Workflow config missing nodes list.")
|
||||
|
||||
@@ -3,16 +3,20 @@ from .value_helpers import ValueHelpers
|
||||
|
||||
|
||||
class InputResolver:
|
||||
"""Resolve bindings in workflow inputs."""
|
||||
def __init__(self, store: dict):
|
||||
self.store = store
|
||||
|
||||
def resolve_inputs(self, inputs: dict) -> dict:
|
||||
"""Resolve bindings for every input."""
|
||||
return {key: self.resolve_binding(value) for key, value in (inputs or {}).items()}
|
||||
|
||||
def resolve_binding(self, value):
|
||||
"""Resolve a single binding value."""
|
||||
if isinstance(value, str) and value.startswith("$"):
|
||||
return self.store.get(value[1:])
|
||||
return value
|
||||
|
||||
def coerce_bool(self, value) -> bool:
|
||||
"""Coerce values into booleans."""
|
||||
return ValueHelpers.coerce_bool(value)
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
|
||||
class LoopExecutor:
|
||||
"""Execute loop nodes."""
|
||||
def __init__(self, runtime, input_resolver):
|
||||
self.runtime = runtime
|
||||
self.input_resolver = input_resolver
|
||||
self.node_executor = None
|
||||
|
||||
def set_node_executor(self, node_executor) -> None:
|
||||
"""Inject node executor dependency."""
|
||||
self.node_executor = node_executor
|
||||
|
||||
def execute(self, node):
|
||||
"""Run loop body until stop condition."""
|
||||
inputs = node.get("inputs", {})
|
||||
max_iterations = self.input_resolver.resolve_binding(inputs.get("max_iterations", 1))
|
||||
stop_when_raw = inputs.get("stop_when")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
class NodeExecutor:
|
||||
"""Execute workflow nodes with plugins."""
|
||||
def __init__(self, runtime, plugin_registry, input_resolver, loop_executor):
|
||||
self.runtime = runtime
|
||||
self.plugin_registry = plugin_registry
|
||||
@@ -9,10 +10,12 @@ class NodeExecutor:
|
||||
self.loop_executor = loop_executor
|
||||
|
||||
def execute_nodes(self, nodes):
|
||||
"""Execute a list of nodes."""
|
||||
for node in nodes:
|
||||
self.execute_node(node)
|
||||
|
||||
def execute_node(self, node):
|
||||
"""Execute a single node."""
|
||||
node_type = node.get("type")
|
||||
if not node_type:
|
||||
self.runtime.logger.error("Workflow node missing type.")
|
||||
|
||||
@@ -3,4 +3,5 @@ from ..callable_loader import load_callable
|
||||
|
||||
|
||||
def load_plugin_callable(path: str):
|
||||
"""Load a workflow plugin callable."""
|
||||
return load_callable(path)
|
||||
|
||||
@@ -8,6 +8,7 @@ logger = logging.getLogger("autometabuilder")
|
||||
|
||||
|
||||
def load_plugin_map() -> dict:
|
||||
"""Load workflow plugin map JSON."""
|
||||
map_path = os.path.join(os.path.dirname(__file__), "plugin_map.json")
|
||||
if not os.path.exists(map_path):
|
||||
return {}
|
||||
@@ -21,6 +22,7 @@ def load_plugin_map() -> dict:
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
"""Resolve workflow plugin handlers."""
|
||||
def __init__(self, plugin_map: dict):
|
||||
self._plugins = {}
|
||||
for node_type, path in plugin_map.items():
|
||||
@@ -31,4 +33,5 @@ class PluginRegistry:
|
||||
logger.error("Failed to register plugin %s: %s", node_type, error)
|
||||
|
||||
def get(self, node_type: str):
|
||||
"""Return plugin handler for node type."""
|
||||
return self._plugins.get(node_type)
|
||||
|
||||
@@ -3,6 +3,7 @@ from ...openai_client import get_completion
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Invoke the model with current messages."""
|
||||
messages = list(inputs.get("messages") or [])
|
||||
response = get_completion(
|
||||
runtime.context["client"],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Append context to the message list."""
|
||||
messages = list(inputs.get("messages") or [])
|
||||
context_val = inputs.get("context")
|
||||
if context_val:
|
||||
|
||||
@@ -4,6 +4,7 @@ from ...roadmap_utils import is_mvp_reached
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Append tool results to the message list."""
|
||||
messages = list(inputs.get("messages") or [])
|
||||
tool_results = inputs.get("tool_results") or []
|
||||
if tool_results:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Append the next user instruction."""
|
||||
messages = list(inputs.get("messages") or [])
|
||||
messages.append({"role": "user", "content": runtime.context["msgs"]["user_next_step"]})
|
||||
return {"messages": messages}
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
from ...context_loader import get_sdlc_context
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
def run(runtime, _inputs):
|
||||
"""Load SDLC context into the workflow store."""
|
||||
return {"context": get_sdlc_context(runtime.context["gh"], runtime.context["msgs"])}
|
||||
|
||||
@@ -4,6 +4,7 @@ from ..tool_calls_handler import handle_tool_calls
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Execute tool calls from an AI response."""
|
||||
resp_msg = inputs.get("response")
|
||||
tool_calls = getattr(resp_msg, "tool_calls", None) or []
|
||||
if not resp_msg:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Workflow plugin: seed messages."""
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
def run(runtime, _inputs):
|
||||
"""Seed messages from the prompt."""
|
||||
prompt = runtime.context["prompt"]
|
||||
return {"messages": list(prompt["messages"])}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Create a branch via tool runner."""
|
||||
result = runtime.tool_runner.call(
|
||||
"create_branch",
|
||||
branch_name=inputs.get("branch_name"),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Create a pull request via tool runner."""
|
||||
result = runtime.tool_runner.call(
|
||||
"create_pull_request",
|
||||
title=inputs.get("title"),
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""List files via tool runner."""
|
||||
result = runtime.tool_runner.call("list_files", directory=inputs.get("path", "."))
|
||||
return {"files": result}
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Read a file via tool runner."""
|
||||
result = runtime.tool_runner.call("read_file", path=inputs.get("path"))
|
||||
return {"content": result}
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Run lint via tool runner."""
|
||||
result = runtime.tool_runner.call("run_lint", path=inputs.get("path", "src"))
|
||||
return {"results": result}
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
"""Run tests via tool runner."""
|
||||
result = runtime.tool_runner.call("run_tests", path=inputs.get("path", "tests"))
|
||||
return {"results": result}
|
||||
|
||||
@@ -3,7 +3,8 @@ import re
|
||||
from ..value_helpers import ValueHelpers
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
def run(_runtime, inputs):
|
||||
"""Evaluate a branch condition."""
|
||||
value = inputs.get("value")
|
||||
mode = inputs.get("mode", "is_truthy")
|
||||
compare = inputs.get("compare", "")
|
||||
|
||||
@@ -3,7 +3,8 @@ import re
|
||||
from ..value_helpers import ValueHelpers
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
def run(_runtime, inputs):
|
||||
"""Filter items using a match mode."""
|
||||
items = ValueHelpers.ensure_list(inputs.get("items"))
|
||||
mode = inputs.get("mode", "contains")
|
||||
pattern = inputs.get("pattern", "")
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
from ..value_helpers import ValueHelpers
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
def run(_runtime, inputs):
|
||||
"""Map items to formatted strings."""
|
||||
items = ValueHelpers.ensure_list(inputs.get("items"))
|
||||
template = inputs.get("template", "{item}")
|
||||
mapped = []
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
from ..value_helpers import ValueHelpers
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
def run(_runtime, inputs):
|
||||
"""Negate a boolean value."""
|
||||
return {"result": not ValueHelpers.coerce_bool(inputs.get("value"))}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
from ..value_helpers import ValueHelpers
|
||||
|
||||
|
||||
def run(runtime, inputs):
|
||||
def run(_runtime, inputs):
|
||||
"""Reduce a list into a string."""
|
||||
items = ValueHelpers.ensure_list(inputs.get("items"))
|
||||
separator = ValueHelpers.normalize_separator(inputs.get("separator", ""))
|
||||
reduced = separator.join([str(item) for item in items])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
|
||||
class WorkflowRuntime:
|
||||
"""Runtime state for workflow execution."""
|
||||
def __init__(self, context: dict, store: dict, tool_runner, logger):
|
||||
self.context = context
|
||||
self.store = store
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
|
||||
|
||||
def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict, logger) -> list:
|
||||
"""Execute tool calls and return tool result messages."""
|
||||
if not resp_msg.tool_calls:
|
||||
return []
|
||||
|
||||
@@ -17,9 +18,11 @@ def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict
|
||||
|
||||
handler = tool_map.get(function_name)
|
||||
if not handler:
|
||||
msg = msgs.get("error_tool_not_found", "Tool {name} not found or unavailable.").format(
|
||||
name=function_name
|
||||
msg_template = msgs.get(
|
||||
"error_tool_not_found",
|
||||
"Tool {name} not found or unavailable."
|
||||
)
|
||||
msg = msg_template.format(name=function_name)
|
||||
logger.error(msg)
|
||||
tool_results.append({
|
||||
"tool_call_id": call_id,
|
||||
@@ -37,7 +40,8 @@ def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict
|
||||
).format(name=function_name, args=payload)
|
||||
)
|
||||
if confirm.lower() != "y":
|
||||
logger.info(msgs.get("info_tool_skipped", "Skipping tool: {name}").format(name=function_name))
|
||||
skipped_template = msgs.get("info_tool_skipped", "Skipping tool: {name}")
|
||||
logger.info(skipped_template.format(name=function_name))
|
||||
tool_results.append({
|
||||
"tool_call_id": call_id,
|
||||
"role": "tool",
|
||||
@@ -61,7 +65,8 @@ def handle_tool_calls(resp_msg, tool_map: dict, msgs: dict, args, policies: dict
|
||||
})
|
||||
continue
|
||||
|
||||
logger.info(msgs.get("info_executing_tool", "Executing tool: {name}").format(name=function_name))
|
||||
exec_template = msgs.get("info_executing_tool", "Executing tool: {name}")
|
||||
logger.info(exec_template.format(name=function_name))
|
||||
try:
|
||||
result = handler(**payload)
|
||||
content = str(result) if result is not None else "Success"
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
|
||||
class ToolRunner:
|
||||
"""Run tool callables with shared logging."""
|
||||
def __init__(self, tool_map: dict, msgs: dict, logger):
|
||||
self.tool_map = tool_map
|
||||
self.msgs = msgs
|
||||
self.logger = logger
|
||||
|
||||
def call(self, tool_name: str, **kwargs):
|
||||
"""Call a named tool with filtered kwargs."""
|
||||
tool = self.tool_map.get(tool_name)
|
||||
if not tool:
|
||||
msg = self.msgs.get(
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
|
||||
class ValueHelpers:
|
||||
"""Normalize values for workflow helpers."""
|
||||
@staticmethod
|
||||
def ensure_list(value):
|
||||
"""Return a list for any incoming value."""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
@@ -16,6 +18,7 @@ class ValueHelpers:
|
||||
|
||||
@staticmethod
|
||||
def coerce_bool(value) -> bool:
|
||||
"""Coerce values into booleans."""
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
@@ -28,6 +31,7 @@ class ValueHelpers:
|
||||
|
||||
@staticmethod
|
||||
def normalize_separator(text):
|
||||
"""Normalize escaped separators."""
|
||||
if text is None:
|
||||
return ""
|
||||
if isinstance(text, str):
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
|
||||
def load_workflow_config(metadata: dict) -> dict:
|
||||
"""Load workflow config referenced by metadata."""
|
||||
workflow_file = metadata.get("workflow_path", "workflow.json")
|
||||
workflow_path = os.path.join(os.path.dirname(__file__), workflow_file)
|
||||
with open(workflow_path, "r", encoding="utf-8") as f:
|
||||
|
||||
@@ -2,15 +2,9 @@
|
||||
from .model_resolver import resolve_model_name
|
||||
|
||||
|
||||
def build_workflow_context(args, gh, msgs, client, tools, tool_map, prompt, tool_policies) -> dict:
|
||||
return {
|
||||
"args": args,
|
||||
"gh": gh,
|
||||
"msgs": msgs,
|
||||
"client": client,
|
||||
"model_name": resolve_model_name(prompt),
|
||||
"tools": tools,
|
||||
"tool_map": tool_map,
|
||||
"prompt": prompt,
|
||||
"tool_policies": tool_policies,
|
||||
}
|
||||
def build_workflow_context(parts: dict) -> dict:
|
||||
"""Build the workflow context dict."""
|
||||
prompt = parts["prompt"]
|
||||
context = dict(parts)
|
||||
context["model_name"] = resolve_model_name(prompt)
|
||||
return context
|
||||
|
||||
@@ -9,6 +9,7 @@ from .workflow.tool_runner import ToolRunner
|
||||
|
||||
|
||||
def build_workflow_engine(workflow_config: dict, context: dict, logger):
|
||||
"""Assemble workflow engine dependencies."""
|
||||
runtime = WorkflowRuntime(context=context, store={}, tool_runner=None, logger=logger)
|
||||
tool_runner = ToolRunner(context["tool_map"], context["msgs"], logger)
|
||||
runtime.tool_runner = tool_runner
|
||||
|
||||
Reference in New Issue
Block a user