Equipo Vortex · HackSpain 2026 · track Prosper AI («El Turno»)

Un agente de voz que coge el teléfono de una clínica

Los organizadores llaman a nuestro socket, interpretan a un paciente, y nosotros enviamos la acción que habríamos hecho: reservar, registrar, cambiar, cancelar, no hacer nada o escalar. Acierta o falla: no hay nota parcial. Esta página explica cómo está hecho, qué decidimos y qué no sabemos todavía.

36 / 40 puntos en el mejor Run All 1.º en el marcador 18 de 20 casos privados superados 737 tests
Hecho Inferencia Pendiente Cada afirmación lleva una de las tres marcas. «Hecho» significa que está verificado en el repositorio o en el panel de la plataforma. «Inferencia» es un juicio nuestro. «Pendiente» es algo que todavía no hemos probado.
Visión

La recepcionista que nunca deja el teléfono sonando

Una clínica pierde citas porque nadie coge el teléfono a la vez que atiende el mostrador. Vortex atiende esas llamadas: identifica a quien llama, lee su historia en el sistema de la clínica, decide qué cita cabe según las reglas, y deja constancia de lo que ha hecho.

No es un chatbot con voz. Es un sistema de decisión al que le hemos puesto voz: la parte difícil no es hablar bonito, es no equivocarse de paciente, de médico, de centro o de minuto, y saber decir «no» cuando la regla de la clínica lo exige.

Qué resuelve

  • Coge la llamada a la primera, en cualquier momento, y varias a la vez.
  • Reconoce al paciente por su nombre y un segundo dato, sin pedirle que repita lo que la clínica ya sabe.
  • Da la primera hora real que existe, no una aproximada: consulta la disponibilidad de verdad antes de ofrecer nada.
  • Se niega con un motivo concreto cuando la regla lo dice: edad, derivación, seguro, médico de baja, centro cerrado.
  • Escala a una persona cuando hay un síntoma de urgencia.
  • Deja una ficha de cada llamada que un humano puede auditar en diez segundos.

La regla que ordena todo el diseño

Nunca enviar nada es el peor resultado posible. Una llamada sin registro enviado es un caso intentado y fallado, y desde fuera un agente que ha petado y una negativa correcta se ven igual.

Por eso cada llamada acaba, sí o sí, con una acción enviada. Si no se puede reservar, se envía NO_ACTION con el motivo tipado que nombra la regla que ha mordido. Esa decisión aparece luego en casi todas las demás: en la escalera de reintentos, en el fallback al cerrar el socket, en las herramientas que devuelven tipos y no frases.

Está escrita como regla dura del equipo en CLAUDE.md, no es una intención.

Arquitectura de punta a punta

Un socket entra, una acción sale

No hay número de teléfono en nuestro lado. La plataforma abre un WebSocket y nos manda audio de teléfono en el formato de Twilio Media Streams. Cada conexión monta su propio agente completo y no comparte nada con las demás.

plataforma ──ws──> line/server.py ── CallSession (una por socket)
                        │                 ├─ ToolContext: call_id, ahora (Madrid), clínica, log, submitter
                        │                 └─ pipeline de voz: Soniox → LLM → TTS
                        ▼
              conversation/ prompt + turnos
                        ▼
              tools.py ── call_tool(nombre, ctx, args) ──> identity/ diary/ rules/
                                                            │
                                                     clinic/ (API de solo lectura)
                        ▼
              line/submit.py ── POST /api/v1/submit/<acción> ── dentro de los 30 s tras cerrar
                        ▼
              observability/ logs/calls.jsonl (un JSON por evento, con su call_id)
Las siete etapas y dónde vive cada una
EtapaQué haceDóndeEstado
Entrada Acepta el socket y espera el mensaje start de Twilio. line/server.py, line/twilio.py Hecho
Sesión Crea una CallSession con su contexto: reloj de Madrid, cliente de la clínica, log y cliente de envío. line/session.py Hecho
Oído Transcribe en tiempo real con identificación de idioma y vocabulario de clínica reforzado. Soniox stt-rt-v5 Hecho
Cabeza Un modelo de lenguaje con el prompt de la clínica y un juego cerrado de herramientas. No improvisa datos: los pide. conversation/prompt.py, models.py Hecho
Manos Las herramientas leen la API de la clínica y devuelven datos tipados o un rechazo tipado. Nunca prosa. tools.pyidentity/ diary/ rules/ Hecho
Voz Habla en español con Chirp 3 HD y en catalán, gallego y euskera por el proveedor alternativo. Un filtro revisa lo que va a decir antes de decirlo. line/pipecat_voice.py, line/privacy.py Hecho
Constancia POST de la acción a la plataforma dentro de la ventana de 30 s tras cerrar el socket, y una línea JSON por evento. line/submit.py, observability/ Hecho

