Реализован MVP телеграм бота для изучения языков
Основные компоненты: - База данных (PostgreSQL) с моделями User, Vocabulary, Task - Интеграция с OpenAI API для перевода слов - Команды: /start, /add, /vocabulary, /help - Сервисы для работы с пользователями, словарем и AI Реализовано: ✅ Регистрация и приветствие пользователя ✅ Добавление слов в словарь с автоматическим переводом ✅ Просмотр личного словаря ✅ Архитектура проекта с разделением на слои 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
11
.env.example
Normal file
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Telegram Bot Token (получить у @BotFather)
|
||||||
|
BOT_TOKEN=your_telegram_bot_token_here
|
||||||
|
|
||||||
|
# OpenAI API Key
|
||||||
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/language_bot
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
DEBUG=True
|
||||||
142
README.md
142
README.md
@@ -1,3 +1,141 @@
|
|||||||
# tg_bot_language
|
# Telegram Бот для изучения языков
|
||||||
|
|
||||||
Бот для изучения иностранных языков
|
Интеллектуальный Telegram бот для изучения английского языка с использованием AI.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- 📚 Управление словарным запасом с автоматическим переводом через AI
|
||||||
|
- ✍️ Ежедневные задания для практики (в разработке)
|
||||||
|
- 💬 Диалоговая практика с ИИ (в разработке)
|
||||||
|
- 📊 Статистика прогресса (в разработке)
|
||||||
|
|
||||||
|
## Текущая версия (MVP)
|
||||||
|
|
||||||
|
**Реализовано:**
|
||||||
|
- ✅ Команда `/start` - приветствие и регистрация пользователя
|
||||||
|
- ✅ Команда `/add [слово]` - добавление слов в словарь с AI-переводом
|
||||||
|
- ✅ Команда `/vocabulary` - просмотр словаря
|
||||||
|
- ✅ Команда `/help` - справка
|
||||||
|
- ✅ База данных (PostgreSQL) для хранения пользователей и словарей
|
||||||
|
- ✅ Интеграция с OpenAI API для перевода слов
|
||||||
|
|
||||||
|
## Установка и запуск
|
||||||
|
|
||||||
|
### 1. Клонирование репозитория
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone http://103.137.249.134:3000/NANDI/tg_bot_language.git
|
||||||
|
cd tg_bot_language
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Настройка окружения
|
||||||
|
|
||||||
|
Скопируйте `.env.example` в `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Отредактируйте `.env` и заполните необходимые параметры:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BOT_TOKEN=your_telegram_bot_token_here
|
||||||
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/language_bot
|
||||||
|
DEBUG=True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Получение токенов:**
|
||||||
|
- Telegram Bot Token: создайте бота через [@BotFather](https://t.me/BotFather)
|
||||||
|
- OpenAI API Key: получите на [platform.openai.com](https://platform.openai.com/api-keys)
|
||||||
|
|
||||||
|
### 4. Настройка базы данных
|
||||||
|
|
||||||
|
Создайте PostgreSQL базу данных:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
createdb language_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
Или используйте Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --name language-bot-db -e POSTGRES_PASSWORD=password -e POSTGRES_DB=language_bot -p 5432:5432 -d postgres:15
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Запуск бота
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
bot_tg_language/
|
||||||
|
├── bot/
|
||||||
|
│ ├── handlers/ # Обработчики команд
|
||||||
|
│ │ ├── start.py # /start, /help
|
||||||
|
│ │ └── vocabulary.py # /add, /vocabulary
|
||||||
|
│ └── keyboards/ # Клавиатуры (пока не используется)
|
||||||
|
├── database/
|
||||||
|
│ ├── models.py # Модели БД (User, Vocabulary, Task)
|
||||||
|
│ └── db.py # Подключение к БД
|
||||||
|
├── services/
|
||||||
|
│ ├── ai_service.py # Сервис для работы с OpenAI
|
||||||
|
│ ├── user_service.py # Сервис пользователей
|
||||||
|
│ └── vocabulary_service.py # Сервис словаря
|
||||||
|
├── config/
|
||||||
|
│ └── settings.py # Настройки приложения
|
||||||
|
├── main.py # Точка входа
|
||||||
|
├── requirements.txt # Зависимости
|
||||||
|
├── .env.example # Пример конфигурации
|
||||||
|
└── TZ.md # Техническое задание
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Команды бота
|
||||||
|
|
||||||
|
- `/start` - Начать работу с ботом
|
||||||
|
- `/add [слово]` - Добавить слово в словарь
|
||||||
|
- `/vocabulary` - Посмотреть свой словарь
|
||||||
|
- `/help` - Показать справку
|
||||||
|
|
||||||
|
### Пример использования
|
||||||
|
|
||||||
|
1. Запустите бота: `/start`
|
||||||
|
2. Добавьте слово: `/add elephant`
|
||||||
|
3. Бот переведёт слово через AI и предложит добавить в словарь
|
||||||
|
4. Подтвердите добавление
|
||||||
|
5. Просмотрите словарь: `/vocabulary`
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
См. [TZ.md](TZ.md) для полного технического задания.
|
||||||
|
|
||||||
|
**Следующие этапы:**
|
||||||
|
- [ ] Ежедневные задания с разными типами упражнений
|
||||||
|
- [ ] Тематические подборки слов
|
||||||
|
- [ ] Импорт слов из текста
|
||||||
|
- [ ] Диалоговая практика с AI
|
||||||
|
- [ ] Статистика и прогресс
|
||||||
|
- [ ] Spaced repetition алгоритм
|
||||||
|
|
||||||
|
## Технологии
|
||||||
|
|
||||||
|
- **Python 3.11+**
|
||||||
|
- **aiogram 3.x** - Telegram Bot framework
|
||||||
|
- **SQLAlchemy 2.x** - ORM для работы с БД
|
||||||
|
- **PostgreSQL** - База данных
|
||||||
|
- **OpenAI API** - AI для перевода и проверки
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
0
bot/__init__.py
Normal file
0
bot/__init__.py
Normal file
0
bot/handlers/__init__.py
Normal file
0
bot/handlers/__init__.py
Normal file
66
bot/handlers/start.py
Normal file
66
bot/handlers/start.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import CommandStart, Command
|
||||||
|
from aiogram.types import Message
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
from database.db import async_session_maker
|
||||||
|
from services.user_service import UserService
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(CommandStart())
|
||||||
|
async def cmd_start(message: Message, state: FSMContext):
|
||||||
|
"""Обработчик команды /start"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=message.from_user.id,
|
||||||
|
username=message.from_user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
if user.created_at.timestamp() > (message.date.timestamp() - 60):
|
||||||
|
# Новый пользователь (создан менее минуты назад)
|
||||||
|
await message.answer(
|
||||||
|
f"👋 Привет, {message.from_user.first_name}!\n\n"
|
||||||
|
f"Я бот для изучения английского языка. Помогу тебе:\n"
|
||||||
|
f"📚 Пополнять словарный запас\n"
|
||||||
|
f"✍️ Выполнять ежедневные задания\n"
|
||||||
|
f"💬 Практиковать язык в диалоге\n\n"
|
||||||
|
f"<b>Основные команды:</b>\n"
|
||||||
|
f"/add [слово] - добавить слово в словарь\n"
|
||||||
|
f"/vocabulary - мой словарь\n"
|
||||||
|
f"/task - получить задание\n"
|
||||||
|
f"/stats - статистика\n"
|
||||||
|
f"/help - справка\n\n"
|
||||||
|
f"Давай начнём! Отправь мне слово, которое хочешь выучить, или используй команду /add"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Существующий пользователь
|
||||||
|
await message.answer(
|
||||||
|
f"С возвращением, {message.from_user.first_name}! 👋\n\n"
|
||||||
|
f"Готов продолжить обучение?\n"
|
||||||
|
f"/vocabulary - посмотреть словарь\n"
|
||||||
|
f"/task - получить задание\n"
|
||||||
|
f"/stats - статистика"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("help"))
|
||||||
|
async def cmd_help(message: Message):
|
||||||
|
"""Обработчик команды /help"""
|
||||||
|
await message.answer(
|
||||||
|
"<b>📖 Справка по командам:</b>\n\n"
|
||||||
|
"<b>Управление словарём:</b>\n"
|
||||||
|
"/add [слово] - добавить слово в словарь\n"
|
||||||
|
"/vocabulary - просмотр словаря\n"
|
||||||
|
"/import - импортировать слова из текста\n\n"
|
||||||
|
"<b>Обучение:</b>\n"
|
||||||
|
"/task - получить задание\n"
|
||||||
|
"/practice - практика с ИИ\n\n"
|
||||||
|
"<b>Статистика:</b>\n"
|
||||||
|
"/stats - твой прогресс\n\n"
|
||||||
|
"<b>Настройки:</b>\n"
|
||||||
|
"/settings - настройки бота\n\n"
|
||||||
|
"Ты также можешь просто отправить мне слово, и я предложу добавить его в словарь!"
|
||||||
|
)
|
||||||
191
bot/handlers/vocabulary.py
Normal file
191
bot/handlers/vocabulary.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
from database.db import async_session_maker
|
||||||
|
from database.models import WordSource
|
||||||
|
from services.user_service import UserService
|
||||||
|
from services.vocabulary_service import VocabularyService
|
||||||
|
from services.ai_service import ai_service
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
class AddWordStates(StatesGroup):
|
||||||
|
"""Состояния для добавления слова"""
|
||||||
|
waiting_for_confirmation = State()
|
||||||
|
waiting_for_word = State()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("add"))
|
||||||
|
async def cmd_add(message: Message, state: FSMContext):
|
||||||
|
"""Обработчик команды /add [слово]"""
|
||||||
|
# Получаем слово из команды
|
||||||
|
parts = message.text.split(maxsplit=1)
|
||||||
|
|
||||||
|
if len(parts) < 2:
|
||||||
|
await message.answer(
|
||||||
|
"Отправь слово, которое хочешь добавить:\n"
|
||||||
|
"Например: <code>/add elephant</code>\n\n"
|
||||||
|
"Или просто отправь слово без команды!"
|
||||||
|
)
|
||||||
|
await state.set_state(AddWordStates.waiting_for_word)
|
||||||
|
return
|
||||||
|
|
||||||
|
word = parts[1].strip()
|
||||||
|
await process_word_addition(message, state, word)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(AddWordStates.waiting_for_word)
|
||||||
|
async def process_word_input(message: Message, state: FSMContext):
|
||||||
|
"""Обработка ввода слова"""
|
||||||
|
word = message.text.strip()
|
||||||
|
await process_word_addition(message, state, word)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_word_addition(message: Message, state: FSMContext, word: str):
|
||||||
|
"""Обработка добавления слова"""
|
||||||
|
# Получаем пользователя
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
await message.answer("Сначала запусти бота командой /start")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверяем, есть ли уже такое слово
|
||||||
|
existing_word = await VocabularyService.find_word(session, user.id, word)
|
||||||
|
if existing_word:
|
||||||
|
await message.answer(
|
||||||
|
f"Слово '<b>{word}</b>' уже есть в твоём словаре!\n"
|
||||||
|
f"Перевод: {existing_word.word_translation}"
|
||||||
|
)
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Показываем индикатор загрузки
|
||||||
|
processing_msg = await message.answer("⏳ Ищу перевод и примеры...")
|
||||||
|
|
||||||
|
# Получаем перевод через AI
|
||||||
|
word_data = await ai_service.translate_word(word)
|
||||||
|
|
||||||
|
# Удаляем сообщение о загрузке
|
||||||
|
await processing_msg.delete()
|
||||||
|
|
||||||
|
# Формируем примеры
|
||||||
|
examples_text = ""
|
||||||
|
if word_data.get("examples"):
|
||||||
|
examples_text = "\n\n<b>Примеры:</b>\n"
|
||||||
|
for idx, example in enumerate(word_data["examples"][:2], 1):
|
||||||
|
examples_text += f"{idx}. {example['en']}\n <i>{example['ru']}</i>\n"
|
||||||
|
|
||||||
|
# Отправляем карточку слова
|
||||||
|
card_text = (
|
||||||
|
f"📝 <b>{word_data['word']}</b>\n"
|
||||||
|
f"🔊 [{word_data.get('transcription', '')}]\n\n"
|
||||||
|
f"🇷🇺 {word_data['translation']}\n"
|
||||||
|
f"📂 Категория: {word_data.get('category', 'общая')}\n"
|
||||||
|
f"📊 Уровень: {word_data.get('difficulty', 'A1')}"
|
||||||
|
f"{examples_text}\n\n"
|
||||||
|
f"Добавить это слово в словарь?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаём inline-кнопки
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="✅ Добавить", callback_data=f"add_word_confirm"),
|
||||||
|
InlineKeyboardButton(text="❌ Отмена", callback_data="add_word_cancel")
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Сохраняем данные слова в состоянии
|
||||||
|
await state.update_data(word_data=word_data, user_id=user.id)
|
||||||
|
await state.set_state(AddWordStates.waiting_for_confirmation)
|
||||||
|
|
||||||
|
await message.answer(card_text, reply_markup=keyboard)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "add_word_confirm", AddWordStates.waiting_for_confirmation)
|
||||||
|
async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Подтверждение добавления слова"""
|
||||||
|
data = await state.get_data()
|
||||||
|
word_data = data.get("word_data")
|
||||||
|
user_id = data.get("user_id")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Добавляем слово в базу
|
||||||
|
await VocabularyService.add_word(
|
||||||
|
session,
|
||||||
|
user_id=user_id,
|
||||||
|
word_original=word_data["word"],
|
||||||
|
word_translation=word_data["translation"],
|
||||||
|
transcription=word_data.get("transcription"),
|
||||||
|
examples={"examples": word_data.get("examples", [])},
|
||||||
|
category=word_data.get("category"),
|
||||||
|
difficulty_level=word_data.get("difficulty"),
|
||||||
|
source=WordSource.MANUAL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем общее количество слов
|
||||||
|
words_count = await VocabularyService.get_words_count(session, user_id)
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
f"✅ Слово '<b>{word_data['word']}</b>' добавлено в твой словарь!\n\n"
|
||||||
|
f"Всего слов в словаре: {words_count}\n\n"
|
||||||
|
f"Продолжай добавлять новые слова или используй /task для практики!"
|
||||||
|
)
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "add_word_cancel")
|
||||||
|
async def cancel_add_word(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Отмена добавления слова"""
|
||||||
|
await callback.message.edit_text("Отменено. Можешь добавить другое слово командой /add")
|
||||||
|
await state.clear()
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("vocabulary"))
|
||||||
|
async def cmd_vocabulary(message: Message):
|
||||||
|
"""Обработчик команды /vocabulary"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
await message.answer("Сначала запусти бота командой /start")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Получаем слова пользователя
|
||||||
|
words = await VocabularyService.get_user_words(session, user.id, limit=10)
|
||||||
|
total_count = await VocabularyService.get_words_count(session, user.id)
|
||||||
|
|
||||||
|
if not words:
|
||||||
|
await message.answer(
|
||||||
|
"📚 Твой словарь пока пуст!\n\n"
|
||||||
|
"Добавь первое слово командой /add или просто отправь мне слово."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Формируем список слов
|
||||||
|
words_list = "<b>📚 Твой словарь:</b>\n\n"
|
||||||
|
for idx, word in enumerate(words, 1):
|
||||||
|
progress = ""
|
||||||
|
if word.times_reviewed > 0:
|
||||||
|
accuracy = int((word.correct_answers / word.times_reviewed) * 100)
|
||||||
|
progress = f" ({accuracy}% точность)"
|
||||||
|
|
||||||
|
words_list += (
|
||||||
|
f"{idx}. <b>{word.word_original}</b> — {word.word_translation}\n"
|
||||||
|
f" 🔊 [{word.transcription or ''}]{progress}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if total_count > 10:
|
||||||
|
words_list += f"\n<i>Показаны последние 10 из {total_count} слов</i>"
|
||||||
|
else:
|
||||||
|
words_list += f"\n<i>Всего слов: {total_count}</i>"
|
||||||
|
|
||||||
|
await message.answer(words_list)
|
||||||
0
bot/keyboards/__init__.py
Normal file
0
bot/keyboards/__init__.py
Normal file
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
26
config/settings.py
Normal file
26
config/settings.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Настройки приложения"""
|
||||||
|
|
||||||
|
# Telegram
|
||||||
|
bot_token: str
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
openai_api_key: str
|
||||||
|
|
||||||
|
# Database
|
||||||
|
database_url: str
|
||||||
|
|
||||||
|
# App settings
|
||||||
|
debug: bool = False
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file='.env',
|
||||||
|
env_file_encoding='utf-8',
|
||||||
|
case_sensitive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
0
database/__init__.py
Normal file
0
database/__init__.py
Normal file
29
database/db.py
Normal file
29
database/db.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||||
|
from config.settings import settings
|
||||||
|
from database.models import Base
|
||||||
|
|
||||||
|
# Создание движка базы данных
|
||||||
|
engine = create_async_engine(
|
||||||
|
settings.database_url,
|
||||||
|
echo=settings.debug,
|
||||||
|
future=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Фабрика сессий
|
||||||
|
async_session_maker = async_sessionmaker(
|
||||||
|
engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
"""Инициализация базы данных (создание таблиц)"""
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_session() -> AsyncSession:
|
||||||
|
"""Получение сессии базы данных"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
yield session
|
||||||
83
database/models.py
Normal file
83
database/models.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import String, BigInteger, DateTime, Integer, Boolean, JSON, Enum as SQLEnum
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
"""Базовая модель"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LanguageLevel(str, enum.Enum):
|
||||||
|
"""Уровни владения языком"""
|
||||||
|
A1 = "A1"
|
||||||
|
A2 = "A2"
|
||||||
|
B1 = "B1"
|
||||||
|
B2 = "B2"
|
||||||
|
C1 = "C1"
|
||||||
|
C2 = "C2"
|
||||||
|
|
||||||
|
|
||||||
|
class WordSource(str, enum.Enum):
|
||||||
|
"""Источник добавления слова"""
|
||||||
|
MANUAL = "manual" # Ручное добавление
|
||||||
|
SUGGESTED = "suggested" # Предложено ботом
|
||||||
|
CONTEXT = "context" # Из контекста диалога
|
||||||
|
IMPORT = "import" # Импорт из текста
|
||||||
|
ERROR = "error" # Из ошибок в заданиях
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
learning_language: Mapped[str] = mapped_column(String(2), default="en") # en
|
||||||
|
level: Mapped[LanguageLevel] = mapped_column(SQLEnum(LanguageLevel), default=LanguageLevel.A1)
|
||||||
|
timezone: Mapped[str] = mapped_column(String(50), default="UTC")
|
||||||
|
daily_task_time: Mapped[Optional[str]] = mapped_column(String(5)) # HH:MM
|
||||||
|
streak_days: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
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"
|
||||||
|
|
||||||
|
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)
|
||||||
|
transcription: Mapped[Optional[str]] = mapped_column(String(255))
|
||||||
|
examples: Mapped[Optional[dict]] = mapped_column(JSON) # JSON массив примеров
|
||||||
|
category: Mapped[Optional[str]] = mapped_column(String(100))
|
||||||
|
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 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)
|
||||||
41
main.py
Normal file
41
main.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
|
||||||
|
from config.settings import settings
|
||||||
|
from bot.handlers import start, vocabulary
|
||||||
|
from database.db import init_db
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Главная функция запуска бота"""
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO if settings.debug else logging.WARNING,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Инициализация бота и диспетчера
|
||||||
|
bot = Bot(
|
||||||
|
token=settings.bot_token,
|
||||||
|
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||||
|
)
|
||||||
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
# Регистрация роутеров
|
||||||
|
dp.include_router(start.router)
|
||||||
|
dp.include_router(vocabulary.router)
|
||||||
|
|
||||||
|
# Инициализация базы данных
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
# Запуск бота
|
||||||
|
logging.info("Бот запущен")
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
aiogram==3.13.1
|
||||||
|
sqlalchemy==2.0.36
|
||||||
|
asyncpg==0.30.0
|
||||||
|
alembic==1.14.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
openai==1.57.3
|
||||||
|
pydantic==2.10.3
|
||||||
|
pydantic-settings==2.6.1
|
||||||
0
services/__init__.py
Normal file
0
services/__init__.py
Normal file
117
services/ai_service.py
Normal file
117
services/ai_service.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
from openai import AsyncOpenAI
|
||||||
|
from config.settings import settings
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
class AIService:
|
||||||
|
"""Сервис для работы с OpenAI API"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.client = AsyncOpenAI(api_key=settings.openai_api_key)
|
||||||
|
|
||||||
|
async def translate_word(self, word: str, target_lang: str = "ru") -> Dict:
|
||||||
|
"""
|
||||||
|
Перевести слово и получить дополнительную информацию
|
||||||
|
|
||||||
|
Args:
|
||||||
|
word: Слово для перевода
|
||||||
|
target_lang: Язык перевода (по умолчанию русский)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict с переводом, транскрипцией и примерами
|
||||||
|
"""
|
||||||
|
prompt = f"""Переведи английское слово/фразу "{word}" на русский язык.
|
||||||
|
|
||||||
|
Верни ответ строго в формате JSON:
|
||||||
|
{{
|
||||||
|
"word": "{word}",
|
||||||
|
"translation": "перевод",
|
||||||
|
"transcription": "транскрипция в IPA",
|
||||||
|
"examples": [
|
||||||
|
{{"en": "пример на английском", "ru": "перевод примера"}},
|
||||||
|
{{"en": "ещё один пример", "ru": "перевод примера"}}
|
||||||
|
],
|
||||||
|
"category": "категория слова (работа, еда, путешествия и т.д.)",
|
||||||
|
"difficulty": "уровень сложности (A1/A2/B1/B2/C1/C2)"
|
||||||
|
}}
|
||||||
|
|
||||||
|
Важно: верни только JSON, без дополнительного текста."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.client.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "Ты - помощник для изучения английского языка. Отвечай только в формате JSON."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
temperature=0.3,
|
||||||
|
response_format={"type": "json_object"}
|
||||||
|
)
|
||||||
|
|
||||||
|
import json
|
||||||
|
result = json.loads(response.choices[0].message.content)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback в случае ошибки
|
||||||
|
return {
|
||||||
|
"word": word,
|
||||||
|
"translation": "Ошибка перевода",
|
||||||
|
"transcription": "",
|
||||||
|
"examples": [],
|
||||||
|
"category": "unknown",
|
||||||
|
"difficulty": "A1"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def check_answer(self, question: str, correct_answer: str, user_answer: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Проверить ответ пользователя с помощью ИИ
|
||||||
|
|
||||||
|
Args:
|
||||||
|
question: Вопрос задания
|
||||||
|
correct_answer: Правильный ответ
|
||||||
|
user_answer: Ответ пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict с результатом проверки и обратной связью
|
||||||
|
"""
|
||||||
|
prompt = f"""Проверь ответ пользователя на задание по английскому языку.
|
||||||
|
|
||||||
|
Задание: {question}
|
||||||
|
Правильный ответ: {correct_answer}
|
||||||
|
Ответ пользователя: {user_answer}
|
||||||
|
|
||||||
|
Верни ответ в формате JSON:
|
||||||
|
{{
|
||||||
|
"is_correct": true/false,
|
||||||
|
"feedback": "краткое объяснение (если ответ неверный, объясни ошибку и дай правильный вариант)",
|
||||||
|
"score": 0-100
|
||||||
|
}}
|
||||||
|
|
||||||
|
Учитывай возможные вариации ответа. Если смысл передан правильно, даже с небольшими грамматическими неточностями, засчитывай ответ."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.client.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "Ты - преподаватель английского языка. Проверяй ответы справедливо, учитывая контекст."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
temperature=0.3,
|
||||||
|
response_format={"type": "json_object"}
|
||||||
|
)
|
||||||
|
|
||||||
|
import json
|
||||||
|
result = json.loads(response.choices[0].message.content)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"is_correct": False,
|
||||||
|
"feedback": "Ошибка проверки ответа",
|
||||||
|
"score": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр сервиса
|
||||||
|
ai_service = AIService()
|
||||||
59
services/user_service.py
Normal file
59
services/user_service.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from database.models import User, LanguageLevel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class UserService:
|
||||||
|
"""Сервис для работы с пользователями"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_or_create_user(session: AsyncSession, telegram_id: int, username: Optional[str] = None) -> User:
|
||||||
|
"""
|
||||||
|
Получить пользователя или создать нового
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Сессия базы данных
|
||||||
|
telegram_id: Telegram ID пользователя
|
||||||
|
username: Username пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Объект пользователя
|
||||||
|
"""
|
||||||
|
# Попытка найти существующего пользователя
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.telegram_id == telegram_id)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Создание нового пользователя
|
||||||
|
new_user = User(
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
username=username,
|
||||||
|
level=LanguageLevel.A1
|
||||||
|
)
|
||||||
|
session.add(new_user)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(new_user)
|
||||||
|
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_by_telegram_id(session: AsyncSession, telegram_id: int) -> Optional[User]:
|
||||||
|
"""
|
||||||
|
Получить пользователя по Telegram ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Сессия базы данных
|
||||||
|
telegram_id: Telegram ID пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Объект пользователя или None
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.telegram_id == telegram_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
123
services/vocabulary_service.py
Normal file
123
services/vocabulary_service.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from database.models import Vocabulary, WordSource, LanguageLevel
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class VocabularyService:
|
||||||
|
"""Сервис для работы со словарным запасом"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def add_word(
|
||||||
|
session: AsyncSession,
|
||||||
|
user_id: int,
|
||||||
|
word_original: str,
|
||||||
|
word_translation: str,
|
||||||
|
transcription: Optional[str] = None,
|
||||||
|
examples: Optional[dict] = None,
|
||||||
|
category: Optional[str] = None,
|
||||||
|
difficulty_level: Optional[str] = None,
|
||||||
|
source: WordSource = WordSource.MANUAL,
|
||||||
|
notes: Optional[str] = None
|
||||||
|
) -> Vocabulary:
|
||||||
|
"""
|
||||||
|
Добавить слово в словарь пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Сессия базы данных
|
||||||
|
user_id: ID пользователя
|
||||||
|
word_original: Оригинальное слово
|
||||||
|
word_translation: Перевод
|
||||||
|
transcription: Транскрипция
|
||||||
|
examples: Примеры использования
|
||||||
|
category: Категория слова
|
||||||
|
difficulty_level: Уровень сложности
|
||||||
|
source: Источник добавления
|
||||||
|
notes: Заметки пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Созданный объект слова
|
||||||
|
"""
|
||||||
|
# Преобразование difficulty_level в enum
|
||||||
|
difficulty_enum = None
|
||||||
|
if difficulty_level:
|
||||||
|
try:
|
||||||
|
difficulty_enum = LanguageLevel[difficulty_level]
|
||||||
|
except KeyError:
|
||||||
|
difficulty_enum = None
|
||||||
|
|
||||||
|
new_word = Vocabulary(
|
||||||
|
user_id=user_id,
|
||||||
|
word_original=word_original,
|
||||||
|
word_translation=word_translation,
|
||||||
|
transcription=transcription,
|
||||||
|
examples=examples,
|
||||||
|
category=category,
|
||||||
|
difficulty_level=difficulty_enum,
|
||||||
|
source=source,
|
||||||
|
notes=notes
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(new_word)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(new_word)
|
||||||
|
|
||||||
|
return new_word
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_words(session: AsyncSession, user_id: int, limit: int = 50) -> List[Vocabulary]:
|
||||||
|
"""
|
||||||
|
Получить все слова пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Сессия базы данных
|
||||||
|
user_id: ID пользователя
|
||||||
|
limit: Максимальное количество слов
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список слов пользователя
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Vocabulary)
|
||||||
|
.where(Vocabulary.user_id == user_id)
|
||||||
|
.order_by(Vocabulary.created_at.desc())
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_words_count(session: AsyncSession, user_id: int) -> int:
|
||||||
|
"""
|
||||||
|
Получить количество слов в словаре пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Сессия базы данных
|
||||||
|
user_id: ID пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Количество слов
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Vocabulary).where(Vocabulary.user_id == user_id)
|
||||||
|
)
|
||||||
|
return len(list(result.scalars().all()))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def find_word(session: AsyncSession, user_id: int, word: str) -> Optional[Vocabulary]:
|
||||||
|
"""
|
||||||
|
Найти слово в словаре пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Сессия базы данных
|
||||||
|
user_id: ID пользователя
|
||||||
|
word: Слово для поиска
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Объект слова или None
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Vocabulary)
|
||||||
|
.where(Vocabulary.user_id == user_id)
|
||||||
|
.where(Vocabulary.word_original.ilike(f"%{word}%"))
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
Reference in New Issue
Block a user