Files
tg_bot_language/services/ai_service.py

587 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
import httpx
from openai import AsyncOpenAI
from config.settings import settings
from typing import Dict, List
logger = logging.getLogger(__name__)
class AIService:
"""Сервис для работы с OpenAI API через Cloudflare Gateway"""
def __init__(self):
self.api_key = settings.openai_api_key
# Проверяем, настроен ли Cloudflare AI Gateway
if settings.cloudflare_account_id:
# Используем Cloudflare AI Gateway с прямыми HTTP запросами
self.base_url = (
f"https://gateway.ai.cloudflare.com/v1/"
f"{settings.cloudflare_account_id}/"
f"{settings.cloudflare_gateway_id}/"
f"openai"
)
self.use_cloudflare = True
logger.info(f"AI Service initialized with Cloudflare Gateway: {self.base_url}")
else:
# Прямое подключение к OpenAI
self.base_url = "https://api.openai.com/v1"
self.use_cloudflare = False
logger.info("AI Service initialized with direct OpenAI connection")
# HTTP клиент для всех запросов
self.http_client = httpx.AsyncClient(
timeout=httpx.Timeout(60.0, connect=10.0),
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
)
async def _make_openai_request(self, messages: list, temperature: float = 0.3, model: str = "gpt-4o-mini") -> dict:
"""Выполнить запрос к OpenAI API (через Cloudflare или напрямую)"""
url = f"{self.base_url}/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"response_format": {"type": "json_object"}
}
response = await self.http_client.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
async def translate_word(self, word: str, source_lang: str = "en", translation_lang: str = "ru") -> Dict:
"""
Перевести слово и получить дополнительную информацию
Args:
word: Слово для перевода
source_lang: Язык исходного слова (ISO2)
translation_lang: Язык перевода (ISO2)
Returns:
Dict с переводом, транскрипцией и примерами
"""
prompt = f"""Переведи слово/фразу "{word}" с языка {source_lang} на {translation_lang}.
Верни ответ строго в формате JSON:
{{
"word": "исходное слово на {source_lang}",
"translation": "перевод на {translation_lang}",
"transcription": "транскрипция в IPA (если применимо)",
"examples": [
{{"{source_lang}": "пример на языке обучения", "{translation_lang}": "перевод примера"}}
],
"category": "категория слова (работа, еда, путешествия и т.д.)",
"difficulty": "уровень сложности (A1/A2/B1/B2/C1/C2)"
}}
Важно: верни только JSON, без дополнительного текста."""
try:
logger.info(f"[GPT Request] translate_word: word='{word}', source='{source_lang}', to='{translation_lang}'")
messages = [
{"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.3)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
logger.info(f"[GPT Response] translate_word: success, translation='{result.get('translation', 'N/A')}'")
return result
except Exception as e:
logger.error(f"[GPT Error] translate_word: {type(e).__name__}: {str(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:
logger.info(f"[GPT Request] check_answer: user_answer='{user_answer[:30]}...'")
messages = [
{"role": "system", "content": "Ты - преподаватель английского языка. Проверяй ответы справедливо, учитывая контекст."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.3)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
logger.info(f"[GPT Response] check_answer: is_correct={result.get('is_correct', False)}, score={result.get('score', 0)}")
return result
except Exception as e:
logger.error(f"[GPT Error] check_answer: {type(e).__name__}: {str(e)}")
return {
"is_correct": False,
"feedback": "Ошибка проверки ответа",
"score": 0
}
async def generate_fill_in_sentence(self, word: str, learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
"""
Сгенерировать предложение с пропуском для заданного слова
Args:
word: Слово (на языке обучения), для которого нужно создать предложение
learning_lang: Язык обучения (ISO2)
translation_lang: Язык перевода предложения (ISO2)
Returns:
Dict с предложением и правильным ответом
"""
prompt = f"""Создай предложение на языке {learning_lang}, используя слово "{word}".
Замени это слово на пропуск "___".
Верни ответ в формате JSON:
{{
"sentence": "предложение с пропуском ___",
"answer": "{word}",
"translation": "перевод предложения на {translation_lang}"
}}
Предложение должно быть простым и естественным. Контекст должен четко подсказывать правильное слово."""
try:
logger.info(f"[GPT Request] generate_fill_in_sentence: word='{word}', lang='{learning_lang}', to='{translation_lang}'")
messages = [
{"role": "system", "content": "Ты - преподаватель иностранных языков. Создавай простые и понятные упражнения."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.7)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
logger.info(f"[GPT Response] generate_fill_in_sentence: success")
return result
except Exception as e:
logger.error(f"[GPT Error] generate_fill_in_sentence: {type(e).__name__}: {str(e)}")
return {
"sentence": f"I like to ___ every day.",
"answer": word,
"translation": f"Мне нравится {word} каждый день."
}
async def generate_thematic_words(self, theme: str, level: str = "B1", count: int = 10, learning_lang: str = "en", translation_lang: str = "ru") -> List[Dict]:
"""
Сгенерировать подборку слов по теме
Args:
theme: Тема для подборки слов
level: Уровень сложности (A1-C2)
count: Количество слов
Returns:
Список словарей с информацией о словах
"""
prompt = f"""Создай подборку из {count} слов на языке {learning_lang} по теме "{theme}" для уровня {level}. Переводы дай на {translation_lang}.
Верни ответ в формате JSON:
{{
"theme": "{theme}",
"words": [
{{
"word": "слово на {learning_lang}",
"translation": "перевод на {translation_lang}",
"transcription": "транскрипция в IPA (если применимо)",
"example": "пример использования на {learning_lang}"
}}
]
}}
Слова должны быть:
- Полезными и часто используемыми
- Соответствовать уровню {level}
- Связаны с темой "{theme}"
- Разнообразными (существительные, глаголы, прилагательные)"""
try:
logger.info(f"[GPT Request] generate_thematic_words: theme='{theme}', level='{level}', count={count}, learn='{learning_lang}', to='{translation_lang}'")
messages = [
{"role": "system", "content": "Ты - преподаватель иностранных языков. Подбирай полезные и актуальные слова."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.7)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
words_count = len(result.get('words', []))
logger.info(f"[GPT Response] generate_thematic_words: success, generated {words_count} words")
return result.get('words', [])
except Exception as e:
logger.error(f"[GPT Error] generate_thematic_words: {type(e).__name__}: {str(e)}")
return []
async def extract_words_from_text(self, text: str, level: str = "B1", max_words: int = 15, learning_lang: str = "en", translation_lang: str = "ru") -> List[Dict]:
"""
Извлечь ключевые слова из текста для изучения
Args:
text: Текст на английском языке
level: Уровень пользователя (A1-C2)
max_words: Максимальное количество слов для извлечения
Returns:
Список словарей с информацией о словах
"""
prompt = f"""Проанализируй следующий текст на языке {learning_lang} и извлеки из него до {max_words} самых полезных слов для изучения на уровне {level}. Переводы дай на {translation_lang}.
Текст:
{text}
Верни ответ в формате JSON:
{{
"words": [
{{
"word": "слово на {learning_lang} (в базовой форме)",
"translation": "перевод на {translation_lang}",
"transcription": "транскрипция в IPA (если применимо)",
"context": "предложение из текста на {learning_lang}, где используется это слово"
}}
]
}}
Критерии отбора слов:
- Выбирай самые важные и полезные слова из текста
- Слова должны быть интересны для уровня {level}
- Не включай простейшие слова (a, the, is, и т.д.)
- Слова должны быть в базовой форме (инфинитив для глаголов, ед.число для существительных)
- Разнообразие: существительные, глаголы, прилагательные, устойчивые выражения"""
try:
text_preview = text[:100] + "..." if len(text) > 100 else text
logger.info(f"[GPT Request] extract_words_from_text: text_length={len(text)}, level='{level}', max_words={max_words}, learn='{learning_lang}', to='{translation_lang}'")
messages = [
{"role": "system", "content": "Ты - преподаватель иностранных языков. Помогаешь извлекать полезные слова для изучения из текстов."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.5)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
words_count = len(result.get('words', []))
logger.info(f"[GPT Response] extract_words_from_text: success, extracted {words_count} words")
return result.get('words', [])
except Exception as e:
logger.error(f"[GPT Error] extract_words_from_text: {type(e).__name__}: {str(e)}")
return []
async def start_conversation(self, scenario: str, level: str = "B1", learning_lang: str = "en", translation_lang: str = "ru") -> 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, "повседневный разговор")
extra_fields = ''
if learning_lang.lower() == 'ja':
extra_fields = ",\n \"furigana\": \"фуригана (кана над/после иероглифов для поля message)\""
prompt = f"""Ты - собеседник для практики языка {learning_lang} уровня {level}.
Начни диалог в сценарии: {scenario_desc} на {learning_lang}.
Верни ответ в формате JSON:
{{
"message": "твоя первая реплика на {learning_lang}",
"translation": "перевод на {translation_lang}",
"context": "краткое описание ситуации на {translation_lang}",
"suggestions": ["подсказка 1", "подсказка 2", "подсказка 3"]{extra_fields}
}}
Требования:
- Говори естественно, используй уровень {level}
- Создай интересную ситуацию
- Задай вопрос или начни разговор
- Подсказки должны помочь пользователю ответить"""
try:
logger.info(f"[GPT Request] start_conversation: scenario='{scenario}', level='{level}', learn='{learning_lang}', to='{translation_lang}'")
messages = [
{"role": "system", "content": "Ты - дружелюбный собеседник для практики иностранных языков. Веди естественный диалог."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.8)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
logger.info(f"[GPT Response] start_conversation: success, scenario='{scenario}'")
return result
except Exception as e:
logger.error(f"[GPT Error] start_conversation: {type(e).__name__}: {str(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",
learning_lang: str = "en",
translation_lang: str = "ru"
) -> 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 сообщений
])
extra_fields_resp = ''
if learning_lang.lower() == 'ja':
extra_fields_resp = ",\n \"furigana\": \"фуригана (кана над/после иероглифов для поля response)\""
prompt = f"""Ты ведешь диалог на языке {learning_lang} уровня {level} в сценарии "{scenario}".
История диалога:
{history_text}
User: {user_message}
Верни ответ в формате JSON:
{{
"response": "твой ответ на {learning_lang}",
"translation": "перевод твоего ответа на {translation_lang}",
"feedback": {{
"has_errors": true/false,
"corrections": "исправления ошибок пользователя (если есть)",
"comment": "краткий комментарий об ответе пользователя"
}},
"suggestions": ["подсказка 1 для следующего ответа", "подсказка 2"]{extra_fields_resp}
}}
Требования:
- Продолжай естественный диалог
- Если у пользователя есть грамматические или лексические ошибки, укажи их в corrections
- Будь дружелюбным и поддерживающим
- Используй лексику уровня {level}"""
try:
logger.info(f"[GPT Request] continue_conversation: scenario='{scenario}', level='{level}', history_length={len(conversation_history)}, learn='{learning_lang}', to='{translation_lang}'")
# Формируем сообщения для API
messages = [
{"role": "system", "content": f"Ты - дружелюбный собеседник для практики языка {learning_lang} уровня {level}. Веди естественный диалог и помогай исправлять ошибки."}
]
# Добавляем историю
for msg in conversation_history[-6:]:
messages.append(msg)
# Добавляем текущее сообщение пользователя
messages.append({"role": "user", "content": user_message})
# Добавляем инструкцию для форматирования ответа
messages.append({"role": "user", "content": prompt})
response_data = await self._make_openai_request(messages, temperature=0.8)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
has_errors = result.get('feedback', {}).get('has_errors', False)
logger.info(f"[GPT Response] continue_conversation: success, has_errors={has_errors}")
return result
except Exception as e:
logger.error(f"[GPT Error] continue_conversation: {type(e).__name__}: {str(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:
logger.info(f"[GPT Request] generate_level_test: generating 7 questions")
messages = [
{"role": "system", "content": "Ты - эксперт по тестированию уровня английского языка. Создавай объективные тесты."},
{"role": "user", "content": prompt}
]
response_data = await self._make_openai_request(messages, temperature=0.7)
import json
result = json.loads(response_data['choices'][0]['message']['content'])
questions_count = len(result.get('questions', []))
logger.info(f"[GPT Response] generate_level_test: success, generated {questions_count} questions")
return result.get('questions', [])
except Exception as e:
logger.error(f"[GPT Error] generate_level_test: {type(e).__name__}: {str(e)}, using fallback questions")
# 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()