Cinco carriles, un solo contrato

Somos cinco personas trabajando un fin de semana. Para no pisarnos, el código está dividido en carriles con una sola superficie compartida: vortex/contract.py, que congela la firma de cada herramienta, las seis acciones y los motivos de rechazo.

line/
El socket, el audio, el ciclo de la llamada y el envío.
conversation/
El prompt, los turnos, las interrupciones, el idioma.
identity/
Quién llama: directorio, segundo dato, letra del DNI/NIE, alta de nuevo paciente.
diary/
La agenda: disponibilidad, fechas relativas al minuto exacto, horarios, cambios y cancelaciones.
rules/
Lo que la clínica rechaza: edad, derivación, seguro, médico, triaje, centro más cercano.
clinic/
Cliente HTTP de solo lectura y datos de prueba para trabajar sin red.
observability/
El log de llamadas y la consola que ve el jurado.

Una herramienta se puede reescribir sin tocar el resto, porque su firma no cambia. Eso es lo que nos ha permitido arreglar fallos de puntuación en paralelo durante la noche.

Flujo de una llamada

Lo que pasa en los tres minutos que dura

Cada llamada está limitada a tres minutos. Este es el camino completo, desde que suena hasta que queda constancia. Los nombres en monoespaciado son los eventos reales que se escriben en el log, así que esta lista se puede seguir mirando la consola.

  1. 01
    Se abre el socket y nace la llamada
    Llega start con el identificador de la llamada y el número de origen. Se crea la sesión, se fija el reloj en Europe/Madrid en ese instante —nunca la hora de la máquina— y se escribe call.started.
  2. 02
    Antes de saludar, ya buscamos quién llama
    Con el número de origen se intenta resolver el paciente en el directorio, con un tope de 2 segundos. Si responde, el agente ya sabe con quién habla y no le hace repetir lo que la clínica tiene apuntado. Si tarda, se sigue sin ello y se anota identity.caller_line_timed_out.
  3. 03
    Saluda
    El saludo sale por el sintetizador en cuanto el transporte está conectado, sin esperar al modelo. Es la diferencia entre «ha cogido el teléfono» y «esto está roto».
  4. 04
    Escucha y decide cuándo le toca hablar
    La transcripción llega en vivo. El final de turno se detecta por el propio motor de voz o por detección de actividad más un modelo de turnos, según configuración. Hay empujones suaves si el paciente se queda callado, y una puerta de palabras para que un «ajá» no corte al agente en mitad de una frase.
  5. 05
    Identifica: nombre y un segundo dato
    El modelo llama a find_patient. Si hay dos personas con el mismo nombre, la herramienta no elige: devuelve que hace falta un segundo dato. La letra del DNI o del NIE se valida con su algoritmo, no se cree. Si no existe, la vía es registrar, no inventar.
  6. 06
    Comprueba las reglas antes de ofrecer nada
    Edad, derivación, seguro y póliza, médico correcto, centro correcto, síntoma de urgencia. Si una regla muerde, la herramienta devuelve un rechazo tipado con el motivo exacto. Ese motivo es el que se enviará: el modelo no lo redacta.
  7. 07
    Busca hueco de verdad
    find_slots pregunta a la disponibilidad real. «Lo antes posible» empieza el día siguiente a la llamada: nada se reserva el mismo día. La hora se resuelve al minuto con su zona horaria explícita, porque se compara exacta contra la de la plataforma.
  8. 08
    Prepara, confirma con el paciente, y solo entonces envía
    El agente lee en voz alta día, hora, médico y centro. Un «sí» confirma una sola cosa: la propuesta que está sobre la mesa en ese momento. Se envía con submit_action y se anota submit.sent / submit.result.
  9. 09
    Se despide y cuelga
    Cuando el envío se acepta, se arma el colgado: el agente termina la frase de despedida y cierra. No se queda la línea abierta gastando el reloj.
  10. 10
    Y si nada de esto salió bien, se envía de todas formas
    Al cerrar, si no hay ningún envío aceptado, entra una escalera de ocho ramas ordenadas por probabilidad de acertar el caso: lo preparado y confirmado, el último rechazo tipado, una reserva en frío a partir del historial del que llama, y como último recurso NO_ACTION(out_of_scope). Queda como submit.fallback. Todo dentro de la ventana de 30 segundos, con 5 de margen.
