9a0b3ba92c
- Страница /settings: ввод и проверка API ключа DeepSeek - POST /api/llm/generate — генерация вопросов по названию теста - POST /api/llm/improve — улучшение формулировки вопроса + ответов (модал с галочками) - POST /api/llm/distractors — генерация дистракторов - POST /api/llm/review — рецензия теста + кнопка «Предложить вариант» - POST /api/llm/improve_all — улучшение всего теста с постатейным сравнением - Миграция 004: таблица settings (key-value) - Шапка приложения с навигацией на /settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
204 lines
7.6 KiB
Python
204 lines
7.6 KiB
Python
import json
|
|
|
|
from openai import AsyncOpenAI
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.setting import Setting
|
|
|
|
DEEPSEEK_BASE_URL = "https://api.deepseek.com"
|
|
DEEPSEEK_MODEL = "deepseek-chat"
|
|
|
|
|
|
async def _get_api_key(db: AsyncSession) -> str:
|
|
result = await db.execute(select(Setting).where(Setting.key == "deepseek_api_key"))
|
|
setting = result.scalar_one_or_none()
|
|
if not setting or not setting.value:
|
|
raise ValueError("API ключ DeepSeek не настроен. Перейдите в Настройки.")
|
|
return setting.value
|
|
|
|
|
|
def _client(api_key: str) -> AsyncOpenAI:
|
|
return AsyncOpenAI(api_key=api_key, base_url=DEEPSEEK_BASE_URL)
|
|
|
|
|
|
async def check_connection(db: AsyncSession) -> str:
|
|
api_key = await _get_api_key(db)
|
|
client = _client(api_key)
|
|
response = await client.chat.completions.create(
|
|
model=DEEPSEEK_MODEL,
|
|
messages=[{"role": "user", "content": "Ответь одним словом: работает"}],
|
|
max_tokens=10,
|
|
)
|
|
return response.choices[0].message.content.strip()
|
|
|
|
|
|
async def generate_questions(db: AsyncSession, topic: str, count: int = 7) -> list[dict]:
|
|
api_key = await _get_api_key(db)
|
|
client = _client(api_key)
|
|
|
|
prompt = f"""Сгенерируй {count} вопросов для теста по теме: "{topic}".
|
|
|
|
Верни ТОЛЬКО JSON без пояснений в следующем формате:
|
|
{{
|
|
"questions": [
|
|
{{
|
|
"text": "Текст вопроса",
|
|
"answers": [
|
|
{{"text": "Вариант 1", "is_correct": true}},
|
|
{{"text": "Вариант 2", "is_correct": false}},
|
|
{{"text": "Вариант 3", "is_correct": false}}
|
|
]
|
|
}}
|
|
]
|
|
}}
|
|
|
|
Требования:
|
|
- Минимум 3 варианта ответа на каждый вопрос
|
|
- Ровно один правильный ответ на каждый вопрос
|
|
- Вопросы должны проверять практические знания по теме
|
|
- Варианты ответов должны быть правдоподобными"""
|
|
|
|
response = await client.chat.completions.create(
|
|
model=DEEPSEEK_MODEL,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
response_format={"type": "json_object"},
|
|
max_tokens=3000,
|
|
)
|
|
data = json.loads(response.choices[0].message.content)
|
|
return data["questions"]
|
|
|
|
|
|
async def improve_question(
|
|
db: AsyncSession, question: str, answers: list[str]
|
|
) -> dict:
|
|
api_key = await _get_api_key(db)
|
|
client = _client(api_key)
|
|
|
|
answers_str = "\n".join(f"{i + 1}. {a}" for i, a in enumerate(answers))
|
|
prompt = f"""Улучши формулировки вопроса и вариантов ответов для теста. Сделай их более чёткими, однозначными и профессиональными.
|
|
|
|
Вопрос: {question}
|
|
|
|
Варианты ответов (верни в том же порядке и том же количестве):
|
|
{answers_str}
|
|
|
|
Верни ТОЛЬКО JSON без пояснений:
|
|
{{
|
|
"question": "улучшенный текст вопроса",
|
|
"answers": ["улучшенный вариант 1", "улучшенный вариант 2", ...]
|
|
}}"""
|
|
|
|
response = await client.chat.completions.create(
|
|
model=DEEPSEEK_MODEL,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
response_format={"type": "json_object"},
|
|
max_tokens=600,
|
|
)
|
|
return json.loads(response.choices[0].message.content)
|
|
|
|
|
|
async def generate_distractors(
|
|
db: AsyncSession, question: str, existing_answers: list[str]
|
|
) -> list[str]:
|
|
api_key = await _get_api_key(db)
|
|
client = _client(api_key)
|
|
|
|
existing_str = "\n".join(f"- {a}" for a in existing_answers)
|
|
prompt = f"""Для вопроса теста сгенерируй 3 правдоподобных неправильных варианта ответа (дистракторы).
|
|
|
|
Вопрос: {question}
|
|
|
|
Уже существующие варианты ответов:
|
|
{existing_str}
|
|
|
|
Верни ТОЛЬКО JSON без пояснений:
|
|
{{"distractors": ["Вариант 1", "Вариант 2", "Вариант 3"]}}
|
|
|
|
Требования:
|
|
- Дистракторы должны быть правдоподобными, но неправильными
|
|
- Не повторяй уже существующие варианты
|
|
- Дистракторы должны быть сопоставимы по длине с существующими вариантами"""
|
|
|
|
response = await client.chat.completions.create(
|
|
model=DEEPSEEK_MODEL,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
response_format={"type": "json_object"},
|
|
max_tokens=400,
|
|
)
|
|
data = json.loads(response.choices[0].message.content)
|
|
return data["distractors"]
|
|
|
|
|
|
async def review_test(db: AsyncSession, title: str, questions: list[dict]) -> str:
|
|
api_key = await _get_api_key(db)
|
|
client = _client(api_key)
|
|
|
|
questions_str = ""
|
|
for i, q in enumerate(questions, 1):
|
|
questions_str += f"\nВопрос {i}: {q.get('text', '')}\n"
|
|
for a in q.get("answers", []):
|
|
marker = "✓" if a.get("is_correct") else "✗"
|
|
questions_str += f" {marker} {a.get('text', '')}\n"
|
|
|
|
prompt = f"""Проанализируй тест и дай рекомендации по улучшению его качества.
|
|
|
|
Название теста: {title}
|
|
|
|
Вопросы:
|
|
{questions_str}
|
|
|
|
Оцени по следующим критериям:
|
|
1. Качество и чёткость формулировок вопросов
|
|
2. Качество вариантов ответов (правдоподобность дистракторов)
|
|
3. Охват темы и разнообразие вопросов
|
|
4. Конкретные рекомендации по улучшению
|
|
|
|
Отвечай на русском языке, структурированно."""
|
|
|
|
response = await client.chat.completions.create(
|
|
model=DEEPSEEK_MODEL,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
max_tokens=1500,
|
|
)
|
|
return response.choices[0].message.content.strip()
|
|
|
|
|
|
async def improve_all(
|
|
db: AsyncSession, title: str, questions: list[dict]
|
|
) -> list[dict]:
|
|
api_key = await _get_api_key(db)
|
|
client = _client(api_key)
|
|
|
|
questions_str = ""
|
|
for i, q in enumerate(questions, 1):
|
|
questions_str += f"\nВопрос {i}: {q.get('text', '')}\n"
|
|
for j, a in enumerate(q.get("answers", []), 1):
|
|
questions_str += f" {j}. {a.get('text', '') if isinstance(a, dict) else a}\n"
|
|
|
|
prompt = f"""Улучши формулировки всех вопросов и вариантов ответов в тесте. Сделай их более чёткими, однозначными и профессиональными.
|
|
|
|
Название теста: {title}
|
|
|
|
Вопросы:
|
|
{questions_str}
|
|
|
|
Верни ТОЛЬКО JSON. Для каждого вопроса — улучшенную формулировку и все варианты ответов в том же порядке и том же количестве:
|
|
{{
|
|
"questions": [
|
|
{{
|
|
"question": "улучшенный текст вопроса 1",
|
|
"answers": ["улучшенный вариант 1", "улучшенный вариант 2", "..."]
|
|
}}
|
|
]
|
|
}}"""
|
|
|
|
response = await client.chat.completions.create(
|
|
model=DEEPSEEK_MODEL,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
response_format={"type": "json_object"},
|
|
max_tokens=4000,
|
|
)
|
|
data = json.loads(response.choices[0].message.content)
|
|
return data["questions"]
|