# El patrón enrutado en profundidad: clasificar y derivar en Python

> Implementa el patrón de enrutado en Python: clasificación con salida estructurada, un umbral de confianza y un ejemplo completo y ejecutable.

- Capítulo: 0.2
- Dificultad: básico
- Fuente: https://book-ai-engineer.lucenyo.dev/es/lessons/fundamentos/patron-enrutado-python/

---
En este capítulo vas a implementar el patrón de enrutado de verdad: una primera llamada que solo clasifica el mensaje y, en función del resultado, una segunda llamada que activa un único especialista —o ninguna, si lo mejor es derivar a una persona—. Al terminar este capítulo serás capaz de:

1. Entender en qué consiste el patrón de enrutado y su ruta de fallback a un humano.
2. Forzar que el router devuelva una decisión estructurada: departamento, confianza y motivo.
3. Escribir la función de clasificación y la función que activa al especialista elegido.
4. Implementar el umbral de confianza mínima que decide cuándo es más seguro derivar a un humano que arriesgarse.
5. Encadenar todo en una función completa que atiende una consulta real.

---

## El patrón de enrutado

El router analiza la petición de entrada y decide a qué especialista delegarla. Lo veremos con un ejemplo concreto: un chat de soporte con tres departamentos —facturación, técnico y devoluciones— y una ruta de fallback a un humano cuando la confianza de la clasificación es baja.

```mermaid
sequenceDiagram
    participant M as atender_consulta()
    participant API as OpenAI API
    participant Esp as responder_como_especialista()
    participant Hum as Persona humana

    M->>API: clasificar(mensaje) — responses.parse
    API-->>M: Decision(departamento, confianza, motivo)
    alt confianza >= confianza_minima
        M->>Esp: responder_como_especialista(departamento, mensaje)
        Esp->>API: responses.create
        API-->>Esp: respuesta
        Esp-->>M: respuesta
    else confianza < confianza_minima
        M->>Hum: deriva la conversación
        Hum-->>M: "Voy a pasarte con un compañero..."
    end
```

---

## Preparar el proyecto

Si vienes siguiendo los capítulos anteriores, el proyecto ya tiene lo necesario. Si empiezas directamente aquí, sigue primero [Preparar el proyecto: herramientas y entorno](/es/lessons/preparacion/preparar-el-proyecto/) y luego instala lo que usa este capítulo:

```bash
uv add openai pydantic
```

---

## El router también necesita salida estructurada

Igual que en el pipeline, aquí la salida del primer paso alimenta directamente al segundo: si el modelo no devolviera un departamento válido y un número de confianza, no habría forma fiable de decidir hacia dónde enrutar. Por eso el router también se apoya en salida estructurada, esta vez con un campo restringido a un conjunto cerrado de valores:

```python
import os
from typing import Literal

from openai import OpenAI
from pydantic import BaseModel, Field

DEFAULT_MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.1")

class Decision(BaseModel):
    departamento: Literal["facturacion", "tecnico", "devoluciones"]
    confianza: float = Field(ge=0, le=1)
    motivo: str
```

`Literal[...]` obliga a que `departamento` sea exactamente uno de esos tres valores —no un texto libre que luego haya que normalizar— y `Field(ge=0, le=1)` mantiene la confianza dentro de un rango de 0 a 1.

---

## Paso 1: clasificar el mensaje

```python
def clasificar(client: OpenAI, mensaje: str) -> Decision:
    respuesta = client.responses.parse(
        model=DEFAULT_MODEL,
        instructions=(
            "Clasifica el mensaje de un cliente de una tienda online "
            "en el departamento adecuado e indica tu confianza (0 a 1)."
        ),
        input=mensaje,
        text_format=Decision,
    )
    decision = respuesta.output_parsed
    if decision is None:
        raise ValueError("El modelo no devolvió una decisión válida")
    return decision
```

Fíjate en que esta función **no responde al cliente**: solo clasifica. Separar "decidir a dónde va" de "generar la respuesta final" es lo que permite insertar el umbral de confianza en medio, antes de comprometerse con un especialista.

---

## Paso 2: responder con el especialista elegido

Cada departamento es, de nuevo, un prompt distinto sobre la misma función:

