#!/usr/bin/env python3 """Diagnostico del RAG: comprueba la cadena entera y dice que falla y como arreglarlo. python -m saber.diagnostico comprobaciones rapidas (segundos) python -m saber.diagnostico --eval ademas mide hit@k con el eval_set python -m saber.diagnostico --verboso ensena el detalle de cada resultado Pensado para cuando "el RAG no responde bien": en vez de adivinar, corres esto y te marca en verde/rojo cada eslabon —dependencias, indice, embedder, la busqueda, y opcionalmente ollama— con una linea de que hacer si falla. Sale con codigo != 0 si hay algun fallo duro, para poder meterlo en CI. """ import argparse import os import sys import traceback AQUI = os.path.dirname(os.path.abspath(__file__)) NUCLEO = os.path.dirname(AQUI) if NUCLEO not in sys.path: sys.path.insert(0, NUCLEO) _ok = _fallo = _aviso = 0 def _color(txt, c): return f"\033[{c}m{txt}\033[0m" if sys.stdout.isatty() else txt def marca(estado, nombre, detalle="", arregla=""): global _ok, _fallo, _aviso etq = {"ok": _color("OK ", "32"), "fallo": _color("FALLO", "31"), "aviso": _color("AVISO", "33")}[estado] print(f" {etq} {nombre}") if detalle: print(f" {detalle}") if arregla and estado != "ok": print(f" {_color('→ ' + arregla, '90')}") _ok += estado == "ok" _fallo += estado == "fallo" _aviso += estado == "aviso" # ── Consultas de humo: valen tanto en el indice publico como en el personal ── # (consulta, [palabras que un buen resultado deberia contener, cualquiera vale]) HUMO = [ ("como cambiar los permisos de un fichero", ["chmod"]), ("como saco una shell reversa", ["/dev/tcp", "nc ", "bash -i", "reversa"]), ("escanear puertos de una maquina", ["nmap"]), ("alternativa self-hosted a google analytics", ["analytic", "plausible", "matomo", "goatcounter", "umami"]), ] def check_deps(): try: import numpy # noqa: F401 import model2vec # noqa: F401 marca("ok", "dependencias (numpy, model2vec) importan") return True except ImportError as e: marca("fallo", "dependencias", f"falta: {e.name}", "instala con: pip install -r requirements.txt (o activa el venv)") return False def check_indice(): from saber import busca fuente = busca._fuente() if not fuente: marca("fallo", "indice presente", "no hay saber.jsonl ni saber-publico.jsonl", "corre nucleo/saber/indexa.py, o comprueba que el repo trae " "nucleo/datos/saber-publico.jsonl") return None cual = "personal" if fuente.endswith("saber.jsonl") else "publico (del repo)" kb = os.path.getsize(fuente) // 1024 marca("ok", "indice presente", f"{cual}: {os.path.basename(fuente)} ({kb} KB)") return busca def check_carga(busca): try: if not busca._carga(): marca("fallo", "carga del indice", "el indice esta vacio o ilegible", "reconstruyelo con nucleo/saber/indexa.py") return False except Exception as e: marca("fallo", "carga del indice", f"{type(e).__name__}: {e}", "el jsonl puede estar corrupto; reconstruye con indexa.py") if os.environ.get("JARVIS_DEBUG"): traceback.print_exc() return False n = len(busca._entradas) tipos = {} for e in busca._entradas: tipos[e.get("tipo", "?")] = tipos.get(e.get("tipo", "?"), 0) + 1 resumen = ", ".join(f"{k}:{v}" for k, v in sorted(tipos.items(), key=lambda x: -x[1])[:6]) marca("ok", "carga del indice", f"{n} entradas ({resumen})") # avisos si falta contenido que deberia venir con el repo if tipos.get("metodologia", 0) == 0: marca("aviso", "pack de conocimiento", "0 fragmentos de metodologia", "deberia venir en nucleo/saber/conocimiento/; revisa el clon") if tipos.get("catalogo", 0) == 0: marca("aviso", "catalogo (awesome lists)", "0 fragmentos de catalogo", "clona listas en ~/COFRE/CODERS/ y reindexa, o usa el indice publico") return True def check_busqueda(busca, verboso): todo_ok = True for consulta, esperadas in HUMO: try: res = busca.busca(consulta, cuantos=5) except Exception as e: marca("fallo", f"busqueda: '{consulta}'", f"{type(e).__name__}: {e}", "falla el embedder o la busqueda; revisa model2vec") todo_ok = False continue blob = " ".join(r.get("texto", "").lower() for r in res) acierto = any(p.lower() in blob for p in esperadas) top = (res[0].get("herramienta") or res[0].get("perfil") or "?") if res else "(nada)" if acierto: marca("ok", f"busqueda: '{consulta}'", f"top: {top}" if verboso else "") else: marca("aviso", f"busqueda: '{consulta}'", f"top: {top} — no contiene {esperadas}", "recuperacion floja para esta consulta; puede faltar contenido") return todo_ok def check_ollama(): import json import urllib.request endpoint = os.environ.get("JARVIS_OLLAMA", "http://127.0.0.1:11434") try: with urllib.request.urlopen(endpoint + "/api/tags", timeout=5) as r: modelos = [m["name"] for m in json.load(r).get("models", [])] except Exception as e: marca("aviso", "ollama (para el cerebro)", f"no responde en {endpoint}: {e}", "el RAG recupera sin ollama, pero el asistente necesita: " "arranca ollama y descarga el modelo (ollama pull qwen3.5:4b)") return if not modelos: marca("aviso", "ollama (para el cerebro)", "responde pero sin modelos", "descarga uno: ollama pull qwen3.5:4b") return marca("ok", "ollama (para el cerebro)", f"{len(modelos)} modelos: {', '.join(modelos[:4])}") def check_eval(busca): """Mide hit@k con el conjunto de evaluacion, si esta.""" import json ruta = os.path.join(AQUI, "enriquece", "eval_set.jsonl") if not os.path.exists(ruta): marca("aviso", "eval hit@k", "no hay enriquece/eval_set.jsonl") return casos = [json.loads(l) for l in open(ruta, encoding="utf-8") if l.strip()] h1 = h5 = 0 for c in casos: res = busca.busca(c["pregunta"], cuantos=5) textos = [r.get("texto", "").lower() for r in res] esperado = [p.lower() for p in c.get("espera", c.get("contiene", []))] # misma semantica que evalua.py: basta que UNA palabra esperada aparezca acierta = lambda t: any(p and p in t for p in esperado) if res and acierta(textos[0]): h1 += 1 if any(acierta(t) for t in textos): h5 += 1 n = len(casos) or 1 est = "ok" if 100 * h1 // n >= 50 else "aviso" marca(est, "eval hit@k", f"hit@1 {h1}/{n} ({100*h1//n}%) hit@5 {h5}/{n} ({100*h5//n}%)", "recuperacion baja: reconstruye el indice o revisa el eval_set") def main(): p = argparse.ArgumentParser() p.add_argument("--eval", action="store_true", help="ademas mide hit@k") p.add_argument("--verboso", action="store_true", help="ensena el top de cada busqueda") a = p.parse_args() print("\n Diagnostico del RAG de JARVIS\n " + "─" * 40) if not check_deps(): print(f"\n {_color('Para el resto hacen falta las dependencias.', '31')}\n") return 1 busca = check_indice() if busca and check_carga(busca): check_busqueda(busca, a.verboso) if a.eval: check_eval(busca) check_ollama() print(" " + "─" * 40) resumen = f" {_ok} OK" if _aviso: resumen += f" {_aviso} avisos" if _fallo: resumen += f" {_color(str(_fallo) + ' fallos', '31')}" print(resumen + "\n") return 1 if _fallo else 0 if __name__ == "__main__": sys.exit(main())