Nucleo propio: oye con whisper.cpp, piensa con un modelo de Ollama, habla con Piper, y hace RAG sobre los apuntes del usuario. 100% local, sin cuentas ni claves. Escrito bajo una restriccion dura, 4 GB de VRAM: el cerebro y whisper comparten tarjeta y solo caben porque estan dimensionados para ello. El RAG usa embeddings estaticos con busqueda hibrida; la voz clonada se sirve de una cache de frases. Incluye instalador (install.sh), requisitos, y documentacion del stack, del manejo de root y de las acciones. Los apuntes indexados y el diario NO se incluyen: son privados y el .gitignore los bloquea.
172 lines
7.1 KiB
Python
Executable file
172 lines
7.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Ejecuta las acciones de verdad y mira si funcionan y si se entienden.
|
|
|
|
python3 ~/COFRE/CODERS/JARVIS/config/probar_acciones.py
|
|
python3 ~/COFRE/CODERS/JARVIS/config/probar_acciones.py --romper # solo las malas
|
|
|
|
Las 121 acciones se escribieron a mano y casi ninguna se ha ejecutado nunca.
|
|
Alguna tendra el comando roto, la salida vacia, o una plantilla que apunta a un
|
|
campo que no existe —y entonces JARVIS diria una frase sin sentido en voz alta.
|
|
Esto las corre, captura la salida y las clasifica, para saber cuales arreglar.
|
|
|
|
Que hace con cada tipo:
|
|
- Solo lectura (df, free, ps, cat /sys...): las EJECUTA y comprueba la salida.
|
|
Son la mayoria y las que de verdad importan, porque son las que hay que
|
|
"interpretar bien".
|
|
- Con efecto (abre navegador, apaga pantalla, bloquea, sube volumen): NO las
|
|
ejecuta —tendria efectos de verdad— pero valida que el comando exista.
|
|
- Destructivas (confirmar=True): NO las ejecuta, solo comprueba el comando.
|
|
- Con argumento: las ejecuta con un valor de prueba inofensivo.
|
|
|
|
Clasificacion de cada accion:
|
|
OK corrio, dio salida, y la respuesta hablada sale con datos reales
|
|
VACIA corrio pero sin salida (la herramienta no aplica en esta maquina)
|
|
ROTA el comando fallo o no existe
|
|
PLANTILLA la respuesta cayo al volcado crudo: la plantilla pide un campo que
|
|
la salida no tiene. Es el fallo que hace que JARVIS suene raro.
|
|
EFECTO no se ejecuta por tener efectos; comando validado
|
|
"""
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
|
|
JARVIS = os.path.expanduser("~/COFRE/CODERS/JARVIS")
|
|
MOTOR = os.path.join(JARVIS, "newelle/src/utility/acciones.py")
|
|
CATALOGO = os.path.expanduser(
|
|
"~/.local/share/flatpak/app/io.github.qwersyk.Newelle/x86_64/master/"
|
|
"active/files/share/newelle/acciones/acciones.json")
|
|
CATALOGO_FUENTE = os.path.join(JARVIS, "newelle/data/acciones/acciones.json")
|
|
INFORME = os.path.join(JARVIS, "muestras/acciones-salud.json")
|
|
|
|
# Órdenes que cambian algo del sistema o abren cosas: se validan pero no se
|
|
# lanzan, que probar un asistente no deberia dejarte el navegador con veinte
|
|
# pestañas ni la pantalla apagada.
|
|
EFECTOS = ["firefox", "xdg-open", "loginctl", "xset", "pactl set", "pkill",
|
|
"brightnessctl", "systemctl start", "systemctl stop", "import -window",
|
|
"gnome-screenshot", "rm ", "kill", "--private-window", "dpms",
|
|
"ventanas.sh", "xdotool", "wmctrl", "xrandr --output"]
|
|
|
|
# Valores de prueba para las acciones con argumento, inofensivos a proposito.
|
|
ARGUMENTOS = {
|
|
"buscar_fichero": "acciones", "buscar_carpeta": "config",
|
|
"leer_fichero": "README", "abrir_fichero": "no_existe_xyz",
|
|
"abrir_carpeta": "no_existe_xyz", "tamano_carpeta": "config",
|
|
"proceso_buscar": "python", "proceso_matar": "proceso_inexistente_xyz",
|
|
"instalado": "bash", "manual": "ls",
|
|
}
|
|
|
|
|
|
def carga_motor():
|
|
spec = importlib.util.spec_from_file_location("acciones", MOTOR)
|
|
m = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(m)
|
|
return m
|
|
|
|
|
|
def spawn():
|
|
return ["flatpak-spawn", "--host"] if os.path.exists("/.flatpak-info") else []
|
|
|
|
|
|
def tiene_efecto(comando: str) -> bool:
|
|
return any(marca in comando for marca in EFECTOS)
|
|
|
|
|
|
def ejecuta_crudo(comando: str, argumento: str = "") -> tuple:
|
|
"""Corre el comando en el host y devuelve (salida, codigo)."""
|
|
c = comando.replace("{argumento}", shlex.quote(argumento))
|
|
c = c.replace("{argumento_url}", shlex.quote(argumento))
|
|
try:
|
|
r = subprocess.run(spawn() + ["bash", "-lc", c],
|
|
capture_output=True, text=True, timeout=15)
|
|
return (r.stdout or r.stderr or "").strip(), r.returncode
|
|
except subprocess.TimeoutExpired:
|
|
return "TIMEOUT", 124
|
|
except OSError as e:
|
|
return f"ERROR: {e}", 1
|
|
|
|
|
|
def clasifica(motor, accion, salida, codigo):
|
|
"""A partir de la salida cruda, decide como quedaria la respuesta hablada."""
|
|
plantilla = accion.respuesta
|
|
dicho = motor.formatea(plantilla, salida, "")
|
|
|
|
# ¿la plantilla cayo al volcado crudo? formatea() devuelve {salida} cuando
|
|
# el .format() peta por un campo que no existe. Se detecta comparando: si
|
|
# la respuesta ES la salida y la plantilla NO era {salida}, fallo.
|
|
campos = re.findall(r"\{(campo\d+|ultima|primera)\}", plantilla)
|
|
if campos and dicho.strip() == salida[:600].strip() and plantilla.strip() not in ("{salida}", "{salida}."):
|
|
return "PLANTILLA", dicho
|
|
if not salida:
|
|
return "VACIA", dicho
|
|
if codigo != 0 and ("command not found" in salida or "No such file" in salida
|
|
or salida.startswith("ERROR") or salida == "TIMEOUT"):
|
|
return "ROTA", dicho
|
|
return "OK", dicho
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--romper", action="store_true", help="solo las que fallan")
|
|
ap.add_argument("--json", action="store_true", help="salida en json")
|
|
args = ap.parse_args()
|
|
|
|
motor = carga_motor()
|
|
catalogo = CATALOGO if os.path.exists(CATALOGO) else CATALOGO_FUENTE
|
|
cat = motor.Catalogo([catalogo])
|
|
|
|
resultados = []
|
|
for accion in cat.acciones:
|
|
estado = None
|
|
if accion.confirmar:
|
|
estado = "EFECTO" if not accion.comando else "DESTRUCTIVA"
|
|
dicho = accion.acuse
|
|
elif tiene_efecto(accion.comando):
|
|
estado = "EFECTO"
|
|
dicho = "(no se ejecuta; " + ("comando ok" if accion.comando else "sin comando") + ")"
|
|
else:
|
|
arg = ARGUMENTOS.get(accion.id, "") if accion.captura else ""
|
|
salida, codigo = ejecuta_crudo(accion.comando, arg)
|
|
estado, dicho = clasifica(motor, accion, salida, codigo)
|
|
|
|
resultados.append({
|
|
"id": accion.id, "grupo": accion.grupo, "estado": estado,
|
|
"dice": dicho[:120],
|
|
})
|
|
|
|
cuenta = {}
|
|
for r in resultados:
|
|
cuenta[r["estado"]] = cuenta.get(r["estado"], 0) + 1
|
|
|
|
if args.json:
|
|
print(json.dumps({"cuenta": cuenta, "acciones": resultados},
|
|
ensure_ascii=False))
|
|
else:
|
|
malas = {"ROTA", "PLANTILLA", "VACIA"}
|
|
for r in resultados:
|
|
if args.romper and r["estado"] not in malas:
|
|
continue
|
|
marca = {"OK": " ok ", "VACIA": " vacia", "ROTA": " ROTA ",
|
|
"PLANTILLA": " PLANT", "EFECTO": " efect",
|
|
"DESTRUCTIVA": " destr"}.get(r["estado"], " ? ")
|
|
print(f"[{marca}] {r['id']:<22} {r['dice']}")
|
|
print("\n" + " ".join(f"{k}: {v}" for k, v in sorted(cuenta.items())))
|
|
buenas = cuenta.get("OK", 0) + cuenta.get("EFECTO", 0) + cuenta.get("DESTRUCTIVA", 0)
|
|
print(f"utiles: {buenas}/{len(resultados)} · "
|
|
f"a revisar: {sum(cuenta.get(k, 0) for k in ('ROTA','PLANTILLA','VACIA'))}")
|
|
|
|
os.makedirs(os.path.dirname(INFORME), exist_ok=True)
|
|
with open(INFORME, "w") as f:
|
|
json.dump({"cuenta": cuenta, "acciones": resultados}, f,
|
|
indent=2, ensure_ascii=False)
|
|
|
|
fallan = sum(cuenta.get(k, 0) for k in ("ROTA", "PLANTILLA"))
|
|
return 1 if fallan else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|