bugfix
This commit is contained in:
@@ -33,7 +33,30 @@ def parse_and_validate_shape(s: Any) -> list[dict]:
|
||||
raise HttpError(400, f'shape[{i}]: optionsCount от 2 до 12.')
|
||||
if n < 2 or n > 12:
|
||||
raise HttpError(400, f'shape[{i}]: optionsCount от 2 до 12.')
|
||||
out.append({'optionsCount': n, 'hasMultipleAnswers': bool(row.get('hasMultipleAnswers'))})
|
||||
has_multi = bool(row.get('hasMultipleAnswers'))
|
||||
raw_min = row.get('minCorrect', 1)
|
||||
raw_max = row.get('maxCorrect', n if has_multi else 1)
|
||||
try:
|
||||
min_c = int(float(raw_min))
|
||||
max_c = int(float(raw_max))
|
||||
except (TypeError, ValueError):
|
||||
raise HttpError(400, f'shape[{i}]: minCorrect/maxCorrect должны быть числами.')
|
||||
if not has_multi:
|
||||
min_c = 1
|
||||
max_c = 1
|
||||
if min_c < 1 or max_c < 1 or min_c > max_c or max_c > n:
|
||||
raise HttpError(
|
||||
400,
|
||||
f'shape[{i}]: корректный диапазон правильных ответов — от 1 до {n}, min<=max.',
|
||||
)
|
||||
out.append(
|
||||
{
|
||||
'optionsCount': n,
|
||||
'hasMultipleAnswers': has_multi,
|
||||
'minCorrect': min_c,
|
||||
'maxCorrect': max_c,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@@ -51,7 +74,10 @@ def generate_full_test_by_shape(test_title: str, test_description: str, shape: l
|
||||
lines = []
|
||||
for i, sh in enumerate(shape):
|
||||
if sh['hasMultipleAnswers']:
|
||||
tail = 'несколько вариантов помечены как верные (hasMultipleAnswers: true).'
|
||||
tail = (
|
||||
'несколько вариантов помечены как верные (hasMultipleAnswers: true), '
|
||||
f'число правильных от {sh["minCorrect"]} до {sh["maxCorrect"]}.'
|
||||
)
|
||||
else:
|
||||
tail = 'ровно один верный вариант (hasMultipleAnswers: false).'
|
||||
lines.append(f'Вопрос {i + 1}: ровно {sh["optionsCount"]} вариантов ответа; {tail}')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .draft_validator import (
|
||||
normalize_draft_to_shape,
|
||||
parse_json_from_llm_text,
|
||||
validate_and_normalize_draft,
|
||||
)
|
||||
@@ -11,7 +12,7 @@ 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 = '') -> dict:
|
||||
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 {
|
||||
@@ -42,13 +43,27 @@ def generation_for_import_document(extracted_text: str, user_hint: str = '') ->
|
||||
'Текст и формулировки — на русском, по содержанию входного материала.'
|
||||
)
|
||||
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
|
||||
'на основе текста:\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)
|
||||
return {
|
||||
'available': True,
|
||||
'message': (
|
||||
|
||||
@@ -103,3 +103,65 @@ def assert_draft_matches_shape(o: dict, shape: list[dict]) -> None:
|
||||
f'Вопрос {i + 1}: hasMultipleAnswers должен быть {sh["hasMultipleAnswers"]}.',
|
||||
code='llm_shape',
|
||||
)
|
||||
min_c = int(sh.get('minCorrect', 1))
|
||||
max_c = int(sh.get('maxCorrect', sh['optionsCount']))
|
||||
correct_n = sum(1 for op in opts if bool(op.get('isCorrect')))
|
||||
if correct_n < min_c or correct_n > max_c:
|
||||
raise LlmError(
|
||||
f'Вопрос {i + 1}: правильных ответов должно быть от {min_c} до {max_c}, в ответе: {correct_n}.',
|
||||
code='llm_shape',
|
||||
)
|
||||
|
||||
|
||||
def normalize_draft_to_shape(draft: dict, shape: list[dict]) -> dict:
|
||||
"""Приводит draft к shape: число вопросов/вариантов/мульти и диапазон correct."""
|
||||
qs = list((draft or {}).get('questions') or [])
|
||||
out_qs = []
|
||||
|
||||
def _mk_option(i: int) -> dict:
|
||||
return {'text': f'Вариант {i + 1}', 'isCorrect': False}
|
||||
|
||||
for i, sh in enumerate(shape):
|
||||
src = qs[i] if i < len(qs) and isinstance(qs[i], dict) else {}
|
||||
text = str(src.get('text') or '').strip() or f'Вопрос {i + 1}'
|
||||
has_multi = bool(sh.get('hasMultipleAnswers'))
|
||||
min_c = int(sh.get('minCorrect', 1))
|
||||
max_c = int(sh.get('maxCorrect', sh['optionsCount']))
|
||||
if not has_multi:
|
||||
min_c = 1
|
||||
max_c = 1
|
||||
|
||||
raw_opts = src.get('options') if isinstance(src.get('options'), list) else []
|
||||
opts = []
|
||||
for j in range(sh['optionsCount']):
|
||||
if j < len(raw_opts) and isinstance(raw_opts[j], dict):
|
||||
t = str(raw_opts[j].get('text') or '').strip() or f'Вариант {j + 1}'
|
||||
opts.append({'text': t, 'isCorrect': bool(raw_opts[j].get('isCorrect'))})
|
||||
else:
|
||||
opts.append(_mk_option(j))
|
||||
|
||||
true_idx = [idx for idx, op in enumerate(opts) if op['isCorrect']]
|
||||
if not has_multi:
|
||||
keep = true_idx[0] if true_idx else 0
|
||||
for idx, op in enumerate(opts):
|
||||
op['isCorrect'] = (idx == keep)
|
||||
else:
|
||||
if len(true_idx) < min_c:
|
||||
for idx in range(len(opts)):
|
||||
if idx not in true_idx:
|
||||
opts[idx]['isCorrect'] = True
|
||||
true_idx.append(idx)
|
||||
if len(true_idx) >= min_c:
|
||||
break
|
||||
if len(true_idx) > max_c:
|
||||
keep = set(true_idx[:max_c])
|
||||
for idx, op in enumerate(opts):
|
||||
op['isCorrect'] = idx in keep
|
||||
|
||||
out_qs.append({'text': text, 'hasMultipleAnswers': has_multi, 'options': opts})
|
||||
|
||||
return {
|
||||
'title': str((draft or {}).get('title') or '').strip() or 'Тест',
|
||||
'description': (draft or {}).get('description'),
|
||||
'questions': out_qs,
|
||||
}
|
||||
|
||||
@@ -364,9 +364,18 @@ def count_missing_hints(session_or_eng, test_id: str) -> dict:
|
||||
if not active_version:
|
||||
return {'total': 0, 'missing': 0}
|
||||
|
||||
all_qs = session.query(Question).filter(Question.test_version_id == active_version.id).all()
|
||||
total = len(all_qs)
|
||||
missing = sum(1 for q in all_qs if not q.ai_hint)
|
||||
all_qs = session.query(Question).options(selectinload(Question.options)).filter(
|
||||
Question.test_version_id == active_version.id
|
||||
).all()
|
||||
filled_qs = []
|
||||
for q in all_qs:
|
||||
if not (q.text or '').strip():
|
||||
continue
|
||||
if len([o for o in q.options if (o.text or '').strip()]) < 2:
|
||||
continue
|
||||
filled_qs.append(q)
|
||||
total = len(filled_qs)
|
||||
missing = sum(1 for q in filled_qs if not q.ai_hint)
|
||||
return {'total': total, 'missing': missing}
|
||||
|
||||
|
||||
@@ -400,8 +409,16 @@ def generate_missing_hints_for_test(session_or_eng, author_id: str, test_id: str
|
||||
.all()
|
||||
)
|
||||
|
||||
generated = failed = 0
|
||||
generated = failed = skipped = 0
|
||||
for q in missing_qs:
|
||||
# Подсказку строим только по заполненному вопросу (есть текст и >=2 непустых варианта).
|
||||
if not (q.text or '').strip():
|
||||
skipped += 1
|
||||
continue
|
||||
valid_opts = [o for o in q.options if (o.text or '').strip()]
|
||||
if len(valid_opts) < 2:
|
||||
skipped += 1
|
||||
continue
|
||||
opt_payload = [{'text': o.text, 'isCorrect': bool(o.is_correct)} for o in q.options]
|
||||
hint = generate_question_hint(question_text=q.text, options=opt_payload)
|
||||
if hint:
|
||||
@@ -410,7 +427,7 @@ def generate_missing_hints_for_test(session_or_eng, author_id: str, test_id: str
|
||||
else:
|
||||
failed += 1
|
||||
session.commit()
|
||||
return {'generated': generated, 'failed': failed, 'total': len(missing_qs)}
|
||||
return {'generated': generated, 'failed': failed, 'skipped': skipped, 'total': len(missing_qs)}
|
||||
|
||||
|
||||
def check_question_for_attempt(session_or_eng, user_id: str, test_id: str, attempt_id: str,
|
||||
|
||||
Reference in New Issue
Block a user