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ón | dueño | ¿LLM? |
|---|---|---|
| reservar / reagendar / cancelar (acción) | widget de agenda | no |
| política (“¿puedo cancelar?”, “¿cuánto antes?“) | bot (corpus) | sí |
| info (precio, horario, dirección) | bot (corpus) | sí |
| fuera de alcance | bot → negar + rutear | sí |
La respuesta de agenda es { answer, action: "open_booking", url }, generada antes
del modelo.
Por qué (el doble beneficio)
- 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.
- 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 queaiCallsno 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 →
actionausente (ningún falso positivo del router).
- q21 “Quiero agendar una cita”, q22 “¿Cómo reservo?”, q23 “Necesito cancelar mi
cita de mañana” →
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_URLes un placeholder). - Página de FAQ generada del mismo corpus, para que informacion y bot no diverjan.