Router de intención: la agenda tiene un solo dueño (2026-09-27)

Repo: [usuario]/clinica-chat, rama main. 78 asserts locales + 24/24 en vivo.

La idea

El chat no debe ser un tubo directo al modelo. Cada cosa tiene un dueño autoritativo, y el router decide ANTES de llamar al modelo:

intencióndueño¿LLM?
reservar / reagendar / cancelar (acción)widget de agendano
política (“¿puedo cancelar?”, “¿cuánto antes?“)bot (corpus)sí
info (precio, horario, dirección)bot (corpus)sí
fuera de alcancebot → negar + rutearsí

La respuesta de agenda es { answer, action: "open_booking", url }, generada antes del modelo.

Por qué (el doble beneficio)

  1. No gasta neurons. Un booking cuesta 0 neurons. Con el corpus de ~4.8k tokens de prompt y ~50 neurons por pregunta, cada reserva que se atiende con el widget es ahorro directo de presupuesto Free.
  2. Elimina por diseño la clase “el bot inventó una cita”. No es un prompt que pide no hacerlo (los prompts se rompen); es que el bot no tiene esa capacidad. La agenda solo la escribe el widget, que es el único con el calendario real.

Propiedad clave del diseño

El router solo puede ahorrar, nunca romper. Si un patrón no matchea, la pregunta cae al bot normal (comportamiento de antes). Un falso positivo sí rompería (mandaría a la agenda una pregunta de información), así que la precisión manda sobre el recall.

Por eso:

  • los patrones exigen ACCIÓN explícita (“quiero agendar”, “agéndame”, “cómo reservo”);
  • y POLICY_PATTERNS tiene precedencia y descarta las preguntas de política.

El caso que lo prueba: “¿Puedo cancelar mi cita si me surgió algo?” contiene puedo + cancelar, pero es una pregunta de política → NO va al widget, la contesta el bot con el corpus (“con al menos 4 horas de anticipación”). En cambio “necesito cancelar mi cita de mañana” sí es acción → widget.

Verificación

  • node test/controles.test.mjs → 78 ok / 0 fail (AI mockeado). Los asserts de agenda comprueban que aiCalls no aumenta: se resuelve sin modelo.
  • node test/llm.test.mjs → 24/24 contra el Worker desplegado:
    • q21 “Quiero agendar una cita”, q22 “¿Cómo reservo?”, q23 “Necesito cancelar mi cita de mañana” → action: open_booking + url.
    • las 21 restantes → action ausente (ningún falso positivo del router).

Dos fallos intermedios fueron del test, no del sistema (misma lección que el set LLM): exigir $600 en una pregunta que pedía “¿me pueden ver hoy?” (no pidió precio). Se separó en q06 (urgencia) y q24 (precio de la cita de urgencia).

Reutilizable (la regla general)

El LLM nunca es dueño de (a) cambios de estado ni (b) hechos que pueden ser exactos. Si el dueño del dato es un calendario o una fuente determinista, el bot no opina: enlaza. Aplicable a cualquier servicio: reservas, pagos, rastreo de pedidos, expedientes.

Pendiente

  • Widget de agenda real (hoy BOOKING_URL es un placeholder).
  • Página de FAQ generada del mismo corpus, para que informacion y bot no diverjan.