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).
304 lines
14 KiB
Bash
Executable file
304 lines
14 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Deja el carril verde listo para usar: mide, configura y dictamina.
|
|
#
|
|
# ./arrancar.sh medir, configurar y dictaminar
|
|
# ./arrancar.sh --solo-medir medir sin tocar la configuracion
|
|
# ./arrancar.sh --igual no plantarse aunque dataset.py siga vivo
|
|
#
|
|
# ── Por que existe ─────────────────────────────────────────────────────────
|
|
#
|
|
# El primer arranque del verde topo con algo que no se anticipaba: el prompt
|
|
# fijo de Hermes son ~20.500 tokens (26 KB de sistema + 56 KB de esquemas de 19
|
|
# herramientas) y qwen3.5:4b-jarvis va con num_ctx 4096. Cinco veces por encima.
|
|
# No es que rinda mal: no entra la pregunta.
|
|
#
|
|
# Para eso existe qwen3.5:4b-verde. Pero cuanto contexto cabe en una GPU de
|
|
# 4 GB no se puede calcular sobre el papel —ollama no reporta head_count_kv—,
|
|
# hay que medirlo. Este script lo mide, escribe el resultado donde toca y
|
|
# contesta la unica pregunta que de verdad decide si esto va a funcionar.
|
|
#
|
|
# ── El suelo de 64k, que manda sobre todo lo anterior (16 ago) ──────────────
|
|
#
|
|
# La primera version de este script buscaba el mayor num_ctx que cupiera en la
|
|
# tarjeta y encontro 24576, con 32 capas en GPU y 3301 MiB. Todo correcto y
|
|
# todo inutil: al abrir Hermes salta
|
|
#
|
|
# Model qwen3.5:4b-verde has a context window of 24,576 tokens, which is
|
|
# below the minimum 64,000 required by Hermes Agent.
|
|
#
|
|
# Es un tope DURO del arnes, no un aviso: agent/model_metadata.py:405,
|
|
# MINIMUM_CONTEXT_LENGTH = 64_000, comprobado en agent/agent_init.py:2656.
|
|
#
|
|
# Hay una puerta de atras en ese mismo if —si provider es "lmstudio" y
|
|
# context_length esta puesto a mano, se salta el suelo— y NO se usa: el
|
|
# proveedor lmstudio llama a /api/v1/models, que es la API nativa de LM Studio
|
|
# y ollama no tiene, ademas de intentar precargar el modelo por su cuenta.
|
|
# Fingir ser otro servidor romperia mas adelante y de forma mas confusa.
|
|
#
|
|
# Tampoco se miente en el config.yaml poniendo 64000 con el modelo a 24576: el
|
|
# arnes creeria tener 64k, no comprimiria hasta acercarse, y ollama iria tirando
|
|
# los tokens mas viejos —que son el prompt de sistema y los esquemas de las
|
|
# herramientas—. El sintoma seria que el agente deja de llamar herramientas a
|
|
# mitad de sesion, sin un solo error en el log.
|
|
#
|
|
# Asi que el numero a buscar ya no es el contexto: es CUANTAS CAPAS caben en la
|
|
# tarjeta con num_ctx 65536 fijo. Medido el 16 ago:
|
|
#
|
|
# num_gpu 32 -> cudaMalloc failed num_gpu 16 -> 2795 MiB, CARGA
|
|
# num_gpu 24 -> cudaMalloc failed num_gpu 8 -> 1772 MiB, CARGA
|
|
# num_gpu 0 -> 0 MiB, todo en RAM
|
|
#
|
|
# ── Se planta en vez de seguir ─────────────────────────────────────────────
|
|
#
|
|
# Cada paso comprueba lo suyo y aborta si no cuadra. Es a proposito: un fallo
|
|
# aqui es de una linea, y el mismo fallo tres pasos mas adelante sale como
|
|
# "respuestas raras" y cuesta una tarde.
|
|
set -uo pipefail
|
|
|
|
AQUI=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)
|
|
RAIZ=$(cd -P "$AQUI/.." && pwd)
|
|
CONFIG=$HOME/.hermes/config.yaml
|
|
OLLAMA=${OLLAMA_URL:-http://127.0.0.1:11434}
|
|
MODELO=qwen3.5:4b-verde
|
|
NARANJA=qwen3.5:4b-jarvis
|
|
|
|
# El suelo duro de Hermes es 64.000 (agent/model_metadata.py:405). Se pide la
|
|
# potencia de dos justo por encima, que es lo que entiende ollama sin discutir.
|
|
CTX_MINIMO=65536
|
|
|
|
SOLO_MEDIR=no
|
|
IGUAL=no
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--solo-medir) SOLO_MEDIR=si ;;
|
|
--igual) IGUAL=si ;;
|
|
-h|--help) sed -n '2,10p' "$0"; exit 0 ;;
|
|
esac
|
|
done
|
|
|
|
rojo() { printf '\033[31m%s\033[0m\n' "$*"; }
|
|
verde() { printf '\033[32m%s\033[0m\n' "$*"; }
|
|
gris() { printf '\033[90m%s\033[0m\n' "$*"; }
|
|
titulo(){ printf '\n\033[1m── %s ─────────────────────────────────────\033[0m\n' "$*"; }
|
|
abortar(){ rojo " ✗ $*"; exit 1; }
|
|
|
|
vram() { nvidia-smi --query-gpu=memory.used --format=csv,noheader 2>/dev/null | tr -d ' MiB'; }
|
|
|
|
# ═══ 1. La tarjeta ═════════════════════════════════════════════════════════
|
|
titulo "1. Liberar la tarjeta"
|
|
|
|
if pgrep -f 'entrena/dataset.py' >/dev/null 2>&1; then
|
|
if [ "$IGUAL" = no ]; then
|
|
rojo " ✗ dataset.py sigue corriendo."
|
|
gris " Es un trabajo de 7-10 h que usa $NARANJA en la grafica."
|
|
gris " Competir con el por la VRAM estropea las dos cosas: el verde"
|
|
gris " no cargara y el dataset ira mas lento."
|
|
gris ""
|
|
gris " Por donde va:"
|
|
[ -f "$RAIZ/entrena/dataset.log" ] && sed 's/^/ /' "$RAIZ/entrena/dataset.log" | tail -3
|
|
gris ""
|
|
gris " Espera a que acabe, o --igual para seguir de todas formas."
|
|
exit 1
|
|
fi
|
|
rojo " ! dataset.py corriendo y --igual puesto: siguiendo bajo tu responsabilidad"
|
|
fi
|
|
|
|
# Restos del naranja. Se para el proceso; NO se toca ni un fichero suyo.
|
|
for p in 'nucleo/jarvis.py' 'whisper-server'; do
|
|
if pgrep -f "$p" >/dev/null 2>&1; then
|
|
gris " cerrando $p"
|
|
pkill -f "$p" 2>/dev/null
|
|
fi
|
|
done
|
|
ollama stop "$NARANJA" >/dev/null 2>&1
|
|
sleep 3
|
|
|
|
LIBRE=$(vram)
|
|
if [ -z "$LIBRE" ]; then
|
|
abortar "no hay nvidia-smi: esto necesita la grafica"
|
|
elif [ "$LIBRE" -gt 400 ]; then
|
|
rojo " ! la tarjeta sigue con $LIBRE MiB usados"
|
|
nvidia-smi --query-compute-apps=pid,used_memory,name --format=csv,noheader | sed 's/^/ /'
|
|
gris " Algo la esta usando. Mira la lista de arriba antes de seguir."
|
|
[ "$IGUAL" = no ] && exit 1
|
|
else
|
|
verde " ✓ tarjeta libre: $LIBRE MiB"
|
|
fi
|
|
|
|
# ═══ 2. Cuantas capas caben con el contexto que Hermes exige ═══════════════
|
|
titulo "2. Cuantas capas caben a 65536 (medido, no supuesto)"
|
|
|
|
gris " El contexto NO se negocia: Hermes rechaza cualquier modelo por debajo de"
|
|
gris " 64.000 tokens. Lo que se busca es el mayor num_gpu que cargue a 65536:"
|
|
gris " cada capa que baja a la CPU cuesta velocidad, asi que se cogen todas las"
|
|
gris " que quepan."
|
|
|
|
TMP=$(mktemp --suffix=.Modelfile); trap 'rm -f "$TMP"' EXIT
|
|
GANADOR=""; GANADOR_VRAM=""
|
|
|
|
probar_ctx() { # $1 = num_ctx $2 = num_gpu
|
|
printf 'FROM qwen3.5:4b\nPARAMETER num_gpu %s\nPARAMETER num_ctx %s\nPARAMETER num_predict 4096\nPARAMETER temperature 0.2\n' "$2" "$1" > "$TMP"
|
|
ollama create "$MODELO" -f "$TMP" >/dev/null 2>&1 || return 1
|
|
local r
|
|
r=$(curl -s --max-time 240 "$OLLAMA/v1/chat/completions" -H 'Content-Type: application/json' \
|
|
-d "{\"model\":\"$MODELO\",\"messages\":[{\"role\":\"user\",\"content\":\"ok\"}],\"max_tokens\":5}" 2>/dev/null)
|
|
echo "$r" | grep -q '"choices"' || return 1
|
|
return 0
|
|
}
|
|
|
|
for GPU in 32 24 16 8 0; do
|
|
printf ' num_ctx %-6s num_gpu %-3s ... ' "$CTX_MINIMO" "$GPU"
|
|
if probar_ctx "$CTX_MINIMO" "$GPU"; then
|
|
V=$(curl -fsS --max-time 5 "$OLLAMA/api/ps" 2>/dev/null | python3 -c "
|
|
import json,sys
|
|
m=json.load(sys.stdin)['models']
|
|
print(m[0]['size_vram']//1048576 if m else 0)" 2>/dev/null)
|
|
verde "CABE (en tarjeta $V MiB, tarjeta $(vram) MiB)"
|
|
GANADOR=$CTX_MINIMO; GANADOR_GPU=$GPU; GANADOR_VRAM=$V
|
|
break
|
|
fi
|
|
rojo "no cabe"
|
|
ollama stop "$MODELO" >/dev/null 2>&1; sleep 2
|
|
done
|
|
|
|
if [ -z "$GANADOR" ]; then
|
|
rojo " ✗ no carga a 65536 ni con TODAS las capas en la CPU."
|
|
gris " Eso ya no es la grafica: mira la RAM libre con free -h."
|
|
exit 1
|
|
fi
|
|
|
|
verde " ✓ num_ctx $GANADOR con $GANADOR_GPU de 32 capas en GPU, $GANADOR_VRAM MiB"
|
|
|
|
if [ "$GANADOR_GPU" -lt 32 ]; then
|
|
gris ""
|
|
gris " $(( 32 - GANADOR_GPU )) capas van por CPU y eso se paga en el prefill: el prompt fijo"
|
|
gris " son ~20.500 tokens que hay que procesar en CADA vuelta del agente."
|
|
gris ""
|
|
gris " Palanca para recuperar capas, y NO la aplico yo porque es del SERVICIO"
|
|
gris " de ollama, no del modelo: afectaria tambien al naranja la proxima vez"
|
|
gris " que arranque. Esa decision es tuya."
|
|
gris ""
|
|
gris " sudo systemctl edit ollama"
|
|
gris " [Service]"
|
|
gris " Environment=\"OLLAMA_FLASH_ATTENTION=1\""
|
|
gris " Environment=\"OLLAMA_KV_CACHE_TYPE=q8_0\""
|
|
gris " sudo systemctl restart ollama"
|
|
gris ""
|
|
gris " Parte el KV cache por la mitad. Si lo haces, anotalo en NOTAS.md y en"
|
|
gris " config/qwen3.5-4b-jarvis.Modelfile, o en tres semanas nadie sabra de"
|
|
gris " donde salio el cambio."
|
|
fi
|
|
|
|
HUECO=$(( GANADOR - 20500 ))
|
|
gris " quedan ~$HUECO tokens de conversacion tras el prompt fijo"
|
|
|
|
# ═══ 3. Configurar Hermes ══════════════════════════════════════════════════
|
|
titulo "3. Decirle a Hermes la verdad"
|
|
|
|
if [ "$SOLO_MEDIR" = si ]; then
|
|
gris " --solo-medir: no toco $CONFIG"
|
|
gris " Para aplicarlo a mano: model.default=$MODELO y context_length=$GANADOR"
|
|
else
|
|
cp "$CONFIG" "$CONFIG.antes-de-arrancar" 2>/dev/null
|
|
python3 - "$CONFIG" "$MODELO" "$GANADOR" <<'PY'
|
|
import re, sys
|
|
ruta, modelo, ctx = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
lineas = open(ruta).read().splitlines(True)
|
|
|
|
# Se edita por LINEAS, no reescribiendo el YAML: el fichero son 1890 lineas de
|
|
# documentacion propia y volcarlo con yaml.dump la perderia entera.
|
|
hecho_modelo = hecho_ctx = False
|
|
for i, l in enumerate(lineas):
|
|
if not hecho_modelo and re.match(r'^ default: ', l):
|
|
lineas[i] = f' default: "{modelo}"\n'; hecho_modelo = True
|
|
# context_length viene comentada de fabrica dentro del bloque model:.
|
|
# La propia plantilla dice que es justo para "a local server with a custom
|
|
# num_ctx", que es exactamente este caso.
|
|
elif not hecho_ctx and re.match(r'^ # context_length: ', l):
|
|
lineas[i] = (f' # JARVIS verde: el num_ctx REAL del Modelfile. Sin esto Hermes\n'
|
|
f' # se cree lo que dice /api/show, que son 262144 (el maximo del\n'
|
|
f' # modelo) y no lo configurado.\n'
|
|
f' context_length: {ctx}\n')
|
|
hecho_ctx = True
|
|
|
|
if not hecho_modelo: sys.exit("no encontre la linea 'default:' en el bloque model")
|
|
if not hecho_ctx: sys.exit("no encontre la linea '# context_length:' comentada")
|
|
open(ruta, 'w').writelines(lineas)
|
|
PY
|
|
[ $? -ne 0 ] && abortar "no pude escribir la configuracion (copia en $CONFIG.antes-de-arrancar)"
|
|
|
|
# Verificar PARSEANDO, nunca leyendo: este fichero declara provider y
|
|
# base_url DOS veces dentro de model: y en YAML gana la ultima. Leyendolo
|
|
# parecia configurado y el trafico salia por OpenRouter.
|
|
python3 - "$CONFIG" "$MODELO" "$GANADOR" <<'PY'
|
|
import sys, yaml
|
|
ruta, modelo, ctx = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
m = yaml.safe_load(open(ruta))['model']
|
|
fallos = []
|
|
if m.get('default') != modelo: fallos.append(f"default={m.get('default')}")
|
|
if m.get('context_length') != ctx: fallos.append(f"context_length={m.get('context_length')}")
|
|
if '127.0.0.1' not in str(m.get('base_url','')): fallos.append(f"base_url={m.get('base_url')}")
|
|
if fallos: sys.exit("la configuracion NO quedo como se pedia: " + ", ".join(fallos))
|
|
print(f" modelo={m['default']} context_length={m['context_length']} base_url={m['base_url']}")
|
|
PY
|
|
[ $? -ne 0 ] && abortar "verificacion fallida; restaura con: cp $CONFIG.antes-de-arrancar $CONFIG"
|
|
verde " ✓ configurado y verificado parseando"
|
|
fi
|
|
|
|
# ═══ 4. La prueba decisiva ═════════════════════════════════════════════════
|
|
titulo "4. ¿Sabe el modelo llamar herramientas?"
|
|
|
|
gris " Esto va ANTES de abrir el TUI a proposito. Separa dos preguntas que"
|
|
gris " juntas se contestan mal: ¿sabe el modelo emitir una llamada? y"
|
|
gris " ¿funciona Hermes? Si falla aqui, no hay nada que depurar en Hermes."
|
|
|
|
R=$(curl -s --max-time 300 "$OLLAMA/v1/chat/completions" -H 'Content-Type: application/json' -d "{
|
|
\"model\": \"$MODELO\",
|
|
\"messages\": [{\"role\":\"user\",\"content\":\"¿Cuánto espacio libre queda en el disco?\"}],
|
|
\"tools\": [{\"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\"]}}}]
|
|
}" 2>/dev/null)
|
|
|
|
echo "$R" | python3 -c "
|
|
import json, sys
|
|
try: d = json.load(sys.stdin)
|
|
except Exception: print(' RESPUESTA ILEGIBLE'); sys.exit(2)
|
|
if 'error' in d: print(' ERROR:', str(d['error'])[:200]); sys.exit(2)
|
|
msg = d['choices'][0]['message']
|
|
tc = msg.get('tool_calls') or []
|
|
if not tc:
|
|
print(' SIN tool_calls. Contesto en prosa:')
|
|
print(' ', repr((msg.get('content') or '')[:200]))
|
|
sys.exit(1)
|
|
f = tc[0]['function']
|
|
print(' herramienta :', f['name'])
|
|
print(' argumentos :', f['arguments'])
|
|
try:
|
|
json.loads(f['arguments']); print(' JSON valido')
|
|
except Exception: print(' JSON ROTO'); sys.exit(1)
|
|
sys.exit(0)
|
|
"
|
|
VEREDICTO=$?
|
|
|
|
titulo "Resumen"
|
|
printf ' contexto %s tokens (%s capas en GPU, %s MiB)\n' "$GANADOR" "$GANADOR_GPU" "$GANADOR_VRAM"
|
|
printf ' prompt fijo ~20.500 tokens -> quedan ~%s de conversacion\n' "$HUECO"
|
|
|
|
if [ "$VEREDICTO" -eq 0 ]; then
|
|
verde " ✓ el modelo llama herramientas. Lo que falle a partir de aqui es de Hermes."
|
|
gris ""
|
|
gris " Siguiente:"
|
|
gris " $AQUI/pruebas/ejecutar.sh las comprobaciones, ninguna abre el TUI"
|
|
gris " $AQUI/verde-launcher.sh el TUI en verde -> sigue ESCALERA.md"
|
|
else
|
|
rojo " ✗ el modelo NO emite llamadas usables en este formato."
|
|
gris ""
|
|
gris " Abrir el TUI ahora solo daria un fallo mas confuso. Dos salidas:"
|
|
gris " · modelo mayor por CPU: hay 49 GB de RAM y 8 nucleos fisicos."
|
|
gris " qwen3.5:9b esta en disco. Lento pero es un banco de pruebas."
|
|
gris " · recortar herramientas en platform_toolsets del config.yaml:"
|
|
gris " cli: [hermes-cli] -> una lista corta. Menos esquema, menos lio."
|
|
exit 1
|
|
fi
|