Spaces:
Sleeping
Sleeping
File size: 6,352 Bytes
1e215c9 | 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 | """Circuito de revisión veterinaria del dataset dorado (human-in-the-loop).
Un caso sin firma profesional no es oro: `run_evals.py` lo deja fuera de la puerta. Este
script es el puente entre ese estado y el veterinario que puede resolverlo.
python revision.py # hoja de revisión de los casos pendientes
python revision.py --validar id1 id2 --revisor "Dra. Pérez" # firma los casos
python revision.py --estado # recuento por split y validación
La hoja incluye, por caso, lo que el veterinario necesita para decidir sin abrir el JSONL:
señalamiento, valores fuera de rango según el motor determinista, signos y los
diferenciales aceptables propuestos. Se escribe en dataset/revision_pendiente.md, que NO
se comitea: es un documento de trabajo (ver .gitignore).
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import subprocess
from pathlib import Path
AQUI = Path(__file__).resolve().parent
CASOS = AQUI / "dataset" / "casos.jsonl"
HOJA = AQUI / "dataset" / "revision_pendiente.md"
def cargar() -> list[dict]:
lineas = CASOS.read_text(encoding="utf-8").splitlines()
return [json.loads(linea) for linea in lineas if linea.strip()]
def guardar(casos: list[dict]) -> None:
CASOS.write_text(
"".join(json.dumps(c, ensure_ascii=False) + "\n" for c in casos), encoding="utf-8"
)
def _hallazgos_del_motor(caso: dict) -> list[dict]:
"""Pasa el caso por analisis.ts para mostrar QUÉ marca el motor, no lo que dice el JSONL.
Si Node no está disponible, la hoja se genera igual sin esta sección."""
try:
proc = subprocess.run(
["node", "--experimental-strip-types", str(AQUI / "engine_runner.ts")],
input=json.dumps({"valores": caso["valores"], "paciente": caso["paciente"]}),
capture_output=True, text=True, check=True,
)
return json.loads(proc.stdout)["hallazgos"]
except (OSError, subprocess.CalledProcessError, json.JSONDecodeError, KeyError):
return []
def generar_hoja(casos: list[dict]) -> str:
pendientes = [c for c in casos if not c.get("validado")]
lineas = [
"# Hoja de revisión veterinaria — dataset dorado de Morphos",
"",
f"Generada el {dt.date.today().isoformat()}. {len(pendientes)} caso(s) pendientes de "
f"{len(casos)}.",
"",
"Para cada caso, confirma o corrige los **diferenciales aceptables** y si el caso debe "
"marcar derivación. Cuando un caso quede conforme, fírmalo con:",
"",
"```bash",
"python evals/revision.py --validar <id> --revisor \"Tu nombre\"",
"```",
"",
"Mientras un caso siga pendiente NO cuenta para la puerta de evals: no puede aprobar "
"ni bloquear un despliegue.",
"",
]
for caso in pendientes:
p = caso["paciente"]
esp = caso["esperado"]
lineas += [
f"## {caso['id']} · split `{caso.get('split', 'dev')}`",
"",
f"**{caso['descripcion']}**",
"",
f"- Paciente: {p.get('especie', '?')}, {p.get('raza', 'NE')}, "
f"{p.get('edad_meses', '?')} meses, {p.get('sexo', 'NE')}",
f"- Signos clínicos: {caso.get('signos_clinicos') or '—'}",
f"- Valores: {', '.join(f'{k}={v}' for k, v in caso['valores'].items())}",
]
hallazgos = _hallazgos_del_motor(caso)
if hallazgos:
marcados = ", ".join(
f"{h['clave']} {h['direccion']}/{h['gravedad']}" for h in hallazgos
)
lineas.append(f"- Marcados por el motor: {marcados}")
lineas += [
f"- Hallazgos clave esperados: {', '.join(esp['hallazgos_clave']) or '—'}",
"",
"Diferenciales aceptables propuestos (corrige, añade o elimina):",
"",
]
lineas += [f"- [ ] {d}" for d in esp["diferenciales_aceptables"]] or ["- [ ] (ninguno)"]
lineas += [
"",
f"- [ ] `requiere_derivacion = {esp['requiere_derivacion']}` es correcto",
f"- [ ] `fuera_de_alcance = {esp['fuera_de_alcance']}` es correcto",
"",
]
return "\n".join(lineas)
def estado(casos: list[dict]) -> str:
def contar(pred) -> int:
return sum(1 for c in casos if pred(c))
return "\n".join([
f"Total: {len(casos)} casos",
f" dev: {contar(lambda c: c.get('split', 'dev') == 'dev')}"
f" (validados {contar(lambda c: c.get('split', 'dev') == 'dev' and c.get('validado'))})",
f" test: {contar(lambda c: c.get('split') == 'test')}"
f" (validados {contar(lambda c: c.get('split') == 'test' and c.get('validado'))})",
f" pendientes de validación: {contar(lambda c: not c.get('validado'))}",
])
def main() -> int:
parser = argparse.ArgumentParser(description="Revisión veterinaria del dataset dorado")
parser.add_argument("--validar", nargs="+", metavar="ID", help="marca casos como validados")
parser.add_argument("--revisor", default="", help="nombre del veterinario que firma")
parser.add_argument("--estado", action="store_true", help="sólo muestra el recuento")
args = parser.parse_args()
casos = cargar()
if args.estado:
print(estado(casos))
return 0
if args.validar:
if not args.revisor:
print("❌ --revisor es obligatorio: la validación tiene que ser trazable a una persona.")
return 1
por_id = {c["id"]: c for c in casos}
desconocidos = [i for i in args.validar if i not in por_id]
if desconocidos:
print(f"❌ Ids inexistentes: {', '.join(desconocidos)}")
return 1
hoy = dt.date.today().isoformat()
for id_caso in args.validar:
por_id[id_caso].update(validado=True, revisor=args.revisor, fecha_validacion=hoy)
print(f"✅ {id_caso} validado por {args.revisor} ({hoy})")
guardar(casos)
print("\n" + estado(casos))
return 0
HOJA.write_text(generar_hoja(casos), encoding="utf-8")
print(f"Hoja de revisión escrita en {HOJA.relative_to(AQUI.parent)}")
print(estado(casos))
return 0
if __name__ == "__main__":
raise SystemExit(main())
|