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 sí 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.
| Flag | Para 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-llm | Sugerencias 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
| Nombre | Qué ejercita |
|---|---|
full | El 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-dni | Nunca se dicta documento: la llamada corre sin ningún contexto de paciente, que es lo que pasa hasta que lo dictan |
quick | Dos turnos, con el documento dictado de entrada, para ver el panel llenarse |
dni-manual | Dos 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 guion | Qué se ve |
|---|---|
| El reclamo de apertura | Nada, y está bien: el copiloto se calla porque no hay ni código ni modelo que buscar |
| Nombre / DNI / WhatsApp | Las confirmaciones del guion, sin RAG (skipped_by_adapter) |
error H08 | HIT 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:
| Momento | Qué pasa |
|---|---|
| ~0:25 | El panel queda en "Sin registro" con el documento equivocado |
| ~0:25 – 0:55 | Treinta segundos de aire: buscá 7289053 a mano |
| ~1:15 y ~1:25 | El paciente vuelve a dictar 7289058 |
| resto | Lo 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 panel | PATIENT_LOOKUP_MOCK | Guion |
|---|---|---|
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 DNIde la colección Bruno es el camino más corto.
Cómo está construido
?scripted=1en 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
injectTranscript→acceptTurn, 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.
injectTranscriptse 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.