Backend: - Chat, Message, ContextFile models + Alembic migration - Chat CRUD with per-user limit enforcement (max_chats) - SSE streaming endpoint: saves user message, streams Claude response, saves assistant message with token usage metadata - Context assembly: primary context file + conversation history - Admin context CRUD (GET/PUT with version tracking) - Anthropic SDK integration with async streaming - Chat ownership isolation (users can't access each other's chats) Frontend: - Chat page with sidebar chat list + main chat window - Real-time SSE streaming via fetch + ReadableStream - Message bubbles with Markdown rendering (react-markdown) - Auto-growing message input (Enter to send, Shift+Enter newline) - Zustand chat store for streaming state management - Admin primary context editor with unsaved changes warning - Updated routing: /chat, /chat/:chatId, /admin/context - Enabled Chat and Admin sidebar navigation - English + Russian translations for all new UI Infrastructure: - nginx: disabled proxy buffering for SSE support - Added ANTHROPIC_API_KEY and CLAUDE_MODEL to config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
253 lines
12 KiB
Markdown
253 lines
12 KiB
Markdown
# Phase 2: Chat & AI Core — Subplan
|
||
|
||
## Goal
|
||
|
||
Deliver a working AI chat system where users can create conversations, send messages, and receive streamed Claude API responses via SSE, with admin-editable primary context and per-user chat limits enforced.
|
||
|
||
## Prerequisites
|
||
|
||
- Phase 1 completed: auth endpoints, frontend shell with login/register/dashboard
|
||
- Anthropic API key available (`ANTHROPIC_API_KEY` env var)
|
||
|
||
---
|
||
|
||
## Database Schema (Phase 2)
|
||
|
||
### `chats` table
|
||
|
||
| Column | Type | Constraints |
|
||
|---|---|---|
|
||
| id | UUID | PK, default uuid4 (inherited from Base) |
|
||
| user_id | UUID | FK -> users.id ON DELETE CASCADE, NOT NULL, indexed |
|
||
| title | VARCHAR(255) | NOT NULL, default 'New Chat' |
|
||
| skill_id | UUID | NULL (unused until Phase 3, added now for schema stability) |
|
||
| is_archived | BOOLEAN | NOT NULL, default false |
|
||
| created_at | TIMESTAMPTZ | inherited from Base |
|
||
| updated_at | TIMESTAMPTZ | NOT NULL, default now(), onupdate now() |
|
||
|
||
### `messages` table
|
||
|
||
| Column | Type | Constraints |
|
||
|---|---|---|
|
||
| id | UUID | PK, default uuid4 (inherited from Base) |
|
||
| chat_id | UUID | FK -> chats.id ON DELETE CASCADE, NOT NULL, indexed |
|
||
| role | VARCHAR(20) | NOT NULL, CHECK IN ('user','assistant','system','tool') |
|
||
| content | TEXT | NOT NULL |
|
||
| metadata | JSONB | NULL (token counts, model info) |
|
||
| created_at | TIMESTAMPTZ | inherited from Base |
|
||
|
||
### `context_files` table
|
||
|
||
| Column | Type | Constraints |
|
||
|---|---|---|
|
||
| id | UUID | PK, default uuid4 (inherited from Base) |
|
||
| type | VARCHAR(20) | NOT NULL, CHECK IN ('primary','personal') |
|
||
| user_id | UUID | FK -> users.id ON DELETE CASCADE, NULL (NULL for primary) |
|
||
| content | TEXT | NOT NULL, default '' |
|
||
| version | INTEGER | NOT NULL, default 1 |
|
||
| updated_by | UUID | FK -> users.id ON DELETE SET NULL, NULL |
|
||
| created_at | TIMESTAMPTZ | inherited from Base |
|
||
| updated_at | TIMESTAMPTZ | NOT NULL, default now(), onupdate now() |
|
||
|
||
UNIQUE constraint: `(type, user_id)`
|
||
|
||
---
|
||
|
||
## API Endpoints (Phase 2)
|
||
|
||
### Chat CRUD — `/api/v1/chats/`
|
||
|
||
| Method | Path | Request | Response | Notes |
|
||
|---|---|---|---|---|
|
||
| POST | `/` | `{ title?: string }` | `ChatResponse` 201 | Enforces `user.max_chats` on non-archived chats |
|
||
| GET | `/` | Query: `archived?: bool` | `ChatListResponse` | User's chats, ordered by updated_at desc |
|
||
| GET | `/{chat_id}` | — | `ChatResponse` | 404 if not owned |
|
||
| PATCH | `/{chat_id}` | `{ title?, is_archived? }` | `ChatResponse` | Ownership check |
|
||
| DELETE | `/{chat_id}` | — | 204 | Cascades messages |
|
||
|
||
### Messages — `/api/v1/chats/{chat_id}/messages`
|
||
|
||
| Method | Path | Request | Response | Notes |
|
||
|---|---|---|---|---|
|
||
| GET | `/` | Query: `limit?=50, before?: UUID` | `MessageListResponse` | Cursor-paginated |
|
||
| POST | `/` | `{ content: string }` | SSE stream | Saves user msg, streams Claude response, saves assistant msg |
|
||
|
||
### Admin Context — `/api/v1/admin/context`
|
||
|
||
| Method | Path | Request | Response | Notes |
|
||
|---|---|---|---|---|
|
||
| GET | `/` | — | `ContextFileResponse` | Primary context content + version |
|
||
| PUT | `/` | `{ content: string }` | `ContextFileResponse` | Upsert, increments version |
|
||
|
||
### SSE Event Format
|
||
|
||
```
|
||
event: message_start
|
||
data: {"message_id": "<uuid>"}
|
||
|
||
event: content_delta
|
||
data: {"delta": "partial text..."}
|
||
|
||
event: message_end
|
||
data: {"message_id": "<uuid>", "metadata": {"model": "...", "input_tokens": N, "output_tokens": N}}
|
||
|
||
event: error
|
||
data: {"detail": "Error description"}
|
||
```
|
||
|
||
---
|
||
|
||
## Context Assembly (Phase 2)
|
||
|
||
1. Primary context file (admin-edited) → Claude `system` parameter
|
||
2. Conversation history → Claude `messages` array
|
||
3. Current user message → appended as final user message
|
||
|
||
Hardcoded default system prompt used when no primary context file exists.
|
||
|
||
---
|
||
|
||
## Tasks
|
||
|
||
### A. Backend Database & Models (Tasks 1–4)
|
||
|
||
- [x] **- [x] **A1.** Add `ANTHROPIC_API_KEY` and `CLAUDE_MODEL` (default `claude-sonnet-4-20250514`) to `backend/app/config.py`. Add `anthropic` to `backend/pyproject.toml`. Add vars to `.env.example`.
|
||
|
||
- [x] **- [x] **A2.** Create `backend/app/models/chat.py`: Chat model per schema. Relationships: `messages`, `user`. Add `chats` relationship on User model.
|
||
|
||
- [x] **- [x] **A3.** Create `backend/app/models/message.py`: Message model per schema. Relationship: `chat`.
|
||
|
||
- [x] **- [x] **A4.** Create `backend/app/models/context_file.py`: ContextFile model per schema. UNIQUE(type, user_id). Update `backend/app/models/__init__.py`. Create Alembic migration `002_create_chats_messages_context_files.py`.
|
||
|
||
### B. Backend Schemas (Task 5)
|
||
|
||
- [x] **- [x] **B5.** Create `backend/app/schemas/chat.py`: `CreateChatRequest`, `UpdateChatRequest`, `SendMessageRequest`, `ChatResponse`, `ChatListResponse`, `MessageResponse`, `MessageListResponse`, `ContextFileResponse`, `UpdateContextRequest`.
|
||
|
||
### C. Backend Services (Tasks 6–8)
|
||
|
||
- [x] **- [x] **C6.** Create `backend/app/services/chat_service.py`: `create_chat`, `get_user_chats`, `get_chat`, `update_chat`, `delete_chat`, `get_messages`, `save_message`. Chat limit enforcement: count non-archived chats vs `user.max_chats`.
|
||
|
||
- [x] **- [x] **C7.** Create `backend/app/services/ai_service.py`: `assemble_context` (loads primary context + history), `stream_ai_response` (async generator: saves user msg → calls Claude streaming API → yields SSE events → saves assistant msg). Uses `anthropic.AsyncAnthropic().messages.stream()`.
|
||
|
||
- [x] **- [x] **C8.** Create `backend/app/services/context_service.py`: `get_primary_context`, `upsert_primary_context`. Default system prompt constant.
|
||
|
||
### D. Backend API Endpoints (Tasks 9–11)
|
||
|
||
- [x] **- [x] **D9.** Create `backend/app/api/v1/chats.py`: Chat CRUD + message list + SSE streaming endpoint. Returns `StreamingResponse(media_type="text/event-stream")` with no-cache headers.
|
||
|
||
- [x] **- [x] **D10.** Create `backend/app/api/v1/admin.py`: `GET /admin/context`, `PUT /admin/context`. Protected by `require_admin`.
|
||
|
||
- [x] **- [x] **D11.** Register routers in `backend/app/api/v1/router.py`. Add `proxy_buffering off;` to nginx `/api/` location for SSE.
|
||
|
||
### E. Frontend API & Store (Tasks 12–14)
|
||
|
||
- [x] **- [x] **E12.** Create `frontend/src/api/chats.ts`: typed API functions + `sendMessage` using `fetch()` + `ReadableStream` for SSE parsing.
|
||
|
||
- [x] **- [x] **E13.** Create `frontend/src/api/admin.ts`: `getPrimaryContext()`, `updatePrimaryContext(content)`.
|
||
|
||
- [x] **- [x] **E14.** Create `frontend/src/stores/chat-store.ts` (Zustand, not persisted): chats, currentChatId, messages, isStreaming, streamingContent, actions.
|
||
|
||
### F. Frontend Chat Components (Tasks 15–20)
|
||
|
||
- [x] **- [x] **F15.** Create `frontend/src/components/chat/chat-list.tsx`: chat sidebar with list, new chat button, archive/delete actions. TanStack Query for fetching.
|
||
|
||
- [x] **- [x] **F16.** Create `frontend/src/components/chat/message-bubble.tsx`: role-based styling (user right/blue, assistant left/gray). Markdown rendering for assistant messages (`react-markdown` + `remark-gfm`). Add deps to `package.json`.
|
||
|
||
- [x] **- [x] **F17.** Create `frontend/src/components/chat/message-input.tsx`: auto-growing textarea, Enter to send (Shift+Enter newline), disabled while streaming, send icon button.
|
||
|
||
- [x] **- [x] **F18.** Create `frontend/src/components/chat/chat-window.tsx`: message list (auto-scroll) + input. Loads messages via TanStack Query. Handles streaming flow. Empty state.
|
||
|
||
- [x] **- [x] **F19.** Create `frontend/src/pages/chat.tsx`: chat list panel (left) + chat window (right). Routes: `/chat` and `/chat/:chatId`.
|
||
|
||
- [x] **- [x] **F20.** Create `frontend/src/hooks/use-chat.ts`: encapsulates TanStack queries, mutations, SSE send flow with store updates, abort on unmount.
|
||
|
||
### G. Frontend Admin Context Editor (Tasks 21–22)
|
||
|
||
- [x] **- [x] **G21.** Create `frontend/src/components/admin/context-editor.tsx`: textarea editor, load/save, version display, unsaved changes warning.
|
||
|
||
- [x] **- [x] **G22.** Create `frontend/src/pages/admin/context.tsx`: page wrapper, admin role protected.
|
||
|
||
### H. Frontend Routing & Navigation (Tasks 23–24)
|
||
|
||
- [x] **- [x] **H23.** Update `frontend/src/routes.tsx`: add `/chat`, `/chat/:chatId`, `/admin/context` routes.
|
||
|
||
- [x] **- [x] **H24.** Update `frontend/src/components/layout/sidebar.tsx`: enable Chats and Admin nav items.
|
||
|
||
### I. i18n (Task 25)
|
||
|
||
- [x] **- [x] **I25.** Update `en/translation.json` and `ru/translation.json` with chat and admin keys.
|
||
|
||
### J. Backend Tests (Task 26)
|
||
|
||
- [x] **- [x] **J26.** Create `backend/tests/test_chats.py`: chat CRUD, limit enforcement, message pagination, SSE streaming format, admin context CRUD, ownership isolation.
|
||
|
||
---
|
||
|
||
## Files to Create
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `backend/app/models/chat.py` | Chat ORM model |
|
||
| `backend/app/models/message.py` | Message ORM model |
|
||
| `backend/app/models/context_file.py` | ContextFile ORM model |
|
||
| `backend/alembic/versions/002_create_chats_messages_context_files.py` | Migration |
|
||
| `backend/app/schemas/chat.py` | Request/response schemas |
|
||
| `backend/app/services/chat_service.py` | Chat + message business logic |
|
||
| `backend/app/services/ai_service.py` | Claude API integration + SSE streaming |
|
||
| `backend/app/services/context_service.py` | Primary context CRUD |
|
||
| `backend/app/api/v1/chats.py` | Chat + message endpoints |
|
||
| `backend/app/api/v1/admin.py` | Admin context endpoints |
|
||
| `backend/tests/test_chats.py` | Tests |
|
||
| `frontend/src/api/chats.ts` | Chat API client + SSE consumer |
|
||
| `frontend/src/api/admin.ts` | Admin API client |
|
||
| `frontend/src/stores/chat-store.ts` | Chat UI state |
|
||
| `frontend/src/hooks/use-chat.ts` | Chat data + streaming hook |
|
||
| `frontend/src/components/chat/chat-list.tsx` | Chat list sidebar |
|
||
| `frontend/src/components/chat/message-bubble.tsx` | Message display |
|
||
| `frontend/src/components/chat/message-input.tsx` | Message input |
|
||
| `frontend/src/components/chat/chat-window.tsx` | Main chat area |
|
||
| `frontend/src/pages/chat.tsx` | Chat page |
|
||
| `frontend/src/components/admin/context-editor.tsx` | Context editor |
|
||
| `frontend/src/pages/admin/context.tsx` | Admin context page |
|
||
|
||
## Files to Modify
|
||
|
||
| File | Change |
|
||
|---|---|
|
||
| `backend/pyproject.toml` | Add `anthropic` dependency |
|
||
| `backend/app/config.py` | Add `ANTHROPIC_API_KEY`, `CLAUDE_MODEL` |
|
||
| `backend/app/models/__init__.py` | Import Chat, Message, ContextFile |
|
||
| `backend/app/models/user.py` | Add `chats` relationship |
|
||
| `backend/app/api/v1/router.py` | Include chats + admin routers |
|
||
| `nginx/nginx.conf` | Add `proxy_buffering off;` for SSE |
|
||
| `.env.example` | Add `ANTHROPIC_API_KEY`, `CLAUDE_MODEL` |
|
||
| `frontend/package.json` | Add `react-markdown`, `remark-gfm` |
|
||
| `frontend/src/routes.tsx` | Add chat + admin routes |
|
||
| `frontend/src/components/layout/sidebar.tsx` | Enable chat + admin nav |
|
||
| `frontend/public/locales/en/translation.json` | Chat + admin keys |
|
||
| `frontend/public/locales/ru/translation.json` | Chat + admin keys |
|
||
|
||
---
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. Migration creates `chats`, `messages`, `context_files` tables correctly
|
||
2. `POST /chats/` enforces max_chats limit (403 when exceeded)
|
||
3. Chat CRUD works with ownership isolation (user A can't access user B's chats)
|
||
4. `POST /chats/{id}/messages` returns SSE stream with `message_start`, `content_delta`, `message_end` events
|
||
5. Both user and assistant messages are persisted after streaming completes
|
||
6. Context assembly includes primary context file as system prompt
|
||
7. Admin context GET/PUT works with version incrementing, protected by admin role
|
||
8. Frontend: `/chat` shows chat list, create/select/archive/delete chats
|
||
9. Frontend: sending a message streams the AI response character-by-character
|
||
10. Frontend: admin can edit the primary context at `/admin/context`
|
||
11. Sidebar shows enabled Chat and Admin navigation
|
||
12. All new UI text works in both English and Russian
|
||
13. All backend tests pass
|
||
|
||
---
|
||
|
||
## Status
|
||
|
||
**COMPLETED** — All code written. TypeScript compiles clean. Vite builds successfully. Docker smoke test deferred (Docker not installed).
|