Arquitectura
Esta página no existe en ningún repo individual porque ninguno de los tres tiene el panorama completo — cada uno documenta su propio servicio, no cómo encaja con los otros dos. Este es ese mapa.
Los tres arms, quién los corre
Sección titulada «Los tres arms, quién los corre»El código define tres “harnesses” (STUDY_HARNESSES = ["plain", "rheumaai", "grader"], pokta-care-monorepo/apps/api/src/services/biobadamex-study-harness.ts). Un “arm” del estudio es en realidad (modelo × harness × prompt opcional) — no hay una lista fija de modelos hardcodeada, se configura en runtime vía la variable de entorno STUDY_ARMS.
| Harness | Cómo se invoca | Dónde vive el código |
|---|---|---|
plain |
Llamada LLM en proceso, dentro del monorepo — sin repo externo involucrado | pokta-care-monorepo (plainInvoker) |
rheumaai |
HTTP hacia RHEUMAI_STRUCTURED_URL |
servicio externo: rheuma-ai-bioagent, ruta POST /v1/study/rheumaai-reply |
grader |
HTTP hacia GRADER_STRUCTURED_URL |
servicio externo: pokta-grader-bioagent, ruta POST /v1/chat/completions |
El cuarto arm (LLM ajustado por otro reumatólogo) no es un harness separado. Es el harness plain corriendo con un promptId distinto (rheum-v1), registrado en código pero con el prompt vacío/reservado — si se invoca, el código lanza un error en vez de correr silenciosamente con contenido incorrecto. No está “casi listo”, está sin construir todavía.
Request flow
Sección titulada «Request flow»flowchart TD
M["pokta-care-monorepo<br/>(orquestador del estudio)"]
P["plain<br/>(llamada LLM en proceso)"]
R["rheumaai<br/>HTTP POST → rheuma-ai-bioagent<br/>/v1/study/rheumaai-reply"]
G["grader<br/>HTTP POST → pokta-grader-bioagent<br/>/v1/chat/completions"]
O[("biobadamex_study_arm_outputs<br/>una fila por nota × arm")]
S["scoring<br/>(biobadamex-eval.ts)"]
A[("biobadamex_study_analysis<br/>agregado, sin llave por nota")]
M --> P
M --> R
M --> G
P --> O
R --> O
G --> O
O --> S
S --> Arheumaai y grader nunca se llaman entre sí ni comparten proceso — cada uno es un servicio HTTP independiente, desplegado por separado (Railway), con su propia autenticación por API key. El monorepo es el único que sabe que existen los tres; ninguno de los servicios sabe del otro (con una excepción cosmética: el comentario en rheuma-ai-bioagent’s rheumaai-reply-route.ts reconoce explícitamente que su técnica de auth fue copiada de pokta-grader-bioagent’s structured.ts — copiada, no importada, los repos no tienen dependencia en runtime).
Sweep flow — de principio a fin
Sección titulada «Sweep flow — de principio a fin»Secuencia real de pokta-care-monorepo/apps/api/scripts/biobadamex-sweep-run.ts:
- Cargar el corpus — lista los
.docxenCORPUS_DIR(ruta de filesystem, nunca dentro de un repo git). Deriva unnote_id = sha256(filename)[:12]; el nombre real del archivo (que es PHI — nombre del paciente) nunca se loguea ni persiste. - Resolver los arms — lee
STUDY_ARMSdel entorno. Si está vacío, termina sin tocar nada. - Modo dry-run opcional — con
PLAN_ONLY=1, imprime el plan (notas × arms) y termina — cero llamadas a proveedores, cero escritura a DB. - Guard contra corridas previas sin drenar, luego crea un
study_run. - Por cada nota: la lee/parsea y arma un job por cada
(nota, arm)— se encolan enpg-boss. - Workers toman los jobs y llaman al invoker correcto por arm, con un semáforo de concurrencia por proveedor.
- Poll cada 5s hasta que todas las celdas estén “settled” (hay fila de salida, o se agotaron los reintentos), hasta 45 min.
- Reporte final: cuenta esperado/settled/output/fallas, agrupado por causa de falla.
Scoring — qué significa “completeness”
Sección titulada «Scoring — qué significa “completeness”»compareField (biobadamex-eval.ts) compara cada campo con un veredicto de cuatro estados, siempre usando un chequeo explícito de “desconocido” (nunca un chequeo falsy — así un 0 real nunca se confunde con “faltante”):
- match — ambos desconocidos, o los valores coinciden (para
patient.sexespecíficamente, vía un catálogo tipo HL7 AdministrativeGender, no comparación exacta de string). - mismatch — ambos conocidos, valores distintos.
- missing — el gold tiene valor, la extracción no.
- unexpected — la extracción tiene valor, el gold no.
“Completeness” es una tasa de presencia, no una tasa de acierto. Es presente / total sobre los campos requeridos que no son UNKNOWN — no factoriza si el valor es correcto contra el gold. La exactitud/acierto es una métrica separada (variant_accuracy, delta_grader_minus_plain). Esta distinción ya causó un bug real en el estudio (arm-2 estructurando la prosa del modelo en vez de la nota original inflaba completeness sin mejorar exactitud) — si vas a reportar un número, sé explícito sobre cuál de las dos métricas es.
Dónde caen los resultados
Sección titulada «Dónde caen los resultados»biobadamex_study_arm_outputs— una fila por(run_id, note_id, arm_id). Columnas relevantes:model_id,harness,record(el JSON extraído),das28,missing_required,latency_ms, tokens. Sin FK sobrenote_id— a propósito.biobadamex_study_analysis— solo agregado, formato largo, sin llave a nivel de nota (a propósito, para prevenir re-identificación). Columnas:run_id,arm_id,model_id,harness,scope,field_key,metric,numerator,n,value, intervalos de confianza. Aquí es donde filtras porscope="field",metric="field_present_rate"para ver el desglose campo por campo (ej. el camposex).
Corpus real de notas
Sección titulada «Corpus real de notas»Vive fuera de todo repo, en una ruta de filesystem pasada por CORPUS_DIR. El script de censo (biobadamex-instrument-census.ts) verifica explícitamente que esa ruta no esté dentro de un working tree de git — “Real patient notes must never be committable” es un comentario literal en el código, no solo una convención. Ver Reglas para las reglas de trabajo con datos sintéticos en su lugar.