carril verde (Hermes Agent): documentacion, panel, voz local y RAG
Rama JARVIS-GREEN, independiente de master (el nucleo naranja queda intacto). - README propio del carril verde con diagrama de arquitectura y capturas del panel. - verde/: arranque, panel web, puente de voz (escucha local con faster-whisper + habla con la voz del naranja), pruebas (07 voz, 08 escucha), config y notas. - Integracion RAG: nucleo/saber/busca_cli.py + skill buscar-en-apuntes, para que el agente consulte el mismo indice que el nucleo. - Todo local (127.0.0.1); sin datos personales (rutas y modelo de GPU scrubeados).
This commit is contained in:
parent
b1f8118272
commit
1e3e5ca20d
32 changed files with 3697 additions and 161 deletions
74
verde/pruebas/01_gpu.sh
Executable file
74
verde/pruebas/01_gpu.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
# La tarjeta, y quien la esta usando.
|
||||
#
|
||||
# Va la primera porque es la que explica los fallos de las demas. En 4 GB no
|
||||
# caben el verde y el naranja a la vez: si el naranja esta cargado, el verde
|
||||
# dara "cudaMalloc failed" y parecera que el modelo esta roto cuando lo que
|
||||
# pasa es que no hay sitio.
|
||||
#
|
||||
# El caso que mas ha costado en este proyecto es whisper-server: lo lanza
|
||||
# flatpak-spawn --host, vive FUERA del sandbox y no se entera de que la app
|
||||
# cerro. Cada reinicio deja uno agarrado a ~850 MiB. Dos o tres y ya no cabe
|
||||
# nada. Por eso se cuentan, no solo se mira si hay alguno.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
titulo "La tarjeta"
|
||||
|
||||
USADA=$(vram)
|
||||
if [ -z "$USADA" ]; then
|
||||
echo " $(rojo FALLO) no hay nvidia-smi: esto necesita la grafica"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOTAL=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader | tr -d ' MiB')
|
||||
nota "$USADA MiB usados de $TOTAL"
|
||||
nvidia-smi --query-compute-apps=used_memory,process_name --format=csv,noheader 2>/dev/null \
|
||||
| sed 's/^/ /'
|
||||
|
||||
titulo "Nadie compitiendo por ella"
|
||||
|
||||
# pgrep -c YA imprime 0 cuando no encuentra nada, y ademas devuelve 1. Un
|
||||
# "|| echo 0" detras parece prudente y lo que hace es imprimir DOS ceros, que
|
||||
# no son iguales a "0" y hacen fallar una prueba que en realidad pasa.
|
||||
cuantos() { local n; n=$(pgrep -cf "$1" 2>/dev/null); echo "${n:-0}"; }
|
||||
cuantos_x() { local n; n=$(pgrep -cx "$1" 2>/dev/null); echo "${n:-0}"; }
|
||||
|
||||
# dataset.py genera pares con el NARANJA: mientras corra, ollama recarga el
|
||||
# modelo naranja en segundos aunque se le haga stop. No es un fallo de ollama.
|
||||
comprueba "dataset.py no esta corriendo" "$(cuantos 'entrena/dataset.py')" "0"
|
||||
|
||||
comprueba "el nucleo naranja no esta corriendo" "$(cuantos 'nucleo/jarvis.py')" "0"
|
||||
|
||||
comprueba "sin whisper-server huerfanos" "$(cuantos_x whisper-server)" "0"
|
||||
|
||||
CARGADO=$(curl -fsS --max-time 5 "$OLLAMA/api/ps" 2>/dev/null | python3 -c "
|
||||
import json, sys
|
||||
try: m = json.load(sys.stdin).get('models', [])
|
||||
except Exception: m = []
|
||||
print(','.join(x['name'] for x in m) or 'ninguno')
|
||||
" 2>/dev/null)
|
||||
nota "en ollama ahora: $CARGADO"
|
||||
|
||||
case "$CARGADO" in
|
||||
*"$NARANJA"*)
|
||||
echo " $(rojo FALLO) el naranja esta cargado: no cabe el verde encima"
|
||||
echo " $(gris "ollama stop $NARANJA")"
|
||||
_fallo=$((_fallo + 1)) ;;
|
||||
*)
|
||||
echo " $(verde OK) el naranja no ocupa la tarjeta"
|
||||
_ok=$((_ok + 1)) ;;
|
||||
esac
|
||||
|
||||
# Sitio para el verde: 2795 MiB medidos el 16 ago a num_ctx 65536 con 16 de 32
|
||||
# capas en GPU. La cifra anterior (3301) era la del modelo a 24576 con las 32
|
||||
# capas dentro, que Hermes rechaza por su suelo de 64k.
|
||||
#
|
||||
# Si el modelo ya esta dentro, la comprobacion no aplica: el hueco ya se uso.
|
||||
case "$CARGADO" in
|
||||
*"$MODELO"*) nota "el verde ya esta cargado, no hace falta hueco" ;;
|
||||
*) mayor_que "queda hueco para el verde (MiB libres)" \
|
||||
"$((TOTAL - USADA))" 2800 ;;
|
||||
esac
|
||||
|
||||
resumen
|
||||
62
verde/pruebas/02_modelo.sh
Executable file
62
verde/pruebas/02_modelo.sh
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env bash
|
||||
# Los dos modelos: que el verde tenga lo suyo y que el naranja siga intacto.
|
||||
#
|
||||
# La segunda mitad importa tanto como la primera. El verde es un banco de
|
||||
# pruebas y el naranja es lo que el usuario usa a diario: si tocar uno estropea
|
||||
# el otro, el experimento sale caro. Los cuatro parametros del naranja
|
||||
# (num_ctx 4096, num_gpu 24, num_predict 300, temperature 0.4) estan medidos y
|
||||
# elegidos —el num_gpu 24 deja 1540 MiB para whisper— asi que se comprueban uno
|
||||
# a uno y no "existe el modelo".
|
||||
#
|
||||
# Se pregunta a ollama, no al .Modelfile del repo: arrancar.sh RECREA el verde
|
||||
# mientras busca el techo de contexto, asi que el fichero es la intencion y
|
||||
# ollama es lo que se va a cargar de verdad.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
titulo "El verde"
|
||||
|
||||
if ! ollama list 2>/dev/null | grep -q "^$MODELO"; then
|
||||
echo " $(rojo FALLO) no existe $MODELO"
|
||||
echo " $(gris "creado por: verde/arrancar.sh, o a mano con qwen3.5-4b-verde.Modelfile")"
|
||||
exit 1
|
||||
fi
|
||||
echo " $(verde OK) $MODELO existe"; _ok=$((_ok + 1))
|
||||
|
||||
CTX=$(parametro "$MODELO" num_ctx)
|
||||
nota "num_ctx $CTX"
|
||||
|
||||
# El suelo NO lo pone la tarjeta ni el tamaño del prompt: lo pone Hermes, que
|
||||
# rechaza de plano cualquier modelo por debajo de 64.000 tokens
|
||||
# (agent/model_metadata.py:405, comprobado en agent/agent_init.py:2656).
|
||||
#
|
||||
# Esta comprobacion existe porque el carril se dimensiono entero para 24576
|
||||
# —lo maximo que cabia en la GPU con las 32 capas dentro— y ese numero, que
|
||||
# era correcto en todo lo demas, es rechazado antes de la primera frase. En
|
||||
# frio no se veia: solo salta al abrir Hermes.
|
||||
mayor_que "num_ctx llega al suelo de 64k que exige Hermes" "$CTX" 63999
|
||||
|
||||
# 300 tokens —lo que hereda el naranja del modelo base— cortan el JSON de una
|
||||
# tool call por la mitad. Es el tope escondido que casi hace concluir que el 4B
|
||||
# no sabe llamar herramientas.
|
||||
mayor_que "num_predict da para una tool call entera" \
|
||||
"$(parametro "$MODELO" num_predict)" 1024
|
||||
|
||||
# Emitir JSON no es escribir prosa: temperatura baja.
|
||||
menor_que "temperature baja para emitir JSON" \
|
||||
"$(parametro "$MODELO" temperature)" 0.5
|
||||
|
||||
titulo "El naranja, sin tocar"
|
||||
|
||||
comprueba "num_ctx del naranja" "$(parametro "$NARANJA" num_ctx)" "4096"
|
||||
comprueba "num_gpu del naranja" "$(parametro "$NARANJA" num_gpu)" "24"
|
||||
comprueba "num_predict del naranja" "$(parametro "$NARANJA" num_predict)" "300"
|
||||
comprueba "temperature del naranja" "$(parametro "$NARANJA" temperature)" "0.4"
|
||||
|
||||
# Los dos salen del mismo blob de pesos: si dejaran de compartirlo, alguien
|
||||
# habria bajado un modelo distinto y las comparaciones no valdrian.
|
||||
BLOB_V=$(ollama show --modelfile "$MODELO" 2>/dev/null | awk '$1=="FROM"{print $2; exit}')
|
||||
BLOB_N=$(ollama show --modelfile "$NARANJA" 2>/dev/null | awk '$1=="FROM"{print $2; exit}')
|
||||
comprueba "los dos carriles usan los mismos pesos" "$BLOB_V" "$BLOB_N"
|
||||
|
||||
resumen
|
||||
136
verde/pruebas/03_config.sh
Executable file
136
verde/pruebas/03_config.sh
Executable file
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env bash
|
||||
# La configuracion de Hermes, PARSEADA.
|
||||
#
|
||||
# Esta prueba existe por un fallo concreto que casi cuela. El config.yaml que
|
||||
# genera el instalador declara `provider` y `base_url` DOS VECES dentro del
|
||||
# bloque model:, una arriba y otra ~45 lineas mas abajo. En YAML gana la ultima.
|
||||
# Al configurar Ollama en las de arriba el fichero PARECIA correcto leyendolo, y
|
||||
# al parsearlo salia provider=auto, base_url=https://openrouter.ai/api/v1: el
|
||||
# carril habria salido por OpenRouter creyendo nosotros que iba a localhost.
|
||||
#
|
||||
# Con 1890 lineas y 121 activas, leer el fichero no demuestra nada. Aqui todo se
|
||||
# comprueba con yaml.safe_load, que es lo mismo que hace Hermes al arrancar.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
titulo "El cerebro"
|
||||
|
||||
afirma "existe $CONFIG" test -f "$CONFIG"
|
||||
|
||||
comprueba "el modelo por defecto es el verde" "$(config_model default)" "$MODELO"
|
||||
|
||||
# Sin esto Hermes se cree lo que dice /api/show, que son 262144 —el maximo del
|
||||
# modelo— y no los 24576 configurados. El sintoma serian respuestas truncadas y
|
||||
# llamadas rotas, y la conclusion natural ("el 4B no vale") seria falsa.
|
||||
CTX_YAML=$(config_model context_length)
|
||||
CTX_REAL=$(parametro "$MODELO" num_ctx)
|
||||
comprueba "Hermes sabe el contexto REAL, no el que reporta /api/show" \
|
||||
"$CTX_YAML" "$CTX_REAL"
|
||||
|
||||
# Los dos numeros tienen que ser iguales Y llegar al suelo de 64k. Poner aqui
|
||||
# 65536 con el modelo a 24576 pasaria el arranque y romperia en marcha: Hermes
|
||||
# no comprimiria hasta acercarse a 64k y ollama iria tirando los tokens mas
|
||||
# viejos, que son el prompt de sistema y los esquemas de las herramientas. El
|
||||
# agente dejaria de llamar herramientas a mitad de sesion, sin un solo error.
|
||||
mayor_que "y ese contexto llega al suelo de Hermes" "${CTX_YAML:-0}" 63999
|
||||
|
||||
BASE=$(config_model base_url)
|
||||
case "$BASE" in
|
||||
http://127.0.0.1:*|http://localhost:*)
|
||||
echo " $(verde OK) el cerebro apunta a esta maquina ($BASE)"; _ok=$((_ok + 1)) ;;
|
||||
*)
|
||||
echo " $(rojo FALLO) base_url NO es local: $BASE"; _fallo=$((_fallo + 1)) ;;
|
||||
esac
|
||||
|
||||
titulo "Cero nube"
|
||||
|
||||
# Se recorre el YAML entero, no solo el bloque model:. Cualquier http(s) que no
|
||||
# apunte al propio equipo es una salida potencial y tiene que estar justificada.
|
||||
FUERA=$(python3 - "$CONFIG" <<'PY'
|
||||
import re, sys, yaml
|
||||
|
||||
LOCAL = re.compile(r'^https?://(127\.0\.0\.1|localhost|0\.0\.0\.0|\[::1\])')
|
||||
URL = re.compile(r'https?://[^\s"\']+')
|
||||
fuera = []
|
||||
|
||||
def anda(nodo, ruta=""):
|
||||
if isinstance(nodo, dict):
|
||||
for k, v in nodo.items():
|
||||
anda(v, f"{ruta}.{k}" if ruta else str(k))
|
||||
elif isinstance(nodo, list):
|
||||
for i, v in enumerate(nodo):
|
||||
anda(v, f"{ruta}[{i}]")
|
||||
elif isinstance(nodo, str):
|
||||
for u in URL.findall(nodo):
|
||||
if not LOCAL.match(u):
|
||||
fuera.append(f"{ruta} = {u}")
|
||||
|
||||
anda(yaml.safe_load(open(sys.argv[1])))
|
||||
for f in fuera:
|
||||
print(f)
|
||||
PY
|
||||
)
|
||||
if [ -z "$FUERA" ]; then
|
||||
echo " $(verde OK) ninguna URL de nube activa en el config"; _ok=$((_ok + 1))
|
||||
else
|
||||
echo " $(rojo FALLO) hay URLs que salen de esta maquina:"
|
||||
printf '%s\n' "$FUERA" | sed 's/^/ /'
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
|
||||
titulo "Desde donde mira el agente"
|
||||
|
||||
# `cwd: "."` de fabrica NO es "el directorio desde el que lanzas hermes", por
|
||||
# mucho que lo diga su documentacion: el agente arranca en $HOME. Con la raiz
|
||||
# ahi, "lee verde/README.md" acaba leyendo $HOME/README.md —el de
|
||||
# Gitleaks— y resumiendolo perfectamente. Ningun error, respuesta inutil.
|
||||
CWD_AGENTE=$(python3 -c "
|
||||
import yaml
|
||||
print(yaml.safe_load(open('$CONFIG')).get('terminal', {}).get('cwd', ''))" 2>/dev/null)
|
||||
comprueba "el agente arranca en la raiz del proyecto" "$CWD_AGENTE" "$RAIZ"
|
||||
|
||||
titulo "El lanzador entra donde debe"
|
||||
|
||||
# Un `cd` antes de llamar a hermes NO basta: Hermes restaura el directorio que
|
||||
# tenga apuntado en la sesion. El sintoma no parece de rutas — se le pide "lee
|
||||
# verde/README.md y resumelo" y devuelve un resumen impecable de OTRO fichero,
|
||||
# sin un solo error — asi que se comprueba estaticamente.
|
||||
if grep -q -- '--in "\$RAIZ"' "$VERDE/verde-launcher.sh"; then
|
||||
echo " $(verde OK) verde-launcher.sh pasa --in y no confia en el cd"; _ok=$((_ok + 1))
|
||||
else
|
||||
echo " $(rojo FALLO) verde-launcher.sh no pasa --in: Hermes abrira en otro sitio"
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
|
||||
titulo "La voz apunta a la del naranja"
|
||||
|
||||
# Sin esto la comparacion de voz entre carriles no diria nada: Hermes se
|
||||
# descargaria su propia voz de Piper (la GPL de OHF-Voice) en vez de usar el
|
||||
# binario MIT y el .onnx que ya tiene el naranja.
|
||||
ORDEN=$(python3 -c "
|
||||
import yaml
|
||||
t = yaml.safe_load(open('$CONFIG')).get('tts', {})
|
||||
p = t.get('providers', {}).get(t.get('provider', ''), {})
|
||||
print(p.get('command', ''))
|
||||
" 2>/dev/null)
|
||||
case "$ORDEN" in
|
||||
*/verde/voz-verde.sh*)
|
||||
echo " $(verde OK) TTS por comando -> voz-verde.sh"; _ok=$((_ok + 1)) ;;
|
||||
*)
|
||||
echo " $(rojo FALLO) el TTS no apunta al adaptador: $ORDEN"; _fallo=$((_fallo + 1)) ;;
|
||||
esac
|
||||
|
||||
stt_local() { # $1 = clave dentro de stt.local
|
||||
python3 -c "
|
||||
import yaml
|
||||
print(yaml.safe_load(open('$CONFIG')).get('stt', {}).get('local', {}).get('$1', ''))
|
||||
" 2>/dev/null | tr -d '\n'
|
||||
}
|
||||
|
||||
comprueba "la escucha es la local de Hermes" "$(stt_local model)" "small"
|
||||
|
||||
# 839 MiB de pico contra una tarjeta con 3301 MiB de modelo dentro: whisper
|
||||
# tiene que soltar la VRAM cuando calla, o la siguiente frase da OOM.
|
||||
menor_que "whisper suelta la VRAM tras callar (s)" "$(stt_local unload_after_idle_seconds)" 601
|
||||
|
||||
resumen
|
||||
105
verde/pruebas/04_tool_calling.sh
Executable file
105
verde/pruebas/04_tool_calling.sh
Executable file
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env bash
|
||||
# LA DECISIVA: ¿emite el 4B llamadas a herramienta bien formadas?
|
||||
#
|
||||
# Es la duda que dejo abierta todo el carril. La guia de Hermes dice que para
|
||||
# trabajo agentico completo hace falta gemma4:31b y marca SIN tool calling todo
|
||||
# lo de 9B para abajo. qwen3.5 no sale en su tabla y si lo hace —el commit
|
||||
# "el cerebro con tool calling nativo" lo demuestra— pero lo demuestra en el
|
||||
# arnes propio, que inyecta muchas menos definiciones por vuelta.
|
||||
#
|
||||
# Se pregunta al ENDPOINT directamente, sin Hermes de por medio, a proposito.
|
||||
# Junta, estas dos preguntas se contestan mal:
|
||||
#
|
||||
# ¿sabe el modelo emitir una llamada? <- esto mide esta prueba
|
||||
# ¿funciona Hermes? <- eso es la ESCALERA
|
||||
#
|
||||
# Si esto falla, no hay nada que depurar en Hermes. Si esto pasa y el TUI se
|
||||
# atraganta, el fallo esta en el arnes y se busca alli.
|
||||
#
|
||||
# Tres casos, no uno: uno que PIDE herramienta, uno que NO la necesita, y uno de
|
||||
# dos argumentos. Un modelo que llama siempre es tan inutil como uno que no
|
||||
# llama nunca, y con una sola pregunta no se distingue.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
titulo "Llamadas a herramienta"
|
||||
|
||||
HERRAMIENTAS='[
|
||||
{"type":"function","function":{
|
||||
"name":"ejecuta_comando",
|
||||
"description":"Ejecuta un comando de shell y devuelve su salida",
|
||||
"parameters":{"type":"object","properties":{"comando":{"type":"string"}},"required":["comando"]}}},
|
||||
{"type":"function","function":{
|
||||
"name":"escribe_fichero",
|
||||
"description":"Escribe texto en un fichero del disco",
|
||||
"parameters":{"type":"object","properties":{
|
||||
"ruta":{"type":"string"},"contenido":{"type":"string"}},"required":["ruta","contenido"]}}}
|
||||
]'
|
||||
|
||||
# pregunta <descripcion> <texto> <herramienta esperada, o "" si no debe llamar>
|
||||
pregunta() {
|
||||
local desc=$1 texto=$2 espera=$3 cuerpo r
|
||||
cuerpo=$(python3 -c "
|
||||
import json, sys
|
||||
print(json.dumps({
|
||||
'model': '$MODELO',
|
||||
'messages': [{'role': 'user', 'content': sys.argv[1]}],
|
||||
'tools': json.loads(sys.argv[2]),
|
||||
}))" "$texto" "$HERRAMIENTAS")
|
||||
|
||||
r=$(curl -s --max-time 300 "$OLLAMA/v1/chat/completions" \
|
||||
-H 'Content-Type: application/json' -d "$cuerpo" 2>/dev/null)
|
||||
|
||||
local salida
|
||||
salida=$(printf '%s' "$r" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print('ILEGIBLE|'); raise SystemExit
|
||||
if 'error' in d:
|
||||
print('ERROR|' + str(d['error'])[:120]); raise SystemExit
|
||||
m = d['choices'][0]['message']
|
||||
tc = m.get('tool_calls') or []
|
||||
if not tc:
|
||||
print('PROSA|' + (m.get('content') or '')[:90].replace('\n', ' ')); raise SystemExit
|
||||
f = tc[0]['function']
|
||||
try:
|
||||
args = json.loads(f['arguments'])
|
||||
except Exception:
|
||||
print('ROTO|' + str(f['arguments'])[:90]); raise SystemExit
|
||||
print(f\"{f['name']}|\" + json.dumps(args, ensure_ascii=False)[:90])
|
||||
")
|
||||
local nombre=${salida%%|*} detalle=${salida#*|}
|
||||
|
||||
if [ -z "$espera" ]; then
|
||||
# aqui lo correcto es NO llamar
|
||||
if [ "$nombre" = PROSA ]; then
|
||||
printf ' %s %s\n' "$(verde OK)" "$desc"
|
||||
printf ' %s\n' "$(gris "contesto: $detalle")"
|
||||
_ok=$((_ok + 1))
|
||||
else
|
||||
printf ' %s %s\n' "$(rojo FALLO)" "$desc"
|
||||
printf ' llamo a %s cuando no hacia falta: %s\n' "$nombre" "$detalle"
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$nombre" = "$espera" ]; then
|
||||
printf ' %s %s\n' "$(verde OK)" "$desc"
|
||||
printf ' %s\n' "$(gris "$nombre $detalle")"
|
||||
_ok=$((_ok + 1))
|
||||
else
|
||||
printf ' %s %s\n' "$(rojo FALLO)" "$desc"
|
||||
printf ' esperado: %s\n obtenido: %s %s\n' "$espera" "$nombre" "$detalle"
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
pregunta "llama cuando hace falta" "¿Cuánto espacio libre queda en el disco?" "ejecuta_comando"
|
||||
pregunta "acierta la herramienta y sus DOS argumentos" \
|
||||
"Guarda la palabra hola en el fichero /tmp/prueba-verde.txt" "escribe_fichero"
|
||||
pregunta "NO llama cuando no hace falta" "¿Cuánto son dos más dos? Contesta solo con el número." ""
|
||||
|
||||
resumen
|
||||
92
verde/pruebas/05_cabe.sh
Executable file
92
verde/pruebas/05_cabe.sh
Executable file
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env bash
|
||||
# ¿Cabe la pregunta? El bloqueo que paro el primer arranque.
|
||||
#
|
||||
# No es que qwen3.5:4b rindiera mal: es que el prompt fijo de Hermes —26 KB de
|
||||
# sistema + 56 KB de esquemas de 19 herramientas— son ~20.500 tokens contra los
|
||||
# 4.096 de num_ctx del naranja. Cinco veces por encima. Con eso, la pregunta del
|
||||
# usuario ni entra.
|
||||
#
|
||||
# DOS TRAMPAS que esta prueba evita, las dos costaron tiempo:
|
||||
#
|
||||
# 1. El directorio desde el que se lanza CAMBIA el prompt de sistema. Desde
|
||||
# ~/.hermes/hermes-agent son 55.261 B en vez de 26.281: se cuela el AGENTS.md
|
||||
# de 81 KB del propio repo de Hermes como fichero de contexto. Por eso aqui
|
||||
# se hace cd a la raiz del proyecto, que es lo que hace verde-launcher.sh.
|
||||
#
|
||||
# 2. El contexto que se compara es el del MODELFILE, no el de /api/show. Ollama
|
||||
# reporta ahi 262144, el maximo del modelo, no lo configurado.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
titulo "El prompt fijo, medido"
|
||||
|
||||
SALIDA=$TMP/prompt-size.txt
|
||||
( cd "$RAIZ" && timeout 180 hermes prompt-size ) > "$SALIDA" 2>&1
|
||||
if [ $? -ne 0 ] || ! grep -q 'System prompt total' "$SALIDA"; then
|
||||
echo " $(rojo FALLO) 'hermes prompt-size' no dio una medida"
|
||||
sed 's/^/ /' "$SALIDA" | head -5
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Se parsea con python y no con awk por el awk de Debian: mawk no admite el
|
||||
# match() de tres argumentos de gawk y devolveria vacio en silencio, que en una
|
||||
# resta da un numero plausible y equivocado.
|
||||
#
|
||||
# " System prompt total : 26,278 B (25.7 KB, 26,148 chars)"
|
||||
# " Tool schemas : 55,978 B (54.7 KB, 19 tools)"
|
||||
leer=$(python3 - "$SALIDA" <<'PY'
|
||||
import re, sys
|
||||
texto = open(sys.argv[1]).read()
|
||||
|
||||
def bytes_de(etiqueta):
|
||||
m = re.search(re.escape(etiqueta) + r'\s*:\s*([\d,]+)\s*B', texto)
|
||||
return int(m.group(1).replace(',', '')) if m else 0
|
||||
|
||||
herr = re.search(r'([\d,]+)\s+tools', texto)
|
||||
print(bytes_de('System prompt total'), bytes_de('Tool schemas'),
|
||||
herr.group(1).replace(',', '') if herr else '?')
|
||||
PY
|
||||
)
|
||||
read -r SISTEMA ESQUEMAS HERRAM <<<"$leer"
|
||||
|
||||
if [ "${SISTEMA:-0}" -eq 0 ] || [ "${ESQUEMAS:-0}" -eq 0 ]; then
|
||||
echo " $(rojo FALLO) no pude leer las cifras de 'hermes prompt-size'"
|
||||
sed 's/^/ /' "$SALIDA" | head -8
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FIJO=$((SISTEMA + ESQUEMAS))
|
||||
# ~4 B por token en castellano y JSON. Es la misma regla con la que se dimensiono
|
||||
# el modelo verde; sirve para decidir, no para presumir de precision.
|
||||
TOKENS=$((FIJO / 4))
|
||||
|
||||
nota "sistema $SISTEMA B"
|
||||
nota "esquemas $ESQUEMAS B ($HERRAM herramientas)"
|
||||
nota "fijo $FIJO B ~ $TOKENS tokens"
|
||||
|
||||
titulo "Contra el contexto configurado"
|
||||
|
||||
CTX=$(parametro "$MODELO" num_ctx)
|
||||
nota "num_ctx del Modelfile: $CTX"
|
||||
|
||||
menor_que "el prompt fijo cabe en el contexto" "$TOKENS" "$CTX"
|
||||
|
||||
# Que quepa no basta: si no sobra sitio, Hermes comprime el historial en cuanto
|
||||
# empiece la conversacion y el agente se olvida de lo que acaba de hacer. El
|
||||
# suelo son los 2000 que usa arrancar.sh para avisar.
|
||||
HUECO=$((CTX - TOKENS))
|
||||
nota "quedan ~$HUECO tokens para la conversacion"
|
||||
mayor_que "queda sitio para conversar" "$HUECO" 2000
|
||||
|
||||
# El otro tope, el escondido: 300 tokens de num_predict cortan el JSON de una
|
||||
# tool call por la mitad. El naranja lo hereda del modelo base y es correcto
|
||||
# para el —respuestas habladas cortas—, pero aqui seria un fallo silencioso.
|
||||
mayor_que "la respuesta no se corta a media llamada" \
|
||||
"$(parametro "$MODELO" num_predict)" 1024
|
||||
|
||||
titulo "De donde viene el numero"
|
||||
|
||||
nota "medido con cd a $RAIZ"
|
||||
nota "desde ~/.hermes/hermes-agent saldrian ~30 KB mas (su AGENTS.md de 81 KB)"
|
||||
|
||||
resumen
|
||||
90
verde/pruebas/06_privacidad.sh
Executable file
90
verde/pruebas/06_privacidad.sh
Executable file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env bash
|
||||
# Que siga siendo local. Es la premisa del carril, asi que se verifica igual que
|
||||
# cualquier otra cosa en vez de darla por hecha.
|
||||
#
|
||||
# Misma idea que pruebas/06_privacidad.sh del naranja, con las preguntas que
|
||||
# aplican aqui: alli se interroga a gsettings, aqui al .env, al YAML y a ss.
|
||||
#
|
||||
# Lo que SI puede salir, y no es fallo, se lista al final para que no sorprenda:
|
||||
# el navegador si el agente decide navegar, y la comprobacion de versiones.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
titulo "Credenciales"
|
||||
|
||||
# Las 11 lineas con valor del .env no son claves: son TERMINAL_TIMEOUT,
|
||||
# BROWSER_SESSION_TIMEOUT, banderas de depuracion y similares. Se busca por el
|
||||
# NOMBRE de la variable, que es lo que distingue una clave de un ajuste.
|
||||
CLAVES=$(grep -vE '^\s*#|^\s*$' "$HERMES/.env" 2>/dev/null \
|
||||
| grep -iE '^[A-Z_]*(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[A-Z_]*=.+' \
|
||||
| grep -vE '=\s*$' | wc -l)
|
||||
comprueba "cero credenciales con valor en el .env" "$CLAVES" "0"
|
||||
|
||||
# Sin tokens de mensajeria el gateway no arranca, que es justo lo que se quiere:
|
||||
# un canal de mensajeria es la unica pieza del carril que sacaria conversaciones
|
||||
# de esta maquina.
|
||||
MENSAJERIA=$(grep -vE '^\s*#|^\s*$' "$HERMES/.env" 2>/dev/null \
|
||||
| grep -icE '^(TELEGRAM|DISCORD|SLACK|WHATSAPP|SIGNAL|TEAMS|QQ)[A-Z_]*=.+')
|
||||
comprueba "cero tokens de mensajeria" "$MENSAJERIA" "0"
|
||||
|
||||
titulo "La red"
|
||||
|
||||
# Ollama es el cerebro: tiene que escuchar, pero solo aqui.
|
||||
FUERA=$(ss -ltn 2>/dev/null | awk 'NR>1 {print $4}' \
|
||||
| grep -E '^(0\.0\.0\.0|\*|\[::\]):(11434|8000|8080|3000)$' | wc -l)
|
||||
comprueba "ningun puerto nuestro escucha fuera de loopback" "$FUERA" "0"
|
||||
|
||||
OLLAMA_LOCAL=$(ss -ltn 2>/dev/null | awk 'NR>1 {print $4}' | grep -c '127.0.0.1:11434')
|
||||
comprueba "ollama escucha SOLO en 127.0.0.1" "$OLLAMA_LOCAL" "1"
|
||||
|
||||
# Conexiones salientes vivas de hermes en este momento. Cero es lo normal: la
|
||||
# 04 y la 05 hablan con 127.0.0.1.
|
||||
SALIENTES=$(ss -tnp 2>/dev/null | grep -c 'hermes' || true)
|
||||
nota "conexiones de hermes ahora mismo: ${SALIENTES:-0}"
|
||||
|
||||
titulo "El cerebro no sale de aqui"
|
||||
|
||||
BASE=$(config_model base_url)
|
||||
case "$BASE" in
|
||||
http://127.0.0.1:*|http://localhost:*)
|
||||
echo " $(verde OK) inferencia en esta maquina ($BASE)"; _ok=$((_ok + 1)) ;;
|
||||
*)
|
||||
echo " $(rojo FALLO) la inferencia sale a $BASE"; _fallo=$((_fallo + 1)) ;;
|
||||
esac
|
||||
|
||||
# telemetry es de Hermes y viene en el config; si se enciende, sale. No es una
|
||||
# bandera suelta sino un arbol —hoy telemetry.shared_metrics.enabled— asi que se
|
||||
# recorre entero: leer solo la clave de arriba devolvia el diccionario y la
|
||||
# prueba fallaba con la telemetria APAGADA, que es la peor forma de fallar.
|
||||
ENCENDIDAS=$(python3 - "$CONFIG" <<'PY'
|
||||
import sys, yaml
|
||||
|
||||
encendidas = []
|
||||
|
||||
def anda(nodo, ruta="telemetry"):
|
||||
if isinstance(nodo, dict):
|
||||
for k, v in nodo.items():
|
||||
if k == "enabled" and v is True:
|
||||
encendidas.append(ruta)
|
||||
else:
|
||||
anda(v, f"{ruta}.{k}")
|
||||
elif nodo is True and ruta.endswith("enabled"):
|
||||
encendidas.append(ruta)
|
||||
|
||||
anda(yaml.safe_load(open(sys.argv[1])).get('telemetry', {}) or {})
|
||||
print(",".join(encendidas))
|
||||
PY
|
||||
)
|
||||
if [ -z "$ENCENDIDAS" ]; then
|
||||
echo " $(verde OK) telemetria apagada en todas sus ramas"; _ok=$((_ok + 1))
|
||||
else
|
||||
echo " $(rojo FALLO) telemetria encendida en: $ENCENDIDAS"; _fallo=$((_fallo + 1))
|
||||
fi
|
||||
|
||||
titulo "Salidas conocidas, que no son fallo"
|
||||
|
||||
nota "el navegador (Playwright/Chromium) SOLO si el agente decide navegar"
|
||||
nota "hermes update comprueba versiones contra su repo cuando se le pide"
|
||||
nota "el driver de Computer Use esta INSTALADO pero nada lo invoca hoy"
|
||||
|
||||
resumen
|
||||
88
verde/pruebas/07_voz.sh
Executable file
88
verde/pruebas/07_voz.sh
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env bash
|
||||
# Que el verde hable, y que hable IGUAL que el naranja.
|
||||
#
|
||||
# No es un detalle estetico. Si el verde sonara con una voz que Piper se
|
||||
# descarga por su cuenta —la GPL de OHF-Voice que trae Hermes de serie—,
|
||||
# comparar los dos carriles no diria nada: la mitad de la impresion de un
|
||||
# asistente de voz es el timbre.
|
||||
#
|
||||
# Por eso el TTS de Hermes va por comando (type: command, sin documentar en su
|
||||
# web pero si en tools/tts_tool.py) contra voz-verde.sh, que es un adaptador de
|
||||
# 15 lineas al mismo config/jarvis-piper.sh del naranja: mismo binario Piper
|
||||
# 1.2.0 MIT, mismo .onnx, misma cadena de audio.
|
||||
#
|
||||
# La prueba llama al adaptador EXACTAMENTE como lo llama Hermes: dos argumentos,
|
||||
# el WAV de salida y un FICHERO con el texto. Es donde estaba la trampa —
|
||||
# jarvis-piper.sh quiere el texto como argumento— y una interfaz mal encajada no
|
||||
# se ve hasta que suena mal.
|
||||
#
|
||||
# No suena nada: se genera el fichero y se mide.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
titulo "El adaptador"
|
||||
|
||||
afirma "existe voz-verde.sh" test -x "$VERDE/voz-verde.sh"
|
||||
afirma "existe jarvis-piper.sh del naranja" test -x "$RAIZ/config/jarvis-piper.sh"
|
||||
|
||||
titulo "Generar"
|
||||
|
||||
TEXTO=$TMP/texto.txt
|
||||
WAV=$TMP/voz.wav
|
||||
rm -f "$WAV"
|
||||
printf 'A sus órdenes, señor. Todos los sistemas responden con normalidad.' > "$TEXTO"
|
||||
|
||||
INICIO=$(date +%s.%N)
|
||||
"$VERDE/voz-verde.sh" "$WAV" "$TEXTO" >"$TMP/voz.log" 2>&1
|
||||
SALIO=$?
|
||||
TARDO=$(awk "BEGIN{printf \"%.2f\", $(date +%s.%N) - $INICIO}")
|
||||
|
||||
if [ $SALIO -ne 0 ] || [ ! -s "$WAV" ]; then
|
||||
echo " $(rojo FALLO) voz-verde.sh no genero el WAV (codigo $SALIO)"
|
||||
sed 's/^/ /' "$TMP/voz.log" | head -8
|
||||
exit 1
|
||||
fi
|
||||
echo " $(verde OK) WAV generado en ${TARDO}s"; _ok=$((_ok + 1))
|
||||
|
||||
titulo "El fichero"
|
||||
|
||||
# soxi/ffprobe no estan garantizados: se lee la cabecera RIFF a mano, que son
|
||||
# cuatro campos en posiciones fijas y no depende de nada instalado.
|
||||
leer=$(python3 - "$WAV" <<'PY'
|
||||
import struct, sys, wave
|
||||
with wave.open(sys.argv[1]) as w:
|
||||
print(w.getframerate(), w.getnchannels(), w.getsampwidth() * 8,
|
||||
round(w.getnframes() / w.getframerate(), 2))
|
||||
PY
|
||||
)
|
||||
read -r HZ CANALES BITS SEGUNDOS <<<"$leer"
|
||||
|
||||
comprueba "22050 Hz, como el naranja" "$HZ" "22050"
|
||||
comprueba "mono" "$CANALES" "1"
|
||||
comprueba "16 bits" "$BITS" "16"
|
||||
|
||||
# Frase de ~4 s. Si sale mucho mas corta, Piper se comio el texto; si sale mucho
|
||||
# mas larga, el tono esta mal y sonaria a camara lenta.
|
||||
mayor_que "dura lo que una frase (s)" "$SEGUNDOS" 2
|
||||
menor_que "y no se alarga de mas (s)" "$SEGUNDOS" 12
|
||||
|
||||
# Tiempo real: 0,34 s para 3,2 s de audio, medido el 15 ago. Es CPU, no toca la
|
||||
# grafica, asi que no compite con el modelo.
|
||||
RATIO=$(awk "BEGIN{printf \"%.2f\", $TARDO / $SEGUNDOS}")
|
||||
nota "sintesis $TARDO s para $SEGUNDOS s de audio (x$RATIO del tiempo real)"
|
||||
menor_que "sintetiza mas rapido de lo que dura" "$RATIO" 1
|
||||
|
||||
titulo "La misma voz que el naranja"
|
||||
|
||||
# voz-verde.sh copia al NUCLEO (nucleo/boca/voz.py: POR_DEFECTO = "davefx"), no
|
||||
# al README, que describe sharvard y es del JARVIS sobre Newelle. Se compara
|
||||
# contra el nucleo porque es contra quien se va a comparar el carril.
|
||||
VOZ_VERDE=$(grep -oP 'JARVIS_PIPER_VOZ:-\K[^}]+' "$VERDE/voz-verde.sh")
|
||||
VOZ_NUCLEO=$(grep -oP 'POR_DEFECTO\s*=\s*"\K[^"]+' "$RAIZ/nucleo/boca/voz.py" 2>/dev/null)
|
||||
if [ -z "$VOZ_NUCLEO" ]; then
|
||||
nota "no pude leer la voz del nucleo; comparacion omitida"
|
||||
else
|
||||
comprueba "misma voz que el nucleo" "$VOZ_VERDE" "es_ES-${VOZ_NUCLEO}-medium"
|
||||
fi
|
||||
|
||||
resumen
|
||||
73
verde/pruebas/08_escucha.sh
Executable file
73
verde/pruebas/08_escucha.sh
Executable file
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env bash
|
||||
# Que el verde ESCUCHE, y en local. El complemento del 07 (que hable).
|
||||
#
|
||||
# El TUI de Hermes no tiene microfono, y su STT nativo es para notas de voz de
|
||||
# mensajeria —que saldrian de la maquina—. El puente escucha-verde.sh cierra el
|
||||
# hueco con faster-whisper local (el mismo 'small' en espanol que ya declara la
|
||||
# seccion stt: del config.yaml), en CPU para no pelear por la VRAM con el 4B.
|
||||
#
|
||||
# La prueba no necesita microfono: hace un BUCLE. Genera voz con el mismo TTS del
|
||||
# verde (voz-verde.sh, 07), la transcribe con escucha_transcribe.py, y comprueba
|
||||
# que el texto vuelve entero. Si el oido funciona, lo que entro por el altavoz
|
||||
# sale por el transcriptor.
|
||||
|
||||
cd "$(dirname "$0")" && . ./comun.sh
|
||||
|
||||
PY=$RAIZ/nucleo/venv/bin/python
|
||||
[ -x "$PY" ] || PY=$(command -v python3)
|
||||
TRANS=$VERDE/escucha_transcribe.py
|
||||
|
||||
titulo "Las piezas"
|
||||
|
||||
afirma "existe escucha_transcribe.py" test -f "$TRANS"
|
||||
afirma "existe el puente escucha-verde.sh" test -x "$VERDE/escucha-verde.sh"
|
||||
afirma "faster-whisper importa" "$PY" -c "import faster_whisper"
|
||||
afirma "el TTS del verde existe (para el bucle)" test -x "$VERDE/voz-verde.sh"
|
||||
|
||||
# quita acentos y baja a minusculas, para comparar sin depender de la tilde
|
||||
_norm() { "$PY" -c "import sys,unicodedata as u; t=u.normalize('NFD',sys.stdin.read().lower()); print(''.join(c for c in t if u.category(c)!='Mn'))"; }
|
||||
|
||||
titulo "El bucle voz -> texto"
|
||||
|
||||
# (frase dicha, palabras clave que TIENEN que volver). Se evita 'jarvis' a
|
||||
# proposito: es un nombre raro y whisper lo oye 'harvis'; eso no mide el oido.
|
||||
probar_frase() {
|
||||
local frase=$1 claves=$2
|
||||
local wav=$TMP/oir.wav txt=$TMP/oir.txt
|
||||
printf '%s' "$frase" > "$txt"
|
||||
rm -f "$wav"
|
||||
"$VERDE/voz-verde.sh" "$wav" "$txt" >/dev/null 2>&1
|
||||
if [ ! -s "$wav" ]; then
|
||||
echo " $(rojo FALLO) no se genero el WAV para: $frase"; _fallo=$((_fallo+1)); return
|
||||
fi
|
||||
local ini out norm falta=""
|
||||
ini=$(date +%s.%N)
|
||||
out=$(timeout 120 "$PY" "$TRANS" "$wav" 2>/dev/null)
|
||||
local tardo; tardo=$(awk "BEGIN{printf \"%.1f\", $(date +%s.%N) - $ini}")
|
||||
norm=$(printf '%s' "$out" | _norm)
|
||||
local c
|
||||
for c in $claves; do
|
||||
printf '%s' "$norm" | grep -q "$c" || falta="$falta $c"
|
||||
done
|
||||
if [ -z "$falta" ]; then
|
||||
printf ' %s escucho: "%s" (%ss)\n' "$(verde OK)" "$out" "$tardo"; _ok=$((_ok+1))
|
||||
else
|
||||
printf ' %s faltan [%s] en: "%s"\n' "$(rojo FALLO)" "$falta" "$out"; _fallo=$((_fallo+1))
|
||||
fi
|
||||
}
|
||||
|
||||
probar_frase "cuanto espacio libre queda en el disco" "espacio disco"
|
||||
probar_frase "abre el navegador y busca el tiempo en madrid" "navegador tiempo madrid"
|
||||
probar_frase "apaga las luces del salon a las diez" "luces salon"
|
||||
|
||||
titulo "Local de verdad"
|
||||
|
||||
# El transcriptor no debe abrir ningun socket a la nube: se comprueba que el
|
||||
# codigo no importa clientes de APIs de STT de pago.
|
||||
if grep -qiE "openai|groq|mistral|requests\.post|urllib.*http" "$TRANS"; then
|
||||
echo " $(rojo FALLO) el transcriptor referencia una API remota"; _fallo=$((_fallo+1))
|
||||
else
|
||||
echo " $(verde OK) el transcriptor no llama a ninguna API remota"; _ok=$((_ok+1))
|
||||
fi
|
||||
|
||||
resumen
|
||||
111
verde/pruebas/comun.sh
Executable file
111
verde/pruebas/comun.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
# Utilidades compartidas por las pruebas del verde. No se ejecuta suelto.
|
||||
#
|
||||
# Misma forma que pruebas/comun.sh del naranja —comprueba, afirma, resumen— para
|
||||
# que las dos suites se lean igual. Lo que cambia es a QUE se le pregunta: alli
|
||||
# es gsettings dentro de un flatpak, aqui es un YAML, ollama y un binario.
|
||||
#
|
||||
# Ninguna prueba de esta carpeta abre el TUI ni habla por el altavoz.
|
||||
|
||||
RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")/../.." && pwd)
|
||||
VERDE=$RAIZ/verde
|
||||
CONFIG=$HOME/.hermes/config.yaml
|
||||
HERMES=$HOME/.hermes
|
||||
OLLAMA=${OLLAMA_URL:-http://127.0.0.1:11434}
|
||||
MODELO=qwen3.5:4b-verde
|
||||
NARANJA=qwen3.5:4b-jarvis
|
||||
|
||||
TMP=${TMPDIR:-/tmp}/jarvis-verde-pruebas
|
||||
mkdir -p "$TMP"
|
||||
|
||||
_ok=0
|
||||
_fallo=0
|
||||
|
||||
verde() { printf '\033[32m%s\033[0m' "$1"; }
|
||||
rojo() { printf '\033[31m%s\033[0m' "$1"; }
|
||||
gris() { printf '\033[90m%s\033[0m' "$1"; }
|
||||
|
||||
# comprueba <descripcion> <valor obtenido> <valor esperado>
|
||||
comprueba() {
|
||||
local desc=$1 obtenido=$2 esperado=$3
|
||||
if [ "$obtenido" = "$esperado" ]; then
|
||||
printf ' %s %s\n' "$(verde OK)" "$desc"
|
||||
_ok=$((_ok + 1))
|
||||
else
|
||||
printf ' %s %s\n' "$(rojo FALLO)" "$desc"
|
||||
printf ' esperado: %s\n obtenido: %s\n' "$esperado" "$obtenido"
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# afirma <descripcion> <comando...> — pasa si el comando devuelve 0
|
||||
afirma() {
|
||||
local desc=$1; shift
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
printf ' %s %s\n' "$(verde OK)" "$desc"
|
||||
_ok=$((_ok + 1))
|
||||
else
|
||||
printf ' %s %s\n' "$(rojo FALLO)" "$desc"
|
||||
printf ' fallo: %s\n' "$*"
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# menor_que <descripcion> <valor> <tope>
|
||||
menor_que() {
|
||||
local desc=$1 valor=$2 tope=$3
|
||||
if awk "BEGIN{exit !($valor < $tope)}"; then
|
||||
printf ' %s %s %s\n' "$(verde OK)" "$desc" "$(gris "($valor < $tope)")"
|
||||
_ok=$((_ok + 1))
|
||||
else
|
||||
printf ' %s %s %s\n' "$(rojo FALLO)" "$desc" "$(gris "($valor >= $tope)")"
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# mayor_que <descripcion> <valor> <suelo>
|
||||
mayor_que() {
|
||||
local desc=$1 valor=$2 suelo=$3
|
||||
if awk "BEGIN{exit !($valor > $suelo)}"; then
|
||||
printf ' %s %s %s\n' "$(verde OK)" "$desc" "$(gris "($valor > $suelo)")"
|
||||
_ok=$((_ok + 1))
|
||||
else
|
||||
printf ' %s %s %s\n' "$(rojo FALLO)" "$desc" "$(gris "($valor <= $suelo)")"
|
||||
_fallo=$((_fallo + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
titulo() { printf '\n\033[1m%s\033[0m\n' "$1"; }
|
||||
|
||||
nota() { printf ' %s %s\n' "$(gris '·')" "$1"; }
|
||||
|
||||
resumen() {
|
||||
printf '\n %s pasan · %s fallan\n' "$(verde $_ok)" \
|
||||
"$([ $_fallo -eq 0 ] && gris 0 || rojo $_fallo)"
|
||||
[ $_fallo -eq 0 ]
|
||||
}
|
||||
|
||||
# Lo que ocupa la tarjeta ahora mismo, en MiB.
|
||||
vram() { nvidia-smi --query-gpu=memory.used --format=csv,noheader 2>/dev/null | tr -d ' MiB'; }
|
||||
|
||||
# Un parametro del Modelfile tal y como lo tiene ollama guardado. Se pregunta a
|
||||
# ollama y no al fichero del repo: el Modelfile del repo es la intencion, esto
|
||||
# es lo que de verdad se va a cargar. arrancar.sh recrea el modelo al medir el
|
||||
# techo de contexto, asi que los dos pueden diferir legitimamente.
|
||||
parametro() { # $1 = modelo $2 = parametro
|
||||
ollama show --modelfile "$1" 2>/dev/null \
|
||||
| awk -v p="$2" '$1=="PARAMETER" && $2==p {print $3; exit}'
|
||||
}
|
||||
|
||||
# El bloque model: del config.yaml, PARSEADO. Nunca leido con grep: el fichero
|
||||
# declara provider y base_url dos veces dentro de model: y en YAML gana la
|
||||
# ultima. Leyendolo parecia local y el trafico salia por OpenRouter.
|
||||
config_model() { # $1 = clave
|
||||
python3 -c "
|
||||
import yaml, sys
|
||||
try:
|
||||
print(yaml.safe_load(open('$CONFIG'))['model'].get('$1', ''))
|
||||
except Exception as e:
|
||||
print('ERROR:', e)
|
||||
" 2>/dev/null | tr -d '\n'
|
||||
}
|
||||
39
verde/pruebas/ejecutar.sh
Executable file
39
verde/pruebas/ejecutar.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
# Pasa las pruebas del verde y resume.
|
||||
#
|
||||
# ./ejecutar.sh todas
|
||||
# ./ejecutar.sh 04 05 solo esas
|
||||
#
|
||||
# NINGUNA abre el TUI ni suena por el altavoz. Lo que no se puede automatizar
|
||||
# —si contesta bien, si encadena herramientas, si la voz convence— esta en
|
||||
# ESCALERA.md, que se recorre a mano.
|
||||
#
|
||||
# Van en este orden porque cada una explica los fallos de la siguiente: sin
|
||||
# tarjeta libre (01) no carga el modelo (02), sin modelo no hay tool calling
|
||||
# (04), y sin que quepa el prompt (05) el TUI dara respuestas truncadas que
|
||||
# parecen otra cosa.
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
PRUEBAS=()
|
||||
for n in "$@"; do PRUEBAS+=("$(ls "${n}"_*.sh 2>/dev/null | head -1)"); done
|
||||
else
|
||||
PRUEBAS=($(ls [0-9][0-9]_*.sh))
|
||||
fi
|
||||
|
||||
FALLADAS=()
|
||||
for p in "${PRUEBAS[@]}"; do
|
||||
[ -n "$p" ] || continue
|
||||
[ -x "$p" ] || chmod +x "$p" 2>/dev/null
|
||||
"./$p" || FALLADAS+=("$p")
|
||||
done
|
||||
|
||||
printf '\n\033[1m════════════════════════════════════\033[0m\n'
|
||||
if [ ${#FALLADAS[@]} -eq 0 ]; then
|
||||
printf '\033[32m todo en orden\033[0m\n'
|
||||
printf '\033[90m siguiente: ../verde-launcher.sh y seguir ../ESCALERA.md\033[0m\n'
|
||||
exit 0
|
||||
fi
|
||||
printf '\033[31m fallan: %s\033[0m\n' "${FALLADAS[*]}"
|
||||
exit 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue