Phase 3: Skills & Context — skill system, personal context, context layering

Backend:
- Skill model + migration (with FK on chats.skill_id)
- Personal + general skill CRUD services with access isolation
- Admin skill CRUD endpoints (POST/GET/PATCH/DELETE /admin/skills)
- User skill CRUD endpoints (POST/GET/PATCH/DELETE /skills/)
- Personal context GET/PUT at /users/me/context
- Extended context assembly: primary + personal context + skill prompt
- Chat creation/update now accepts skill_id with validation

Frontend:
- Skill selector dropdown in chat header (grouped: general + personal)
- Reusable skill editor form component
- Admin skills management page (/admin/skills)
- Personal skills page (/skills)
- Personal context editor page (/profile/context)
- Updated sidebar: Skills, My Context nav items + admin skills link
- English + Russian translations for all skill/context UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 12:55:02 +03:00
parent 70469beef8
commit 03afb7a075
33 changed files with 1387 additions and 62 deletions

View File

@@ -0,0 +1,46 @@
"""Create skills table and add FK on chats.skill_id
Revision ID: 003
Revises: 002
Create Date: 2026-03-19
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
revision: str = "003"
down_revision: Union[str, None] = "002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"skills",
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=True, index=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("description", sa.Text, nullable=True),
sa.Column("system_prompt", sa.Text, nullable=False),
sa.Column("icon", sa.String(50), nullable=True),
sa.Column("is_active", sa.Boolean, nullable=False, server_default=sa.text("true")),
sa.Column("sort_order", sa.Integer, nullable=False, server_default=sa.text("0")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
)
op.create_foreign_key(
"fk_chats_skill_id",
"chats",
"skills",
["skill_id"],
["id"],
ondelete="SET NULL",
)
def downgrade() -> None:
op.drop_constraint("fk_chats_skill_id", "chats", type_="foreignkey")
op.drop_table("skills")