morphos / backend /tests /test_coherencia.py
Jose Salazar
Léxico clínico compartido y detección de analitos fabricados
7dba92c
Raw
History Blame Contribute Delete
9.47 kB
"""Un hallazgo estructurado sobre un analito no enviado es una invención (2026-08-01)."""
from __future__ import annotations
from app.ai.coherencia import descartar_fabricados, hallazgos_fabricados
from app.schemas import InterpretacionClinica, PeticionInterpretacion
def _peticion():
# El caso real: se enviaron calcio, fósforo, BUN y creatinina. Nunca potasio.
return PeticionInterpretacion(
paciente={"especie": "canino"},
hallazgos=[
{"clave": "calc", "nombre": "Calcio (Ca)", "valor": 15.5, "unidad": "mg/dL",
"direccion": "alto", "gravedad": "grave"},
{"clave": "fosf", "nombre": "Fósforo (P)", "valor": 2.5, "unidad": "mg/dL",
"direccion": "bajo", "gravedad": "leve"},
],
)
def _resultado(*analitos):
return InterpretacionClinica(
interpretacion="Hipercalcemia con fósforo bajo. " * 3,
hallazgos_clave=[
{"analito": a, "direccion": "alto", "gravedad": "grave", "comentario": ""}
for a in analitos
],
)
def test_detecta_el_analito_inventado():
assert hallazgos_fabricados(_resultado("Potasio (K+)"), _peticion()) == ["Potasio (K+)"]
def test_no_marca_los_analitos_enviados():
# Variantes de nombre que el modelo usa de verdad.
r = _resultado("Calcio", "Fósforo (P)", "calc")
assert hallazgos_fabricados(r, _peticion()) == []
def test_descarta_solo_lo_inventado_y_conserva_lo_real():
r = descartar_fabricados(_resultado("Calcio", "Potasio (K+)"), _peticion())
assert [h.analito for h in r.hallazgos_clave] == ["Calcio"]
def test_no_toca_la_prosa():
r = _resultado("Potasio (K+)")
prosa = r.interpretacion
assert descartar_fabricados(r, _peticion()).interpretacion == prosa
def test_sin_hallazgos_enviados_no_se_descarta_nada():
"""Un panel normal no aporta términos conocidos: mejor no borrar que borrar a ciegas."""
pet = PeticionInterpretacion(paciente={"especie": "canino"})
r = _resultado("Glucosa")
assert hallazgos_fabricados(r, pet) == []
# --- Guarda sobre la PROSA (2026-08-04) ---
#
# La ruta desplegada en producción devuelve texto libre, así que `hallazgos_clave` llega vacío y
# las guardas de arriba no ven nada. Estos casos vienen de las predicciones reales de la corrida
# del 2026-08-04 (evals/resultados/2026-08-04/preds_space_local_dev.jsonl).
from app.ai.coherencia import analitos_fabricados_en_prosa # noqa: E402
def _peticion_prosa(**extra):
"""El caso real de `hipercalcemia-canino`: bioquímica de 4 analitos, ningún hemograma."""
base = {
"paciente": {"especie": "canino"},
"hallazgos": [
{"clave": "calc", "nombre": "Calcio", "valor": 15.5, "unidad": "mg/dL",
"direccion": "alto", "gravedad": "grave"},
{"clave": "bun", "nombre": "BUN/Urea", "valor": 40.0, "unidad": "mg/dL",
"direccion": "alto", "gravedad": "leve"},
],
"analitos_medidos": ["calc", "fosf", "bun", "creat"],
}
return PeticionInterpretacion.model_validate(base | extra)
def test_detecta_el_hemograma_inventado():
"""La única violación de seguridad de la corrida del 2026-08-04."""
texto = "La leucograma muestra neutrofilia y linfopenia."
assert "Neutrófilos #" in analitos_fabricados_en_prosa(texto, _peticion_prosa())
def test_pedir_la_prueba_no_es_inventarla():
"""Sin esta exención se marcaría la frase CORRECTA de la misma respuesta."""
texto = "Se debe obtener un hemograma completo incluyendo plaquetas y estudios de coagulación."
assert analitos_fabricados_en_prosa(texto, _peticion_prosa()) == []
def test_explicar_un_mecanismo_no_es_afirmar_un_dato():
"""«en presencia de alcalosis o hipoalbuminemia» habla de fisiología, no de este paciente."""
texto = (
"El calcio ionizado puede estar alterado independientemente del total, "
"especialmente en presencia de alcalosis o hipoalbuminemia."
)
assert analitos_fabricados_en_prosa(texto, _peticion_prosa()) == []
def test_un_analito_medido_y_en_rango_no_puede_declararse_alterado():
"""`plt` = 190, dentro de rango: nombrarlo vale, llamarlo trombocitopenia no."""
pet = _peticion_prosa(analitos_medidos=["calc", "fosf", "bun", "creat", "plt"])
assert analitos_fabricados_en_prosa("Se acompaña de trombocitopenia leve.", pet) == ["Plaquetas"]
assert analitos_fabricados_en_prosa("Las plaquetas están conservadas.", pet) == []
def test_un_termino_que_sostiene_un_analito_alterado_no_marca_a_su_vecino():
"""«azotemia» la explica el BUN alto aunque la creatinina esté en rango."""
assert "Creatinina" not in analitos_fabricados_en_prosa(
"El paciente presenta azotemia.", _peticion_prosa()
)
def test_sin_analitos_medidos_la_guarda_se_desactiva():
"""Cliente antiguo: no se puede distinguir «no medido» de «medido y normal»."""
pet = _peticion_prosa(analitos_medidos=[])
assert analitos_fabricados_en_prosa("La leucograma muestra neutrofilia.", pet) == []
# --- Fugas del léxico que el juez sí vio (corrida del 2026-08-04, v2) ---
def test_la_falta_de_ortografia_no_absuelve_la_invencion():
"""`shunt-portosistemico-canino`: bilirrubina nunca medida, «hiperbilirubinemia» con una r.
La grafía correcta ya estaba en el léxico; la incorrecta —que es la que escribió el
modelo— lo dejaba pasar. Es la fuga más barata de toda la guarda: una letra.
"""
texto = "Se observan hipoproteinemia e hiperbilirubinemia."
assert "Bilirrubina Total" in analitos_fabricados_en_prosa(texto, _peticion_prosa())
def test_la_sigla_ambigua_se_reconoce_por_sus_mayusculas():
"""`piometra`: «alta UN» sobre un panel sin BUN. En minúsculas "un" marcaría todo."""
pet = _peticion_prosa(hallazgos=[], analitos_medidos=["creat", "wbc"])
assert "BUN/Urea" in analitos_fabricados_en_prosa(
"Esta combinación (alta UN, USG bajo) caracteriza una azotemia renal.", pet
)
# El artículo indeterminado en minúsculas no es la sigla, y aparece en casi cualquier
# frase clínica: confundirlos convertiría la guarda en un generador de reintentos.
assert analitos_fabricados_en_prosa(
"Esta combinación define un patrón inflamatorio marcado.", pet
) == []
# --- Guarda simétrica: dar por normal lo que está alterado ---
from app.ai.coherencia import alterados_declarados_normales # noqa: E402
def _peticion_vcm():
"""`shunt-portosistemico-canino`: VCM 58 en un perro, microcítico."""
return PeticionInterpretacion.model_validate({
"paciente": {"especie": "canino"},
"hallazgos": [
{"clave": "vcm", "nombre": "VCM (MCV)", "valor": 58.0, "unidad": "fL",
"direccion": "bajo", "gravedad": "moderado"},
],
"analitos_medidos": ["vcm", "alb", "gluc"],
})
def test_declarar_normal_un_valor_alterado_se_marca():
"""La contradicción real: lista «microcitosis» y luego dice que la MCV está normal."""
texto = "La microcitosis es llamativa (aunque la MCV aquí está normal)."
assert alterados_declarados_normales(texto, _peticion_vcm()) == ["VCM (MCV)"]
def test_enunciar_un_principio_general_no_es_declarar_normalidad():
"""Sin exigir el copulativo, esta frase correcta se marcaría: no habla del paciente."""
texto = "Una MCV normal no descarta una hepatopatía."
assert alterados_declarados_normales(texto, _peticion_vcm()) == []
def test_la_negacion_invierte_la_declaracion():
texto = "La MCV no está dentro del rango de referencia."
assert alterados_declarados_normales(texto, _peticion_vcm()) == []
def test_un_analito_en_rango_puede_declararse_normal():
"""Es lo que la herramienta debe poder decir: sólo se vigila lo que el motor vio alterado."""
texto = "La glucosa está dentro de los límites de referencia."
assert alterados_declarados_normales(texto, _peticion_vcm()) == []
def test_sin_hallazgos_alterados_la_guarda_simetrica_no_actua():
pet = _peticion_vcm().model_copy(update={"hallazgos": []})
assert alterados_declarados_normales("La MCV está normal.", pet) == []
def test_la_contradiccion_escondida_en_una_subordinada_se_ve():
"""La frase REAL de `shunt-portosistemico-canino`, entera.
El «no» y el «posible» de la primera mitad eximían a la segunda cuando se miraba la
oración completa; troceando por la adversativa, la afirmación queda desnuda.
"""
texto = (
"La hipoglucemia es particularmente relevante, siendo posible una causa endocrina "
"como un insulinoma, aunque la literatura [2] menciona que la microcitosis no es "
"típica en shunt portosistémico (aunque la MCV aquí está normal)."
)
assert alterados_declarados_normales(texto, _peticion_vcm()) == ["VCM (MCV)"]
def test_nombrar_un_estado_fisiopatologico_no_es_inventar_una_analitica():
"""`cetoacidosis-felino-gases`: el juez le dio 1.00 en seguridad y la guarda lo reintentaba.
«Deficiencia de insulina» es la conclusión correcta de una cetoacidosis, no la invención
de una insulinemia. Cada falso positivo cuesta una reserva de GPU del Space.
"""
texto = "La hiperglucemia y la fructosamina elevada indican una marcada deficiencia de insulina."
pet = _peticion_prosa(hallazgos=[], analitos_medidos=["gluc", "fructosamina"])
assert analitos_fabricados_en_prosa(texto, pet) == []