Files
personal-ai-assistant/backend/alembic/versions/002_create_chats_messages_context_files.py
dolgolyov.alexei 70469beef8 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>
2026-03-19 12:38:30 +03:00

60 lines
2.7 KiB
Python

"""Create chats, messages, and context_files tables
Revision ID: 002
Revises: 001
Create Date: 2026-03-19
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB
revision: str = "002"
down_revision: Union[str, None] = "001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"chats",
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("title", sa.String(255), nullable=False, server_default="New Chat"),
sa.Column("skill_id", UUID(as_uuid=True), nullable=True),
sa.Column("is_archived", sa.Boolean, nullable=False, server_default=sa.text("false")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
)
op.create_table(
"messages",
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("chat_id", UUID(as_uuid=True), sa.ForeignKey("chats.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("role", sa.String(20), nullable=False),
sa.Column("content", sa.Text, nullable=False),
sa.Column("metadata", JSONB, nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
)
op.create_table(
"context_files",
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("type", sa.String(20), nullable=False),
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=True),
sa.Column("content", sa.Text, nullable=False, server_default=""),
sa.Column("version", sa.Integer, nullable=False, server_default=sa.text("1")),
sa.Column("updated_by", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.UniqueConstraint("type", "user_id", name="uq_context_files_type_user"),
)
def downgrade() -> None:
op.drop_table("context_files")
op.drop_table("messages")
op.drop_table("chats")