Spaces:
Sleeping
Sleeping
File size: 5,185 Bytes
af530a3 | 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 | """Pruebas de `run_ragas.py`: construcción de muestras y umbrales de groundedness.
La recuperación real y el juez local quedan fuera (uno necesita el índice, el otro Ollama):
lo que se fija aquí es que la muestra se arme con la MISMA consulta que produce el servicio
—si la eval midiera una consulta aproximada, mediría otra tubería— y que un NaN no se
confunda nunca con un cero.
"""
from __future__ import annotations
from dataclasses import dataclass
import pytest
import run_ragas
from run_ragas import UMBRALES, evaluar_umbrales
# --- Umbrales ---
def test_todo_por_encima_del_umbral_no_reporta_nada():
assert evaluar_umbrales({m: u + 0.1 for m, u in UMBRALES.items()}) == ([], [])
def test_metrica_por_debajo_del_umbral_se_reporta_con_su_valor():
fallos, incalculables = evaluar_umbrales({"faithfulness": 0.42})
assert fallos == ["faithfulness=0.42 < 0.70"]
assert incalculables == []
def test_nan_es_incalculable_y_no_un_suspenso():
fallos, incalculables = evaluar_umbrales({"faithfulness": float("nan")})
assert fallos == []
assert incalculables == ["faithfulness"]
def test_metrica_ausente_se_ignora():
assert evaluar_umbrales({}) == ([], [])
def test_valor_justo_en_el_umbral_aprueba():
assert evaluar_umbrales({"context_recall": UMBRALES["context_recall"]}) == ([], [])
def test_las_metricas_ajenas_a_la_puerta_no_la_afectan():
assert evaluar_umbrales({"answer_relevancy": 0.01}) == ([], [])
# --- Construcción de muestras ---
@dataclass
class FragmentoFalso:
texto: str
CASO = {
"id": "erc-felino",
"descripcion": "Azotemia en gato",
"paciente": {"especie": "felino"},
"valores": {"creat": 4.0},
"esperado": {"diferenciales_aceptables": ["enfermedad renal crónica"]},
}
INTERP = {
"interpretacion": "Azotemia con isostenuria.",
"diferenciales": [
{"nombre": "ERC", "evidencia": ["creatinina 4.0", "USG 1.012"]},
{"nombre": "Deshidratación", "evidencia": []},
],
}
@pytest.fixture
def tuberia(monkeypatch):
"""Sustituye motor y recuperación por dobles, y registra con qué se les llamó."""
pytest.importorskip("ragas", reason="requiere el grupo de dependencias 'evals'")
from app.rag import retriever
registro = {}
def motor_falso(valores, paciente):
registro["motor"] = (valores, paciente)
return [{"nombre": "creatinina alta"}], [{"nombre": "azotemia renal"}]
def recuperar_falso(consulta, especie=None, **kw):
registro["consulta"] = consulta
registro["especie"] = especie
return [FragmentoFalso("Chronic kidney disease in cats."), FragmentoFalso("USG < 1.035")]
monkeypatch.setattr(run_ragas, "_motor_determinista", motor_falso)
monkeypatch.setattr(retriever, "recuperar", recuperar_falso)
return registro
def test_la_consulta_se_construye_con_el_codigo_de_produccion(tuberia):
run_ragas.construir_muestras([CASO], {CASO["id"]: INTERP})
# `construir_consulta` es la misma función que usa `ai/service.py`: la eval mide la
# tubería real, no una aproximación escrita para la eval.
assert "azotemia renal" in tuberia["consulta"].lower()
assert "creatinina" in tuberia["consulta"].lower()
def test_la_especie_del_paciente_llega_al_filtro_de_recuperacion(tuberia):
run_ragas.construir_muestras([CASO], {CASO["id"]: INTERP})
assert tuberia["especie"] == "felino"
def test_la_respuesta_evaluada_incluye_prosa_y_diferenciales(tuberia):
muestra = run_ragas.construir_muestras([CASO], {CASO["id"]: INTERP})[0]
# Los diferenciales son afirmaciones clínicas: deben estar tan fundamentados como la prosa.
assert "Azotemia con isostenuria." in muestra.response
assert "ERC: creatinina 4.0; USG 1.012" in muestra.response
assert "Deshidratación" in muestra.response
def test_los_contextos_son_los_fragmentos_recuperados(tuberia):
muestra = run_ragas.construir_muestras([CASO], {CASO["id"]: INTERP})[0]
assert muestra.retrieved_contexts == ["Chronic kidney disease in cats.", "USG < 1.035"]
def test_la_referencia_sale_de_los_diferenciales_aceptables(tuberia):
muestra = run_ragas.construir_muestras([CASO], {CASO["id"]: INTERP})[0]
assert muestra.reference == "enfermedad renal crónica"
def test_sin_diferenciales_aceptables_la_referencia_cae_a_la_descripcion(tuberia):
caso = {**CASO, "esperado": {"diferenciales_aceptables": []}}
muestra = run_ragas.construir_muestras([caso], {caso["id"]: INTERP})[0]
assert muestra.reference == "Azotemia en gato"
def test_un_caso_sin_prediccion_se_omite(tuberia):
assert run_ragas.construir_muestras([CASO], {}) == []
def test_un_caso_sin_fragmentos_se_omite_en_vez_de_puntuar_contra_nada(monkeypatch):
pytest.importorskip("ragas", reason="requiere el grupo de dependencias 'evals'")
from app.rag import retriever
monkeypatch.setattr(run_ragas, "_motor_determinista", lambda v, p: ([], []))
monkeypatch.setattr(retriever, "recuperar", lambda *a, **k: [])
# Con 0 contextos, faithfulness sería 0 por falta de índice y no por el modelo.
assert run_ragas.construir_muestras([CASO], {CASO["id"]: INTERP}) == []
|