Phase 5: Docker containerization and API server for remote access

This commit is contained in:
truetype committed 2026-03-20 18:16:11 +01:00
1 parent b17c180fbe
commit 5541770896
6 files changed
+578 -1

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
# Git
.git
.gitignore
# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env
pip-log.txt
pip-delete-this-directory.txt
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.mypy_cache
.pytest_cache
.hypothesis
# Virtual environments
venv/
.venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Documentation
docs/
*.md
!README.md
# Tests
tests/
test_*.py
# Completions
completions/
# Configuration examples
*.example
*.example.*
# Backup files
*.bak
*.backup
# Temporary files
tmp/
temp/
.tmp/
+36
View File
@@ -0,0 +1,36 @@
# MaxClaw Agent Docker Image
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create directory for configuration
RUN mkdir -p /root/.maxclaw && \
chmod 700 /root/.maxclaw
# Expose port for API (future use)
EXPOSE 8000
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV MODEL_PROVIDER=ollama
ENV MODEL_NAME=gemma2:9b
ENV OLLAMA_BASE_URL=http://host.docker.internal:11434
# Default command
CMD ["python", "main.py"]
+117
View File
@@ -13,15 +13,36 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM.
- **Strumenti personalizzati**: Include strumenti per gestire Proxmox e Ollama
- **Completamento shell**: Supporta bash e zsh per autocompletamento dei comandi
- **Test completi**: Suite di test per garantire la qualità del codice
- **Docker support**: Containerizzazione per deployment facile
- **API REST**: Server API per accesso remoto
## Installazione
### Installazione Locale
1. Clona questo repository
2. Installa i pacchetti richiesti:
```bash
pip install -r requirements.txt
```
### Installazione con Docker
```bash
# Build dell'immagine
docker build -t maxclaw-agent .
# Esecuzione con Docker Compose
docker-compose up -d
# Esecuzione diretta
docker run -it --rm \
-e MODEL_PROVIDER=ollama \
-e MODEL_NAME=gemma2:9b \
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
maxclaw-agent
```
### Configurazione del Completamento Shell
#### Bash
@@ -128,6 +149,8 @@ La TUI ora include:
## Utilizzo
### Utilizzo Locale
Esegui l'agente:
```bash
python main.py
@@ -135,6 +158,64 @@ python main.py
Puoi quindi interagire con l'agente nel terminale. Digita 'exit' per uscire.
### Utilizzo con Docker
```bash
# Avvia con Docker Compose
docker-compose up -d
# Accedi al terminale
docker exec -it maxclaw-agent python main.py
# Oppure esegui direttamente
docker run -it --rm maxclaw-agent
```
### Utilizzo dell'API REST
Avvia il server API:
```bash
python api_server.py
```
L'API sarà disponibile su http://localhost:8000
#### Endpoint API Disponibili
- `GET /`: Informazioni sull'API
- `GET /health`: Health check
- `POST /chat`: Chat con l'agente
- `GET /configurations`: Elenca tutte le configurazioni
- `POST /configurations`: Aggiungi una configurazione
- `DELETE /configurations/{name}`: Rimuovi una configurazione
- `POST /configurations/{name}/activate`: Attiva una configurazione
- `GET /configurations/active`: Mostra la configurazione attiva
- `POST /configurations/{name}/test`: Testa una configurazione
- `GET /providers`: Elenca i provider disponibili
#### Esempio di Utilizzo dell'API
```bash
# Chat con l'agente
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Ciao, chi sei?"}'
# Elenca configurazioni
curl http://localhost:8000/configurations
# Aggiungi una configurazione
curl -X POST http://localhost:8000/configurations \
-H "Content-Type: application/json" \
-d '{
"name": "mio-ollama",
"provider": "ollama",
"model_name": "gemma2:9b",
"base_url": "http://192.168.0.50:11434",
"set_active": true
}'
```
## Strumenti Unificati per Provider
MaxClaw Agent ora fornisce un'interfaccia unificata per gestire qualsiasi provider LLM. Una volta configurato un provider, puoi usare gli stessi comandi per tutti.
@@ -234,6 +315,42 @@ pytest tests/test_provider_tools.py
pytest tests/ -v
```
## Docker
### Build dell'Immagine
```bash
docker build -t maxclaw-agent .
```
### Esecuzione con Docker Compose
```bash
# Avvia tutti i servizi
docker-compose up -d
# Visualizza i log
docker-compose logs -f
# Ferma i servizi
docker-compose down
```
### Variabili d'Ambiente Docker
Puoi configurare il container Docker con le seguenti variabili d'ambiente:
```bash
docker run -it --rm \
-e MODEL_PROVIDER=ollama \
-e MODEL_NAME=gemma2:9b \
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
-e OPENAI_API_KEY=sk-... \
-e OPENROUTER_API_KEY=sk-or-... \
-e PROXMOX_HOST=192.168.0.100 \
-e PROXMOX_USER=root \
-e PROXMOX_PASSWORD=password \
maxclaw-agent
```
## Personalizzazione
Per personalizzare l'agente, modifica `main.py`:
+287
View File
@@ -0,0 +1,287 @@
"""
MaxClaw Agent API Server
Provides REST API for remote access to MaxClaw Agent
"""
import os
import sys
from typing import Optional, Dict, Any, List
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
# Add current directory to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from config_manager import config_manager
from main import create_maxclaw_agent
from langchain_core.messages import HumanMessage
# Initialize FastAPI app
app = FastAPI(
title="MaxClaw Agent API",
description="REST API for MaxClaw Agent - configurable AI agent",
version="1.0.0"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global agent instance
agent = None
class ChatRequest(BaseModel):
"""Chat request model"""
message: str
conversation_id: Optional[str] = None
provider: Optional[str] = None
model: Optional[str] = None
class ChatResponse(BaseModel):
"""Chat response model"""
response: str
conversation_id: Optional[str] = None
provider: Optional[str] = None
model: Optional[str] = None
class ConfigRequest(BaseModel):
"""Configuration request model"""
name: str
provider: str
model_name: str
api_key: Optional[str] = None
base_url: Optional[str] = None
set_active: bool = False
class ConfigResponse(BaseModel):
"""Configuration response model"""
name: str
provider: str
model_name: str
is_active: bool
def get_agent():
"""Get or create agent instance"""
global agent
if agent is None:
try:
agent = create_maxclaw_agent()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create agent: {str(e)}")
return agent
@app.on_event("startup")
async def startup_event():
"""Initialize agent on startup"""
global agent
try:
agent = create_maxclaw_agent()
print("✅ MaxClaw Agent API server started")
except Exception as e:
print(f"❌ Failed to initialize agent: {e}")
@app.get("/")
async def root():
"""Root endpoint"""
return {
"message": "MaxClaw Agent API",
"version": "1.0.0",
"endpoints": {
"chat": "/chat",
"configurations": "/configurations",
"health": "/health"
}
}
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "agent_initialized": agent is not None}
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""Chat with MaxClaw Agent"""
try:
agent_instance = get_agent()
# Create message
message = HumanMessage(content=request.message)
# Invoke agent
result = agent_instance.invoke({"messages": [message]})
# Extract response
ai_message = result["messages"][-1]
return ChatResponse(
response=ai_message.content,
conversation_id=request.conversation_id,
provider=os.getenv("MODEL_PROVIDER", "unknown"),
model=os.getenv("MODEL_NAME", "unknown")
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/configurations", response_model=List[ConfigResponse])
async def list_configurations():
"""List all configurations"""
try:
configs = config_manager.get_configurations()
active = config_manager.get_active_configuration()
result = []
for name, config in configs.items():
result.append(ConfigResponse(
name=name,
provider=config.get("provider", "unknown"),
model_name=config.get("model_name", "unknown"),
is_active=(name == active)
))
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/configurations", response_model=ConfigResponse)
async def add_configuration(request: ConfigRequest):
"""Add a new configuration"""
try:
config = {
"provider": request.provider,
"model_name": request.model_name
}
if request.api_key:
config["api_key"] = request.api_key
if request.base_url:
config["base_url"] = request.base_url
# Add configuration
config_manager.add_configuration(request.name, config)
# Set as active if requested
if request.set_active:
config_manager.set_active_configuration(request.name)
return ConfigResponse(
name=request.name,
provider=request.provider,
model_name=request.model_name,
is_active=request.set_active
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/configurations/{name}")
async def delete_configuration(name: str):
"""Delete a configuration"""
try:
if config_manager.remove_configuration(name):
return {"message": f"Configuration '{name}' deleted successfully"}
else:
raise HTTPException(status_code=404, detail=f"Configuration '{name}' not found")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/configurations/{name}/activate")
async def activate_configuration(name: str):
"""Set a configuration as active"""
try:
if config_manager.set_active_configuration(name):
return {"message": f"Configuration '{name}' set as active"}
else:
raise HTTPException(status_code=404, detail=f"Configuration '{name}' not found")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/configurations/active")
async def get_active_configuration():
"""Get the active configuration"""
try:
active_name = config_manager.get_active_configuration()
if not active_name:
return {"message": "No active configuration set"}
config = config_manager.get_configuration(active_name)
if not config:
raise HTTPException(status_code=404, detail=f"Active configuration '{active_name}' not found")
return {
"name": active_name,
"provider": config.get("provider", "unknown"),
"model_name": config.get("model_name", "unknown")
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/configurations/{name}/test")
async def test_configuration(name: str):
"""Test connection to a configuration"""
try:
success, message = config_manager.test_configuration_connection(name)
return {
"success": success,
"message": message
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/providers")
async def list_providers():
"""List available providers"""
return {
"providers": [
{
"name": "openai",
"description": "OpenAI GPT models",
"required_env": ["OPENAI_API_KEY"],
"optional_env": []
},
{
"name": "ollama",
"description": "Local Ollama models",
"required_env": [],
"optional_env": ["OLLAMA_BASE_URL"]
},
{
"name": "openrouter",
"description": "OpenRouter multi-provider access",
"required_env": ["OPENROUTER_API_KEY"],
"optional_env": ["OPENROUTER_BASE_URL"]
}
]
}
if __name__ == "__main__":
# Run the API server
uvicorn.run(
"api_server:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)
+53
View File
@@ -0,0 +1,53 @@
version: '3.8'
services:
maxclaw:
build: .
container_name: maxclaw-agent
restart: unless-stopped
volumes:
# Mount configuration directory for persistence
- maxclaw-config:/root/.maxclaw
# Mount current directory for development (optional)
# - .:/app
environment:
# Model configuration
- MODEL_PROVIDER=${MODEL_PROVIDER:-ollama}
- MODEL_NAME=${MODEL_NAME:-gemma2:9b}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
- OPENROUTER_BASE_URL=${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1}
# Proxmox configuration (optional)
- PROXMOX_HOST=${PROXMOX_HOST:-}
- PROXMOX_USER=${PROXMOX_USER:-root}
- PROXMOX_PASSWORD=${PROXMOX_PASSWORD:-}
- PROXMOX_TOKEN=${PROXMOX_TOKEN:-}
networks:
- maxclaw-network
# Interactive mode for terminal usage
stdin_open: true
tty: true
# Optional: Ollama service (if you want to run Ollama in Docker)
# ollama:
# image: ollama/ollama:latest
# container_name: ollama
# restart: unless-stopped
# volumes:
# - ollama-data:/root/.ollama
# ports:
# - "11434:11434"
# networks:
# - maxclaw-network
volumes:
maxclaw-config:
driver: local
# ollama-data:
# driver: local
networks:
maxclaw-network:
driver: bridge
+8 -1
View File
@@ -1,4 +1,11 @@
deepagents>=0.0.34
langchain>=0.3.0
langchain-community>=0.3.0
langchain-openai>=0.1.0
langchain-openai>=0.1.0
cryptography>=41.0.0
requests>=2.31.0
pyyaml>=6.0
fastapi>=0.104.0
uvicorn>=0.24.0
pydantic>=2.5.0
pytest>=7.4.0