```python
DEPARTAMENTOS = {
    "facturacion": (
        "Eres del equipo de facturación de una tienda online. "
        "Responde con empatía y explica el siguiente paso concreto."
    ),
    "tecnico": (
        "Eres soporte técnico de una tienda online. Da una posible "
        "causa y una solución paso a paso, sin jerga."
    ),
    "devoluciones": (
        "Eres del equipo de devoluciones. Explica el proceso de "
        "devolución en tres pasos numerados."
    ),
}

def responder_como_especialista(
    client: OpenAI, departamento: str, mensaje: str
) -> str:
    respuesta = client.responses.create(
        model=DEFAULT_MODEL,
        instructions=DEPARTAMENTOS[departamento],
        input=mensaje,
    )
    return respuesta.output_text
```

---

## El umbral de confianza: cuándo derivar a un humano

Cuando la confianza de la clasificación es baja, activar igualmente a un especialista es apostar a que el router acertó. Un umbral mínimo convierte esa apuesta en una decisión explícita: por debajo de cierta confianza, se deriva a una persona en vez de arriesgarse a una respuesta mal dirigida.

```python
def atender_consulta(
    mensaje: str, client: OpenAI | None = None, confianza_minima: float = 0.7
) -> dict:
    client = client or OpenAI()

    print("El router clasifica el mensaje…")
    decision = clasificar(client, mensaje)
    print(f"   Departamento: {decision.departamento}")
    print(f"   Confianza:    {decision.confianza}")
    print(f"   Motivo:       {decision.motivo}")

    if decision.confianza < confianza_minima:
        print("Confianza baja: se deriva a un agente humano")
        return {
            "departamento": "humano",
            "confianza": decision.confianza,
            "respuesta": (
                "Voy a pasarte con un compañero para asegurarnos de "
                "resolverlo bien. Un momento, por favor."
            ),
        }

    print(f"Solo se activa el especialista de {decision.departamento}")
    respuesta = responder_como_especialista(client, decision.departamento, mensaje)
    return {
        "departamento": decision.departamento,
        "confianza": decision.confianza,
        "respuesta": respuesta,
    }
```

---

## Ejecutarlo

```python
if __name__ == "__main__":
    resultado = atender_consulta(
        "Me habéis cobrado dos veces el mismo pedido, ¿qué hago?"
    )
    print(f"\nRespuesta de {resultado['departamento']}")
    print(resultado["respuesta"])
```

Ejecútalo con `uv run main.py`: verás primero la clasificación (departamento, confianza y motivo) y después, solo si la confianza supera el umbral, la respuesta del especialista correspondiente.

---

## Qué te llevas de este ejemplo

- **Clasificar y responder son dos llamadas distintas, no una.** Eso es lo que permite insertar el umbral de confianza entre medias, antes de comprometerse con una respuesta.
- **Añadir un departamento nuevo no toca la lógica del router.** Basta con añadir una entrada al diccionario `DEPARTAMENTOS` y al `Literal` de `Decision`.
- **El umbral de confianza es un parámetro, no una regla fija.** `confianza_minima=0.7` es una decisión de negocio que puedes ajustar según cuánto te cueste equivocarte de departamento.
- **El router puede ir delante de cualquier otro patrón.** El mismo `clasificar()` que aquí decide un departamento podría decidir, en su lugar, qué pipeline ejecutar o qué especialistas activar en un planificador-ejecutor.

---

## Resumen

- El patrón de enrutado separa **clasificar** de **responder**: una llamada decide a qué especialista delegar, otra genera la respuesta final.
- La salida estructurada (aquí con un `Literal` para el departamento) evita depender de que el modelo escriba el nombre exacto del departamento en texto libre.
- Un **umbral de confianza mínima** decide cuándo es más seguro derivar a un humano que arriesgarse a una clasificación incorrecta.
- Añadir un departamento nuevo es solo una entrada más en un diccionario, sin tocar la lógica del router.

En el siguiente capítulo volvemos al patrón planificador-ejecutor, esta vez con concurrencia real: varios agentes especialistas trabajando en paralelo de verdad con `asyncio`.

---

## Autoevaluación

Antes de continuar, comprueba que los conceptos clave han quedado claros.
