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:
2026-01-10 01:56:08 +00:00
parent 4f92baa801
commit cd7591d3da
7 changed files with 306 additions and 17 deletions

View File

@@ -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.

View File

@@ -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)

View 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)},
}

View 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"

View File

@@ -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>

View File

@@ -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>
);
}

View 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" }}
/>
);
});
}