diagnostico del RAG: probar_rag.sh comprueba la cadena y explica los fallos
- nucleo/saber/diagnostico.py revisa dependencias, indice (personal o publico), carga, embedder, consultas de humo y ollama; marca verde/rojo con una linea de como arreglar cada fallo. --eval mide hit@k. Sale != 0 si hay fallo duro (CI). - probar_rag.sh es el envoltorio: encuentra el venv y lo corre sin nada arrancado. - README: seccion Comprobar el RAG.
This commit is contained in:
parent
873a22ea6a
commit
b1f8118272
3 changed files with 239 additions and 0 deletions
18
README.md
18
README.md
|
|
@ -81,6 +81,24 @@ Después:
|
||||||
./nucleo/arranca.sh # el asistente y el panel, en 127.0.0.1
|
./nucleo/arranca.sh # el asistente y el panel, en 127.0.0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
El RAG funciona nada más clonar: el repo trae un índice público pre-construido, así
|
||||||
|
que no hace falta indexar para que sepa de metodología, comandos y herramientas.
|
||||||
|
|
||||||
|
### Comprobar el RAG
|
||||||
|
|
||||||
|
Si la búsqueda de conocimiento no responde bien, este diagnóstico revisa la cadena
|
||||||
|
entera y marca en verde/rojo cada eslabón —dependencias, índice, embedder,
|
||||||
|
búsqueda y Ollama— con una línea de qué hacer si algo falla:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./probar_rag.sh # comprobaciones rápidas
|
||||||
|
./probar_rag.sh --eval # además mide la calidad de recuperación (hit@k)
|
||||||
|
./probar_rag.sh --verboso # enseña el primer resultado de cada búsqueda
|
||||||
|
```
|
||||||
|
|
||||||
|
No necesita nada arrancado y sale con código distinto de cero si hay algún fallo
|
||||||
|
duro, así que sirve también para CI.
|
||||||
|
|
||||||
## Qué le puedes pedir
|
## Qué le puedes pedir
|
||||||
|
|
||||||
Habla en castellano, natural. **150 acciones** en el catálogo, agrupadas:
|
Habla en castellano, natural. **150 acciones** en el catálogo, agrupadas:
|
||||||
|
|
|
||||||
205
nucleo/saber/diagnostico.py
Normal file
205
nucleo/saber/diagnostico.py
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
#!/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())
|
||||||
16
probar_rag.sh
Executable file
16
probar_rag.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Diagnostico del RAG: comprueba que la busqueda de conocimiento funciona y, si
|
||||||
|
# algo falla, dice que y como arreglarlo.
|
||||||
|
#
|
||||||
|
# ./probar_rag.sh comprobaciones rapidas
|
||||||
|
# ./probar_rag.sh --eval ademas mide la calidad de recuperacion (hit@k)
|
||||||
|
# ./probar_rag.sh --verboso ensena el primer resultado de cada busqueda
|
||||||
|
#
|
||||||
|
# No necesita nada arrancado: si no tienes indice propio, usa el publico del repo.
|
||||||
|
set -e
|
||||||
|
RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)
|
||||||
|
PY="$RAIZ/nucleo/venv/bin/python"
|
||||||
|
[ -x "$PY" ] || PY=$(command -v python3)
|
||||||
|
[ -n "$PY" ] || { echo "no encuentro python3"; exit 1; }
|
||||||
|
cd "$RAIZ/nucleo"
|
||||||
|
exec "$PY" -m saber.diagnostico "$@"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue