JARVIS/config/probar_acciones.py
sito 8e4bc8ad94 JARVIS: asistente de voz local para Linux
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.
2026-08-16 15:34:37 +02:00

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())