Componentes y proveedores

Cada pieza es una variable de entorno

Ningún proveedor está escrito en el código. Cambiar de modelo, de voz o de transcriptor es editar el .env y reiniciar. Eso nos ha permitido cambiar de modelo tres veces en una noche sin abrir un pull request.

Lo que corre hoy en la línea que marca la plataforma
PiezaQué usamosVariableEstado
Transporte WebSocket propio en formato Twilio Media Streams, µ-law 8 kHz, tramas de 20 ms VORTEX_WS_PATH En producción
Transcripción Soniox stt-rt-v5, con identificación de idioma y términos de clínica reforzados SONIOX_API_KEY En producción
Modelo Cualquier modelo compatible con la API de OpenAI. Hoy, por preajuste helmcode LLM_PROVIDER, LLM_MODEL En producción
Modelo de reserva Un segundo modelo al que se reintenta si el primero se cuelga LLM_ALT_MODEL Hecho
Voz, español Google Chirp 3 HD VORTEX_TTS_PROVIDER En producción
Voz, ca / gl / eu Proveedor alternativo, enrutado por idioma detectado VORTEX_TTS_PROVIDER_ALT Hecho
Voz a voz Gemini Live, un único modelo que oye y habla VORTEX_VOICE_MODE=gemini-live Solo demo
Clínica API de solo lectura de la plataforma; sin clave, datos de prueba locales PLATFORM_API_KEY En producción
Despliegue Contenedor en el servidor del equipo, publicado por Traefik con HTTPS; se levanta solo tras un reinicio deploy/compose.yaml En producción
Trazas Langfuse, con lista blanca de campos LANGFUSE_* Hecho

El sistema arranca aunque falten todas las claves

Si falta la clave de la plataforma, se usa la clínica de prueba y un cliente de envío que escribe en el log en vez de hacer el POST. Si faltan las claves de voz, corre un pipeline de relleno que cuenta tramas y envía una negativa tipada al final. GET /health dice en qué modo está.

Suena a detalle, pero es lo que permite que cinco personas ejecuten toda la suite y una llamada completa en su portátil, sin red y sin gastar un céntimo, mientras la clave de verdad está en un solo sitio.

Decisiones y lo que cuestan

Nueve decisiones, cada una con su precio

Toda decisión de arquitectura renuncia a algo. Estas son las nuestras y lo que hemos sacrificado en cada una. Ninguna es gratis y ninguna es irreversible.

