Spaces:
Sleeping
Sleeping
File size: 5,610 Bytes
70e641d bf9f7d1 70e641d bf9f7d1 70e641d bf9f7d1 | 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 | """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": ""}')
|