Módulo 2: El patrón supervisor/router
Construyendo pricing_agent
Descripción
El Módulo 1 nombró a pricing_agent desde la lección 02: "compone get_quote para comparar el
costo de varias salas". Pero en las ocho lecciones de ese módulo, pricing_agent nunca corrió como
agente completo — el mini-proyecto (lección 08) lo dejó explícito: el criterio estricto de ese
módulo, aplicado al escenario de comparar precios en aislamiento, no exigía un agente separado, así
que nunca se ejecutó su propio run_agent_parallel. Esta lección cierra esa cuenta pendiente: acá
pricing_agent corre por primera vez de punta a punta, con su propio guion, sobre el mismo runner
de siempre.
Vas a ver algo que el Módulo 1 ya adelantó pero que ahora se vuelve concreto en ejecución:
pricing_agent no tiene ninguna tool que booking_agent no tenga — su único registro es
{"get_quote": rt.get_quote}, un subconjunto exacto del de booking_agent. Lo que lo hace un
especialista real, ejecutado en esta lección, es la forma en que compone esa tool: tres
llamadas a get_quote en el mismo turno, ninguna dependiente de la otra, seguidas de una
comparación en texto — nunca una reserva.
Conexión con el módulo
Esta lección completa el registro SPECIALISTS de la lección 02: ahí pricing_agent ya estaba
declarado, con su tools y su expertise, pero nunca se había ejecutado un guion real sobre él.
Con esta lección, los tres especialistas del módulo quedan probados de punta a punta, listos para
el dispatcher completo de la lección 06.
Analogía: el mismo llavero, un trabajo distinto
Retoma el llavero de agent-fundamentals M5: una sola llave, get_quote, que booking_agent usa
para saber cuánto cobrar antes de reservar. pricing_agent tiene esa misma llave — literalmente la
misma función de Python, rt.get_quote— pero la usa para un trabajo distinto: no abrir una puerta
(reservar), sino comparar cuánto costaría abrir tres puertas distintas, sin abrir ninguna. La llave
es igual; el oficio de quien la sostiene, no.
Ejemplo trabajado: pricing_agent de punta a punta
import concurrent.futures
import reservo_tools as rt
def dispatch_parallel(tool_use_blocks, tools):
"""El de agent-fundamentals M4/M5, sin cambios."""
with concurrent.futures.ThreadPoolExecutor(max_workers=len(tool_use_blocks)) as pool:
futures = [pool.submit(tools[b["name"]], **b["input"]) for b in tool_use_blocks]
results = [f.result() for f in futures]
return [
{"type": "tool_result", "tool_use_id": b["id"], "content": str(r)}
for b, r in zip(tool_use_blocks, results)
]
def run_agent_parallel(question, model_script, tools, max_iterations=10):
"""El de agent-fundamentals M4/M5, sin cambios."""
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations alcanzado ({max_iterations})")
def count_model_calls(history):
return sum(1 for m in history if m["role"] == "assistant")
def count_tool_calls(history):
total = 0
for m in history:
if isinstance(m["content"], list):
total += sum(1 for b in m["content"] if b["type"] == "tool_use")
return total
# pricing_agent NO tiene una tool nueva -- reusa get_quote de booking_agent.
PRICING_TOOLS = {"get_quote": rt.get_quote}
TASK_PRICING = "Compara el precio de Focus, Studio y Boardroom, todos pro, 3h."
# Guion (concepto, claude-sonnet-5): las tres cotizaciones son independientes
# entre sí -- ninguna necesita el resultado de otra -- así que el modelo las
# pide las TRES en el mismo turno (dispatch_parallel ya sabe despachar más
# de un tool_use por turno, sin cambios, desde agent-fundamentals M5).
model_script_pricing = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_03", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 3}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Focus pro 3h: 6000 centavos. Studio pro 3h: 9600 centavos. "
"Boardroom pro 3h: 19200 centavos. Focus es la opción más "
"barata de las tres."
)}]},
]
final, history = run_agent_parallel(TASK_PRICING, model_script_pricing, PRICING_TOOLS)
print("--- historial completo de pricing_agent ---")
for i, m in enumerate(history):
role, content = m["role"], m["content"]
if isinstance(content, str):
print(f" [{i}] {role:<9} pregunta: {content!r}")
continue
for block in content:
if block["type"] == "tool_use":
print(f" [{i}] {role:<9} tool_use({block['name']}): {block['input']}")
elif block["type"] == "tool_result":
print(f" [{i}] {role:<9} tool_result: {block['content']}")
elif block["type"] == "text":
print(f" [{i}] {role:<9} texto final: {block['text']!r}")
print()
print("respuesta final:", final["content"][0]["text"])
print("llamadas al modelo:", count_model_calls(history))
print("llamadas a tools: ", count_tool_calls(history))
Qué esperar:
--- historial completo de pricing_agent ---
[0] user pregunta: 'Compara el precio de Focus, Studio y Boardroom, todos pro, 3h.'
[1] assistant tool_use(get_quote): {'room': 'Focus', 'tier': 'pro', 'hours': 3}
[1] assistant tool_use(get_quote): {'room': 'Studio', 'tier': 'pro', 'hours': 3}
[1] assistant tool_use(get_quote): {'room': 'Boardroom', 'tier': 'pro', 'hours': 3}
[2] user tool_result: {'price_cents': 6000}
[2] user tool_result: {'price_cents': 9600}
[2] user tool_result: {'price_cents': 19200}
[3] assistant texto final: 'Focus pro 3h: 6000 centavos. Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. Focus es la opción más barata de las tres.'
respuesta final: Focus pro 3h: 6000 centavos. Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. Focus es la opción más barata de las tres.
llamadas al modelo: 2
llamadas a tools: 3
Dos llamadas al modelo —el turno que pide las tres cotizaciones y el turno del texto final—, tres
llamadas a tools —una por sala—, y ningún hop entre agentes todavía, porque este ejemplo corre
pricing_agent en aislamiento, sin supervisor por encima. Los tres precios coinciden con la
fórmula de siempre: Studio pro 3h = 4000 * 3 * 80 // 100 = 9600; Boardroom pro 3h = 8000 * 3 * 80 // 100 = 19200.
Por qué las tres cotizaciones van en el mismo turno
Compara esto con booking_agent cotizando y reservando (lección 02): ahí, book_room necesita
el price_cents que get_quote todavía no había devuelto, así que las dos tool calls tenían que ir
en turnos separados, en secuencia. Acá, ninguna de las tres cotizaciones de pricing_agent depende
de las otras dos — Focus, Studio y Boardroom se cotizan de forma completamente independiente, así
que el guion las pide todas en el mismo turno. dispatch_parallel ya sabe manejar esto sin
ningún cambio: recorre los tres tool_use del turno, llama a get_quote tres veces, y arma los
tres tool_result en el mismo orden en que llegaron los bloques de entrada.
Esta es, dicho sea de paso, exactamente la propiedad de "separabilidad genuina" que midió el Módulo 1, lección 06 —el resultado de una sub-tarea no depende del resultado de otra—. El Módulo 4 de esta guía retoma esta misma idea a una escala mayor: fan-out entre agentes completos, no solo entre tool calls dentro de uno solo.
La garantía técnica: pricing_agent no puede reservar nada
pricing_agent nunca reserva una sala, y esa no es solo una promesa de comportamiento del modelo
—es una imposibilidad técnica. PRICING_TOOLS = {"get_quote": rt.get_quote} no incluye
book_room en absoluto. Si el guion de pricing_agent intentara, por error o por una decisión mal
tomada, pedir un tool_use con name="book_room", dispatch_parallel fallaría con un KeyError
—la misma clase de error que viste en la lección 02 con un target inválido—, porque
tools["book_room"] no existe en el registro de pricing_agent. El Ejercicio 3 de esta lección lo
confirma ejecutando exactamente ese caso.
Esta es una distinción importante frente al ejemplo de search_docs del Módulo 1: ahí, el límite
de policy_agent era de cobertura (el stub no conoce todas las políticas posibles). Acá, el
límite de pricing_agent es de capacidad — no es que decida no reservar, es que no tiene la
tool para hacerlo, sin importar qué decida el modelo.
Errores comunes
-
Pensar que
pricing_agentnecesita una tool nueva para ser un especialista "de verdad". El Módulo 1, lección 02, ya lo señaló: lo que distingue a un agente no es su inventario de funciones Python, es el objetivo con el que las usa.pricing_agentcomponeget_quotepara informar;booking_agentla usa como paso previo para transaccionar. -
Ejecutar el guion de
pricing_agentconPRICING_TOOLSincompleto o vacío. Sin"get_quote": rt.get_quoteen el diccionario,dispatch_parallelfalla conKeyErroren la primera tool call — el mismo tipo de error de la lección 02, ahora en un contexto distinto. -
Confundir "tres tool_use en el mismo turno" con el patrón de fan-out completo del Módulo 4. Esto es fan-out dentro de un solo agente —tres llamadas a la misma tool, en el mismo registro—; el Módulo 4 construye fan-out entre agentes distintos, cada uno con su propio registro y su propio historial. Son parientes, no lo mismo.
-
Olvidar que el orden de los resultados no depende del orden de finalización de los hilos.
dispatch_parallelarma la lista final conzip(tool_use_blocks, results)— el mismo mecanismo ya visto en el Módulo 1, lección 06 —, así que los trestool_resultsiempre aparecen en el orden Focus/Studio/Boardroom, sin importar cuálget_quoteterminó primero en la realidad. -
Pensar que este ejemplo prueba que multi-agente conviene para comparar precios. No — el Módulo 1, lección 08 (Escenario E), ya midió que comparar precios en aislamiento no justifica un agente separado por sí solo (1/3 señales).
pricing_agentse justifica por continuidad pedagógica con el resto de esta guía —los Módulos 2 a 8 lo usan como ejemplo recurrente de composición—, no porque el criterio estricto del Módulo 1 lo exija para este caso aislado.
Ejercicios
Ejercicio 1: Verifica las anclas a mano (Fácil)
Sin ejecutar nada, calcula get_quote("Studio", "pro", 3) y get_quote("Boardroom", "pro", 3) a
mano, usando la fórmula ROOM_RATE_CENTS[room] * hours * 80 // 100. Confirma tu cálculo
ejecutando las dos llamadas reales.
Ver solución
Cálculo a mano: Studio pro 3h = 4000 * 3 = 12000, 12000 * 80 // 100 = 9600. Boardroom pro 3h =
8000 * 3 = 24000, 24000 * 80 // 100 = 19200.
print("Studio pro 3h:", rt.get_quote("Studio", "pro", 3))
print("Boardroom pro 3h:", rt.get_quote("Boardroom", "pro", 3))
Salida esperada:
Studio pro 3h: {'price_cents': 9600}
Boardroom pro 3h: {'price_cents': 19200}
Ambos coinciden con el cálculo a mano y con la respuesta final del ejemplo trabajado.
Ejercicio 2: Compara basic contra pro para la misma sala (Medio)
Escribe un guion nuevo para pricing_agent que compare "Focus pro 3h" contra "Focus basic 3h" —dos
get_quote en el mismo turno, cada uno con un tier distinto— y ejecutalo.
Ver solución
task_ex2 = "Compara Focus pro 3h contra Focus basic 3h."
script_ex2 = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Focus", "tier": "basic", "hours": 3}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Focus pro 3h: 6000 centavos. Focus basic 3h: 7500 centavos. El tier pro ahorra 1500 centavos frente a basic."}]},
]
final_ex2, hist_ex2 = run_agent_parallel(task_ex2, script_ex2, PRICING_TOOLS)
print("respuesta:", final_ex2["content"][0]["text"])
Salida esperada:
respuesta: Focus pro 3h: 6000 centavos. Focus basic 3h: 7500 centavos. El tier pro ahorra 1500 centavos frente a basic.
Explicación: pricing_agent puede comparar cualquier combinación de sala/tier/horas, no solo
las tres salas entre sí — su expertise es "comparar", no "comparar exactamente tres salas". La
diferencia de 1500 centavos (7500 - 6000) es exactamente el 20% de descuento que aplica pro
sobre basic, para las mismas 3 horas de Focus.
Ejercicio 3: Confirma que pricing_agent no puede reservar (Difícil)
Arma un guion donde el primer turno de pricing_agent pida un tool_use con name="book_room"
(en vez de get_quote) y ejecutalo contra PRICING_TOOLS. Confirma que run_agent_parallel falla
con un KeyError, y explica por qué esa falla es una garantía técnica, no una decisión de
comportamiento del modelo.
Ver solución
task_ex3 = "Compara Focus pro 3h y resérvalo directamente."
script_ex3 = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
]
try:
run_agent_parallel(task_ex3, script_ex3, PRICING_TOOLS)
except KeyError as e:
print(f"KeyError capturado: {e!r} -- 'book_room' no está en PRICING_TOOLS")
Salida real:
KeyError capturado: KeyError('book_room') -- 'book_room' no está en PRICING_TOOLS
Explicación: el KeyError ocurre dentro de dispatch_parallel, en la línea tools[b["name"]]
— Python busca "book_room" en el diccionario PRICING_TOOLS y no lo encuentra, sin importar qué
haya "decidido" el guion. Esto confirma que la restricción no depende de que el modelo se comporte
bien: aunque el guion (concepto) intentara forzar una reserva, la ejecución real fallaría antes de
tocar ningún dato de Reservo. Es una garantía del registro de tools, no de la buena voluntad de
la decisión — el mismo tipo de barrera técnica que separaba policy_agent de booking_agent en el
Módulo 1, lección 06.
Resumen y siguiente paso
pricing_agentcorrió por primera vez de punta a punta: dos llamadas al modelo, tres tool calls aget_quoteen el mismo turno —porque ninguna de las tres cotizaciones depende de otra—, y una comparación final en texto.- Su registro de tools,
{"get_quote": rt.get_quote}, es un subconjunto exacto del debooking_agent— lo que lo hace un especialista distinto es la composición, no una tool nueva. pricing_agentno puede llamarbook_room— no por decisión del modelo, sino porque esa tool no está en su registro. Es una garantía técnica, confirmada ejecutando el error.- Con esto, los tres especialistas de Reservo —
booking_agent,policy_agent,pricing_agent— quedan probados de punta a punta y listos para el dispatcher completo.
Siguiente lección: 06 — El dispatcher ejecutado de punta a punta. La pieza central del
módulo: route_deterministic (lección 03) más SPECIALISTS/run_specialist (lección 02) sobre
los tres especialistas completos, ruteando tres peticiones distintas al agente correcto de cada una.
Recursos adicionales
- Anthropic — Tool use (function calling) overview — La forma de varios
tool_useen el mismo turno, la pieza del protocolo que hace posible quepricing_agentpida las tres cotizaciones a la vez. - Python —
concurrent.futures— El módulo detrás dedispatch_parallel, reusado sin cambios desdeagent-fundamentalsM5. - Anthropic — Building effective agents — El patrón "parallelization" para sub-tareas independientes, la base conceptual de por qué las tres cotizaciones van en un solo turno.
- Python — Diccionarios — La estructura detrás de
PRICING_TOOLS, la garantía técnica de qué puede y qué no puede hacer este especialista.