#!/usr/bin/env python3 """ Agent Architecture Diagram v2 - Refined Layout Focus: Agent perspective with clean separation of concerns """ from PIL import Image, ImageDraw, ImageFont import math W, H = 2800, 1800 BG = (15, 23, 35) img = Image.new('RGB', (W, H), BG) draw = ImageDraw.Draw(img) # ─── Colors ─── SB = (86, 119, 164) # Slate blue SL = (120, 155, 195) # Slate light AM = (224, 172, 68) # Amber AD = (180, 140, 55) # Amber dim GD = (92, 178, 120) # Green done YH = (218, 195, 78) # Yellow half RT = (198, 88, 82) # Red todo WH = (230, 235, 240) # White WD = (160, 170, 185) # White dim BD = (55, 72, 95) # Border FB = (100, 160, 220) # Flow blue FG = (80, 180, 130) # Flow green FR = (200, 100, 90) # Flow red AB = (25, 38, 55) # Agent bg GC = (30, 42, 58) # Grid # ─── Fonts ─── FD = "/Users/liulidong/.claude/plugins/cache/anthropic-agent-skills/document-skills/69c0b1a06741/skills/canvas-design/canvas-fonts" CF = "/System/Library/Fonts/PingFang.ttc" cn = lambda s: ImageFont.truetype(CF, s) en = lambda n, s: ImageFont.truetype(f"{FD}/{n}", s) ft = cn(36); fs = cn(20); fl = cn(15); fm = cn(12); fx = cn(11) et = en("Tektur-Medium.ttf", 26); es = en("InstrumentSans-Bold.ttf", 15) em = en("InstrumentSans-Regular.ttf", 12); ex = en("GeistMono-Regular.ttf", 11) ej = en("JetBrainsMono-Regular.ttf", 11); ejb = en("JetBrainsMono-Bold.ttf", 12) # ─── Helpers ─── def grid(): for x in range(0, W, 40): draw.line([(x, 0), (x, H)], fill=(25, 36, 52) if x % 200 else GC, width=1) for y in range(0, H, 40): draw.line([(0, y), (W, y)], fill=(25, 36, 52) if y % 200 else GC, width=1) def rr(x, y, w, h, r, fill, ol=None, ow=1): draw.rounded_rectangle([(x, y), (x+w, y+h)], radius=r, fill=fill, outline=ol, width=ow) def dot(x, y, st): c = {'done': GD, 'half': YH, 'todo': RT}.get(st, WD) draw.ellipse([(x-5, y-5), (x+5, y+5)], fill=c) def arrow(x1, y1, x2, y2, c, w=2, dash=False): dx, dy = x2-x1, y2-y1 L = math.sqrt(dx*dx+dy*dy) if L < 1: return if dash: for i in range(int(L/13)): t1, t2 = i*13/L, min((i*13+8)/L, 1.0) draw.line([(x1+dx*t1, y1+dy*t1), (x1+dx*t2, y1+dy*t2)], fill=c, width=w) else: draw.line([(x1, y1), (x2, y2)], fill=c, width=w) a = math.atan2(dy, dx) pts = [(x2, y2), (x2+10*math.cos(a+2.5), y2+10*math.sin(a+2.5)), (x2+10*math.cos(a-2.5), y2+10*math.sin(a-2.5))] draw.polygon(pts, fill=c) def bezier(x1, y1, x2, y2, c, w=2, cv=40): dx, dy = x2-x1, y2-y1 L = math.sqrt(dx*dx+dy*dy) if L < 1: return mx, my = (x1+x2)/2, (y1+y2)/2 cx, cy = mx + (-dy/L)*cv, my + (dx/L)*cv prev = (x1, y1) for i in range(1, 21): t = i/20 px = (1-t)**2*x1 + 2*(1-t)*t*cx + t**2*x2 py = (1-t)**2*y1 + 2*(1-t)*t*cy + t**2*y2 draw.line([prev, (px, py)], fill=c, width=w) prev = (px, py) tx, ty = 2*(x2-cx), 2*(y2-cy) a = math.atan2(ty, tx) pts = [(x2, y2), (x2+10*math.cos(a+2.5), y2+10*math.sin(a+2.5)), (x2+10*math.cos(a-2.5), y2+10*math.sin(a-2.5))] draw.polygon(pts, fill=c) # ═══ DRAW ═══ grid() # ─── Title Bar ─── draw.rectangle([(0, 0), (W, 65)], fill=(18, 28, 42)) draw.line([(0, 65), (W, 65)], fill=SB, width=2) draw.text((30, 14), "腾讯广告自动化投放 Agent 系统", font=ft, fill=WH) draw.text((530, 22), "TECHNICAL ARCHITECTURE", font=et, fill=SL) draw.text((W-340, 16), "Reson Agent Framework", font=es, fill=AD) draw.text((W-340, 36), "v3.0 · Agent Perspective", font=em, fill=WD) # ─── Legend ─── lx, ly = W-310, 78 rr(lx, ly, 280, 90, 6, (20, 30, 48), BD) draw.text((lx+10, ly+5), "图例 LEGEND", font=fm, fill=WD) for i, (lb, st) in enumerate([("已完成 Done", "done"), ("半完成 Partial", "half"), ("未完成 Todo", "todo")]): dot(lx+20, ly+28+i*20+6, st) draw.text((lx+32, ly+28+i*20), lb, font=fx, fill=WD) for i, (lb, c) in enumerate([("数据流", FB), ("执行流", FG), ("告警流", FR)]): draw.line([(lx+160, ly+30+i*20), (lx+200, ly+30+i*20)], fill=c, width=2) draw.text((lx+205, ly+24+i*20), lb, font=fx, fill=WD) # ═══ USER INPUT ═══ uy = 82 rr(30, uy, 240, 48, 8, (35, 50, 70), AM, 2) draw.text((48, uy+4), "用户输入 / 运营", font=fs, fill=AM) draw.text((48, uy+28), "\"今日预算10w\"", font=ex, fill=WD) rr(30, uy+56, 240, 32, 8, (35, 50, 70), FG, 1) draw.text((48, uy+60), "运营确认 → 执行", font=fl, fill=FG) # ═══ MAIN AGENT ═══ mx, my, mw, mh = 340, 80, 440, 90 rr(mx, my, mw, mh, 10, (30, 45, 65), AM, 2) dot(mx+15, my+15, 'half') draw.text((mx+28, my+6), "Main Agent · 投放决策中枢", font=fs, fill=AM) draw.text((mx+28, my+32), "task.prompt | 任务拆解与全局调度", font=fl, fill=WH) draw.text((mx+28, my+52), "temp 0.3(分析)/0.1(执行) · 单Agent运行", font=fm, fill=WD) draw.text((mx+28, my+70), "AgentRunner → LLM → Tool → Trace", font=ej, fill=SL) arrow(270, uy+24, mx, my+24, AM, 2) # ═══ SUB-AGENTS (3 cols × 2 rows, left 2/3 of canvas) ═══ cw, ch = 370, 210 gx = 25 sx = 40 r1y, r2y = 215, 460 agents = [ {'n': 'Budget Agent', 'c': '预算出价优化', 's': 'done', 'x': sx, 'y': r1y, 'd': '每日预算分配 · ROI计算 · 出价策略', 't': ['budget_calc.py', 'data_query.py'], 'td': ['ROI×跑量 二维矩阵, 5种动作', '冷启动保护: 48h+转化<6', '赔付门槛保护', '调价: 单次≤10%, 间隔≥2h', '出价边界: 10分~10000分'], 'sk': 'budget_strategy.md'}, {'n': 'Data Analyst', 'c': '数据查询分析(只读)', 's': 'done', 'x': sx+cw+gx, 'y': r1y, 'd': '只读查询 · 7种数据类型 · ODPS SQL', 't': ['data_query.py'], 'td': ['data_query(7种查询类型)', 'data_aggregate 聚合分析', 'get_ad_current_status', 'creative_detail SQL', 'ODPS/MaxCompute → loghubods'], 'sk': 'ad_domain.md'}, {'n': 'Audience Agent', 'c': '人群定向分析', 's': 'half', 'x': sx+(cw+gx)*2, 'y': r1y, 'd': '人群规划 · 圈选 · 定向策略', 't': ['audience_tools.py', 'data_query.py'], 'td': ['audience_build_targeting', 'audience_recommend_targeting', '年龄预设 18-24/25-34/35-49', 'custom_audience 人群包ID', '需求驱动 + 定向策略'], 'sk': 'audience_strategy.md'}, {'n': 'Creative Agent', 'c': '素材效果分析', 's': 'todo', 'x': sx, 'y': r2y, 'd': '素材供给 · 人群-素材匹配', 't': ['ad_api.py', 'data_query.py'], 'td': ['creative_create / update', '素材效果数据分析', '投放人群-素材匹配', '素材-视频匹配', '动态创意 dynamic_creatives'], 'sk': 'creative_strategy.md'}, {'n': 'System Ops', 'c': 'API操作执行', 's': 'todo', 'x': sx+cw+gx, 'y': r2y, 'd': '广告搭建 · 配置 · 调整 · 关停', 't': ['ad_api.py'], 'td': ['ad_create / ad_update', 'ad_batch_update_status', 'creative_create / update', 'account_get_info', '广告+创意配置+落地页'], 'sk': 'ad_domain.md'}, {'n': 'Monitor Agent', 'c': '异常检测与熔断', 's': 'todo', 'x': sx+(cw+gx)*2, 'y': r2y, 'd': '异常监控 · 熔断策略 · 变更通知', 't': ['monitor_tools.py', 'ad_api.py'], 'td': ['monitor_check_metrics', 'monitor_circuit_break', '异常暂停 AD_STATUS_SUSPEND', '操作变更通知', '消耗/CPA/预算异常'], 'sk': 'monitor_rules.md'}, ] for ag in agents: x, y = ag['x'], ag['y'] bc = {'done': GD, 'half': YH, 'todo': RT}[ag['s']] hc = {'done': (30, 55, 40), 'half': (50, 48, 25), 'todo': (55, 30, 30)}[ag['s']] rr(x, y, cw, ch, 8, AB, bc, 2) rr(x+2, y+2, cw-4, 32, 6, hc) dot(x+16, y+18, ag['s']) draw.text((x+28, y+7), ag['n'], font=es, fill=WH) # Position CN name after EN name en_w = draw.textbbox((0,0), ag['n'], font=es)[2] draw.text((x+28+en_w+8, y+8), ag['c'], font=fm, fill=bc) draw.text((x+12, y+38), ag['d'], font=fm, fill=WD) draw.line([(x+12, y+56), (x+cw-12, y+56)], fill=BD, width=1) draw.text((x+12, y+60), "Tools:", font=ex, fill=SL) for i, t in enumerate(ag['t']): draw.text((x+60+i*120, y+60), t, font=ej, fill=AD) for i, line in enumerate(ag['td']): draw.text((x+16, y+80+i*15), f"· {line}", font=fx, fill=WD) draw.text((x+12, y+ch-20), "Skill:", font=ex, fill=SL) draw.text((x+50, y+ch-20), ag['sk'], font=ej, fill=AD) # ═══ MAIN → SUB ARROWS ═══ mcx = mx + mw//2 mbot = my + mh for ag in agents[:3]: arrow(mcx, mbot, ag['x']+cw//2, ag['y'], AM, 2) for ag in agents[3:]: bezier(mcx, mbot, ag['x']+cw//2, ag['y'], AD, 1, cv=25) # ═══ INFRASTRUCTURE (Right column) ═══ ix = sx + (cw+gx)*3 + 30 # Right of all agent cards iy = 80 # ODPS rr(ix, iy, 440, 160, 8, (20, 35, 50), SB, 2) draw.text((ix+15, iy+8), "ODPS / MaxCompute 数据仓库", font=fs, fill=SL) draw.text((ix+15, iy+34), "loghubods 库", font=fl, fill=AM) for i, (t, d) in enumerate([ ("ad_put_tencent_ad", "广告基础"), ("ad_put_tencent_creative_data_day", "创意日报"), ("ad_put_tencent_account_data", "账户消耗"), ("fission_data / roi_data", "裂变&ROI"), ]): draw.text((ix+20, iy+56+i*22), f"├─ {t}", font=ej, fill=WD) draw.text((ix+300, iy+56+i*22), d, font=fx, fill=SL) # Tencent API ty = iy + 175 rr(ix, ty, 440, 140, 8, (20, 35, 50), SB, 2) draw.text((ix+15, ty+8), "腾讯广告 Marketing API v3.0", font=fs, fill=SL) draw.text((ix+15, ty+34), "2层: 广告(adgroups) → 创意(dynamic_creatives)", font=fm, fill=WH) for i, item in enumerate([ "/v3.0/adgroups/add|update|get", "/v3.0/dynamic_creatives/add|update", "/v3.0/adgroups/update_daily_budget", "bid_mode=OCPM QPS≤10 batch≤50", ]): draw.text((ix+20, ty+54+i*18), item, font=ej, fill=WD) # Business Rules ry = ty + 150 rr(ix, ry, 440, 230, 8, (20, 30, 48), BD) draw.text((ix+15, ry+6), "关键业务规则 Key Rules", font=fl, fill=AM) rules = [ ("─── oCPM 出价 ───", True), ("eCPM = bid × pCTR × pCVR × 1000", False), ("降10%出价 ≈ 降15-25%消耗(非线性)", False), ("掉量悬崖: eCPM<竞争水位→断崖", False), ("─── 决策矩阵 ROI×跑量 ───", True), ("高ROI+低跑量→increase +10%~15%", False), ("中ROI+高跑量→decrease -5%~10%", False), ("低ROI+低跑量→close(标记关停)", False), ("─── 保护机制 ───", True), ("冷启动: <48h∨转化<6→observe", False), ("赔付: 转化≥6且CPA偏离≥20%→先赔付", False), ("熔断: CPA>阈值∨消耗异常→暂停", False), ] for i, (r, is_h) in enumerate(rules): draw.text((ix+15, ry+26+i*17), r, font=fx if not is_h else fm, fill=AM if is_h else WD) # Data Flow Summary fy = ry + 240 rr(ix, fy, 440, 120, 8, (20, 30, 48), BD) draw.text((ix+15, fy+8), "核心数据流 Core Flows", font=fl, fill=AM) flows = [ ("① 用户输入", "→ Main → 子Agent分发", AM), ("② Budget", "→ data_query(ODPS) → budget_calc", FB), ("③ 运营确认", "→ System Ops → ad_api(腾讯)", FG), ("④ Monitor", "→ ad_api(熔断) → 异常暂停", FR), ("⑤ 反馈", "→ 投放数据 → 策略归纳 → 优化", SL), ] for i, (lb, d, c) in enumerate(flows): draw.text((ix+15, fy+30+i*19), lb, font=fm, fill=c) draw.text((ix+110, fy+30+i*19), d, font=fx, fill=WD) # ═══ DATA FLOW ARROWS (Agents → Infrastructure) ═══ # Data Analyst → ODPS da = agents[1] arrow(da['x']+cw, da['y']+ch//2, ix, iy+80, FB, 2) draw.text((da['x']+cw+8, da['y']+ch//2-16), "SQL Query", font=ex, fill=FB) # Budget → Data Analyst (inter-agent) ba, daa = agents[0], agents[1] arrow(ba['x']+cw, ba['y']+110, daa['x'], daa['y']+110, FB, 2) draw.text((ba['x']+cw+4, ba['y']+96), "查询数据", font=fx, fill=FB) # System Ops → Tencent API sa = agents[4] arrow(sa['x']+cw, sa['y']+ch//2, ix, ty+70, FG, 2) draw.text((sa['x']+cw+8, sa['y']+ch//2-16), "API Call", font=ex, fill=FG) # Monitor → Tencent API (circuit break) ma = agents[5] arrow(ma['x']+cw, ma['y']+ch//3, ix, ty+110, FR, 2) draw.text((ma['x']+cw+8, ma['y']+ch//3-16), "熔断", font=fx, fill=FR) # Creative → Tencent API ca = agents[3] bezier(ca['x']+cw, ca['y']+ch//2, ix, ty+40, FG, 1, cv=35) # Audience → Creative (人群-素材匹配) aa = agents[2] bezier(aa['x']+cw//3, aa['y']+ch, ca['x']+cw//3, ca['y'], YH, 1, cv=-40) draw.text((aa['x']+20, aa['y']+ch+4), "人群-素材匹配", font=fx, fill=YH) # User confirm → System Ops bezier(150, uy+88, sa['x']+cw//2, sa['y'], FG, 2, cv=-80) # ═══ SKILLS BAR ═══ sky = 690 rr(30, sky, (cw+gx)*3+10, 22, 4, (30, 40, 55), BD) draw.text((40, sky+3), "Skills:", font=ex, fill=SL) for i, sk in enumerate(["ad_domain.md", "budget_strategy.md", "audience_strategy.md", "creative_strategy.md", "monitor_rules.md"]): draw.text((90+i*195, sky+3), sk, font=ej, fill=AD) draw.text((40, sky-14), "↑ 自动注入 Auto-inject into Agent context", font=fx, fill=SL) # ═══ FRAMEWORK BAR (Bottom) ═══ fwy = 730 rr(30, fwy, W-60, 60, 8, (18, 26, 40), SB, 1) draw.text((50, fwy+5), "底层框架 Reson Agent Framework", font=fs, fill=SL) fw_items = [("AgentRunner", "LLM循环引擎"), ("LLM Adapters", "Qwen/Gemini"), ("Tool Registry", "@tool注册"), ("Skill System", "知识注入"), ("Trace", "GoalTree/回溯"), ("Knowledge", "Config")] fw_start = 50 fw_gap = 190 for i, (n, d) in enumerate(fw_items): xx = fw_start + i * fw_gap draw.text((xx, fwy+28), n, font=es, fill=AM) draw.text((xx, fwy+44), d, font=fx, fill=WD) # Workflow modes rr(fw_start + len(fw_items)*fw_gap + 20, fwy+10, 220, 18, 4, (25, 45, 35), GD, 1) draw.text((fw_start + len(fw_items)*fw_gap + 28, fwy+11), "分析 Analysis temp=0.3", font=fx, fill=GD) rr(fw_start + len(fw_items)*fw_gap + 250, fwy+10, 220, 18, 4, (45, 35, 25), (200, 120, 100), 1) draw.text((fw_start + len(fw_items)*fw_gap + 258, fwy+11), "执行 Execution temp=0.1", font=fx, fill=(200, 120, 100)) arrow(fw_start + len(fw_items)*fw_gap + 240, fwy+19, fw_start + len(fw_items)*fw_gap + 250, fwy+19, WD, 1) rr(fw_start + len(fw_items)*fw_gap + 20, fwy+32, 220, 18, 4, (25, 38, 55), BD) draw.text((fw_start + len(fw_items)*fw_gap + 28, fwy+33), "只读工具 read-only", font=fx, fill=WD) rr(fw_start + len(fw_items)*fw_gap + 250, fwy+32, 220, 18, 4, (25, 38, 55), BD) draw.text((fw_start + len(fw_items)*fw_gap + 258, fwy+33), "读写工具 read-write", font=fx, fill=WD) # ═══ BORDER ═══ draw.rectangle([(0, 0), (W-1, H-1)], outline=SB, width=2) # Crop to actual content height (framework ends at ~795) final_h = 810 img2 = img.crop((0, 0, W, final_h)) out = "/Users/liulidong/project/agent/Agent/outputs/agent_architecture.png" img2.save(out, "PNG", dpi=(150, 150)) print(f"Saved: {out} ({W}x{final_h})")