You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
90 lines
4.3 KiB
90 lines
4.3 KiB
"""Генерация черновика теста из извлечённого текста (порт части `documentGenService.js`).""" |
|
from __future__ import annotations |
|
|
|
from .draft_validator import ( |
|
normalize_draft_to_shape, |
|
parse_json_from_llm_text, |
|
shuffle_options_in_questions, |
|
validate_and_normalize_draft, |
|
) |
|
from .llm_client import LlmError, chat_completion_text_content, get_llm_config |
|
|
|
|
|
MAX_EXTRACT_CHARS = 14000 |
|
|
|
|
|
def generation_for_import_document(extracted_text: str, user_hint: str = '', shape: list[dict] | None = None) -> dict: |
|
text = (extracted_text or '').strip() |
|
if not text: |
|
return { |
|
'available': False, |
|
'message': 'Нет извлечённого текста — нечего передавать в модель.', |
|
} |
|
cfg = get_llm_config() |
|
if cfg is None: |
|
return { |
|
'available': False, |
|
'message': ( |
|
'Автогенерация выключена: задайте DEEPSEEK_API_KEY или OPENAI_API_KEY ' |
|
'в .env. Превью текста ниже — можно вставить вручную.' |
|
), |
|
'textPreview': text[:4000], |
|
} |
|
if len(text) > MAX_EXTRACT_CHARS: |
|
slice_ = text[:MAX_EXTRACT_CHARS] + '\n\n[…фрагмент обрезан для API]' |
|
else: |
|
slice_ = text |
|
try: |
|
system = ( |
|
'Ты помощник для составления тестов. Отвечай ТОЛЬКО одним JSON-объектом ' |
|
'без пояснений. Схема: {"title": string, "description"?: string, ' |
|
'"questions": array}. Каждый вопрос: {"text", "hasMultipleAnswers": boolean, ' |
|
'"options": [{"text", "isCorrect": boolean}, ...]}. Минимум 2 варианта. ' |
|
'Для одиночного выбора ровно один isCorrect: true. ' |
|
'Текст и формулировки — на русском, по содержанию входного материала.' |
|
) |
|
hint_block = f'\n\nДополнительные инструкции от автора теста:\n{user_hint.strip()}' if user_hint and user_hint.strip() else '' |
|
shape_block = '' |
|
if shape: |
|
rows = [] |
|
for i, sh in enumerate(shape): |
|
if sh.get('hasMultipleAnswers'): |
|
rows.append( |
|
f'- Вопрос {i + 1}: ровно {sh["optionsCount"]} вариантов, ' |
|
f'правильных от {sh.get("minCorrect", 1)} до {sh.get("maxCorrect", sh["optionsCount"])}.' |
|
) |
|
else: |
|
rows.append(f'- Вопрос {i + 1}: ровно {sh["optionsCount"]} вариантов, ровно 1 правильный.') |
|
shape_block = '\n\nСтрого соблюди шаблон:\n' + '\n'.join(rows) |
|
user = ( |
|
'Составь тест с вопросами с одним или несколькими правильными ответами ' |
|
'на основе текста:\n\n' + slice_ + hint_block + shape_block |
|
) |
|
raw = chat_completion_text_content(cfg, system, user, 0.25) |
|
parsed = parse_json_from_llm_text(raw) |
|
draft = validate_and_normalize_draft(parsed) |
|
if shape: |
|
draft = normalize_draft_to_shape(draft, shape) |
|
shuffle_options_in_questions(draft['questions']) |
|
return { |
|
'available': True, |
|
'message': ( |
|
f'Сгенерировано: «{draft["title"]}», вопросов: ' |
|
f'{len(draft["questions"])}. Нажмите «Применить сгенерированный черновик».' |
|
), |
|
'draft': draft, |
|
} |
|
except LlmError as e: |
|
return { |
|
'available': False, |
|
'message': f'Генерация не удалась: {e}', |
|
'errorCode': e.code, |
|
'textPreview': text[:4000], |
|
} |
|
except Exception as e: |
|
return { |
|
'available': False, |
|
'message': f'Генерация не удалась: {e}', |
|
'errorCode': 'unknown', |
|
'textPreview': text[:4000], |
|
}
|
|
|