diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..42bf228 --- /dev/null +++ b/.dockerignore @@ -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/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..69def03 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/README.md b/README.md index 12ca66d..3a36cf8 100644 --- a/README.md +++ b/README.md @@ -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`: diff --git a/api_server.py b/api_server.py new file mode 100644 index 0000000..5f66fea --- /dev/null +++ b/api_server.py @@ -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" + ) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..14af2af --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2ba1276..4b261a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,11 @@ deepagents>=0.0.34 langchain>=0.3.0 langchain-community>=0.3.0 -langchain-openai>=0.1.0 \ No newline at end of file +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 \ No newline at end of file