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