Skip to main content

Llamada simulada (pnpm fake-call)

Una llamada entera del copiloto, desde un guion, sin teléfono, sin grabación y sin Deepgram.

El backend corre el camino real —lookup del paciente en Oxitesa, extracción de datos, RAG, sugerencias, el socket al dashboard— y lo único que se saca del medio es la transcripción, porque no hay audio que transcribir. Los turnos entran como JSON por el mismo /audio-fork.

Eso es el punto: la llamada es determinista y gratis. Una grabación te da lo que el STT entendió ese día, que es la variable equivocada contra la cual pelear mientras debuggeás el copiloto.

:::note Cuándo usar la otra herramienta

aura-tap-client/tools/replay_wav.py replayea WAVs reales por el mismo socket y pasa por Deepgram. Es la que sirve cuando lo que querés probar es la transcripción misma. Para todo lo demás, el guion es más rápido y no te miente.

:::

Habilitarlo

# Aura-reloaded/.env
AUDIO_FORK_SCRIPTED=true

Apagado por defecto, y rechazado siempre que NODE_ENV=production — el && en security.config.ts es el control, no la variable de entorno. Un deploy de producción que la setee recibe un error en el boot avisando que se ignoró, en vez de que la flag parezca funcionar.

Con la flag prendida, el boot dice:

[Security] AUDIO_FORK_SCRIPTED=true — /audio-fork accepts SCRIPTED transcript turns as JSON.

Si el backend rechaza la conexión, el script te lo dice y no sigue mandando turnos a un socket que nadie lee.

Correrlo

cd Aura-reloaded
pnpm fake-call # el guion completo del call center
pnpm fake-call -- --list # qué guiones hay
pnpm fake-call -- --script no-dni # otro escenario

Atendé la llamada en el dashboard mientras corre. Es lo que hace el operador, y las sugerencias y el panel de Oxitesa sólo van a quien la tiene tomada. El panel arranca en "Falta el DNI" y se llena recién cuando el guion lo dicta — no hay consulta al entrar la llamada. --wait (8s por defecto) es la ventaja que te da para hacer clic en Atender.

FlagPara qué
--script <nombre>Cuál de los guiones. Default full
--caller-id <número>El teléfono que se muestra en el dashboard. No identifica al paciente
--pace <n>Multiplica las pausas. 0 replayea todo de golpe, 2 va al doble de lento
--wait <s>Cuánto espera antes del primer turno, para que llegues a atender
--hold <s>Cuánto mantiene el socket abierto después del último turno
--call-id <id>Fijar el id, para volver a mirar la misma llamada en /logs
--fake-llmSugerencias enlatadas: sin LLM y sin RAG. Ver abajo

Al terminar cierra con código 1000, que es lo que el backend lee como corte real: se escribe la fila, corre la extracción final de datos y se abre la revisión post-llamada sobre el transcript crudo, con la pregunta de si querés mejorarlo con IA. La corrección ya no es automática.

Los guiones

NombreQué ejercita
fullEl guion completo: saludo, motivo, nombre, DNI dictado y confirmado, WhatsApp, y después la pregunta técnica. Toca el extractor, forDni, y la unión del filtro: el paciente está registrado con un 7F-5 y pregunta por un M50, así que los dos manuales tienen que quedar adentro
no-dniNunca se dicta documento: la llamada corre sin ningún contexto de paciente, que es lo que pasa hasta que lo dictan
quickDos turnos, con el documento dictado de entrada, para ver el panel llenarse
dni-manualDos minutos para probar la búsqueda manual de DNI. Corre sin LLM, ver abajo

Viven en scripts/fake-call.ts. Agregar uno es sumar un objeto al SCENARIOS.

:::tip Cómo se dicta el DNI en un guion

Dígito por dígito en palabras, como lo lee un paciente ("siete dos ocho nueve cero cinco tres"), y el extractor lo resuelve — medido en una corrida real. En número pelado también funciona. Lo que importa es el intercambio de repetir para confirmar, porque es para eso que está afinado el prompt del extractor.

Lo que un guion no puede reproducir es el artefacto de un número entero escuchado como una sola palabra ("siete millones doscientos…"). Ese lo produce Deepgram, que es exactamente lo que este camino saca del medio, así que un guion que lo imitara no estaría probando nada.

:::

--fake-llm: probar la pantalla sin gastar LLM

pnpm fake-call -- --fake-llm

Cambia el motor de conversación por FakeConversationEngine y el extractor de datos del paciente por FakeCallDataService: respuestas enlatadas, sin una sola llamada al LLM y sin consulta al vector store. La consulta de pacientes no se toca — esa sigue siendo real. Sirve para una sola cosa — mirar que el dashboard pinte bien durante la transcripción — y no dice absolutamente nada sobre la calidad de las respuestas. Una corrida linda acá significa que el cableado y la pantalla andan, nunca que el RAG anda.

Para qué conviene usarlo:

  • iterar sobre la UI del copiloto sin quemar tokens en cada reload;
  • probar sin tener el corpus ingestado ni la API del LLM configurada;
  • tener tiempos y resultados repetibles — el LLM real cambia de respuesta cada corrida.

