Files
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

1053 lines
49 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""[DEPRECATED] Old Chapter 9 (multi-Agent) figure generator.
⚠️ DO NOT RUN: After the 2026-03-12 refactoring, chapter numbers changed (original Ch 9 → Ch 10).
The figures generated by this file actually correspond to the current [Chapter 10] content (multi-Agent collaboration), but are still saved as fig9-*.svg.
Running it would overwrite the current correct [Chapter 9] (multimodal & real-time interaction) figures, causing content misalignment.
The current correct Chapter 9 SVGs (multimodal/voice/Computer Use/VLA/Sim2Real) were
renamed from original fig8-*.svg in commit a33c88f on 2026-03-12; the current correct Chapter 10 SVGs
(multi-Agent collaboration) were **manually migrated** from the content originally generated by this file to fig10-*.svg.
To regenerate figures from this file, you should instead generate fig10-*.svg and reference them according to chapter10.md.
Original (now Ch 10) figure list:
fig10-1: Shared context vs independent context (concrete context windows)
fig10-2: Phase-based role switching (prompt/tool-set changes per phase)
fig10-3: Proposer-Reviewer loop (Slidev PPT iterative feedback)
fig10-4: Manager sequential coordination (sequential sub-agent delegation)
fig10-5: Book translation Agent architecture (NEW — Exp 10.4)
fig10-6: Manager parallel coordination (concurrent agents + message bus)
fig10-7: Phone + Computer dual Agent (NEW — Exp 10.5/10.6)
fig10-8: Parallel web scraping (NEW — Exp 10.7)
fig10-9: Handoff chain pattern (peer control passing)
fig10-10: MetaGPT SOP pipeline (PM→Arch→Eng→QA with artifacts)
fig10-11: AI town architecture (memory stream + reflection + planning)
fig10-12: Voice Werewolf Agent system (NEW — Exp 10.9)
"""
import sys
print(
"ERROR: gen_ch9_figs.py is DEPRECATED. Running it would overwrite the\n"
"correct Chapter 9 (multimodal) figures with old Chapter 10 (multi-agent) content.\n"
"See module docstring at the top of this file for the rename history.",
file=sys.stderr,
)
sys.exit(2)
import sys, os
sys.path.insert(0, os.path.dirname(__file__))
from svg_lib import (
SVG, COLORS, FONT, MONO,
FS_TITLE, FS_BODY, FS_SMALL, FS_TINY, FS_LABEL, STROKE_W, CORNER_R,
_escape,
)
OUT = os.path.join(os.path.dirname(__file__), 'images')
# ════════════════════════════════════════════════════════════════════
# fig9-1: Shared context vs independent context
# ════════════════════════════════════════════════════════════════════
def fig9_1():
W, H = 780, 560
s = SVG(W, H)
s.text(W // 2, 30, 'Shared context vs independent context', size=FS_TITLE, bold=True)
col_w = 350
lx, rx = 20, W - col_w - 20
# ── Left: shared context ──
s.group_box(lx, 55, col_w, 480, 'Shared context (single Agent, multiple phases)')
ctx_x, ctx_w = lx + 15, col_w - 30
phases = [
('Phase 1: Requirements Analyst', 'medium', [
'sys: "Your responsibility is to fully understand the requirements..."',
'tools: [ask_question, save_req]',
'user: "Write a CSV analysis script"',
'agent: "What file types need to be processed?"',
]),
('Phase 2: Software Engineer', 'light', [
'sys: "Write code based on confirmed requirements..."',
'tools: [write_file, execute_code]',
'agent: write_file("analyze.py", ...)',
'agent: execute_code("python test.py")',
]),
('Phase 3: Code Reviewer', 'light', [
'sys: "Review code quality and security..."',
'tools: [run_linter, run_tests]',
'agent: run_linter → 2 warnings',
'agent: approve_code()',
]),
]
cy = 82
for title, fill, lines in phases:
ph = 18 + len(lines) * 18 + 10
s.rect(ctx_x, cy, ctx_w, ph, fill=fill, rx=4)
s.text(ctx_x + 8, cy + 14, title, size=FS_SMALL, bold=True, anchor='start')
for i, ln in enumerate(lines):
s.mono(ctx_x + 12, cy + 32 + i * 18, ln, size=12)
cy += ph + 2
s.rect(ctx_x, cy, ctx_w, 28, fill='code_bg', rx=3)
s.text(ctx_x + ctx_w // 2, cy + 14, '↑ All phases share the same conversation history', size=FS_TINY, bold=True)
cy += 36
s.text(lx + col_w // 2, cy + 10, '✓ Complete execution trace', size=FS_SMALL, fill='text_light')
s.text(lx + col_w // 2, cy + 32, '✗ Context expands rapidly', size=FS_SMALL, fill='text_light')
# ── Right: independent context ──
s.group_box(rx, 55, col_w, 480, 'Independent context (true multi-Agent)')
agents_data = [
('Glossary Agent', [
'sys: "Identify terms and translate..."',
'tools: [search_dict, write_file]',
'→ glossary.json',
]),
('Translation Agent', [
'sys: "Translate this chapter..."',
'tools: [read_file, write_file]',
'→ chapter3_zh.md',
]),
('Proofreading Agent', [
'sys: "Check terminology consistency..."',
'tools: [read_file, write_file]',
'→ review_report.md',
]),
]
ay = 82
for name, lines in agents_data:
ah = 18 + len(lines) * 18 + 8
s.rect(rx + 15, ay, ctx_w, ah, fill='light', rx=4)
s.text(rx + 23, ay + 14, name, size=FS_SMALL, bold=True, anchor='start')
for i, ln in enumerate(lines):
s.mono(rx + 27, ay + 32 + i * 18, ln, size=12)
ay += ah + 8
fs_y = ay + 5
s.rect(rx + 15, fs_y, ctx_w, 65, fill='medium', rx=4)
s.text(rx + 15 + ctx_w // 2, fs_y + 16, 'Shared file system', size=FS_SMALL, bold=True)
files = ['glossary.json', 'chapter3_zh.md', 'review_report.md']
s.mono(rx + 27, fs_y + 38, ' '.join(files), size=11)
s.text(rx + 15 + ctx_w // 2, fs_y + 55, '+ Tool call parameters pass structured data', size=FS_TINY, fill='text_light')
s.text(rx + col_w // 2, fs_y + 82, '✓ Modular · Extensible · Parallel', size=FS_SMALL, fill='text_light')
s.text(rx + col_w // 2, fs_y + 104, '✗ Information synchronization complex', size=FS_SMALL, fill='text_light')
s.save(os.path.join(OUT, 'fig9-1.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-2: Phase-based role switching
# ════════════════════════════════════════════════════════════════════
def fig9_2():
W, H = 780, 520
s = SVG(W, H)
s.text(W // 2, 28, 'Phase-based role switching: Coding Agent three phases', size=FS_TITLE, bold=True)
phases = [
('Requirements Analyst', 'medium',
'"Your responsibility is to fully understand the requirements.\nDo not rush to implement; at this stage\nyour task is to ask questions and confirm."',
['ask_clarifying_question(q)', 'save_requirement(k, v)', 'complete_requirements_analysis()'],
'complete_requirements_analysis()'),
('Software Engineer', 'light',
'"Write high-quality Python code based on the confirmed requirements.\nFollow modular design and error handling best practices."',
['write_file(path, content)', 'read_file(path)', 'execute_code(code)'],
'submit_for_review()'),
('Code Reviewer', '#e8e8e8',
'"Evaluate code quality from multiple dimensions: \nfunctional correctness, coding standards, \nand security. Adopt critical thinking."',
['run_linter(file)', 'run_tests(file)', 'analyze_complexity(file)'],
None),
]
s.rect(30, 55, W - 60, 28, fill='code_bg', rx=3)
s.text(W // 2, 69, '▼ Continuous flow within the same context — conversation history fully preserved across stages ▼', size=FS_SMALL, bold=True)
pw = 225
gap = 18
px_start = (W - 3 * pw - 2 * gap) // 2
py = 100
for i, (role, fill, prompt, tools, trigger) in enumerate(phases):
x = px_start + i * (pw + gap)
s.rect(x, py, pw, 380, fill=fill, rx=6)
s.text(x + pw // 2, py + 22, f'Stage {i + 1}', size=FS_TINY, fill='text_light')
s.text(x + pw // 2, py + 42, role, size=FS_BODY, bold=True)
s.rect(x + 8, py + 60, pw - 16, 88, fill='code_bg', rx=3)
s.text(x + 14, py + 75, 'System Prompt', size=FS_TINY, fill='text_light', anchor='start')
for j, ln in enumerate(prompt.split('\n')):
s.text(x + 14, py + 92 + j * 16, ln, size=12, anchor='start', fill='text_light')
s.rect(x + 8, py + 158, pw - 16, 18 + len(tools) * 20, fill='white', rx=3)
s.text(x + 14, py + 172, 'Tool Set', size=FS_TINY, fill='text_light', anchor='start')
for j, tool in enumerate(tools):
s.mono(x + 14, py + 190 + j * 20, tool, size=11)
if trigger:
ty = py + 290
s.rect(x + 8, ty, pw - 16, 48, fill='dark', rx=12)
s.text(x + pw // 2, ty + 16, 'Trigger Transition', size=FS_TINY, fill='white')
s.mono(x + pw // 2, ty + 34, trigger, size=10, anchor='middle', fill='white')
if i < 2:
ax1 = x + pw + 2
ax2 = x + pw + gap - 2
ay = py + 310
s.arrow(ax1, ay, ax2, ay)
s.text(W // 2, H - 10, 'Role Transition: Update system prompt + tool set, conversation history and state continuously preserved',
size=FS_SMALL, fill='text_light')
s.save(os.path.join(OUT, 'fig9-2.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-3: Proposer-Reviewer Loop (Slidev PPT Generation)
# ════════════════════════════════════════════════════════════════════
def fig9_3():
W, H = 780, 520
s = SVG(W, H)
s.text(W // 2, 28, 'Proposer-Reviewer Loop: Slidev PPT Generation', size=FS_TITLE, bold=True)
# Editor (Proposer)
ex, ey, ew, eh = 30, 65, 300, 200
s.rect(ex, ey, ew, eh, fill='light')
s.text(ex + ew // 2, ey + 22, 'Proposer Agent', size=FS_BODY, bold=True)
s.text(ex + 12, ey + 48, 'Input: Extended paper abstract (2000 characters)', size=FS_TINY, anchor='start', fill='text_light')
editor_lines = [
'---',
'theme: academic',
'---',
'# Transformer Attention Mechanism',
'',
'## Core Idea',
'- Self-attention computes Q·K^T/√d',
'- Multi-head attention processes in parallel',
]
ch = s.code_block(ex + 10, ey + 62, ew - 20, editor_lines, font_size=11, line_h=14)
s.text(ex + ew // 2, ey + eh - 10, 'Understand content structure → Decompose into slides', size=FS_TINY, fill='text_light')
# Critic (Reviewer)
cx, cy, cw, ch_h = 450, 65, 300, 200
s.rect(cx, cy, cw, ch_h, fill='medium')
s.text(cx + cw // 2, cy + 22, 'Reviewer Agent', size=FS_BODY, bold=True)
s.rect(cx + 10, cy + 42, cw - 20, 38, fill='code_bg', rx=3)
s.text(cx + 18, cy + 55, '① Slidev rendering → PDF/PNG', size=FS_TINY, anchor='start')
s.text(cx + 18, cy + 70, '② Vision LLM multi-dimensional evaluation', size=FS_TINY, anchor='start')
feedback_items = [
'Page Issue Type Severity',
'P3 Content too dense High',
'P7 Font too small Medium',
'P11 Color mismatch Low',
]
s.rect(cx + 10, cy + 86, cw - 20, 75, fill='code_bg', rx=3)
s.text(cx + 18, cy + 100, 'Structured Feedback:', size=FS_TINY, anchor='start', bold=True)
for i, fb in enumerate(feedback_items):
s.mono(cx + 18, cy + 118 + i * 15, fb, size=11)
s.text(cx + cw // 2, cy + ch_h - 10, 'Rendering + visual analysis → actionable improvement suggestions', size=FS_TINY, fill='text_light')
# Arrows between Editor and Critic
mid_y1 = ey + 70
mid_y2 = ey + eh - 50
s.arrow(ex + ew + 2, mid_y1, cx - 2, mid_y1)
s.text((ex + ew + cx) / 2, mid_y1 - 12, 'Slidev Code', size=FS_SMALL, bold=True)
s.arrow(cx - 2, mid_y2, ex + ew + 2, mid_y2)
s.text((ex + ew + cx) / 2, mid_y2 + 16, 'Structured Feedback', size=FS_SMALL, bold=True)
# Iteration timeline
iy = 290
s.rect(30, iy, W - 60, 100, fill='code_bg', rx=4)
s.text(W // 2, iy + 18, 'Iterative Improvement Process', size=FS_BODY, bold=True)
rounds = [
('Round 1', '12-page draft\n5 issues', 'light'),
('Round 2', '14 pages (split dense pages)\n2 issues', 'light'),
('Round 3', '14 pages (font corrected)\n0 issues ✓', 'medium'),
]
rw = 190
rx_start = (W - 3 * rw - 2 * 30) // 2
for i, (name, desc, fill) in enumerate(rounds):
rx = rx_start + i * (rw + 30)
ry = iy + 35
s.rect(rx, ry, rw, 52, fill=fill, rx=3)
s.text(rx + 10, ry + 16, name, size=FS_SMALL, bold=True, anchor='start')
for j, ln in enumerate(desc.split('\n')):
s.text(rx + 10, ry + 34 + j * 16, ln, size=FS_TINY, anchor='start', fill='text_light')
if i < 2:
s.arrow(rx + rw + 4, ry + 26, rx + rw + 26, ry + 26, color='dark')
# Why not single agent
wy = 405
s.rect(30, wy, W - 60, 90, fill='light', rx=4)
s.text(W // 2, wy + 20, 'Why not use a single agent?', size=FS_BODY, bold=True)
single_x = 60
dual_x = W // 2 + 20
s.text(single_x, wy + 45, 'Single Agent: Renderings × N rounds → context explosion', size=FS_TINY, anchor='start', fill='text_light')
s.text(single_x, wy + 63, '(1080p screenshot = thousands of tokens × 14 pages × 5 rounds)', size=FS_TINY, anchor='start', fill='text_light')
s.text(dual_x, wy + 45, 'Dual Agent: Critic only sees current version', size=FS_TINY, anchor='start')
s.text(dual_x, wy + 63, 'Editor only accumulates text feedback → clean context', size=FS_TINY, anchor='start')
s.save(os.path.join(OUT, 'fig9-3.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-4: Manager sequential coordination
# ════════════════════════════════════════════════════════════════════
def fig9_4():
W, H = 780, 480
s = SVG(W, H)
s.text(W // 2, 28, 'Manager sequential coordination: Sub-Agent as tool', size=FS_TITLE, bold=True)
# Manager
mx, my, mw, mh = 240, 60, 300, 100
s.rect(mx, my, mw, mh, fill='medium')
s.text(mx + mw // 2, my + 22, 'Manager Agent', size=FS_BODY, bold=True)
s.text(mx + mw // 2, my + 46, 'Task understanding → decomposition → scheduling → synthesis', size=FS_TINY, fill='text_light')
s.text(mx + mw // 2, my + 66, 'Tool set: [call_agent_A, call_agent_B,', size=FS_TINY, fill='text_light')
s.text(mx + mw // 2, my + 82, 'call_agent_C, search, write_file]', size=FS_TINY, fill='text_light')
# Sub-agents in sequence
agents = [
('Sub-Agent A', 'Data collection', 'Search technical documentation\nExtract key information', 'light'),
('Sub-Agent B', 'Analysis and processing', 'Compare and analyze data\nGenerate statistical report', 'light'),
('Sub-Agent C', 'Report generation', 'Write final report\nFormat output', 'light'),
]
aw = 210
ax_start = (W - 3 * aw - 2 * 25) // 2
ay = 240
for i, (name, role, desc, fill) in enumerate(agents):
x = ax_start + i * (aw + 25)
s.rect(x, ay, aw, 120, fill=fill, rx=6)
s.text(x + aw // 2, ay + 20, name, size=FS_SMALL, bold=True)
s.text(x + aw // 2, ay + 40, f'Roles: {role}', size=FS_TINY, fill='text_light')
for j, ln in enumerate(desc.split('\n')):
s.text(x + aw // 2, ay + 62 + j * 18, ln, size=FS_TINY, fill='text_light')
badge_labels = [f'Step {i + 1}']
s.badge(x + aw - 55, ay + 95, 50, 20, badge_labels[0], fill='dark', font_size=FS_TINY)
# Arrow from Manager to sub-agent
s.arrow(mx + mw // 2 - 100 + i * 100, my + mh + 2,
x + aw // 2, ay - 2, color='dark')
# Sequential arrow between sub-agents
if i < 2:
s.arrow(x + aw + 2, ay + 60, x + aw + 23, ay + 60)
# Data flow
dy = 380
s.rect(30, dy, W - 60, 80, fill='code_bg', rx=4)
s.text(W // 2, dy + 18, 'Sequential execution flow', size=FS_BODY, bold=True)
flow_items = [
'Manager calls Agent A',
'→ A returns data',
'→ Manager passes to B',
'→ B returns analysis',
'→ Manager passes to C',
'→ C returns report',
]
fx_start = 55
for i, item in enumerate(flow_items):
s.text(fx_start + i * 118, dy + 42, item, size=FS_TINY, anchor='start',
fill='text' if 'Call' in item or 'Return' in item else 'text_light')
s.text(W // 2, dy + 65, 'Manager perspective: calling Agent = calling tool (send request → receive response)',
size=FS_SMALL, fill='text_light')
s.save(os.path.join(OUT, 'fig9-4.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-5: Book translation Agent architecture (Exp 9.4)
# ════════════════════════════════════════════════════════════════════
def fig9_5():
W, H = 780, 540
s = SVG(W, H)
s.text(W // 2, 28, 'Experiment 9.4: Book translation Agent — Manager mode', size=FS_TITLE, bold=True)
# Manager at top
mx, my, mw, mh = 240, 55, 300, 70
s.rect(mx, my, mw, mh, fill='medium')
s.text(mx + mw // 2, my + 22, 'Manager Agent', size=FS_BODY, bold=True)
s.text(mx + mw // 2, my + 48, 'Task planning · Progress monitoring · Exception handling · Result synthesis', size=FS_TINY, fill='text_light')
# Three sub-agents
sub_agents = [
(30, 'Glossary Agent', 'Glossary of terms',
['Receive entire book → Identify specialized terms', 'Search specialized dictionaries + translation conventions', 'Output: glossary.json'],
['{"attention": "attention",', ' "transformer": "Transformer",', ' "backprop": "backpropagation"}']),
(270, 'Translation Agent ×N', 'Chapter translation',
['Input: chapter + glossary + guide', 'Strictly translate terms according to the glossary', 'Output: chapter{n}_zh.md'],
['"...attention mechanism computes the similarity of', ' Query·Key^T ..."']),
(520, 'Proofreading Agent', 'Full-text review',
['Scan and verify term consistency', 'Check fluency and readability', 'Output: review_report.md'],
['P3: "attention"→"focus" inconsistency', 'P8: Long sentence suggested to split']),
]
aw = 230
ay = 170
for x, name, role, desc, output in sub_agents:
s.rect(x, ay, aw, 185, fill='light', rx=6)
s.text(x + aw // 2, ay + 20, name, size=FS_SMALL, bold=True)
s.text(x + aw // 2, ay + 38, role, size=FS_TINY, fill='text_light')
for i, ln in enumerate(desc):
s.text(x + 12, ay + 60 + i * 18, ln, size=FS_TINY, anchor='start', fill='text_light')
s.rect(x + 8, ay + 115, aw - 16, 10 + len(output) * 15, fill='code_bg', rx=3)
for i, ln in enumerate(output):
s.mono(x + 14, ay + 128 + i * 15, ln, size=10)
# Arrow from Manager
s.arrow(mx + mw // 2, my + mh + 2, x + aw // 2, ay - 2, color='dark')
# Sequential arrows between sub-agents
s.arrow(30 + aw + 4, ay + 90, 270 - 4, ay + 90, label='Glossary')
s.arrow(270 + aw + 4, ay + 90, 520 - 4, ay + 90, label='Translation')
# Shared file system
fy = 375
s.rect(30, fy, W - 60, 70, fill='medium', rx=6)
s.text(W // 2, fy + 18, 'Shared file system', size=FS_BODY, bold=True)
files = [
('glossary.json', 'Glossary of terms'),
('chapter{1..10}_zh.md', 'Chapter translation'),
('review_report.md', 'Review report'),
('translation_guide.md', 'Translation guide'),
]
fw = (W - 80) // len(files)
for i, (fname, desc) in enumerate(files):
cx = 50 + i * fw + fw // 2
s.mono(cx, fy + 40, fname, size=11, anchor='middle')
s.text(cx, fy + 58, desc, size=FS_TINY, fill='text_light')
# Key insight
ky = 460
s.rect(30, ky, W - 60, 60, fill='code_bg', rx=4)
s.text(W // 2, ky + 18, 'Context isolation advantages', size=FS_BODY, bold=True)
s.text(W // 2, ky + 42,
'Glossary: only view terms | Translation: only view current chapter + glossary | Manager: only maintain file index',
size=FS_TINY, fill='text_light')
s.save(os.path.join(OUT, 'fig9-5.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-6: Manager parallel coordination
# ════════════════════════════════════════════════════════════════════
def fig9_6():
W, H = 780, 500
s = SVG(W, H)
s.text(W // 2, 28, 'Manager parallel coordination: message bus architecture', size=FS_TITLE, bold=True)
# Orchestration Agent
ox, oy, ow, oh = 240, 55, 300, 70
s.rect(ox, oy, ow, oh, fill='medium')
s.text(ox + ow // 2, oy + 22, 'Orchestration Agent', size=FS_BODY, bold=True)
s.text(ox + ow // 2, oy + 48, 'Parallel scheduling · Real-time monitoring · Result aggregation', size=FS_TINY, fill='text_light')
# Message bus
bus_y = 155
s.rect(50, bus_y, W - 100, 36, fill='dark', rx=4)
s.text(W // 2, bus_y + 18, 'Message Bus', size=FS_SMALL, fill='white', bold=True)
s.arrow(ox + ow // 2, oy + oh + 2, ox + ow // 2, bus_y - 2)
# Parallel agents
agents = [
('Agent 1', 'Data collection', 'Running ◎', 'light'),
('Agent 2', 'Content analysis', 'Running ◎', 'light'),
('Agent 3', 'Chart Generation', 'Completed ✓', 'medium'),
('Agent 4', 'Format Validation', 'Waiting ○', 'code_bg'),
]
aw = 160
gap = 14
total = len(agents) * aw + (len(agents) - 1) * gap
ax_start = (W - total) // 2
ay = 225
for i, (name, role, status, fill) in enumerate(agents):
x = ax_start + i * (aw + gap)
s.rect(x, ay, aw, 100, fill=fill, rx=6)
s.text(x + aw // 2, ay + 20, name, size=FS_SMALL, bold=True)
s.text(x + aw // 2, ay + 40, role, size=FS_TINY, fill='text_light')
s.text(x + aw // 2, ay + 65, status, size=FS_TINY,
fill='text_light' if 'Waiting' in status else 'text')
s.text(x + aw // 2, ay + 82, 'Independent Context', size=FS_TINY, fill='text_light')
s.arrow(x + aw // 2, bus_y + 38, x + aw // 2, ay - 2, color='dark')
# Message examples
my = 350
s.rect(30, my, W - 60, 125, fill='code_bg', rx=4)
s.text(W // 2, my + 18, 'Message Bus Communication Example', size=FS_BODY, bold=True)
messages = [
('Orch → Agent 1', '{"type":"start","task":"Collect arxiv papers","params":{"query":"LLM agent"}}'),
('Agent 3 → Orch', '{"type":"completed","agent_id":"3","result":"charts/fig1.svg generated"}'),
('Agent 1 → Agent 2', '{"type":"data_ready","source":"agent_1","file":"raw_data.json"}'),
('Orch → Agent 4', '{"type":"start","depends_on":["agent_2","agent_3"]}'),
]
for i, (sender, msg) in enumerate(messages):
y = my + 40 + i * 22
s.text(40, y, sender, size=FS_TINY, bold=True, anchor='start')
s.mono(200, y, msg, size=10, anchor='start')
s.save(os.path.join(OUT, 'fig9-6.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-7: Phone + Computer Dual Agent (Exp 9.5/9.6)
# ════════════════════════════════════════════════════════════════════
def fig9_7():
W, H = 780, 560
s = SVG(W, H)
s.text(W // 2, 28, 'Experiment 9.5/9.6: Phone + Computer Dual Agent', size=FS_TITLE, bold=True)
# Phone Agent (left)
px, py, pw, ph = 30, 65, 310, 240
s.rect(px, py, pw, ph, fill='light', rx=6)
s.text(px + pw // 2, py + 22, 'Phone Agent', size=FS_BODY, bold=True)
s.text(px + pw // 2, py + 42, 'Node.js · Real-time Voice Call', size=FS_TINY, fill='text_light')
phone_pipeline = [
('User Voice', 'Microphone Input', 'medium'),
('VAD + ASR', 'Silero VAD → STT Transcription', 'light'),
('LLM Inference', 'Understand Intent + Extract Information', 'light'),
('TTS Synthesis', 'Generate Voice Reply → Playback', 'medium'),
]
for i, (label, desc, fill) in enumerate(phone_pipeline):
y = py + 60 + i * 42
s.rect(px + 10, y, pw - 20, 36, fill=fill, rx=3)
s.text(px + 20, y + 14, label, size=FS_TINY, bold=True, anchor='start')
s.text(px + 20, y + 28, desc, size=11, anchor='start', fill='text_light')
if i < len(phone_pipeline) - 1:
s.arrow(px + pw // 2, y + 38, px + pw // 2, y + 42, color='dark')
# Computer Agent (right)
cx, cy, cw, ch_h = 440, 65, 310, 240
s.rect(cx, cy, cw, ch_h, fill='light', rx=6)
s.text(cx + cw // 2, cy + 22, 'Computer Agent', size=FS_BODY, bold=True)
s.text(cx + cw // 2, cy + 42, 'Python · Browser Automation', size=FS_TINY, fill='text_light')
comp_pipeline = [
('Screenshot', 'Current Browser Page', 'medium'),
('Vision LLM', 'Understand Page Structure + Form Fields', 'light'),
('Action Planning', 'Locate Fields → Plan Input Sequence', 'light'),
('Execute Actions', 'Click / Input / Submit', 'medium'),
]
for i, (label, desc, fill) in enumerate(comp_pipeline):
y = cy + 60 + i * 42
s.rect(cx + 10, y, cw - 20, 36, fill=fill, rx=3)
s.text(cx + 20, y + 14, label, size=FS_TINY, bold=True, anchor='start')
s.text(cx + 20, y + 28, desc, size=11, anchor='start', fill='text_light')
if i < len(comp_pipeline) - 1:
s.arrow(cx + cw // 2, y + 38, cx + cw // 2, y + 42, color='dark')
# WebSocket connection between agents
ws_y = py + ph + 15
s.rect(30, ws_y, W - 60, 36, fill='dark', rx=4)
s.text(W // 2, ws_y + 18, 'WebSocket Bidirectional Communication (ws://localhost:8849)', size=FS_SMALL, fill='white', bold=True)
s.arrow(px + pw // 2, py + ph + 2, px + pw // 2, ws_y - 2, color='dark')
s.arrow(cx + cw // 2, cy + ch_h + 2, cx + cw // 2, ws_y - 2, color='dark')
# Message examples
my = ws_y + 50
s.rect(30, my, W - 60, 150, fill='code_bg', rx=4)
s.text(W // 2, my + 18, 'Real-time Bidirectional Message Stream (Use phone and computer simultaneously)', size=FS_BODY, bold=True)
msgs = [
('Phone → Computer', '[FROM_PHONE_AGENT] User says name is Zhang San', '→'),
('Computer → Phone', '[FROM_COMPUTER_AGENT] Name filled in, ID number required', '←'),
('Phone → Computer', '[FROM_PHONE_AGENT] ID number 310101199001011234', '→'),
('Computer → Phone', '[FROM_COMPUTER_AGENT] Form submitted, registration successful', '←'),
]
for i, (sender, content, direction) in enumerate(msgs):
y = my + 42 + i * 26
s.text(42, y, sender, size=FS_TINY, bold=True, anchor='start',
fill='text' if '→' == direction else 'text_light')
s.mono(210, y, content, size=10, anchor='start')
# Key point
s.text(W // 2, my + 140,
'Key: Two agents run independent ReAct loops in parallel without blocking each other',
size=FS_SMALL, fill='text_light')
s.save(os.path.join(OUT, 'fig9-7.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-8: Parallel Web Scraping Agent (Exp 9.7)
# ════════════════════════════════════════════════════════════════════
def fig9_8():
W, H = 780, 530
s = SVG(W, H)
s.text(W // 2, 28, 'Experiment 9.7: Parallel Web Scraping — Cascade Termination', size=FS_TITLE, bold=True)
# Orchestration Agent
ox, oy, ow, oh = 230, 55, 320, 65
s.rect(ox, oy, ow, oh, fill='medium')
s.text(ox + ow // 2, oy + 20, 'Orchestration Agent', size=FS_BODY, bold=True)
s.text(ox + ow // 2, oy + 44, 'Dynamic creation · Real-time monitoring · Cascade termination', size=FS_TINY, fill='text_light')
# Parallel Computer Use Agents
agents = [
('Agent 1', 'cs.edu.cn', 'Searching... ◎', 'light'),
('Agent 2', 'math.edu.cn', 'Not found ✗', '#e8e8e8'),
('Agent 3', 'phys.edu.cn', 'Found! ✓', 'medium'),
('Agent 4', 'chem.edu.cn', 'Terminated ⊘', 'code_bg'),
('Agent 5', 'bio.edu.cn', 'Terminated ⊘', 'code_bg'),
]
aw = 130
gap = 12
total_w = len(agents) * aw + (len(agents) - 1) * gap
ax_start = (W - total_w) // 2
ay = 160
for i, (name, url, status, fill) in enumerate(agents):
x = ax_start + i * (aw + gap)
s.rect(x, ay, aw, 95, fill=fill, rx=4)
s.text(x + aw // 2, ay + 16, name, size=FS_SMALL, bold=True)
s.mono(x + aw // 2, ay + 35, url, size=10, anchor='middle')
s.text(x + aw // 2, ay + 55, 'Faculty Directory Search', size=FS_TINY, fill='text_light')
s.text(x + aw // 2, ay + 75, status, size=FS_TINY,
bold=('Found' in status), fill='text' if 'Found' in status else 'text_light')
s.arrow(ox + ow // 2, oy + oh + 2, x + aw // 2, ay - 2, color='dark')
# Cascade termination flow
ty = 280
s.rect(30, ty, W - 60, 120, fill='code_bg', rx=4)
s.text(W // 2, ty + 18, 'Cascade Termination Timeline', size=FS_BODY, bold=True)
timeline = [
('t=0s', 'Start 5 agents\nparallel search for teacher "Zhang Wei"'),
('t=12s', 'Agent 2 completed\nNot found → Normal exit'),
('t=18s', 'Agent 3 found target!\nSend target_found'),
('t=18.1s', 'Orch broadcasts terminate\nto Agents 1,4,5'),
('t=19s', 'All confirm termination\nAggregate results and return'),
]
tw = 130
tx_start = (W - len(timeline) * tw) // 2
for i, (time, desc) in enumerate(timeline):
x = tx_start + i * tw
s.text(x + tw // 2, ty + 42, time, size=FS_SMALL, bold=True)
for j, ln in enumerate(desc.split('\n')):
s.text(x + tw // 2, ty + 60 + j * 16, ln, size=FS_TINY, fill='text_light')
if i < len(timeline) - 1:
s.arrow(x + tw - 2, ty + 55, x + tw + 4, ty + 55, color='dark')
# Result and comparison
ry = 420
s.rect(30, ry, 340, 85, fill='light', rx=4)
s.text(200, ry + 18, 'Result found', size=FS_BODY, bold=True)
result_lines = [
'Name: Zhang Wei School: School of Physics',
'Position: Professor Field: Quantum Computing',
'Email: zhangwei@phys.edu.cn',
]
for i, ln in enumerate(result_lines):
s.mono(50, ry + 40 + i * 16, ln, size=11)
s.rect(400, ry, 350, 85, fill='medium', rx=4)
s.text(575, ry + 18, 'Performance Comparison', size=FS_BODY, bold=True)
s.text(420, ry + 42, 'Serial: 10 websites × 30s = ~5 minutes', size=FS_TINY, anchor='start', fill='text_light')
s.text(420, ry + 60, 'Parallel: 18s to find + 1s to terminate = 19s', size=FS_TINY, anchor='start', bold=True)
s.text(420, ry + 78, 'Speedup: ~15× (with cascade termination optimization)', size=FS_TINY, anchor='start', fill='text_light')
s.save(os.path.join(OUT, 'fig9-8.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-9: Handoff Chain Pattern
# ════════════════════════════════════════════════════════════════════
def fig9_9():
W, H = 780, 440
s = SVG(W, H)
s.text(W // 2, 28, 'Handoff Chain Pattern: Peer-to-peer handoff + Contract-based collaboration', size=FS_TITLE, bold=True)
nodes = [
('Agent A', 'Requirements Analysis', 'Output: structured requirements document \nspec.json', 'medium'),
('Agent B', 'Architecture Design', 'Output: technical design document \ndesign.md', 'light'),
('Agent C', 'Code Implementation', 'Output: source code \nsrc/*.py', 'light'),
('Agent D', 'Test Verification', 'Output: test report \ntest_report.md', 'medium'),
]
nw, nh = 160, 130
gap = 22
total_w = len(nodes) * nw + (len(nodes) - 1) * gap
nx_start = (W - total_w) // 2
ny = 60
for i, (name, role, output, fill) in enumerate(nodes):
x = nx_start + i * (nw + gap)
s.rect(x, ny, nw, nh, fill=fill, rx=6)
s.text(x + nw // 2, ny + 20, name, size=FS_SMALL, bold=True)
s.text(x + nw // 2, ny + 40, role, size=FS_TINY, fill='text_light')
s.rect(x + 8, ny + 55, nw - 16, 50, fill='code_bg', rx=3)
for j, ln in enumerate(output.split('\n')):
s.text(x + nw // 2, ny + 72 + j * 16, ln, size=FS_TINY, fill='text_light')
s.text(x + nw // 2, ny + nh - 8, 'Handoff after completion →', size=FS_TINY, fill='text_light')
if i < len(nodes) - 1:
s.arrow(x + nw + 4, ny + nh // 2, x + nw + gap - 4, ny + nh // 2)
# Handoff data detail
hy = 215
s.rect(30, hy, W - 60, 90, fill='code_bg', rx=4)
s.text(W // 2, hy + 18, 'Handoff Content (Agent A → Agent B Example)', size=FS_BODY, bold=True)
handoff_fields = [
('Trigger Condition', 'A completes requirements document → is_complete=True'),
('Target Agent', 'target="architect" (Agent B)'),
('Handoff Content', 'files=["spec.json"] + summary="E-commerce system: 3 microservices, REST API"'),
('Post-handoff Status', 'status="exit" (release resources, do not remain on standby)'),
]
for i, (field, value) in enumerate(handoff_fields):
y = hy + 38 + i * 16
s.text(42, y, field + ':', size=FS_TINY, bold=True, anchor='start')
s.text(150, y, value, size=FS_TINY, anchor='start', fill='text_light')
# Comparison with Manager mode
cy = 320
s.rect(30, cy, 340, 100, fill='light', rx=4)
s.text(200, cy + 18, 'Decentralization Advantages', size=FS_SMALL, bold=True)
advantages = [
'✓ No central Manager needed to understand all roles',
'✓ Clear responsibility boundaries, interface decoupling',
'✓ Engineer can have multiple parallel instances',
]
for i, adv in enumerate(advantages):
s.text(48, cy + 42 + i * 20, adv, size=FS_TINY, anchor='start', fill='text_light')
s.rect(400, cy, 350, 100, fill='light', rx=4)
s.text(575, cy + 18, 'Decentralization Limitations', size=FS_SMALL, bold=True)
limits = [
'✗ Lack of global optimization perspective',
'✗ Difficult exception handling (no central coordination)',
'✗ Fixed process, hard to adjust dynamically',
]
for i, lim in enumerate(limits):
s.text(418, cy + 42 + i * 20, lim, size=FS_TINY, anchor='start', fill='text_light')
s.save(os.path.join(OUT, 'fig9-9.svg'))
# ════════════════════════════════════════════════════════════════════
# fig9-10: MetaGPT SOP Pipeline
# ════════════════════════════════════════════════════════════════════
def fig9_10():
W, H = 780, 530
s = SVG(W, H)
s.text(W // 2, 28, 'MetaGPT SOP Pipeline: Standardized Document-Driven', size=FS_TITLE, bold=True)
roles = [
('Product Manager', 'medium',
'Input: User Requirement Description',
['Feature List + Priority', 'User Stories (5 items)', 'Acceptance Criteria'],
'docs/PRD.md'),
('Architect', 'light',
'Input: PRD.md',
['Tech Stack: FastAPI+React', 'API Specification (OpenAPI)', 'Database Schema'],
'docs/design.md'),
('Engineer ×3', 'light',
'Input: design.md + module specification',
['Module A: User Service', 'Module B: Order Service', 'Module C: Payment Service'],
'src/*.py'),
('QA Engineer', 'medium',
'Input: src/ + PRD.md',
['Unit Tests (pytest)', 'Integration Tests (API)', 'Bug Report → Engineer'],
'docs/test_report.md'),
]
rw = 170
gap = 16
total_w = len(roles) * rw + (len(roles) - 1) * gap
rx_start = (W - total_w) // 2
ry = 55
for i, (name, fill, input_desc, outputs, artifact) in enumerate(roles):
x = rx_start + i * (rw + gap)
s.rect(x, ry, rw, 230, fill=fill, rx=6)
s.text(x + rw // 2, ry + 20, name, size=FS_SMALL, bold=True)
s.text(x + 8, ry + 42, input_desc, size=11, anchor='start', fill='text_light')
s.rect(x + 8, ry + 58, rw - 16, 20 + len(outputs) * 16, fill='code_bg', rx=3)
s.text(x + 14, ry + 72, 'Output:', size=FS_TINY, bold=True, anchor='start')
for j, out in enumerate(outputs):
s.text(x + 14, ry + 88 + j * 16, out, size=11, anchor='start', fill='text_light')
s.rect(x + 8, ry + 168, rw - 16, 30, fill='dark', rx=12)
s.mono(x + rw // 2, ry + 183, artifact, size=11, anchor='middle', fill='white')
if i < len(roles) - 1:
ax1 = x + rw + 2
ax2 = x + rw + gap - 2
s.arrow(ax1, ry + 115, ax2, ry + 115)
# QA → Engineer feedback loop
qa_x = rx_start + 3 * (rw + gap) + rw // 2
eng_x = rx_start + 2 * (rw + gap) + rw // 2
s.arrow_curved(qa_x, ry + 230 + 5, eng_x, ry + 230 + 5, curve=-30, label='Bug Fix', dash=True)
# Shared file system
fy = 310
s.rect(30, fy, W - 60, 50, fill='medium', rx=4)
s.text(W // 2, fy + 16, 'Shared Project Directory', size=FS_SMALL, bold=True)
s.mono(W // 2, fy + 36, 'docs/PRD.md docs/design.md src/*.py docs/test_report.md',
size=11, anchor='middle')
# Key insight
ky = 375
s.rect(30, ky, W - 60, 130, fill='code_bg', rx=4)
s.text(W // 2, ky + 18, 'MetaGPT Core Design', size=FS_BODY, bold=True)
insights = [
('Standardized Documents', 'Each role outputs a clear format — downstream only needs to understand the format, not the upstream thought process'),
('Interface Decoupling', 'Improve PM (switch to a stronger model) — as long as the output conforms to the PRD format, downstream requires zero modification'),
('No Manager', 'Control flows naturally along the DAG: PM→Arch→Eng→QA, no central scheduling overhead'),
('Exception Channel', 'QA test failure → Bug report routed by module back to Engineer → iterative fix'),
]
for i, (title, desc) in enumerate(insights):
y = ky + 42 + i * 24
s.text(42, y, '▸ ' + title, size=FS_SMALL, bold=True, anchor='start')
s.text(180, y, desc, size=FS_TINY, anchor='start', fill='text_light')
s.save(os.path.join(OUT, 'fig9-10.svg'))
# ════════════════════════════════════════════════════════════════════
# Fig9-11: VLA Architecture (Vision-Language-Action) — Comparison of Three Technical Paths
# ════════════════════════════════════════════════════════════════════
def fig9_11():
W, H = 900, 620
s = SVG(W, H)
# Common Input
in_x, in_y, in_w, in_h = 230, 55, 440, 64
s.rect(in_x, in_y, in_w, in_h, fill='medium', rx=6)
s.text(in_x + in_w / 2, in_y + 22, 'Common Input: camera image + language instruction', size=FS_SMALL, bold=True)
s.text(in_x + in_w / 2, in_y + 44, '"Put the red block into the blue box"', size=FS_TINY, fill='text_light')
# Arrows leading to three branches
s.arrow(in_x + 70, in_y + in_h + 2, 145, 165) # → OpenVLA
s.arrow(W / 2, in_y + in_h + 2, W / 2, 165) # → π₀
s.arrow(in_x + in_w - 70, in_y + in_h + 2, 755, 165) # → RT-2
# Three-Column Architecture
col_w, col_gap = 270, 30
cols_total = 3 * col_w + 2 * col_gap
sx0 = (W - cols_total) / 2 # = 30
columns = [
('OpenVLA', 'Open source · discrete action tokens', 'light', [
('Vision encoder', 'DINOv2 + SigLIP', 'Extract pixel features'),
('LLM backbone', 'Llama 2 (7B)', 'Understand instructions and scenes'),
('Decoding method', 'Autoregressive · text tokens', 'Discretize actions into options'),
('Action output', '"a=[3,2,5,...]" tokens', 'Generate 7-DOF control values step by step'),
]),
('π₀(Pi-Zero', 'Diffusion policy · smooth trajectory', 'medium', [
('Vision encoder', 'ViT multi-view fusion', 'Extract pixel features'),
('Mixture-of-Transformers', 'Fast-slow separated backbone', 'Language slow thinking + control fast thinking'),
('Decoding method', 'Diffusion denoising iteration', 'Coarse-to-fine refinement of entire trajectory'),
('Action output', 'Continuous action sequence (50 steps/batch)', 'High-frequency smooth control signal'),
]),
('RT-2', 'Language model as action model', 'light', [
('Vision-language backbone', 'PaLI-X / PaLM-E', 'VLM end-to-end understanding'),
('Action representation', 'Action → natural language tokens', '"move arm 5cm right"'),
('Decoding method', 'Reuse VLM autoregression', 'Shared weights with text generation'),
('Action output', 'Text description → controller parsing', 'Inherit VLM\'s semantic generalization'),
]),
]
top_y = 165
title_h = 50
row_h = 80
for i, (name, tag, fill, rows) in enumerate(columns):
cx = sx0 + i * (col_w + col_gap)
#Column container
col_total_h = title_h + len(rows) * row_h + 14
s.rect(cx, top_y, col_w, col_total_h, fill='white', stroke='border')
#Title bar
s.rect(cx, top_y, col_w, title_h, fill=fill)
s.text(cx + col_w / 2, top_y + 18, name, size=FS_BODY, bold=True)
s.text(cx + col_w / 2, top_y + 38, tag, size=FS_TINY, fill='text_light')
#Rows
for j, (label, value, hint) in enumerate(rows):
ry = top_y + title_h + 8 + j * row_h
s.rect(cx + 10, ry, col_w - 20, row_h - 8, fill='code_bg', rx=4)
s.text(cx + col_w / 2, ry + 18, label, size=FS_TINY, bold=True)
s.text(cx + col_w / 2, ry + 38, value, size=FS_TINY)
s.text(cx + col_w / 2, ry + 56, hint, size=FS_TINY, fill='text_light')
# Bottom: unified output layer
out_y = top_y + title_h + 4 * row_h + 14 + 24
s.rect(30, out_y, W - 60, 50, fill='darker', rx=6)
s.text(W / 2, out_y + 18,
'Robot control signals: 7-DOF joint angles / end-effector pose',
size=FS_SMALL, bold=True, fill='white')
s.text(W / 2, out_y + 36,
'The difference lies in "how to tell the robot what to do next," but both ultimately fall into a unified control interface',
size=FS_TINY, fill='white')
s.save(os.path.join(OUT, 'fig9-11.svg'))
# ════════════════════════════════════════════════════════════════════
# Fig 9-12: Voice Werewolf Agent System (Exp 9.9)
# ════════════════════════════════════════════════════════════════════
def fig9_12():
W, H = 780, 550
s = SVG(W, H)
s.text(W // 2, 28, 'Experiment 9.9: Voice Werewolf — Information Permission Control', size=FS_TITLE, bold=True)
# Judge (code-driven)
jx, jy, jw, jh = 260, 55, 260, 75
s.rect(jx, jy, jw, jh, fill='dark', rx=6)
s.text(jx + jw // 2, jy + 20, 'Judge (code-driven)', size=FS_BODY, bold=True, fill='white')
s.text(jx + jw // 2, jy + 42, 'Game state · Phase control · Information distribution', size=FS_TINY, fill='white')
s.text(jx + jw // 2, jy + 58, 'Night → Day → Vote → Settle', size=FS_TINY, fill='white')
# Role agents
roles = [
(40, 'Werewolf 1', '🐺', 'medium',
['Visible: Teammate identities', 'Strategy: Disguise as villager', 'Night: Choose target']),
(185, 'Werewolf 2', '🐺', 'medium',
['Visible: Teammate identities', 'Strategy: Follow votes to protect', 'Night: Negotiate target']),
(330, 'Seer', '🔮', 'light',
['Visible: Investigation results', 'Strategy: Choose timing to reveal', 'Night: Check 1 person']),
(475, 'Witch', '🧪', 'light',
['Visible: Death/healing', 'Strategy: Save potion/antidote', 'Night: Save/poison 1 person']),
(620, 'Villager ×2', '👤', '#e8e8e8',
['Visible: Public information only', 'Strategy: Logical reasoning', 'Day: Analyze speech']),
]
aw, ay = 135, 180
for x, name, icon, fill, info in roles:
s.rect(x, ay, aw, 140, fill=fill, rx=6)
s.text(x + aw // 2, ay + 18, f'{icon} {name}', size=FS_SMALL, bold=True)
for i, ln in enumerate(info):
s.text(x + aw // 2, ay + 42 + i * 20, ln, size=11, fill='text_light')
# Arrow from Judge
s.arrow(jx + jw // 2, jy + jh + 2, x + aw // 2, ay - 2, color='dark')
# Permission badge
if 'Werewolf' in name:
s.badge(x + aw - 45, ay + aw - 15, 40, 18, 'Mutual knowledge', fill='darker', font_size=11)
elif 'Seer' in name:
s.badge(x + aw - 55, ay + aw - 15, 50, 18, 'Investigation results', fill='darker', font_size=10)
# Info permission control
iy = 340
s.rect(30, iy, W - 60, 90, fill='code_bg', rx=4)
s.text(W // 2, iy + 18, 'Information permission control: judge filters context by role', size=FS_BODY, bold=True)
perms = [
('Werewolf', 'All werewolf identities + night discussion + public speech'),
('Seer', 'Investigation results (only self-investigated) + public speech'),
('Witch', 'Deaths of the night + antidote/poison status + public speech'),
('Villager', 'Public speech only + voting records (zero private information)'),
]
pw = (W - 80) // 2
for i, (role, perm) in enumerate(perms):
row, col = i // 2, i % 2
x = 50 + col * pw
y = iy + 40 + row * 22
s.text(x, y, role + ':', size=FS_TINY, bold=True, anchor='start')
s.text(x + 55, y, perm, size=FS_TINY, anchor='start', fill='text_light')
# Voice interaction
vy = 445
s.rect(30, vy, W - 60, 85, fill='light', rx=4)
s.text(W // 2, vy + 18, 'Real-time voice interaction (ASR + LLM + TTS)', size=FS_BODY, bold=True)
voice_flow = [
('Day discussion', 'Judge manages speaking order\nspeaking in seat order'),
('Voting phase', 'Collect all player votes\ntally votes and announce results'),
('Night phase', 'Judge wakes roles in sequence\nprivate voice channel'),
('Human player', 'Random role assignment\nvoice-based voting/speech'),
]
vw = (W - 80) // len(voice_flow)
for i, (title, desc) in enumerate(voice_flow):
cx = 50 + i * vw + vw // 2
s.text(cx, vy + 42, title, size=FS_SMALL, bold=True)
for j, ln in enumerate(desc.split('\n')):
s.text(cx, vy + 60 + j * 16, ln, size=FS_TINY, fill='text_light')
s.save(os.path.join(OUT, 'fig9-12.svg'))
# ════════════════════════════════════════════════════════════════════
# Main
# ════════════════════════════════════════════════════════════════════
def main():
os.makedirs(OUT, exist_ok=True)
figs = [
('fig9-1', fig9_1),
('fig9-2', fig9_2),
('fig9-3', fig9_3),
('fig9-4', fig9_4),
('fig9-5', fig9_5),
('fig9-6', fig9_6),
('fig9-7', fig9_7),
('fig9-8', fig9_8),
('fig9-9', fig9_9),
('fig9-10', fig9_10),
('fig9-11', fig9_11),
('fig9-12', fig9_12),
]
for name, func in figs:
func()
print(f' ✓ {name}')
print(f'\nGenerated {len(figs)} figures in {OUT}/')
if __name__ == '__main__':
main()