DecisiónPor quéQué sacrificamos
Cascada (oír → pensar → hablar) en vez de voz a voz Los modelos de voz a voz están por debajo del 52 % en tareas agénticas con herramientas. En una reserva el error no es un matiz de tono: es el paciente equivocado. LiveKit, Coval y Deepgram recomiendan la cascada para este caso. Latencia y naturalidad. La cascada suma el retardo de tres servicios. Gemini Live queda como demostración para el jurado y nunca puntúa.
Soniox como transcriptor Es la única opción barata con español y catalán nativos, identificación de idioma, detección semántica de fin de turno y entrada µ-law de 8 kHz directa. Alternativas con mejor detección de turno, pero sin catalán. Cambiar tiraría a la basura todo el ajuste de vocabulario de clínica.
Las herramientas devuelven tipos o un rechazo tipado. Nunca prosa El motivo que devuelve la herramienta es literalmente el motivo que enviamos. Si la herramienta devolviera una frase, el modelo tendría que interpretarla para encontrar el motivo, y ahí se pierde el punto. Flexibilidad. Un caso nuevo exige un motivo nuevo en el contrato, no una frase distinta.
Los identificadores salen siempre de la API Se comparan carácter a carácter. Un identificador adivinado falla el caso incluso cuando la hora es la correcta. Una llamada de red más en cada paso, y un agente que no puede «recordar» lo que no ha consultado.
Una sesión completa por socket, sin nada compartido Un Run All abre diez sockets a la vez y el reto 2 abre veinte. Compartir una conversación, un contador o una llamada en vuelo es exactamente el fallo que el reto busca. Memoria: cada llamada paga su propio pipeline. El contenedor está limitado a 4 GB y eso hay que vigilarlo.
Escalera de fallback al cerrar Nunca enviar nada es el peor resultado. Antes de rendirse, la escalera prueba lo preparado, el último rechazo tipado y una reserva en frío a partir del historial. Riesgo de enviar algo peor que el silencio. Lo aceptamos: el silencio nunca es más barato que una respuesta equivocada, porque puntúan igual.
Proveedores por variable de entorno Cambiar de modelo o de voz es editar el .env y reiniciar, no un cambio de código con revisión. Más superficie de configuración de la que nadie ha verificado entera. Dos preajustes de modelo siguen marcados como no verificados en el README.
Sin juez basado en modelo en el camino de cada PR Las comprobaciones deterministas primero. Un juez de modelo mete varianza justo donde queremos una señal estable. No medimos el tono ni la calidad conversacional automáticamente. Eso queda en juicio humano.
pass^k en lugar de pass@1 Un escenario que pasa el 70 % de las veces es un escenario que falla en una tanda de 68 llamadas. Solo cuenta como aprobado si pasa las k veces. Cuesta k veces más tiempo y dinero por medida.
Seguridad y privacidad

Datos de pacientes, y una clave que es de todo el equipo

Manejamos nombres, documentos de identidad, teléfonos e historial clínico. Y una sola clave de API que si se filtra hay que rotar en el mostrador para los cinco.

La clave y los secretos

  • La clave vive solo en el entorno. .env, las credenciales de Google y cualquier fichero de cuenta de servicio están ignorados por git.
  • GET /health y el evento de arranque de cada llamada publican booleanos (has_platform_key), nunca el valor de una clave.
  • Sin clave de plataforma, el cliente de envío pasa a modo seco: escribe en el log en vez de hacer el POST. No hay forma de mandar algo real sin querer.
  • La contraseña de la consola de equipo se compara con una función de tiempo constante, no con un igual.
  • La exploración de la API de la clínica guarda las respuestas con los campos de paciente sustituidos por [redacted], para que un cambio de nombre de campo se vea sin dejar datos en el disco.

Lo que el agente no puede decir ni exportar

  • Un filtro antes del sintetizador revisa cada frase que va a salir por el altavoz. Si intenta leer un documento de identidad o un teléfono, se bloquea y queda como voice.privacy_block. Uno de los retos puntuados revisa precisamente el transcrito en busca de datos filtrados.
  • La entrada y la salida de cada herramienta pasan por una función de redacción antes de entrar en el log.
  • Las trazas externas se exportan con lista blanca: lo que no está en la lista sale como [redacted]. No se envían ni los prompts ni las respuestas en bruto.
  • En las páginas públicas —el muro del jurado y la ficha de una llamada— los teléfonos van enmascarados.
  • Honestidad El log operativo logs/calls.jsonl sí guarda el texto de los turnos. Es la herramienta de depuración del equipo, está ignorada por git y no sale de la máquina.
Resiliencia

Qué pasa cuando algo se rompe a las tres de la mañana

Dependemos de tres servicios externos y de una red. Todos fallan alguna vez. La pregunta de diseño no es «cómo evitamos el fallo» sino «qué envía la llamada cuando falla».

