Files
ai-agent-book/book-es/gen_ch4_figs.py
T
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

873 lines
37 KiB
Python
Raw 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.
"""Generate all SVG illustrations for Chapter 4 (Tools & MCP & Async)."""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
from svg_lib import (
SVG, COLORS, FS_TITLE, FS_BODY, FS_SMALL, FS_TINY,
)
OUT = os.path.join(os.path.dirname(__file__), 'images')
def _pill(svg, x, y, w, h, label, fill='light', font_size=FS_SMALL, bold=False):
svg.rect(x, y, w, h, fill=fill, rx=h // 2)
c = 'white' if fill in ('dark', 'darker') else 'text'
svg.text(x + w / 2, y + h / 2, label, size=font_size, fill=c, bold=bold)
def fig4_1():
"""MCP protocol sequence diagram — Figure 4-1."""
w, h = 880, 620
svg = SVG(w, h)
svg.text(w / 2, 30, "Secuencia de interacción del protocolo MCP", size=FS_TITLE, bold=True)
cl_x, sv_x = 200, 680
svg.box(cl_x - 80, 50, 160, 44, "Cliente MCP", fill='medium', bold=True)
svg.box(sv_x - 80, 50, 160, 44, "Servidor MCP", fill='medium', bold=True)
svg.line(cl_x, 94, cl_x, 600, color='dark', dash=True)
svg.line(sv_x, 94, sv_x, 600, color='dark', dash=True)
y = 130
svg.arrow(cl_x + 4, y, sv_x - 4, y)
svg.text((cl_x + sv_x) / 2, y - 14, "initialize", size=FS_BODY, bold=True)
svg.code_block(cl_x + 30, y + 6, 350, [
'{"method": "initialize",',
' "capabilities": {"tools": true}}',
], font_size=FS_TINY, line_h=18)
y = 200
svg.arrow(sv_x - 4, y, cl_x + 4, y, dash=True)
svg.text((cl_x + sv_x) / 2, y - 14, "respuesta initialize", size=FS_BODY, bold=True)
svg.code_block(cl_x + 30, y + 6, 350, [
'{"serverInfo": {"name": "weather-server"},',
' "capabilities": {"tools": {"listChanged":true}}}',
], font_size=FS_TINY, line_h=18)
y = 280
svg.arrow(cl_x + 4, y, sv_x - 4, y)
svg.text((cl_x + sv_x) / 2, y - 14, "tools/list", size=FS_BODY, bold=True)
svg.code_block(cl_x + 30, y + 6, 350, [
'{"method": "tools/list"}',
], font_size=FS_TINY, line_h=18)
y = 340
svg.arrow(sv_x - 4, y, cl_x + 4, y, dash=True)
svg.text((cl_x + sv_x) / 2, y - 14, "respuesta tools/list", size=FS_BODY, bold=True)
svg.code_block(cl_x + 10, y + 6, 400, [
'{"tools": [{"name": "get_weather",',
' "inputSchema": {"city": "string"}}]}',
], font_size=FS_TINY, line_h=18)
y = 420
svg.arrow(cl_x + 4, y, sv_x - 4, y)
svg.text((cl_x + sv_x) / 2, y - 14, "tools/call", size=FS_BODY, bold=True)
svg.code_block(cl_x + 30, y + 6, 350, [
'{"method": "tools/call",',
' "params": {"name": "get_weather",',
' "arguments": {"city": "Beijing"}}}',
], font_size=FS_TINY, line_h=18)
y = 510
svg.arrow(sv_x - 4, y, cl_x + 4, y, dash=True)
svg.text((cl_x + sv_x) / 2, y - 14, "resultado tools/call", size=FS_BODY, bold=True)
svg.code_block(cl_x + 30, y + 6, 350, [
'{"content": [{"type": "text",',
' "text": "Beijing: 22°C, despejado"}]}',
], font_size=FS_TINY, line_h=18)
svg.text(50, 165, "① Negociación", size=FS_SMALL, bold=True, fill='text_light')
svg.text(50, 310, "② Descubrimiento", size=FS_SMALL, bold=True, fill='text_light')
svg.text(50, 465, "③ Invocación", size=FS_SMALL, bold=True, fill='text_light')
svg.save(os.path.join(OUT, 'fig4-1.svg'))
def fig4_2():
"""Sub-Agent context preparation — Figure 4-2."""
w, h = 880, 530
svg = SVG(w, h)
svg.text(w / 2, 30, "Estrategias de transferencia de contexto para Subagentes", size=FS_TITLE, bold=True)
strategies = [
("Transferencia mínima", "dark",
'"Consultar estado de pedido 12345"',
"Cero contexto → privacidad y seguridad"),
("Filtrado y trans. manual", "medium",
'"Región: ES\\nResumen: Consulta reembolso"',
"Selección explícita → controlable"),
("Recorte y trans. aut.", "light",
'"Info usuario + últimas 3 rondas\\n+ res. herramientas"',
"Basado en reglas → equilibrado"),
("Contexto generado por LLM", "code_bg",
'"LLM analiza trayectoria\\n→ objeto contexto estructurado"',
"Más inteligente → 1 llamada extra"),
]
col_w = 190
gap = 18
start_x = (w - 4 * col_w - 3 * gap) / 2
svg.box(w / 2 - 100, 55, 200, 44, "Agente principal", fill='medium', bold=True)
svg.text(w / 2, 118, "¿Cómo preparar el contexto para el Subagente?", size=FS_SMALL, fill='text_light')
for i, (title, fill, example, note) in enumerate(strategies):
x = start_x + i * (col_w + gap)
top_y = 145
svg.arrow(w / 2, 99, x + col_w / 2, top_y - 2)
svg.rect(x, top_y, col_w, 36, fill=fill)
tc = 'white' if fill in ('dark', 'darker') else 'text'
svg.text(x + col_w / 2, top_y + 18, title, size=FS_SMALL, bold=True, fill=tc)
svg.rect(x, top_y + 46, col_w, 80, fill='code_bg', stroke='dark', rx=4)
for j, line in enumerate(example.split('\\n')):
svg.mono(x + 8, top_y + 70 + j * 20, line, size=FS_TINY)
svg.text(x + col_w / 2, top_y + 150, note, size=FS_TINY, fill='text_light')
svg.box(x + 15, top_y + 175, col_w - 30, 36, "Subagente", fill='light', font_size=FS_SMALL)
svg.line(30, 395, w - 30, 395, color='dark', dash=True)
svg.text(w / 2, 418, "Guía de selección", size=FS_BODY, bold=True)
guides = [
("Llamadas simples alta frec.", "Consulta clima, calculadora", "→ Mínima"),
("Complejidad media", "Consulta datos, proc. archivos", "→ Recorte automático"),
("Tareas complejas", "Generar informe, atención al cliente", "→ Generación LLM"),
]
gx = 80
for label, example, rec in guides:
svg.rect(gx, 438, 230, 70, fill='light')
svg.text(gx + 115, 458, label, size=FS_SMALL, bold=True)
svg.text(gx + 115, 478, example, size=FS_TINY, fill='text_light')
svg.text(gx + 115, 498, rec, size=FS_SMALL, bold=True, fill='darker')
gx += 260
svg.save(os.path.join(OUT, 'fig4-2.svg'))
def fig4_3():
"""Event-driven architecture — Figure 4-3."""
w, h = 880, 540
svg = SVG(w, h)
svg.text(w / 2, 30, "Arquitectura de Agente asíncrona orientada a eventos", size=FS_TITLE, bold=True)
sources = [
("Email", 'on_email_reply', '{"from":"alice@...",\n "subject":"Re:reunión"}'),
("Temporizador", 'on_timer_expire', '{"task_id":"daily_report",\n "scheduled":"09:00"}'),
("Webhook", 'on_webhook', '{"repo":"agent-lib",\n "event":"pr_merged"}'),
("Usuario", 'on_user_message', '{"text":"Ver clima\nmañana"}'),
]
src_x, src_w = 20, 155
svg.text(src_x + src_w / 2, 65, "Fuente de eventos", size=FS_BODY, bold=True)
for i, (name, event_type, payload) in enumerate(sources):
y = 85 + i * 110
svg.box(src_x, y, src_w, 40, name, fill='medium', bold=True, font_size=FS_SMALL)
svg.mono(src_x + 5, y + 56, event_type, size=FS_TINY)
for j, pl in enumerate(payload.split('\n')):
svg.mono(src_x + 5, y + 74 + j * 16, pl, size=11)
q_x, q_w = 215, 190
svg.text(q_x + q_w / 2, 65, "Cola de eventos", size=FS_BODY, bold=True)
svg.rect(q_x, 85, q_w, 390, fill='white', stroke='border', dash=True)
queue_events = [
("user.input", "Prioridad: normal", 'light'),
("email.reply", "Prioridad: normal", 'light'),
("user.interrupt", "Prioridad: ¡urgente!", 'dark'),
("timer.trigger", "Prioridad: normal", 'light'),
]
for i, (evt, pri, fill) in enumerate(queue_events):
ey = 105 + i * 85
svg.rect(q_x + 10, ey, q_w - 20, 60, fill=fill, rx=4)
tc = 'white' if fill in ('dark', 'darker') else 'text'
svg.text(q_x + q_w / 2, ey + 22, evt, size=FS_SMALL, bold=True, fill=tc)
svg.text(q_x + q_w / 2, ey + 44, pri, size=FS_TINY, fill='white' if fill == 'dark' else 'text_light')
for i in range(4):
sy = 105 + i * 110
svg.arrow(src_x + src_w + 2, sy, q_x - 2, 120 + i * 85)
ag_x = 450
svg.text(ag_x + 200, 65, "Flujo de procesamiento del Agente", size=FS_BODY, bold=True)
svg.arrow(q_x + q_w + 2, 280, ag_x - 2, 280, label="Obtener evento")
steps = [
("Enrutador", "El LLM determina la urgencia", 'medium'),
("Añadir a trayectoria", "Formato de evento estructurado", 'light'),
("Inferencia LLM", "Observar → Pensar → Actuar", 'light'),
("Ejecutar herramienta", "Despacho async/sync", 'light'),
("Procesar resultado", "Notificar/responder/guardar", 'medium'),
]
step_w, step_h = 360, 50
for i, (title, desc, fill) in enumerate(steps):
sy = 110 + i * 80
svg.rect(ag_x, sy, step_w, step_h, fill=fill)
svg.text(ag_x + 18, sy + step_h / 2, title, size=FS_SMALL, bold=True, anchor='start')
svg.text(ag_x + step_w - 12, sy + step_h / 2, desc, size=FS_TINY, fill='text_light', anchor='end')
if i < len(steps) - 1:
svg.arrow(ag_x + step_w / 2, sy + step_h + 2, ag_x + step_w / 2, sy + 78)
svg.arrow_curved(ag_x + step_w, 450, ag_x + step_w, 130, curve=45, label="Bucle", dash=True, color='dark')
svg.save(os.path.join(OUT, 'fig4-3.svg'))
def fig4_4():
"""Async event handling — Figure 4-4."""
w, h = 880, 580
svg = SVG(w, h)
svg.text(w / 2, 30, "Tres estrategias para el manejo de eventos", size=FS_TITLE, bold=True)
lane_x = 130
lane_w = 720
tl_x0 = lane_x + 10
tl_w = lane_w - 20
def time_bar(y, x_start_pct, x_end_pct, fill, label, h_bar=28):
xs = tl_x0 + tl_w * x_start_pct
xe = tl_x0 + tl_w * x_end_pct
svg.rect(xs, y, xe - xs, h_bar, fill=fill, rx=4)
svg.text((xs + xe) / 2, y + h_bar / 2, label, size=FS_TINY,
fill='white' if fill in ('dark', 'darker') else 'text')
svg.text(tl_x0 + tl_w * 0.25, 55, "t₁", size=FS_SMALL, fill='text_light')
svg.text(tl_x0 + tl_w * 0.50, 55, "t₂", size=FS_SMALL, fill='text_light')
svg.text(tl_x0 + tl_w * 0.75, 55, "t₃", size=FS_SMALL, fill='text_light')
y1 = 80
svg.rect(lane_x, y1, lane_w, 140, fill='white', stroke='border', dash=True)
svg.text(lane_x / 2, y1 + 70, "Cancelación", size=FS_BODY, bold=True)
svg.text(lane_x / 2, y1 + 95, "(Urgente)", size=FS_SMALL, fill='text_light')
time_bar(y1 + 15, 0.0, 0.40, 'medium', 'Razonamiento LLM...')
svg.line(tl_x0 + tl_w * 0.40, y1 + 10, tl_x0 + tl_w * 0.40, y1 + 130, color='border', dash=True)
svg.text(tl_x0 + tl_w * 0.40, y1 + 10, "⚡ user.interrupt: \"¡Detener!\"", size=FS_TINY, bold=True)
time_bar(y1 + 15, 0.40, 0.45, 'dark', '×', h_bar=28)
time_bar(y1 + 55, 0.0, 0.35, 'light', 'Ejecutando herramienta...')
time_bar(y1 + 55, 0.40, 0.45, 'dark', '×', h_bar=28)
time_bar(y1 + 95, 0.47, 1.0, 'medium', 'Nuevo razonamiento LLM (evento interrupción + cola limpia)')
y2 = 240
svg.rect(lane_x, y2, lane_w, 140, fill='white', stroke='border', dash=True)
svg.text(lane_x / 2, y2 + 70, "En encolamiento", size=FS_BODY, bold=True)
svg.text(lane_x / 2, y2 + 95, "(Normal)", size=FS_SMALL, fill='text_light')
time_bar(y2 + 15, 0.0, 0.15, 'medium', 'LLM', h_bar=24)
time_bar(y2 + 15, 0.18, 0.60, 'light', 'Ejecución herramienta (search_web)')
time_bar(y2 + 15, 0.63, 0.90, 'medium', 'Procesamiento exhaustivo LLM')
svg.line(tl_x0 + tl_w * 0.35, y2 + 46, tl_x0 + tl_w * 0.35, y2 + 130, color='dark', dash=True)
svg.text(tl_x0 + tl_w * 0.35, y2 + 58, "user: \"Solo mirar último mes\"", size=FS_TINY, fill='text_light')
_pill(svg, tl_x0 + tl_w * 0.30, y2 + 65, 150, 24, "Encolado, esperando", fill='light', font_size=FS_TINY)
time_bar(y2 + 100, 0.63, 0.68, 'dark', '', h_bar=20)
svg.text(tl_x0 + tl_w * 0.61, y2 + 110, "Inyección por lotes: tool.result + entrada usuario", size=FS_TINY, fill='text_light', anchor='end')
y3 = 400
svg.rect(lane_x, y3, lane_w, 140, fill='white', stroke='border', dash=True)
svg.text(lane_x / 2, y3 + 70, "Paralelo", size=FS_BODY, bold=True)
svg.text(lane_x / 2, y3 + 95, "(Independiente)", size=FS_SMALL, fill='text_light')
time_bar(y3 + 15, 0.0, 0.80, 'light', 'Tarea principal: Análisis de datos (ejecución larga)')
svg.line(tl_x0 + tl_w * 0.30, y3 + 50, tl_x0 + tl_w * 0.30, y3 + 130, color='dark', dash=True)
svg.text(tl_x0 + tl_w * 0.30, y3 + 58, "user: \"¿Clima hoy?\"", size=FS_TINY, fill='text_light')
time_bar(y3 + 70, 0.32, 0.50, 'medium', 'LLM paralelo', h_bar=24)
time_bar(y3 + 70, 0.52, 0.62, 'dark', 'Clima', h_bar=24)
svg.text(tl_x0 + tl_w * 0.635, y3 + 82, "→ Responder al usuario de inmediato", size=FS_TINY, fill='text_light', anchor='start')
svg.text(tl_x0 + tl_w * 0.50, y3 + 115, "Etiqueta: [En paralelo con tarea principal]", size=FS_TINY, fill='text_light')
svg.save(os.path.join(OUT, 'fig4-4.svg'))
def fig4_5():
"""Experiment 4.4 — Figure 4-5."""
w, h = 880, 480
svg = SVG(w, h)
svg.text(w / 2, 30, "Experimento 4.4: Arquitectura de Agente orientada a eventos", size=FS_TITLE, bold=True)
src_data = [
("on_user_message", "Web/App"),
("on_email_reply", "Sistema de email"),
("on_github_pr_update", "GitHub"),
("on_timer_expire", "Temporizador"),
("on_webhook_received", "Webhook"),
("on_resource_alert", "Alerta de sistema"),
]
svg.text(85, 65, "Fuente de eventos externa", size=FS_BODY, bold=True)
for i, (evt, src) in enumerate(src_data):
y = 82 + i * 58
svg.rect(10, y, 150, 44, fill='light')
svg.text(85, y + 16, src, size=FS_SMALL, bold=True)
svg.mono(15, y + 36, evt, size=11)
svg.rect(200, 80, 200, 390, fill='white', stroke='border', dash=True)
svg.text(300, 100, "Servidor FastAPI", size=FS_BODY, bold=True)
svg.rect(215, 120, 170, 50, fill='medium')
svg.text(300, 137, "Endpoint HTTP", size=FS_SMALL, bold=True)
svg.text(300, 157, "POST /events/{type}", size=FS_TINY, fill='text_light')
svg.rect(215, 190, 170, 50, fill='light')
svg.text(300, 207, "Enrutador de eventos", size=FS_SMALL, bold=True)
svg.text(300, 227, "LLM determina urgencia", size=FS_TINY, fill='text_light')
svg.rect(215, 260, 170, 50, fill='light')
svg.text(300, 277, "Cola de eventos", size=FS_SMALL, bold=True)
svg.text(300, 297, "Orden por prioridad", size=FS_TINY, fill='text_light')
svg.rect(215, 330, 170, 50, fill='light')
svg.text(300, 347, "Bucle del Agente", size=FS_SMALL, bold=True)
svg.text(300, 367, "Obtener → Razonar → Ejecutar", size=FS_TINY, fill='text_light')
svg.rect(215, 400, 170, 50, fill='medium')
svg.text(300, 417, "Gestión de sesiones", size=FS_SMALL, bold=True)
svg.text(300, 437, "Contexto multihilo", size=FS_TINY, fill='text_light')
for i in range(4):
svg.arrow(300, 170 + i * 70, 300, 190 + i * 70)
for i in range(6):
svg.arrow(160, 104 + i * 58, 213, 145)
svg.text(610, 65, "Servidor de herramientas MCP", size=FS_BODY, bold=True)
tools = [
("Herramientas percepción", "search_web, read_file\nread_webpage, parse_image"),
("Herramienta ejecución", "code_interpreter\nvirtual_terminal, write_file"),
("Herramienta colaboración", "browser_use\nrequest_human_approval"),
("Herramienta notificación", "send_email, send_slack\nsend_im_notification"),
]
for i, (name, desc) in enumerate(tools):
y = 82 + i * 100
svg.rect(460, y, 250, 80, fill='light')
svg.text(585, y + 22, name, size=FS_SMALL, bold=True)
for j, line in enumerate(desc.split('\n')):
svg.mono(470, y + 48 + j * 18, line, size=12)
svg.arrow(400, 355, 458, 180)
svg.arrow(458, 260, 400, 355)
svg.rect(740, 82, 130, 380, fill='code_bg', stroke='dark', rx=4)
svg.text(805, 115, "Capa persistente", size=FS_SMALL, bold=True)
items = ["historial chat", "registro eventos", "tarea progr.", "est. herram.", "trazabilidad"]
for i, item in enumerate(items):
svg.text(805, 160 + i * 55, item, size=FS_SMALL)
svg.save(os.path.join(OUT, 'fig4-5.svg'))
def fig4_6():
"""sync-async model contradiction — Figure 4-6."""
w, h = 880, 520
svg = SVG(w, h)
svg.text(w / 2, 30, "Contradicción: paradigma síncrono vs realidad asíncrona", size=FS_TITLE, bold=True)
svg.rect(20, 55, w - 40, 195, fill='white', stroke='border', dash=True)
svg.text(60, 78, "Paradigma de entrenamiento (secuencia estrictamente síncrona)", size=FS_BODY, bold=True, anchor='start')
_pill(svg, w - 200, 64, 160, 28, "Restricción API", fill='dark', font_size=FS_SMALL)
steps_train = [
("Observación", 'medium', "Usuario: Verificar clima en Pekín"),
("Pensamiento", 'light', "Se requiere llamar a get_weather"),
("Acción", 'medium', "get_weather(Pekín)"),
("Observación", 'light', "22°C, despejado"),
]
bw, bh, gap = 180, 55, 22
sx = (w - (4 * bw + 3 * gap)) / 2
for i, (phase, fill, content) in enumerate(steps_train):
x = sx + i * (bw + gap)
svg.rect(x, 100, bw, bh, fill=fill)
svg.text(x + bw / 2, 120, phase, size=FS_SMALL, bold=True)
svg.text(x + bw / 2, 142, content, size=FS_TINY, fill='text_light')
if i < 3:
svg.arrow(x + bw + 2, 128, x + bw + gap - 2, 128)
svg.rect(sx, 170, 4 * bw + 3 * gap, 30, fill='code_bg', stroke='dark', rx=4)
svg.mono(sx + 10, 185,
"tool_call → debe ir seguido inmediatamente de tool_result, o error de API", size=FS_TINY)
svg.line(20, 262, w - 20, 262, color='dark', dash=True)
svg.text(w / 2, 280, "Contradicción", size=FS_BODY, bold=True, fill='darker')
svg.rect(20, 295, w - 40, 210, fill='white', stroke='border', dash=True)
svg.text(60, 318, "Realidad de despliegue (eventos asíncronos entrelazados)", size=FS_BODY, bold=True, anchor='start')
_pill(svg, w - 200, 304, 160, 28, "¡Conflicto de formato!", fill='dark', font_size=FS_SMALL)
items = [
("Asistente", 'medium', "tool_call:\nget_weather(Pekín)", 0.0, 0.20),
("Esperando...", 'code_bg', "Ejecución herramienta ~5s", 0.22, 0.50),
("Usuario interrumpe", 'dark', "\"No importa, \nmira Shanghái\"", 0.40, 0.55),
("???", 'code_bg', "¿Cuándo llega tool_result? \n¿Cómo preservar formato?", 0.57, 0.78),
("Marcador", 'light', "[Herramienta ejecutando, \nprioridad a interrupción]", 0.80, 1.0),
]
tl_x0, tl_w = 50, w - 100
for role, fill, txt, t0, t1 in items:
x0 = tl_x0 + tl_w * t0
x1 = tl_x0 + tl_w * t1
svg.rect(x0, 340, x1 - x0, 50, fill=fill, rx=4)
tc = 'white' if fill in ('dark', 'darker') else 'text'
svg.text((x0 + x1) / 2, 355, role, size=FS_TINY, bold=True, fill=tc)
for j, tl in enumerate(txt.split('\n')):
svg.text((x0 + x1) / 2, 372 + j * 14, tl, size=11, fill=tc)
svg.rect(50, 410, w - 100, 40, fill='code_bg', stroke='dark', rx=4)
svg.mono(60, 430,
"Solución: preservar formato con marcadores + encolar no urgentes + interrumpir solo si es urgente",
size=FS_TINY)
svg.rect(140, 465, w - 280, 40, fill='dark')
svg.text(w / 2, 485,
"Solución de fondo: los modelos futuros deben entrenarse con RL en entornos asíncronos",
size=FS_SMALL, fill='white', bold=True)
svg.save(os.path.join(OUT, 'fig4-6.svg'))
def fig4_7():
"""Experiment 4.5 — Figure 4-7."""
w, h = 880, 520
svg = SVG(w, h)
svg.text(w / 2, 30, "Experimento 4.5: Interrupción y recuperación en Agente asíncrono", size=FS_TITLE, bold=True)
tl_x0, tl_w = 120, 740
lanes = [
("Agente", 80),
("Herramienta A", 180),
("Herramienta B", 260),
("Herramienta C", 340),
("Trayectoria", 420),
]
for name, y in lanes:
svg.text(55, y, name, size=FS_SMALL, bold=True)
svg.line(tl_x0, y, tl_x0 + tl_w, y, color='dark', dash=True)
def tbar(y, t0, t1, fill, label, h_bar=22):
xs = tl_x0 + tl_w * t0
xe = tl_x0 + tl_w * t1
svg.rect(xs, y - h_bar / 2, xe - xs, h_bar, fill=fill, rx=3)
tc = 'white' if fill in ('dark', 'darker') else 'text'
svg.text((xs + xe) / 2, y, label, size=11, fill=tc)
tbar(80, 0.0, 0.12, 'medium', 'LLM: Iniciar 3 herramientas')
tbar(180, 0.13, 0.45, 'light', 'Script A: 3%/s → completa en 33s')
tbar(260, 0.13, 0.70, 'light', 'Script B: 2%/s → 50s...')
tbar(340, 0.13, 0.90, 'code_bg', 'Script C: 1%/s → 100s...')
t_done = 0.45
svg.line(tl_x0 + tl_w * t_done, 70, tl_x0 + tl_w * t_done, 450, color='border', dash=True)
svg.text(tl_x0 + tl_w * t_done, 62, "A completado", size=FS_TINY, bold=True)
tbar(80, 0.46, 0.58, 'medium', 'Consultar avance B, C')
tbar(420, 0.46, 0.58, 'light', 'B≈66% C≈33%')
t_cancel = 0.60
svg.line(tl_x0 + tl_w * t_cancel, 70, tl_x0 + tl_w * t_cancel, 450, color='dark', dash=True)
svg.text(tl_x0 + tl_w * t_cancel, 62, "Cancelar C", size=FS_TINY, bold=True, fill='darker')
tbar(340, 0.60, 0.65, 'dark', '×')
t_b_done = 0.70
svg.line(tl_x0 + tl_w * t_b_done, 70, tl_x0 + tl_w * t_b_done, 450, color='border', dash=True)
svg.text(tl_x0 + tl_w * t_b_done, 62, "B completado", size=FS_TINY, bold=True)
tbar(80, 0.72, 0.95, 'medium', 'LLM: Combinar res. A+B y generar informe')
tbar(420, 0.72, 0.95, 'light', 'Res. A + Res. B + Reg. cancelación C')
svg.rect(tl_x0, 460, tl_w, 40, fill='code_bg', stroke='dark', rx=4)
svg.mono(tl_x0 + 10, 480,
"Clave: inyección marcadores + evento finalización async + API cancel_tool(task_id)",
size=FS_TINY)
svg.save(os.path.join(OUT, 'fig4-7.svg'))
def fig4_8():
"""Tool discovery hierarchy — Figure 4-8."""
w, h = 880, 540
svg = SVG(w, h)
svg.text(w / 2, 30, "Coincidencia jerárquica de herramientas", size=FS_TITLE, bold=True)
svg.rect(250, 55, 380, 44, fill='medium')
svg.text(440, 77, "Agente: \"Necesito consultar estadísticas de colaboradores en GitHub\"", size=FS_SMALL, bold=True)
svg.arrow(440, 99, 440, 130)
svg.rect(300, 132, 280, 44, fill='dark')
svg.text(440, 154, "discover_tools(req_lenguaje_natural)", size=FS_SMALL, fill='white', bold=True)
svg.arrow(440, 176, 440, 210)
svg.rect(20, 210, w - 40, 110, fill='white', stroke='border', dash=True)
svg.text(55, 233, "Capa 1: Coincidencia de servidor (similitud semántica)", size=FS_BODY, bold=True, anchor='start')
servers = [
("GitHub", 0.92, 'dark'),
("Clima", 0.15, 'light'),
("Finanzas", 0.23, 'light'),
("ArXiv", 0.18, 'light'),
("Sistema arch.", 0.31, 'light'),
]
sx = 50
for name, score, fill in servers:
svg.rect(sx, 255, 145, 50, fill=fill)
tc = 'white' if fill in ('dark', 'darker') else 'text'
svg.text(sx + 72, 272, name, size=FS_SMALL, bold=True, fill=tc)
svg.text(sx + 72, 292, f"Similitud: {score:.2f}", size=FS_TINY, fill='white' if fill == 'dark' else 'text_light')
sx += 165
svg.arrow(123, 305, 123, 345)
svg.text(175, 330, "Top 1 servidor", size=FS_SMALL, fill='text_light')
svg.rect(20, 345, w - 40, 160, fill='white', stroke='border', dash=True)
svg.text(55, 368, "Capa 2: Coincidencia de herramientas (26 herramientas en servidor GitHub)", size=FS_BODY, bold=True, anchor='start')
tools = [
("search_repositories", 0.41, "Buscar repos"),
("list_contributors", 0.89, "Lista colaboradores"),
("get_repo_stats", 0.85, "Estadísticas repo"),
("create_issue", 0.12, "Crear issue"),
("get_commit_history", 0.67, "Historial commits"),
]
tx = 30
for name, score, desc in tools:
is_top = score > 0.80
fill = 'dark' if is_top else 'light'
svg.rect(tx, 388, 155, 55, fill=fill)
tc = 'white' if is_top else 'text'
svg.mono(tx + 5, 406, name, size=11, fill=tc)
svg.text(tx + 78, 428, f"{score:.2f} | {desc}", size=11, fill='white' if is_top else 'text_light')
tx += 170
svg.rect(180, 468, 520, 30, fill='code_bg', stroke='dark', rx=4)
svg.mono(190, 483, "Devolver top 3: list_contributors, get_repo_stats, get_commit_history", size=12)
svg.save(os.path.join(OUT, 'fig4-8.svg'))
def fig4_9():
"""KV Cache Optimization — Figure 4-9."""
w, h = 880, 560
svg = SVG(w, h)
svg.text(w / 2, 30, "Optimización de KV Cache para carga dinámica de herramientas", size=FS_TITLE, bold=True)
left_x = 30
svg.text(220, 65, "Enfoque ingenuo (la caché se invalida)", size=FS_BODY, bold=True)
blocks_naive = [
("Prompt del sistema", 120, 'medium', "Eres un asistente de IA...\n+ Esquemas de todas las herramientas", "~50K tokens"),
("Mensaje del usuario", 100, 'light', "Consultar precio de acción NVDA", ""),
("Asistente", 80, 'light', "tool_call: ...", ""),
]
ny = 85
for label, bh, fill, content, note in blocks_naive:
svg.rect(left_x, ny, 380, bh, fill=fill, rx=4)
svg.text(left_x + 190, ny + 22, label, size=FS_SMALL, bold=True)
for j, line in enumerate(content.split('\n')):
svg.text(left_x + 190, ny + 44 + j * 20, line, size=FS_TINY, fill='text_light')
if note:
svg.text(left_x + 360, ny + 22, note, size=FS_TINY, fill='darker', anchor='end')
ny += bh + 8
svg.rect(left_x, ny + 5, 380, 40, fill='dark')
svg.text(left_x + 190, ny + 25, "¡Cada vez que se carga una herramienta → se invalida la caché!", size=FS_SMALL, fill='white', bold=True)
right_x = 460
svg.text(660, 65, "Enfoque optimizado (estabilidad de caché)", size=FS_BODY, bold=True)
blocks_opt = [
("Prompt del sistema (fijo)", 75, 'medium',
"Eres un asistente de IA...\nRol + Reglas + Herramientas básicas",
"~2K tokens | KV Cache"),
("Barra estado Agente (ligera)", 45, 'code_bg',
"Herramientas disps: web_search, get_weather...",
"~200 tokens"),
("User: discover_tools", 40, 'light',
'"Necesito revisar precio de acciones"',
""),
("Resultado herramienta", 55, 'light',
"Devolver esquema de get_stock_quote",
"Definición de herramienta aquí"),
("Mensaje del usuario", 40, 'light',
"Consultar precio de acción NVDA",
""),
("Barra estado Agente (actualizada)", 45, 'code_bg',
"+get_stock_quote añadido",
"~220 tokens"),
]
oy = 85
for label, bh, fill, content, note in blocks_opt:
svg.rect(right_x, oy, 400, bh, fill=fill, rx=4)
svg.text(right_x + 200, oy + 16, label, size=FS_SMALL, bold=True)
for j, line in enumerate(content.split('\n')):
svg.text(right_x + 200, oy + 32 + j * 16, line, size=FS_TINY, fill='text_light')
if note:
svg.text(right_x + 390, oy + 16, note, size=11, fill='darker', anchor='end')
oy += bh + 5
svg.rect(right_x, oy + 5, 400, 40, fill='medium')
svg.text(right_x + 200, oy + 25, "Prompt sistema no cambia → KV Cache se reutiliza totalmente", size=FS_SMALL, bold=True)
svg.line(30, 475, w - 30, 475, color='dark', dash=True)
comps = [
("Tasa acierto de caché", "~0% (se invalida en cada cambio)", "~95% (solo cambia ligeramente la pista)"),
("Latencia primer token", "Alta (recálculo 50K tokens cada vez)", "Baja (cómputo incremental ~200 tokens)"),
]
cy = 495
svg.text(250, cy, "Dimensión de comparación", size=FS_SMALL, bold=True)
svg.text(500, cy, "Enfoque ingenuo", size=FS_SMALL, bold=True)
svg.text(740, cy, "Enfoque optimizado", size=FS_SMALL, bold=True)
for metric, naive, opt in comps:
cy += 28
svg.text(250, cy, metric, size=FS_TINY)
svg.text(500, cy, naive, size=FS_TINY, fill='text_light')
svg.text(740, cy, opt, size=FS_TINY, fill='text_light')
svg.save(os.path.join(OUT, 'fig4-9.svg'))
def fig4_10():
"""Tool Self-Evolution Pipeline — Figure 4-10."""
w, h = 880, 500
svg = SVG(w, h)
svg.text(w / 2, 30, "Autoevolución del Agente: de requisito a herramienta", size=FS_TITLE, bold=True)
stages = [
("① Detección requisito", 'medium', [
"Tarea: Extraer subtítulos YouTube",
"Agente: Herramientas insufic.",
"→ Iniciar autoevolución",
]),
("② Búsqueda web", 'light', [
"search: youtube transcript",
"python library",
"→ 3 librerías candidatas",
]),
("③ Exploración GitHub", 'light', [
"Visitar repo jdepoix/youtube-",
"transcript-api",
"→ Leer README + ejemplos",
]),
("④ Aprendizaje y prueba", 'light', [
"Prueba code_interpreter:",
"from youtube_transcript",
" _api import ...",
]),
("⑤ Encapsular herramienta", 'medium', [
"Crear herramienta MCP:",
"get_youtube_transcript",
"(video_id) → texto",
]),
]
stage_w, stage_h = 155, 145
gap = 12
total_w = len(stages) * stage_w + (len(stages) - 1) * gap
sx = (w - total_w) / 2
for i, (title, fill, details) in enumerate(stages):
x = sx + i * (stage_w + gap)
svg.rect(x, 60, stage_w, stage_h, fill=fill)
svg.text(x + stage_w / 2, 82, title, size=FS_SMALL, bold=True)
svg.line(x + 10, 94, x + stage_w - 10, 94, color='dark')
for j, line in enumerate(details):
svg.mono(x + 8, 114 + j * 20, line, size=11)
if i < len(stages) - 1:
svg.arrow(x + stage_w + 2, 60 + stage_h / 2, x + stage_w + gap - 2, 60 + stage_h / 2)
svg.arrow(w / 2, 205, w / 2, 240)
svg.rect(120, 240, w - 240, 50, fill='dark')
svg.text(w / 2, 265, "⑥ Guardar en biblioteca de herramientas → reutilizar directamente en el futuro", size=FS_BODY, fill='white', bold=True)
svg.arrow(w / 2, 290, w / 2, 320)
svg.rect(60, 320, w - 120, 160, fill='white', stroke='border', dash=True)
svg.text(w / 2, 345, "Reutilización de herramienta: siguiente encuentro con tarea similar", size=FS_BODY, bold=True)
svg.rect(80, 365, 340, 50, fill='code_bg', stroke='dark', rx=4)
svg.mono(90, 382, "Agente: \"Necesito extraer subtítulos de YouTube\"", size=FS_TINY)
svg.mono(90, 400, "→ search_tools(\"youtube transcript\")", size=FS_TINY)
svg.arrow(420, 390, 460, 390)
svg.rect(460, 365, 330, 50, fill='light')
svg.text(625, 382, "¡Encontrada! get_youtube_transcript", size=FS_SMALL, bold=True)
svg.text(625, 402, "Omitir búsqueda y creación, llamar directamente", size=FS_TINY, fill='text_light')
svg.rect(200, 430, 480, 35, fill='medium')
svg.text(w / 2, 448, "Capa herramientas + Capa conocimiento + Capa estrategia → mayor dominio con el uso", size=FS_SMALL, bold=True)
svg.save(os.path.join(OUT, 'fig4-10.svg'))
def fig4_11():
"""Experiment 4.7 — Figure 4-11."""
w, h = 880, 480
svg = SVG(w, h)
svg.text(w / 2, 30, "Experimento 4.7: Canalización de Agente autoevolutivo", size=FS_TITLE, bold=True)
svg.rect(30, 60, w - 60, 48, fill='medium')
svg.text(w / 2, 76, "Herramientas básicas (conjunto mínimo)", size=FS_SMALL, bold=True)
base_tools = ["web_search", "read_webpage", "code_interpreter", "create_tool", "search_tools"]
btx = 65
for t in base_tools:
tw = len(t) * 8 + 20
_pill(svg, btx, 82, tw, 22, t, fill='dark', font_size=11, bold=True)
btx += tw + 10
svg.arrow(w / 2, 108, w / 2, 135)
svg.rect(100, 135, w - 200, 45, fill='code_bg', stroke='dark', rx=4)
svg.mono(110, 150,
"Tarea: \"¿Precio reciente acción NVDA? ¿Cambio semanal?\" → Agente: ¡Sin herr. financiera!",
size=FS_TINY)
svg.mono(110, 168,
"→ Detectar brecha de capacidad → Iniciar autoevolución",
size=FS_TINY)
svg.arrow(w / 2, 180, w / 2, 210)
pipe_y = 210
pipe_stages = [
("web_search", "Buscar soluciones", 'light',
["\"python stock price API\"",
"→ yfinance, Alpha Vantage..."]),
("read_webpage", "Evaluar soluciones", 'light',
["yfinance: gratis, sin clave API",
"Alpha Vantage: requiere registro..."]),
("code_interpreter", "Probar y verificar", 'light',
["import yfinance as yf",
"yf.Ticker('NVDA').history()"]),
("create_tool", "Encapsular y registrar", 'medium',
["name: get_stock_data",
"schema: {ticker, period}"]),
]
pw = 190
pgap = 15
total_pw = len(pipe_stages) * pw + (len(pipe_stages) - 1) * pgap
px = (w - total_pw) / 2
for i, (tool, desc, fill, details) in enumerate(pipe_stages):
svg.rect(px, pipe_y, pw, 120, fill=fill)
_pill(svg, px + 10, pipe_y + 8, pw - 20, 22, tool, fill='dark', font_size=11, bold=True)
svg.text(px + pw / 2, pipe_y + 48, desc, size=FS_SMALL, bold=True)
for j, line in enumerate(details):
svg.mono(px + 8, pipe_y + 70 + j * 18, line, size=11)
if i < len(pipe_stages) - 1:
svg.arrow(px + pw + 2, pipe_y + 60, px + pw + pgap - 2, pipe_y + 60)
px += pw + pgap
svg.arrow(w / 2, 330, w / 2, 360)
svg.rect(200, 360, w - 400, 44, fill='dark')
svg.text(w / 2, 382, "Biblioteca de herramientas: get_stock_data registrada", size=FS_BODY, fill='white', bold=True)
svg.arrow(w / 2, 404, w / 2, 430)
svg.rect(100, 430, w - 200, 40, fill='code_bg', stroke='dark', rx=4)
svg.mono(110, 442,
"Verificación de reutilización: \"Consultar acción TSLA\" → search_tools coincide → llama a get_stock_data",
size=FS_TINY)
svg.mono(110, 458,
"Omitir búsqueda/evaluación/prueba → reducción de costo +90%",
size=FS_TINY)
svg.save(os.path.join(OUT, 'fig4-11.svg'))
def fig4_12():
"""Voyager learning loop — Figure 4-12."""
w, h = 880, 520
svg = SVG(w, h)
svg.text(w / 2, 30, "Voyager: Arquitectura de Agente para aprendizaje continuo", size=FS_TITLE, bold=True)
svg.rect(20, 65, 260, 180, fill='white', stroke='border', dash=True)
svg.text(150, 88, "Generador automático de plan", size=FS_BODY, bold=True)
curriculum = [
"Entrada: estado actual + habilidades",
"Salida: siguiente objetivo de exploración",
"",
"Ejemplo secuencia objetivos:",
" Talar árbol → Crear tablas de madera",
" → Crear pico de madera → Minar piedra",
" → Hacer horno → Fundir mineral hierro",
]
for i, line in enumerate(curriculum):
svg.mono(32, 112 + i * 20, line, size=12)
svg.rect(600, 65, 260, 180, fill='white', stroke='border', dash=True)
svg.text(730, 88, "Mecanismo de prompt iterativo", size=FS_BODY, bold=True)
iterative = [
"Recopilar retroalim. en fallos:",
" - Observación entorno (error)",
" - Resultado autoverificación",
"",
"Integrar en prompt LLM",
"→ Guiar refinamiento de código",
"→ Iterar hasta tener éxito",
]
for i, line in enumerate(iterative):
svg.mono(612, 112 + i * 20, line, size=12)
svg.arrow(280, 155, 370, 155, label="Objetivo")
svg.arrow(560, 155, 600, 155, label="Retroalim.")
svg.rect(370, 110, 190, 80, fill='medium')
svg.text(465, 140, "Ejecución del Agente", size=FS_BODY, bold=True)
svg.text(465, 165, "Generación código GPT-4", size=FS_SMALL, fill='text_light')
svg.arrow(465, 190, 465, 260)
svg.text(510, 230, "Éxito → Extraer", size=FS_SMALL, fill='text_light')
svg.rect(120, 260, 640, 240, fill='white', stroke='border', dash=True)
svg.text(440, 283, "Biblioteca de habilidades — núcleo del aprendizaje externalizado", size=FS_BODY, bold=True)
skills = [
("chopTree()", "Talar árbol\nHabilidad básica", "function chopTree() {\n bot.dig(nearest('log'));\n}"),
("craftPlanks()", "Crear tablas madera\nLlama a chopTree", "function craftPlanks() {\n chopTree(); craft('planks');\n}"),
("craftPickaxe()", "Crear pico madera\nCombina habilidades", "function craftPickaxe() {\n craftPlanks(); craft('stick');\n craft('wooden_pickaxe');\n}"),
]
skx = 140
for name, desc, code in skills:
svg.rect(skx, 305, 190, 175, fill='light')
svg.text(skx + 95, 325, name, size=FS_SMALL, bold=True)
for j, dl in enumerate(desc.split('\n')):
svg.text(skx + 95, 347 + j * 18, dl, size=FS_TINY, fill='text_light')
svg.rect(skx + 10, 385, 170, 80, fill='code_bg', stroke='dark', rx=4)
for j, cl in enumerate(code.split('\n')):
svg.mono(skx + 18, 400 + j * 18, cl, size=11)
skx += 215
svg.arrow_curved(120, 380, 150, 245, curve=60, label="Habilidades dispon.", dash=True, color='dark')
svg.save(os.path.join(OUT, 'fig4-12.svg'))
def main():
os.makedirs(OUT, exist_ok=True)
figs = [
fig4_1, fig4_2, fig4_3, fig4_4, fig4_5, fig4_6,
fig4_7, fig4_8, fig4_9, fig4_10, fig4_11, fig4_12,
]
for fn in figs:
fn()
print(f" ✓ {fn.__name__}: {fn.__doc__}")
print(f"\nGenerated {len(figs)} figures in {OUT}/")
if __name__ == '__main__':
main()