""" lambdagentpaas 系统架构图 — PPT 优化版 v3 修复: - 卡片标题与正文留足间距 (title_gap 参数) - 显式计算文字实际占位, 防止与 va 基准点的计算误差 - 增大所有卡片高度, 给正文足够空间 - L1/L2 层高度相应增加 - 所有尺寸支持 SCALE 参数统一缩放 用法: python3 docs/assets/draw_architecture.py python3 docs/assets/draw_architecture.py --scale 1.5 # 放大 1.5 倍 python3 docs/assets/draw_architecture.py --scale 0.8 # 缩小到 0.8 倍 """ import argparse import os import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch, Rectangle plt.rcParams["font.sans-serif"] = [ "PingFang HK", "PingFang SC", "Hiragino Sans GB", "STHeiti", "Heiti TC", "Songti SC", "Arial Unicode MS", ] plt.rcParams["axes.unicode_minus"] = False # ═══════════════════════════════════════════════════════════ # 配色 # ═══════════════════════════════════════════════════════════ C = { "L3_bg": "#FFF4E0", "L3_edge": "#E67E22", "L3_header": "#F39C12", "L3_card": "#FFD79A", "L2_bg": "#E3F2FD", "L2_edge": "#1976D2", "L2_header": "#1E88E5", "L2_card": "#A8D0F2", "L2_safety_bg": "#FFEBEE", "L2_safety_edge": "#C62828", "L2_safety_header": "#E53935", "L2_safety_card": "#FFCDD2", "L1_bg": "#F3E5F5", "L1_edge": "#7B1FA2", "L1_header": "#8E24AA", "L1_card": "#D7A8E0", "L1_paper": "#C789D3", "L0_bg": "#E8F5E9", "L0_edge": "#388E3C", "L0_header": "#43A047", "L0_card": "#9FD9A5", "text": "#1A1A1A", "text_mid": "#37474F", "text_light":"#FFFFFF", "arrow": "#263238", } # ═══════════════════════════════════════════════════════════ # 绘图辅助 # ═══════════════════════════════════════════════════════════ class Scaler: """统一缩放器 — 一个参数控制所有尺寸。""" def __init__(self, scale: float = 1.0): self.s = scale def fs(self, size: int) -> float: """字号缩放""" return size * self.s def lw(self, width: float) -> float: """线宽缩放""" return width * self.s # 数据单位的坐标 / 尺寸保持不变 (ax 坐标系固定 0-20, 0-15.5) # 只需要让 figsize 按 scale 变化即可让输出像素 = scale × 原始像素 def figsize(self, w: float, h: float): return (w * self.s, h * self.s) def layer(ax, s: Scaler, x, y, w, h, header_h, label, bg, edge, header_color): """层: 外框 + 顶部 header 带""" ax.add_patch(FancyBboxPatch( (x, y), w, h, boxstyle="round,pad=0.08,rounding_size=0.18", facecolor=bg, edgecolor=edge, linewidth=s.lw(2.5), alpha=0.5, zorder=1, )) header_y = y + h - header_h ax.add_patch(Rectangle( (x + 0.1, header_y + 0.05), w - 0.2, header_h - 0.1, facecolor=header_color, edgecolor="none", alpha=0.92, zorder=2, )) ax.text( x + w / 2, header_y + header_h / 2, label, ha="center", va="center", fontsize=s.fs(17), fontweight="bold", color=C["text_light"], zorder=5, ) def card(ax, s: Scaler, x, y, w, h, title, lines, face, edge, title_size=15, body_size=12, pad_top=0.22, title_gap=0.28): """ 组件卡片 — 正确的文字布局 如果有 lines (正文): - 标题 va='top', 放在卡片顶部 (距顶 pad_top) - 正文 va='top', 在标题下方 title_gap + 标题高度 处 否则: - 标题居中显示 """ ax.add_patch(FancyBboxPatch( (x, y), w, h, boxstyle="round,pad=0.06,rounding_size=0.12", facecolor=face, edgecolor=edge, linewidth=s.lw(1.8), zorder=3, )) if lines: title_h = title_size / 72.0 title_top_y = y + h - pad_top ax.text( x + w / 2, title_top_y, title, ha="center", va="top", fontsize=s.fs(title_size), fontweight="bold", color=C["text"], zorder=4, ) body_top_y = title_top_y - title_h - title_gap body = "\n".join(lines) ax.text( x + w / 2, body_top_y, body, ha="center", va="top", fontsize=s.fs(body_size), color=C["text_mid"], linespacing=1.55, zorder=4, ) else: ax.text( x + w / 2, y + h / 2, title, ha="center", va="center", fontsize=s.fs(title_size), fontweight="bold", color=C["text"], zorder=4, ) def arrow_down(ax, s: Scaler, x, y_top, y_bot, color=None, lw=3): ax.annotate( "", xy=(x, y_bot), xytext=(x, y_top), arrowprops=dict(arrowstyle="-|>", color=color or C["arrow"], lw=s.lw(lw), mutation_scale=25), zorder=2, ) # ═══════════════════════════════════════════════════════════ # 主绘图 # ═══════════════════════════════════════════════════════════ def draw(output: str, scale: float = 1.0): """ 绘制架构图。 Args: output: 输出 PNG 路径 scale: 全局缩放比例 (1.0 = 默认; 1.5 = 放大; 0.8 = 缩小) 所有字号、线宽、figsize 按此比例缩放 """ s = Scaler(scale) BASE_W, BASE_H = 20, 15.5 # 基础画布尺寸 (数据单位) fig, ax = plt.subplots(figsize=s.figsize(BASE_W, BASE_H)) ax.set_xlim(0, BASE_W) ax.set_ylim(0, BASE_H) ax.axis("off") # ━━━ 顶部标题 ━━━ ax.text(10, 15.0, "lambdagentpaas 系统架构", ha="center", va="center", fontsize=s.fs(28), fontweight="bold", color=C["text"]) ax.text(10, 14.45, "形式化 λA 演算驱动的智能体编程语言 + PaaS 平台", ha="center", va="center", fontsize=s.fs(15), color=C["text_mid"], style="italic") # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # L3 应用层 y=11.3 ~ 13.95 h=2.65 header 0.6 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ layer(ax, s, 0.3, 11.3, 19.4, 2.65, 0.6, "L3 应用层 — 真实案例", C["L3_bg"], C["L3_edge"], C["L3_header"]) card(ax, s, 0.9, 11.45, 5.9, 1.85, "qaagent67wiki", ["海事法规问答", "1326 源文件 / 2989 wiki 页"], C["L3_card"], C["L3_edge"], title_size=18, body_size=13) card(ax, s, 7.05, 11.45, 5.9, 1.85, "agent67v2", ["多智能体编程助手", "42 工具 / 5 个子智能体"], C["L3_card"], C["L3_edge"], title_size=18, body_size=13) card(ax, s, 13.2, 11.45, 5.9, 1.85, "research67", ["科研助手 (IDEA → 论文)", "8 个子智能体 / 迭代优化"], C["L3_card"], C["L3_edge"], title_size=18, body_size=13) # L3 → L2 for x in (3.85, 10, 16.15): arrow_down(ax, s, x, 11.25, 10.8) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # L2 工程实现 y=5.0 ~ 10.75 h=5.75 header 0.55 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ layer(ax, s, 0.3, 5.0, 19.4, 5.75, 0.55, "L2 工程实现 (agentpaas)", C["L2_bg"], C["L2_edge"], C["L2_header"]) card(ax, s, 0.9, 9.05, 18.2, 1.0, "REST API (28 端点) + MCP Server (5 工具) + GitHub Action", [], C["L2_card"], C["L2_edge"], title_size=16) card(ax, s, 0.9, 7.2, 5.9, 1.7, "Instance 机制", ["template / instance", "YAML 深度合并 + workspace"], C["L2_card"], C["L2_edge"], title_size=15, body_size=12) card(ax, s, 7.05, 7.2, 5.9, 1.7, "双引擎执行", ["Recursive / CEK / Adaptive", "Python 栈 vs CEK Machine"], C["L2_card"], C["L2_edge"], title_size=15, body_size=12) card(ax, s, 13.2, 7.2, 5.9, 1.7, "多租户 + 安全", ["RBAC + Quota + Rate Limit", "审计日志 + API Key"], C["L2_card"], C["L2_edge"], title_size=15, body_size=12) # ── Safety Net ── safety_x, safety_y, safety_w, safety_h = 0.9, 5.15, 18.2, 1.9 ax.add_patch(FancyBboxPatch( (safety_x, safety_y), safety_w, safety_h, boxstyle="round,pad=0.06,rounding_size=0.15", facecolor=C["L2_safety_bg"], edgecolor=C["L2_safety_edge"], linewidth=s.lw(2), zorder=3, )) safety_header_h = 0.5 ax.add_patch(Rectangle( (safety_x + 0.1, safety_y + safety_h - safety_header_h - 0.05), safety_w - 0.2, safety_header_h - 0.05, facecolor=C["L2_safety_header"], edgecolor="none", alpha=0.92, zorder=4, )) ax.text( 10, safety_y + safety_h - safety_header_h / 2 - 0.05, "编译时四道安全网 Static Analysis", ha="center", va="center", fontsize=s.fs(15), fontweight="bold", color=C["text_light"], zorder=5, ) checks = [ ("Lint\n26 条规则", 2.0), ("Type Check\nT-Compose", 6.1), ("Cost Predict\n分级类型", 10.2), ("Store Indep.\nPair 汇合", 14.3), ] check_y, check_h = 5.25, 1.1 for label, cx in checks: ax.add_patch(FancyBboxPatch( (cx, check_y), 3.0, check_h, boxstyle="round,pad=0.04,rounding_size=0.1", facecolor=C["L2_safety_card"], edgecolor=C["L2_safety_edge"], linewidth=s.lw(1.5), zorder=5, )) ax.text( cx + 1.5, check_y + check_h / 2, label, ha="center", va="center", fontsize=s.fs(13), fontweight="bold", color=C["text"], zorder=6, linespacing=1.5, ) mid_y = check_y + check_h / 2 for i in range(3): x1 = checks[i][1] + 3.0 x2 = checks[i+1][1] ax.annotate( "", xy=(x2, mid_y), xytext=(x1, mid_y), arrowprops=dict(arrowstyle="->", color=C["L2_safety_edge"], lw=s.lw(2.5)), zorder=6, ) ax.annotate( "", xy=(18.0, mid_y), xytext=(17.3, mid_y), arrowprops=dict(arrowstyle="->", color=C["L2_safety_edge"], lw=s.lw(2.5)), zorder=6, ) ax.text(18.05, mid_y, " PASS", ha="left", va="center", fontsize=s.fs(14), fontweight="bold", color="#2E7D32", zorder=7) # L2 → L1 for x in (5, 10, 15): arrow_down(ax, s, x, 4.95, 4.55) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # L1 理论技术 y=1.5 ~ 4.5 h=3.0 header 0.55 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ layer(ax, s, 0.3, 1.5, 19.4, 3.0, 0.55, "L1 理论技术 (lambdagent / λA 演算)", C["L1_bg"], C["L1_edge"], C["L1_header"]) card(ax, s, 0.9, 2.55, 5.9, 1.35, "三层编程模型", ["Skill → Pattern → Orchestration"], C["L1_card"], C["L1_edge"], title_size=15, body_size=12) card(ax, s, 7.05, 2.55, 5.9, 1.35, "λA 构造子 11 + 5", ["Lam / Tool / Compose / Loop / Route / Pair / ..."], C["L1_card"], C["L1_edge"], title_size=15, body_size=12) card(ax, s, 13.2, 2.55, 5.9, 1.35, "6 个协作模式", ["review / fan_out / pipeline / escalation / ..."], C["L1_card"], C["L1_edge"], title_size=15, body_size=12) card(ax, s, 0.9, 1.6, 18.2, 0.9, "形式化理论基础 — 三篇论文", [], C["L1_paper"], C["L1_edge"], title_size=15) ax.text( 10, 1.78, "Paper I λA 类型系统 | Paper II 操作语义 + CEK | Paper III 类型与效果 + 分级类型", ha="center", va="center", fontsize=s.fs(12), color=C["text_mid"], style="italic", zorder=5, ) # L1 → L0 for x in (5, 10, 15): arrow_down(ax, s, x, 1.45, 1.15) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # L0 基础设施 y=0.1 ~ 1.1 h=1.0 header 0.4 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ layer(ax, s, 0.3, 0.1, 19.4, 1.0, 0.4, "L0 基础设施", C["L0_bg"], C["L0_edge"], C["L0_header"]) card(ax, s, 0.9, 0.2, 5.9, 0.48, "LLM Providers", [], C["L0_card"], C["L0_edge"], title_size=14) card(ax, s, 7.05, 0.2, 5.9, 0.48, "Storage", [], C["L0_card"], C["L0_edge"], title_size=14) card(ax, s, 13.2, 0.2, 5.9, 0.48, "Integration", [], C["L0_card"], C["L0_edge"], title_size=14) # 保存 os.makedirs(os.path.dirname(output), exist_ok=True) plt.savefig(output, dpi=180, bbox_inches="tight", facecolor="white", edgecolor="none") plt.close(fig) print(f"✅ Saved: {output} (scale={scale})") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Draw lambdagentpaas architecture diagram") parser.add_argument("--scale", type=float, default=1.0, help="全局缩放比例 (1.0=默认, 1.5=放大, 0.8=缩小)") parser.add_argument("--output", type=str, default=None, help="输出路径 (默认: docs/assets/lambdagentpaas-architecture.png)") args = parser.parse_args() here = os.path.dirname(os.path.abspath(__file__)) output = args.output or os.path.join(here, "lambdagentpaas-architecture.png") draw(output, scale=args.scale)