Phase 2: Chat & AI Core — Claude API streaming, chat UI, admin context

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>
This commit is contained in:
2026-03-19 12:38:30 +03:00
parent 7c752cae6b
commit 70469beef8
39 changed files with 4168 additions and 47 deletions

252
plans/phase-2-chat-ai.md Normal file
View File

@@ -0,0 +1,252 @@
# 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 14)
- [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 68)
- [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 911)
- [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 1214)
- [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 1520)
- [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 2122)
- [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 2324)
- [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).