575 lines
26 KiB
Python
575 lines
26 KiB
Python
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, 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:
|
||
logger.info(f"[GPT Request] translate_word: word='{word}', target_lang='{target_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) -> Dict:
|
||
"""
|
||
Сгенерировать предложение с пропуском для заданного слова
|
||
|
||
Args:
|
||
word: Слово, для которого нужно создать предложение
|
||
|
||
Returns:
|
||
Dict с предложением и правильным ответом
|
||
"""
|
||
prompt = f"""Создай предложение на английском языке, используя слово "{word}".
|
||
Замени это слово на пропуск "___".
|
||
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"sentence": "предложение с пропуском ___",
|
||
"answer": "{word}",
|
||
"translation": "перевод предложения на русский"
|
||
}}
|
||
|
||
Предложение должно быть простым и естественным. Контекст должен четко подсказывать правильное слово."""
|
||
|
||
try:
|
||
logger.info(f"[GPT Request] generate_fill_in_sentence: word='{word}'")
|
||
|
||
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) -> 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:
|
||
logger.info(f"[GPT Request] generate_thematic_words: theme='{theme}', level='{level}', count={count}")
|
||
|
||
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) -> 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:
|
||
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}")
|
||
|
||
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") -> 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:
|
||
logger.info(f"[GPT Request] start_conversation: scenario='{scenario}', level='{level}'")
|
||
|
||
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"
|
||
) -> 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:
|
||
logger.info(f"[GPT Request] continue_conversation: scenario='{scenario}', level='{level}', history_length={len(conversation_history)}")
|
||
|
||
# Формируем сообщения для 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_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()
|