mirror of
https://github.com/johndoe6345789/AutoMetabuilder.git
synced 2026-04-25 06:15:01 +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:
@@ -55,3 +55,5 @@ React component logic (just above the bottom jsx) can be converted to one or mor
|
||||
Use atomic React components.
|
||||
|
||||
Don't hardcode strings, use a translation file or some sort of external JSON mapping.
|
||||
|
||||
Don't hardcode React Styling - MUI themes or SASS.
|
||||
|
||||
@@ -31,6 +31,7 @@ from .data import (
|
||||
write_prompt,
|
||||
write_workflow,
|
||||
)
|
||||
from .workflow_graph import build_workflow_graph
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["JSON_SORT_KEYS"] = False
|
||||
@@ -233,5 +234,10 @@ def api_get_workflow_package(package_id: str) -> tuple[Dict[str, object], int]:
|
||||
return {"error": "package not found"}, 404
|
||||
|
||||
|
||||
@app.route("/api/workflow/graph")
|
||||
def api_workflow_graph() -> tuple[Dict[str, object], int]:
|
||||
return build_workflow_graph(), 200
|
||||
|
||||
|
||||
def start_web_ui(host: str = "0.0.0.0", port: int = 8000) -> None:
|
||||
app.run(host=host, port=port)
|
||||
|
||||
70
backend/autometabuilder/web/workflow_graph.py
Normal file
70
backend/autometabuilder/web/workflow_graph.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Build a node/edge view of the declarative workflow for visualization."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Iterable, List
|
||||
|
||||
from .data import get_workflow_content, load_metadata
|
||||
|
||||
|
||||
def _parse_workflow_definition() -> Dict[str, Any]:
|
||||
payload = get_workflow_content()
|
||||
if not payload:
|
||||
return {"nodes": []}
|
||||
try:
|
||||
parsed = json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
return {"nodes": []}
|
||||
return parsed if isinstance(parsed, dict) else {"nodes": []}
|
||||
|
||||
|
||||
def _gather_nodes(nodes: Iterable[Dict[str, Any]], plugin_map: Dict[str, Any], parent_id: str | None = None, collected: List[Dict[str, Any]] | None = None) -> List[Dict[str, Any]]:
|
||||
collected = collected or []
|
||||
for node in nodes:
|
||||
node_id = node.get("id") or f"node-{len(collected)}"
|
||||
node_type = node.get("type", "unknown")
|
||||
metadata = plugin_map.get(node_type, {})
|
||||
node_summary: Dict[str, Any] = {
|
||||
"id": node_id,
|
||||
"type": node_type,
|
||||
"label_key": metadata.get("label"),
|
||||
"parent": parent_id,
|
||||
"inputs": node.get("inputs", {}),
|
||||
"outputs": node.get("outputs", {}),
|
||||
}
|
||||
collected.append(node_summary)
|
||||
body = node.get("body")
|
||||
if isinstance(body, list):
|
||||
_gather_nodes(body, plugin_map, parent_id=node_id, collected=collected)
|
||||
return collected
|
||||
|
||||
|
||||
def _build_edges(nodes: Iterable[Dict[str, Any]]) -> List[Dict[str, str]]:
|
||||
producers: Dict[str, str] = {}
|
||||
for node in nodes:
|
||||
outputs = node.get("outputs", {})
|
||||
for value in outputs.values():
|
||||
if isinstance(value, str):
|
||||
producers[value] = node["id"]
|
||||
edges: List[Dict[str, str]] = []
|
||||
for node in nodes:
|
||||
inputs = node.get("inputs", {})
|
||||
for port, value in inputs.items():
|
||||
if isinstance(value, str) and value.startswith("$"):
|
||||
variable = value[1:]
|
||||
source = producers.get(variable)
|
||||
if source:
|
||||
edges.append({"from": source, "to": node["id"], "var": variable, "port": port})
|
||||
return edges
|
||||
|
||||
|
||||
def build_workflow_graph() -> Dict[str, Any]:
|
||||
definition = _parse_workflow_definition()
|
||||
plugin_map = load_metadata().get("workflow_plugins", {})
|
||||
nodes = _gather_nodes(definition.get("nodes", []), plugin_map)
|
||||
edges = _build_edges(nodes)
|
||||
return {
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"count": {"nodes": len(nodes), "edges": len(edges)},
|
||||
}
|
||||
16
backend/tests/test_workflow_graph.py
Normal file
16
backend/tests/test_workflow_graph.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from autometabuilder.web.workflow_graph import build_workflow_graph
|
||||
|
||||
|
||||
def test_build_workflow_graph_structure():
|
||||
graph = build_workflow_graph()
|
||||
assert isinstance(graph.get("nodes"), list), "nodes list should be present"
|
||||
assert isinstance(graph.get("edges"), list), "edges list should be present"
|
||||
counts = graph.get("count", {})
|
||||
assert counts.get("nodes", 0) >= 5, "Expect at least the top-level nodes to exist"
|
||||
assert counts.get("edges", 0) > 0, "Edges should exist between workflow nodes"
|
||||
|
||||
node_ids = {node["id"] for node in graph["nodes"]}
|
||||
for edge in graph["edges"]:
|
||||
assert edge["from"] in node_ids
|
||||
assert edge["to"] in node_ids
|
||||
assert isinstance(edge["var"], str) and edge["var"], "edges should reference a variable"
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Button, Paper, Stack, Typography } from "@mui/material";
|
||||
import { WorkflowPackageSummary } from "../../lib/types";
|
||||
import WorkflowGraphPanel from "../workflow/WorkflowGraphPanel";
|
||||
import WorkflowPalette from "../workflow/WorkflowPalette";
|
||||
|
||||
type WorkflowSectionProps = {
|
||||
content: string;
|
||||
@@ -59,23 +61,29 @@ export default function WorkflowSection({ content, packages, onSave, onTemplateS
|
||||
</Stack>
|
||||
</Box>
|
||||
<Paper sx={{ flex: 1, p: 2, backgroundColor: "#0b1221" }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("ui.workflow.templates.title", "Workflow Templates")}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{packages.map((pkg) => (
|
||||
<Paper key={pkg.id} variant="outlined" sx={{ p: 1 }}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle2">{pkg.label}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{pkg.description}
|
||||
</Typography>
|
||||
<Button size="small" variant="text" onClick={() => onTemplateSelect(pkg.id)}>
|
||||
{t("ui.workflow.templates.apply", "Apply Template")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("ui.workflow.templates.title", "Workflow Templates")}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{packages.map((pkg) => (
|
||||
<Paper key={pkg.id} variant="outlined" sx={{ p: 1 }}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle2">{pkg.label}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{pkg.description}
|
||||
</Typography>
|
||||
<Button size="small" variant="text" onClick={() => onTemplateSelect(pkg.id)}>
|
||||
{t("ui.workflow.templates.apply", "Apply Template")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
<WorkflowGraphPanel t={t} />
|
||||
<WorkflowPalette t={t} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Chip, Divider, Paper, Stack, Typography } from "@mui/material";
|
||||
import { fetchWorkflowGraph } from "../../lib/api";
|
||||
import { WorkflowGraph } from "../../lib/types";
|
||||
|
||||
type WorkflowGraphPanelProps = {
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function WorkflowGraphPanel({ t }: WorkflowGraphPanelProps) {
|
||||
const [graph, setGraph] = useState<WorkflowGraph | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
void fetchWorkflowGraph()
|
||||
.then((payload) => {
|
||||
if (alive) {
|
||||
setGraph(payload);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (alive) {
|
||||
setError(String(err));
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2, backgroundColor: "#0b1221" }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("ui.workflow.graph.title", "Workflow Graph")}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" gutterBottom>
|
||||
{graph
|
||||
? t("ui.workflow.graph.summary", "Nodes: {nodes}, Edges: {edges}")
|
||||
.replace("{nodes}", String(graph.count.nodes))
|
||||
.replace("{edges}", String(graph.count.edges))
|
||||
: t("ui.workflow.graph.loading", "Loading graph…")}
|
||||
</Typography>
|
||||
{error ? (
|
||||
<Typography color="error">{error}</Typography>
|
||||
) : (
|
||||
<>
|
||||
<Stack spacing={1} divider={<Divider light sx={{ borderColor: "rgba(255,255,255,0.08)" }} />}>
|
||||
{graph?.nodes.map((node) => (
|
||||
<Box key={node.id} sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Chip size="small" label={node.type} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{node.label_key ? t(node.label_key, node.type) : node.type}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{node.parent ? `${t("ui.workflow.graph.child_of", "child of")} ${node.parent}` : t("ui.workflow.graph.top_level", "top level")}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
<Divider sx={{ my: 2, borderColor: "rgba(255,255,255,0.08)" }} />
|
||||
<Stack spacing={1}>
|
||||
{graph?.edges.map((edge, index) => (
|
||||
<Typography key={`${edge.from}-${edge.to}-${edge.var}-${index}`} variant="caption" color="text.secondary">
|
||||
{t("ui.workflow.graph.edge", "{from} → {to} ({var})")
|
||||
.replace("{from}", edge.from)
|
||||
.replace("{to}", edge.to)
|
||||
.replace("{var}", edge.var)}
|
||||
</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
109
frontend/autometabuilder/components/workflow/WorkflowPalette.tsx
Normal file
109
frontend/autometabuilder/components/workflow/WorkflowPalette.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Box, Chip, Divider, Paper, Stack, TextField, Typography } from "@mui/material";
|
||||
import { fetchWorkflowPlugins } from "../../lib/api";
|
||||
import { WorkflowPluginDefinition, WorkflowPluginMap, WorkflowPluginPort } from "../../lib/types";
|
||||
|
||||
type WorkflowPaletteProps = {
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function WorkflowPalette({ t }: WorkflowPaletteProps) {
|
||||
const [plugins, setPlugins] = useState<WorkflowPluginMap>({});
|
||||
const [search, setSearch] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
void fetchWorkflowPlugins()
|
||||
.then((data) => {
|
||||
if (alive) {
|
||||
setPlugins(data);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (alive) {
|
||||
setError(String(err));
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const entries = useMemo(() => {
|
||||
const query = search.trim().toLowerCase();
|
||||
return Object.entries(plugins).filter(([id, plugin]) => {
|
||||
const label = plugin.label ? t(plugin.label, id) : id;
|
||||
return !query || `${id} ${label}`.toLowerCase().includes(query);
|
||||
});
|
||||
}, [plugins, search, t]);
|
||||
|
||||
const isLoading = entries.length === 0 && !search && !error;
|
||||
const hasQuery = search.trim().length > 0;
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2, backgroundColor: "#0b1221" }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("ui.workflow.palette.title", "Workflow Palette")}
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder={t("ui.workflow.palette.search_placeholder", "Search nodes or keywords...")}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
{error ? (
|
||||
<Typography color="error">{error}</Typography>
|
||||
) : isLoading ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t("ui.workflow.palette.loading", "Loading node library…")}
|
||||
</Typography>
|
||||
) : entries.length === 0 && hasQuery ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t("ui.workflow.palette.missing", "No nodes match \"{query}\"").replace("{query}", search.trim())}
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack spacing={1} divider={<Divider sx={{ borderColor: "rgba(255,255,255,0.08)" }} />}>
|
||||
{entries.map(([id, plugin]) => (
|
||||
<Box key={id} sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<Typography variant="subtitle2">
|
||||
{plugin.label ? t(plugin.label, id) : id}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t("ui.workflow.palette.node_id", "Node ID")}: {id}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" mt={1}>
|
||||
{renderPortTags(t, "in", plugin.inputs)}
|
||||
{renderPortTags(t, "out", plugin.outputs)}
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPortTags(
|
||||
t: (key: string, fallback?: string) => string,
|
||||
direction: "in" | "out",
|
||||
ports?: Record<string, WorkflowPluginPort>
|
||||
) {
|
||||
if (!ports) {
|
||||
return null;
|
||||
}
|
||||
return Object.keys(ports).map((name) => {
|
||||
const port = ports[name];
|
||||
const label = port?.label ? t(port.label, name) : name;
|
||||
return (
|
||||
<Chip
|
||||
key={`${direction}-${name}`}
|
||||
label={`${direction === "in" ? "⮂" : "⮀"} ${label}`}
|
||||
size="small"
|
||||
sx={{ backgroundColor: "rgba(255,255,255,0.08)", color: "white" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user