gen_arch_diagram.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. #!/usr/bin/env python3
  2. """
  3. Agent Architecture Diagram v2 - Refined Layout
  4. Focus: Agent perspective with clean separation of concerns
  5. """
  6. from PIL import Image, ImageDraw, ImageFont
  7. import math
  8. W, H = 2800, 1800
  9. BG = (15, 23, 35)
  10. img = Image.new('RGB', (W, H), BG)
  11. draw = ImageDraw.Draw(img)
  12. # ─── Colors ───
  13. SB = (86, 119, 164) # Slate blue
  14. SL = (120, 155, 195) # Slate light
  15. AM = (224, 172, 68) # Amber
  16. AD = (180, 140, 55) # Amber dim
  17. GD = (92, 178, 120) # Green done
  18. YH = (218, 195, 78) # Yellow half
  19. RT = (198, 88, 82) # Red todo
  20. WH = (230, 235, 240) # White
  21. WD = (160, 170, 185) # White dim
  22. BD = (55, 72, 95) # Border
  23. FB = (100, 160, 220) # Flow blue
  24. FG = (80, 180, 130) # Flow green
  25. FR = (200, 100, 90) # Flow red
  26. AB = (25, 38, 55) # Agent bg
  27. GC = (30, 42, 58) # Grid
  28. # ─── Fonts ───
  29. FD = "/Users/liulidong/.claude/plugins/cache/anthropic-agent-skills/document-skills/69c0b1a06741/skills/canvas-design/canvas-fonts"
  30. CF = "/System/Library/Fonts/PingFang.ttc"
  31. cn = lambda s: ImageFont.truetype(CF, s)
  32. en = lambda n, s: ImageFont.truetype(f"{FD}/{n}", s)
  33. ft = cn(36); fs = cn(20); fl = cn(15); fm = cn(12); fx = cn(11)
  34. et = en("Tektur-Medium.ttf", 26); es = en("InstrumentSans-Bold.ttf", 15)
  35. em = en("InstrumentSans-Regular.ttf", 12); ex = en("GeistMono-Regular.ttf", 11)
  36. ej = en("JetBrainsMono-Regular.ttf", 11); ejb = en("JetBrainsMono-Bold.ttf", 12)
  37. # ─── Helpers ───
  38. def grid():
  39. for x in range(0, W, 40):
  40. draw.line([(x, 0), (x, H)], fill=(25, 36, 52) if x % 200 else GC, width=1)
  41. for y in range(0, H, 40):
  42. draw.line([(0, y), (W, y)], fill=(25, 36, 52) if y % 200 else GC, width=1)
  43. def rr(x, y, w, h, r, fill, ol=None, ow=1):
  44. draw.rounded_rectangle([(x, y), (x+w, y+h)], radius=r, fill=fill, outline=ol, width=ow)
  45. def dot(x, y, st):
  46. c = {'done': GD, 'half': YH, 'todo': RT}.get(st, WD)
  47. draw.ellipse([(x-5, y-5), (x+5, y+5)], fill=c)
  48. def arrow(x1, y1, x2, y2, c, w=2, dash=False):
  49. dx, dy = x2-x1, y2-y1
  50. L = math.sqrt(dx*dx+dy*dy)
  51. if L < 1: return
  52. if dash:
  53. for i in range(int(L/13)):
  54. t1, t2 = i*13/L, min((i*13+8)/L, 1.0)
  55. draw.line([(x1+dx*t1, y1+dy*t1), (x1+dx*t2, y1+dy*t2)], fill=c, width=w)
  56. else:
  57. draw.line([(x1, y1), (x2, y2)], fill=c, width=w)
  58. a = math.atan2(dy, dx)
  59. 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))]
  60. draw.polygon(pts, fill=c)
  61. def bezier(x1, y1, x2, y2, c, w=2, cv=40):
  62. dx, dy = x2-x1, y2-y1
  63. L = math.sqrt(dx*dx+dy*dy)
  64. if L < 1: return
  65. mx, my = (x1+x2)/2, (y1+y2)/2
  66. cx, cy = mx + (-dy/L)*cv, my + (dx/L)*cv
  67. prev = (x1, y1)
  68. for i in range(1, 21):
  69. t = i/20
  70. px = (1-t)**2*x1 + 2*(1-t)*t*cx + t**2*x2
  71. py = (1-t)**2*y1 + 2*(1-t)*t*cy + t**2*y2
  72. draw.line([prev, (px, py)], fill=c, width=w)
  73. prev = (px, py)
  74. tx, ty = 2*(x2-cx), 2*(y2-cy)
  75. a = math.atan2(ty, tx)
  76. 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))]
  77. draw.polygon(pts, fill=c)
  78. # ═══ DRAW ═══
  79. grid()
  80. # ─── Title Bar ───
  81. draw.rectangle([(0, 0), (W, 65)], fill=(18, 28, 42))
  82. draw.line([(0, 65), (W, 65)], fill=SB, width=2)
  83. draw.text((30, 14), "腾讯广告自动化投放 Agent 系统", font=ft, fill=WH)
  84. draw.text((530, 22), "TECHNICAL ARCHITECTURE", font=et, fill=SL)
  85. draw.text((W-340, 16), "Reson Agent Framework", font=es, fill=AD)
  86. draw.text((W-340, 36), "v3.0 · Agent Perspective", font=em, fill=WD)
  87. # ─── Legend ───
  88. lx, ly = W-310, 78
  89. rr(lx, ly, 280, 90, 6, (20, 30, 48), BD)
  90. draw.text((lx+10, ly+5), "图例 LEGEND", font=fm, fill=WD)
  91. for i, (lb, st) in enumerate([("已完成 Done", "done"), ("半完成 Partial", "half"), ("未完成 Todo", "todo")]):
  92. dot(lx+20, ly+28+i*20+6, st)
  93. draw.text((lx+32, ly+28+i*20), lb, font=fx, fill=WD)
  94. for i, (lb, c) in enumerate([("数据流", FB), ("执行流", FG), ("告警流", FR)]):
  95. draw.line([(lx+160, ly+30+i*20), (lx+200, ly+30+i*20)], fill=c, width=2)
  96. draw.text((lx+205, ly+24+i*20), lb, font=fx, fill=WD)
  97. # ═══ USER INPUT ═══
  98. uy = 82
  99. rr(30, uy, 240, 48, 8, (35, 50, 70), AM, 2)
  100. draw.text((48, uy+4), "用户输入 / 运营", font=fs, fill=AM)
  101. draw.text((48, uy+28), "\"今日预算10w\"", font=ex, fill=WD)
  102. rr(30, uy+56, 240, 32, 8, (35, 50, 70), FG, 1)
  103. draw.text((48, uy+60), "运营确认 → 执行", font=fl, fill=FG)
  104. # ═══ MAIN AGENT ═══
  105. mx, my, mw, mh = 340, 80, 440, 90
  106. rr(mx, my, mw, mh, 10, (30, 45, 65), AM, 2)
  107. dot(mx+15, my+15, 'half')
  108. draw.text((mx+28, my+6), "Main Agent · 投放决策中枢", font=fs, fill=AM)
  109. draw.text((mx+28, my+32), "task.prompt | 任务拆解与全局调度", font=fl, fill=WH)
  110. draw.text((mx+28, my+52), "temp 0.3(分析)/0.1(执行) · 单Agent运行", font=fm, fill=WD)
  111. draw.text((mx+28, my+70), "AgentRunner → LLM → Tool → Trace", font=ej, fill=SL)
  112. arrow(270, uy+24, mx, my+24, AM, 2)
  113. # ═══ SUB-AGENTS (3 cols × 2 rows, left 2/3 of canvas) ═══
  114. cw, ch = 370, 210
  115. gx = 25
  116. sx = 40
  117. r1y, r2y = 215, 460
  118. agents = [
  119. {'n': 'Budget Agent', 'c': '预算出价优化', 's': 'done', 'x': sx, 'y': r1y,
  120. 'd': '每日预算分配 · ROI计算 · 出价策略',
  121. 't': ['budget_calc.py', 'data_query.py'],
  122. 'td': ['ROI×跑量 二维矩阵, 5种动作', '冷启动保护: 48h+转化<6', '赔付门槛保护', '调价: 单次≤10%, 间隔≥2h', '出价边界: 10分~10000分'],
  123. 'sk': 'budget_strategy.md'},
  124. {'n': 'Data Analyst', 'c': '数据查询分析(只读)', 's': 'done', 'x': sx+cw+gx, 'y': r1y,
  125. 'd': '只读查询 · 7种数据类型 · ODPS SQL',
  126. 't': ['data_query.py'],
  127. 'td': ['data_query(7种查询类型)', 'data_aggregate 聚合分析', 'get_ad_current_status', 'creative_detail SQL', 'ODPS/MaxCompute → loghubods'],
  128. 'sk': 'ad_domain.md'},
  129. {'n': 'Audience Agent', 'c': '人群定向分析', 's': 'half', 'x': sx+(cw+gx)*2, 'y': r1y,
  130. 'd': '人群规划 · 圈选 · 定向策略',
  131. 't': ['audience_tools.py', 'data_query.py'],
  132. 'td': ['audience_build_targeting', 'audience_recommend_targeting', '年龄预设 18-24/25-34/35-49', 'custom_audience 人群包ID', '需求驱动 + 定向策略'],
  133. 'sk': 'audience_strategy.md'},
  134. {'n': 'Creative Agent', 'c': '素材效果分析', 's': 'todo', 'x': sx, 'y': r2y,
  135. 'd': '素材供给 · 人群-素材匹配',
  136. 't': ['ad_api.py', 'data_query.py'],
  137. 'td': ['creative_create / update', '素材效果数据分析', '投放人群-素材匹配', '素材-视频匹配', '动态创意 dynamic_creatives'],
  138. 'sk': 'creative_strategy.md'},
  139. {'n': 'System Ops', 'c': 'API操作执行', 's': 'todo', 'x': sx+cw+gx, 'y': r2y,
  140. 'd': '广告搭建 · 配置 · 调整 · 关停',
  141. 't': ['ad_api.py'],
  142. 'td': ['ad_create / ad_update', 'ad_batch_update_status', 'creative_create / update', 'account_get_info', '广告+创意配置+落地页'],
  143. 'sk': 'ad_domain.md'},
  144. {'n': 'Monitor Agent', 'c': '异常检测与熔断', 's': 'todo', 'x': sx+(cw+gx)*2, 'y': r2y,
  145. 'd': '异常监控 · 熔断策略 · 变更通知',
  146. 't': ['monitor_tools.py', 'ad_api.py'],
  147. 'td': ['monitor_check_metrics', 'monitor_circuit_break', '异常暂停 AD_STATUS_SUSPEND', '操作变更通知', '消耗/CPA/预算异常'],
  148. 'sk': 'monitor_rules.md'},
  149. ]
  150. for ag in agents:
  151. x, y = ag['x'], ag['y']
  152. bc = {'done': GD, 'half': YH, 'todo': RT}[ag['s']]
  153. hc = {'done': (30, 55, 40), 'half': (50, 48, 25), 'todo': (55, 30, 30)}[ag['s']]
  154. rr(x, y, cw, ch, 8, AB, bc, 2)
  155. rr(x+2, y+2, cw-4, 32, 6, hc)
  156. dot(x+16, y+18, ag['s'])
  157. draw.text((x+28, y+7), ag['n'], font=es, fill=WH)
  158. # Position CN name after EN name
  159. en_w = draw.textbbox((0,0), ag['n'], font=es)[2]
  160. draw.text((x+28+en_w+8, y+8), ag['c'], font=fm, fill=bc)
  161. draw.text((x+12, y+38), ag['d'], font=fm, fill=WD)
  162. draw.line([(x+12, y+56), (x+cw-12, y+56)], fill=BD, width=1)
  163. draw.text((x+12, y+60), "Tools:", font=ex, fill=SL)
  164. for i, t in enumerate(ag['t']):
  165. draw.text((x+60+i*120, y+60), t, font=ej, fill=AD)
  166. for i, line in enumerate(ag['td']):
  167. draw.text((x+16, y+80+i*15), f"· {line}", font=fx, fill=WD)
  168. draw.text((x+12, y+ch-20), "Skill:", font=ex, fill=SL)
  169. draw.text((x+50, y+ch-20), ag['sk'], font=ej, fill=AD)
  170. # ═══ MAIN → SUB ARROWS ═══
  171. mcx = mx + mw//2
  172. mbot = my + mh
  173. for ag in agents[:3]:
  174. arrow(mcx, mbot, ag['x']+cw//2, ag['y'], AM, 2)
  175. for ag in agents[3:]:
  176. bezier(mcx, mbot, ag['x']+cw//2, ag['y'], AD, 1, cv=25)
  177. # ═══ INFRASTRUCTURE (Right column) ═══
  178. ix = sx + (cw+gx)*3 + 30 # Right of all agent cards
  179. iy = 80
  180. # ODPS
  181. rr(ix, iy, 440, 160, 8, (20, 35, 50), SB, 2)
  182. draw.text((ix+15, iy+8), "ODPS / MaxCompute 数据仓库", font=fs, fill=SL)
  183. draw.text((ix+15, iy+34), "loghubods 库", font=fl, fill=AM)
  184. for i, (t, d) in enumerate([
  185. ("ad_put_tencent_ad", "广告基础"),
  186. ("ad_put_tencent_creative_data_day", "创意日报"),
  187. ("ad_put_tencent_account_data", "账户消耗"),
  188. ("fission_data / roi_data", "裂变&ROI"),
  189. ]):
  190. draw.text((ix+20, iy+56+i*22), f"├─ {t}", font=ej, fill=WD)
  191. draw.text((ix+300, iy+56+i*22), d, font=fx, fill=SL)
  192. # Tencent API
  193. ty = iy + 175
  194. rr(ix, ty, 440, 140, 8, (20, 35, 50), SB, 2)
  195. draw.text((ix+15, ty+8), "腾讯广告 Marketing API v3.0", font=fs, fill=SL)
  196. draw.text((ix+15, ty+34), "2层: 广告(adgroups) → 创意(dynamic_creatives)", font=fm, fill=WH)
  197. for i, item in enumerate([
  198. "/v3.0/adgroups/add|update|get",
  199. "/v3.0/dynamic_creatives/add|update",
  200. "/v3.0/adgroups/update_daily_budget",
  201. "bid_mode=OCPM QPS≤10 batch≤50",
  202. ]):
  203. draw.text((ix+20, ty+54+i*18), item, font=ej, fill=WD)
  204. # Business Rules
  205. ry = ty + 150
  206. rr(ix, ry, 440, 230, 8, (20, 30, 48), BD)
  207. draw.text((ix+15, ry+6), "关键业务规则 Key Rules", font=fl, fill=AM)
  208. rules = [
  209. ("─── oCPM 出价 ───", True),
  210. ("eCPM = bid × pCTR × pCVR × 1000", False),
  211. ("降10%出价 ≈ 降15-25%消耗(非线性)", False),
  212. ("掉量悬崖: eCPM<竞争水位→断崖", False),
  213. ("─── 决策矩阵 ROI×跑量 ───", True),
  214. ("高ROI+低跑量→increase +10%~15%", False),
  215. ("中ROI+高跑量→decrease -5%~10%", False),
  216. ("低ROI+低跑量→close(标记关停)", False),
  217. ("─── 保护机制 ───", True),
  218. ("冷启动: <48h∨转化<6→observe", False),
  219. ("赔付: 转化≥6且CPA偏离≥20%→先赔付", False),
  220. ("熔断: CPA>阈值∨消耗异常→暂停", False),
  221. ]
  222. for i, (r, is_h) in enumerate(rules):
  223. draw.text((ix+15, ry+26+i*17), r, font=fx if not is_h else fm, fill=AM if is_h else WD)
  224. # Data Flow Summary
  225. fy = ry + 240
  226. rr(ix, fy, 440, 120, 8, (20, 30, 48), BD)
  227. draw.text((ix+15, fy+8), "核心数据流 Core Flows", font=fl, fill=AM)
  228. flows = [
  229. ("① 用户输入", "→ Main → 子Agent分发", AM),
  230. ("② Budget", "→ data_query(ODPS) → budget_calc", FB),
  231. ("③ 运营确认", "→ System Ops → ad_api(腾讯)", FG),
  232. ("④ Monitor", "→ ad_api(熔断) → 异常暂停", FR),
  233. ("⑤ 反馈", "→ 投放数据 → 策略归纳 → 优化", SL),
  234. ]
  235. for i, (lb, d, c) in enumerate(flows):
  236. draw.text((ix+15, fy+30+i*19), lb, font=fm, fill=c)
  237. draw.text((ix+110, fy+30+i*19), d, font=fx, fill=WD)
  238. # ═══ DATA FLOW ARROWS (Agents → Infrastructure) ═══
  239. # Data Analyst → ODPS
  240. da = agents[1]
  241. arrow(da['x']+cw, da['y']+ch//2, ix, iy+80, FB, 2)
  242. draw.text((da['x']+cw+8, da['y']+ch//2-16), "SQL Query", font=ex, fill=FB)
  243. # Budget → Data Analyst (inter-agent)
  244. ba, daa = agents[0], agents[1]
  245. arrow(ba['x']+cw, ba['y']+110, daa['x'], daa['y']+110, FB, 2)
  246. draw.text((ba['x']+cw+4, ba['y']+96), "查询数据", font=fx, fill=FB)
  247. # System Ops → Tencent API
  248. sa = agents[4]
  249. arrow(sa['x']+cw, sa['y']+ch//2, ix, ty+70, FG, 2)
  250. draw.text((sa['x']+cw+8, sa['y']+ch//2-16), "API Call", font=ex, fill=FG)
  251. # Monitor → Tencent API (circuit break)
  252. ma = agents[5]
  253. arrow(ma['x']+cw, ma['y']+ch//3, ix, ty+110, FR, 2)
  254. draw.text((ma['x']+cw+8, ma['y']+ch//3-16), "熔断", font=fx, fill=FR)
  255. # Creative → Tencent API
  256. ca = agents[3]
  257. bezier(ca['x']+cw, ca['y']+ch//2, ix, ty+40, FG, 1, cv=35)
  258. # Audience → Creative (人群-素材匹配)
  259. aa = agents[2]
  260. bezier(aa['x']+cw//3, aa['y']+ch, ca['x']+cw//3, ca['y'], YH, 1, cv=-40)
  261. draw.text((aa['x']+20, aa['y']+ch+4), "人群-素材匹配", font=fx, fill=YH)
  262. # User confirm → System Ops
  263. bezier(150, uy+88, sa['x']+cw//2, sa['y'], FG, 2, cv=-80)
  264. # ═══ SKILLS BAR ═══
  265. sky = 690
  266. rr(30, sky, (cw+gx)*3+10, 22, 4, (30, 40, 55), BD)
  267. draw.text((40, sky+3), "Skills:", font=ex, fill=SL)
  268. for i, sk in enumerate(["ad_domain.md", "budget_strategy.md", "audience_strategy.md", "creative_strategy.md", "monitor_rules.md"]):
  269. draw.text((90+i*195, sky+3), sk, font=ej, fill=AD)
  270. draw.text((40, sky-14), "↑ 自动注入 Auto-inject into Agent context", font=fx, fill=SL)
  271. # ═══ FRAMEWORK BAR (Bottom) ═══
  272. fwy = 730
  273. rr(30, fwy, W-60, 60, 8, (18, 26, 40), SB, 1)
  274. draw.text((50, fwy+5), "底层框架 Reson Agent Framework", font=fs, fill=SL)
  275. fw_items = [("AgentRunner", "LLM循环引擎"), ("LLM Adapters", "Qwen/Gemini"),
  276. ("Tool Registry", "@tool注册"), ("Skill System", "知识注入"),
  277. ("Trace", "GoalTree/回溯"), ("Knowledge", "Config")]
  278. fw_start = 50
  279. fw_gap = 190
  280. for i, (n, d) in enumerate(fw_items):
  281. xx = fw_start + i * fw_gap
  282. draw.text((xx, fwy+28), n, font=es, fill=AM)
  283. draw.text((xx, fwy+44), d, font=fx, fill=WD)
  284. # Workflow modes
  285. rr(fw_start + len(fw_items)*fw_gap + 20, fwy+10, 220, 18, 4, (25, 45, 35), GD, 1)
  286. draw.text((fw_start + len(fw_items)*fw_gap + 28, fwy+11), "分析 Analysis temp=0.3", font=fx, fill=GD)
  287. rr(fw_start + len(fw_items)*fw_gap + 250, fwy+10, 220, 18, 4, (45, 35, 25), (200, 120, 100), 1)
  288. draw.text((fw_start + len(fw_items)*fw_gap + 258, fwy+11), "执行 Execution temp=0.1", font=fx, fill=(200, 120, 100))
  289. arrow(fw_start + len(fw_items)*fw_gap + 240, fwy+19, fw_start + len(fw_items)*fw_gap + 250, fwy+19, WD, 1)
  290. rr(fw_start + len(fw_items)*fw_gap + 20, fwy+32, 220, 18, 4, (25, 38, 55), BD)
  291. draw.text((fw_start + len(fw_items)*fw_gap + 28, fwy+33), "只读工具 read-only", font=fx, fill=WD)
  292. rr(fw_start + len(fw_items)*fw_gap + 250, fwy+32, 220, 18, 4, (25, 38, 55), BD)
  293. draw.text((fw_start + len(fw_items)*fw_gap + 258, fwy+33), "读写工具 read-write", font=fx, fill=WD)
  294. # ═══ BORDER ═══
  295. draw.rectangle([(0, 0), (W-1, H-1)], outline=SB, width=2)
  296. # Crop to actual content height (framework ends at ~795)
  297. final_h = 810
  298. img2 = img.crop((0, 0, W, final_h))
  299. out = "/Users/liulidong/project/agent/Agent/outputs/agent_architecture.png"
  300. img2.save(out, "PNG", dpi=(150, 150))
  301. print(f"Saved: {out} ({W}x{final_h})")