feat(trainer): P3 — текстовые задачи от LLM с серверной проверкой подстановкой
- practiceVerify.js: грузит SimExpr в Node (require), verifyRoot подстановкой корня - practiceGenService.js: LLM (инъектируемый ask) → parse → validateAndVerify (SimExpr + подстановка + санитизация) → авторетрай по фидбэку; дефолт ask = assistantController.callLLMFailover - пул practice_problems (мигр.083); POST /api/practice/generate (учитель/админ) + GET /api/practice/pool - инвариант: невалидная/неверная задача в БД НЕ пишется → ученику не попадёт - клиент: LS.practicePool/Generate, тема «Текстовые задачи» (из пула; учителю кнопка «Сгенерировать») - тесты practice-gen.test.js 13/13 (verify, ретраи, off→503, 403 ученику, пул); смоуки страница 26/26; план P3 → DONE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,4 +82,53 @@ function submitAttempt(req, res) {
|
||||
res.json({ ok: true, progress: row, masteryStreak: MASTERY_STREAK });
|
||||
}
|
||||
|
||||
module.exports = { listProgress, submitAttempt };
|
||||
/* ── Пул текстовых задач (Уровень 1, LLM + проверка) ── */
|
||||
const genService = require('../services/practiceGenService');
|
||||
const POOL_TOPICS = { 'word-linear': 1, 'word-proportion': 1, 'word-percent': 1 };
|
||||
|
||||
function toClientProblem(r) {
|
||||
let solution = [];
|
||||
try { solution = r.solution_json ? JSON.parse(r.solution_json) : []; } catch (e) { solution = []; }
|
||||
return {
|
||||
id: r.id, kind: 'word', topic: r.topic, skill: r.skill,
|
||||
story: r.story, lhsExpr: r.lhs, rhsExpr: r.rhs,
|
||||
answerVar: r.answer_var, answer: r.answer, solution: solution
|
||||
};
|
||||
}
|
||||
|
||||
/* GET /api/practice/pool?skill=&limit= — одобренные задачи пула (ученикам). */
|
||||
function listPool(req, res) {
|
||||
const skill = (req.query && typeof req.query.skill === 'string') ? req.query.skill.trim().slice(0, MAX_SKILL) : '';
|
||||
const limit = Math.min(parseInt((req.query && req.query.limit), 10) || 20, 50);
|
||||
const rows = skill
|
||||
? db.prepare("SELECT * FROM practice_problems WHERE status='approved' AND (skill = ? OR topic = ?) ORDER BY id DESC LIMIT ?").all(skill, skill, limit)
|
||||
: db.prepare("SELECT * FROM practice_problems WHERE status='approved' ORDER BY id DESC LIMIT ?").all(limit);
|
||||
res.json({ problems: rows.map(toClientProblem) });
|
||||
}
|
||||
|
||||
/* POST /api/practice/generate { topic } — учитель/админ генерирует задачу в пул.
|
||||
* Сервис проверяет корректность подстановкой; не прошло — в БД НЕ пишем. */
|
||||
async function generateProblem(req, res) {
|
||||
const topic = (req.body && typeof req.body.topic === 'string') ? req.body.topic.trim() : 'word-linear';
|
||||
if (!POOL_TOPICS[topic]) return res.status(400).json({ error: 'unknown topic' });
|
||||
|
||||
let result;
|
||||
try { result = await genService.generate(topic, { maxRetries: 3 }); }
|
||||
catch (e) { return res.status(500).json({ error: 'generation failed' }); }
|
||||
|
||||
if (!result.ok) {
|
||||
const code = (result.error === 'off') ? 503 : 422; // нет провайдера → 503; не проверилось → 422
|
||||
return res.status(code).json({ error: result.error, reason: result.reason || null, attempts: result.attempts });
|
||||
}
|
||||
|
||||
const p = result.problem;
|
||||
const info = db.prepare(`
|
||||
INSERT INTO practice_problems (topic, skill, difficulty, story, lhs, rhs, answer_var, answer, solution_json, status, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'approved', ?)
|
||||
`).run(topic, topic, 1, p.story, p.lhs, p.rhs, p.answerVar, p.answer, JSON.stringify(p.solution || []), req.user.id);
|
||||
|
||||
const row = db.prepare('SELECT * FROM practice_problems WHERE id = ?').get(info.lastInsertRowid);
|
||||
res.json({ ok: true, problem: toClientProblem(row), attempts: result.attempts });
|
||||
}
|
||||
|
||||
module.exports = { listProgress, submitAttempt, listPool, generateProblem };
|
||||
|
||||
Reference in New Issue
Block a user