Reproduce a propósito los cuatro estados que son fáciles de romper en pantalla:

Turno del guionQué se ve
El reclamo de aperturaNada, y está bien: el copiloto se calla porque no hay ni código ni modelo que buscar
Nombre / DNI / WhatsAppLas confirmaciones del guion, sin RAG (skipped_by_adapter)
error H08HIT en 0.66, con el chip del manual del M50
¿cada cuánto limpio el filtro?MISS con el score de cerca (0.486): el hueco de corpus real

El streaming también se emula: espera ~1,4s antes del primer chunk (lo que mide el motor real) y después va palabra por palabra, así que se ve el mismo aparece-y-se-completa que en vivo.

Un turno que ninguna regla reconoce igual contesta algo genérico, a propósito: una sugerencia vacía es indistinguible de un dashboard roto, y eso es justo lo que esta herramienta no puede parecer.

:::warning Sólo en llamadas scripteadas El backend acepta fake_llm=1 únicamente junto con scripted=1, que a su vez está apagado salvo con AUDIO_FORK_SCRIPTED=true en un build que no sea de producción. No hay forma de que un deploy conteste con respuestas enlatadas por accidente. :::

dni-manual: probar la búsqueda manual de DNI

pnpm fake-call -- --script dni-manual

Enciende --fake-llm solo (offline: true en el escenario): no hay una sola llamada al LLM en todo el guion. La consulta de pacientes sí es real — es justamente lo que se está probando.

El paciente dicta 7289058, que está mal: lo dice de memoria y erra el último dígito. Entonces la consulta automática cae en "Sin registro" y la única forma de llegar a los equipos es que el operador tipee el documento correcto (7289053) en el campo del panel.

Los huecos largos no son relleno, son el lugar donde se hace la prueba:

MomentoQué pasa
~0:25El panel queda en "Sin registro" con el documento equivocado
~0:25 – 0:55Treinta segundos de aire: buscá 7289053 a mano
~1:15 y ~1:25El paciente vuelve a dictar 7289058
restoLo que hay que mirar: el panel no vuelve al documento equivocado

Esa última fila es la afirmación real del escenario. Un documento cargado a mano queda fijo: la extracción sigue leyendo el equivocado del audio y no lo pisa, ni dispara otra consulta. Si el panel vuelve a 7289058, el pin se rompió.

El WhatsApp llega recién al final a propósito: mientras falte un dato, el extractor corre en cada turno, y eso es lo que mantiene vivo el re-reporte del documento que tiene que ser ignorado.

:::note El extractor offline no es el de producción Con el modo offline la extracción la hace FakeCallDataService, que atribuye cada turno por lo que el operador preguntó justo antes y rearma los números dictados dígito por dígito. Un número compuesto ("once cincuenta y uno") lo deja en null en vez de adivinarlo. El de producción es un LLM precisamente por eso: Deepgram escribe un número dictado como una sola palabra y ningún regex sobrevive a eso. :::

Forzar los estados del panel de Oxitesa

El guion decide qué se dice; quién es el paciente lo decide el roster. Sin Oxitesa levantado:

PATIENT_LOOKUP_PROVIDER=mock

El roster por defecto (DEFAULT_ROSTER en mock-patient-lookup.ts) ya tiene al paciente 7289053 con un 7F-5, que es el que dictan los guiones full y quick.

Estado del panelPATIENT_LOOKUP_MOCKGuion
dni, con equipos(el roster por defecto)full o quick
waiting (falta el DNI)(cualquiera)no-dni
sin equipos activos[{"dni":"7289053","equipment":[]}]full
not_found[]full

Con el provider en mock el panel muestra la línea ámbar "Datos de prueba", a propósito: un panel mockeado no se tiene que poder confundir nunca con datos reales. Y poner el provider en mock además apaga el warning de arranque por PATIENT_LOOKUP_URL vacío.

Qué no prueba

  • La transcripción. Es justamente lo que saca. Para eso, replay_wav.py.
  • Que el DNI que guarda Oxitesa matchee con el que extraemos, porque eso depende de sus datos. Para eso hace falta una consulta contra la base real — el request oxitesa-lookup / by DNI de la colección Bruno es el camino más corto.

Cómo está construido

  • ?scripted=1 en la conexión. El gateway rechaza la conexión si la flag está apagada, en vez de degradarla a un tap de audio normal: un script mandando JSON a una sesión que lo ignora se ve exactamente igual que un backend roto.
  • La sesión arranca en la conexión y no en el primer frame, para que exista algo que atender antes de que alguien del guion hable.
  • Un turno scripteado entra por injectTranscriptacceptTurn, que es el mismo método por el que entra un final de Deepgram. Un harness con su propio camino se testea a sí mismo.
  • Una llamada scripteada no abre Deepgram al ser atendida, que es lo que la hace gratis.
  • injectTranscript se niega a inyectar en una sesión que no sea scripteada, sin importar cómo arrancó. Defensa en profundidad: un bug en la puerta del gateway no puede convertirse en frames de texto cayendo en la llamada de un paciente real.