Si falla…Qué hace el sistemaEstado
El modelo tarda o se cuelga sin soltar el primer token Hay un plazo para el primer token. Al vencer, reintenta; si se configura un modelo alternativo, reintenta con él; si todo falla, el agente dice una frase de espera en el idioma del paciente en vez de quedarse mudo. Queda como llm.timeout y llm.retry. Hecho
Una herramienta tarda El agente dice «un momento» en cuanto arranca la llamada a herramienta, para que el silencio no se lea como línea muerta. Hecho
La búsqueda por número de origen no responde Tope de 2 segundos y se sigue sin ella. No bloquea el saludo. Hecho
El POST de la acción falla Tiempo límite de 8 segundos y cada código tratado por separado: aceptado, duplicado, ventana cerrada, llamada desconocida, campos inválidos. Un duplicado se trata como éxito, no como error. Hecho
La llamada termina sin haber enviado nada La escalera de ocho ramas, con una reserva en frío antes de rendirse. Dentro de los 30 segundos, con 5 de margen. Hecho
El pipeline lanza una excepción Se anota call.crashed y el cierre sigue ejecutándose: un agente que ha petado también envía su acción. Hecho
El servidor se cae o se reinicia la máquina Corre como contenedor con reinicio automático, detrás de Traefik con certificado propio. Ya no depende del portátil de nadie ni de un túnel. En producción
Se pierde la conexión a mitad de llamada El cierre intenta enviar igualmente. Aun así, es nuestro fallo abierto más caro: los dos casos que perdimos en el mejor run traen la señal connection_lost. Sin resolver
Observabilidad y evals

Cómo sabemos si funciona sin esperar a la plataforma

La plataforma puntúa un Run All cada media hora aproximadamente y nunca dice qué campo hemos fallado. Medir allí es lento y ciego. Así que medimos aquí.

Una línea por evento

Cada llamada escribe un JSON por evento, todos etiquetados con su identificador de llamada: inicio, cada turno del paciente y del agente, cada herramienta llamada y lo que devolvió, el envío y su resultado, el consumo y el fin.

Sobre ese fichero se construye todo lo demás: la consola, el muro del jurado, la ficha de una llamada y las métricas. No hay una segunda fuente de verdad.

La consola, hecha para que un humano audite

  • Muro público: transcrito, traza de herramientas y el veredicto, en tres columnas, legible a tres metros.
  • Ficha de una llamada, compartible por enlace.
  • «Por qué no se reservó» es una columna de primera clase, no una nota al pie: el motivo tipado y su explicación en una frase.
  • Coste por llamada calculado del consumo real de transcripción, modelo y voz. Se muestran dos números: precio de tarifa y lo que realmente pagamos.
  • Todo estado es un punto de color y una palabra. El color solo nunca lleva significado.
Cinco capas de evaluación, cada una respondiendo a una pregunta distinta
CapaQué pruebaClínicaQué vale
1 · Lógica Una herramienta, o un flujo de herramientas, aislada. 132 casos. De prueba Forma, no verdad. En cada PR.
2 · Conversación Un paciente guionizado, turno a turno, en texto. 57 escenarios con correcciones, interrupciones, silencios y terceras personas. De prueba Prueba que el prompt conduce las herramientas. Ciego al audio.
3 · Voz Proveedores reales sobre las mismas frases, limpias y con ruido, tras un ciclo telefónico de 8 kHz. Latencia percibida, tasa de error por idioma, coste. Cuesta dinero de verdad. Fuera del camino de PR, a propósito.
4 · Corpus Un juez que replica el de la plataforma, sobre una copia real de la clínica. Real La única capa con verdad de campo. Las 73 respuestas oficiales pasan por él y las 130 reservas publicadas se reproducen contra la clínica real.
5 · Banco de modelos Los modelos candidatos jugando los mismos escenarios, con el prompt y las herramientas reales, y el enrutado que corre hoy al lado. De prueba Convierte «qué modelo usamos» en un número en vez de una costumbre.

La respuesta honesta a «¿vuestros evals predicen la nota?»

Hoy, no. Nuestras capas comprueban condiciones necesarias; ninguna comprueba la suficiente. Las capas 1, 2 y 5 corren contra una clínica de prueba con pacientes inventados, así que un escenario en verde dice que el prompt sabe conducir las herramientas, no que el agente reservaría el paciente correcto en el hueco que el caso acepta.

Lo que sí está sólido es el juez de la capa 4: si dice que un registro pasa, la plataforma dice que pasa. Y la copia de la clínica es real. Lo que falta es el puente: pasar los 73 casos oficiales por el agente y juzgar el resultado. Está diseñado y documentado; no está construido.

Esto está escrito así, con estas palabras, en docs/NEXT-STEPS.md desde antes de que nadie nos lo preguntara. Lo contamos porque es la diferencia entre un equipo que mide y un equipo que cree.

737 tests 18 modelos en el banco CI bloqueante: tests, lint, formato Evals en CI: informativo, comenta en el PR Arranque sin red y sin claves:
Estado del reto 1

