# OpusAgent — Design Specification > Servizio self-hosted che consente l'interazione programmatica con i modelli > Anthropic attraverso Claude Code SDK, sfruttando un abbonamento Claude > Pro/Max esistente. Il servizio espone un'API REST asincrona con gestione > degli argomenti, coda di richieste sequenziale, rate limiting e generazione > automatica di riassunti. --- ## 1. Panoramica OpusAgent è un servizio a due componenti, deployato via Docker su una VPS Hostinger, che funge da intermediario tra software client e i modelli Anthropic. Invece di richiedere API key a consumo, il servizio sfrutta un'istanza autenticata di Claude Code che opera con l'abbonamento Pro/Max dell'utente. ### Obiettivi - Consentire ai propri software di comunicare con Claude in modo programmatico - Utilizzare l'abbonamento Claude Pro/Max esistente, evitando costi API a consumo - Organizzare le interazioni per argomento con system prompt dedicati - Supportare sia richieste one-shot che conversazioni multi-turn - Accumulare conoscenza tramite riassunti incrementali e completi - Garantire sicurezza con autenticazione API key e rate limiting ### Utenti Uso esclusivamente personale: l'operatore e i suoi software. --- ## 2. Architettura Il sistema è composto da due container Docker che comunicano via HTTP sulla rete interna Docker, con persistenza su SQLite in un volume condiviso. ``` ┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Client │ REST │ FastAPI │ HTTP │ Node.js Worker │ │ Software │───────▶│ (Python) │───────▶│ (Claude Code │ │ │◀───────│ Queue + Auth │◀───────│ SDK) │ └─────────────┘ │ Rate Limit │ └────────┬─────────┘ └────────┬──────────┘ │ │ ┌─────▼─────────┐ ┌────▼────┐ │ ~/.claude/ │ │ SQLite │ │ (auth volume) │ │ (coda) │ └───────────────┘ └─────────┘ ``` ### Componenti | Componente | Tecnologia | Ruolo | |------------|------------|-------| | **fastapi-service** | Python 3.11+, FastAPI, SQLite | API REST pubblica, autenticazione, rate limiting, gestione coda | | **claude-worker** | Node.js, @anthropic-ai/claude-code | Processamento richieste via Claude Code SDK, gestione sessioni | | **SQLite** | Volume Docker condiviso | Persistenza coda richieste e argomenti | | **Claude Auth** | Volume Docker dedicato | Credenziali Claude Code (solo worker) | ### Principi architetturali - Il worker non è esposto all'esterno: comunica solo via rete Docker interna - Processamento sequenziale: una richiesta alla volta (FIFO) - Il FastAPI service è l'unico punto di ingresso - Nessuna dipendenza da NATS: HTTP interno sufficiente per due servizi - Integrazione Traefik per reverse proxy (ultima fase del deploy) --- ## 3. Data Model ### Tabella `topics` Contiene gli argomenti con i rispettivi system prompt. Ogni argomento rappresenta un contesto di conversazione distinto. | Campo | Tipo | Vincoli | Descrizione | |-------|------|---------|-------------| | `id` | TEXT (UUID) | PK | Identificativo unico | | `name` | TEXT | NOT NULL, UNIQUE | Nome breve (es. "coding", "analisi") | | `system_prompt` | TEXT | NOT NULL | System prompt inviato a Claude per questo argomento | | `summary` | TEXT | DEFAULT "" | Riassunto accumulato delle interazioni | | `created_at` | TEXT (ISO 8601) | NOT NULL | Timestamp di creazione | | `model` | TEXT | NULLABLE | Modello Claude per questo topic (override del default globale) | | `updated_at` | TEXT (ISO 8601) | NOT NULL | Timestamp ultima modifica | ### Tabella `requests` Contiene le richieste in coda, in elaborazione e completate. | Campo | Tipo | Vincoli | Descrizione | |-------|------|---------|-------------| | `id` | TEXT (UUID) | PK | Identificativo unico | | `session_id` | TEXT | NULLABLE | Raggruppa richieste multi-turn. Null = one-shot | | `topic_id` | TEXT (UUID) | FK → topics.id, NOT NULL | Argomento di riferimento | | `prompt` | TEXT | NOT NULL | Il prompt da inviare a Claude | | `model` | TEXT | NULLABLE | Override modello per questa richiesta (prevale su topic e default) | | `summarize` | INTEGER | DEFAULT 0 | 1 = genera riassunto e lo appende al topic | | `status` | TEXT | NOT NULL, DEFAULT "pending" | `pending`, `processing`, `completed`, `failed` | | `result` | TEXT | NULLABLE | Risposta di Claude | | `error` | TEXT | NULLABLE | Messaggio di errore (se status = failed) | | `created_at` | TEXT (ISO 8601) | NOT NULL | Timestamp di creazione | | `completed_at` | TEXT (ISO 8601) | NULLABLE | Timestamp di completamento | ### Relazioni - `requests.topic_id` → `topics.id` (molti a uno) - `requests.session_id` raggruppa logicamente richieste della stessa conversazione - Un topic può avere zero o più richieste - Una sessione è definita implicitamente dal `session_id` condiviso tra richieste --- ## 4. API REST Tutti gli endpoint richiedono l'header `X-Api-Key` con la chiave configurata nel `.env`. Risposte nel formato envelope standard: `{success: bool, data: any, error: {code, message} | null}`. ### 4.1 Topics #### `POST /api/topics` Crea un nuovo argomento. ```json // Request { "name": "coding", "system_prompt": "Sei un esperto sviluppatore Python e TypeScript...", "summary": "Argomento per domande di programmazione", "model": "claude-sonnet-4-6" } // Response 201 { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "coding", "system_prompt": "Sei un esperto sviluppatore Python e TypeScript...", "summary": "Argomento per domande di programmazione", "created_at": "2026-05-25T10:00:00Z", "updated_at": "2026-05-25T10:00:00Z" }, "error": null } ``` #### `GET /api/topics` Lista tutti gli argomenti. #### `GET /api/topics/{id}` Dettaglio di un argomento. #### `PUT /api/topics/{id}` Aggiorna un argomento (name, system_prompt, summary). #### `DELETE /api/topics/{id}` Elimina un argomento. Fallisce se ci sono richieste pending/processing. #### `POST /api/topics/{id}/summarize` Rigenera il summary completo del topic analizzando tutte le richieste completate. Invia l'elenco degli scambi (prompt + risultato) a Claude Code e sostituisce il campo `summary` con il nuovo riassunto complessivo. ### 4.2 Requests #### `POST /api/requests` Invia una nuova richiesta alla coda. ```json // Request { "prompt": "Come implemento un middleware di autenticazione JWT in FastAPI?", "topic_id": "550e8400-e29b-41d4-a716-446655440000", "session_id": null, "model": null, "summarize": true } // Response 202 { "success": true, "data": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "status": "pending", "created_at": "2026-05-25T10:05:00Z" }, "error": null } ``` #### `GET /api/requests/{id}` Restituisce stato e risultato (quando disponibile). ```json // Response (completata) { "success": true, "data": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "session_id": null, "topic_id": "550e8400-e29b-41d4-a716-446655440000", "prompt": "Come implemento un middleware di autenticazione JWT in FastAPI?", "summarize": true, "status": "completed", "result": "Per implementare un middleware JWT in FastAPI...", "error": null, "created_at": "2026-05-25T10:05:00Z", "completed_at": "2026-05-25T10:05:45Z" }, "error": null } ``` #### `GET /api/requests` Lista richieste con filtri opzionali: `?status=pending&topic_id=xxx&session_id=yyy`. #### `DELETE /api/requests/{id}` Cancella una richiesta con status `pending`. Non è possibile cancellare richieste in `processing`, `completed` o `failed`. ### 4.3 Sessions #### `GET /api/sessions` Lista sessioni attive con conteggio richieste per ciascuna. #### `DELETE /api/sessions/{id}` Chiude una sessione. Le richieste existing restano, ma non si possono aggiungere nuove richieste a questa sessione. ### 4.4 System #### `GET /api/health` Health check del servizio. Verifica: - Stato FastAPI - Connettività al worker (HTTP ping) - Accesso al database SQLite --- ## 5. Flusso di Processamento ### 5.1 Flusso richiesta standard 1. Il client invia `POST /api/requests` con prompt e topic_id 2. FastAPI valida la richiesta, verifica API key, controlla rate limit 3. Crea record in SQLite con `status: "pending"`, restituisce `202 Accepted` con ID 4. Il client riceve l'ID e può fare polling su `GET /api/requests/{id}` 5. Il worker loop (nel FastAPI service) prende la richiesta pending più vecchia 6. Aggiorna status a `processing` 7. Carica il `system_prompt` dal topic associato 8. Chiama il worker Node.js via HTTP: `POST http://claude-worker:3001/process` 9. Il worker Node.js esegue la richiesta tramite Claude Code SDK 10. Il worker restituisce il risultato 11. FastAPI aggiorna il record: `status: "completed"`, `result: "..."` 12. Se `summarize: true`: il worker genera un riassunto dello scambio e lo appende al `summary` del topic 13. Il client ottiene il risultato al prossimo polling ### 5.2 Flusso multi-turn 1. Prima richiesta: `POST /api/requests {prompt, topic_id}` (senza session_id) 2. Il sistema genera un `session_id` e lo restituisce nella risposta 3. Richieste successive: `POST /api/requests {prompt, topic_id, session_id: "..."}` 4. Il worker Node.js usa il session_id per mantenere il contesto con Claude Code (flag `--resume` o equivalente SDK) 5. Ogni richiesta nella sessione condivide la storia della conversazione ### 5.3 Flusso summary completo 1. Il client chiama `POST /api/topics/{id}/summarize` 2. FastAPI raccoglie tutte le richieste `completed` del topic 3. Compone un payload con tutti gli scambi (prompt + risultato) 4. Chiama il worker per generare un riassunto complessivo 5. Sostituisce il campo `summary` del topic con il nuovo riassunto 6. Restituisce il summary aggiornato ### 5.4 Flusso summary incrementale 1. Richiesta con `summarize: true` viene completata 2. Dopo aver salvato il risultato, il worker genera un riassunto in 1-2 frasi dello scambio 3. Il riassunto viene appeso al `summary` del topic con timestamp: `[2026-05-25] Descrizione dello scambio.` --- ## 6. Node.js Worker ### Responsabilità Il worker è un server HTTP interno minimale con un'unica responsabilità: eseguire richieste tramite Claude Code SDK e restituire il risultato. ### Endpoint interni | Metodo | Path | Descrizione | |--------|------|-------------| | `POST` | `/process` | Processa una richiesta Claude Code | | `POST` | `/summarize` | Genera riassunto di uno o più scambi | | `GET` | `/health` | Health check del worker | ### Payload `/process` ```json // Request { "prompt": "Come implemento JWT?", "system_prompt": "Sei un esperto sviluppatore...", "model": "claude-sonnet-4-6", "session_id": null } // Response { "success": true, "result": "Per implementare JWT in FastAPI...", "session_id": "generated-or-existing-id" } ``` ### Payload `/summarize` ```json // Request { "exchanges": [ {"prompt": "Come faccio X?", "result": "Si fa così..."}, {"prompt": "E se devo Y?", "result": "In quel caso..."} ], "mode": "incremental" } // Response { "success": true, "summary": "[2026-05-25] Discusso come implementare X e gestire il caso Y." } ``` ### Claude Code SDK Usage ```javascript import { claude } from '@anthropic-ai/claude-code'; const result = await claude({ prompt: request.prompt, systemPrompt: request.system_prompt, model: request.model, // Per multi-turn: resume dalla sessione ...(request.session_id && { resume: request.session_id }), // Output in formato testo, non interattivo print: true, }); ``` --- ## 7. Selezione Modello Il modello Claude da utilizzare si risolve con priorità a cascata: 1. **Request `model`** — se la richiesta specifica un modello, usa quello 2. **Topic `model`** — se il topic ha un modello configurato, usa quello 3. **Default globale** — `CLAUDE_MODEL` dal `.env` Questa gerarchia consente di avere un default ragionevole (es. `claude-sonnet-4-6`) con la flessibilità di usare modelli diversi per argomenti specifici (es. Opus per analisi complesse) o per singole richieste (es. Haiku per domande semplici). ### Modelli supportati I valori accettati corrispondono agli identificativi dei modelli Claude: - `claude-opus-4-7` - `claude-sonnet-4-6` - `claude-haiku-4-5-20251001` La validazione accetta qualsiasi stringa che inizi con `claude-` per compatibilità con modelli futuri. --- ## 8. Rate Limiting Implementato con `slowapi` (wrapper di `limits` per FastAPI). ### Configurazione ``` # .env RATE_LIMIT_PER_MINUTE=10 RATE_LIMIT_PER_HOUR=100 ``` ### Comportamento - Limiti applicati per API key - Quando superati: risposta `429 Too Many Requests` con header `Retry-After` e `X-RateLimit-Remaining` - I limiti si applicano solo a `POST /api/requests` (invio richieste) - Gli endpoint di lettura (GET) e gestione topics non sono limitati --- ## 9. Sicurezza ### Autenticazione API - Header `X-Api-Key` obbligatorio su tutti gli endpoint - Chiave configurata in `.env` - Risposta `403 Forbidden` se mancante o non valida ### Isolamento Worker - Il worker Node.js non è esposto all'esterno (`expose`, non `ports`) - Raggiungibile solo dalla rete Docker interna - Nessuna autenticazione sul worker (rete fidata interna) ### Credenziali Claude Code - Conservate in volume Docker dedicato (`claude-auth`) - Non accessibili dal container FastAPI - Autenticazione manuale una tantum alla prima configurazione ### Integrazione Traefik - Sarà implementata come ultima fase del deploy - Il servizio FastAPI sarà raggiungibile tramite reverse proxy Traefik - HTTPS via Let's Encrypt tramite Traefik - Labels Docker per il routing automatico --- ## 10. Configurazione ### .env ``` # Server API_HOST=0.0.0.0 API_PORT=8000 # Sicurezza API_KEY= # Rate Limiting RATE_LIMIT_PER_MINUTE=10 RATE_LIMIT_PER_HOUR=100 # Worker WORKER_URL=http://claude-worker:3001 # Database DB_PATH=/data/opus-agent.db # Claude Code (worker) CLAUDE_MODEL=claude-sonnet-4-6 ``` ### Variabili condivise Entrambi i container ricevono lo stesso `.env` via `env_file`. Il FastAPI service usa le variabili di server, sicurezza, rate limit e DB. Il worker usa `CLAUDE_MODEL` e il path DB per eventuali accessi diretti. --- ## 11. Struttura Progetto ``` opus-agent/ ├── src/ │ └── backend/ │ ├── __init__.py │ ├── main.py # Entry point FastAPI │ ├── config.py # Pydantic Settings │ ├── api/ │ │ ├── __init__.py │ │ ├── dependencies.py # API Key auth + rate limiting │ │ └── routes/ │ │ ├── __init__.py │ │ ├── topics.py # CRUD topics + summarize │ │ ├── requests.py # CRUD requests │ │ ├── sessions.py # Lista/chiudi sessioni │ │ └── health.py # Health check │ ├── models/ │ │ ├── __init__.py │ │ └── api.py # Pydantic models request/response │ ├── db/ │ │ ├── __init__.py │ │ ├── database.py # SQLite connection + init schema │ │ └── models.py # Schema tabelle │ └── services/ │ ├── __init__.py │ ├── queue.py # Logica coda (pick pending, update status) │ └── worker_client.py # Client HTTP per chiamare il worker │ ├── worker/ │ ├── index.js # Server HTTP + Claude Code SDK │ ├── package.json │ └── Dockerfile │ ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_api_topics.py │ ├── test_api_requests.py │ └── test_services.py │ ├── Dockerfile # Backend Python ├── docker-compose.yml ├── pyproject.toml ├── .env.example ├── .gitignore └── README.md ``` --- ## 12. Docker ### Dockerfile Backend (Python) ```dockerfile FROM python:3.11-slim AS base COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /app COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev COPY src/ src/ EXPOSE 8000 CMD ["uv", "run", "start"] ``` ### Dockerfile Worker (Node.js) ```dockerfile FROM node:20-slim RUN npm install -g @anthropic-ai/claude-code WORKDIR /app COPY package.json ./ RUN npm ci --production COPY index.js ./ EXPOSE 3001 CMD ["node", "index.js"] ``` ### docker-compose.yml ```yaml services: fastapi-service: build: context: . dockerfile: Dockerfile ports: - "${API_PORT:-8000}:8000" env_file: .env volumes: - opus-data:/data depends_on: claude-worker: condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"] interval: 10s timeout: 5s retries: 3 claude-worker: build: context: ./worker dockerfile: Dockerfile expose: - "3001" env_file: .env volumes: - opus-data:/data - claude-auth:/home/node/.claude restart: unless-stopped healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:3001/health"] interval: 10s timeout: 5s retries: 3 volumes: opus-data: claude-auth: ``` ### Integrazione Traefik (ultima fase) Nella configurazione finale, le porte dirette verranno sostituite da labels Traefik per il routing automatico: ```yaml # Da aggiungere a fastapi-service labels: - "traefik.enable=true" - "traefik.http.routers.opus-agent.rule=Host(`opus.tuodominio.com`)" - "traefik.http.routers.opus-agent.tls.certresolver=letsencrypt" - "traefik.http.services.opus-agent.loadbalancer.server.port=8000" ``` --- ## 13. Setup Iniziale ### Prima installazione sulla VPS 1. Clona il repository e copia `.env.example` in `.env`, compila i valori 2. Avvia solo il worker per autenticare Claude Code: ```bash docker compose run --rm claude-worker npx claude login ``` 3. Segui il flusso di autenticazione nel browser 4. Le credenziali vengono salvate nel volume `claude-auth` 5. Avvia il sistema completo: ```bash docker compose up -d ``` 6. Verifica con `GET /api/health` ### Rinnovo credenziali Se le credenziali Claude Code scadono: ```bash docker compose run --rm claude-worker npx claude login docker compose restart claude-worker ``` --- ## 14. Limiti e Vincoli Noti - **Throughput**: Una richiesta alla volta. Il tempo di risposta dipende dalla complessità del prompt e dal carico sull'abbonamento Claude. - **Rate limit abbonamento**: Claude Pro/Max ha limiti d'uso propri. Se l'abbonamento raggiunge il suo limite, le richieste falliranno con un errore specifico. Il servizio marca queste come `failed` con il messaggio d'errore di Claude. - **Sessioni multi-turn**: La persistenza delle sessioni dipende dal comportamento del Claude Code SDK. Se il SDK non supporta nativamente il resume di sessioni, si implementa passando lo storico come contesto. - **Volume credenziali**: Le credenziali Claude Code nel volume Docker possono scadere. Serve rinnovo manuale periodico. - **SQLite concorrenza**: Con un solo writer (il queue processor), non ci sono problemi di locking. Se in futuro si volessero più worker paralleli, servirebbe migrare a PostgreSQL o usare WAL mode.