feat(lesson-ai): начало фразы отчёта зависит от номера темы

Отчёт о занятии теперь начинается по-разному в зависимости от
номера темы: «начали знакомство» для первого занятия модуля (1/N),
«продолжили изучение» для промежуточного (2…N-1 из N) и
«завершили изучение» для последнего (N/N). Раньше первое
предложение всегда звучало одинаково, из-за чего первое и последнее
занятие модуля читались в отчёте для родителя одинаково.

Позицию разбирает воркер, а не модель. На локальной модели
(qwen2.5-1.5b) проверка показала, что с одной только подсказкой в
промпте «2/2» уверенно читается как «продолжали» вместо
«завершили», а в отчёте без номера темы модель выдумывала начало и
подмешивала «Scratch Jr» из примера. Поэтому номер разбирается
детерминированно и передаётся готовой строкой.

Что добавлено:
- lessonTopicPosition в worker.js — разбор «N/M» в конце свободного
  текста темы: { kind, n, total, label }, kind = first | middle | last.
  N = M проверяется раньше N = 1, поэтому «1/1» — это last. Мусорные
  значения (N > M, M = 0, пустая тема) дают null, и строка подсказки
  не добавляется. Хелпер экспортирован ради юнит-проверок
- в контекст запроса к модели добавлена строка
  «Позиция темы: последнее занятие модуля (2 из 2)»; модель только
  выбирает формулировку по ней

Промпт (LESSON_AI_DEFAULT_PROMPT, 2348 → 4523 симв.): новый блок
«НАЧАЛО ФРАЗЫ» с обязательными глаголами для каждого случая,
приоритет системной подсказки над «N/M», нейтральное начало без
номера, пример с тремя вариантами и запрет переносить «модуль» и
цифры в текст отчёта. Четыре копии промпта (server.js, db/init.sql,
db/migration.sql INSERT и UPDATE) синхронизированы побайтово; в
value IN миграции добавлен прежний дефолт, иначе правка не доехала
бы до существующих БД.

Проверено на живой модели: 1/2 → «начали знакомство», 2/2 →
«завершили изучение», 3/5 → «продолжили», 1/7 → «начали знакомство»,
без номера — нейтрально и без утечки из примера; e2e через
POST /api/lesson-reports с темой «2/2» дал ai_status = done.

AGENTS.md §3d: описание lesson_ai_prompt всё ещё называло его
«шаблоном делового сообщения», а список контекста воркера не
упоминал тему — исправлено, добавлены два пункта про позицию темы.
This commit is contained in:
dev
2026-10-04 16:12:53 +03:00
parent 28fac8a7aa
commit b5f377b1bf
5 changed files with 141 additions and 32 deletions
+19 -1
View File
@@ -766,6 +766,22 @@ function createEntryAutoChecker({ pool, getSetting, logAudit, aiUrl, defaultProm
return { start, notify, getStats, getInfo };
}
const LESSON_TOPIC_POSITION_LABELS = {
first: 'первое занятие модуля',
middle: 'промежуточное занятие модуля',
last: 'последнее занятие модуля',
};
function lessonTopicPosition(topic) {
const m = /(\d+)\s*\/\s*(\d+)\s*$/.exec(String(topic || '').trim());
if (!m) return null;
const n = parseInt(m[1], 10);
const total = parseInt(m[2], 10);
if (!Number.isInteger(n) || !Number.isInteger(total) || total < 1 || n < 1 || n > total) return null;
const kind = n === total ? 'last' : n === 1 ? 'first' : 'middle';
return { kind, n, total, label: `${LESSON_TOPIC_POSITION_LABELS[kind]} (${n} из ${total})` };
}
function createLessonReportChecker({ pool, getSetting, logAudit, aiUrl, model, defaultPrompt, bus, onDone }) {
const AI_URL = aiUrl || process.env.AI_URL || 'http://text-corrector:8080';
const MODEL = model || process.env.AI_MODEL || 'qwen2.5-1.5b-instruct-q4_k_m.gguf';
@@ -929,11 +945,13 @@ async function finish(row, status, text, error, aiText) {
const time = row.lesson_time ? String(row.lesson_time).slice(0, 5) : '';
const input = original.length > MAX_INPUT_CHARS ? original.slice(0, MAX_INPUT_CHARS) : original;
const topic = String(row.topic || '').trim();
const position = lessonTopicPosition(topic);
const context = [
`Группа: ${row.group_name || '—'}`,
`Дата занятия: ${date}`,
time ? `Время занятия: ${time}` : '',
topic ? `Тема занятия: ${topic}` : '',
position ? `Позиция темы: ${position.label}` : '',
'',
'Текст отчёта:',
input,
@@ -1033,4 +1051,4 @@ async function loop() {
return { start, stop, notify, getInfo };
}
module.exports = { createEntryAutoChecker, createPhotoEnhanceWorker, createLessonReportChecker };
module.exports = { createEntryAutoChecker, createPhotoEnhanceWorker, createLessonReportChecker, lessonTopicPosition };