Compare commits

..
9 Commits
20 changed files with 1657 additions and 2263 deletions
+2 -61
View File
@@ -1,61 +1,2 @@
# Models & Large Files # Backups locales: pueden contener claves y secretos
.ollama/ ubuntu-config-*.tar.gz
models/
*.safetensors
*.gguf
*.bin
*.pth
# Workspace
workspace/
uploads/
backups/
# Python
__pycache__/
*.pyc
*.pyo
*.egg-info/
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
.venv/
venv/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Environment
.env
.env.local
.env.*.local
# Logs
logs/
*.log
# Temporary
*.tmp
*.cache
.pytest_cache/
.coverage
# OS
.DS_Store
Thumbs.db
-89
View File
@@ -1,89 +0,0 @@
.PHONY: help install update monitor health-check backup clean reinstall logs
help:
@echo "LLM Server - Comandos disponibles:"
@echo ""
@echo " make install - Instalar todo desde cero (requiere sudo)"
@echo " make update - Actualizar código y dependencias"
@echo " make monitor - Ver monitoreo en tiempo real"
@echo " make health-check - Verificar estado del sistema"
@echo " make backup - Hacer backup de configuración"
@echo " make logs - Ver logs de servicios"
@echo " make status - Ver estado de servicios"
@echo " make start - Iniciar servicios"
@echo " make stop - Detener servicios"
@echo " make restart - Reiniciar servicios"
@echo " make clean - Limpiar archivos temporales"
@echo " make reinstall - Reinstalar (redownload modelos)"
install:
@echo "Instalando LLM Server..."
sudo bash scripts/install.sh
install-quick:
@echo "Instalación rápida (sin modelos)..."
sudo bash scripts/install.sh --quick
install-gpu-only:
@echo "Instalando solo GPU drivers..."
sudo bash scripts/install.sh --gpu-only
update:
bash scripts/update.sh
monitor:
bash scripts/monitor.sh
health-check:
bash scripts/health-check.sh
backup:
bash scripts/backup.sh
logs:
@echo "Logs de Ollama:"
sudo journalctl -u ollama -f
logs-api:
@echo "Logs de API:"
sudo journalctl -u llm-api -f
status:
@echo "Ollama:"
@sudo systemctl status ollama --no-pager
@echo ""
@echo "LLM API:"
@sudo systemctl status llm-api --no-pager
start:
sudo systemctl start ollama llm-api
@echo "✅ Servicios iniciados"
stop:
sudo systemctl stop ollama llm-api
@echo "✅ Servicios detenidos"
restart:
sudo systemctl restart ollama llm-api
@echo "✅ Servicios reiniciados"
clean:
rm -rf __pycache__ .pytest_cache .venv venv
find . -type f -name "*.pyc" -delete
@echo "✅ Limpiado"
reinstall:
@echo "Reinstalando LLM Server..."
sudo bash scripts/install.sh --gpu-only
make update
@echo "✅ Reinstalación completada"
info:
@echo "========== INFORMACIÓN DEL SERVIDOR =========="
@echo "Hostname: $$(hostname)"
@echo "IP: $$(hostname -I)"
@echo "SO: $$(lsb_release -d | cut -f2)"
@echo "CPU: $$(lscpu | grep 'Model name' | cut -d: -f2)"
@echo "RAM: $$(free -h | awk 'NR==2 {print $$2}')"
@echo "GPU: $$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || echo 'No disponible')"
@echo "=============================================="
+525
View File
@@ -0,0 +1,525 @@
# OpenClaw con servidor local
Guía para ejecutar OpenClaw usando el modelo local administrado por Ollama y
expuesto mediante LiteLLM.
> El video de referencia es [Aprende OpenClaw Ahora! curso completo desde cero
> para programadores](https://www.youtube.com/watch?v=4UtyJt2rMfo). Esta guía
> adapta el flujo de configuración a este servidor local y no depende de un
> proveedor cloud.
## Arquitectura
```text
OpenClaw Gateway
|
| OpenAI-compatible: http://127.0.0.1:8000/v1
v
LiteLLM Proxy
|
| Ollama native API: http://127.0.0.1:11434
v
Ollama -> qwen2.5-coder:14b -> NVIDIA GPU
```
Cuando OpenClaw y el servidor LLM están en la misma máquina, usa `127.0.0.1`.
Si OpenClaw corre en otro equipo, reemplaza esa dirección por la IP privada del
servidor, por ejemplo `http://192.168.1.50:8000/v1`.
## Requisitos
En el servidor LLM:
- Ubuntu 22.04 o 24.04.
- Node.js 24.16 o superior para OpenClaw.
- GPU NVIDIA con controladores funcionales.
- 16 GB de RAM como mínimo y espacio para el modelo.
- `sudo`, `curl`, `git`, Python 3 y conexión a Internet.
En el cliente remoto solo necesitas Node.js compatible con OpenClaw y acceso de
red al puerto `8000` del servidor.
## 1. Preparar el servidor LLM
Clona este repositorio y ejecuta el instalador principal:
```bash
git clone https://gitea.oemspot.com.ar/carlostellocba/llm-server-setup.git
cd llm-server-setup
chmod +x setup_llm_server.sh
sudo ./setup_llm_server.sh
```
El script instala y configura:
- controladores NVIDIA y un límite de potencia de 300 W;
- 8 GB de swap;
- Ollama como servicio systemd en `0.0.0.0:11434`;
- el modelo `qwen2.5-coder:14b`;
- LiteLLM en `/opt/litellm-env`;
- LiteLLM como `litellm.service` en `0.0.0.0:8000`;
- reglas UFW para SSH y LiteLLM.
Si se instalaron nuevos controladores, reinicia antes de continuar:
```bash
sudo reboot
```
## 2. Verificar Ollama y LiteLLM
Ejecuta estas comprobaciones en el servidor:
```bash
systemctl is-active ollama
systemctl is-active litellm
nvidia-smi
curl http://127.0.0.1:11434/api/tags
curl http://127.0.0.1:8000/health/liveliness
```
Confirma que el modelo existe:
```bash
ollama list
```
La configuración generada por `setup_llm_server.sh` está en:
```text
~/litellm_config.yaml
```
El servicio LiteLLM debe ejecutarse con esa configuración y escuchar en el
puerto `8000`. Para revisar errores:
```bash
sudo journalctl -u ollama -n 100 --no-pager
sudo journalctl -u litellm -n 100 --no-pager
```
## 3. Probar el endpoint OpenAI-compatible
Antes de instalar OpenClaw, prueba LiteLLM directamente:
```bash
curl http://127.0.0.1:8000/v1/models
```
Después envía una consulta usando una clave local de prueba. El proxy de este
repositorio no configura autenticación, pero OpenClaw espera un valor de API
key para el proveedor LiteLLM; `local` sirve como valor de configuración local:
```bash
curl http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer local' \
-d '{
"model": "qwen2.5-coder:14b",
"messages": [{"role": "user", "content": "Responde exactamente: OK"}],
"temperature": 0
}'
```
La respuesta debe contener `OK` y no un error de conexión o de modelo.
## 4. Instalar OpenClaw
En el servidor o en el equipo donde se ejecutará el Gateway:
```bash
bash install_openclaw.sh
```
El script instala OpenClaw sin iniciar el asistente. También puedes ejecutar el
instalador oficial directamente:
```bash
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
```
Comprueba Node.js y OpenClaw:
```bash
node --version
openclaw --version
```
## 5. Configurar OpenClaw con LiteLLM local
La configuración de OpenClaw se guarda en:
```text
~/.openclaw/openclaw.json
```
Crea o edita ese archivo como JSON5:
```json5
{
models: {
providers: {
litellm: {
baseUrl: "http://127.0.0.1:8000/v1",
apiKey: "local",
api: "openai-completions",
models: [
{
id: "qwen2.5-coder:14b",
name: "Qwen 2.5 Coder 14B local",
reasoning: false,
input: ["text"],
contextWindow: 32768,
maxTokens: 32768
}
]
}
}
},
agents: {
defaults: {
model: {
primary: "litellm/qwen2.5-coder:14b"
}
}
}
}
```
Puntos importantes:
- En `baseUrl` incluye `/v1` porque OpenClaw usa la API compatible con OpenAI de
LiteLLM.
- No uses `http://127.0.0.1:11434/v1` directamente con el proveedor LiteLLM.
- Para Ollama nativo, OpenClaw usa otra configuración y no debe mezclarse con
esta ruta.
- `apiKey: "local"` coincide con el valor usado en las pruebas. No es una
credencial real porque el servicio actual no habilita autenticación.
Valida la configuración:
```bash
openclaw doctor
openclaw models list --provider litellm --refresh --json
```
## 6. Configurar con el asistente
Como alternativa a editar JSON5, ejecuta el onboarding indicando LiteLLM:
```bash
openclaw onboard --auth-choice litellm-api-key
```
Cuando solicite la URL del proxy, usa:
```text
http://127.0.0.1:8000/v1
```
Y como API key local:
```text
local
```
Si el asistente permite modo no interactivo en tu versión instalada:
```bash
export LITELLM_API_KEY=local
openclaw onboard --non-interactive --accept-risk --skip-health \
--auth-choice litellm-api-key \
--litellm-api-key "$LITELLM_API_KEY" \
--custom-base-url "http://127.0.0.1:8000/v1"
```
Después revisa el modelo primario con:
```bash
openclaw config get agents.defaults.model.primary
```
Debe devolver `litellm/qwen2.5-coder:14b`.
## 7. Instalar y verificar el Gateway
Instala el servicio de usuario de OpenClaw:
```bash
openclaw gateway install
openclaw gateway status
```
El Gateway normalmente escucha en el puerto `18789`. Abre el dashboard:
```bash
openclaw dashboard
```
También puedes verificarlo desde el navegador en:
```text
http://127.0.0.1:18789
```
Prueba un mensaje desde el dashboard. En otra terminal puedes observar los
logs del Gateway:
```bash
openclaw logs --follow
```
## 8. Acceder desde Windows con PuTTY o PowerShell
En este servidor, la IP de la máquina Ubuntu es:
```text
192.168.0.225
```
El Gateway está enlazado intencionalmente a `127.0.0.1:18789`, por lo que no
debes abrir `192.168.0.225:18789` directamente. Usa un túnel SSH desde el PC
Windows.
### PowerShell
Ejecuta este comando en PowerShell del PC cliente:
```powershell
ssh -N -L 18789:127.0.0.1:18789 ctello@192.168.0.225
```
Introduce la contraseña de `ctello` y deja esa ventana abierta. El parámetro
`-L` significa:
```text
Puerto local del PC:18789 -> servidor:127.0.0.1:18789
```
En otra ventana de PowerShell, comprueba el túnel:
```powershell
Test-NetConnection 127.0.0.1 -Port 18789
```
Debe mostrar:
```text
TcpTestSucceeded : True
```
Después abre el dashboard en el navegador del PC:
```text
http://127.0.0.1:18789
```
Si el puerto local `18789` ya está ocupado, usa otro puerto solo en el PC:
```powershell
ssh -N -L 18790:127.0.0.1:18789 ctello@192.168.0.225
```
En ese caso abre:
```text
http://127.0.0.1:18790
```
El destino remoto sigue siendo siempre `127.0.0.1:18789`.
### PuTTY
Configura la sesión con:
```text
Host Name: 192.168.0.225
Port: 22
Connection type: SSH
```
Después ve a `Connection > SSH > Tunnels` y añade:
```text
Source port: 18789
Destination: 127.0.0.1:18789
Type: Local
```
Pulsa `Add`, conecta la sesión y abre en el navegador:
```text
http://127.0.0.1:18789
```
Mantén abierta la sesión SSH mientras uses el dashboard.
## 9. Usar OpenClaw desde otro equipo
Si el Gateway corre en otro equipo distinto al servidor LLM, cambia únicamente
`baseUrl` en `~/.openclaw/openclaw.json`:
```json5
{
models: {
providers: {
litellm: {
baseUrl: "http://192.168.1.50:8000/v1",
apiKey: "local",
api: "openai-completions",
models: [
{
id: "qwen2.5-coder:14b",
name: "Qwen 2.5 Coder 14B local",
input: ["text"],
contextWindow: 32768,
maxTokens: 32768
}
]
}
}
},
agents: {
defaults: {
model: { primary: "litellm/qwen2.5-coder:14b" }
}
}
}
```
En el servidor, permite el acceso solo desde la red privada. Por ejemplo,
reemplaza la regla abierta actual de UFW por una regla limitada a tu subred:
```bash
sudo ufw delete allow 8000/tcp
sudo ufw allow from 192.168.1.0/24 to any port 8000 proto tcp
sudo ufw status verbose
```
No expongas el puerto `8000` directamente a Internet: el proxy actual no tiene
una API key real ni TLS.
## Solución de problemas
### Gateway activo, pero el navegador muestra `ERR_CONNECTION_REFUSED`
Comprueba el estado en Ubuntu:
```bash
openclaw gateway status
ss -ltnp | grep 18789
```
Debe aparecer:
```text
Runtime: running
Connectivity probe: ok
Listening: 127.0.0.1:18789
```
Si el Gateway no está activo, configura el modo local y arráncalo:
```bash
openclaw config set gateway.mode local
openclaw gateway install
openclaw gateway status
```
Si el servicio de usuario necesita una sesión persistente, habilita el
`linger` para `ctello` desde una cuenta con `sudo`:
```bash
sudo loginctl enable-linger ctello
```
Después vuelve a iniciar sesión SSH como `ctello` y ejecuta:
```bash
systemctl --user daemon-reload
systemctl --user enable --now openclaw-gateway.service
```
Si `systemctl --user` muestra que faltan `DBUS_SESSION_BUS_ADDRESS` o
`XDG_RUNTIME_DIR`, vuelve a conectarte por SSH después de habilitar `linger`.
No ejecutes un segundo `openclaw gateway run` si el servicio ya está activo.
Para revisar errores:
```bash
journalctl --user -u openclaw-gateway.service -n 100 --no-pager
```
### El túnel de PowerShell no conecta
En el PC cliente verifica primero que SSH funciona:
```powershell
Test-NetConnection 192.168.0.225 -Port 22
```
Si SSH responde pero el puerto del túnel no, comprueba en Ubuntu que el
Gateway esté escuchando en `127.0.0.1:18789`. No abras el puerto `18789` en
UFW ni cambies el Gateway a `0.0.0.0` solo para evitar el túnel.
### `Connection refused` en el puerto 8000
```bash
sudo systemctl restart ollama litellm
sudo systemctl status ollama litellm
sudo journalctl -u litellm -n 100 --no-pager
```
### LiteLLM no encuentra el modelo
```bash
ollama list
ollama pull qwen2.5-coder:14b
curl http://127.0.0.1:8000/v1/models
```
El nombre debe coincidir exactamente en los tres lugares:
```text
qwen2.5-coder:14b
```
### OpenClaw muestra respuestas de herramientas como texto
Comprueba que OpenClaw usa LiteLLM con `/v1` y no la URL equivocada de Ollama:
```bash
openclaw config get models.providers.litellm.baseUrl
```
Debe devolver:
```text
http://127.0.0.1:8000/v1
```
### El modelo se queda sin memoria
Reduce `contextWindow` y `maxTokens` en la configuración de OpenClaw y revisa
la VRAM disponible:
```bash
nvidia-smi
```
### La configuración no es válida
```bash
openclaw doctor
openclaw doctor --fix
```
OpenClaw valida estrictamente `openclaw.json`; un campo desconocido o un tipo
incorrecto puede impedir que el Gateway arranque.
## Referencias
- [Documentación oficial de OpenClaw](https://docs.openclaw.ai/)
- [Proveedor LiteLLM en OpenClaw](https://docs.openclaw.ai/providers/litellm)
- [Proveedor Ollama en OpenClaw](https://docs.openclaw.ai/providers/ollama)
- [Configuración del Gateway](https://docs.openclaw.ai/gateway/configuration)
+215 -76
View File
@@ -1,92 +1,231 @@
# LLM Server - Deployment Automatizado # LLM Server
Sistema completo de IA agentica con LLMs locales (Phi + DeepSeek 33B) en Ubuntu 22.04. Despliegue automatizado de un servidor LLM local con Ollama, LiteLLM y
OpenClaw sobre Ubuntu 22.04/24.04.
## Requisitos Mínimos ## Requisitos
- **CPU**: Intel i3-10105 (4 cores) - Ubuntu 22.04 o 24.04
- **RAM**: 16GB DDR4 - CPU de 4 núcleos o superior
- **GPU**: NVIDIA RTX 3090 (24GB VRAM) - 16 GB de RAM como mínimo
- **PSU**: 850W+ - GPU NVIDIA compatible con sus controladores Linux
- **SO**: Ubuntu 22.04 LTS - Acceso `sudo` y conexión a Internet
## Solución para tool calling con rutas de Windows La configuración está orientada a una NVIDIA RTX 3090 de 24 GB, pero el
script puede utilizarse con otras GPU compatibles ajustando el modelo si es
necesario.
Cuando el LLM intenta ejecutar una herramienta, la respuesta no debe mostrarse como texto plano. Debe ser manejada como un `tool_call` o `function_call` y ejecutada localmente. ## Instalación
### Regla importante Clona el repositorio en el servidor:
Añade esta instrucción al `System Prompt`:
```text
When outputting Windows file paths in JSON arguments, you must strictly escape all backslashes (for example: C:\\Users\\name\\file.txt).
```
### Patrón recomendado
1. El LLM devuelve una llamada como JSON.
2. Tu cliente detecta `tool_calls` o `function_call`.
3. Tu código ejecuta la función localmente.
4. Se envía de vuelta al LLM el resultado con rol `tool` o `function`.
### Ejemplo de implementación
El proyecto incluye un ejemplo funcional en `tool_call_demo.py` que:
- simula un `tool_call` del modelo,
- valida que la ruta JSON esté escapada correctamente,
- lee el archivo físico en disco,
- devuelve el contenido al modelo para continuar.
```bash ```bash
python tool_call_demo.py git clone https://gitea.oemspot.com.ar/carlostellocba/llm-server-setup.git
```
### Ejemplo de payload válido
```json
{
"tool_calls": [
{
"id": "call_read_001",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"c:\\\\Workspace\\\\llm-server-setup\\\\README.md\"}"
}
}
]
}
```
> Si el JSON tiene barras invertidas sin escape, el parser falla y el modelo parece "no responder". Usar rutas con `\\` en JSON es obligatorio en Windows.
## Instalación Rápida
```bash
# 1. Clonar repositorio
git clone https://github.com/tuuser/llm-server-setup.git
cd llm-server-setup cd llm-server-setup
# 2. Instalar OpenClaw sin iniciar el asistente
bash install_openclaw.sh
# 3. Ejecutar el asistente inicial cuando quieras
openclaw onboard --install-daemon
# También puedes instalar y abrir el asistente en el mismo paso
bash install_openclaw.sh --onboard
``` ```
El script está pensado para Ubuntu 22.04/24.04 y WSL2. Usa el instalador oficial Ejecuta el instalador principal como `root` o mediante `sudo`:
de OpenClaw, que instala Node.js si es necesario. Revisa las instrucciones
oficiales si vas a usar otra distribución o un entorno restringido.
## Instalación del servidor LLM
```bash ```bash
nano setup_llm_server.sh
chmod +x setup_llm_server.sh chmod +x setup_llm_server.sh
sudo ./setup_llm_server.sh sudo ./setup_llm_server.sh
``` ```
El instalador:
- actualiza los paquetes del sistema e instala dependencias;
- configura un archivo de swap de 8 GB;
- instala los controladores NVIDIA si no están disponibles;
- instala y configura Ollama en `0.0.0.0:11434`;
- crea un entorno virtual de LiteLLM en `/opt/litellm-env`;
- descarga `qwen2.5-coder:14b`;
- crea el servicio `litellm.service` en el puerto `8000`;
- permite SSH y LiteLLM mediante UFW.
El proceso puede tardar varios minutos y requiere espacio suficiente para el
modelo. Si se instalan controladores NVIDIA nuevos, reinicia el servidor:
```bash
sudo reboot
```
## Verificación
Después de la instalación, comprueba los servicios:
```bash
systemctl status ollama
systemctl status litellm
nvidia-smi
curl http://127.0.0.1:11434/api/tags
curl http://127.0.0.1:8000/health/liveliness
```
La configuración de LiteLLM se genera en el directorio personal del usuario
que ejecuta `sudo` y queda guardada como `~/litellm_config.yaml`.
## Instalación de OpenClaw
Para la configuración completa de OpenClaw con el servidor local, consulta
[OPENCLAW_LOCAL_SERVER.md](OPENCLAW_LOCAL_SERVER.md).
El script `install_openclaw.sh` utiliza el instalador oficial de OpenClaw y
está pensado para Linux y WSL2. Para instalarlo sin abrir el asistente inicial:
```bash
bash install_openclaw.sh
```
Para instalarlo y ejecutar el asistente inmediatamente:
```bash
bash install_openclaw.sh --onboard
```
Si la instalación se hizo sin asistente, puedes iniciarlo después y configurar
el servicio de Gateway:
```bash
openclaw onboard --install-daemon
```
Comprueba la instalación con:
```bash
openclaw --version
openclaw doctor
openclaw gateway status
```
Si OpenClaw devuelve respuestas vacías o de un token, ejecuta el reparador
local como el usuario que ejecuta OpenClaw (`ctello`, no `root`):
```bash
chmod +x fix_openclaw_local.sh
bash ./fix_openclaw_local.sh
```
El script respalda `openclaw.json`, fuerza el Gateway local, desactiva el
razonamiento predeterminado y la memoria semántica sin embeddings, inicializa
el workspace Git y reinicia el Gateway. No borra sesiones ni secretos.
Después crea una sesión nueva en el dashboard y prueba:
```text
Responde exactamente: SESION NUEVA OK
```
Para acceder al dashboard desde Windows mediante PowerShell, usa un túnel SSH
hacia el servidor Ubuntu:
```powershell
ssh -N -L 18789:127.0.0.1:18789 ctello@192.168.0.225
```
Mantén la sesión abierta y entra desde el navegador en:
```text
http://127.0.0.1:18789
```
La guía completa de acceso desde PuTTY, PowerShell y solución de problemas está
en [OPENCLAW_LOCAL_SERVER.md](OPENCLAW_LOCAL_SERVER.md).
## Respaldo y recuperación
Para ejecutar un diagnóstico independiente y de solo lectura del servidor usa
`ubuntu_server_diagnostic.sh`. Comprueba Ubuntu, hardware, servicios, puertos,
Ollama, LiteLLM, OpenClaw y UFW:
```bash
chmod +x ubuntu_server_diagnostic.sh
sudo ./ubuntu_server_diagnostic.sh --report /tmp/ubuntu-server-test.txt
```
Puedes cambiar los endpoints o el modelo esperado mediante variables de entorno:
```bash
OLLAMA_URL=http://127.0.0.1:11434 \
LITELLM_URL=http://127.0.0.1:8000 \
MODEL_NAME=qwen2.5-coder:14b \
sudo -E ./ubuntu_server_diagnostic.sh
```
Para guardar la configuración del servidor antes de realizar cambios, ejecuta
el script como `root`:
```bash
chmod +x backup_ubuntu_config.sh restore_ubuntu_config.sh
sudo ./backup_ubuntu_config.sh
```
El respaldo se guarda por defecto en `/var/backups` e incluye inventario de
hardware, paquetes, servicios, red, UFW, systemd, NVIDIA, Ollama, LiteLLM,
OpenClaw y configuraciones de usuario. No incluye modelos Ollama, el entorno
virtual de LiteLLM, bases de datos ni logs completos.
Las claves SSH, certificados privados y credenciales de OpenClaw quedan fuera
por defecto. Para incluirlos explícitamente, protege el archivo resultante:
```bash
sudo ./backup_ubuntu_config.sh --include-secrets
sudo chmod 600 /var/backups/ubuntu-config-*.tar.gz
```
En una instalación Ubuntu nueva, instala primero el sistema base y crea los
usuarios necesarios. Después copia el archivo de respaldo y usa el restaurador
en modo vista previa:
```bash
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz
```
Para aplicar la configuración:
```bash
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz --yes
```
Puedes reinstalar los paquetes APT y arrancar los servicios habilitados de la
máquina original de forma explícita:
```bash
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz \
--yes --install-packages --start-services
```
Revisa siempre los servicios y el firewall después de restaurar:
```bash
systemctl --failed
systemctl status ollama litellm
sudo ufw status verbose
nvidia-smi
```
## Tool calling y rutas de Windows
Cuando un modelo envía rutas de Windows dentro de argumentos JSON, las barras
invertidas deben estar escapadas. Por ejemplo:
```json
{
"path": "C:\\Users\\name\\file.txt"
}
```
Si las barras no están escapadas, el parser JSON puede fallar antes de que la
herramienta llegue a ejecutarse.
## Archivos principales
- `setup_llm_server.sh`: instala y configura Ollama, LiteLLM, NVIDIA y UFW.
- `install_openclaw.sh`: instala OpenClaw mediante el instalador oficial.
- `litellm_config.yaml`: configuración local opcional para LiteLLM.
- `tool_call_demo.py`: ejemplo local de manejo de llamadas a herramientas.
## Seguridad
El instalador expone Ollama en todas las interfaces y abre el puerto `8000`
en UFW. En un servidor conectado a Internet, limita esos puertos a la red
privada o protégelos detrás de un proxy con autenticación y HTTPS.
-316
View File
@@ -1,316 +0,0 @@
"""
Code Agent - Handles file operations, code execution, and testing
"""
import os
import json
import requests
import subprocess
import webbrowser
import time
from pathlib import Path
from datetime import datetime
from typing import Dict, Any
from dotenv import load_dotenv
load_dotenv()
# ============================================================================
# CONFIGURATION
# ============================================================================
LLM_SERVER = os.getenv("LLM_SERVER_URL", "http://localhost:8000")
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434")
LLM_FAST = os.getenv("LLM_FAST_MODEL", "phi")
LLM_POWER = os.getenv("LLM_POWER_MODEL", "deepseek-coder:33b")
# ============================================================================
# CODE AGENT CLASS
# ============================================================================
class CodeAgent:
"""Agent that modifies files, executes code, and tests in browser"""
def __init__(self, work_dir: str = "./workspace"):
self.work_dir = Path(work_dir)
self.work_dir.mkdir(exist_ok=True)
print(f"📁 Workspace: {self.work_dir.absolute()}")
# ========================================================================
# FILE OPERATIONS
# ========================================================================
def read_file(self, file_path: str) -> str:
"""Read file from workspace"""
file = self.work_dir / file_path
if not file.exists():
return f"❌ File not found: {file_path}"
try:
with open(file, 'r', encoding='utf-8') as f:
content = f.read()
print(f"✅ Read: {file_path} ({len(content)} bytes)")
return content
except Exception as e:
return f"❌ Error reading {file_path}: {e}"
def write_file(self, file_path: str, content: str) -> str:
"""Write/modify file in workspace"""
file = self.work_dir / file_path
file.parent.mkdir(parents=True, exist_ok=True)
try:
with open(file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✅ Wrote: {file_path}")
return f"File saved: {file_path}"
except Exception as e:
return f"❌ Error writing {file_path}: {e}"
def delete_file(self, file_path: str) -> str:
"""Delete file from workspace"""
file = self.work_dir / file_path
if not file.exists():
return f"❌ File not found: {file_path}"
try:
file.unlink()
return f"✅ Deleted: {file_path}"
except Exception as e:
return f"❌ Error deleting {file_path}: {e}"
def list_files(self) -> str:
"""List all files in workspace"""
files = list(self.work_dir.rglob("*"))
file_list = "\n".join([
f" {f.relative_to(self.work_dir)}"
for f in files if f.is_file()
])
return f"📁 Files in workspace:\n{file_list}" if file_list else "📁 No files"
# ========================================================================
# CODE EXECUTION
# ========================================================================
def execute_python(self, code: str, file_name: str = "exec.py") -> str:
"""Execute Python code"""
script_path = self.work_dir / file_name
try:
# Save script
with open(script_path, 'w', encoding='utf-8') as f:
f.write(code)
# Execute
result = subprocess.run(
["python3", str(script_path)],
capture_output=True,
text=True,
timeout=30,
cwd=self.work_dir
)
output = result.stdout + result.stderr
print(f"🐍 Python executed: {file_name}")
return output[:1000] # Limit output
except subprocess.TimeoutExpired:
return "❌ Timeout: Script took more than 30 seconds"
except Exception as e:
return f"❌ Error: {e}"
def execute_node(self, code: str, file_name: str = "exec.js") -> str:
"""Execute Node.js code"""
script_path = self.work_dir / file_name
try:
with open(script_path, 'w', encoding='utf-8') as f:
f.write(code)
result = subprocess.run(
["node", str(script_path)],
capture_output=True,
text=True,
timeout=30,
cwd=self.work_dir
)
output = result.stdout + result.stderr
print(f"📟 Node.js executed: {file_name}")
return output[:1000]
except FileNotFoundError:
return "❌ Node.js not installed"
except Exception as e:
return f"❌ Error: {e}"
def execute_shell(self, command: str) -> str:
"""Execute shell command (whitelist safe commands only)"""
allowed_commands = [
'ls', 'pwd', 'mkdir', 'rm', 'cp', 'mv',
'cat', 'grep', 'find', 'du', 'df',
'git', 'npm', 'pip', 'python'
]
cmd_name = command.split()[0] if command else ""
if cmd_name not in allowed_commands:
return f"❌ Command not allowed: {cmd_name}"
try:
result = subprocess.run(
command.split(),
capture_output=True,
text=True,
timeout=30,
cwd=self.work_dir
)
output = result.stdout + result.stderr
return output[:500]
except Exception as e:
return f"❌ Error: {e}"
# ========================================================================
# TESTING & BROWSER
# ========================================================================
def test_html(self, html_file: str) -> str:
"""Open HTML file in browser for testing"""
file_path = self.work_dir / html_file
if not file_path.exists():
return f"❌ File not found: {html_file}"
try:
url = f"file:///{file_path.absolute()}".replace("\\", "/")
webbrowser.open(url)
print(f"🌐 Opened in browser: {html_file}")
return f"✅ HTML opened: {html_file}"
except Exception as e:
return f"❌ Error: {e}"
def open_browser(self, url: str) -> str:
"""Open URL in browser"""
try:
webbrowser.open(url)
return f"✅ Browser opened: {url}"
except Exception as e:
return f"❌ Error: {e}"
# ========================================================================
# GIT OPERATIONS
# ========================================================================
def git_commit(self, message: str = None) -> str:
"""Auto-commit changes"""
try:
if not message:
message = f"Auto-commit from agent - {datetime.now().isoformat()}"
# Git add
result = subprocess.run(
['git', 'add', '.'],
cwd=self.work_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
return f"❌ Git add failed: {result.stderr}"
# Git commit
result = subprocess.run(
['git', 'commit', '-m', message],
cwd=self.work_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
return f"ℹ️ Nothing to commit"
return f"✅ Committed: {message}"
except Exception as e:
return f"❌ Error: {e}"
def git_push(self) -> str:
"""Push to remote repository"""
try:
result = subprocess.run(
['git', 'push'],
cwd=self.work_dir,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return "✅ Pushed to remote"
else:
return f"❌ Push failed: {result.stderr}"
except Exception as e:
return f"❌ Error: {e}"
# ========================================================================
# LLM INTERACTION
# ========================================================================
def call_llm(self, prompt: str, model: str = "phi") -> str:
"""Call LLM server"""
try:
response = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": model,
"prompt": prompt,
"stream": False
},
timeout=300
)
if response.status_code == 200:
return response.json()["response"]
else:
return f"❌ LLM Error: {response.status_code}"
except requests.exceptions.ConnectionError:
return f"❌ Cannot connect to {OLLAMA_URL}"
except Exception as e:
return f"❌ Error: {e}"
# ============================================================================
# GET AGENT TOOLS
# ============================================================================
def get_agent_tools(agent: CodeAgent) -> Dict[str, Any]:
"""Return dictionary of available tools"""
return {
"read_file": agent.read_file,
"write_file": agent.write_file,
"delete_file": agent.delete_file,
"execute_python": agent.execute_python,
"execute_node": agent.execute_node,
"execute_shell": agent.execute_shell,
"test_html": agent.test_html,
"open_browser": agent.open_browser,
"git_commit": agent.git_commit,
"git_push": agent.git_push,
"list_files": agent.list_files,
"call_llm": agent.call_llm,
}
# ============================================================================
# MAIN (for testing)
# ============================================================================
if __name__ == "__main__":
agent = CodeAgent()
# Test
print("\n🧪 Testing Agent...")
print(agent.list_files())
# Create test file
agent.write_file("test.txt", "Hello from agent!")
print(agent.read_file("test.txt"))
-247
View File
@@ -1,247 +0,0 @@
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from fastapi.responses import StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.cors import CORSMiddleware
import aiofiles
import base64
import requests
import json
import os
from pathlib import Path
from dotenv import load_dotenv
import asyncio
import io
from PIL import Image
import uuid
load_dotenv()
app = FastAPI(title="Code Agent Chat")
# CORS para que el frontend acceda
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Config
LLM_SERVER = os.getenv("LLM_SERVER_URL", "http://localhost:8000")
UPLOAD_DIR = Path("./uploads")
WORKSPACE_DIR = Path("./workspace")
UPLOAD_DIR.mkdir(exist_ok=True)
WORKSPACE_DIR.mkdir(exist_ok=True)
# Importar agent
from agent import CodeAgent, get_agent_tools
agent = CodeAgent(work_dir=str(WORKSPACE_DIR))
# ============================================================================
# ENDPOINTS
# ============================================================================
@app.get("/api/health")
async def health():
"""Verificar que el servidor está vivo"""
return {"status": "ok", "workspace": str(WORKSPACE_DIR.absolute())}
@app.get("/api/files")
async def list_files():
"""Listar archivos en workspace"""
files = []
for f in WORKSPACE_DIR.rglob("*"):
if f.is_file():
files.append({
"name": str(f.relative_to(WORKSPACE_DIR)),
"size": f.stat().st_size,
"type": f.suffix
})
return {"files": files}
@app.get("/api/file/{file_path:path}")
async def read_file(file_path: str):
"""Leer contenido de archivo"""
file = WORKSPACE_DIR / file_path
if not file.exists():
raise HTTPException(status_code=404, detail="Archivo no encontrado")
try:
with open(file, 'r') as f:
content = f.read()
return {"content": content, "name": file_path}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/chat")
async def chat(
message: str = Form(...),
image: UploadFile = File(None),
model: str = Form("phi")
):
"""
Endpoint principal de chat
- message: tu prompt
- image: imagen adjunta (opcional)
- model: "phi" (rápido) o "deepseek-coder:33b" (potente)
"""
# Procesar imagen si hay
image_data = None
image_base64 = None
if image:
try:
contents = await image.read()
image_base64 = base64.b64encode(contents).decode()
# Guardar imagen
image_path = UPLOAD_DIR / f"{uuid.uuid4()}.png"
with open(image_path, 'wb') as f:
f.write(contents)
image_data = f"[Imagen adjunta: {image.filename}]"
print(f"📸 Imagen procesada: {image.filename}")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error procesando imagen: {e}")
# Construir prompt para el LLM
full_prompt = f"""
{message}
{f"Contexto de imagen: {image_data}" if image_data else ""}
Tienes acceso a estas herramientas:
- read_file(file_path): Leer archivo
- write_file(file_path, content): Escribir archivo
- execute_python(code): Ejecutar Python
- execute_node(code): Ejecutar Node.js
- test_html(file_name): Servir y testear HTML
- open_browser(url): Abrir navegador
- list_files(): Listar archivos
Responde en JSON con tu pensamiento y la acción a ejecutar.
Ejemplo:
{{"thought": "Voy a leer el archivo", "action": "read_file", "args": {{"file_path": "app.js"}}}}
"""
# Llamar LLM
try:
response = requests.post(
f"{LLM_SERVER}/code",
json={
"message": full_prompt,
"model": model
},
timeout=300,
stream=False
)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=response.text
)
llm_response = response.json()["response"]
# Parsear JSON y ejecutar acción
result = process_agent_response(llm_response)
return {
"message": message,
"image": image.filename if image else None,
"model": model,
"llm_response": llm_response,
"action_result": result,
"image_base64": image_base64
}
except requests.exceptions.ConnectionError:
raise HTTPException(
status_code=503,
detail=f"No se puede conectar a LLM Server: {LLM_SERVER}"
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/chat-stream")
async def chat_stream(
message: str = Form(...),
image: UploadFile = File(None),
model: str = Form("phi")
):
"""Versión streaming (respuestas en tiempo real)"""
image_data = None
if image:
contents = await image.read()
image_path = UPLOAD_DIR / f"{uuid.uuid4()}.png"
with open(image_path, 'wb') as f:
f.write(contents)
image_data = f"[Imagen: {image.filename}]"
full_prompt = f"{message}\n{image_data if image_data else ''}"
async def generate():
try:
# Streaming desde LLM
response = requests.post(
f"{LLM_SERVER}/code",
json={
"message": full_prompt,
"model": model
},
timeout=300,
stream=True
)
for line in response.iter_lines():
if line:
yield line + b'\n'
except Exception as e:
yield json.dumps({"error": str(e)}).encode() + b'\n'
return StreamingResponse(generate(), media_type="text/event-stream")
def process_agent_response(response: str) -> dict:
"""Parsear respuesta del LLM y ejecutar acción"""
tools = get_agent_tools(agent)
try:
# Extraer JSON
json_start = response.find('{')
json_end = response.rfind('}') + 1
json_str = response[json_start:json_end]
action = json.loads(json_str)
except:
return {"error": "No se pudo parsear la respuesta", "raw": response[:200]}
action_name = action.get("action") or action.get("tool")
args = action.get("args", {})
if action_name not in tools:
return {"error": f"Acción desconocida: {action_name}"}
try:
result = tools[action_name](**args)
return {"success": True, "action": action_name, "result": result}
except Exception as e:
return {"error": f"Error ejecutando {action_name}: {str(e)}"}
# ============================================================================
# SERVIR STATIC FILES (UI Frontend)
# ============================================================================
app.mount("/", StaticFiles(directory="static", html=True), name="static")
if __name__ == "__main__":
import uvicorn
print(f"🚀 Backend en http://localhost:5000")
print(f"📁 Workspace: {WORKSPACE_DIR.absolute()}")
uvicorn.run(app, host="0.0.0.0", port=5000)
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
readonly DEFAULT_OUTPUT_DIR="/var/backups"
include_secrets=false
output_dir="$DEFAULT_OUTPUT_DIR"
show_help() {
cat <<EOF
Uso: sudo $SCRIPT_NAME [opciones]
Crea un respaldo comprimido de la configuración de Ubuntu para replicarla en
otra instalación. No incluye modelos Ollama, entornos virtuales ni bases de datos.
Opciones:
--output-dir DIR Directorio donde guardar el archivo .tar.gz.
--include-secrets Incluye claves SSH, certificados privados y secretos
de OpenClaw. El archivo resultante debe protegerse.
-h, --help Muestra esta ayuda.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--output-dir)
[[ $# -ge 2 ]] || { echo "Falta DIR para --output-dir" >&2; exit 2; }
output_dir="$2"
shift 2
;;
--include-secrets)
include_secrets=true
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Opción desconocida: $1" >&2
show_help >&2
exit 2
;;
esac
done
if [[ "${EUID}" -ne 0 ]]; then
echo "Ejecuta este script con sudo o como root." >&2
exit 1
fi
command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; }
command -v gzip >/dev/null || { echo "gzip es obligatorio." >&2; exit 1; }
hostname_value="$(hostname -s 2>/dev/null || echo unknown)"
timestamp="$(date +%Y%m%d-%H%M%S)"
backup_name="ubuntu-config-${hostname_value}-${timestamp}"
work_dir="$(mktemp -d)"
archive_path="${output_dir}/${backup_name}.tar.gz"
cleanup() {
rm -rf "$work_dir"
}
trap cleanup EXIT
mkdir -p "$work_dir/metadata" "$work_dir/files" "$work_dir/packages" "$work_dir/services"
mkdir -p "$output_dir"
umask 077
copy_path() {
local source="$1"
local destination="$work_dir/files$source"
if [[ -e "$source" || -L "$source" ]]; then
mkdir -p "$(dirname "$destination")"
cp -a "$source" "$destination"
fi
}
capture() {
local name="$1"
shift
"$@" > "$work_dir/metadata/$name.txt" 2>&1 || true
}
capture_shell() {
local name="$1"
local command_text="$2"
bash -c "$command_text" > "$work_dir/metadata/$name.txt" 2>&1 || true
}
printf 'Creando respaldo en %s\n' "$archive_path"
printf '%s\n' "hostname=$hostname_value" "created_at=$(date --iso-8601=seconds)" \
"include_secrets=$include_secrets" > "$work_dir/metadata/backup-info.txt"
capture_shell os-release 'cat /etc/os-release'
capture_shell kernel 'uname -a'
capture_shell hardware 'lscpu; echo; free -h; echo; lsblk -f'
capture_shell disks 'df -hT; echo; findmnt'
capture_shell network 'ip -brief address; echo; ip route; echo; resolvectl status 2>/dev/null || true'
capture_shell users 'getent passwd | awk -F: '\''$3 >= 1000 || $1 == "root" {print $1 ":" $3 ":" $4 ":" $6}'\'''
capture_shell mounts 'mount'
# Registrar nombres, no valores que puedan contener credenciales.
capture_shell environment 'printenv | cut -d= -f1 | sort -u'
capture_shell nvidia 'command -v nvidia-smi && nvidia-smi -q || true'
capture_shell ollama 'command -v ollama && ollama list || true'
capture_shell versions 'command -v node && node --version || true; command -v npm && npm --version || true; command -v python3 && python3 --version || true; command -v ollama && ollama --version || true; command -v openclaw && openclaw --version || true'
capture_shell ufw 'command -v ufw && ufw status verbose || true'
capture_shell iptables 'command -v iptables-save && iptables-save || true'
capture_shell sysctl 'sysctl -a 2>/dev/null'
capture_shell timers 'systemctl list-timers --all --no-pager'
capture_shell enabled-services 'systemctl list-unit-files --state=enabled --no-legend --no-pager'
capture_shell failed-services 'systemctl --failed --no-pager'
if command -v dpkg-query >/dev/null; then
dpkg-query -W -f='${binary:Package}\t${Version}\n' > "$work_dir/packages/dpkg-status.tsv" || true
fi
if command -v apt-mark >/dev/null; then
apt-mark showmanual | sort > "$work_dir/packages/apt-manual.txt" || true
fi
if command -v snap >/dev/null; then
snap list > "$work_dir/packages/snap-list.txt" 2>&1 || true
fi
if command -v pip3 >/dev/null; then
pip3 freeze > "$work_dir/packages/pip3-freeze.txt" 2>&1 || true
fi
systemctl list-unit-files --type=service --no-legend --no-pager \
> "$work_dir/services/all-service-units.txt" 2>&1 || true
systemctl list-unit-files --state=enabled --type=service --no-legend --no-pager \
| awk '{print $1}' | sort -u > "$work_dir/services/enabled-service-names.txt" || true
# Configuración del sistema y de los servicios usados por este proyecto.
for path in \
/etc/apt \
/etc/default \
/etc/environment \
/etc/fstab \
/etc/hostname \
/etc/hosts \
/etc/issue \
/etc/netplan \
/etc/NetworkManager \
/etc/systemd/system \
/etc/sysctl.d \
/etc/modprobe.d \
/etc/ufw \
/etc/ollama \
/etc/nvidia \
/etc/profile.d
do
copy_path "$path"
done
# Configuraciones de usuario que no suelen contener credenciales.
while IFS=: read -r username _ uid _ _ home _; do
[[ -d "$home" ]] || continue
[[ "$uid" -ge 1000 || "$username" == root ]] || continue
for relative_path in .bashrc .profile .config/systemd/user litellm_config.yaml; do
copy_path "$home/$relative_path"
done
printf '%s\t%s\t%s\t%s\n' "$username" "$uid" "$(id -g "$username" 2>/dev/null || echo 0)" "$home" \
>> "$work_dir/metadata/users.tsv"
done < <(getent passwd)
if [[ "$include_secrets" == true ]]; then
printf 'Incluyendo archivos sensibles: SSH, certificados y configuración privada de OpenClaw.\n'
for path in /etc/ssh /etc/ssl/private /etc/letsencrypt; do
copy_path "$path"
done
while IFS=: read -r username _ uid _ _ home _; do
[[ -d "$home" ]] || continue
[[ "$uid" -ge 1000 || "$username" == root ]] || continue
for relative_path in .ssh .openclaw .npmrc; do
copy_path "$home/$relative_path"
done
done < <(getent passwd)
else
cat > "$work_dir/metadata/excluded-secrets.txt" <<EOF
No se incluyeron secretos. Para incluirlos explícitamente, usa:
sudo $SCRIPT_NAME --include-secrets
Excluidos por defecto: /etc/ssh, /etc/ssl/private, /etc/letsencrypt, ~/.ssh,
~/.openclaw y ~/.npmrc.
EOF
fi
cat > "$work_dir/RESTORE.txt" <<EOF
Este archivo fue generado por $SCRIPT_NAME.
1. Instala Ubuntu con la misma arquitectura y crea el usuario de servicio.
2. Copia este archivo a la nueva máquina.
3. Ejecuta restore_ubuntu_config.sh con este archivo y revisa los cambios.
4. Reinstala los modelos Ollama y valida los servicios antes de exponerlos.
El respaldo no contiene modelos Ollama, /opt/litellm-env, bases de datos ni
logs completos. Si no usaste --include-secrets, debes restaurar las credenciales
manualmente.
EOF
printf 'Archivos incluidos:\n'
find "$work_dir" -type f -printf '%P\n' | sort > "$work_dir/metadata/file-list.txt"
cat "$work_dir/metadata/file-list.txt"
tar -C "$work_dir" -czf "$archive_path" .
chmod 600 "$archive_path"
printf '\nRespaldo creado: %s\n' "$archive_path"
printf 'Protección: permisos 600\n'
-74
View File
@@ -1,74 +0,0 @@
{
"server": {
"host": "0.0.0.0",
"api_port": 8000,
"web_port": 5000,
"workers": 4,
"debug": false,
"environment": "production"
},
"models": {
"fast": {
"name": "phi",
"description": "Phi 2.7B - Rápido",
"vram_required_gb": 2,
"tokens_per_second": 50
},
"power": {
"name": "deepseek-coder:33b",
"description": "DeepSeek 33B - Potente",
"vram_required_gb": 18,
"tokens_per_second": 6
}
},
"gpu": {
"memory_fraction": 0.9,
"max_batch_size": 8,
"enable_cuda": true,
"device_id": 0
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"log_dir": "./logs",
"max_file_size_mb": 10,
"backup_count": 10
},
"database": {
"type": "sqlite",
"path": "./data/llm_history.db",
"enable_history": true
},
"security": {
"enable_rate_limit": true,
"rate_limit_requests": 100,
"rate_limit_period_seconds": 60,
"require_api_key": false,
"allowed_origins": ["*"]
},
"workspace": {
"path": "./workspace",
"max_file_size_mb": 100,
"allowed_extensions": [".py", ".js", ".html", ".css", ".json", ".txt", ".md"]
},
"api_endpoints": {
"health": "/api/health",
"models": "/api/models",
"chat": "/api/chat",
"files": "/api/files",
"metrics": "/api/metrics"
},
"ollama": {
"host": "localhost",
"port": 11434,
"timeout_seconds": 300,
"auto_download_models": true
},
"features": {
"enable_git_commit": true,
"enable_code_execution": true,
"enable_file_operations": true,
"enable_browser_testing": true,
"enable_metrics": true
}
}
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
readonly OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
readonly CONFIG_FILE="${OPENCLAW_CONFIG_PATH:-$OPENCLAW_HOME/openclaw.json}"
readonly WORKSPACE_DIR="$OPENCLAW_HOME/workspace"
readonly BACKUP_DIR="$OPENCLAW_HOME/config-backups"
show_help() {
cat <<EOF
Uso: $SCRIPT_NAME
Corrige la configuración local de OpenClaw para este servidor:
- usa Gateway local;
- desactiva thinking por defecto;
- desactiva memoria semántica sin proveedor de embeddings;
- inicializa el workspace Git requerido por OpenClaw;
- reinicia el Gateway de usuario;
- verifica LiteLLM y el modelo local.
El script no borra sesiones, no elimina secretos y no publica archivos.
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
show_help
exit 0
fi
if ! command -v openclaw >/dev/null 2>&1; then
echo "Error: openclaw no está disponible en PATH." >&2
echo "Ejecuta: source ~/.bashrc" >&2
exit 1
fi
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Error: no existe la configuración: $CONFIG_FILE" >&2
exit 1
fi
mkdir -p "$BACKUP_DIR"
backup_file="$BACKUP_DIR/openclaw.json.$(date +%Y%m%d-%H%M%S).bak"
cp -p "$CONFIG_FILE" "$backup_file"
chmod 600 "$backup_file"
echo "Respaldo de configuración: $backup_file"
echo "Aplicando configuración local..."
openclaw config set gateway.mode local
openclaw config set agents.defaults.thinkingDefault off
openclaw config set memory.search.enabled false
if [[ ! -d "$WORKSPACE_DIR/.git" ]]; then
mkdir -p "$WORKSPACE_DIR"
git -C "$WORKSPACE_DIR" init
git -C "$WORKSPACE_DIR" config user.name "OpenClaw"
git -C "$WORKSPACE_DIR" config user.email "openclaw@localhost"
touch "$WORKSPACE_DIR/.gitkeep"
git -C "$WORKSPACE_DIR" add .gitkeep
git -C "$WORKSPACE_DIR" commit -m "Initialize OpenClaw workspace" >/dev/null
echo "Workspace Git inicializado: $WORKSPACE_DIR"
else
echo "Workspace Git ya estaba inicializado: $WORKSPACE_DIR"
fi
if command -v systemctl >/dev/null 2>&1; then
if systemctl --user daemon-reload 2>/dev/null; then
systemctl --user enable openclaw-gateway.service >/dev/null 2>&1 || true
systemctl --user restart openclaw-gateway.service
else
echo "Aviso: no se pudo conectar al bus systemd de usuario." >&2
echo "Cierra esta sesión SSH y vuelve a entrar como $(id -un)." >&2
echo "Después ejecuta: systemctl --user restart openclaw-gateway.service" >&2
fi
fi
echo
echo "Verificando configuración..."
openclaw config get gateway.mode
openclaw config get agents.defaults.thinkingDefault
openclaw config get agents.defaults.model.primary
openclaw config get models.providers.litellm.baseUrl
if command -v curl >/dev/null 2>&1; then
if curl --fail --silent --show-error --max-time 10 \
http://127.0.0.1:8000/v1/models | grep -q 'qwen2.5-coder:14b'; then
echo "OK: LiteLLM publica qwen2.5-coder:14b"
else
echo "ERROR: LiteLLM no publica qwen2.5-coder:14b" >&2
exit 1
fi
fi
if command -v systemctl >/dev/null 2>&1; then
systemctl --user is-active --quiet openclaw-gateway.service && \
echo "OK: Gateway activo" || \
echo "Aviso: Gateway no está activo; revisa openclaw gateway status" >&2
fi
cat <<EOF
Configuración aplicada.
IMPORTANTE: la sesión antigua puede seguir teniendo un historial demasiado
largo. En la web crea una sesión nueva y prueba:
Responde exactamente: SESION NUEVA OK
No continúes usando la sesión que mostraba estimatedInput=49724 y output=1.
Comprobación manual:
openclaw gateway status
openclaw doctor
EOF
+10
View File
@@ -0,0 +1,10 @@
model_list:
- model_name: qwen2.5-coder:14b
litellm_params:
model: ollama_chat/qwen2.5-coder:14b
api_base: http://127.0.0.1:11434
max_tokens: 4096
litellm_settings:
drop_params: true
json_to_tool_call: true
-22
View File
@@ -1,22 +0,0 @@
# API & Web Framework
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
aiofiles==23.2.1
# LLM & AI
langchain==0.1.0
requests==2.31.0
# Data Processing
pillow==10.1.0
pydantic==2.5.0
# Configuration
python-dotenv==1.0.0
# Development (opcional)
pytest==7.4.3
pytest-asyncio==0.21.1
black==23.12.0
flake8==6.1.0
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
archive_path=""
confirmed=false
install_packages=false
start_services=false
show_help() {
cat <<EOF
Uso: sudo $SCRIPT_NAME ARCHIVO.tar.gz [opciones]
Restaura configuraciones de un respaldo creado por backup_ubuntu_config.sh.
Por seguridad, sin --yes solo muestra lo que se restauraría.
Opciones:
--yes Ejecuta la restauración.
--install-packages Instala los paquetes marcados como manuales en el respaldo.
--start-services Habilita y arranca los servicios que estaban habilitados.
-h, --help Muestra esta ayuda.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--yes)
confirmed=true
shift
;;
--install-packages)
install_packages=true
shift
;;
--start-services)
start_services=true
shift
;;
-h|--help)
show_help
exit 0
;;
-*)
echo "Opción desconocida: $1" >&2
show_help >&2
exit 2
;;
*)
if [[ -n "$archive_path" ]]; then
echo "Solo se admite un archivo de respaldo." >&2
exit 2
fi
archive_path="$1"
shift
;;
esac
done
if [[ -z "$archive_path" ]]; then
echo "Debes indicar un archivo .tar.gz." >&2
show_help >&2
exit 2
fi
if [[ "${EUID}" -ne 0 ]]; then
echo "Ejecuta este script con sudo o como root." >&2
exit 1
fi
[[ -f "$archive_path" ]] || { echo "No existe: $archive_path" >&2; exit 1; }
command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; }
work_dir="$(mktemp -d)"
cleanup() {
rm -rf "$work_dir"
}
trap cleanup EXIT
tar -xzf "$archive_path" -C "$work_dir" --no-same-owner
if [[ ! -d "$work_dir/files" || ! -f "$work_dir/metadata/backup-info.txt" ]]; then
echo "El archivo no parece un respaldo válido de backup_ubuntu_config.sh." >&2
exit 1
fi
printf 'Respaldo: %s\n' "$archive_path"
cat "$work_dir/metadata/backup-info.txt"
printf '\nArchivos de configuración que se restaurarían:\n'
find "$work_dir/files" -mindepth 1 -maxdepth 8 -printf '%P\n' | sort | sed -n '1,200p'
if [[ "$confirmed" != true ]]; then
cat <<EOF
Vista previa solamente. Para aplicar estos cambios:
sudo $SCRIPT_NAME "$archive_path" --yes
Opciones adicionales:
--install-packages Reinstala paquetes APT manuales.
--start-services Habilita y arranca los servicios respaldados.
EOF
exit 0
fi
if [[ "$install_packages" == true && -s "$work_dir/packages/apt-manual.txt" ]]; then
export DEBIAN_FRONTEND=noninteractive
apt-get update
xargs -r apt-get install -y --no-install-recommends < "$work_dir/packages/apt-manual.txt"
fi
if [[ -d "$work_dir/files/etc" ]]; then
cp -a "$work_dir/files/etc/." /etc/
fi
if [[ -d "$work_dir/files/home" ]]; then
cp -a "$work_dir/files/home/." /home/
fi
if [[ -d "$work_dir/files/root" ]]; then
cp -a "$work_dir/files/root/." /root/
fi
if [[ -s "$work_dir/metadata/users.tsv" ]]; then
while IFS=$'\t' read -r username _ _ home; do
[[ -n "$username" && -d "$home" ]] || continue
if id "$username" >/dev/null 2>&1; then
chown -R "$username:$(id -gn "$username")" "$home" 2>/dev/null || true
else
echo "Aviso: no existe el usuario $username; revisa $home manualmente." >&2
fi
done < "$work_dir/metadata/users.tsv"
fi
systemctl daemon-reload
if [[ "$start_services" == true && -s "$work_dir/services/enabled-service-names.txt" ]]; then
while IFS= read -r service; do
[[ -n "$service" ]] || continue
systemctl enable "$service" 2>/dev/null || echo "Aviso: no se pudo habilitar $service" >&2
done < "$work_dir/services/enabled-service-names.txt"
systemctl restart ollama 2>/dev/null || true
systemctl restart litellm 2>/dev/null || true
systemctl restart openclaw-gateway 2>/dev/null || true
fi
if command -v ufw >/dev/null 2>&1; then
ufw --force reload 2>/dev/null || true
fi
cat <<EOF
Restauración completada.
Revisa antes de exponer el servidor:
systemctl --failed
systemctl status ollama litellm
ufw status verbose
nvidia-smi
Los modelos Ollama y el entorno virtual de LiteLLM no forman parte del respaldo.
Debes reinstalarlos o descargarlos por separado.
EOF
-54
View File
@@ -1,54 +0,0 @@
#!/bin/bash
################################################################################
# BACKUP SCRIPT
# Realiza backup de modelos y configuración
################################################################################
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
BACKUP_DIR="$PROJECT_DIR/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $@"; }
log_success() { echo -e "${GREEN}✅ $@${NC}"; }
log "Iniciando backup..."
mkdir -p "$BACKUP_DIR"
# Backup configuración
log "Haciendo backup de configuración..."
tar -czf "$BACKUP_DIR/config_$TIMESTAMP.tar.gz" \
-C "$PROJECT_DIR" \
config/ \
.env \
requirements.txt \
2>/dev/null || true
# Backup modelos (OPCIONAL - muy grandes)
read -p "¿Hacer backup de modelos Ollama? (y/n - muy grande, ~20GB): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
log "Haciendo backup de modelos (esto tardará)..."
tar -czf "$BACKUP_DIR/models_$TIMESTAMP.tar.gz" \
/home/charle/.ollama/models \
2>/dev/null || true
fi
# Backup workspace
log "Haciendo backup de workspace..."
tar -czf "$BACKUP_DIR/workspace_$TIMESTAMP.tar.gz" \
-C "$PROJECT_DIR" \
workspace/ \
2>/dev/null || true
log_success "Backups completados en: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"
-88
View File
@@ -1,88 +0,0 @@
#!/bin/bash
################################################################################
# HEALTH CHECK SCRIPT
# Verifica que todo funcione correctamente
################################################################################
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
FAILURES=0
check() {
local name=$1
local cmd=$2
local expected=$3
echo -n "Verificando $name... "
if eval "$cmd" &>/dev/null; then
echo -e "${GREEN}✅${NC}"
return 0
else
echo -e "${RED}❌${NC}"
((FAILURES++))
return 1
fi
}
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} LLM SERVER HEALTH CHECK${NC}"
echo -e "${BLUE}========================================${NC}\n"
# Sistema
echo -e "${YELLOW}Sistema:${NC}"
check "Ubuntu 22.04" "grep -q '22.04' /etc/os-release"
check "Internet" "ping -c 1 8.8.8.8"
# Drivers & GPU
echo -e "\n${YELLOW}GPU & Drivers:${NC}"
check "NVIDIA Driver" "command -v nvidia-smi"
check "CUDA 12.3" "command -v nvcc"
check "RTX 3090" "nvidia-smi | grep -q 'RTX 3090'"
check "cuDNN" "ldconfig -p | grep -q cudnn"
# Software
echo -e "\n${YELLOW}Software:${NC}"
check "Python 3.11" "python3.11 --version"
check "Docker" "command -v docker"
check "Git" "command -v git"
# Services
echo -e "\n${YELLOW}Servicios:${NC}"
check "Ollama service" "sudo systemctl is-active ollama"
check "API service" "sudo systemctl is-active llm-api"
# Conectividad
echo -e "\n${YELLOW}API Connectivity:${NC}"
check "Ollama API" "curl -s http://localhost:11434/api/tags"
check "FastAPI" "curl -s http://localhost:8000/health"
# Modelos
echo -e "\n${YELLOW}Modelos Ollama:${NC}"
check "Phi disponible" "curl -s http://localhost:11434/api/tags | grep -q 'phi'"
check "DeepSeek disponible" "curl -s http://localhost:11434/api/tags | grep -q 'deepseek'"
# Disk Space
echo -e "\n${YELLOW}Espacio en Disco:${NC}"
root_usage=$(df / | awk 'NR==2 {print int($5)}')
if [ "$root_usage" -lt 90 ]; then
echo -e "Uso de disco (root): ${GREEN}${root_usage}%${NC}"
else
echo -e "Uso de disco (root): ${RED}${root_usage}%${NC}"
((FAILURES++))
fi
# Summary
echo -e "\n${BLUE}========================================${NC}"
if [ "$FAILURES" -eq 0 ]; then
echo -e "${GREEN}✅ TODOS LOS CHECKS PASARON${NC}"
exit 0
else
echo -e "${RED}❌ $FAILURES CHECKS FALLARON${NC}"
exit 1
fi
-534
View File
@@ -1,534 +0,0 @@
#!/bin/bash
################################################################################
# LLM SERVER DEPLOYMENT SCRIPT
# Ubuntu 22.04 LTS + ASUS H510M + RTX 3090
#
# Uso: bash scripts/install.sh [--quick] [--gpu-only]
#
# Opciones:
# --quick Salta verificaciones lentas
# --gpu-only Solo instala GPU drivers (para re-install)
################################################################################
set -e # Exit si hay error
# Colores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Variables
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
WORKSPACE_DIR="$PROJECT_DIR/workspace"
VENV_DIR="$PROJECT_DIR/venv"
LOG_FILE="$PROJECT_DIR/logs/install.log"
# Crear directorio de logs
mkdir -p "$PROJECT_DIR/logs"
# Function: Log con timestamp
log() {
local level=$1
shift
local message="$@"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "${BLUE}[$timestamp]${NC} ${level}: ${message}" | tee -a "$LOG_FILE"
}
# Function: Log success
log_success() {
echo -e "${GREEN}✅ $@${NC}" | tee -a "$LOG_FILE"
}
# Function: Log error
log_error() {
echo -e "${RED}❌ $@${NC}" | tee -a "$LOG_FILE"
}
# Function: Log warning
log_warning() {
echo -e "${YELLOW}⚠️ $@${NC}" | tee -a "$LOG_FILE"
}
# Function: Check command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function: Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "Este script debe ejecutarse con sudo"
exit 1
fi
}
################################################################################
# MAIN INSTALLATION
################################################################################
main() {
local quick_mode=false
local gpu_only=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--quick)
quick_mode=true
shift
;;
--gpu-only)
gpu_only=true
shift
;;
*)
log_error "Opción desconocida: $1"
usage
exit 1
;;
esac
done
log "LOG" "=========================================="
log "LOG" "LLM Server Installation"
log "LOG" "=========================================="
log "LOG" "Project Dir: $PROJECT_DIR"
log "LOG" "Workspace: $WORKSPACE_DIR"
log "LOG" "Quick Mode: $quick_mode"
log "LOG" "GPU Only: $gpu_only"
log "LOG" "=========================================="
# Checks iniciales
check_root
check_os
check_hardware
if [ "$gpu_only" = false ]; then
install_dependencies
install_docker
fi
install_nvidia_drivers
install_cuda_toolkit
install_ollama
setup_python_venv
create_services
if [ "$quick_mode" = false ]; then
download_models
fi
setup_directories
generate_config
log_success "=========================================="
log_success "✨ INSTALACIÓN COMPLETADA"
log_success "=========================================="
print_next_steps
}
################################################################################
# FUNCIONES AUXILIARES
################################################################################
check_os() {
log "LOG" "Verificando Sistema Operativo..."
if [ ! -f /etc/os-release ]; then
log_error "No se pudo detectar el SO"
exit 1
fi
. /etc/os-release
if [[ "$ID" != "ubuntu" ]]; then
log_error "Este script solo soporta Ubuntu"
exit 1
fi
if [[ "$VERSION_ID" != "22.04" ]]; then
log_warning "Se recomienda Ubuntu 22.04, detectado: $VERSION_ID"
fi
log_success "Ubuntu $VERSION_ID detectado"
}
check_hardware() {
log "LOG" "Verificando Hardware..."
# Check GPU
if ! command_exists nvidia-smi; then
log_warning "nvidia-smi no disponible aún (se instalará)"
else
gpu_info=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader)
log_success "GPU detectada: $gpu_info"
fi
# Check CPU
cpu_count=$(nproc)
log_success "CPU: $cpu_count cores"
# Check RAM
ram_gb=$(free -h | awk '/^Mem:/ {print $2}')
log_success "RAM: $ram_gb"
# Check Disk
disk_info=$(df -h / | awk 'NR==2 {print $2}')
log_success "Disco: $disk_info disponible"
}
install_dependencies() {
log "LOG" "Instalando dependencias del sistema..."
apt update
apt install -y \
build-essential \
git \
wget \
curl \
htop \
nano \
openssh-server \
python3.11 \
python3.11-venv \
python3.11-dev \
pkg-config \
libssl-dev \
libffi-dev
log_success "Dependencias instaladas"
}
install_docker() {
log "LOG" "Instalando Docker..."
if command_exists docker; then
log_success "Docker ya está instalado"
return
fi
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
sh /tmp/get-docker.sh
# Agregar usuario al grupo docker
if id "charle" &>/dev/null; then
usermod -aG docker charle
log_success "Usuario 'charle' agregado al grupo docker"
fi
systemctl start docker
systemctl enable docker
log_success "Docker instalado"
}
install_nvidia_drivers() {
log "LOG" "Instalando NVIDIA Drivers..."
if command_exists nvidia-smi; then
current_driver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -1)
log_success "Driver NVIDIA $current_driver ya instalado"
return
fi
# Agregar repositorio NVIDIA
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A4B469963BF863CC 2>&1 | grep -v "Warning" || true
add-apt-repository "deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/ /" 2>&1 | grep -v "already" || true
apt update
apt install -y cuda-drivers
log_success "NVIDIA Drivers instalados"
log_warning "Se recomienda reiniciar: sudo reboot"
}
install_cuda_toolkit() {
log "LOG" "Instalando CUDA Toolkit 12.3..."
if [ -d "/usr/local/cuda-12.3" ]; then
log_success "CUDA 12.3 ya está instalado"
return
fi
log "LOG" "Descargando CUDA 12.3.0..."
cd /tmp
wget -q https://developer.download.nvidia.com/compute/cuda/12.3.0/local_installers/cuda_12.3.0_545.23.06_linux.run \
-O cuda_12.3.0_545.23.06_linux.run
log "LOG" "Instalando CUDA (esto puede tardar 10-15 minutos)..."
chmod +x cuda_12.3.0_545.23.06_linux.run
./cuda_12.3.0_545.23.06_linux.run --silent --driver=false --toolkit
# Configurar PATH
if ! grep -q "cuda-12.3" /root/.bashrc; then
cat >> /root/.bashrc << 'EOF'
# CUDA 12.3
export PATH=/usr/local/cuda-12.3/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-12.3/lib64:$LD_LIBRARY_PATH
EOF
fi
source /root/.bashrc
# Verificar
if command_exists nvcc; then
cuda_version=$(nvcc --version | grep "release" | awk '{print $5}')
log_success "CUDA $cuda_version instalado"
fi
# Install cuDNN
log "LOG" "Instalando cuDNN..."
apt install -y libcudnn8
log_success "CUDA Toolkit instalado"
}
install_ollama() {
log "LOG" "Instalando Ollama..."
if command_exists ollama; then
log_success "Ollama ya está instalado"
else
curl https://ollama.ai/install.sh | sh
log_success "Ollama instalado"
fi
# Configurar servicio systemd
log "LOG" "Configurando servicio Ollama..."
mkdir -p /etc/systemd/system
cat > /etc/systemd/system/ollama.service << 'EOF'
[Unit]
Description=Ollama
After=network-online.target
[Service]
ExecStart=/usr/local/bin/ollama serve
User=charle
Group=charle
Restart=always
RestartSec=3
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_MODELS=/home/charle/.ollama/models"
Environment="OLLAMA_NUM_GPU=1"
[Install]
WantedBy=default.target
EOF
systemctl daemon-reload
systemctl enable ollama
systemctl restart ollama
sleep 2
if systemctl is-active --quiet ollama; then
log_success "Servicio Ollama activo"
else
log_error "Error iniciando Ollama"
fi
}
setup_python_venv() {
log "LOG" "Creando Python Virtual Environment..."
if [ -d "$VENV_DIR" ]; then
log_success "VEnv ya existe"
return
fi
python3.11 -m venv "$VENV_DIR"
source "$VENV_DIR/bin/activate"
pip install --upgrade pip setuptools wheel
pip install \
fastapi \
uvicorn \
python-multipart \
aiofiles \
pillow \
python-dotenv \
requests \
langchain \
pydantic
log_success "Python VEnv configurado"
}
create_services() {
log "LOG" "Creando systemd services..."
# API Service
cat > /etc/systemd/system/llm-api.service << EOF
[Unit]
Description=LLM API Server
After=network.target ollama.service
[Service]
Type=simple
User=charle
WorkingDirectory=$PROJECT_DIR
Environment="PATH=$VENV_DIR/bin"
Environment="OLLAMA_URL=http://localhost:11434"
ExecStart=$VENV_DIR/bin/python $PROJECT_DIR/backend.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable llm-api
log_success "Systemd services creados"
}
download_models() {
log "LOG" "Descargando modelos (esto puede tardar 20-30 minutos)..."
log_warning "Esto es OPCIONAL. Presiona Ctrl+C para cancelar."
sleep 5
source "$VENV_DIR/bin/activate"
# Esperar a que Ollama esté listo
for i in {1..30}; do
if curl -s http://localhost:11434/api/tags > /dev/null; then
log_success "Ollama está listo"
break
fi
log "LOG" "Esperando Ollama... ($i/30)"
sleep 2
done
log "LOG" "Descargando Phi..."
ollama pull phi:latest
log "LOG" "Descargando DeepSeek Coder 33B..."
ollama pull deepseek-coder:33b
log_success "Modelos descargados"
}
setup_directories() {
log "LOG" "Creando directorios..."
mkdir -p "$WORKSPACE_DIR"
mkdir -p "$PROJECT_DIR/uploads"
mkdir -p "$PROJECT_DIR/logs"
# Cambiar permisos
chown -R charle:charle "$PROJECT_DIR"
chmod -R 755 "$PROJECT_DIR"
log_success "Directorios creados"
}
generate_config() {
log "LOG" "Generando archivos de configuración..."
# .env file
if [ ! -f "$PROJECT_DIR/.env" ]; then
cat > "$PROJECT_DIR/.env" << EOF
# LLM Server Configuration
LLM_SERVER_URL=http://localhost:8000
LLM_FAST_MODEL=phi
LLM_POWER_MODEL=deepseek-coder:33b
OLLAMA_URL=http://localhost:11434
WORKSPACE_DIR=$WORKSPACE_DIR
# Server
HOST=0.0.0.0
API_PORT=8000
WEB_PORT=5000
# Logging
LOG_LEVEL=INFO
EOF
log_success ".env creado"
fi
# Config JSON
if [ ! -f "$PROJECT_DIR/config/server.json" ]; then
mkdir -p "$PROJECT_DIR/config"
cat > "$PROJECT_DIR/config/server.json" << 'EOF'
{
"server": {
"host": "0.0.0.0",
"api_port": 8000,
"web_port": 5000,
"workers": 4
},
"models": {
"fast": "phi",
"power": "deepseek-coder:33b"
},
"gpu": {
"memory_fraction": 0.9,
"max_batch_size": 8
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
}
EOF
log_success "config/server.json creado"
fi
}
print_next_steps() {
cat << EOF
${BLUE}=========================================${NC}
${GREEN}✨ PRÓXIMOS PASOS:${NC}
${BLUE}=========================================${NC}
1. ${YELLOW}Verificar instalación:${NC}
sudo systemctl status ollama
sudo systemctl status llm-api
2. ${YELLOW}Ver logs:${NC}
sudo journalctl -u ollama -f
sudo journalctl -u llm-api -f
3. ${YELLOW}Iniciar servicios:${NC}
sudo systemctl start ollama
sudo systemctl start llm-api
4. ${YELLOW}Acceder a la API:${NC}
curl http://localhost:8000/health
5. ${YELLOW}Descargar modelos (si no se descargaron):${NC}
source $VENV_DIR/bin/activate
ollama pull phi:latest
ollama pull deepseek-coder:33b
6. ${YELLOW}Ver estado en tiempo real:${NC}
watch -n 1 nvidia-smi
${BLUE}=========================================${NC}
${GREEN}📁 Archivos importantes:${NC}
${BLUE}=========================================${NC}
Config: $PROJECT_DIR/config/server.json
.env: $PROJECT_DIR/.env
Logs: $PROJECT_DIR/logs/
Workspace: $WORKSPACE_DIR/
${BLUE}=========================================${NC}
EOF
}
# Ejecutar main
main "$@"
-100
View File
@@ -1,100 +0,0 @@
#!/bin/bash
################################################################################
# MONITOR SCRIPT
# Monitorea servicios y hardware en tiempo real
################################################################################
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
clear_screen() {
clear
}
print_header() {
echo -e "${BLUE}=================================================${NC}"
echo -e "${BLUE} LLM SERVER MONITOR - $(date '+%Y-%m-%d %H:%M:%S')${NC}"
echo -e "${BLUE}=================================================${NC}"
}
print_section() {
echo -e "\n${YELLOW}>>> $1${NC}"
}
check_service() {
local service=$1
if sudo systemctl is-active --quiet "$service"; then
echo -e "${GREEN}✅ $service - ACTIVO${NC}"
sudo systemctl status "$service" --no-pager | grep -E "(Active|ExecStart)" | sed 's/^/ /'
else
echo -e "${RED}❌ $service - INACTIVO${NC}"
fi
}
get_ip() {
hostname -I | awk '{print $1}'
}
main() {
while true; do
clear_screen
print_header
# System Info
print_section "SISTEMA"
echo "Hostname: $(hostname)"
echo "IP: $(get_ip)"
echo "Uptime: $(uptime -p)"
echo "Usuarios conectados: $(who | wc -l)"
# CPU & RAM
print_section "CPU & MEMORIA"
free -h | awk 'NR==1 {print ""; print $0} NR==2 {print $0}'
echo ""
top -bn1 | head -3 | tail -1
# Disk
print_section "DISCO"
df -h / | awk 'NR==2 {printf "Root: %s used / %s total (%.1f%%)\n", $3, $2, ($3/$2)*100}'
# GPU
print_section "GPU - NVIDIA RTX 3090"
nvidia-smi --query-gpu=index,name,driver_version,memory.used,memory.total,temperature.gpu,utilization.gpu \
--format=csv,noheader | while read line; do
echo " $line"
done
# Services
print_section "SERVICIOS"
check_service "ollama"
echo ""
check_service "llm-api"
# Network
print_section "RED"
echo "API (port 8000): $(curl -s http://localhost:8000/health | jq '.' 2>/dev/null || echo 'NO RESPONDE')"
echo "Ollama (port 11434): $(curl -s http://localhost:11434/api/tags | jq '.models | length' 2>/dev/null || echo '0') modelos"
# Logs recientes
print_section "ÚLTIMOS ERRORES (últimas 5 líneas)"
echo "Ollama:"
sudo journalctl -u ollama -n 3 --no-pager | sed 's/^/ /'
echo ""
echo "API:"
sudo journalctl -u llm-api -n 3 --no-pager | sed 's/^/ /'
# Footer
echo ""
echo -e "${BLUE}=================================================${NC}"
echo "Presiona Ctrl+C para salir | Se actualiza cada 10 segundos"
echo -e "${BLUE}=================================================${NC}"
sleep 10
done
}
main
-53
View File
@@ -1,53 +0,0 @@
#!/bin/bash
################################################################################
# UPDATE SCRIPT
# Actualiza código, modelos y dependencias
################################################################################
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
VENV_DIR="$PROJECT_DIR/venv"
log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $@"; }
log_success() { echo -e "${GREEN}✅ $@${NC}"; }
log_error() { echo -e "${RED}❌ $@${NC}"; exit 1; }
log "Actualizando LLM Server..."
# Pull latest from git
log "Descargando cambios de git..."
cd "$PROJECT_DIR"
git pull origin main || log "Git pull completado con warnings"
# Update Python dependencies
log "Actualizando dependencias Python..."
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install -r requirements.txt --upgrade
# Update Ollama models (opcional)
read -p "¿Actualizar modelos Ollama? (y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
log "Actualizando Phi..."
ollama pull phi:latest
log "Actualizando DeepSeek..."
ollama pull deepseek-coder:33b
fi
# Restart services
log "Reiniciando servicios..."
sudo systemctl restart ollama
sudo systemctl restart llm-api
log_success "Actualización completada"
-549
View File
@@ -1,549 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Agent Chat</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
display: flex;
}
.container {
display: flex;
width: 100%;
gap: 20px;
padding: 20px;
}
/* Chat Panel */
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
font-size: 20px;
font-weight: bold;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.message {
display: flex;
gap: 10px;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
word-wrap: break-word;
}
.message.user .message-content {
background: #667eea;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: #f0f0f0;
color: #333;
border-bottom-left-radius: 4px;
}
.message-image {
max-width: 200px;
border-radius: 8px;
margin-top: 8px;
}
.message-meta {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* Input Area */
.input-area {
border-top: 1px solid #e0e0e0;
padding: 20px;
background: #fafafa;
}
.input-container {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
resize: vertical;
min-height: 60px;
max-height: 120px;
font-family: inherit;
font-size: 14px;
}
textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.button-group {
display: flex;
gap: 10px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.send-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
flex: 1;
}
.send-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.file-input-wrapper {
position: relative;
overflow: hidden;
}
.file-input-wrapper input[type="file"] {
display: none;
}
.upload-btn {
background: #4CAF50;
color: white;
padding: 10px 20px;
}
.upload-btn:hover {
background: #45a049;
}
.selected-image {
max-width: 150px;
max-height: 150px;
border-radius: 8px;
margin-top: 10px;
}
.image-indicator {
font-size: 12px;
color: #666;
margin-top: 5px;
}
/* Sidebar */
.sidebar {
width: 300px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
background: #f5f5f5;
padding: 15px;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.file-item {
padding: 8px 12px;
background: #f9f9f9;
border-radius: 6px;
margin-bottom: 8px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.file-item:hover {
background: #f0f0f0;
border-left-color: #667eea;
}
.model-selector {
padding: 15px;
border-top: 1px solid #e0e0e0;
}
select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 6px;
}
.loading {
display: inline-block;
width: 8px;
height: 8px;
background: #667eea;
border-radius: 50%;
animation: pulse 1.5s infinite;
margin-right: 5px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.error {
background: #ffebee;
color: #c62828;
padding: 12px;
border-radius: 8px;
margin-top: 10px;
}
.success {
background: #e8f5e9;
color: #2e7d32;
padding: 12px;
border-radius: 8px;
margin-top: 10px;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.message-content {
max-width: 90%;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Chat Panel -->
<div class="chat-panel">
<div class="chat-header">🤖 Code Agent Chat</div>
<div class="chat-messages" id="chatMessages"></div>
<div class="input-area">
<div class="input-container">
<textarea
id="messageInput"
placeholder="Escribe tu directive aquí... (ej: Crea un contador HTML)"
onkeydown="if(event.key==='Enter' && event.ctrlKey) sendMessage();"
></textarea>
</div>
<div id="selectedImageContainer" style="display:none;">
<img id="selectedImagePreview" class="selected-image" alt="Imagen seleccionada">
<div class="image-indicator" id="imageIndicator"></div>
</div>
<div class="button-group">
<div class="file-input-wrapper">
<input type="file" id="imageInput" accept="image/*">
<button class="upload-btn" onclick="document.getElementById('imageInput').click();">
📸 Adjuntar Imagen
</button>
</div>
<button class="send-btn" id="sendBtn" onclick="sendMessage();">
Enviar (Ctrl+Enter)
</button>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">📁 Workspace</div>
<div class="sidebar-content" id="fileList">
<p style="color: #999; font-size: 13px;">Cargando archivos...</p>
</div>
<div class="model-selector">
<label style="font-size: 12px; color: #666;">Modelo LLM:</label>
<select id="modelSelect" onchange="localStorage.setItem('selectedModel', this.value)">
<option value="phi">⚡ Phi (Rápido)</option>
<option value="deepseek-coder:33b">🧠 DeepSeek (Potente)</option>
</select>
</div>
</div>
</div>
<script>
const API_URL = "http://localhost:5000/api";
const chatMessagesDiv = document.getElementById("chatMessages");
const messageInput = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
const imageInput = document.getElementById("imageInput");
const fileList = document.getElementById("fileList");
const modelSelect = document.getElementById("modelSelect");
const selectedImageContainer = document.getElementById("selectedImageContainer");
const selectedImagePreview = document.getElementById("selectedImagePreview");
const imageIndicator = document.getElementById("imageIndicator");
let selectedImage = null;
// Load saved model
const savedModel = localStorage.getItem("selectedModel") || "phi";
modelSelect.value = savedModel;
// Manejo de imagen
imageInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) {
selectedImage = file;
const reader = new FileReader();
reader.onload = (event) => {
selectedImagePreview.src = event.target.result;
imageIndicator.textContent = `✅ ${file.name} seleccionada`;
selectedImageContainer.style.display = "block";
};
reader.readAsDataURL(file);
}
});
// Limpiar imagen
function clearImage() {
selectedImage = null;
imageInput.value = "";
selectedImageContainer.style.display = "none";
}
// Enviar mensaje
async function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
const model = modelSelect.value;
// Mostrar mensaje del usuario
addMessage(message, "user", selectedImage);
messageInput.value = "";
clearImage();
sendBtn.disabled = true;
try {
// Preparar form data
const formData = new FormData();
formData.append("message", message);
formData.append("model", model);
if (selectedImage) {
formData.append("image", selectedImage);
}
// Enviar
const response = await fetch(`${API_URL}/chat`, {
method: "POST",
body: formData
});
if (!response.ok) {
const error = await response.json();
addMessage(`❌ Error: ${error.detail}`, "assistant");
return;
}
const data = await response.json();
// Mostrar respuesta del LLM
addMessage(data.llm_response, "assistant", null, model);
// Mostrar resultado de la acción
if (data.action_result) {
const resultText = JSON.stringify(data.action_result, null, 2);
if (data.action_result.success) {
addMessage(`✅ ${data.action_result.result}`, "assistant");
} else {
addMessage(`⚠️ ${data.action_result.error || data.action_result.result}`, "assistant");
}
}
// Recargar archivos
loadFiles();
} catch (error) {
addMessage(`❌ Error: ${error.message}`, "assistant");
} finally {
sendBtn.disabled = false;
messageInput.focus();
}
}
// Agregar mensaje al chat
function addMessage(text, sender, image = null, model = null) {
const messageDiv = document.createElement("div");
messageDiv.className = `message ${sender}`;
const contentDiv = document.createElement("div");
contentDiv.className = "message-content";
// Formatear texto (markdown básico)
let formattedText = text
.replace(/```([^`]*?)```/g, "<pre><code>$1</code></pre>")
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/_(.*?)_/g, "<em>$1</em>");
contentDiv.innerHTML = formattedText;
if (image instanceof File) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement("img");
img.src = e.target.result;
img.className = "message-image";
contentDiv.appendChild(img);
};
reader.readAsDataURL(image);
}
messageDiv.appendChild(contentDiv);
if (model) {
const meta = document.createElement("div");
meta.className = "message-meta";
meta.textContent = `Modelo: ${model}`;
messageDiv.appendChild(meta);
}
chatMessagesDiv.appendChild(messageDiv);
chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
}
// Cargar archivos del workspace
async function loadFiles() {
try {
const response = await fetch(`${API_URL}/files`);
const data = await response.json();
fileList.innerHTML = "";
if (data.files.length === 0) {
fileList.innerHTML = '<p style="color: #999; font-size: 13px;">Sin archivos aún</p>';
return;
}
data.files.forEach(file => {
const item = document.createElement("div");
item.className = "file-item";
let icon = "📄";
if (file.type === ".html") icon = "🌐";
if (file.type === ".js") icon = "📜";
if (file.type === ".py") icon = "🐍";
if (file.type === ".css") icon = "🎨";
item.innerHTML = `${icon} <strong>${file.name}</strong><br>
<span style="font-size: 11px; color: #999;">${(file.size / 1024).toFixed(1)} KB</span>`;
item.onclick = async () => {
try {
const fileResponse = await fetch(`${API_URL}/file/${file.name}`);
const fileData = await fileResponse.json();
addMessage(`\`\`\`\n${fileData.content}\n\`\`\``, "assistant");
} catch (e) {
addMessage(`Error leyendo ${file.name}`, "assistant");
}
};
fileList.appendChild(item);
});
} catch (error) {
console.error("Error cargando archivos:", error);
}
}
// Cargar archivos al iniciar
loadFiles();
// Auto-refresh cada 10 segundos
setInterval(loadFiles, 10000);
// Enter para enviar
messageInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.ctrlKey) {
sendMessage();
}
});
// Mensaje inicial
setTimeout(() => {
addMessage("👋 Hola! Soy tu agent agentico. Puedes:\n\n- **Pedir que cree código** (HTML, Python, Node.js)\n- **Adjuntar imágenes** (capturas, diseños)\n- **Dar directivas** para modificar tus archivos\n- **Ejecutar y testear** automáticamente en el navegador\n\n¿Qué querés crear?", "assistant");
}, 500);
</script>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
import json
from pathlib import Path
SYSTEM_PROMPT = (
"You are a helpful assistant. "
"When outputting Windows file paths in JSON arguments, you must strictly escape all backslashes "
"(for example: C:\\\\Users\\\\name\\\\file.txt). "
"If you need to read a file, emit a tool call with the path field."
)
def read_file(path: str) -> str:
"""Read a file from disk and return its contents."""
file_path = Path(path)
try:
return file_path.read_text(encoding="utf-8")
except FileNotFoundError:
return f"ERROR: File not found: {path}"
except Exception as exc: # pragma: no cover - demo only
return f"ERROR: {type(exc).__name__}: {exc}"
def sanitize_tool_arguments(raw_arguments: str):
"""Repair malformed JSON emitted by the model when Windows paths are not escaped."""
try:
return json.loads(raw_arguments)
except json.JSONDecodeError:
repaired = raw_arguments.replace("\\", "\\\\")
return json.loads(repaired)
def handle_tool_call(response: dict) -> dict:
"""Detect a tool call in the LLM response and execute it locally."""
tool_calls = response.get("tool_calls") or response.get("function_call")
if tool_calls is None:
return {"status": "final_response", "content": response}
if isinstance(tool_calls, dict):
tool_calls = [tool_calls]
for tool_call in tool_calls:
function_data = tool_call.get("function", tool_call)
name = function_data.get("name")
arguments = function_data.get("arguments", {})
if isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except json.JSONDecodeError:
try:
arguments = sanitize_tool_arguments(arguments)
except json.JSONDecodeError:
return {
"status": "invalid_arguments",
"raw_arguments": arguments,
"message": "The model emitted malformed JSON. Ensure backslashes are escaped.",
}
if name == "read_file":
file_path = arguments.get("path")
content = read_file(file_path)
return {
"status": "tool_result",
"tool_call_id": tool_call.get("id"),
"content": content,
}
return {"status": "unsupported_tool_call", "raw": response}
if __name__ == "__main__":
# A valid OpenAI-style tool call with a Windows path escaped correctly.
valid_response = {
"tool_calls": [
{
"id": "call_read_001",
"type": "function",
"function": {
"name": "read_file",
"arguments": '{"path": "c:\\Workspace\\llm-server-setup\\README.md"}',
},
}
]
}
# This reproduces the common bug: malformed Windows path in JSON.
broken_response = {
"tool_calls": [
{
"id": "call_read_002",
"type": "function",
"function": {
"name": "read_file",
"arguments": '{"path": "c:\Workspace\llm-server-setup\README.md"}',
},
}
]
}
result = handle_tool_call(valid_response)
print(json.dumps(result, ensure_ascii=False, indent=2))
print("\n--- malformed-path fallback sample ---")
fallback = handle_tool_call(broken_response)
print(json.dumps(fallback, ensure_ascii=False, indent=2))
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/env bash
set -uo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
readonly DEFAULT_OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11434}"
readonly DEFAULT_LITELLM_URL="${LITELLM_URL:-http://127.0.0.1:8000}"
readonly MODEL_NAME="${MODEL_NAME:-qwen2.5-coder:14b}"
readonly OPENCLAW_CONFIG="${OPENCLAW_CONFIG:-${HOME}/.openclaw/openclaw.json}"
passed=0
failed=0
warnings=0
report_file=""
show_help() {
cat <<EOF
Uso: $SCRIPT_NAME [opciones]
Ejecuta un diagnóstico de solo lectura del servidor Ubuntu y de los servicios
Ollama, LiteLLM y OpenClaw. No modifica archivos ni reinicia servicios.
Opciones:
--report FILE Guarda la salida en FILE además de mostrarla en pantalla.
-h, --help Muestra esta ayuda.
Variables opcionales:
OLLAMA_URL URL de Ollama (por defecto: $DEFAULT_OLLAMA_URL)
LITELLM_URL URL de LiteLLM (por defecto: $DEFAULT_LITELLM_URL)
MODEL_NAME Modelo esperado (por defecto: $MODEL_NAME)
OPENCLAW_CONFIG Ruta de configuración de OpenClaw.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--report)
[[ $# -ge 2 ]] || { echo "Falta FILE para --report" >&2; exit 2; }
report_file="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Opción desconocida: $1" >&2
show_help >&2
exit 2
;;
esac
done
if [[ -n "$report_file" ]]; then
mkdir -p "$(dirname "$report_file")"
: > "$report_file" || { echo "No se puede escribir $report_file" >&2; exit 1; }
exec > >(tee -a "$report_file") 2>&1
fi
pass() {
printf '[ OK ] %s\n' "$1"
passed=$((passed + 1))
}
fail() {
printf '[FAIL] %s\n' "$1"
failed=$((failed + 1))
}
warn() {
printf '[WARN] %s\n' "$1"
warnings=$((warnings + 1))
}
section() {
printf '\n=== %s ===\n' "$1"
}
has_command() {
command -v "$1" >/dev/null 2>&1
}
check_command() {
local command_name="$1"
local description="$2"
if has_command "$command_name"; then
pass "$description: $command_name"
else
fail "$description: falta $command_name"
fi
}
check_service() {
local service="$1"
if ! has_command systemctl; then
warn "No se puede consultar $service: falta systemctl"
return
fi
if systemctl is-active --quiet "$service"; then
pass "Servicio activo: $service"
elif systemctl is-enabled --quiet "$service" 2>/dev/null; then
warn "Servicio habilitado pero no activo: $service"
else
fail "Servicio no activo: $service"
fi
}
check_file() {
local file_path="$1"
local description="$2"
if [[ -e "$file_path" || -L "$file_path" ]]; then
pass "$description: $file_path"
else
warn "$description no encontrado: $file_path"
fi
}
http_check() {
local url="$1"
local description="$2"
local status
if ! has_command curl; then
fail "$description: falta curl"
return
fi
status="$(curl --silent --show-error --max-time 10 --output /dev/null --write-out '%{http_code}' "$url" 2>/dev/null)"
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
pass "$description: HTTP $status ($url)"
else
fail "$description: HTTP ${status:-sin respuesta} ($url)"
fi
}
section "Sistema"
printf 'Fecha: %s\n' "$(date --iso-8601=seconds 2>/dev/null || date)"
printf 'Host: %s\n' "$(hostname 2>/dev/null || echo desconocido)"
printf 'Usuario: %s\n' "$(id -un 2>/dev/null || echo desconocido)"
if [[ -r /etc/os-release ]]; then
. /etc/os-release
printf 'Sistema: %s %s\n' "${NAME:-desconocido}" "${VERSION_ID:-desconocida}"
if [[ "${ID:-}" == ubuntu ]]; then
pass "Sistema operativo Ubuntu"
else
warn "El sistema operativo detectado no es Ubuntu: ${ID:-desconocido}"
fi
else
fail "No se puede leer /etc/os-release"
fi
check_command curl "Herramienta HTTP"
check_command systemctl "Gestor de servicios"
check_command ss "Inspección de puertos"
if has_command free; then
free -h
fi
if has_command df; then
df -hT / 2>/dev/null || true
fi
if has_command swapon; then
if swapon --show --noheadings | grep -q .; then
pass "Swap activa"
else
warn "No hay swap activa"
fi
fi
section "Hardware"
if has_command nvidia-smi; then
if nvidia-smi --query-gpu=name,driver_version,memory.total,memory.used --format=csv,noheader 2>/dev/null; then
pass "NVIDIA disponible"
else
fail "nvidia-smi no puede consultar la GPU"
fi
else
warn "nvidia-smi no está instalado o la GPU NVIDIA no está disponible"
fi
if [[ -r /proc/sys/vm/swappiness ]]; then
printf 'vm.swappiness: %s\n' "$(cat /proc/sys/vm/swappiness)"
fi
section "Archivos de configuración"
check_file /etc/systemd/system/ollama.service.d/override.conf "Override de Ollama"
check_file /etc/systemd/system/litellm.service "Servicio de LiteLLM"
check_file /etc/systemd/system/nvidia-power-limit.service "Servicio de límite NVIDIA"
check_file /etc/ufw "Configuración UFW"
check_file /etc/netplan "Configuración Netplan"
check_file "$OPENCLAW_CONFIG" "Configuración OpenClaw"
if [[ -f "$OPENCLAW_CONFIG" ]]; then
if grep -Eq '"(apiKey|token|password|secret)"[[:space:]]*:' "$OPENCLAW_CONFIG"; then
warn "La configuración OpenClaw contiene campos sensibles; no se mostrarán"
fi
if grep -q 'litellm/qwen2.5-coder:14b' "$OPENCLAW_CONFIG"; then
pass "OpenClaw apunta al modelo LiteLLM esperado"
else
warn "No se encontró el modelo LiteLLM esperado en OpenClaw"
fi
fi
for config_path in \
/etc/litellm_config.yaml \
/root/litellm_config.yaml \
"$HOME/litellm_config.yaml"
do
if [[ -f "$config_path" ]]; then
printf 'LiteLLM config detectada: %s\n' "$config_path"
if grep -q "$MODEL_NAME" "$config_path"; then
pass "LiteLLM contiene el modelo $MODEL_NAME"
else
warn "LiteLLM no contiene el modelo esperado: $MODEL_NAME"
fi
fi
done
section "Servicios"
check_service ollama
check_service litellm
if has_command openclaw; then
printf 'El Gateway de OpenClaw se validará con su propio comando de estado.\n'
else
warn "OpenClaw no está instalado en PATH"
fi
section "Puertos"
if has_command ss; then
printf 'Puertos escuchando relevantes:\n'
ss -ltnp 2>/dev/null | awk 'NR == 1 || /:8000|:11434|:18789/' || true
if ss -ltn 2>/dev/null | grep -Eq ':11434[[:space:]]'; then
pass "Ollama escucha en el puerto 11434"
else
fail "No se detecta Ollama escuchando en el puerto 11434"
fi
if ss -ltn 2>/dev/null | grep -Eq ':8000[[:space:]]'; then
pass "LiteLLM escucha en el puerto 8000"
else
fail "No se detecta LiteLLM escuchando en el puerto 8000"
fi
fi
section "Ollama"
http_check "$DEFAULT_OLLAMA_URL/api/tags" "API de Ollama"
if has_command ollama; then
if ollama list 2>/dev/null | awk 'NR > 1 {print $1}' | grep -Fxq "$MODEL_NAME"; then
pass "Modelo Ollama instalado: $MODEL_NAME"
else
fail "Modelo Ollama no encontrado: $MODEL_NAME"
fi
else
warn "Comando ollama no disponible"
fi
section "LiteLLM"
http_check "$DEFAULT_LITELLM_URL/health/liveliness" "Salud de LiteLLM"
if has_command curl; then
models_response="$(curl --silent --show-error --max-time 10 "$DEFAULT_LITELLM_URL/v1/models" 2>/dev/null || true)"
if [[ "$models_response" == *"$MODEL_NAME"* ]]; then
pass "LiteLLM publica el modelo $MODEL_NAME"
else
fail "LiteLLM no publica el modelo $MODEL_NAME en /v1/models"
fi
fi
section "OpenClaw"
if has_command openclaw; then
openclaw --version 2>/dev/null || true
if openclaw doctor 2>&1; then
pass "openclaw doctor terminó correctamente"
else
fail "openclaw doctor reportó errores"
fi
if openclaw gateway status 2>&1; then
pass "Estado del Gateway consultado correctamente"
else
warn "No se pudo consultar correctamente el Gateway"
fi
else
warn "No se puede probar OpenClaw porque no está instalado"
fi
section "Firewall"
if has_command ufw; then
ufw status verbose 2>/dev/null || true
if ufw status 2>/dev/null | grep -qi active; then
pass "UFW está activo"
else
warn "UFW no está activo"
fi
else
warn "UFW no está instalado"
fi
section "Resumen"
printf 'Correctos: %d | Fallos: %d | Advertencias: %d\n' "$passed" "$failed" "$warnings"
if [[ -n "$report_file" ]]; then
printf 'Reporte: %s\n' "$report_file"
fi
if [[ "$failed" -gt 0 ]]; then
exit 1
fi
exit 0