Spaces:
Running
Running
| # 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. | |