4b0d56ff0e
Этап 1 миграции TestingWebApp на целевой стек (Python/Flask/Jinja),
БД остаётся clinic_tests.
E1.0 — База Flask-приложения: SQLAlchemy/psycopg2 пул, Flask sessions,
фабрика create_app, blueprint main с / и /health, base.html в стиле
кабинета HR (Tailwind CDN + Manrope + Material Symbols), 404/500.
E1.1 — Auth + /api/me: Flask sessions (signed cookie) вместо JWT,
bcrypt + Werkzeug, опц. HR_AUTH=1 с UPSERT в clinic_tests.users по
staff_id. UI /login, JSON /api/auth/{login,logout,me}, декораторы
@login_required / @require_role.
E1.2 — Тесты: список + редактор. 10 эндпоинтов, сервисы test_draft,
test_access, test_chain, ai_editor, llm_client, draft_validator,
editor_content. UI /tests (каталог + создание) и /tests/<id>/edit
(редактор с AI). Полный мобильный UX (аккордеоны/drag-n-drop) — в E1.7.
E1.3 — Импорт документов: pypdf + python-docx, эндпоинт
POST /api/tests/import/document, кнопка «Импорт документа» в
AI-панели редактора, лимит 16 МБ.
E1.8 — AI v2: страница /settings (статус ENV-ключа + ping),
ai/generate-by-title (без сетки), ai/check (рецензия), ai/improve
(массовое было→стало с чекбоксами). Унифицированный ответ AI-ошибок:
{ error, code, settingsUrl }.
Docker:
- docker-compose.dev.yml: добавлены DATABASE_URL, HR_AUTH/HR_DATABASE_URL,
DEEPSEEK_API_KEY/OPENAI_API_KEY/LLM_BASE_URL/LLM_MODEL и сеть postgres
для testing-flask.
Документация:
- docs/migration-final.md — двух-этапный план (Этап 1: унификация
стека внутри TestingWebApp; Этап 2: слияние с tgFlaskForm).
- docs/migration-final-inventory.md — карта 22 эндпоинтов Express.
Made-with: Cursor
73 lines
3.1 KiB
Python
73 lines
3.1 KiB
Python
"""Генерация черновика теста из извлечённого текста (порт части `documentGenService.js`)."""
|
|
from __future__ import annotations
|
|
|
|
from .draft_validator import (
|
|
parse_json_from_llm_text,
|
|
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) -> 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. '
|
|
'Текст и формулировки — на русском, по содержанию входного материала.'
|
|
)
|
|
user = (
|
|
'Составь тест с вопросами с одним или несколькими правильными ответами '
|
|
'на основе текста:\n\n' + slice_
|
|
)
|
|
raw = chat_completion_text_content(cfg, system, user, 0.25)
|
|
parsed = parse_json_from_llm_text(raw)
|
|
draft = validate_and_normalize_draft(parsed)
|
|
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],
|
|
}
|