93d8d0c1cf
# Conflicts: # apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs # apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs # docs/project-memory/shared-memory/decision-log.md # docs/project-memory/shared-memory/pitfalls.md
357 lines
11 KiB
Python
357 lines
11 KiB
Python
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
from textwrap import wrap
|
|
|
|
try:
|
|
import matplotlib.font_manager as font_manager
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch, Polygon, Rectangle
|
|
except ImportError as error:
|
|
raise SystemExit(
|
|
"matplotlib is required. Install it in your local Python environment, "
|
|
"then rerun this script."
|
|
) from error
|
|
|
|
|
|
FONT_CANDIDATES = [
|
|
"Microsoft YaHei",
|
|
"SimHei",
|
|
"Noto Sans CJK SC",
|
|
"Source Han Sans SC",
|
|
"PingFang SC",
|
|
"Arial Unicode MS",
|
|
"DejaVu Sans",
|
|
]
|
|
|
|
|
|
def configure_font():
|
|
available = {font.name for font in font_manager.fontManager.ttflist}
|
|
selected = next((font for font in FONT_CANDIDATES if font in available), "DejaVu Sans")
|
|
plt.rcParams["font.sans-serif"] = [selected, "DejaVu Sans"]
|
|
plt.rcParams["axes.unicode_minus"] = False
|
|
|
|
|
|
def wrap_mixed_text(text, width):
|
|
lines = []
|
|
for raw_line in text.splitlines():
|
|
if not raw_line:
|
|
lines.append("")
|
|
continue
|
|
if len(raw_line) <= width:
|
|
lines.append(raw_line)
|
|
continue
|
|
if any(separator in raw_line for separator in ["_", "/", ".", ":"]) and " " not in raw_line:
|
|
lines.append(raw_line)
|
|
continue
|
|
if " " in raw_line:
|
|
lines.extend(wrap(raw_line, width=width, break_long_words=False, break_on_hyphens=False))
|
|
continue
|
|
lines.extend(raw_line[index : index + width] for index in range(0, len(raw_line), width))
|
|
return "\n".join(lines)
|
|
|
|
|
|
def draw_lane(ax, y0, y1, label, color):
|
|
ax.add_patch(
|
|
Rectangle(
|
|
(0.35, y0),
|
|
17.3,
|
|
y1 - y0,
|
|
facecolor=color,
|
|
edgecolor="none",
|
|
alpha=0.34,
|
|
zorder=0,
|
|
)
|
|
)
|
|
ax.text(
|
|
1.1,
|
|
(y0 + y1) / 2,
|
|
wrap_mixed_text(label, 8),
|
|
fontsize=7.9,
|
|
color="#475569",
|
|
va="center",
|
|
ha="center",
|
|
weight="bold",
|
|
linespacing=1.12,
|
|
)
|
|
|
|
|
|
def add_box(ax, center, size, title, body, facecolor, edgecolor="#334155"):
|
|
x, y = center
|
|
width, height = size
|
|
left = x - width / 2
|
|
bottom = y - height / 2
|
|
ax.add_patch(
|
|
FancyBboxPatch(
|
|
(left, bottom),
|
|
width,
|
|
height,
|
|
boxstyle="round,pad=0.03,rounding_size=0.12",
|
|
linewidth=1.3,
|
|
facecolor=facecolor,
|
|
edgecolor=edgecolor,
|
|
zorder=2,
|
|
)
|
|
)
|
|
ax.text(
|
|
x,
|
|
y + height * 0.22,
|
|
wrap_mixed_text(title, 13),
|
|
fontsize=9.7,
|
|
weight="bold",
|
|
color="#0f172a",
|
|
ha="center",
|
|
va="center",
|
|
zorder=3,
|
|
)
|
|
ax.text(
|
|
x,
|
|
y - height * 0.17,
|
|
wrap_mixed_text(body, 18),
|
|
fontsize=8.0,
|
|
color="#334155",
|
|
ha="center",
|
|
va="center",
|
|
linespacing=1.12,
|
|
zorder=3,
|
|
)
|
|
|
|
|
|
def add_diamond(ax, center, size, title, body, facecolor, edgecolor="#334155"):
|
|
x, y = center
|
|
width, height = size
|
|
points = [
|
|
(x, y + height / 2),
|
|
(x + width / 2, y),
|
|
(x, y - height / 2),
|
|
(x - width / 2, y),
|
|
]
|
|
ax.add_patch(
|
|
Polygon(points, closed=True, facecolor=facecolor, edgecolor=edgecolor, linewidth=1.3, zorder=2)
|
|
)
|
|
ax.text(
|
|
x,
|
|
y + 0.1,
|
|
wrap_mixed_text(title, 12),
|
|
fontsize=9.6,
|
|
weight="bold",
|
|
color="#0f172a",
|
|
ha="center",
|
|
va="center",
|
|
zorder=3,
|
|
)
|
|
ax.text(
|
|
x,
|
|
y - 0.35,
|
|
wrap_mixed_text(body, 14),
|
|
fontsize=7.8,
|
|
color="#334155",
|
|
ha="center",
|
|
va="center",
|
|
linespacing=1.1,
|
|
zorder=3,
|
|
)
|
|
|
|
|
|
def add_arrow(ax, start, end, label=None, color="#475569", curve=0.0, dashed=False):
|
|
arrow = FancyArrowPatch(
|
|
start,
|
|
end,
|
|
arrowstyle="-|>",
|
|
mutation_scale=15,
|
|
linewidth=1.45,
|
|
color=color,
|
|
linestyle="--" if dashed else "-",
|
|
connectionstyle=f"arc3,rad={curve}",
|
|
zorder=1,
|
|
)
|
|
ax.add_patch(arrow)
|
|
if label:
|
|
mid_x = (start[0] + end[0]) / 2
|
|
mid_y = (start[1] + end[1]) / 2
|
|
ax.text(
|
|
mid_x,
|
|
mid_y + 0.22,
|
|
label,
|
|
fontsize=8,
|
|
color=color,
|
|
ha="center",
|
|
va="center",
|
|
bbox={"boxstyle": "round,pad=0.18", "facecolor": "#ffffff", "edgecolor": "none", "alpha": 0.9},
|
|
zorder=4,
|
|
)
|
|
|
|
|
|
def add_polyline_arrow(ax, points, label=None, color="#475569", dashed=False):
|
|
if len(points) < 2:
|
|
return
|
|
for start, end in zip(points[:-2], points[1:-1]):
|
|
ax.plot(
|
|
[start[0], end[0]],
|
|
[start[1], end[1]],
|
|
color=color,
|
|
linewidth=1.45,
|
|
linestyle="--" if dashed else "-",
|
|
zorder=1,
|
|
)
|
|
add_arrow(ax, points[-2], points[-1], color=color, dashed=dashed)
|
|
if label:
|
|
middle = points[len(points) // 2]
|
|
ax.text(
|
|
middle[0],
|
|
middle[1] + 0.18,
|
|
label,
|
|
fontsize=8,
|
|
color=color,
|
|
ha="center",
|
|
va="center",
|
|
bbox={"boxstyle": "round,pad=0.18", "facecolor": "#ffffff", "edgecolor": "none", "alpha": 0.92},
|
|
zorder=4,
|
|
)
|
|
|
|
|
|
def draw_agent_loop(output_path, dpi):
|
|
configure_font()
|
|
fig, ax = plt.subplots(figsize=(18, 11), dpi=dpi)
|
|
ax.set_xlim(0, 18)
|
|
ax.set_ylim(0, 11)
|
|
ax.axis("off")
|
|
|
|
fig.patch.set_facecolor("#f8fafc")
|
|
ax.set_facecolor("#f8fafc")
|
|
|
|
draw_lane(ax, 8.65, 10.2, "用户聊天窗口\n只看聊天、上传、待确认命令", "#dbeafe")
|
|
draw_lane(ax, 6.75, 8.25, "上下文与规格\nTauri / Rust 本地 Runtime", "#dcfce7")
|
|
draw_lane(ax, 3.85, 6.35, "Agent Loop\n最多 3 轮,Evaluator 驱动返工", "#fef3c7")
|
|
draw_lane(ax, 1.05, 3.45, "本地产物、预览\n和可审计证据", "#fce7f3")
|
|
|
|
ax.text(
|
|
8,
|
|
10.72,
|
|
"AI 游戏创作 App Agent Loop 结构",
|
|
ha="center",
|
|
va="center",
|
|
fontsize=18,
|
|
weight="bold",
|
|
color="#0f172a",
|
|
)
|
|
ax.text(
|
|
8,
|
|
0.45,
|
|
"事实源:memory/、assets/、game/、exports/、.agent/manifest.json、.agent/run.latest.json、.agent/passes/pass-N/",
|
|
ha="center",
|
|
va="center",
|
|
fontsize=9,
|
|
color="#64748b",
|
|
)
|
|
|
|
boxes = {
|
|
"input": ((3.2, 9.42), (2.25, 0.92), "用户输入", "/project 授权\n普通需求触发生成"),
|
|
"pending": ((5.9, 9.42), (2.25, 0.92), "Permission Gate", "pending 卡片\nconfirm / cancel 日志"),
|
|
"tauri": ((8.6, 9.42), (2.35, 0.92), "Tauri Command", "generate_local_game_draft\n只操作授权路径"),
|
|
"context": ((11.5, 9.42), (2.45, 0.92), "上下文装载", "LLM 配置\n记忆 + manifest\n资产摘要"),
|
|
"planner": ((3.2, 7.5), (2.25, 0.92), "Planner", "整理规格\n写 .agent/spec.md"),
|
|
"findings": ((5.9, 7.5), (2.25, 0.92), "Findings Seed", "初始化 Evaluator\n.agent/findings.md"),
|
|
"orchestrator": ((8.6, 7.5), (2.35, 0.92), "Orchestrator", "agenda.md\ntask-graph.json"),
|
|
"roles": ((11.5, 7.5), (2.45, 0.92), "6 组角色 Brief", "16 个组内任务\nactive / carry-over"),
|
|
"generator": ((14.45, 7.5), (2.35, 0.92), "Generator", "结构化草案\nHTML + handoffs"),
|
|
"repair": ((9.6, 5.0), (2.45, 1.0), "Repair Routes", "结构化问题路由\n扩展下游影响任务"),
|
|
"failed": ((6.4, 5.0), (2.25, 1.0), "失败终止", "3 轮仍未通过\n只保留 trace 和 pass 快照"),
|
|
"writer": ((12.7, 2.35), (2.45, 1.0), "Artifact Writer", "写 game/index.html\n设计、数值、资产\n发布草案"),
|
|
"smoke": ((9.65, 2.35), (2.45, 1.0), "Playtest", "game.static_smoke\n拒绝占位\n拒绝危险 API"),
|
|
"preview": ((6.6, 2.35), (2.4, 1.0), "本地预览", "127.0.0.1 HTTP\n外部浏览器打开"),
|
|
"chat": ((3.55, 2.35), (2.25, 1.0), "聊天回显", "展示完成摘要\n/trace 读取 latest"),
|
|
"trace": ((15.6, 2.35), (2.55, 1.18), "Trace / Evidence", "run.latest.json\nruns/ logs/ artifacts\nstep + toolCalls"),
|
|
}
|
|
|
|
colors = {
|
|
"input": "#bfdbfe",
|
|
"pending": "#c7d2fe",
|
|
"tauri": "#bbf7d0",
|
|
"context": "#bbf7d0",
|
|
"planner": "#fde68a",
|
|
"findings": "#fde68a",
|
|
"orchestrator": "#fed7aa",
|
|
"roles": "#fef08a",
|
|
"generator": "#fde68a",
|
|
"repair": "#fecaca",
|
|
"failed": "#fee2e2",
|
|
"writer": "#fbcfe8",
|
|
"smoke": "#fbcfe8",
|
|
"preview": "#fbcfe8",
|
|
"chat": "#bfdbfe",
|
|
"trace": "#e2e8f0",
|
|
}
|
|
|
|
for key, (center, size, title, body) in boxes.items():
|
|
add_box(ax, center, size, title, body, colors[key])
|
|
|
|
add_diamond(
|
|
ax,
|
|
(14.45, 5.0),
|
|
(2.2, 1.48),
|
|
"Evaluator",
|
|
"质量门禁\n通过?",
|
|
"#fed7aa",
|
|
)
|
|
|
|
add_arrow(ax, (4.33, 9.42), (4.77, 9.42))
|
|
add_arrow(ax, (7.03, 9.42), (7.43, 9.42))
|
|
add_arrow(ax, (9.78, 9.42), (10.28, 9.42))
|
|
add_polyline_arrow(ax, [(11.5, 8.96), (11.5, 8.38), (3.2, 8.38), (3.2, 7.96)], label="开始 loop")
|
|
add_arrow(ax, (4.33, 7.5), (4.77, 7.5))
|
|
add_arrow(ax, (7.03, 7.5), (7.43, 7.5))
|
|
add_arrow(ax, (9.78, 7.5), (10.27, 7.5))
|
|
add_arrow(ax, (12.73, 7.5), (13.28, 7.5))
|
|
add_polyline_arrow(ax, [(14.45, 7.04), (14.45, 6.03), (14.45, 5.74)])
|
|
add_arrow(ax, (13.35, 5.0), (10.83, 5.0), label="不通过")
|
|
add_polyline_arrow(ax, [(9.6, 5.5), (9.6, 6.15), (8.6, 6.15), (8.6, 7.04)], label="下一轮")
|
|
add_arrow(ax, (8.37, 5.0), (7.57, 5.0), label="超过 3 轮")
|
|
add_polyline_arrow(ax, [(14.45, 4.26), (14.45, 3.28), (12.7, 3.28), (12.7, 2.85)], label="通过")
|
|
add_arrow(ax, (11.48, 2.35), (10.88, 2.35))
|
|
add_arrow(ax, (8.43, 2.35), (7.8, 2.35))
|
|
add_arrow(ax, (5.4, 2.35), (4.68, 2.35))
|
|
add_arrow(ax, (13.93, 2.35), (14.33, 2.35), color="#64748b")
|
|
add_polyline_arrow(
|
|
ax,
|
|
[(15.3, 7.08), (17.15, 7.08), (17.15, 2.95), (16.89, 2.7)],
|
|
color="#64748b",
|
|
dashed=True,
|
|
label="持续写入 trace",
|
|
)
|
|
add_polyline_arrow(
|
|
ax,
|
|
[(15.6, 1.76), (15.6, 1.35), (3.55, 1.35), (3.55, 1.85)],
|
|
color="#64748b",
|
|
dashed=True,
|
|
)
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
fig.savefig(output_path, bbox_inches="tight", facecolor=fig.get_facecolor())
|
|
plt.close(fig)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
description="Draw the AI game creator Agent loop flowchart with matplotlib.",
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output",
|
|
default=".app/agent-loop-flow.png",
|
|
help="Output image path. The extension controls the format, for example .png, .svg or .pdf.",
|
|
)
|
|
parser.add_argument("--dpi", type=int, default=180, help="Output DPI for raster formats.")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
output_path = Path(args.output)
|
|
draw_agent_loop(output_path, args.dpi)
|
|
print(f"agent loop flowchart written to {output_path.resolve()}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|