Files
opus-agent/CLAUDE.md
T
Adriano Dal Pastro ad38cf7eaf docs: document sdk_conversation_id and multi-turn session flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 21:01:03 +00:00

125 lines
5.3 KiB
Markdown

# OpusAgent
Self-hosted REST API proxy to Claude models via Claude Code SDK, leveraging a Claude Pro/Max subscription.
## Architecture
Two Docker containers behind Traefik reverse proxy:
```
Client → Traefik (HTTPS) → FastAPI (Python, :8000) ↔ Node.js Worker (:3001, internal)
↓ ↓
SQLite (WAL) Claude Code SDK
```
- **fastapi-service**: API REST, auth, rate limiting, async queue processing
- **claude-worker**: Express server wrapping `@anthropic-ai/claude-code` SDK
## Key Paths
| Area | Path |
|------|------|
| FastAPI entrypoint | `src/backend/main.py` |
| Config (Pydantic Settings) | `src/backend/config.py` |
| Database layer (aiosqlite) | `src/backend/db/database.py` |
| API models + envelope | `src/backend/models/api.py` |
| Routes | `src/backend/api/routes/{health,topics,requests,sessions}.py` |
| Auth + rate limiter | `src/backend/api/dependencies.py` |
| Worker HTTP client | `src/backend/services/worker_client.py` |
| Queue processor | `src/backend/services/queue.py` |
| Node.js worker | `worker/index.js` |
| Tests | `tests/` |
| Design spec (Italian) | `docs/superpowers/specs/` |
| Implementation plan | `docs/superpowers/plans/` |
## Tech Stack
- **Python 3.11+**: FastAPI, uvicorn, aiosqlite, httpx, slowapi, pydantic-settings
- **Node.js 20**: Express, @anthropic-ai/claude-code SDK (v1.0.128+, async iterable API)
- **Package manager**: uv (Python), npm (Node.js)
- **Database**: SQLite with WAL mode, foreign keys enabled
- **Infra**: Docker Compose, Traefik v3 (TLS via Let's Encrypt)
## Database Schema
Three tables in SQLite:
- **topics**: id (UUID), name (unique), system_prompt, summary, model (nullable), timestamps
- **requests**: id (UUID), session_id, sdk_conversation_id, topic_id (FK), prompt, result, error, model, summarize (bool), status (pending/processing/completed/failed), timestamps
- **closed_sessions**: session_id (PK), closed_at
## API Endpoints
All require `X-Api-Key` header. All responses use envelope: `{success, data, error}`.
- `GET /api/health` — health check (DB + worker)
- `POST|GET /api/topics`, `GET|PUT|DELETE /api/topics/{id}` — topic CRUD
- `POST /api/topics/{id}/summarize` — regenerate complete topic summary
- `POST /api/requests` (202) — submit prompt (async queue), `GET|DELETE /api/requests[/{id}]`
- `GET /api/sessions`, `DELETE /api/sessions/{id}` — session management
## Processing Flow
1. `POST /api/requests` → 202 Accepted (queued)
2. Background loop picks oldest pending, calls worker `POST /process`
3. Worker runs Claude Code SDK (`query()` async iterable) with optional session resume
4. Result stored in DB; SDK conversation ID saved in `sdk_conversation_id`; optional incremental summary appended to topic
5. Client polls `GET /api/requests/{id}` for result
Model resolution: request-level > topic-level > `CLAUDE_MODEL` env var.
### Multi-turn sessions
- `session_id: "new"` → stored as NULL; after completion, set to SDK's conversation ID
- Custom `session_id` → preserved as-is; SDK conversation ID stored separately in `sdk_conversation_id`
- Resume logic: queue looks up `sdk_conversation_id` from previous completed requests in the same session; only passes it to worker if found (avoids resume on non-existent SDK sessions)
## Commands
```bash
# Development
uv sync --all-groups
uv run dev # uvicorn with --reload
# Production
docker compose up -d # both services
docker compose exec claude-worker npx claude login # first-time auth (must run on active container, NOT run --rm)
# Tests
uv run pytest -v
# Logs
docker compose logs -f fastapi-service
docker compose logs -f claude-worker
```
## Environment Variables (.env)
| Variable | Default | Purpose |
|----------|---------|---------|
| `API_KEY` | (required) | Auth key for all endpoints |
| `API_HOST` | 0.0.0.0 | Server bind address |
| `API_PORT` | 8000 | Server port |
| `WORKER_URL` | http://claude-worker:3001 | Internal worker address |
| `DB_PATH` | /data/opus-agent.db | SQLite database path |
| `CLAUDE_MODEL` | claude-sonnet-4-6 | Default Claude model |
| `RATE_LIMIT_PER_MINUTE` | 10 | Rate limit per API key |
| `RATE_LIMIT_PER_HOUR` | 100 | Rate limit per API key |
## Design Decisions
- **Sequential processing**: one request at a time (FIFO), no concurrency — simplifies Claude Code SDK session handling
- **Async Python**: FastAPI + aiosqlite for non-blocking I/O while queue runs in background
- **SQLite + WAL**: sufficient for single-writer pattern, no external DB needed
- **Envelope pattern**: consistent `{success, data, error}` on all responses
- **Session = conversation**: session_id maps to Claude Code SDK `resume` option
- **Worker isolation**: not exposed to host, only reachable from fastapi-service via Docker internal network
- **Claude auth volume**: credentials stored at `/root/.claude` (not `/home/node/.claude`) because the worker process runs as root inside the container
- **SDK async iteration**: `query()` returns an async iterable (not a promise); iterate with `for await...of` and extract the message with `type === "result"`
## Deployment
Domain: `opus-agent.tielogic.xyz` → Traefik → fastapi-service:8000
Traefik config: `/opt/docker/traefik/dynamic.yml` (file provider, auto-reload).