Compare commits
9
Commits
afd786394a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c5b4865a | ||
|
|
2b3e0e605a | ||
|
|
4a2a00966d | ||
|
|
162e03dad2 | ||
|
|
cc06215319 | ||
|
|
c9a2e73c61 | ||
|
|
9bd3d1052d | ||
|
|
b77d275fe0 | ||
|
|
a7a686499e |
+2
-61
@@ -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
|
|
||||||
|
|||||||
@@ -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 "=============================================="
|
|
||||||
@@ -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)
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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)
|
|
||||||
Executable
+210
@@ -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'
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable
+115
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
Executable
+164
@@ -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
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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 "$@"
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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>
|
|
||||||
@@ -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))
|
||||||
Executable
+310
@@ -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
|
||||||
Reference in New Issue
Block a user