36 de 40, primeros, y tres veces seguidas

El marcador guarda el mejor Run All de cada equipo, no el último ni la suma. Estos son los números tal como los devuelve el panel de la plataforma, leídos a las 11:58 del sábado.

36 / 40 puntos · 1.º

18 de los 20 casos privados superados en el mejor run. Y no fue una casualidad: tres runs distintos llegaron a la misma marca, a las 06:25, a las 08:06 y a las 11:30 de la mañana. Repetir un resultado tres veces con semillas privadas diferentes es la mejor prueba que tenemos de que el sistema, cuando funciona, funciona por diseño.

36
de 40 puntos
1.º
en el marcador
18/20
casos del mejor run
29
runs lanzados
347
casos juzgados
Mejor run9562ef24
Runs que alcanzaron 36/403 de 18 privados
Tasa global de acierto171 de 347 casos · 49,3 %
Fallos atribuidos al arnés4
Los 2 casos perdidosconnection_lost + record_mismatch
Endpoint activowss://line.167.233.80.47.sslip.io/ws
Muro de puntuacióndomingo 06:00 Europe/Madrid

Qué nos costó los 4 puntos que faltan

Los dos casos perdidos en el mejor run valían 2 puntos cada uno y traen las mismas dos señales: connection_lost y record_mismatch. La plataforma los atribuye como no concluyentes, lo que significa que no puede decidir si el fallo fue nuestro o suyo.

Inferencia Nuestra lectura es que la conexión se cortó antes de que el registro correcto llegara a salir, y el fallback envió una acción que no coincidía. Es la misma familia de fallo en los tres runs de 36 puntos, así que es un solo defecto, no cuatro problemas distintos.

El número honesto: 49,3 %

Sobre los 347 casos juzgados en todos los runs, hemos pasado 171. El mejor run vale 36; la media está muy por debajo. Esa distancia es varianza, y es nuestro problema abierto número uno.

De dónde viene: un bucle de mejora automática corrió durante la noche optimizando contra cuatro casos privados por problema, sin ninguna barrera local que detuviera una regresión. Nos subió a lo más alto y luego nos bajó. La lección está escrita: el Run All es el último paso de la comprobación, nunca el primero.

Sobre «no habrá más runs»

Decisión, no dato Que no lancemos más Run All es una decisión del equipo en el momento de escribir esto, no un límite de la plataforma. Los datos que sí son hechos: el marcador conserva el mejor run, abrir retos nuevos nunca baja una puntuación anterior, y solo cuentan los runs terminados antes del muro del domingo a las 06:00.

Dicho de otra forma: 36/40 está guardado y no se puede perder. Lanzar otro run solo puede subirlo o no cambiarlo. Si la decisión es no lanzar más, es por dónde queremos gastar las horas que quedan, no porque el marcador esté cerrado.

Preparación del reto 2

La centralita: no puntúa, y es el cero de todo lo demás

El reto 2 es el reto 1 repetido cinco, diez o veinte veces al mismo tiempo. No da puntos y el Run All no lo marca nunca: hay que dispararlo a mano. Pero un socket que mezcla estado entre llamadas no falla un caso, falla el run entero.

Lo que ya está probado

  • Diez llamadas simultáneas sobre sockets reales, en un test automatizado: cada llamada con su identificador propio, su flujo de audio propio, al menos diez tramas recibidas y cada envío atribuido a su llamada y no a otra.
  • Todo el estado es por socket por construcción: la sesión, el contexto de herramientas, el contador de consumo y la política de confirmación. No hay una sola variable a nivel de módulo en el camino de la llamada, y hay un test que lo comprueba con dos instancias a la vez.
  • El marcador de llamadas falsas dispara N llamadas en paralelo con una sola orden, así que reproducir una tanda es inmediato.
  • El contenedor está limitado a 4 GB en una máquina de 22 GB, para que una tanda no se lleve el servidor por delante.

Lo que falta antes de decir que está listo

  • Pendiente La prueba automatizada se queda en diez. Las tandas de 5 y de 20 están documentadas como expectativa, no registradas como resultado.
  • Pendiente No hay una medición guardada de memoria y latencia con veinte líneas abiertas.
  • Inferencia El servidor corre en un solo proceso y la concurrencia es asíncrona, no por procesos. Veinte llamadas deberían caber de sobra porque el trabajo es esperar a la red, no calcular. Deberían no es lo hicieron.
  • La acción concreta: disparar 5, luego 10, luego 20, y guardar el resultado junto con el consumo de memoria del contenedor. Media hora de trabajo.
