Spaces:
Sleeping
Sleeping
| """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": ""}') | |