- Добавлена поддержка персональных AI моделей для каждого пользователя - Оптимизация создания заданий: батч-запрос к AI вместо N запросов - Фильтрация слов по языку изучения (source_lang) в словаре - Удалены неиспользуемые колонки examples и category из vocabulary - Миграции для ai_model_id и удаления колонок 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
142 lines
6.9 KiB
Python
142 lines
6.9 KiB
Python
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import String, BigInteger, DateTime, Integer, Boolean, JSON, Enum as SQLEnum, UniqueConstraint
|
||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||
import enum
|
||
|
||
|
||
class Base(DeclarativeBase):
|
||
"""Базовая модель"""
|
||
pass
|
||
|
||
|
||
class LanguageLevel(str, enum.Enum):
|
||
"""Уровни владения языком (CEFR)"""
|
||
A1 = "A1"
|
||
A2 = "A2"
|
||
B1 = "B1"
|
||
B2 = "B2"
|
||
C1 = "C1"
|
||
C2 = "C2"
|
||
|
||
|
||
class JLPTLevel(str, enum.Enum):
|
||
"""Уровни JLPT для японского языка"""
|
||
N5 = "N5" # Базовый
|
||
N4 = "N4" # Начальный
|
||
N3 = "N3" # Средний
|
||
N2 = "N2" # Продвинутый
|
||
N1 = "N1" # Свободный
|
||
|
||
|
||
# Языки, использующие JLPT вместо CEFR
|
||
JLPT_LANGUAGES = {"ja"}
|
||
|
||
# Дефолтные уровни для разных систем
|
||
DEFAULT_CEFR_LEVEL = "A1"
|
||
DEFAULT_JLPT_LEVEL = "N5"
|
||
|
||
|
||
class WordSource(str, enum.Enum):
|
||
"""Источник добавления слова"""
|
||
MANUAL = "manual" # Ручное добавление
|
||
SUGGESTED = "suggested" # Предложено ботом
|
||
CONTEXT = "context" # Из контекста диалога
|
||
IMPORT = "import" # Импорт из текста
|
||
ERROR = "error" # Из ошибок в заданиях
|
||
AI_TASK = "ai_task" # Из AI-задания
|
||
|
||
|
||
class AIProvider(str, enum.Enum):
|
||
"""Провайдеры AI моделей"""
|
||
openai = "openai"
|
||
google = "google"
|
||
|
||
|
||
class User(Base):
|
||
"""Модель пользователя"""
|
||
__tablename__ = "users"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False)
|
||
username: Mapped[Optional[str]] = mapped_column(String(255))
|
||
language_interface: Mapped[str] = mapped_column(String(2), default="ru") # ru/en/ja - UI language
|
||
learning_language: Mapped[str] = mapped_column(String(2), default="en") # en/ja - language being learned
|
||
translation_language: Mapped[Optional[str]] = mapped_column(String(2), default=None) # ru/en/ja - translation target (defaults to language_interface if None)
|
||
level: Mapped[LanguageLevel] = mapped_column(SQLEnum(LanguageLevel), default=LanguageLevel.A1)
|
||
levels_by_language: Mapped[Optional[dict]] = mapped_column(JSON, default=None) # {"en": "B1", "ja": "N4"}
|
||
timezone: Mapped[str] = mapped_column(String(50), default="UTC")
|
||
daily_task_time: Mapped[Optional[str]] = mapped_column(String(5)) # HH:MM
|
||
reminders_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||
last_reminder_sent: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||
streak_days: Mapped[int] = mapped_column(Integer, default=0)
|
||
tasks_count: Mapped[int] = mapped_column(Integer, default=5) # Количество заданий (5-15)
|
||
ai_model_id: Mapped[Optional[int]] = mapped_column(Integer, default=None) # ID выбранной AI модели (NULL = глобальная)
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
last_active: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||
|
||
|
||
class Vocabulary(Base):
|
||
"""Модель словарного запаса"""
|
||
__tablename__ = "vocabulary"
|
||
__table_args__ = (
|
||
UniqueConstraint("user_id", "source_lang", "word_original", name="uq_vocab_user_lang_word"),
|
||
)
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||
word_original: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
word_translation: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
source_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка слова (язык изучения)
|
||
translation_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка перевода (обычно язык интерфейса)
|
||
transcription: Mapped[Optional[str]] = mapped_column(String(255))
|
||
difficulty_level: Mapped[Optional[LanguageLevel]] = mapped_column(SQLEnum(LanguageLevel))
|
||
source: Mapped[WordSource] = mapped_column(SQLEnum(WordSource), default=WordSource.MANUAL)
|
||
times_reviewed: Mapped[int] = mapped_column(Integer, default=0)
|
||
correct_answers: Mapped[int] = mapped_column(Integer, default=0)
|
||
last_reviewed: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
notes: Mapped[Optional[str]] = mapped_column(String(500)) # Заметки пользователя
|
||
|
||
|
||
class WordTranslation(Base):
|
||
"""Модель перевода слова с контекстом"""
|
||
__tablename__ = "word_translations"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
vocabulary_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||
translation: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
context: Mapped[Optional[str]] = mapped_column(String(500)) # Пример предложения
|
||
context_translation: Mapped[Optional[str]] = mapped_column(String(500)) # Перевод примера
|
||
is_primary: Mapped[bool] = mapped_column(Boolean, default=False) # Основной перевод
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
||
|
||
class Task(Base):
|
||
"""Модель задания"""
|
||
__tablename__ = "tasks"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||
task_type: Mapped[str] = mapped_column(String(50), nullable=False) # translate, sentence, fill, etc.
|
||
content: Mapped[dict] = mapped_column(JSON, nullable=False) # Содержание задания
|
||
correct_answer: Mapped[Optional[str]] = mapped_column(String(500))
|
||
user_answer: Mapped[Optional[str]] = mapped_column(String(500))
|
||
is_correct: Mapped[Optional[bool]] = mapped_column(Boolean)
|
||
ai_feedback: Mapped[Optional[str]] = mapped_column(String(1000))
|
||
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||
|
||
|
||
class AIModel(Base):
|
||
"""Модель AI моделей для генерации"""
|
||
__tablename__ = "ai_models"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
provider: Mapped[AIProvider] = mapped_column(SQLEnum(AIProvider), nullable=False) # openai / google
|
||
model_name: Mapped[str] = mapped_column(String(100), nullable=False) # gpt-4o-mini, gemini-2.5-flash-lite
|
||
display_name: Mapped[str] = mapped_column(String(100), nullable=False) # Название для отображения
|
||
is_active: Mapped[bool] = mapped_column(Boolean, default=False) # Только одна модель активна
|
||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|