dsh-aha-tutor 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/docs/guide.zh-CN.md +15 -8
- package/lib/agent.js +1 -1
- package/lib/client.js +74 -31
- package/lib/index.js +237 -28
- package/lib/{runtime-ywaTyu56.js → runtime-BCGFUDlg.js} +250 -170
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as courseProgress, A as streamCounter, B as documentParts, C as reworkPrompt, D as generateJson, E as TutorError, F as materialsSummary, G as createConceptState, H as passageRange, I as passagesFor, J as extractJson, K as masteryPercent, L as readMaterials, M as emptyProgress, N as masteryFor, O as generateSteps, P as materialImages, Q as conceptIndex, R as readProfile, S as revisePrompt, T as summaryPrompt, U as spreadPassages, V as formatPassages, W as applyEvidence, X as normalizeBoard, Y as boardToPlainText, Z as blueprintOutline, _ as projectReviewPrompt, _t as newId, a as blueprintPrompt, at as nextLesson, b as quizPrompt, c as detectLanguage, ct as normalizeCheatsheet, d as flashcardsPrompt, dt as normalizeProject, et as findLesson, f as gradePrompt, ft as normalizeQuestionnaire, g as profilePrompt, gt as assertId, h as practicePrompt, ht as text, i as assistPrompt, it as lessonAfter, j as EMPTY_PROFILE, k as generateText, l as digestPartPrompt, lt as normalizeLesson, m as materialBriefPrompt, mt as safeRelativePath, n as publishRuntime, nt as insertLesson, o as cheatsheetPrompt, ot as normalizeBlueprint, p as lessonPrompt, pt as outlineForPrompt, q as recommendNext, r as askPrompt, rt as intakeSummary, s as correctionPrompt, st as normalizeCards, tt as flattenLessons, u as digestPrompt, ut as normalizePractice, v as projectsPrompt, w as shortPrompt, x as reexplainPrompt, y as questionnairePrompt, z as readProgress } from "./runtime-BCGFUDlg.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, join, relative, sep } from "node:path";
|
|
4
4
|
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
@@ -217,6 +217,25 @@ const TIMELINES = [
|
|
|
217
217
|
"balanced",
|
|
218
218
|
"relaxed"
|
|
219
219
|
];
|
|
220
|
+
const SIZES = [
|
|
221
|
+
"auto",
|
|
222
|
+
"short",
|
|
223
|
+
"standard",
|
|
224
|
+
"full"
|
|
225
|
+
];
|
|
226
|
+
function outlineStream(api) {
|
|
227
|
+
let soFar = "";
|
|
228
|
+
const counter = streamCounter("\"estMinutes\"", (count) => {
|
|
229
|
+
api.progress(count, 0, "blueprint");
|
|
230
|
+
try {
|
|
231
|
+
api.preview(blueprintOutline(extractJson(soFar)));
|
|
232
|
+
} catch {}
|
|
233
|
+
});
|
|
234
|
+
return (delta) => {
|
|
235
|
+
soFar += String(delta ?? "");
|
|
236
|
+
counter(delta);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
220
239
|
function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date() }) {
|
|
221
240
|
const stamp = () => now().toISOString();
|
|
222
241
|
async function readCourse(courseId) {
|
|
@@ -265,7 +284,11 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
265
284
|
}
|
|
266
285
|
async function get({ courseId }) {
|
|
267
286
|
const course = await readCourse(courseId);
|
|
268
|
-
const [progress, intake] = await Promise.all([
|
|
287
|
+
const [progress, intake, proposal] = await Promise.all([
|
|
288
|
+
readProgress(store, courseId),
|
|
289
|
+
store.readJson(store.paths.intake(courseId), null),
|
|
290
|
+
store.readJson(store.paths.proposal(courseId), null)
|
|
291
|
+
]);
|
|
269
292
|
const boards = new Set(await store.listIds(store.paths.boardsDir(courseId), "les"));
|
|
270
293
|
const practice = new Set(await store.listIds(store.paths.practiceDir(courseId), "les"));
|
|
271
294
|
const sheets = new Set(await store.listIds(store.paths.sheetsDir(courseId), "unt"));
|
|
@@ -273,6 +296,7 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
273
296
|
course,
|
|
274
297
|
progress,
|
|
275
298
|
intake,
|
|
299
|
+
proposal,
|
|
276
300
|
jobs: {
|
|
277
301
|
questions: jobs.running(`questions:${courseId}`),
|
|
278
302
|
blueprint: jobs.running(`blueprint:${courseId}`),
|
|
@@ -339,7 +363,7 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
339
363
|
meta: { courseId }
|
|
340
364
|
});
|
|
341
365
|
}
|
|
342
|
-
function submitIntake({ courseId, answers = {}, timeline = "balanced", weeklyMinutes, deadline }) {
|
|
366
|
+
function submitIntake({ courseId, answers = {}, timeline = "balanced", size = "auto", weeklyMinutes, deadline }) {
|
|
343
367
|
assertId(courseId, "crs");
|
|
344
368
|
return jobs.start("craft/blueprint", async (api) => {
|
|
345
369
|
const course = await readCourse(courseId);
|
|
@@ -353,10 +377,12 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
353
377
|
answers: cleanAnswers,
|
|
354
378
|
at: stamp()
|
|
355
379
|
});
|
|
380
|
+
if (course.materialIds?.length) api.progress(0, 0, "materials");
|
|
356
381
|
const materials = await readMaterials(store, course.materialIds ?? []);
|
|
357
382
|
const profile = await readProfile(store);
|
|
358
383
|
const settings = {
|
|
359
384
|
timeline: TIMELINES.includes(timeline) ? timeline : "balanced",
|
|
385
|
+
size: SIZES.includes(size) ? size : "auto",
|
|
360
386
|
weeklyMinutes: clampMinutes(weeklyMinutes),
|
|
361
387
|
deadline: /^\d{4}-\d{2}-\d{2}$/u.test(String(deadline ?? "")) ? deadline : null
|
|
362
388
|
};
|
|
@@ -370,12 +396,13 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
370
396
|
materialsSummary: materialsSummary(materials),
|
|
371
397
|
profile,
|
|
372
398
|
timeline: settings.timeline,
|
|
399
|
+
size: settings.size,
|
|
373
400
|
weeklyMinutes: settings.weeklyMinutes
|
|
374
401
|
}),
|
|
375
402
|
images: llm.supportsImages?.("teach") ? await materialImages(store, materials) : void 0,
|
|
376
403
|
maxTokens: 16e3,
|
|
377
404
|
signal: api.signal,
|
|
378
|
-
onText:
|
|
405
|
+
onText: outlineStream(api),
|
|
379
406
|
normalize: (raw) => attachProjectUnits(raw, normalizeBlueprint(raw, { language: course.language }))
|
|
380
407
|
});
|
|
381
408
|
const next = {
|
|
@@ -420,19 +447,78 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
420
447
|
onText: streamCounter("\"estMinutes\"", (count) => api.progress(count, 0, "blueprint")),
|
|
421
448
|
normalize: (raw) => attachProjectUnits(raw, normalizeBlueprint(raw, { language: course.language }))
|
|
422
449
|
});
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
450
|
+
const proposal = {
|
|
451
|
+
instruction: request,
|
|
452
|
+
at: stamp(),
|
|
453
|
+
blueprint: revised
|
|
454
|
+
};
|
|
455
|
+
await store.writeJson(store.paths.proposal(courseId), proposal);
|
|
456
|
+
return {
|
|
457
|
+
course,
|
|
458
|
+
proposal
|
|
428
459
|
};
|
|
429
|
-
await saveCourse(next);
|
|
430
|
-
return { course: next };
|
|
431
460
|
}, {
|
|
432
461
|
key: `blueprint:${courseId}`,
|
|
433
462
|
meta: { courseId }
|
|
434
463
|
});
|
|
435
464
|
}
|
|
465
|
+
async function applyProposal({ courseId }) {
|
|
466
|
+
const course = await readCourse(courseId);
|
|
467
|
+
const proposal = await store.readJson(store.paths.proposal(courseId), null);
|
|
468
|
+
if (!proposal?.blueprint?.units) throw new TutorError("not-found", "There is no proposed change");
|
|
469
|
+
const next = {
|
|
470
|
+
...course,
|
|
471
|
+
...proposal.blueprint,
|
|
472
|
+
language: course.language,
|
|
473
|
+
status: course.status === "ready" ? "ready" : "blueprint"
|
|
474
|
+
};
|
|
475
|
+
await saveCourse(next);
|
|
476
|
+
await store.removeDir(store.paths.proposal(courseId));
|
|
477
|
+
return { course: next };
|
|
478
|
+
}
|
|
479
|
+
async function discardProposal({ courseId }) {
|
|
480
|
+
assertId(courseId, "crs");
|
|
481
|
+
await store.removeDir(store.paths.proposal(courseId));
|
|
482
|
+
return { ok: true };
|
|
483
|
+
}
|
|
484
|
+
function reworkLesson({ courseId, lesson: raw, unitTitle, lectureTitle, mode }) {
|
|
485
|
+
const split = mode === "split";
|
|
486
|
+
const lesson = normalizeLesson(raw);
|
|
487
|
+
if (!lesson) throw new TutorError("invalid", "Unknown lesson");
|
|
488
|
+
return jobs.start("craft/rework", async (api) => {
|
|
489
|
+
const course = await readCourse(courseId);
|
|
490
|
+
return { lessons: await generateJson(llm, {
|
|
491
|
+
purpose: "teach",
|
|
492
|
+
prompt: reworkPrompt({
|
|
493
|
+
outline: outlineForPrompt(course, { maxChars: 4e3 }),
|
|
494
|
+
unitTitle: text(unitTitle, 120),
|
|
495
|
+
lectureTitle: text(lectureTitle, 120),
|
|
496
|
+
lesson: {
|
|
497
|
+
title: lesson.title,
|
|
498
|
+
summary: lesson.summary,
|
|
499
|
+
objectives: lesson.objectives,
|
|
500
|
+
concepts: lesson.concepts.map((concept) => concept.name),
|
|
501
|
+
estMinutes: lesson.estMinutes
|
|
502
|
+
},
|
|
503
|
+
mode: split ? "split" : "deepen",
|
|
504
|
+
language: course.language
|
|
505
|
+
}),
|
|
506
|
+
maxTokens: 3e3,
|
|
507
|
+
signal: api.signal,
|
|
508
|
+
normalize: (value) => {
|
|
509
|
+
const list = (Array.isArray(value?.lessons) ? value.lessons : [value]).map((entry) => normalizeLesson({
|
|
510
|
+
...entry,
|
|
511
|
+
id: void 0
|
|
512
|
+
})).filter(Boolean).slice(0, split ? 3 : 1);
|
|
513
|
+
if (split ? list.length < 2 : !list.length) throw new Error("no usable lessons");
|
|
514
|
+
return list;
|
|
515
|
+
}
|
|
516
|
+
}) };
|
|
517
|
+
}, {
|
|
518
|
+
key: `rework:${courseId}:${lesson.id}`,
|
|
519
|
+
meta: { courseId }
|
|
520
|
+
});
|
|
521
|
+
}
|
|
436
522
|
async function saveBlueprint({ courseId, blueprint }) {
|
|
437
523
|
const course = await readCourse(courseId);
|
|
438
524
|
const normalized = normalizeBlueprint({
|
|
@@ -463,7 +549,7 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
463
549
|
language: course.language,
|
|
464
550
|
passages: passagesFor(sources, query, 5e3)
|
|
465
551
|
}),
|
|
466
|
-
maxTokens: Math.min(16e3, 3e3 + lessons.length *
|
|
552
|
+
maxTokens: Math.min(16e3, 3e3 + lessons.length * 2600),
|
|
467
553
|
signal,
|
|
468
554
|
normalize: (raw) => raw?.lessons ?? raw
|
|
469
555
|
});
|
|
@@ -642,6 +728,9 @@ function createCourses({ store, llm, jobs, now = () => /* @__PURE__ */ new Date(
|
|
|
642
728
|
startCraft,
|
|
643
729
|
submitIntake,
|
|
644
730
|
revise,
|
|
731
|
+
applyProposal,
|
|
732
|
+
discardProposal,
|
|
733
|
+
reworkLesson,
|
|
645
734
|
saveBlueprint,
|
|
646
735
|
writeLecturePractice,
|
|
647
736
|
confirm,
|
|
@@ -1522,6 +1611,7 @@ function createJobs({ logger, now = Date.now } = {}) {
|
|
|
1522
1611
|
label: ""
|
|
1523
1612
|
},
|
|
1524
1613
|
partial: [],
|
|
1614
|
+
preview: null,
|
|
1525
1615
|
result: void 0,
|
|
1526
1616
|
error: void 0,
|
|
1527
1617
|
startedAt: now(),
|
|
@@ -1541,6 +1631,9 @@ function createJobs({ logger, now = Date.now } = {}) {
|
|
|
1541
1631
|
},
|
|
1542
1632
|
push(item) {
|
|
1543
1633
|
job.partial.push(item);
|
|
1634
|
+
},
|
|
1635
|
+
preview(value) {
|
|
1636
|
+
job.preview = value ?? null;
|
|
1544
1637
|
}
|
|
1545
1638
|
};
|
|
1546
1639
|
const aborted = new Promise((_, reject) => {
|
|
@@ -1573,6 +1666,7 @@ function createJobs({ logger, now = Date.now } = {}) {
|
|
|
1573
1666
|
progress: job.progress,
|
|
1574
1667
|
partial: job.partial.slice(since),
|
|
1575
1668
|
partialCount: job.partial.length,
|
|
1669
|
+
preview: job.preview ?? void 0,
|
|
1576
1670
|
result: job.status === "done" ? job.result : void 0,
|
|
1577
1671
|
error: job.error
|
|
1578
1672
|
};
|
|
@@ -1668,6 +1762,7 @@ function buildPlan({ today, settings, courses = [], reviews = [], exams = [] })
|
|
|
1668
1762
|
const queues = courses.map((course) => ({
|
|
1669
1763
|
id: course.id,
|
|
1670
1764
|
title: course.title,
|
|
1765
|
+
emoji: course.emoji || "",
|
|
1671
1766
|
deadline: course.deadline || null,
|
|
1672
1767
|
priority: Number(course.priority) || 0,
|
|
1673
1768
|
remaining: (course.lessons || []).filter((lesson) => !lesson.done).map((lesson) => ({
|
|
@@ -1728,6 +1823,7 @@ function buildPlan({ today, settings, courses = [], reviews = [], exams = [] })
|
|
|
1728
1823
|
courseId: queue.id,
|
|
1729
1824
|
title: lesson.title,
|
|
1730
1825
|
courseTitle: queue.title,
|
|
1826
|
+
courseEmoji: queue.emoji,
|
|
1731
1827
|
minutes: lesson.minutes
|
|
1732
1828
|
});
|
|
1733
1829
|
if (queue.remaining.length === 0) queue.finishDay = day;
|
|
@@ -1936,6 +2032,7 @@ function createLearner({ store, llm, now = () => /* @__PURE__ */ new Date(), log
|
|
|
1936
2032
|
courses: active.map(({ course, progress }) => ({
|
|
1937
2033
|
id: course.id,
|
|
1938
2034
|
title: course.title,
|
|
2035
|
+
emoji: course.emoji ?? "",
|
|
1939
2036
|
deadline: course.settings?.deadline ?? null,
|
|
1940
2037
|
lessons: flattenLessons(course).map(({ lesson }) => ({
|
|
1941
2038
|
id: lesson.id,
|
|
@@ -1990,10 +2087,11 @@ function createLearner({ store, llm, now = () => /* @__PURE__ */ new Date(), log
|
|
|
1990
2087
|
}));
|
|
1991
2088
|
return plan();
|
|
1992
2089
|
}
|
|
1993
|
-
async function reviewSession({ limit = 8 } = {}) {
|
|
2090
|
+
async function reviewSession({ limit = 8, courseId } = {}) {
|
|
1994
2091
|
const at = now().getTime();
|
|
1995
2092
|
const items = [];
|
|
1996
2093
|
for (const { course, progress } of await activeCourses()) {
|
|
2094
|
+
if (courseId && course.id !== courseId) continue;
|
|
1997
2095
|
const index = conceptIndex(course);
|
|
1998
2096
|
const due = Object.entries(progress.reviews ?? {}).filter(([id, card]) => index.has(id) && Date.parse(card.dueAt) <= at).sort((a, b) => Date.parse(a[1].dueAt) - Date.parse(b[1].dueAt));
|
|
1999
2097
|
for (const [conceptId] of due) {
|
|
@@ -2002,7 +2100,7 @@ function createLearner({ store, llm, now = () => /* @__PURE__ */ new Date(), log
|
|
|
2002
2100
|
const lesson = findLesson(course, concept.lessonId)?.lesson;
|
|
2003
2101
|
const set = await store.readJson(store.paths.practice(course.id, concept.lessonId), null);
|
|
2004
2102
|
const board = await store.readJson(store.paths.board(course.id, concept.lessonId), null);
|
|
2005
|
-
const candidates = [...set?.items ?? [], ...(board?.steps ?? []).map((step) => step.check).filter(Boolean)].filter((item) => item.conceptIds?.includes(conceptId) && item.kind !== "short");
|
|
2103
|
+
const candidates = [...set?.items ?? [], ...(board?.steps ?? []).map((step) => step.check).filter(Boolean)].filter((item) => item.conceptIds?.includes(conceptId) && item.kind !== "short" && item.kind !== "work");
|
|
2006
2104
|
const item = candidates[Math.floor(Math.random() * candidates.length)];
|
|
2007
2105
|
if (item) items.push({
|
|
2008
2106
|
courseId: course.id,
|
|
@@ -2177,6 +2275,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
|
|
|
2177
2275
|
const kindDirective = row.lesson.kind === "remedial" ? "remediate" : row.lesson.kind === "review" ? "review" : row.lesson.kind === "challenge" ? "challenge" : void 0;
|
|
2178
2276
|
return generateSteps(llm, {
|
|
2179
2277
|
purpose: "teach",
|
|
2278
|
+
effort: "low",
|
|
2180
2279
|
prompt: lessonPrompt({
|
|
2181
2280
|
course,
|
|
2182
2281
|
unit: row.unit,
|
|
@@ -2359,6 +2458,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
|
|
|
2359
2458
|
const index = Math.min(board.steps.length - 1, Math.max(0, Number(stepIndex) || 0));
|
|
2360
2459
|
const answer = await generateSteps(llm, {
|
|
2361
2460
|
purpose: "teach",
|
|
2461
|
+
effort: "low",
|
|
2362
2462
|
prompt: reexplainPrompt({
|
|
2363
2463
|
lesson,
|
|
2364
2464
|
language,
|
|
@@ -2439,18 +2539,25 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
|
|
|
2439
2539
|
}
|
|
2440
2540
|
async function grade({ item, response, language = "zh-CN" }) {
|
|
2441
2541
|
if (!item?.prompt) throw new TutorError("invalid", "Missing question");
|
|
2542
|
+
const work = item.kind === "work";
|
|
2442
2543
|
const answer = text(response, 4e3);
|
|
2443
2544
|
if (!answer) return {
|
|
2444
2545
|
score: 0,
|
|
2445
2546
|
verdict: "incorrect",
|
|
2446
2547
|
feedback: "",
|
|
2447
2548
|
errorTag: "",
|
|
2448
|
-
misconception: false
|
|
2549
|
+
misconception: false,
|
|
2550
|
+
...work ? {
|
|
2551
|
+
wrongStep: "",
|
|
2552
|
+
why: "",
|
|
2553
|
+
fix: ""
|
|
2554
|
+
} : {}
|
|
2449
2555
|
};
|
|
2450
2556
|
return await generateJson(llm, {
|
|
2451
2557
|
purpose: "quick",
|
|
2452
2558
|
prompt: gradePrompt({
|
|
2453
2559
|
item: {
|
|
2560
|
+
kind: work ? "work" : "short",
|
|
2454
2561
|
prompt: text(item.prompt, 2e3),
|
|
2455
2562
|
answer: text(item.answer, 3e3),
|
|
2456
2563
|
rubric: Array.isArray(item.rubric) ? item.rubric.map((entry) => text(entry, 300)) : []
|
|
@@ -2458,7 +2565,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
|
|
|
2458
2565
|
response: answer,
|
|
2459
2566
|
language
|
|
2460
2567
|
}),
|
|
2461
|
-
maxTokens: 1200,
|
|
2568
|
+
maxTokens: work ? 1800 : 1200,
|
|
2462
2569
|
normalize: (raw) => ({
|
|
2463
2570
|
score: Math.min(1, Math.max(0, Number(raw?.score) || 0)),
|
|
2464
2571
|
verdict: [
|
|
@@ -2468,10 +2575,60 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
|
|
|
2468
2575
|
].includes(raw?.verdict) ? raw.verdict : Number(raw?.score) >= .8 ? "correct" : Number(raw?.score) >= .4 ? "partial" : "incorrect",
|
|
2469
2576
|
feedback: text(raw?.feedback, 1200),
|
|
2470
2577
|
errorTag: text(raw?.errorTag, 80),
|
|
2471
|
-
misconception: raw?.misconception === true
|
|
2578
|
+
misconception: raw?.misconception === true,
|
|
2579
|
+
...work ? {
|
|
2580
|
+
wrongStep: text(raw?.wrongStep, 400),
|
|
2581
|
+
why: text(raw?.why, 400),
|
|
2582
|
+
fix: text(raw?.fix, 400)
|
|
2583
|
+
} : {}
|
|
2472
2584
|
})
|
|
2473
2585
|
});
|
|
2474
2586
|
}
|
|
2587
|
+
function correct({ scope, stepIndex = 0, anchorStepId, item, response, grade: graded }) {
|
|
2588
|
+
const working = text(response, 4e3);
|
|
2589
|
+
if (!item?.prompt || !working) throw new TutorError("invalid", "Nothing to correct");
|
|
2590
|
+
const notes = graded && typeof graded === "object" ? graded : {};
|
|
2591
|
+
return jobs.start("lesson/correct", async (api) => {
|
|
2592
|
+
const { course, lesson, board, language } = await scopeBoard(scope);
|
|
2593
|
+
const answer = await generateSteps(llm, {
|
|
2594
|
+
purpose: "quick",
|
|
2595
|
+
prompt: correctionPrompt({
|
|
2596
|
+
course,
|
|
2597
|
+
lesson,
|
|
2598
|
+
language,
|
|
2599
|
+
boardSoFar: board ? boardToPlainText(board, {
|
|
2600
|
+
uptoStep: Number(stepIndex) || 0,
|
|
2601
|
+
maxChars: 5e3
|
|
2602
|
+
}) : "",
|
|
2603
|
+
item: {
|
|
2604
|
+
prompt: text(item.prompt, 2e3),
|
|
2605
|
+
answer: text(item.answer, 3e3)
|
|
2606
|
+
},
|
|
2607
|
+
response: working,
|
|
2608
|
+
grade: {
|
|
2609
|
+
feedback: text(notes.feedback, 1200),
|
|
2610
|
+
wrongStep: text(notes.wrongStep, 400),
|
|
2611
|
+
why: text(notes.why, 400),
|
|
2612
|
+
fix: text(notes.fix, 400)
|
|
2613
|
+
}
|
|
2614
|
+
}),
|
|
2615
|
+
maxTokens: 4e3,
|
|
2616
|
+
signal: api.signal,
|
|
2617
|
+
conceptIds: lesson?.concepts?.map((concept) => concept.id) ?? [],
|
|
2618
|
+
onStep: (step) => api.push(step)
|
|
2619
|
+
});
|
|
2620
|
+
const aside = await keepAside(scope, {
|
|
2621
|
+
kind: "correction",
|
|
2622
|
+
errorTag: text(notes.errorTag, 80),
|
|
2623
|
+
anchorStepId: anchorFor(board, stepIndex, anchorStepId),
|
|
2624
|
+
steps: answer.steps
|
|
2625
|
+
});
|
|
2626
|
+
return {
|
|
2627
|
+
steps: answer.steps,
|
|
2628
|
+
aside
|
|
2629
|
+
};
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2475
2632
|
async function savePosition({ courseId, lessonId, stepIndex = 0 }) {
|
|
2476
2633
|
assertId(lessonId, "les");
|
|
2477
2634
|
await store.updateJson(store.paths.progress(assertId(courseId, "crs")), null, (progress) => {
|
|
@@ -2744,6 +2901,7 @@ function createLessons({ store, llm, jobs, courses, learner, now = () => /* @__P
|
|
|
2744
2901
|
reexplain,
|
|
2745
2902
|
record,
|
|
2746
2903
|
grade,
|
|
2904
|
+
correct,
|
|
2747
2905
|
savePosition,
|
|
2748
2906
|
complete,
|
|
2749
2907
|
practice,
|
|
@@ -2781,6 +2939,17 @@ const RETRYABLE$1 = /* @__PURE__ */ new Set([
|
|
|
2781
2939
|
"TIMEOUT",
|
|
2782
2940
|
"TRANSPORT"
|
|
2783
2941
|
]);
|
|
2942
|
+
const REASONING_ROOM = Object.freeze({
|
|
2943
|
+
off: 0,
|
|
2944
|
+
none: 0,
|
|
2945
|
+
minimal: 4e3,
|
|
2946
|
+
low: 8e3,
|
|
2947
|
+
medium: 16e3,
|
|
2948
|
+
high: 24e3,
|
|
2949
|
+
xhigh: 48e3,
|
|
2950
|
+
max: 48e3
|
|
2951
|
+
});
|
|
2952
|
+
const STOPPED_AT_LIMIT = /* @__PURE__ */ new Set(["max-tokens", "length"]);
|
|
2784
2953
|
const CALL_TIMEOUT_MS = 6 * 6e4;
|
|
2785
2954
|
const MAX_RETRIES = 2;
|
|
2786
2955
|
const USER_ERRORS = Object.freeze({
|
|
@@ -2794,6 +2963,7 @@ const USER_ERRORS = Object.freeze({
|
|
|
2794
2963
|
CONTEXT_WINDOW_EXCEEDED: ["llm-context", "The material is too long for this model. Pick fewer materials or a model with a larger context."],
|
|
2795
2964
|
NO_ADAPTER: ["llm-unavailable", "The selected model is not available. Pick another model in Aha settings."],
|
|
2796
2965
|
TIMEOUT: ["llm-timeout", "The model took too long to respond. Try again."],
|
|
2966
|
+
EMPTY_RESPONSE: ["llm-empty", "The model returned nothing this time. Try again."],
|
|
2797
2967
|
ABORTED: ["cancelled", "Cancelled"]
|
|
2798
2968
|
});
|
|
2799
2969
|
function toTutorError(code, fallback) {
|
|
@@ -2801,6 +2971,17 @@ function toTutorError(code, fallback) {
|
|
|
2801
2971
|
if (known) return new TutorError(known[0], known[1]);
|
|
2802
2972
|
return new TutorError("llm-failed", `The model call failed${code ? ` (${code})` : ""}${fallback ? `: ${fallback}` : ""}`);
|
|
2803
2973
|
}
|
|
2974
|
+
function pickEffort(efforts, wanted) {
|
|
2975
|
+
if (!efforts?.length || !wanted) return void 0;
|
|
2976
|
+
if (wanted === "lowest") return efforts[0].id;
|
|
2977
|
+
return (efforts.find((effort) => effort.id === wanted) ?? efforts.find((effort) => effort.id !== "off" && effort.id !== "none") ?? efforts[0]).id;
|
|
2978
|
+
}
|
|
2979
|
+
function outputBudget(maxTokens, effort, model) {
|
|
2980
|
+
const cap = Number(model?.defaultMaxTokens) || 0;
|
|
2981
|
+
if (!cap || !model?.reasoning) return maxTokens;
|
|
2982
|
+
const room = REASONING_ROOM[effort ?? model.reasoning.defaultEffort] ?? REASONING_ROOM.high;
|
|
2983
|
+
return Math.max(Math.min(maxTokens, cap), Math.min(maxTokens + room, cap));
|
|
2984
|
+
}
|
|
2804
2985
|
const sleep$1 = (ms, signal) => new Promise((resolve, reject) => {
|
|
2805
2986
|
const timer = setTimeout(resolve, ms);
|
|
2806
2987
|
signal?.addEventListener("abort", () => {
|
|
@@ -2860,7 +3041,7 @@ function createLlmPort({ getLlm, getDefault, getAttachments, prefs, logger }) {
|
|
|
2860
3041
|
}
|
|
2861
3042
|
return blocks;
|
|
2862
3043
|
}
|
|
2863
|
-
async function attempt({ purpose, system, user, images, maxTokens, signal, onText }) {
|
|
3044
|
+
async function attempt({ purpose, effort, system, user, images, maxTokens, signal, onText }) {
|
|
2864
3045
|
const llm = getLlm();
|
|
2865
3046
|
if (!llm) throw new TutorError("llm-unavailable", "DSH has no model service in this profile.");
|
|
2866
3047
|
const target = route(purpose);
|
|
@@ -2871,13 +3052,14 @@ function createLlmPort({ getLlm, getDefault, getAttachments, prefs, logger }) {
|
|
|
2871
3052
|
prepared = await llm.prepareCall({
|
|
2872
3053
|
...target,
|
|
2873
3054
|
maxTokens
|
|
2874
|
-
}, fused,
|
|
2875
|
-
const
|
|
2876
|
-
return
|
|
3055
|
+
}, fused, (controls, model) => {
|
|
3056
|
+
const chosen = pickEffort(model?.reasoning?.efforts, purpose === "quick" ? "lowest" : effort);
|
|
3057
|
+
return {
|
|
2877
3058
|
...controls,
|
|
2878
|
-
reasoningEffort:
|
|
2879
|
-
|
|
2880
|
-
|
|
3059
|
+
...chosen ? { reasoningEffort: chosen } : {},
|
|
3060
|
+
maxTokens: outputBudget(maxTokens, chosen, model)
|
|
3061
|
+
};
|
|
3062
|
+
});
|
|
2881
3063
|
} catch (error) {
|
|
2882
3064
|
if (signal?.aborted) throw signal.reason ?? error;
|
|
2883
3065
|
throw Object.assign(toTutorError(error?.code, error?.message), { retryable: false });
|
|
@@ -2916,18 +3098,24 @@ function createLlmPort({ getLlm, getDefault, getAttachments, prefs, logger }) {
|
|
|
2916
3098
|
}
|
|
2917
3099
|
if (!textValue.trim()) throw Object.assign(toTutorError("EMPTY_RESPONSE"), {
|
|
2918
3100
|
retryable: true,
|
|
2919
|
-
streamed: false
|
|
3101
|
+
streamed: false,
|
|
3102
|
+
grow: STOPPED_AT_LIMIT.has(finish?.kind)
|
|
2920
3103
|
});
|
|
2921
3104
|
return textValue;
|
|
2922
3105
|
}
|
|
2923
3106
|
async function call(options) {
|
|
2924
3107
|
let lastError;
|
|
3108
|
+
let current = options;
|
|
2925
3109
|
for (let tries = 0; tries <= MAX_RETRIES; tries += 1) try {
|
|
2926
|
-
return await attempt(
|
|
3110
|
+
return await attempt(current);
|
|
2927
3111
|
} catch (error) {
|
|
2928
3112
|
lastError = error;
|
|
2929
3113
|
if (options.signal?.aborted || !error?.retryable || error?.streamed || tries === MAX_RETRIES) throw error;
|
|
2930
|
-
|
|
3114
|
+
if (error.grow) current = {
|
|
3115
|
+
...current,
|
|
3116
|
+
maxTokens: current.maxTokens * 2
|
|
3117
|
+
};
|
|
3118
|
+
else await sleep$1(Math.min(2e4, Number(error.retryAfterMs) || 1500 * (tries + 1)), options.signal);
|
|
2931
3119
|
}
|
|
2932
3120
|
throw lastError;
|
|
2933
3121
|
}
|
|
@@ -3029,6 +3217,7 @@ function createStore({ root }) {
|
|
|
3029
3217
|
course: (id) => join(paths.courseDir(id), "course.json"),
|
|
3030
3218
|
progress: (id) => join(paths.courseDir(id), "progress.json"),
|
|
3031
3219
|
intake: (id) => join(paths.courseDir(id), "intake.json"),
|
|
3220
|
+
proposal: (id) => join(paths.courseDir(id), "proposal.json"),
|
|
3032
3221
|
cards: (id) => join(paths.courseDir(id), "cards.json"),
|
|
3033
3222
|
boardsDir: (id) => join(paths.courseDir(id), "boards"),
|
|
3034
3223
|
practiceDir: (id) => join(paths.courseDir(id), "practice"),
|
|
@@ -3732,10 +3921,20 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
|
|
|
3732
3921
|
quick: payload.quick === true,
|
|
3733
3922
|
language: typeof payload.language === "string" ? payload.language.slice(0, 12) : void 0
|
|
3734
3923
|
}) }),
|
|
3924
|
+
"course/proposal-apply": (payload) => courses.applyProposal({ courseId: need(payload, "courseId", "crs") }),
|
|
3925
|
+
"course/proposal-discard": (payload) => courses.discardProposal({ courseId: need(payload, "courseId", "crs") }),
|
|
3926
|
+
"course/rework-lesson": (payload) => ({ job: courses.reworkLesson({
|
|
3927
|
+
courseId: need(payload, "courseId", "crs"),
|
|
3928
|
+
lesson: payload.lesson,
|
|
3929
|
+
unitTitle: payload.unitTitle,
|
|
3930
|
+
lectureTitle: payload.lectureTitle,
|
|
3931
|
+
mode: payload.mode === "split" ? "split" : "deepen"
|
|
3932
|
+
}) }),
|
|
3735
3933
|
"course/intake": (payload) => ({ job: courses.submitIntake({
|
|
3736
3934
|
courseId: need(payload, "courseId", "crs"),
|
|
3737
3935
|
answers: payload.answers,
|
|
3738
3936
|
timeline: payload.timeline,
|
|
3937
|
+
size: payload.size,
|
|
3739
3938
|
weeklyMinutes: payload.weeklyMinutes,
|
|
3740
3939
|
deadline: payload.deadline
|
|
3741
3940
|
}) }),
|
|
@@ -3818,6 +4017,14 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
|
|
|
3818
4017
|
response: payload.response,
|
|
3819
4018
|
language: typeof payload.language === "string" ? payload.language.slice(0, 12) : void 0
|
|
3820
4019
|
}),
|
|
4020
|
+
"lesson/correct": (payload) => ({ job: lessons.correct({
|
|
4021
|
+
scope: scopeOf(payload),
|
|
4022
|
+
stepIndex: payload.stepIndex,
|
|
4023
|
+
anchorStepId: payload.anchorStepId,
|
|
4024
|
+
item: payload.item,
|
|
4025
|
+
response: payload.response,
|
|
4026
|
+
grade: payload.grade
|
|
4027
|
+
}) }),
|
|
3821
4028
|
"lesson/position": (payload) => lessons.savePosition({
|
|
3822
4029
|
courseId: need(payload, "courseId", "crs"),
|
|
3823
4030
|
lessonId: need(payload, "lessonId", "les"),
|
|
@@ -3869,7 +4076,7 @@ function createRoutes({ courses, lessons, learner, studio, projects, jobs, setti
|
|
|
3869
4076
|
courseId: payload.courseId
|
|
3870
4077
|
}),
|
|
3871
4078
|
"plan/remove-exam": (payload) => learner.removeExam({ id: need(payload, "id", "exm") }),
|
|
3872
|
-
"review/session": () => learner.reviewSession(),
|
|
4079
|
+
"review/session": (payload) => learner.reviewSession({ courseId: optional(payload, "courseId", "crs") }),
|
|
3873
4080
|
"material/upload": (payload) => ({ job: studio.upload({
|
|
3874
4081
|
name: payload.name,
|
|
3875
4082
|
base64: payload.base64
|
|
@@ -5033,6 +5240,7 @@ function createStudio({ store, llm, jobs, now = () => /* @__PURE__ */ new Date()
|
|
|
5033
5240
|
const images = await assistImages(ids);
|
|
5034
5241
|
const board = await generateSteps(llm, {
|
|
5035
5242
|
purpose: "teach",
|
|
5243
|
+
effort: "low",
|
|
5036
5244
|
prompt: assistPrompt({
|
|
5037
5245
|
mode: chosen,
|
|
5038
5246
|
request: ask,
|
|
@@ -5094,6 +5302,7 @@ function createStudio({ store, llm, jobs, now = () => /* @__PURE__ */ new Date()
|
|
|
5094
5302
|
const language = detectLanguage(subject || passages, uiLanguage());
|
|
5095
5303
|
const board = await generateSteps(llm, {
|
|
5096
5304
|
purpose: "teach",
|
|
5305
|
+
effort: "low",
|
|
5097
5306
|
prompt: shortPrompt({
|
|
5098
5307
|
topic: subject || sourceTitle,
|
|
5099
5308
|
passages,
|