El jurado

Qué juzgan y qué les enseñamos

El domingo hay una segunda puntuación que no es automática: el mismo panel llama a todos los equipos. La columna de la izquierda son los criterios tal como están publicados, sin añadir ni interpretar. La de la derecha es lo nuestro: qué le ponemos delante para cada uno.

Hay una frase en las bases que ordena toda la preparación: un criterio que el jurado no puede observar no puntúa. Así que nada de contar lo que hay; hay que poder mostrarlo.

Criterio publicadoQué le ponemos delante
Experiencia del paciente Que llamen ellos. Saludo inmediato sin esperar al modelo, «un momento» mientras consulta, y colgado limpio al confirmar. Si se quedan callados, el agente les empuja suave en vez de dejar la línea muerta.
Lo personal que resulta (usar la ficha y el historial antes de preguntar) La búsqueda por número de origen ocurre antes del saludo. Si el número está en el directorio, el agente ya sabe quién llama y no le hace repetir su nombre ni su documento. Es el detalle que más se nota por teléfono.
La plataforma alrededor del agente (consola en vivo, «¿por qué dijo eso?») El muro en el proyector mientras hablan: transcrito, traza de herramientas y veredicto. Y la respuesta a «por qué dijo eso» es una columna, no una explicación: el motivo tipado con su frase. La ficha de la llamada se comparte por enlace.
Seguridad y límites Que intenten sacarle un dato de otro paciente. El filtro anterior al sintetizador bloquea documentos y teléfonos en la voz, y queda registrado el bloqueo. Que le pidan algo fuera de alcance: responde NO_ACTION(out_of_scope), no improvisa.
Manejo de idiomas Que llamen en catalán, gallego o euskera. La identificación de idioma va en la transcripción y la voz se enruta al proveedor que sabe hablarlo. Es la razón por la que elegimos el transcriptor que elegimos, y se puede contar en una frase.
Rigor de ingeniería (arnés de evaluación, varianza, fallos con nombre, coste por llamada) Cinco capas de evals con un juez verificado contra el de la plataforma; 18 modelos comparados con número y página publicada; el coste por llamada calculado del consumo real; y la varianza contada como lo que es: 36/40 en el mejor run, 49,3 % de media, y el motivo identificado.
Discreción Teléfonos enmascarados en todo lo público, exportación de trazas con lista blanca, claves que no salen ni en el /health, y un log operativo que no sale de la máquina.

El argumentario en tres frases

  • Está en producción, no en un portátil. El endpoint es un contenedor en un servidor, con HTTPS y reinicio automático. Se puede llamar ahora mismo.
  • Medimos antes de creer. Hay un juez verificado contra el de la plataforma, 737 tests y 18 modelos comparados con números. Y sabemos decir dónde nuestros evals todavía no predicen la nota.
  • Nunca se queda en silencio. Aunque el modelo se cuelgue, el socket se caiga o el proceso lance una excepción, la llamada acaba con una acción enviada y un motivo con nombre.
Preguntas difíciles

Las que preferiríamos que no nos hicieran

Con la respuesta que daríamos. Sin adornos: si algo no lo sabemos, la respuesta dice que no lo sabemos.

