Spaces:
Running
Morphos — Modernization, AI/RAG, Evals & Security Plan
Estado de implementación (auditoría 2026-07-26)
Leyenda: ✅ hecho y verificado · 🟡 parcial (núcleo hecho, falta un subpunto) · ⬜ pendiente
Fase Estado Resumen 1 — Frontend (Vite/TS) ✅ Todos los módulos portados a TS (analisis, ui, pdf-parser, auth, papers, main, tooltip, ia); index.htmlrecableado al bundle TS; 32 tests ✅. Menor: falta archivo de config ESLint/Prettier.2 — Capa IA (FastAPI) ✅ Endpoints, salida estructurada, clientes medGemma/Claude, prompt endurecido, validación imágenes. Orquestación con ClienteModelopropio (no la librería Pydantic AI).3 — RAG ✅ Ingesta + retriever con citas + degradación ✅. Nuevo: búsqueda híbrida (denso + BM25/FTS fusionados con RRF) + rerank cross-encoder bge-reranker-v2-m3, ambos conmutables; traducción ES→EN de consulta. Corpus real ingerido: 6763 fragmentos de 2 libros eninstance/rag_index/(LanceDB, 69 MB, bge-m3).4 — Evals 🟡 Dataset dorado (17 casos), juez clínico, promptfoo, puerta CI ✅ (bloqueo verificado), arnés de evaluación de recuperación aislada ( run_retrieval_eval.py). Faltan: código Ragas (0 líneas), human-in-the-loop, split reservado, informe por-ruta/por-corpus.5 — Seguridad ✅ Guarda auth, rate limit, CORS, CSRF, BD/RAG fuera del webroot, cabeceras, validación imágenes — verificado. Menor: rate limit por-usuario y redirección HTTP→HTTPS. Verificado ejecutando (2026-07-26): 32 tests frontend (Vitest), 97 tests backend (pytest, incluye retriever híbrido + integración con índice real + ingesta/troceo + traducción de consulta), ruff limpio, typecheck limpio. Previamente verificado en el navegador contra el backend FastAPI: motor de patrones (anemia microcítica), import de PDF (parser TS portado), registro/login real (sesión + CSRF), y el botón "Análisis IA" llamando a
POST /api/interpretcon token CSRF. Puerta de evals bloquea (exit≠0), 401 sin sesión, 403 sin CSRF. No verificado en vivo: salida estructurada renderizada con un modelo real (faltaollama pullde medGemma), promptfoo ejecutado, rechazo CORS de orígenes externos, 429 bajo carga,/security-review.Fuera de este plan: la integración de analizadores de laboratorio (ASTM/HL7) ha avanzado en paralelo —
backend/app/routers/lab.py,bridge/,data/lab_mapeos/,tests/test_lab_*.py,frontend/tests/lab-import.test.ts. VerINTEGRACION_ANALIZADORES.md.Legacy pendiente de retirar: los 8
js/*.jsy los 5api/*.phpsiguen en el repo pero YA NO se cargan (index.htmlno referencia ninguno). Son código muerto a eliminar.Lo que queda, en orden sugerido
- Borrar el legacy
js/+api/— riesgo cero, ya es código muerto.- Ragas + split reservado, y ejecutar la suite completa de evals contra el índice real — el mayor valor pendiente, y ya desbloqueado por el corpus ingerido.
- Config ESLint/Prettier; rate limit por-usuario en
/api/interpret; informe de evals por-ruta-de-modelo y por-versión-de-corpus.- Human-in-the-loop (mayor alcance, requiere decisiones de producto).
Ver detalle marcado por tarea abajo y
MIGRACION.mdpara el siguiente incremento.
Context
Morphos is a veterinary diagnostic-support SPA (canine/feline) that detects clinical patterns from lab values in real time and optionally asks an AI model (medGemma 1.5 4B via a HuggingFace Gradio Space, or local Ollama) for a clinical interpretation. It ships as vanilla JS (no build step) + thin PHP proxies, deployed as Docker on HF Spaces.
The app works, but three things block it from being a trustworthy diagnostic assistant:
- The AI layer fights the model instead of controlling it.
js/ia.jsbuilds the prompt by string concatenation, injects a raw<unused95>control token, and post- processes output throughlimpiarRespuesta()— ~70 lines of regex hacks stripping control tokens, English-only reasoning, LaTeX, and looped paragraphs. These are symptoms of an uncontrolled inference path (raw Gradio Space, no chat template, no structured output, no grounding). - There are zero evals. For a tool that suggests differentials on real patients, there is no measurement of accuracy, groundedness, or safety. This is the highest-risk gap.
- Security holes: the AI and papers proxies are unauthenticated with open CORS
(anyone can call
api/hf_proxy.phpand burn the HF key), there is no rate limiting anywhere, the SQLite DB with password hashes may be web-reachable, and MySQL defaults toroot/empty-password.
The vet-validated pattern engine (js/analisis.js, data/*.json) is the crown jewel and
must be preserved exactly through the migration.
Decisions (confirmed with the user)
- AI model strategy: Hybrid. Keep self-hosted medGemma/Ollama as the private default; add an optional hosted frontier-model (Claude) route for higher accuracy. The eval judge always uses a strong model.
- Backend: Python FastAPI for the AI/RAG/eval service (best ecosystem).
- Frontend: Incremental. Add Vite + TypeScript, port modules (especially
analisis.js) to typed TS with unit tests, keep the existing DOM UI. No React rewrite. - Hosting: stay on HF Spaces (PRO). Because the RAG corpus is static licensed books, build the vector index offline and bake it read-only into the Docker image — no paid persistent storage needed. App Space (CPU-basic, FastAPI + static assets) + the existing ZeroGPU medGemma Space + optional Claude route.
HF hosting check: Free Spaces have an ephemeral filesystem; persistent storage is a paid add-on ($5/mo for 20 GB up). PRO ($9/mo) gives 10 ZeroGPU/Docker Spaces + 1 TB repo storage. Since the RAG corpus is static, the vector index is baked into the image and needs no persistent-storage purchase — the plan fits inside the PRO subscription.
Target Architecture
┌────────────────────────────────────────────────────────────────┐
│ App Space (HF, CPU-basic, Docker) │
│ │
│ Frontend (Vite build → static) Backend (FastAPI) │
│ ├─ analisis.ts (ported engine, tested) ├─ /api/auth (session)│
│ ├─ ui.ts / pdf-parser.ts / papers.ts ├─ /api/interpret (AI) │
│ └─ ia.ts (calls /api/interpret) ├─ /api/papers (PubMed)│
│ ├─ RAG retriever │
│ │ └─ baked vector idx │
│ └─ rate limit + authz │
│ │
│ Persistence: SQLite (users) outside webroot │
└───────────────┬──────────────────────────────┬──────────────────┘
│ hybrid route │
┌──────────▼─────────┐ ┌──────────▼──────────┐
│ medGemma ZeroGPU │ │ Claude API (opt.) │
│ Space (private) │ │ (higher accuracy) │
└────────────────────┘ └─────────────────────┘
Monorepo layout after migration:
morphos/
├── frontend/ # Vite + TS; ported js/ modules, unit tests (Vitest)
├── backend/ # FastAPI: auth, interpret, papers, rag, security
│ ├── app/
│ │ ├── main.py # FastAPI app, middleware (CORS, rate limit)
│ │ ├── routers/ # auth.py, interpret.py, papers.py
│ │ ├── ai/ # model clients (medgemma, claude), prompt, schema
│ │ ├── rag/ # ingestion, chunking, embeddings, retriever
│ │ └── security/ # authz deps, rate limiter, session
│ └── tests/
├── evals/ # golden dataset, promptfoo + Ragas configs, CI gate
├── data/ # valores_referencia.json, alteraciones.json (unchanged)
│ └── rag_index/ # baked vector index (built in CI, shipped read-only)
├── Dockerfile # multi-stage: build frontend + index, run FastAPI+static
└── docker-entrypoint.sh
Phase 1 — Frontend modernization (incremental, low risk) — ✅ (salvo config ESLint)
Goal: add tooling and types without changing behavior. The engine is vet-validated; migration must be behavior-preserving and proven so by tests.
- 🟡 Introduce
frontend/with Vite + TypeScript + Vitest. Addpackage.json,tsconfig.json, ESLint/Prettier. Keep Spanish naming perCLAUDE.md; no alignment spaces; no!importantin CSS. — Vite + TS + Vitest ✅ (frontend/package.json,tsconfig.json,vite.config.ts). Falta: no hay archivo de config ESLint/Prettier (sólo la dependencia y el scriptlint). - ✅ Port
js/analisis.js→frontend/src/analisis.tsfirst. It is pure/stateless (single exportanalizarResultados), so it types cleanly and is fully unit-testable. Write a golden test suite that pins current behavior (severity classification, age/breed/sex adjustments, all ~60 pattern rules) using fixtures derived fromdata/*.json. This suite doubles as the regression net for the whole migration. —analisis.ts+tipos.ts+ 27 tests Vitest (32 en total conlab-import.test.ts), typecheck limpio. Verificado. - ✅ Port remaining modules (
main.js,ui.js,ia.js,pdf-parser.js,auth.js,papers.js,tooltip.js) to.ts. Preserve the getter-callback wiring inmain.jsand theimagenesDataUrl/capturasMicroscopioshared arrays inui.js. — Todos portados (frontend/src/*.ts) + helperdom.ts. Typecheck limpio; verificados en el navegador (motor, PDF, auth).auth.ts/papers.tsapuntan a los endpoints nuevos. - ✅ Keep
index.htmlmarkup andcss/styles.cssas-is initially; wire the Vite-built bundle in place of the?v=Nmodule tag. Remove the leftoverconsole.logdebug blocks inia.js. —index.htmlcarga/frontend/src/main.ts(Vite lo empaqueta). Elia.jslegacy ya no se carga (código muerto); el nuevoia.tsno tieneconsole.log. - ✅ Update stale
CLAUDE.mdfacts (analisis.js is 817 lines / 90 analytes / 78 clinical entities; auth + papers layers exist).
Files: new frontend/**; modify index.html script tag; delete debug logging in ia.js.
Phase 2 — AI layer rebuilt on FastAPI (replaces the PHP proxy + regex cleanup) — ✅
Goal: a controlled, model-agnostic inference path with structured, validated output,
so limpiarRespuesta() becomes unnecessary.
- ✅ New FastAPI service (
backend/). Replaceapi/hf_proxy.phpwithPOST /api/interpretandapi/papers_proxy.phpwithGET /api/papers. Keep the PubMed disk-cache logic (portpapers_proxy.phpbehavior). Keepapi/auth.phpsemantics but reimplement in FastAPI (Phase 5 hardens it). — Endpoints creados; caché de PubMed portada; auth reimplementada. Los.phplegacy aún no se han borrado (siguen enapi/) — retirada pendiente hasta migrar la UI. - 🟡 Orchestration: Pydantic AI (model-agnostic, typed, structured outputs). Define a
ModelClientinterface with two implementations:MedGemmaClient(self-hosted default; talk to the ZeroGPU Space / Ollama using the proper chat template, not raw<unused95>injection) andClaudeClient(hybrid opt-in via the Anthropic SDK). Backend selection replaces the currentlocalStoragemx-ia-backendtoggle and is enforced server-side. — Desviación: se implementó un protocoloClienteModelopropio (app/ai/base.py) conMedGemmaClient+ClaudeClienty selección server-side, en vez de la librería Pydantic AI. Cumple el objetivo (salida tipada/validada) con menos dependencias. - ✅ Structured output schema (Pydantic) — the core fix. Instead of free text, the model
returns:
interpretacion(es),hallazgos_clave[](analyte, direccion, gravedad),diferenciales[](nombre, probabilidad, evidencia[], citas[] → RAG sources),siguientes_pruebas[],confianza,requiere_derivacion(safety flag),idioma="es". Enforce with structured/JSON output; validate server-side; on validation failure, retry or return a typed error — never ship unparsed model text. This deletes the entirelimpiarRespuestaregex pile. —app/schemas.py::InterpretacionClinica; reintento + error tipado enservice.py. - ✅ Prompt construction moves server-side into
backend/app/ai/prompt.py, assembled from patient data + flagged findings + detected patterns (reuse the enrichment already inconstruirPrompt) + retrieved RAG context (Phase 3). System prompt hardened: Spanish- only, scope limits, mandatory hedging/deferral language, citation requirement, injection resistance for free-textsignos-clinicosand citology images. - ✅ Vision path preserved: medGemma multimodal for citology images; keep the 1024px client resize and ≤4 images limit, but validate size/count/mime server-side (Phase 5).
- ✅ Frontend
ia.tsbecomes a thin typed client of/api/interpretthat renders the structured result (findings, ranked differentials with citations, next steps, a visible "requiere derivación veterinaria" banner when flagged). — Código completo (frontend/src/ia.ts), cableado vía el bundle TS y ejercitado en el navegador. Falta ver el render con salida de un modelo real (ollama pullpendiente).
Files: new backend/app/ai/**, backend/app/routers/interpret.py, .../papers.py;
rewrite frontend/src/ia.ts; remove api/hf_proxy.php, api/papers_proxy.php.
Phase 3 — RAG layer (structure now; books ingested later) — ✅
Goal: ground interpretations in the licensed veterinary literature with verifiable citations, built so the corpus can be dropped in when provided.
- ✅ Stack: LlamaIndex for ingestion/retrieval; LanceDB (embedded, file-based → bakes
cleanly into the image, zero runtime services) as the vector store. Embeddings via a
self-hosted multilingual model (e.g. BGE-M3 through
sentence-transformers) to keep the privacy positioning — no document content leaves the box at index time. — Dependencias en el gruporagdepyproject.toml; ingesta usa pypdf + sentence-transformers + lancedb directamente (llama-index-core disponible). - ✅ Ingestion pipeline (
backend/app/rag/ingest.py, run offline/CI, not at request time): PDF/EPUB → text extraction → structure-aware chunking (section/heading, ~512–1024 tokens, overlap) → metadata per chunk (libro,edicion,capitulo,pagina,especie?) → embed → write LanceDB table underdata/rag_index/. A manifest records corpus version + hash for eval reproducibility. — Escribe eninstance/rag_index/(nodata/, por seguridad — ver Fase 5). Manifiesto ✅. - ✅ Retriever (
backend/app/rag/retriever.py): hybrid search (dense + BM25/keyword), metadata filtering by species, top-k with a rerank step; returns chunks with source metadata so the interpret schema'scitas[]point tolibro/edicion/página. Retrieval is triggered by detected patterns + flagged analytes (reuseultimoAnalisis), so it targets the patient's actual findings. — Denso + BM25/FTS fusionados con RRF + rerank con cross-encoderbge-reranker-v2-m3, filtro por especie y citas ✅. Conmutables porrag_hibrido/rag_rerank(config.py), con degradación en cascada: rerank → RRF → sólo denso. Añadidotraduccion_consulta.py(consulta ES → corpus EN). Cubierto portest_retriever_hibrido.py+test_retriever_integracion.py. - ✅ Copyright safety: retrieval returns short grounding snippets to the model only; the API never exposes full book text to the client, and the endpoint is auth-gated.
- ✅ Ship path: index built in CI and copied into the Docker image read-only. No HF
persistent storage required. Rebuild only when the corpus changes.
— Dockerfile copia
instance/de sólo lectura. - ✅ Placeholder now: commit the full pipeline + an empty/sample index and a
books/README.mddescribing the expected input layout, so ingesting the real books later is a singlemake ingeststep. The interpret path must degrade gracefully to no-RAG when the index is empty. — Superado: ya no es un placeholder. Corpus real ingerido conmake ingest— 6763 fragmentos de 2 libros (Fundamentals of Veterinary Clinical Pathology 3e; Veterinary Hematology, Clinical Chemistry, and Cytology 3e) eninstance/rag_index/(LanceDB 69 MB, bge-m3, chunks 450 tok / solape 64, troceo estructural markdown, índice FTS, manifiesto conhash_corpus). Degradación a sin-RAG sigue probada. El índice NO se comitea (queda fuera del repo; se construye en CI y se copia a la imagen).
Files: new backend/app/rag/** (ingest.py, retriever.py, traduccion_consulta.py),
instance/rag_index/ (artefacto construido, gitignored — no data/, por seguridad),
books/README.md, Makefile (ingest, retrieval-eval).
Phase 4 — Rigorous evals (the core requirement for a diagnostic tool) — 🟡
Goal: measure accuracy, groundedness, and safety continuously, and gate deploys on it.
- 🟡 Golden dataset (
evals/dataset/): curate expert-validated cases. The user already has real lab results + citologies from practicing vets (perUSO_DE_IA.md) — turn these into labeled cases: patient signalment + lab values → expected key findings, expected differentials (acceptable set), red-flag/safety expectations, and out-of-scope cases the model must decline. Version the dataset; keep a held-out split. —casos.jsonlampliado de 7 a 17 casos (IMHA, hipertiroidismo, HAC, pancreatitis, trombocitopenia, hipercalcemia, enteropatía perdedora, gammapatía, hepatocelular agudo, leucocitosis inflamatoria… + el caso fuera-de-alcance) + README con esquema ✅. Falta: split reservado (ningún caso lleva camposplit) y casos reales del veterinario. - Frameworks (layered):
- ✅ promptfoo — config-driven prompt/response regression in CI: deterministic assertions
(language = es, valid schema, required derivation flag present when expected, no
forbidden claims), plus LLM-rubric graders.
—
evals/promptfooconfig.yamlpresente (no ejecutado aquí, requiere modelo). - ⬜ Ragas — RAG-specific metrics: faithfulness/groundedness (no claims beyond retrieved
context), context precision/recall, answer relevance, citation correctness.
— Dependencia en el grupo
evals(pyproject.toml) y mencionada en los READMEs, pero cero líneas de código Ragas en el repo. Ya está desbloqueado: el índice está poblado. Es el hueco más grande que queda del plan. - ✅ Evaluación de recuperación aislada (no estaba en el plan original, añadida para
decidir por datos):
evals/run_retrieval_eval.py+dump_retrieval.pymiden si los fragmentos recuperados son relevantes al diagnóstico esperado, permitiendo A/B de modelo de embeddings × idioma de consulta (bge-m3 ES vs EN vs MedCPT) antes de invertir en rerank. Juzga con Claude si hayANTHROPIC_API_KEY, si no con heurístico de solape. Expuesto comomake retrieval-eval. - ✅ Custom clinical rubric (
evals/judge/) — LLM-as-judge (strong model, e.g. Claude) scoring: differential correctness vs. expected set, appropriate hedging, safety (no dangerous/overconfident recommendations), completeness. —evals/judge/clinical_judge.py(se activa con ANTHROPIC_API_KEY).
- ✅ promptfoo — config-driven prompt/response regression in CI: deterministic assertions
(language = es, valid schema, required derivation flag present when expected, no
forbidden claims), plus LLM-rubric graders.
—
- 🟡 Metrics & thresholds: per-metric pass bars (e.g. groundedness ≥ X, differential
recall ≥ Y, safety violations = 0). Track per model route (medGemma vs Claude) and per
corpus version so RAG changes are attributable.
— Umbrales + agregación en
run_evals.py✅. Falta: informe explícito por-ruta/por-versión-de-corpus. - ✅ CI gate: GitHub Actions runs the eval suite on PRs touching
backend/app/ai,backend/app/rag, prompts, or the index; block merge/deploy on regression or any safety failure. Store run artifacts/scorecards. —.github/workflows/evals.yml; bloqueo verificado (exit=1 ante violación de seguridad). - ⬜ Human-in-the-loop: a lightweight review workflow for a veterinarian to accept/correct model outputs, feeding new cases back into the golden set. Log (privacy-safe) production interpretations for periodic expert audit. — No implementado.
Files: new evals/**, .github/workflows/evals.yml.
Phase 5 — Security audit & rate limiting — ✅
Findings from the current code, severity-ranked, each with the fix folded into the new FastAPI backend.
Critical
- ✅ Unauthenticated AI/papers proxies + open CORS (
api/hf_proxy.php,papers_proxy.phpsendAccess-Control-Allow-Origin: *, no session check). Anyone can call them directly and burn the HF key / abuse PubMed. → Require an authenticated session on/api/interpret(and rate-limit papers); lock CORS to the app origin. —Depends(usuario_actual)en interpret (401 verificado en producción). Corrección (2026-07-26): papers NO lleva guarda de auth —/api/papers?query=…responde 200 sin sesión; sólo está limitado a 30/min por IP. Coincide con lo que pide el punto («auth en interpret, rate-limit en papers»), pero la anotación anterior decía «interpret/papers» y era falsa. No hay clave de API en juego (eutils se consulta sin credencial), así que el riesgo se limita a abuso del proxy. CORS a orígenes fijos: verificado en local — un origen no listado no recibeAccess-Control-Allow-Origin. Ojo en producción: el proxy de HF Spaces añade por su cuentaAccess-Control-Allow-Origin: <origen>yexpose-headers: *por encima de la app, así que el bloqueo de CORS queda neutralizado en la plataforma. Lo que sostiene la defensa ahí esSameSite=Stricten las cookies de sesión y CSRF: una petición cross-site no las lleva. - ✅ Password-hash DB may be web-served. SQLite fallback writes
data/morphos.dbunder the web root;.htaccessonly denies.env/setup.php, so/data/morphos.dbmay be downloadable, and.htaccessis Apache-only. → Move the DB outside the served root; in FastAPI nothing under the DB path is a static route. Never rely on.htaccessfor secret protection. — BD eninstance/(fuera del webroot); sólodist/y/data(JSON) se montan.
High
3. 🟡 No rate limiting anywhere. → Add slowapi (or Redis-backed) limits: per-IP + per-
user on /api/interpret (expensive), per-IP on /api/auth login (brute force) and
/api/papers. Return 429 with retry-after.
— slowapi por-IP en interpret/login/papers + manejador 429 con Retry-After ✅.
Falta: límite adicional por-usuario (actualmente sólo por-IP).
4. ✅ Login brute force — no throttling/lockout. → Per-account + per-IP attempt throttling
with backoff; keep the already-generic "email o contraseña incorrectos" message.
— Tabla intentos_login + throttle por email+IP; mensaje genérico.
5. ✅ MySQL root / empty password default (conexion.php). → Require DB credentials from
env; no hardcoded defaults; fail closed if unset.
— El backend nuevo no tiene credenciales por defecto (config por entorno); SQLite por defecto.
Medium
6. ✅ Secrets via .env in webroot guarded only by .htaccess. → Load secrets from real
env vars (HF Spaces secrets); keep .env out of any served directory.
— pydantic-settings; backend/.env gitignored; nada bajo el root servido.
7. ✅ No CSRF protection on session POSTs. → SameSite=Strict + Secure + HttpOnly
session cookies, plus a CSRF token (or move to short-lived bearer tokens).
— Cookie firmada HttpOnly/SameSite/Secure + CSRF de doble token (403 verificado).
8. ✅ Server-side upload validation missing for citology images (base64 decoded with no
size/count/mime cap → memory DoS). → Enforce ≤4 images, max bytes, allowed mime, safe
decode server-side.
— interpret.py::_validar_imagenes (nº, mime, tamaño, decode seguro).
9. ✅ Prompt-injection surface via free-text signos-clinicos and citology images. →
Harden system prompt, treat user text as data, validate structured output, cap output
scope.
— System prompt endurecido (trata el texto como datos) + salida estructurada validada.
Low
10. ✅ Remove console.log prompt/PII leakage in ia.js.
— El nuevo ia.ts (ya en uso) no registra el prompt. El ia.js legacy es código muerto
(no se carga) pendiente de borrar junto al resto de js/*.js.
11. 🟡 Raise password min length (6 → ≥8) + optional breach check; registration keeps 409 but
is now rate-limited (acceptable enumeration risk).
— Mínimo 8 ✅ (RegistroBody). El chequeo de brechas (opcional) no se implementó.
12. 🟡 Add security headers (CSP, HSTS, X-Content-Type-Options, Referrer-Policy) and enforce
HTTPS.
— Cabeceras ✅ (headers.py; HSTS en prod). Falta: redirección explícita HTTP→HTTPS
(se delega en la plataforma). Nota: CSP ajustada a worker-src blob: para PDF.js.
New surface introduced by RAG: ✅ licensed book text must never be exfiltratable —
retrieval returns only short grounding snippets to the model, the API never serves full
book content, and the corpus lives read-only in the image. — Índice en instance/, endpoint auth-gated.
Files: backend/app/security/**, backend/app/routers/auth.py, DB path change in the
connection layer, Dockerfile/entrypoint (secrets, DB location, headers).
Verification
- ✅ Engine parity (Phase 1): the
analisis.tsgolden test suite (Vitest) passes with outputs identical to the current JS engine across fixture cases spanning both species and all pattern rules. This is the gate that the migration preserved validated behavior. — 27/27 verde (32/32 con los tests de import de laboratorio). Reejecutado 2026-07-26. - 🟡 AI path (Phase 2): integration test that
/api/interpretreturns schema-valid structured output for representative cases on both the medGemma and Claude routes; confirmlimpiarRespuestais gone and no unparsed text can reach the client. Drive the real UI to confirm findings, ranked differentials with citations, and the derivation banner render. — UI real verificada: el botón "Análisis IA" llama aPOST /api/interpretcon CSRF y el backend alcanza el modelo (Ollama). Salida estructurada validada por test de integración;limpiarRespuestano existe en el código nuevo. Falta:ollama pulldel modelo para ver el render de diferenciales/citas con datos reales, y la ruta Claude ejercitada en vivo. - ✅ RAG (Phase 3): with a small sample corpus, verify retrieved citations resolve to the
correct
libro/páginaand that an empty index degrades gracefully to no-RAG. — Degradación a sin-RAG ✅. Resolución de citas ✅:test_retriever_integracion.pyconstruye un índice LanceDB real (con FTS) y comprueba recuperación end-to-end y filtro por especie; el corpus real de 2 libros está ingerido. 97 tests backend verde (2026-07-26). - 🟡 Evals (Phase 4):
promptfoo eval+ Ragas + the clinical rubric run green locally and in CI; deliberately regress a prompt and confirm the CI gate blocks it; confirm a safety- violation case fails the suite. — Puerta CI: bloqueo por violación de seguridad verificado (exit=1). Falta: ejecutar promptfoo (requiere modelo) y Ragas (sin implementar). - 🟡 Security (Phase 5): confirm unauthenticated
/api/interpretreturns 401; CORS rejects foreign origins; rate limits return 429 under load; the DB path is not reachable as a static route; MySQL refuses to start without credentials. Run/security-reviewon the diff. — 401 sin sesión ✅, 403 sin CSRF ✅, BD fuera del webroot ✅. Falta verificar en vivo: rechazo CORS de orígenes externos, 429 bajo carga, y ejecutar/security-review.
Sequencing & risk notes
- Ship in order: Phase 1 → 5 → 2 → 3 → 4. Rationale: lock behavior with tests first (1), close the exploitable holes early (5), then rebuild the AI path (2), add grounding (3), and stand up evals (4) — though the golden dataset from Phase 1 tests and the eval dataset should be gathered in parallel from the start.
- Biggest risk is regressing the vet-validated engine; the Phase 1 golden suite mitigates it and must exist before any TS port lands.
The real licensed books are pending— resuelto (2026-07-26): 2 libros ingeridos (6763 fragmentos). Los PDF con licencia viven enbooks/sin comitear y el índice eninstance/(gitignored); ambos se reconstruyen conmake ingest. Queda pendiente medir con evals la mejora de exactitud que aporta el corpus (bloqueado por Ragas + promptfoo sin ejecutar).- Hybrid model routing and the eval judge may use a hosted model; confirm a DPA covers any patient data sent on the Claude route (the medGemma route stays fully self-hosted for the privacy-default positioning).