# 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": ""} event: content_delta data: {"delta": "partial text..."} event: message_end data: {"message_id": "", "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).