Files
opus-agent/docs/superpowers/specs/2026-05-25-opus-agent-design.md
T
2026-05-25 18:26:20 +02:00

678 lines
20 KiB
Markdown

# 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",
"model": "claude-sonnet-4-6",
"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",
"session_id": "a1b2c3d4-...",
"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?",
"model": null,
"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
Il `session_id` è generato dal server. Il client non deve inventarne uno.
1. Prima richiesta: `POST /api/requests {prompt, topic_id}` senza session_id
2. Il sistema genera un UUID come `session_id`, lo salva nel record
3. La risposta include il `session_id` generato
4. Richieste successive: il client passa lo stesso `session_id` ricevuto
5. Il worker Node.js usa il session_id per mantenere il contesto con Claude Code
(flag `--resume` o equivalente SDK)
6. Ogni richiesta nella sessione condivide la storia della conversazione
Per one-shot: il client non invia `session_id` (o lo invia null). Il record
viene creato senza session_id e la richiesta è indipendente.
### 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.