Добавлены основные функции MVP: тематические подборки, импорт слов, диалоговая практика, напоминания и тест уровня
Новые команды: - /words [тема] - AI-генерация тематических подборок слов (10 слов по теме с учётом уровня) - /import - извлечение до 15 ключевых слов из текста (книги, статьи, песни) - /practice - диалоговая практика с AI в 6 сценариях (ресторан, магазин, путешествие, работа, врач, общение) - /reminder - настройка ежедневных напоминаний по расписанию - /level_test - тест из 7 вопросов для определения уровня английского (A1-C2) Основные изменения: - AI сервис: добавлены методы generate_thematic_words, extract_words_from_text, start_conversation, continue_conversation, generate_level_test - Диалоговая практика: исправление ошибок в реальном времени, подсказки, перевод реплик - Напоминания: APScheduler для ежедневной отправки напоминаний в выбранное время - Тест уровня: автоматическое определение уровня при регистрации, можно пропустить - База данных: добавлены поля reminders_enabled, last_reminder_sent - Vocabulary service: метод get_word_by_original для проверки дубликатов - Зависимости: apscheduler==3.10.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,361 @@ class AIService:
|
||||
"translation": f"Мне нравится {word} каждый день."
|
||||
}
|
||||
|
||||
async def generate_thematic_words(self, theme: str, level: str = "B1", count: int = 10) -> List[Dict]:
|
||||
"""
|
||||
Сгенерировать подборку слов по теме
|
||||
|
||||
Args:
|
||||
theme: Тема для подборки слов
|
||||
level: Уровень сложности (A1-C2)
|
||||
count: Количество слов
|
||||
|
||||
Returns:
|
||||
Список словарей с информацией о словах
|
||||
"""
|
||||
prompt = f"""Создай подборку из {count} английских слов по теме "{theme}" для уровня {level}.
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"theme": "{theme}",
|
||||
"words": [
|
||||
{{
|
||||
"word": "английское слово",
|
||||
"translation": "перевод на русский",
|
||||
"transcription": "транскрипция в IPA",
|
||||
"example": "пример использования на английском"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
Слова должны быть:
|
||||
- Полезными и часто используемыми
|
||||
- Соответствовать уровню {level}
|
||||
- Связаны с темой "{theme}"
|
||||
- Разнообразными (существительные, глаголы, прилагательные)"""
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": "Ты - преподаватель английского языка. Подбирай полезные и актуальные слова."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
import json
|
||||
result = json.loads(response.choices[0].message.content)
|
||||
return result.get('words', [])
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
async def extract_words_from_text(self, text: str, level: str = "B1", max_words: int = 15) -> List[Dict]:
|
||||
"""
|
||||
Извлечь ключевые слова из текста для изучения
|
||||
|
||||
Args:
|
||||
text: Текст на английском языке
|
||||
level: Уровень пользователя (A1-C2)
|
||||
max_words: Максимальное количество слов для извлечения
|
||||
|
||||
Returns:
|
||||
Список словарей с информацией о словах
|
||||
"""
|
||||
prompt = f"""Проанализируй следующий английский текст и извлеки из него до {max_words} самых полезных слов для изучения на уровне {level}.
|
||||
|
||||
Текст:
|
||||
{text}
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"words": [
|
||||
{{
|
||||
"word": "английское слово (в базовой форме)",
|
||||
"translation": "перевод на русский",
|
||||
"transcription": "транскрипция в IPA",
|
||||
"context": "предложение из текста, где используется это слово"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
Критерии отбора слов:
|
||||
- Выбирай самые важные и полезные слова из текста
|
||||
- Слова должны быть интересны для уровня {level}
|
||||
- Не включай простейшие слова (a, the, is, и т.д.)
|
||||
- Слова должны быть в базовой форме (инфинитив для глаголов, ед.число для существительных)
|
||||
- Разнообразие: существительные, глаголы, прилагательные, устойчивые выражения"""
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": "Ты - преподаватель английского языка. Помогаешь извлекать полезные слова для изучения из текстов."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.5,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
import json
|
||||
result = json.loads(response.choices[0].message.content)
|
||||
return result.get('words', [])
|
||||
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
async def start_conversation(self, scenario: str, level: str = "B1") -> Dict:
|
||||
"""
|
||||
Начать диалоговую практику с AI
|
||||
|
||||
Args:
|
||||
scenario: Сценарий диалога (restaurant, shopping, travel, etc.)
|
||||
level: Уровень пользователя (A1-C2)
|
||||
|
||||
Returns:
|
||||
Dict с начальной репликой и контекстом
|
||||
"""
|
||||
scenarios = {
|
||||
"restaurant": "ресторан - заказ еды",
|
||||
"shopping": "магазин - покупка одежды",
|
||||
"travel": "аэропорт/отель - путешествие",
|
||||
"work": "офис - рабочая встреча",
|
||||
"doctor": "клиника - визит к врачу",
|
||||
"casual": "повседневный разговор"
|
||||
}
|
||||
|
||||
scenario_desc = scenarios.get(scenario, "повседневный разговор")
|
||||
|
||||
prompt = f"""Ты - собеседник для практики английского языка уровня {level}.
|
||||
Начни диалог в сценарии: {scenario_desc}.
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"message": "твоя первая реплика на английском",
|
||||
"translation": "перевод на русский",
|
||||
"context": "краткое описание ситуации на русском",
|
||||
"suggestions": ["подсказка 1", "подсказка 2", "подсказка 3"]
|
||||
}}
|
||||
|
||||
Требования:
|
||||
- Говори естественно, используй уровень {level}
|
||||
- Создай интересную ситуацию
|
||||
- Задай вопрос или начни разговор
|
||||
- Подсказки должны помочь пользователю ответить"""
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": "Ты - дружелюбный собеседник для практики английского. Веди естественный диалог."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.8,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
import json
|
||||
result = json.loads(response.choices[0].message.content)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"message": "Hello! How are you today?",
|
||||
"translation": "Привет! Как дела сегодня?",
|
||||
"context": "Повседневный разговор",
|
||||
"suggestions": ["I'm fine, thank you!", "Good, and you?", "Not bad!"]
|
||||
}
|
||||
|
||||
async def continue_conversation(
|
||||
self,
|
||||
conversation_history: List[Dict],
|
||||
user_message: str,
|
||||
scenario: str,
|
||||
level: str = "B1"
|
||||
) -> Dict:
|
||||
"""
|
||||
Продолжить диалог и проверить ответ пользователя
|
||||
|
||||
Args:
|
||||
conversation_history: История диалога
|
||||
user_message: Сообщение пользователя
|
||||
scenario: Сценарий диалога
|
||||
level: Уровень пользователя
|
||||
|
||||
Returns:
|
||||
Dict с ответом AI, проверкой и подсказками
|
||||
"""
|
||||
# Формируем историю для контекста
|
||||
history_text = "\n".join([
|
||||
f"{'AI' if msg['role'] == 'assistant' else 'User'}: {msg['content']}"
|
||||
for msg in conversation_history[-6:] # Последние 6 сообщений
|
||||
])
|
||||
|
||||
prompt = f"""Ты ведешь диалог на английском языке уровня {level} в сценарии "{scenario}".
|
||||
|
||||
История диалога:
|
||||
{history_text}
|
||||
User: {user_message}
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"response": "твой ответ на английском",
|
||||
"translation": "перевод твоего ответа на русский",
|
||||
"feedback": {{
|
||||
"has_errors": true/false,
|
||||
"corrections": "исправления ошибок пользователя (если есть)",
|
||||
"comment": "краткий комментарий об ответе пользователя"
|
||||
}},
|
||||
"suggestions": ["подсказка 1 для следующего ответа", "подсказка 2"]
|
||||
}}
|
||||
|
||||
Требования:
|
||||
- Продолжай естественный диалог
|
||||
- Если у пользователя есть грамматические или лексические ошибки, укажи их в corrections
|
||||
- Будь дружелюбным и поддерживающим
|
||||
- Используй лексику уровня {level}"""
|
||||
|
||||
try:
|
||||
# Формируем сообщения для API
|
||||
messages = [
|
||||
{"role": "system", "content": f"Ты - дружелюбный собеседник для практики английского языка уровня {level}. Веди естественный диалог и помогай исправлять ошибки."}
|
||||
]
|
||||
|
||||
# Добавляем историю
|
||||
for msg in conversation_history[-6:]:
|
||||
messages.append(msg)
|
||||
|
||||
# Добавляем текущее сообщение пользователя
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
# Добавляем инструкцию для форматирования ответа
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
response = await self.client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=messages,
|
||||
temperature=0.8,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
import json
|
||||
result = json.loads(response.choices[0].message.content)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"response": "I see. Tell me more about that.",
|
||||
"translation": "Понятно. Расскажи мне больше об этом.",
|
||||
"feedback": {
|
||||
"has_errors": False,
|
||||
"corrections": "",
|
||||
"comment": "Good!"
|
||||
},
|
||||
"suggestions": ["Sure!", "Well...", "Actually..."]
|
||||
}
|
||||
|
||||
async def generate_level_test(self) -> List[Dict]:
|
||||
"""
|
||||
Сгенерировать тест для определения уровня английского
|
||||
|
||||
Returns:
|
||||
Список из 7 вопросов разной сложности
|
||||
"""
|
||||
prompt = """Создай тест из 7 вопросов для определения уровня английского языка (A1-C2).
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "текст вопроса на английском",
|
||||
"question_ru": "перевод вопроса на русский",
|
||||
"options": ["вариант A", "вариант B", "вариант C", "вариант D"],
|
||||
"correct": 0,
|
||||
"level": "A1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Требования:
|
||||
- Вопросы 1-2: уровень A1 (базовый)
|
||||
- Вопросы 3-4: уровень A2-B1 (элементарный-средний)
|
||||
- Вопросы 5-6: уровень B2-C1 (продвинутый)
|
||||
- Вопрос 7: уровень C2 (профессиональный)
|
||||
- Каждый вопрос с 4 вариантами ответа
|
||||
- correct - индекс правильного ответа (0-3)
|
||||
- Вопросы на грамматику, лексику и понимание"""
|
||||
|
||||
try:
|
||||
response = await self.client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": "Ты - эксперт по тестированию уровня английского языка. Создавай объективные тесты."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
import json
|
||||
result = json.loads(response.choices[0].message.content)
|
||||
return result.get('questions', [])
|
||||
|
||||
except Exception as e:
|
||||
# Fallback с базовыми вопросами
|
||||
return [
|
||||
{
|
||||
"question": "What is your name?",
|
||||
"question_ru": "Как тебя зовут?",
|
||||
"options": ["My name is", "I am name", "Name my is", "Is name my"],
|
||||
"correct": 0,
|
||||
"level": "A1"
|
||||
},
|
||||
{
|
||||
"question": "I ___ to school every day.",
|
||||
"question_ru": "Я ___ в школу каждый день.",
|
||||
"options": ["go", "goes", "going", "went"],
|
||||
"correct": 0,
|
||||
"level": "A1"
|
||||
},
|
||||
{
|
||||
"question": "She ___ been to Paris twice.",
|
||||
"question_ru": "Она ___ в Париже дважды.",
|
||||
"options": ["have", "has", "had", "having"],
|
||||
"correct": 1,
|
||||
"level": "A2"
|
||||
},
|
||||
{
|
||||
"question": "If I ___ rich, I would travel the world.",
|
||||
"question_ru": "Если бы я был богат, я бы путешествовал по миру.",
|
||||
"options": ["am", "was", "were", "be"],
|
||||
"correct": 2,
|
||||
"level": "B1"
|
||||
},
|
||||
{
|
||||
"question": "The project ___ by next Monday.",
|
||||
"question_ru": "Проект ___ к следующему понедельнику.",
|
||||
"options": ["will complete", "will be completed", "completes", "is completing"],
|
||||
"correct": 1,
|
||||
"level": "B2"
|
||||
},
|
||||
{
|
||||
"question": "Had I known about the meeting, I ___ attended.",
|
||||
"question_ru": "Если бы я знал о встрече, я бы посетил.",
|
||||
"options": ["would have", "will have", "would", "will"],
|
||||
"correct": 0,
|
||||
"level": "C1"
|
||||
},
|
||||
{
|
||||
"question": "The nuances of his argument were so ___ that few could grasp them.",
|
||||
"question_ru": "Нюансы его аргумента были настолько ___, что немногие могли их понять.",
|
||||
"options": ["subtle", "obvious", "simple", "clear"],
|
||||
"correct": 0,
|
||||
"level": "C2"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# Глобальный экземпляр сервиса
|
||||
ai_service = AIService()
|
||||
|
||||
141
services/reminder_service.py
Normal file
141
services/reminder_service.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from database.models import User
|
||||
from database.db import async_session_maker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReminderService:
|
||||
"""Сервис для управления напоминаниями"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
|
||||
def start(self):
|
||||
"""Запустить планировщик"""
|
||||
# Проверяем напоминания каждые 5 минут
|
||||
self.scheduler.add_job(
|
||||
self.check_and_send_reminders,
|
||||
trigger='interval',
|
||||
minutes=5,
|
||||
id='check_reminders',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
self.scheduler.start()
|
||||
logger.info("Планировщик напоминаний запущен")
|
||||
|
||||
def shutdown(self):
|
||||
"""Остановить планировщик"""
|
||||
self.scheduler.shutdown()
|
||||
logger.info("Планировщик напоминаний остановлен")
|
||||
|
||||
async def check_and_send_reminders(self):
|
||||
"""Проверить и отправить напоминания пользователям"""
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
# Получаем всех пользователей с включенными напоминаниями
|
||||
result = await session.execute(
|
||||
select(User).where(
|
||||
User.reminders_enabled == True,
|
||||
User.daily_task_time.isnot(None)
|
||||
)
|
||||
)
|
||||
users = list(result.scalars().all())
|
||||
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
for user in users:
|
||||
if await self._should_send_reminder(user, current_time):
|
||||
await self._send_reminder(user, session)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке напоминаний: {e}")
|
||||
|
||||
async def _should_send_reminder(self, user: User, current_time: datetime) -> bool:
|
||||
"""
|
||||
Проверить, нужно ли отправлять напоминание пользователю
|
||||
|
||||
Args:
|
||||
user: Пользователь
|
||||
current_time: Текущее время (UTC)
|
||||
|
||||
Returns:
|
||||
True если нужно отправить
|
||||
"""
|
||||
if not user.daily_task_time:
|
||||
return False
|
||||
|
||||
# Парсим время напоминания (формат HH:MM)
|
||||
try:
|
||||
hour, minute = map(int, user.daily_task_time.split(':'))
|
||||
except:
|
||||
return False
|
||||
|
||||
# Создаем datetime для времени напоминания сегодня (UTC)
|
||||
reminder_time = current_time.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
|
||||
# Проверяем, не отправляли ли мы уже напоминание сегодня
|
||||
if user.last_reminder_sent:
|
||||
last_sent_date = user.last_reminder_sent.date()
|
||||
current_date = current_time.date()
|
||||
|
||||
# Если уже отправляли сегодня, не отправляем снова
|
||||
if last_sent_date == current_date:
|
||||
return False
|
||||
|
||||
# Проверяем, наступило ли время напоминания (с погрешностью 5 минут)
|
||||
time_diff = abs((current_time - reminder_time).total_seconds())
|
||||
|
||||
return time_diff < 300 # 5 минут в секундах
|
||||
|
||||
async def _send_reminder(self, user: User, session: AsyncSession):
|
||||
"""
|
||||
Отправить напоминание пользователю
|
||||
|
||||
Args:
|
||||
user: Пользователь
|
||||
session: Сессия базы данных
|
||||
"""
|
||||
try:
|
||||
message_text = (
|
||||
"⏰ <b>Время для практики!</b>\n\n"
|
||||
"Не забудь потренироваться сегодня:\n"
|
||||
"• /task - выполни задания\n"
|
||||
"• /practice - попрактикуй диалог\n"
|
||||
"• /words - добавь новые слова\n\n"
|
||||
"💪 Регулярная практика - ключ к успеху!"
|
||||
)
|
||||
|
||||
await self.bot.send_message(
|
||||
chat_id=user.telegram_id,
|
||||
text=message_text
|
||||
)
|
||||
|
||||
# Обновляем время последнего напоминания
|
||||
user.last_reminder_sent = datetime.utcnow()
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"Напоминание отправлено пользователю {user.telegram_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке напоминания пользователю {user.telegram_id}: {e}")
|
||||
|
||||
|
||||
# Глобальный экземпляр сервиса (будет инициализирован в main.py)
|
||||
reminder_service: ReminderService = None
|
||||
|
||||
|
||||
def init_reminder_service(bot):
|
||||
"""Инициализировать сервис напоминаний"""
|
||||
global reminder_service
|
||||
reminder_service = ReminderService(bot)
|
||||
return reminder_service
|
||||
@@ -121,3 +121,23 @@ class VocabularyService:
|
||||
.where(Vocabulary.word_original.ilike(f"%{word}%"))
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_word_by_original(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 == word.lower())
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
Reference in New Issue
Block a user