Spaces:
Sleeping
Sleeping
File size: 8,530 Bytes
70e641d bf9f7d1 70e641d bf9f7d1 70e641d bf9f7d1 6ab7946 bf9f7d1 6ab7946 bf9f7d1 70e641d bf9f7d1 7dba92c | 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | """Pruebas del constructor de prompt y la degradación sin-RAG del recuperador."""
from __future__ import annotations
import pytest
from app.ai.prompt import SISTEMA, construir_mensaje_usuario
from app.rag.retriever import Fragmento, construir_consulta
from app.schemas import PeticionInterpretacion
def _peticion():
return PeticionInterpretacion(
paciente={"especie": "canino", "raza": "Labrador", "edad_meses": 96, "sexo": "Macho"},
hallazgos=[
{"clave": "hct", "nombre": "Hematocrito", "valor": 25, "unidad": "%", "direccion": "bajo", "gravedad": "moderado"},
],
patrones=[
{"nombre": "Anemia microcítica", "descripcion": "Parámetros eritrocitarios disminuidos.", "gravedad": "moderado", "parametros": ["hct"]},
],
signos_clinicos="Letargia y mucosas pálidas",
)
def test_sistema_exige_espanol_y_derivacion():
assert "español" in SISTEMA.lower()
assert "requiere_derivacion" in SISTEMA
def test_mensaje_incluye_paciente_hallazgos_y_patrones():
msg = construir_mensaje_usuario(_peticion(), [])
assert "canino" in msg
assert "Hematocrito" in msg
assert "Anemia microcítica" in msg
assert "Letargia" in msg
def test_mensaje_incluye_bloque_rag_con_cita():
frag = Fragmento(
texto="La anemia ferropénica cursa con microcitosis e hipocromía.",
libro="Thrall Veterinary Hematology", edicion="3.ª ed.", capitulo="Anemia", pagina="210", score=0.1,
)
msg = construir_mensaje_usuario(_peticion(), [frag])
assert "Literatura recuperada" in msg
assert "Thrall" in msg and "p. 210" in msg
def test_panel_normal_no_mete_lineas_de_relleno():
"""Regresión del 2026-07-28: con «Todos los valores dentro de rangos de referencia» y
«Ninguno detectado…» en el prompt, qwen2.5:14b devolvió un hallazgo llamado literalmente
«Todos los valores» (alto · leve) sobre una glucosa en rango. Los bloques vacíos se omiten.
"""
pet = PeticionInterpretacion(
paciente={"especie": "canino", "raza": "Labrador", "edad_meses": 36, "sexo": "Macho"},
signos_clinicos="Chequeo rutinario, asintomático",
)
msg = construir_mensaje_usuario(pet, [])
assert "Todos los valores" not in msg
assert "Ninguno detectado" not in msg
assert "Hallazgos de laboratorio" not in msg
assert "ningún valor fuera de rango" in msg
# Un panel normal no debe pedir diferenciales: eso es pedir que se invente patología.
assert "NO inventes" in msg
def test_caso_con_hallazgos_sigue_pidiendo_diferenciales():
msg = construir_mensaje_usuario(_peticion(), [])
assert "Hallazgos de laboratorio" in msg
assert "diferenciales ordenados por probabilidad" in msg
def test_construir_consulta_combina_terminos():
q = construir_consulta(["Anemia microcítica"], ["Hematocrito"])
assert "Anemia microcítica" in q and "Hematocrito" in q
def test_construir_consultas_separa_una_por_patron():
"""La consulta única promediaba patrones sin literatura común en un solo embedding."""
from app.rag.retriever import construir_consultas
qs = construir_consultas(
["Anemia regenerativa", "Azotemia renal"], ["Hematocrito", "Creatinina"]
)
assert qs[:2] == ["Anemia regenerativa", "Azotemia renal"]
assert "Hematocrito" in qs[-1] and "Creatinina" in qs[-1]
def test_construir_consultas_sin_patrones_degrada_a_una():
from app.rag.retriever import construir_consultas
assert construir_consultas([], ["Hematocrito", "Creatinina"]) == ["Hematocrito ; Creatinina"]
def test_construir_consultas_no_repite_terminos():
from app.rag.retriever import construir_consultas
assert construir_consultas(["Anemia"], ["anemia"]) == ["Anemia"]
@pytest.fixture
def multiconsulta(monkeypatch, request):
"""Fija `MORPHOS_RAG_MULTICONSULTA` invalidando la config cacheada a la ida y a la vuelta."""
from app.config import obtener_config
monkeypatch.setenv("MORPHOS_RAG_MULTICONSULTA", request.param)
obtener_config.cache_clear()
yield request.param == "1"
monkeypatch.undo()
obtener_config.cache_clear()
@pytest.mark.parametrize("multiconsulta", ["0", "1"], indirect=True)
async def test_el_servicio_elige_la_ruta_de_recuperacion_segun_la_bandera(monkeypatch, multiconsulta):
"""Con la bandera apagada (defecto) se recupera con la consulta CONCATENADA. Delegar el
caso en `recuperar_multi` sin más lo dejaría recuperando sólo por el primer patrón."""
from app.ai import service
from app.ai.base import ErrorModelo
vistas: dict[str, object] = {}
def _anotar(clave):
def _registrar(consulta, **_k):
vistas[clave] = consulta
return []
return _registrar
monkeypatch.setattr(service, "recuperar", _anotar("unica"))
monkeypatch.setattr(service, "recuperar_multi", _anotar("multi"))
class ClienteQueNoResponde:
nombre = "medgemma"
prosa = False
modelo = "medgemma:test"
async def interpretar(self, *_a, **_k):
raise ErrorModelo("no importa: sólo se mira la recuperación", reintentable=False)
monkeypatch.setattr(service, "_crear_cliente", lambda *_: ClienteQueNoResponde())
with pytest.raises(ErrorModelo):
await service.interpretar(_peticion())
if multiconsulta:
assert vistas["multi"] == ["Anemia microcítica", "Hematocrito"]
assert "unica" not in vistas
else:
assert vistas["unica"] == "Anemia microcítica ; Hematocrito"
assert "multi" not in vistas
def test_recuperar_degrada_sin_indice(monkeypatch):
# Contrato de degradación: sin recursos RAG (deps ausentes o índice no construido),
# recuperar devuelve [] sin lanzar. Se fuerza vía monkeypatch para no depender de si
# existe un índice real en el entorno de pruebas.
import app.rag.retriever as R
monkeypatch.setattr(R, "_cargar_recursos", lambda: None)
assert R.recuperar("anemia ferropénica", especie="canino") == []
def _frags(n: int, largo: int = 600):
from app.rag.retriever import Fragmento
return [
Fragmento(texto="x" * largo, libro=f"Libro {i}", edicion="1.ª ed.",
capitulo="", pagina=str(i), score=1.0 - i / 100)
for i in range(n)
]
def test_presupuesto_recorta_manteniendo_los_mejor_rankeados():
"""El Space reparte 2048 tokens entre el razonamiento que descarta y la respuesta: pasarle
los 6 fragmentos cortaba la interpretación a mitad de frase."""
from app.ai.service import recortar_a_presupuesto
recortados = recortar_a_presupuesto(_frags(6), 1800)
assert len(recortados) == 3
assert [f.libro for f in recortados] == ["Libro 0", "Libro 1", "Libro 2"]
def test_presupuesto_nunca_deja_la_respuesta_sin_literatura():
from app.ai.service import recortar_a_presupuesto
assert len(recortar_a_presupuesto(_frags(4), 10)) == 1
def test_presupuesto_desactivado_no_recorta():
from app.ai.service import recortar_a_presupuesto
assert len(recortar_a_presupuesto(_frags(6), 0)) == 6
# --- Bloque de panel completo (2026-08-04) ---
def test_el_panel_nombra_lo_medido_en_rango_y_cierra_la_lista():
"""`hallazgos` sólo trae lo alterado, así que sin esto el modelo no distingue «no se midió»
de «se midió y salió normal» y rellena el hueco (leucograma inventado, corrida 2026-08-04)."""
pet = _peticion().model_copy(update={"analitos_medidos": ["hct", "plt", "gluc"]})
msg = construir_mensaje_usuario(pet, [])
assert "DENTRO de rango" in msg
assert "Plaquetas" in msg and "Glucosa" in msg
assert "Hematocrito (Hct)" not in msg, "el alterado ya va en su propio bloque"
assert "panel COMPLETO" in msg
def test_sin_analitos_medidos_el_bloque_desaparece_entero():
"""Cliente antiguo: anunciar un «panel completo» vacío sería peor que no decir nada, y el
relleno se lee como contenido (mismo motivo que test_panel_normal_no_mete_lineas_de_relleno)."""
msg = construir_mensaje_usuario(_peticion(), [])
assert "DENTRO de rango" not in msg
assert "panel COMPLETO" not in msg
def test_el_panel_sin_ninguno_en_rango_conserva_el_cierre():
"""Todo lo medido salió alterado: no hay lista que enseñar, pero sí que decir que no hay más."""
pet = _peticion().model_copy(update={"analitos_medidos": ["hct"]})
msg = construir_mensaje_usuario(pet, [])
assert "DENTRO de rango" not in msg
assert "panel COMPLETO" in msg
|