step 8 complete: REST + WebSocket API routes for chat, plans, storage, vectors, backup, plugins, billing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 15:33:57 +01:00
parent e72d72f4f6
commit 35dd9ac86f
9 changed files with 928 additions and 57 deletions

View File

@@ -197,54 +197,50 @@ adiuva-api/
### Step 6 — Chat Agents ✅
- [x] `app/agents/task_agent.py` — `@registry.register`:
- Description: "Manages tasks: create, update, list, suggest"
- Tools: `create_task(title, description, priority, due_date)`, `update_task(id, updates)`, `list_tasks(filters)`, `suggest_tasks(notes_context)`
- System prompt: PM-oriented, validates task structure, infers priority from context
- `handle()`: LLM + tool loop via `_tool_loop()`, returns response text + list of actions performed
- Accepts flexible context: mandatory fields `user_profile` + `message`, all other fields (from batch/plugin output) are optional
- [x] `app/agents/calendar_agent.py` — `@registry.register`:
- Description: "Calendar management: events, conflicts, scheduling"
- Tools: `list_events(date_range)`, `detect_conflicts(events)`, `suggest_reschedule(conflict)`
- Works with event metadata passed in context (never raw calendar data stored)
- [x] `app/agents/email_agent.py` — `@registry.register`:
- Description: "Email analysis: classify, extract actions, draft responses"
- Tools: `classify_email(metadata)`, `extract_action_items(metadata)`, `draft_response(thread_context)`
- Only processes metadata sent by client — never raw email bodies
- [x] `app/agents/analytics_agent.py` — `@registry.register`:
- Description: "Workspace analytics: metrics, reports, trends"
- Tools: `calculate_metrics(task_data)`, `generate_report(period, data)`, `trend_analysis(data_points)`
- Crunches numbers from context, returns structured insights
- [x] `app/agents/__init__.py`: imports all agent modules to trigger `@registry.register` decorators
- [x] Unit tests per agent with mocked LLM
- **Outcome:** Four specialized agents, all registered and tested.
- Description: "Manages tasks and comments: list, create, update, delete, due-today, comments"
- Tools (8): `list_tasks(project_id, status, search, order_by)`, `create_task(title, description, status, priority, assignees, due_date, project_id, is_ai_suggested, is_approved)`, `update_task(task_id, ...)`, `delete_task(task_id)`, `list_tasks_due_today()`, `list_task_comments(task_id)`, `add_task_comment(task_id, author, content)`, `delete_task_comment(comment_id)`
- status: `todo|in_progress|done`; priority: `high|medium|low`; assignees: JSON-encoded string; due_date: ms timestamp
- Accepts flexible context; sentinel `-1` for optional integer update fields
- [x] `app/agents/checkpoint_agent.py` `@registry.register`:
- Description: "Manages project checkpoints (milestones): list, create, update, delete"
- Tools (4): `list_checkpoints(project_id)`, `create_checkpoint(project_id, title, date, is_ai_suggested, is_approved)`, `update_checkpoint(checkpoint_id, ...)`, `delete_checkpoint(checkpoint_id)`
- `project_id` is required for create; date is a ms timestamp; supports AI-suggestion + approval workflow
- [x] `app/agents/project_agent.py` — `@registry.register`:
- Description: "Manages projects: list, get, create, update, archive, delete"
- Tools (6): `list_projects(client_id, include_archived)`, `list_all_projects()`, `get_project(project_id)`, `create_project(name, client_id)`, `update_project(project_id, ...)`, `delete_project(project_id)`
- status: `active|archived`; prefers archive over deletion (docstring guard on delete)
- [x] `app/agents/note_agent.py` — `@registry.register`:
- Description: "Manages notes: list, get, create, update, delete"
- Tools (5): `list_notes(project_id)`, `get_note(note_id)`, `create_note(title, content, project_id)`, `update_note(note_id, ...)`, `delete_note(note_id)`
- content is Markdown; `get_note` should be called before update to preserve existing content
- [x] `app/agents/__init__.py`: imports all four agent modules to trigger `@registry.register` decorators
- [x] Unit tests per agent with mocked LLM (registration, names, tool counts, handle(), direct tool invocation)
- **Outcome:** Four domain-specific agents matching the UI data model (Tasks, Checkpoints, Projects, Notes), all registered and tested.
### Step 7 — Storage Layer
- [ ] `app/storage/blob_store.py`:
- `BlobStore`:
- `async upload(user_id, table, record_id, blob: bytes, checksum: str) -> str` — returns S3 key
- `async download(user_id, s3_key) -> bytes`
- `async delete(user_id, s3_key) -> None`
- `async list_keys(user_id, table) -> list[str]`
- Keys structured as `{user_id}/{table}/{record_id}` — backend never inspects blob content
- Uses boto3 S3 with server-side encryption at rest (SSE-S3) as extra layer
- [ ] `app/storage/vector_store.py`:
- `VectorStore`:
- `async upsert(user_id, vectors: list[VectorItem]) -> None` — vectors are pre-encrypted blobs
- `async search(user_id, query_blob: bytes, top_k: int) -> list[VectorSearchResult]`
- `async delete(user_id, vector_ids: list[str]) -> None`
- Wraps Pinecone (default) or Qdrant — configurable via settings
- Namespace per `user_id` for isolation
- Note: because vectors are E2E encrypted by client, ANN search is on the encrypted representation — semantic search accuracy is a known trade-off when users choose cloud vectors
- [ ] `app/storage/encryption.py`:
- `verify_checksum(blob: bytes, checksum: str) -> bool` — SHA-256 HMAC integrity check only
- `reject_if_tampered(blob, checksum)` — raises `400` if mismatch
- Backend NEVER holds decryption keys — all crypto is client-side
### Step 7 — Storage Layer
- [x] `app/storage/blob_store.py`:
- `BlobStore`: `async upload`, `async download`, `async delete` (idempotent), `async list_keys`
- Keys: `{user_id}/{table}/{record_id}` — backend never inspects blob content
- boto3 S3 with SSE-S3 at-rest encryption; client checksum stored in S3 object metadata
- [x] `app/storage/vector_store.py`:
- `VectorStore`: `async upsert`, `async search`, `async delete`
- Pinecone (default, `namespace=user_id`) or Qdrant (`user_id` payload filter) — runtime-configurable
- 32-dim SHA-256-derived float vector; blob stored as base64 in metadata/payload
- ANN on encrypted data: known accuracy trade-off, documented
- [x] `app/storage/encryption.py`:
- `verify_checksum(blob, checksum) -> bool` — SHA-256 + `hmac.compare_digest` (constant-time)
- `reject_if_tampered(blob, checksum)` — raises `HTTP 400` on mismatch
- Backend NEVER holds decryption keys
- [x] `app/schemas.py`: added `StorageRecord*`, `VectorItem`, `VectorUpsertRequest`, `VectorSearch*`, `Plugin*` schemas
- [x] `app/config/settings.py`: added `PINECONE_API_KEY`, `PINECONE_INDEX`, `QDRANT_URL`, `QDRANT_API_KEY`
- [x] `requirements.txt`: added `moto[s3]`, `pinecone`, `qdrant-client`
- [x] 37 unit tests covering encryption, BlobStore (moto), VectorStore Pinecone, VectorStore Qdrant
- **Outcome:** Cloud storage layer that handles E2E encrypted blobs without ever accessing plaintext.
### Step 8 — API Routes
### Step 8 — API Routes
#### 8a — Chat endpoint
- [ ] `app/api/routes/chat.py`:
- [x] `app/api/routes/chat.py`:
- `POST /api/v1/chat`:
- Request: `ChatRequest`
- Calls `orchestrate(request)` or `orchestrate()` + `build_plan()`
@@ -256,12 +252,12 @@ adiuva-api/
- Heartbeat ping every 30s to keep connection alive
#### 8b — Plans endpoint
- [ ] `app/api/routes/plans.py`:
- [x] `app/api/routes/plans.py`:
- `GET /api/v1/plans/playbook`: Returns all playbooks available for the user's tier
- `GET /api/v1/plans/playbook/{plan_id}`: Returns a specific plan
#### 8c — Storage endpoint (cloud records)
- [ ] `app/api/routes/storage.py`:
- [x] `app/api/routes/storage.py`:
- `POST /api/v1/storage/records`: Create encrypted record
- Request: `StorageRecordCreate`
- Verifies checksum, stores blob in S3, inserts metadata row in PostgreSQL
@@ -277,7 +273,7 @@ adiuva-api/
- All routes enforce tier cloud_storage_gb quota via `TierManager.check_quota(user_id)`
#### 8d — Vectors endpoint (cloud vector store)
- [ ] `app/api/routes/vectors.py`:
- [x] `app/api/routes/vectors.py`:
- `POST /api/v1/storage/vectors/upsert`:
- Request: `VectorUpsertRequest`
- Verifies checksums, delegates to `VectorStore.upsert()`
@@ -290,7 +286,7 @@ adiuva-api/
- Request: `{ids: list[str]}`
#### 8e — Backup endpoint
- [ ] `app/api/routes/backup.py`:
- [x] `app/api/routes/backup.py`:
- `PUT /api/v1/backup`: Accepts binary blob + metadata headers (`X-Backup-Version`, `X-Backup-Timestamp`, `X-Backup-Checksum`). Stores in S3 keyed by `{user_id}/{timestamp}`. Enforces tier limits:
- Free: 0 (no backup)
- Pro: 5 GB
@@ -301,7 +297,7 @@ adiuva-api/
- `DELETE /api/v1/backup/{backup_id}`: Delete specific backup.
#### 8f — Plugins endpoint
- [ ] `app/api/routes/plugins.py`:
- [x] `app/api/routes/plugins.py`:
- `GET /api/v1/plugins`:
- Query params: `category: str | None`, `q: str | None`, `page: int`, `sort: Literal['rating', 'installs', 'newest']`
- Response: `PluginListResponse`
@@ -317,14 +313,14 @@ adiuva-api/
- Unregisters installation
#### 8g — Auth endpoint
- [ ] `app/api/routes/auth.py`:
- [x] `app/api/routes/auth.py`:
- `POST /api/v1/auth/register`: `{email, password}` → bcrypt hash → insert user → return `AuthTokens`
- `POST /api/v1/auth/login`: Validate credentials → return `AuthTokens`
- `POST /api/v1/auth/refresh`: Rotate refresh token → return new `AuthTokens`
- `GET /api/v1/auth/me`: Return `UserProfile` for current JWT
#### 8h — Billing endpoint
- [ ] `app/api/routes/billing.py`:
- [x] `app/api/routes/billing.py`:
- `POST /api/v1/billing/checkout`: Creates Stripe checkout session → returns URL
- `POST /api/v1/billing/webhook`: Handles Stripe webhooks (subscription lifecycle)
- `GET /api/v1/billing/subscription`: Returns current subscription info