Spaces:
Sleeping
Sleeping
File size: 13,697 Bytes
ce79810 1e215c9 ce79810 1e215c9 ce79810 1e215c9 bf9f7d1 1e215c9 bf9f7d1 ce79810 bf9f7d1 1e215c9 ce79810 bf9f7d1 ce79810 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | # Migración de Morphos — estado y guía
Modernización del stack según `PLAN_MODERNIZACION.md`. Decisiones: IA híbrida
(medGemma privado por defecto + Claude opcional), backend **Python/FastAPI con uv**,
frontend **incremental Vite + TypeScript**, despliegue en **HF Spaces** con índice RAG
horneado en la imagen (sin almacenamiento persistente de pago).
## Estructura nueva
```
frontend/ Vite + TS. Motor portado + suite de regresión (Vitest).
src/analisis.ts Puerto fiel de js/analisis.js (tipado).
src/ia.ts Cliente tipado de /api/interpret (render estructurado).
tests/ 27 pruebas dorada s del motor.
backend/ FastAPI (uv). IA estructurada, RAG, seguridad.
app/schemas.py Salida clínica estructurada (Pydantic) → elimina limpiarRespuesta.
app/ai/ medgemma.py, claude.py, prompt.py, service.py.
app/rag/ retriever.py (degrada sin índice), ingest.py.
app/routers/ interpret.py, papers.py, auth.py.
app/security/ authz, rate_limit, session, headers.
tests/ 15 pruebas (esquema, prompt, RAG, API+seguridad).
evals/ Dataset dorado (split dev/test + firma veterinaria) + run_evals.py
(puerta CI) + juez LLM local gratuito + Ragas + promptfoo.
books/ Corpus con licencia (gitignored). Ver books/README.md.
instance/ BD de usuarios + índice RAG (fuera del webroot; gitignored).
bridge/ Puente local (proyecto uv aparte): lee analizadores (ASTM/HL7 v2) en la
LAN de la clínica y reenvía a /api/lab/ingesta. Ver INTEGRACION_ANALIZADORES.md.
```
## Integración de analizadores de laboratorio
Fases 0-1 implementadas (ingesta de resultados de equipos → autorrelleno del formulario por
ID de muestra). Backend: `app/schemas_lab.py`, `app/lab/` (mapeo + almacén TTL),
`app/routers/lab.py`, `app/security/device.py` (auth por API key). Frontend: `lab-import.ts`
+ `form-inject.ts`. Puente: `bridge/`. Mapeos código→analito en `data/lab_mapeos/`. Detalle
y fases pendientes en **INTEGRACION_ANALIZADORES.md**.
## Cómo ejecutar
```bash
# Frontend: pruebas del motor y build
make frontend-install
make frontend-test # 27/27
make frontend-build # → dist/
# Backend: sync (uv) y pruebas
make backend-sync
make backend-test # 15/15
make dev # uvicorn en :8000
# Evals (puerta de CI)
make evals # split dev, sólo casos con validación veterinaria
make evals-test # split reservado
make revision # hoja de revisión de los casos pendientes
make ragas ARGS="--predicciones preds.jsonl" # groundedness (juez local)
# RAG (cuando haya libros en books/)
make ingest # construye instance/rag_index con el grupo 'rag'
# Docker (multi-stage: build frontend + backend uv)
make docker-build
```
## Qué se ha implementado y verificado
- ✅ **Motor portado a TS** con 27 pruebas de regresión (parity con el JS original) y
typecheck limpio. Es la red de seguridad de la migración.
- ✅ **Backend FastAPI** con **salida estructurada validada** (Pydantic) que sustituye la
limpieza por regex; clientes **medGemma** (Ollama, plantilla de chat + `format` JSON
Schema, sin inyección de `<unused95>`) y **Claude** (tool use). 15 pruebas verdes.
- ✅ **Seguridad**: `/api/interpret` y `/api/papers` requieren sesión (cerrado el acceso
anónimo); CORS bloqueado; sesiones firmadas HttpOnly/SameSite/Secure; CSRF de doble
token; throttling de login; validación de imágenes; cabeceras de seguridad; BD e índice
RAG **fuera del webroot**. Verificado: 401 sin sesión, 403 sin CSRF, flujo completo OK.
- ✅ **RAG**: pipeline de ingesta + recuperador con citas que **degrada a modo sin-RAG**
si faltan deps o índice (probado). Índice horneado en la imagen.
- ✅ **Evals**: dataset dorado, comprobaciones deterministas (recall diferenciales,
cobertura, derivación, idioma, **seguridad tolerancia-cero**), promptfoo y **puerta de CI**
(exit≠0 ante regresión) — verificado que bloquea.
- ✅ **Juez LLM sin clave de API**: la rúbrica clínica y el juez de relevancia corren sobre
el **CLI de Claude Code** (`judge/claude_cli.py`, usa la sesión ya iniciada) o sobre
**Ollama** (`judge/ollama_local.py`, salida estructurada). El SDK con `ANTHROPIC_API_KEY`
queda como opción explícita. Antes el juez exigía esa clave y por eso nunca llegó a
cablearse en `run_evals.py`; ahora forma parte de la puerta.
- ✅ **Atribución verificable en las tres rutas** (`app/ai/citas.py`): las fuentes se
construyen desde los fragmentos realmente recuperados, la prosa del HF Space cita con
marcadores `[n]` y las citas que no se resuelven contra un fragmento real se descartan.
- ✅ **Disciplina del dataset**: `split` dev/test y `validado` por caso, aplicados por el
runner; circuito de firma veterinaria en `evals/revision.py`.
- ✅ **Ragas** (`evals/run_ragas.py`) sobre el índice real, con LLM y embeddings locales.
- ✅ **La derivación ya no la decide el modelo.** Si el motor determinista ve un hallazgo o
patrón `grave`, `requiere_derivacion` se fuerza a true pase lo que pase. Lo motivó una
medición: un 7B general marcó `false` en una ERC felina avanzada (creat 4.8, BUN 68,
isostenuria). Era un fallo de seguridad que dependía de qué modelo hubiera detrás; ahora es
imposible por construcción.
- ✅ **El alcance tampoco lo decide el modelo** (`app/ai/alcance.py`). Mismo patrón que la
derivación, misma causa: en la corrida del 2026-07-28, el caso `fuera-de-alcance-humano`
puntuó 0.00 en corrección y 0.00 en seguridad con **los tres** modelos evaluados —los únicos
ceros de toda la corrida—. Ninguno vio que el paciente era humano, ninguno declinó y los tres
fabricaron clínica (analitos nunca medidos, diagnósticos, hasta una biopsia renal) a partir
de una glucosa en rango. Ahora una guarda determinista inspecciona especie/raza/signos
**antes** de crear el cliente: si el paciente no es canino ni felino, se devuelve un rechazo
tipado (`fuera_de_alcance=true`, sin hallazgos ni diferenciales) sin gastar una llamada. La
guarda es deliberadamente estrecha —exige la especie declarada como tal— para no echar a un
caso legítimo que mencione otra especie de pasada (`test_alcance.py` fija ambos lados).
Medible: `acierto_fuera_de_alcance` es métrica de puerta con tolerancia cero.
- ✅ **`requiere_derivacion` dejó de ser una constante en la ruta de prosa.** El HF Space
devuelve texto, así que el cliente construía el objeto con el default del esquema (`true`)
y el campo no dependía del caso: en `normal-canino` contradecía a su propio texto y el juez
lo penalizó como incoherencia con riesgo de alarma injustificada (seguridad 0.50). Ahora lo
pone el motor determinista (`_derivacion_en_ruta_de_prosa`): `false` si no hay ningún
hallazgo ni patrón, `true` en cuanto haya algo, con el suelo de `_derivacion_obligatoria`
por encima. Con esto **la puerta pasa entera por primera vez** (juez Sonnet, split dev,
2026-07-31): seguridad 0.44→0.93, hedging 0.55→0.85, completitud 0.54→0.83, violaciones
del juez 2→0. Detalle en `evals/resultados/2026-07-31/impacto_guarda_y_prompt.md`.
- ✅ **El prompt ya no lleva líneas de relleno.** «Todos los valores dentro de rangos de
referencia» y «Ninguno detectado por el motor determinista» se leían como contenido: sobre
`normal-canino`, qwen2.5:14b emitió un hallazgo llamado literalmente «Todos los valores»
(alto · leve) sobre una glucosa en rango, y el A/B midió ese mismo caso subiendo de 0.30 a
0.85 sin el bloque. Los bloques vacíos se omiten y un panel normal pide confirmación de
normalidad en vez de diferenciales.
- ✅ **Campos estructurados exigidos donde el backend puede rellenarlos.** El esquema por
defecto admitía `{"interpretacion": "…"}` con diferenciales y hallazgos vacíos, y así pasaba
como buena una respuesta que dejaba al veterinario sin nada accionable (medido con
qwen2.5:7b). `esquema_estructurado()` se lo pide al modelo (`minItems`) y el servicio lo
comprueba **sólo cuando el caso lo admite**: un panel normal sí puede no tener hallazgos.
- ✅ **Truncamiento silencioso del HF Space, detectado y mitigado.** Lo destapó el juez CLI
sobre salidas reales: el Space cortaba a mitad de frase, perdiendo el diferencial clave, y
ninguna comprobación lo veía. Ahora se detecta (`interpretacion_truncada`), el reintento va
con menos literatura y el prompt de prosa lleva presupuesto de contexto
(`rag_max_chars_prompt`) y límite de palabras.
**Causa raíz (leída en el `app.py` del Space, 2026-07-27):** medGemma 1.5 razona antes de
responder, `extract_response` descarta ese razonamiento y `max_new_tokens=2048` es UN solo
presupuesto para ambos. El razonamiento se lleva ~1.100 tokens o más, así que la respuesta
visible se corta cuando la suma pasa del techo. Más literatura alarga el razonamiento, pero
no es el único factor: medido, con 1 solo fragmento también se truncaba. Las mitigaciones
del cliente reducen la probabilidad; no eliminan la causa.
- ✅ **Puente Node** que reusa `analisis.ts` como única fuente de verdad para generar los
hallazgos deterministas en las evals.
## Hecho en el último incremento
- ✅ **Puerto TS de todos los módulos UI** (`ui`, `pdf-parser`, `main`, `auth`, `papers`,
`tooltip`) + helper `dom.ts`. `auth.ts`/`papers.ts` usan los endpoints FastAPI.
- ✅ **`index.html` recableado** a `/frontend/src/main.ts` (Vite lo empaqueta). La app corre
end-to-end sobre el stack nuevo — verificado en navegador: motor, PDF, registro/login
real (sesión + CSRF) y el botón IA llamando a `/api/interpret`.
## Pendiente (siguiente incremento)
- **Retirar** `api/*.php` y `js/*.js` legacy: ya son código muerto (no se cargan). Borrado
seguro cuando se confirme que no se necesitan de referencia.
- ✅ **Ruta de IA funcionando en vivo**: la ruta `medgemma` usa por defecto el HF Space
(Gradio) donde está alojado el modelo — `app/ai/hf_space.py` porta el flujo de
`hf_proxy.php` (upload → analyze → SSE) y envuelve el texto en el esquema. Verificado en
navegador: interpretación real renderizada con el aviso de derivación. (Alternativas por
config: Ollama local si se vacía `MORPHOS_HF_SPACE_URL`, o Claude con API key.)
- **Config ESLint/Prettier** (falta el archivo de configuración; ya está la dependencia).
- **Retriever RAG**: añadir búsqueda híbrida BM25 + rerank.
- **Cerrar el A/B de multi-consulta con un juez LLM local.** La descomposición (una consulta
por patrón, fusión RRF) está implementada y testeada, pero apagada
(`MORPHOS_RAG_MULTICONSULTA=false`): con el único juez gratuito disponible —el heurístico de
palabras— empeora (precision@k 0.81→0.50), y ese juez está sesgado a favor de la consulta
concatenada. Basta `ollama pull` de un generativo para repetirlo bien; detalle y comandos en
`evals/resultados/2026-07-31/retrieval_multiconsulta.md`.
- ✅ **Probado y descartado: saltar el razonamiento en el Space.** El código ya existía
(`prefijar_respuesta`, interruptor `SALTAR_RAZONAMIENTO`), así que esta entrada estaba
obsoleta. Activado y revertido el 2026-07-31: empeora `juez_seguridad` 0.92→0.79 y mete una
violación de seguridad (recomendó insulina y fluidoterapia sin encuadre presencial en un
paciente con potasio 3,0). Detalle en `evals/resultados/2026-07-31/experimentos_robustez.md`.
El texto original de esta entrada se conserva abajo por su análisis de la causa raíz:
- **Arreglar el truncamiento en su origen: el Space (`blackmistcode/morphos_medGemma`).** La
cadena de razonamiento se genera y se tira, consumiendo la mitad o más de los 2048 tokens y
del tiempo de GPU. Prefijando `<unused95>` al turno del modelo —lo que hacía el proxy PHP
legacy— la generación arranca ya en modo respuesta: el presupuesto entero queda para la
interpretación, la latencia baja y con ella se puede recortar `duration`, que es lo que
consume cuota de ZeroGPU. Es el único cambio que elimina la causa en vez de esquivarla; hay
que medirlo con `make evals` antes y después, porque saltarse el razonamiento puede costar
calidad clínica.
```python
inputs = processor.apply_chat_template(..., return_tensors="pt")
prefijo = torch.full((1, 1), UNUSED95_ID, device=inputs["input_ids"].device)
inputs["input_ids"] = torch.cat([inputs["input_ids"], prefijo], dim=-1)
inputs["attention_mask"] = torch.cat([inputs["attention_mask"], torch.ones_like(prefijo)], dim=-1)
```
- **Revisar el límite de 200 palabras del prompt de prosa.** Evita el truncamiento, pero
medido con el juez CLI sobre el split reservado bajó `hedging` (0.75→0.68) y `seguridad`
(0.77→0.67) sin mejorar el resto. Es un parche mientras el Space siga gastando presupuesto
en razonamiento descartado; con la corrección de arriba debería poder retirarse.
- **Validación veterinaria de los 10 casos pendientes** del dataset (`make revision`): hasta
que se firmen, la puerta corre sobre 7 casos.
- **Aumentar el dataset** de evals con más casos validados por veterinario.
- **Calibrar `UMBRALES_JUEZ` por juez.** Sobre las mismas salidas simuladas, qwen2.5:7b dio
`hedging_apropiado` 1.0 y el juez CLI 0.4: el juez pequeño aprueba lo que el grande
suspende. Los umbrales actuales están puestos para el local; falta medir la desviación
sobre salidas reales y fijar un umbral por juez.
- Fijar `MORPHOS_SESSION_SECRET` y `MORPHOS_COOKIE_SECURE=true` en los secrets del Space.
|