morphos / backend /tests /test_hf_space_cleanup.py
Jose Salazar
Ajustes varios al engine y al prompt basado en resultados de los evals
bf9f7d1
Raw
History Blame Contribute Delete
5.61 kB
"""Regresión de la limpieza y detección de salida defectuosa del HF Space (medGemma).
medGemma es un modelo con "pensamiento" que de forma intermitente filtra su cadena de
razonamiento en inglés y/o entra en un bucle de repetición sin llegar a la respuesta. Estas
pruebas fijan que esa salida se detecte (para forzar un reintento) y no llegue al usuario.
"""
from __future__ import annotations
import pytest
from app.ai.hf_space import (
_cortar_bucle_lineas,
interpretacion_defectuosa,
interpretacion_truncada,
limpiar_respuesta,
)
_SALIDA_CON_RAZONAMIENTO = """thought
Here's a thinking process to arrive at the clinical interpretation:
1. Understand the Goal: interpret canine labs.
Highly Recommended:
* Serum Chemistry (repeat): To monitor liver enzymes and bilirubin.
* Serum Chemistry (repeat): To monitor liver enzymes and bilirubin.
* Serum Chemistry (repeat): To monitor liver enzymes and bilirubin.
"""
_SALIDA_VALIDA = (
"Los hallazgos muestran neutrofilia moderada y linfopenia leve, con enzimas hepáticas "
"elevadas y bilirrubina alta que sugieren un patrón colestásico. Los diferenciales "
"principales son hiperadrenocorticismo y hepatopatía. Se recomienda urianálisis, perfil "
"bioquímico y ecografía abdominal para confirmar."
)
def test_detecta_razonamiento_filtrado():
assert interpretacion_defectuosa(limpiar_respuesta(_SALIDA_CON_RAZONAMIENTO)) is True
def test_detecta_bucle_de_repeticion():
bucle = "Introducción válida del caso.\n" + "\n".join(
["* Repetir esta recomendación diagnóstica concreta."] * 5
)
assert interpretacion_defectuosa(bucle) is True
def test_detecta_salida_trivial():
assert interpretacion_defectuosa("ok") is True
def test_interpretacion_valida_no_se_marca():
assert interpretacion_defectuosa(limpiar_respuesta(_SALIDA_VALIDA)) is False
def test_corte_de_bucle_a_nivel_de_linea():
texto = "Intro.\n" + "\n".join(["* Item largo repetido de prueba clínica."] * 6) + "\nfinal"
cortado = _cortar_bucle_lineas(texto)
assert cortado.count("Item largo repetido") < 6
def test_limpieza_conserva_respuesta_valida():
# La limpieza no debe destruir una respuesta correcta.
assert "colestásico" in limpiar_respuesta(_SALIDA_VALIDA)
# El corte a mitad de frase es el único defecto que se ve completo: texto largo, sin
# repeticiones y sin razonamiento filtrado. Visto en producción (colestasis-felino).
_SALIDA_TRUNCADA = (
"El paciente presenta elevación de ALT, ALP y bilirrubina, compatible con un patrón "
"mixto de daño hepatocelular y colestasis. Las principales diferencias diagnósticas "
"a considerar incluyen la lip"
)
def test_detecta_respuesta_cortada_a_mitad_de_frase():
assert interpretacion_truncada(_SALIDA_TRUNCADA) is True
assert interpretacion_defectuosa(_SALIDA_TRUNCADA) is True
def test_respuesta_completa_no_se_marca_como_truncada():
assert interpretacion_truncada(_SALIDA_VALIDA) is False
def test_cierre_con_adornos_markdown_no_es_truncamiento():
assert interpretacion_truncada(_SALIDA_VALIDA + "**") is False
assert interpretacion_truncada(_SALIDA_VALIDA[:-1] + '.")') is False
def test_lista_final_corta_sin_punto_no_es_truncamiento():
"""Falso positivo a evitar: una recomendación en viñeta no lleva punto y es válida."""
texto = _SALIDA_VALIDA + "\n- Ecografía abdominal\n- Perfil de coagulación"
assert interpretacion_truncada(texto) is False
# --- Modo estructurado (hf_space_estructurado) ---
def _cliente(monkeypatch, estructurado: bool):
"""Instancia el cliente con el flag puesto, invalidando la config cacheada."""
from app.ai.hf_space import HFSpaceClient
from app.config import obtener_config
monkeypatch.setenv("MORPHOS_HF_SPACE_ESTRUCTURADO", "1" if estructurado else "0")
obtener_config.cache_clear()
try:
return HFSpaceClient()
finally:
monkeypatch.undo()
obtener_config.cache_clear()
def test_el_nombre_distingue_el_modo(monkeypatch):
# El servicio decide por el nombre si aplica el prompt de prosa y si exige estructura.
assert _cliente(monkeypatch, False).nombre == "medgemma-hf"
assert _cliente(monkeypatch, True).nombre == "medgemma-hf-json"
def test_parsea_json_del_space(monkeypatch):
import json as _json
from app.ai.hf_space import HFSpaceClient
payload = _json.dumps({
"interpretacion": "Anemia microcítica hipocrómica compatible con ferropenia.",
"hallazgos_clave": [{"analito": "hct", "direccion": "bajo", "gravedad": "moderado", "comentario": ""}],
"diferenciales": [{"nombre": "ferropenia", "probabilidad": "alta", "evidencia": [], "citas": []}],
"siguientes_pruebas": ["sangre oculta en heces"],
"confianza": "media",
"requiere_derivacion": True,
})
r = HFSpaceClient._parsear_estructurado(payload)
assert r.diferenciales[0].nombre == "ferropenia"
assert r.hallazgos_clave[0].analito == "hct"
def test_json_invalido_es_error_reintentable():
from app.ai.base import ErrorModelo
from app.ai.hf_space import HFSpaceClient
with pytest.raises(ErrorModelo) as exc:
HFSpaceClient._parsear_estructurado("{no es json")
assert exc.value.reintentable
def test_json_fuera_de_esquema_es_error_reintentable():
from app.ai.base import ErrorModelo
from app.ai.hf_space import HFSpaceClient
# `interpretacion` vacía viola el validador del esquema.
with pytest.raises(ErrorModelo):
HFSpaceClient._parsear_estructurado('{"interpretacion": ""}')