JARVIS/config/repasar_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

174 lines
6.6 KiB
Python
Executable file

#!/usr/bin/env python3
"""Ejecuta TODAS las acciones del catalogo y dice cuales funcionan de verdad.
Por que hace falta: hasta hoy los comandos libres del modelo corrian dentro del
sandbox, donde no hay ni el sistema ni los discos. Se arreglo (virtualization a
false), pero las 134 acciones del catalogo nunca se habian ejecutado todas
seguidas contra la maquina real: la suite prueba que las FRASES caen en la
accion correcta, no que el comando de cada accion haga algo util.
Que se ejecuta y que no:
- Las destructivas (`confirmar: true`) y las que piden root NO se ejecutan.
En uso normal pasan por un dialogo que aprueba una persona; aqui no hay
persona, y aprobarlas solas seria justo lo que el diseño evita.
- Las que abren ventanas (firefox, xdg-open) se ejecutan solo con --abrir,
porque llenan la pantalla. Sin esa opcion se comprueba que el programa
existe, que es lo que puede fallar.
- El resto se ejecuta entero y se mira que conteste algo con sentido.
python repasar_acciones.py [--abrir] [--grupo sistema] [--json salida.json]
"""
import argparse
import json
import os
import re
import subprocess
import sys
import time
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CATALOGO = os.path.join(RAIZ, "newelle", "data", "acciones", "acciones.json")
# Argumentos de mentira para las acciones que capturan uno. Se eligen inocuos:
# nada que borre, mueva o mande nada fuera.
ARGUMENTOS = {
"buscar_fichero": "README.md",
"buscar_carpeta": "config",
"grep_recursivo": "JARVIS",
"leer_fichero": "README.md", # un NOMBRE, que es lo que se dice por voz
"abrir_carpeta": RAIZ,
"tamano_carpeta": os.path.join(RAIZ, "config"),
"buscar_youtube": "musica",
"buscar_wikipedia": "madrid",
"buscar_github": "newelle",
"traducir": "hola",
"buscar_web": "que hora es",
"matar_proceso": "proceso_que_no_existe_jarvis",
"ping": "127.0.0.1",
}
POR_DEFECTO = "prueba"
# Lo que abre o MUEVE ventanas. ventanas.sh reordena el escritorio del usuario:
# ejecutarlo en un repaso automatico le cambia las ventanas de sitio sin avisar.
VENTANA = re.compile(r"\b(firefox|xdg-open|gnome-terminal|nautilus|vlc|mpv|eog|"
r"gedit|gnome-text-editor|totem|ventanas\.sh|xdotool|wmctrl)\b")
def carga():
with open(CATALOGO, encoding="utf-8") as f:
d = json.load(f)
return d.get("acciones", d if isinstance(d, list) else [])
def prefijo_host():
"""Las acciones llevan host:true y en uso salen del sandbox. Aqui ya
estamos fuera, asi que no hace falta prefijo."""
return []
def ejecuta(accion, argumento, tope=25):
import shlex
import urllib.parse
comando = accion.get("comando") or ""
comando = comando.replace("{argumento}", shlex.quote(argumento))
comando = comando.replace("{argumento_url}",
shlex.quote(urllib.parse.quote_plus(argumento)))
t0 = time.monotonic()
try:
r = subprocess.run(["bash", "-lc", comando], capture_output=True,
text=True, errors="replace", timeout=tope)
salida = (r.stdout or r.stderr or "").strip()
return r.returncode, salida, time.monotonic() - t0
except subprocess.TimeoutExpired:
return -1, f"(no termino en {tope}s)", time.monotonic() - t0
except OSError as e:
return -2, f"(no se pudo ejecutar: {e})", time.monotonic() - t0
def programa_existe(comando):
"""El primer ejecutable de la orden, para las que abren o mueven ventanas.
Algunos son ordenes del PATH (firefox) y otros son scripts nuestros con
ruta completa (config/ventanas.sh): `command -v` solo sirve para los
primeros, y con los segundos hay que mirar el fichero.
"""
m = VENTANA.search(comando)
if not m:
return None
nombre = m.group(1)
if nombre.endswith(".sh"):
# sacar la ruta entera tal como aparece en el comando
for trozo in comando.split():
if trozo.endswith(nombre):
ruta = os.path.expandvars(os.path.expanduser(trozo))
return os.access(ruta, os.X_OK)
return False
return subprocess.run(["bash", "-lc", f"command -v {nombre}"],
capture_output=True).returncode == 0
def main():
p = argparse.ArgumentParser()
p.add_argument("--abrir", action="store_true",
help="ejecutar tambien las que abren ventanas")
p.add_argument("--grupo")
p.add_argument("--json")
p.add_argument("--tope", type=int, default=25)
args = p.parse_args()
acciones = carga()
if args.grupo:
acciones = [a for a in acciones if a.get("grupo") == args.grupo]
resultados = []
print(f" repasando {len(acciones)} acciones"
f"{' del grupo ' + args.grupo if args.grupo else ''}\n", flush=True)
for a in acciones:
ident = a.get("id", "?")
comando = a.get("comando") or ""
arg = ARGUMENTOS.get(ident, POR_DEFECTO if a.get("captura") else "")
if not comando:
estado, detalle = "solo habla", a.get("acuse", "")[:50]
elif a.get("confirmar") or "sudo" in comando:
# no se aprueban solas: en uso las aprueba una persona
estado, detalle = "saltada", "destructiva o con root"
elif VENTANA.search(comando) and not args.abrir:
hay = programa_existe(comando)
estado = "programa ok" if hay else "FALTA PROGRAMA"
detalle = VENTANA.search(comando).group(1)
else:
codigo, salida, tardo = ejecuta(a, arg, args.tope)
una_linea = " ".join(salida.split())[:70]
if codigo == 0:
estado = "ok" if salida else "ok (sin salida)"
else:
estado = f"FALLO ({codigo})"
detalle = f"{tardo:4.1f}s {una_linea}"
resultados.append({"id": ident, "grupo": a.get("grupo"),
"estado": estado, "detalle": detalle})
marca = "!" if "FALLO" in estado or "FALTA" in estado else " "
print(f" {marca} {ident:26s} {estado:16s} {detalle}", flush=True)
print()
from collections import Counter
c = Counter(r["estado"].split(" (")[0].split(" ")[0] for r in resultados)
for k, v in c.most_common():
print(f" {v:3d} {k}")
malas = [r for r in resultados if "FALLO" in r["estado"] or "FALTA" in r["estado"]]
print(f"\n {len(resultados) - len(malas)} de {len(resultados)} responden bien")
if malas:
print(" fallan: " + ", ".join(r["id"] for r in malas))
if args.json:
with open(args.json, "w", encoding="utf-8") as f:
json.dump(resultados, f, ensure_ascii=False, indent=2)
return 1 if malas else 0
if __name__ == "__main__":
sys.exit(main())