¿36 de 40 fue suerte?
No, y tenemos la prueba: tres runs distintos llegaron a 36/40, con semillas privadas diferentes y a horas distintas. Lo que sí es cierto es que la media está en el 49,3 % de casos acertados. Los dos números juntos dicen la verdad completa: el techo es real y la varianza también.
¿Por qué no usáis un modelo de voz a voz, que suena mucho mejor?
Porque aquí no se puntúa cómo suena, se puntúa si el registro coincide. Los modelos de voz a voz están por debajo del 52 % en tareas agénticas con herramientas, y menos del 15 % de los agentes en producción los usaban en la primera mitad de 2026. Tenemos Gemini Live montado y lo enseñamos encantados, pero está marcado en el código como solo demo y el modo automático no lo elige nunca.
Si el modelo se cuelga en mitad de la llamada, ¿qué oye el paciente?
Una frase de espera en su idioma. Hay un plazo para el primer token del modelo; al vencer se reintenta, si hay modelo alternativo configurado se reintenta con él, y si todo falla el agente habla en vez de callarse. Un silencio largo se juzga como fallo nuestro, así que el silencio es el enemigo, no el retraso.
¿Y si se cae la conexión justo antes de enviar?
El cierre de la sesión intenta enviar igualmente, dentro de la ventana de 30 segundos. Pero aquí no vamos a vender humo: es exactamente el fallo que nos cuesta los 4 puntos que faltan. Los dos casos perdidos del mejor run traen la señal de conexión perdida. La escalera de fallback envía algo, y ese algo no coincidió.
¿Cómo sé que no mezcláis dos llamadas simultáneas?
Hay un test que abre diez sockets reales a la vez y comprueba que cada llamada tiene su identificador, su flujo de audio y sus envíos atribuidos a ella. Todo el estado del camino de llamada es por socket por construcción, sin variables de módulo. Y sé decir el límite: la prueba automatizada se queda en diez, no en veinte.
¿Vuestros evals predicen la puntuación de la plataforma?
Todavía no, y lo tenemos escrito antes de que nos lo preguntaran. El juez está verificado contra el de la plataforma y la copia de la clínica es real, pero las capas que corren en cada PR usan una clínica de prueba con pacientes inventados. Falta el puente: pasar los 73 casos oficiales por el agente y juzgar el resultado. Está diseñado; no está construido.
Si teníais un bucle que se mejoraba solo, ¿por qué no lo dejasteis corriendo?
Porque funcionó y luego nos hundió. Optimizaba contra cuatro casos privados por problema sin ninguna barrera local, así que una regresión no tenía dónde ser detectada antes de desplegarse. Nos puso primeros y después nos bajó. La conclusión es de método, no de herramienta: el Run All es la última comprobación, nunca la señal que guía.
¿Por qué el agente atiende en inglés por defecto?
Porque de los 73 casos públicos, 69 están en inglés. La clínica del ejercicio opera en inglés y quien llama en otra lengua es la excepción. El agente sigue al paciente al idioma en que le hablen —español, catalán, gallego, euskera— y ese salto es justamente lo que aísla el reto de idiomas.
¿Qué impide que el modelo se invente un identificador de paciente o de cita?
Que no los ve nunca de otra fuente. El identificador de paciente sale del directorio, el de la cita sale de las citas del paciente, y el de tipo de cita sale de la disponibilidad. Se comparan carácter a carácter: uno adivinado falla el caso aunque la hora sea correcta. Está como regla dura del equipo y como forma del contrato de herramientas.
¿Puede el agente decir en voz alta el DNI de un paciente?
No debería, y hay un filtro entre el modelo y el sintetizador que lo bloquea y lo registra. Uno de los retos puntuados revisa el transcrito buscando exactamente eso. El filtro se puede probar en vivo: pídeselo y mira el bloqueo aparecer en la consola.
¿Cuánto cuesta una llamada?
Se calcula del consumo real de cada llamada: segundos de audio transcritos, tokens de entrada y salida del modelo, caracteres sintetizados. Se muestran dos números: el precio de tarifa, que es el que sirve para comparar proveedores, y lo que realmente pagamos, que en el modelo actual es cero porque entra en la bolsa del hackathon. No mezclamos los dos.
¿Qué pasa si el paciente no existe en el directorio?
Se registra, no se reserva. Es un reto propio y tiene una trampa: en esos casos una reserva al lado del registro hace fallar el caso. La letra del documento se valida con su algoritmo antes de dar el alta, así que un dígito mal dictado se detecta en la llamada y no después.
¿Qué es lo siguiente que arreglaríais si tuvierais dos horas más?
El puente de evaluación: pasar los 73 casos oficiales por el agente contra la copia real de la clínica y juzgarlos con el juez verificado. Da un número offline en un minuto y dice qué campo se ha perdido, que es justo lo que el Run All no dice. Sin eso, cada mejora es una apuesta.
Lo que falta

La lista honesta

Todo lo que sabemos que no está hecho o no está verificado, en un solo sitio. Si el jurado encuentra algo que no está en esta lista, hemos fallado en esta sección.