| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- #!/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})")
|