dsh-aha-tutor 1.6.0 → 1.7.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 +22 -0
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/docs/guide.zh-CN.md +18 -5
- package/lib/agent.js +1 -1
- package/lib/client.js +103 -31
- package/lib/index.js +276 -13
- package/lib/{runtime-B6zUpJjA.js → runtime-DfoeKFPj.js} +245 -151
- package/package.json +1 -1
- package/samples/bayes-thinking.aha-course.json +62 -56
- package/samples/harmonic-motion.aha-course.json +70 -63
|
@@ -826,6 +826,13 @@ function normalizeConcepts(raw, fallbackName) {
|
|
|
826
826
|
function keepId(value, prefix) {
|
|
827
827
|
return typeof value === "string" && new RegExp(`^${prefix}_[a-z0-9]{6,40}$`, "u").test(value) ? value : newId(prefix);
|
|
828
828
|
}
|
|
829
|
+
const LESSON_DEPTHS = Object.freeze([
|
|
830
|
+
"intuition",
|
|
831
|
+
"definition",
|
|
832
|
+
"derivation",
|
|
833
|
+
"application",
|
|
834
|
+
"extension"
|
|
835
|
+
]);
|
|
829
836
|
function normalizeLesson(raw) {
|
|
830
837
|
const title = text(raw?.title, 120);
|
|
831
838
|
if (!title) return null;
|
|
@@ -841,7 +848,8 @@ function normalizeLesson(raw) {
|
|
|
841
848
|
"remedial",
|
|
842
849
|
"review",
|
|
843
850
|
"challenge"
|
|
844
|
-
].includes(raw?.kind) ? raw.kind : "lesson"
|
|
851
|
+
].includes(raw?.kind) ? raw.kind : "lesson",
|
|
852
|
+
...LESSON_DEPTHS.includes(raw?.depth) ? { depth: raw.depth } : {}
|
|
845
853
|
};
|
|
846
854
|
}
|
|
847
855
|
function normalizeUnits(rawUnits) {
|
|
@@ -1044,6 +1052,49 @@ function normalizePractice(raw, conceptIds = []) {
|
|
|
1044
1052
|
id: `i${index + 1}`
|
|
1045
1053
|
})) };
|
|
1046
1054
|
}
|
|
1055
|
+
function normalizeHandout(raw) {
|
|
1056
|
+
const points = list(raw?.points ?? raw?.keyPoints, 6, (point) => {
|
|
1057
|
+
const title = text(typeof point === "string" ? point : point?.title ?? point?.point, 160);
|
|
1058
|
+
return title ? {
|
|
1059
|
+
title,
|
|
1060
|
+
oneLine: text(point?.oneLine ?? point?.summary, 300),
|
|
1061
|
+
detail: text(point?.detail, 600)
|
|
1062
|
+
} : null;
|
|
1063
|
+
});
|
|
1064
|
+
if (!points.length) throw new Error("the handout has no key points");
|
|
1065
|
+
const example = raw?.example && typeof raw.example === "object" ? {
|
|
1066
|
+
problem: text(raw.example.problem ?? raw.example.question, 800),
|
|
1067
|
+
steps: strings(raw.example.steps, 10, 400),
|
|
1068
|
+
answer: text(raw.example.answer, 300)
|
|
1069
|
+
} : null;
|
|
1070
|
+
return {
|
|
1071
|
+
title: text(raw?.title, 160),
|
|
1072
|
+
summary: text(raw?.summary ?? raw?.overview, 800),
|
|
1073
|
+
points,
|
|
1074
|
+
example: example?.problem ? example : null,
|
|
1075
|
+
mistakes: list(raw?.mistakes, 5, (item) => {
|
|
1076
|
+
const mistake = text(typeof item === "string" ? item : item?.mistake, 300);
|
|
1077
|
+
return mistake ? {
|
|
1078
|
+
mistake,
|
|
1079
|
+
fix: text(item?.fix ?? item?.instead, 300)
|
|
1080
|
+
} : null;
|
|
1081
|
+
}),
|
|
1082
|
+
glossary: list(raw?.glossary, 10, (item) => {
|
|
1083
|
+
const term = text(item?.term, 80);
|
|
1084
|
+
return term ? {
|
|
1085
|
+
term,
|
|
1086
|
+
meaning: text(item?.meaning ?? item?.definition, 300)
|
|
1087
|
+
} : null;
|
|
1088
|
+
}),
|
|
1089
|
+
questions: list(raw?.questions ?? raw?.selfCheck, 5, (item) => {
|
|
1090
|
+
const question = text(typeof item === "string" ? item : item?.question, 400);
|
|
1091
|
+
return question ? {
|
|
1092
|
+
question,
|
|
1093
|
+
answer: text(item?.answer, 400)
|
|
1094
|
+
} : null;
|
|
1095
|
+
})
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1047
1098
|
function normalizeCards(raw) {
|
|
1048
1099
|
return list(raw?.cards ?? raw, LIMITS.cards, (card, index) => {
|
|
1049
1100
|
const front = text(card?.front ?? card?.q ?? card?.question, 600);
|
|
@@ -1662,6 +1713,171 @@ function parseJsonLines(raw, { final = true } = {}) {
|
|
|
1662
1713
|
};
|
|
1663
1714
|
}
|
|
1664
1715
|
//#endregion
|
|
1716
|
+
//#region src/core/retrieval.js
|
|
1717
|
+
const CJK_RUN = /[-ヿ㐀-䶿一-鿿가-]+/gu;
|
|
1718
|
+
const WORD = /[\p{L}\p{N}][\p{L}\p{N}'_-]*/gu;
|
|
1719
|
+
const STOP = /* @__PURE__ */ new Set([
|
|
1720
|
+
"the",
|
|
1721
|
+
"a",
|
|
1722
|
+
"an",
|
|
1723
|
+
"and",
|
|
1724
|
+
"or",
|
|
1725
|
+
"of",
|
|
1726
|
+
"to",
|
|
1727
|
+
"in",
|
|
1728
|
+
"on",
|
|
1729
|
+
"for",
|
|
1730
|
+
"is",
|
|
1731
|
+
"are",
|
|
1732
|
+
"was",
|
|
1733
|
+
"be",
|
|
1734
|
+
"with",
|
|
1735
|
+
"as",
|
|
1736
|
+
"by",
|
|
1737
|
+
"at",
|
|
1738
|
+
"it",
|
|
1739
|
+
"this",
|
|
1740
|
+
"that",
|
|
1741
|
+
"from"
|
|
1742
|
+
]);
|
|
1743
|
+
function tokenize(value) {
|
|
1744
|
+
const textValue = String(value ?? "").toLowerCase();
|
|
1745
|
+
const tokens = [];
|
|
1746
|
+
for (const run of textValue.match(CJK_RUN) ?? []) {
|
|
1747
|
+
if (run.length === 1) tokens.push(run);
|
|
1748
|
+
for (let index = 0; index < run.length - 1; index += 1) tokens.push(run.slice(index, index + 2));
|
|
1749
|
+
}
|
|
1750
|
+
for (const word of textValue.replace(CJK_RUN, " ").match(WORD) ?? []) if (word.length > 1 && !STOP.has(word)) tokens.push(word);
|
|
1751
|
+
return tokens;
|
|
1752
|
+
}
|
|
1753
|
+
function buildIndex(chunks) {
|
|
1754
|
+
const docs = chunks.map((chunk) => {
|
|
1755
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1756
|
+
const tokens = tokenize(chunk.text);
|
|
1757
|
+
for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
1758
|
+
return {
|
|
1759
|
+
chunk,
|
|
1760
|
+
counts,
|
|
1761
|
+
length: tokens.length
|
|
1762
|
+
};
|
|
1763
|
+
});
|
|
1764
|
+
const df = /* @__PURE__ */ new Map();
|
|
1765
|
+
for (const doc of docs) for (const token of doc.counts.keys()) df.set(token, (df.get(token) ?? 0) + 1);
|
|
1766
|
+
return {
|
|
1767
|
+
docs,
|
|
1768
|
+
df,
|
|
1769
|
+
avg: docs.reduce((sum, doc) => sum + doc.length, 0) / Math.max(1, docs.length)
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function search(index, query, { limit = 6, k1 = 1.4, b = .75 } = {}) {
|
|
1773
|
+
const terms = [...new Set(tokenize(query))];
|
|
1774
|
+
if (!terms.length || !index.docs.length) return [];
|
|
1775
|
+
const total = index.docs.length;
|
|
1776
|
+
return index.docs.map((doc) => {
|
|
1777
|
+
let score = 0;
|
|
1778
|
+
for (const term of terms) {
|
|
1779
|
+
const tf = doc.counts.get(term);
|
|
1780
|
+
if (!tf) continue;
|
|
1781
|
+
const df = index.df.get(term) ?? 0;
|
|
1782
|
+
const idf = Math.log(1 + (total - df + .5) / (df + .5));
|
|
1783
|
+
score += idf * (tf * (k1 + 1) / (tf + k1 * (1 - b + b * (doc.length / (index.avg || 1)))));
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
chunk: doc.chunk,
|
|
1787
|
+
score
|
|
1788
|
+
};
|
|
1789
|
+
}).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1790
|
+
}
|
|
1791
|
+
function selectPassages(chunks, query, { maxChars = 12e3, limit = 12 } = {}) {
|
|
1792
|
+
if (!chunks?.length) return [];
|
|
1793
|
+
const picked = query ? search(buildIndex(chunks), query, { limit }).map((entry) => entry.chunk) : chunks.slice(0, limit);
|
|
1794
|
+
const result = [];
|
|
1795
|
+
let used = 0;
|
|
1796
|
+
for (const chunk of picked) {
|
|
1797
|
+
if (used + chunk.text.length > maxChars) {
|
|
1798
|
+
const room = maxChars - used;
|
|
1799
|
+
if (room > 400) result.push({
|
|
1800
|
+
...chunk,
|
|
1801
|
+
text: `${chunk.text.slice(0, room)}…`
|
|
1802
|
+
});
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
result.push(chunk);
|
|
1806
|
+
used += chunk.text.length;
|
|
1807
|
+
}
|
|
1808
|
+
return result;
|
|
1809
|
+
}
|
|
1810
|
+
function spreadPassages(chunks, { maxChars = 12e3, samples = 16 } = {}) {
|
|
1811
|
+
if (!chunks?.length) return [];
|
|
1812
|
+
if (chunks.reduce((sum, chunk) => sum + chunk.text.length, 0) <= maxChars) return chunks.slice();
|
|
1813
|
+
const count = Math.min(chunks.length, Math.max(2, samples));
|
|
1814
|
+
const share = Math.max(200, Math.floor(maxChars / count));
|
|
1815
|
+
const picked = [];
|
|
1816
|
+
for (let index = 0; index < count; index += 1) {
|
|
1817
|
+
const at = Math.round(index * (chunks.length - 1) / (count - 1));
|
|
1818
|
+
if (picked.at(-1) !== at) picked.push(at);
|
|
1819
|
+
}
|
|
1820
|
+
return picked.map((at) => {
|
|
1821
|
+
const chunk = chunks[at];
|
|
1822
|
+
return chunk.text.length > share ? {
|
|
1823
|
+
...chunk,
|
|
1824
|
+
text: `${chunk.text.slice(0, share)}…`
|
|
1825
|
+
} : chunk;
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
function documentParts(chunks, { partChars = 2e4, maxParts = 24, maxPartChars = 4e4 } = {}) {
|
|
1829
|
+
if (!chunks?.length) return [];
|
|
1830
|
+
const total = Math.max(1, chunks.reduce((sum, chunk) => sum + chunk.text.length, 0));
|
|
1831
|
+
const count = Math.min(maxParts, Math.max(1, Math.ceil(total / partChars)));
|
|
1832
|
+
const parts = Array.from({ length: count }, () => []);
|
|
1833
|
+
let seen = 0;
|
|
1834
|
+
for (const chunk of chunks) {
|
|
1835
|
+
parts[Math.min(count - 1, Math.floor(seen / total * count))].push(chunk);
|
|
1836
|
+
seen += chunk.text.length;
|
|
1837
|
+
}
|
|
1838
|
+
return parts.filter((part) => part.length).map((part) => {
|
|
1839
|
+
return part.reduce((sum, chunk) => sum + chunk.text.length, 0) > maxPartChars ? spreadPassages(part, {
|
|
1840
|
+
maxChars: maxPartChars,
|
|
1841
|
+
samples: 24
|
|
1842
|
+
}) : part;
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
function locationOf(passage) {
|
|
1846
|
+
return passage?.page ? `p.${passage.page}` : passage?.slide ? `slide ${passage.slide}` : passage?.section ?? "";
|
|
1847
|
+
}
|
|
1848
|
+
function passageRange(passages) {
|
|
1849
|
+
const first = locationOf(passages?.[0]);
|
|
1850
|
+
const last = locationOf(passages?.at(-1));
|
|
1851
|
+
return first && last && first !== last ? `${first}–${last}` : first || last;
|
|
1852
|
+
}
|
|
1853
|
+
function formatPassages(passages, sourceTitle) {
|
|
1854
|
+
return passages.map((passage) => {
|
|
1855
|
+
const where = locationOf(passage);
|
|
1856
|
+
return `<<${[sourceTitle ?? passage.source ?? "source", where].filter(Boolean).join(" · ") || "excerpt"}>>\n${passage.text}`;
|
|
1857
|
+
}).join("\n\n");
|
|
1858
|
+
}
|
|
1859
|
+
function snippetOf(text, query, { width = 140 } = {}) {
|
|
1860
|
+
const value = String(text ?? "").replace(/\*\*|__|`/gu, "").replace(/^#{1,6}\s+/gmu, "").replace(/\s*\|\s*(?:\|\s*)*/gu, " · ").replace(/\s+/gu, " ").replace(/^ · | · $/gu, "").trim();
|
|
1861
|
+
if (value.length <= width) return value;
|
|
1862
|
+
const lower = value.toLowerCase();
|
|
1863
|
+
const terms = [...new Set(tokenize(query))];
|
|
1864
|
+
const hits = [];
|
|
1865
|
+
for (const term of terms) for (let at = lower.indexOf(term); at !== -1 && hits.length < 200; at = lower.indexOf(term, at + term.length)) hits.push(at);
|
|
1866
|
+
if (!hits.length) return `${value.slice(0, width)}…`;
|
|
1867
|
+
hits.sort((a, b) => a - b);
|
|
1868
|
+
let best = hits[0];
|
|
1869
|
+
let bestCount = 0;
|
|
1870
|
+
for (const start of hits) {
|
|
1871
|
+
const count = hits.filter((at) => at >= start && at < start + width).length;
|
|
1872
|
+
if (count > bestCount) {
|
|
1873
|
+
best = start;
|
|
1874
|
+
bestCount = count;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const from = Math.max(0, Math.min(best - Math.round(width / 4), value.length - width));
|
|
1878
|
+
return `${from > 0 ? "…" : ""}${value.slice(from, from + width)}${from + width < value.length ? "…" : ""}`;
|
|
1879
|
+
}
|
|
1880
|
+
//#endregion
|
|
1665
1881
|
//#region src/core/mastery.js
|
|
1666
1882
|
const BKT_DEFAULTS = Object.freeze({
|
|
1667
1883
|
pInit: .2,
|
|
@@ -1866,152 +2082,9 @@ function summarizeForPrompt(states, limit = 12) {
|
|
|
1866
2082
|
}));
|
|
1867
2083
|
}
|
|
1868
2084
|
//#endregion
|
|
1869
|
-
//#region src/core/retrieval.js
|
|
1870
|
-
const CJK_RUN = /[-ヿ㐀-䶿一-鿿가-]+/gu;
|
|
1871
|
-
const WORD = /[\p{L}\p{N}][\p{L}\p{N}'_-]*/gu;
|
|
1872
|
-
const STOP = /* @__PURE__ */ new Set([
|
|
1873
|
-
"the",
|
|
1874
|
-
"a",
|
|
1875
|
-
"an",
|
|
1876
|
-
"and",
|
|
1877
|
-
"or",
|
|
1878
|
-
"of",
|
|
1879
|
-
"to",
|
|
1880
|
-
"in",
|
|
1881
|
-
"on",
|
|
1882
|
-
"for",
|
|
1883
|
-
"is",
|
|
1884
|
-
"are",
|
|
1885
|
-
"was",
|
|
1886
|
-
"be",
|
|
1887
|
-
"with",
|
|
1888
|
-
"as",
|
|
1889
|
-
"by",
|
|
1890
|
-
"at",
|
|
1891
|
-
"it",
|
|
1892
|
-
"this",
|
|
1893
|
-
"that",
|
|
1894
|
-
"from"
|
|
1895
|
-
]);
|
|
1896
|
-
function tokenize(value) {
|
|
1897
|
-
const textValue = String(value ?? "").toLowerCase();
|
|
1898
|
-
const tokens = [];
|
|
1899
|
-
for (const run of textValue.match(CJK_RUN) ?? []) {
|
|
1900
|
-
if (run.length === 1) tokens.push(run);
|
|
1901
|
-
for (let index = 0; index < run.length - 1; index += 1) tokens.push(run.slice(index, index + 2));
|
|
1902
|
-
}
|
|
1903
|
-
for (const word of textValue.replace(CJK_RUN, " ").match(WORD) ?? []) if (word.length > 1 && !STOP.has(word)) tokens.push(word);
|
|
1904
|
-
return tokens;
|
|
1905
|
-
}
|
|
1906
|
-
function buildIndex(chunks) {
|
|
1907
|
-
const docs = chunks.map((chunk) => {
|
|
1908
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1909
|
-
const tokens = tokenize(chunk.text);
|
|
1910
|
-
for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
1911
|
-
return {
|
|
1912
|
-
chunk,
|
|
1913
|
-
counts,
|
|
1914
|
-
length: tokens.length
|
|
1915
|
-
};
|
|
1916
|
-
});
|
|
1917
|
-
const df = /* @__PURE__ */ new Map();
|
|
1918
|
-
for (const doc of docs) for (const token of doc.counts.keys()) df.set(token, (df.get(token) ?? 0) + 1);
|
|
1919
|
-
return {
|
|
1920
|
-
docs,
|
|
1921
|
-
df,
|
|
1922
|
-
avg: docs.reduce((sum, doc) => sum + doc.length, 0) / Math.max(1, docs.length)
|
|
1923
|
-
};
|
|
1924
|
-
}
|
|
1925
|
-
function search(index, query, { limit = 6, k1 = 1.4, b = .75 } = {}) {
|
|
1926
|
-
const terms = [...new Set(tokenize(query))];
|
|
1927
|
-
if (!terms.length || !index.docs.length) return [];
|
|
1928
|
-
const total = index.docs.length;
|
|
1929
|
-
return index.docs.map((doc) => {
|
|
1930
|
-
let score = 0;
|
|
1931
|
-
for (const term of terms) {
|
|
1932
|
-
const tf = doc.counts.get(term);
|
|
1933
|
-
if (!tf) continue;
|
|
1934
|
-
const df = index.df.get(term) ?? 0;
|
|
1935
|
-
const idf = Math.log(1 + (total - df + .5) / (df + .5));
|
|
1936
|
-
score += idf * (tf * (k1 + 1) / (tf + k1 * (1 - b + b * (doc.length / (index.avg || 1)))));
|
|
1937
|
-
}
|
|
1938
|
-
return {
|
|
1939
|
-
chunk: doc.chunk,
|
|
1940
|
-
score
|
|
1941
|
-
};
|
|
1942
|
-
}).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1943
|
-
}
|
|
1944
|
-
function selectPassages(chunks, query, { maxChars = 12e3, limit = 12 } = {}) {
|
|
1945
|
-
if (!chunks?.length) return [];
|
|
1946
|
-
const picked = query ? search(buildIndex(chunks), query, { limit }).map((entry) => entry.chunk) : chunks.slice(0, limit);
|
|
1947
|
-
const result = [];
|
|
1948
|
-
let used = 0;
|
|
1949
|
-
for (const chunk of picked) {
|
|
1950
|
-
if (used + chunk.text.length > maxChars) {
|
|
1951
|
-
const room = maxChars - used;
|
|
1952
|
-
if (room > 400) result.push({
|
|
1953
|
-
...chunk,
|
|
1954
|
-
text: `${chunk.text.slice(0, room)}…`
|
|
1955
|
-
});
|
|
1956
|
-
break;
|
|
1957
|
-
}
|
|
1958
|
-
result.push(chunk);
|
|
1959
|
-
used += chunk.text.length;
|
|
1960
|
-
}
|
|
1961
|
-
return result;
|
|
1962
|
-
}
|
|
1963
|
-
function spreadPassages(chunks, { maxChars = 12e3, samples = 16 } = {}) {
|
|
1964
|
-
if (!chunks?.length) return [];
|
|
1965
|
-
if (chunks.reduce((sum, chunk) => sum + chunk.text.length, 0) <= maxChars) return chunks.slice();
|
|
1966
|
-
const count = Math.min(chunks.length, Math.max(2, samples));
|
|
1967
|
-
const share = Math.max(200, Math.floor(maxChars / count));
|
|
1968
|
-
const picked = [];
|
|
1969
|
-
for (let index = 0; index < count; index += 1) {
|
|
1970
|
-
const at = Math.round(index * (chunks.length - 1) / (count - 1));
|
|
1971
|
-
if (picked.at(-1) !== at) picked.push(at);
|
|
1972
|
-
}
|
|
1973
|
-
return picked.map((at) => {
|
|
1974
|
-
const chunk = chunks[at];
|
|
1975
|
-
return chunk.text.length > share ? {
|
|
1976
|
-
...chunk,
|
|
1977
|
-
text: `${chunk.text.slice(0, share)}…`
|
|
1978
|
-
} : chunk;
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1981
|
-
function documentParts(chunks, { partChars = 2e4, maxParts = 24, maxPartChars = 4e4 } = {}) {
|
|
1982
|
-
if (!chunks?.length) return [];
|
|
1983
|
-
const total = Math.max(1, chunks.reduce((sum, chunk) => sum + chunk.text.length, 0));
|
|
1984
|
-
const count = Math.min(maxParts, Math.max(1, Math.ceil(total / partChars)));
|
|
1985
|
-
const parts = Array.from({ length: count }, () => []);
|
|
1986
|
-
let seen = 0;
|
|
1987
|
-
for (const chunk of chunks) {
|
|
1988
|
-
parts[Math.min(count - 1, Math.floor(seen / total * count))].push(chunk);
|
|
1989
|
-
seen += chunk.text.length;
|
|
1990
|
-
}
|
|
1991
|
-
return parts.filter((part) => part.length).map((part) => {
|
|
1992
|
-
return part.reduce((sum, chunk) => sum + chunk.text.length, 0) > maxPartChars ? spreadPassages(part, {
|
|
1993
|
-
maxChars: maxPartChars,
|
|
1994
|
-
samples: 24
|
|
1995
|
-
}) : part;
|
|
1996
|
-
});
|
|
1997
|
-
}
|
|
1998
|
-
function locationOf(passage) {
|
|
1999
|
-
return passage?.page ? `p.${passage.page}` : passage?.slide ? `slide ${passage.slide}` : passage?.section ?? "";
|
|
2000
|
-
}
|
|
2001
|
-
function passageRange(passages) {
|
|
2002
|
-
const first = locationOf(passages?.[0]);
|
|
2003
|
-
const last = locationOf(passages?.at(-1));
|
|
2004
|
-
return first && last && first !== last ? `${first}–${last}` : first || last;
|
|
2005
|
-
}
|
|
2006
|
-
function formatPassages(passages, sourceTitle) {
|
|
2007
|
-
return passages.map((passage) => {
|
|
2008
|
-
const where = locationOf(passage);
|
|
2009
|
-
return `<<${[sourceTitle ?? passage.source ?? "source", where].filter(Boolean).join(" · ") || "excerpt"}>>\n${passage.text}`;
|
|
2010
|
-
}).join("\n\n");
|
|
2011
|
-
}
|
|
2012
|
-
//#endregion
|
|
2013
2085
|
//#region src/host/context.js
|
|
2014
2086
|
const EMPTY_PROFILE = Object.freeze({
|
|
2087
|
+
name: "",
|
|
2015
2088
|
background: "",
|
|
2016
2089
|
goals: [],
|
|
2017
2090
|
strengths: [],
|
|
@@ -2056,7 +2129,7 @@ function masteryFor(course, progress, lessonId) {
|
|
|
2056
2129
|
}
|
|
2057
2130
|
async function readMaterials(store, materialIds = []) {
|
|
2058
2131
|
const items = [];
|
|
2059
|
-
for (const id of materialIds.slice(0,
|
|
2132
|
+
for (const id of materialIds.slice(0, 12)) try {
|
|
2060
2133
|
const meta = await store.readJson(store.paths.material(id), null);
|
|
2061
2134
|
if (!meta) continue;
|
|
2062
2135
|
const chunks = meta.kind === "image" ? [] : await store.readJson(store.paths.materialChunks(id), []) ?? [];
|
|
@@ -2323,6 +2396,7 @@ function join(...parts) {
|
|
|
2323
2396
|
}
|
|
2324
2397
|
function learnerBlock({ profile, mastery } = {}) {
|
|
2325
2398
|
const lines = [];
|
|
2399
|
+
if (profile?.name) lines.push(`The learner likes to be called: ${profile.name} (use it sparingly, as a friendly tutor would)`);
|
|
2326
2400
|
if (profile?.background) lines.push(`Background: ${profile.background}`);
|
|
2327
2401
|
if (profile?.goals?.length) lines.push(`Goals: ${profile.goals.join("; ")}`);
|
|
2328
2402
|
if (profile?.preferences?.style) lines.push(`Prefers explanations that are: ${profile.preferences.style}`);
|
|
@@ -2414,10 +2488,11 @@ function blueprintPrompt({ goal, language, intake, materialsSummary, profile, ti
|
|
|
2414
2488
|
- If materials were provided, follow their scope and order where sensible.
|
|
2415
2489
|
- If web sources are given, use them for scope, ordering and current practice, and prefer what several of them agree on.
|
|
2416
2490
|
- Each lesson lists 1–3 learning objectives (observable: "compute…", "explain why…") and 1–4 key concepts (short names).
|
|
2491
|
+
- Give each lesson a "depth": "intuition" (a feel for the idea through pictures and cases), "definition" (stating it precisely), "derivation" (deriving or proving it), "application" (using it on problems) or "extension" (edge cases, generalizations, connections). Let ideas be felt before they are formalized.
|
|
2417
2492
|
- Add 1–3 projects (one is enough for a short course) that apply the course to something real for this learner, placed after the units they depend on. Use "kind":"code" only when the project is mainly programming or data work.
|
|
2418
2493
|
- Pick one emoji that fits the subject.
|
|
2419
2494
|
|
|
2420
|
-
Return {"title":"…","subtitle":"one line promise of the course","emoji":"…","summary":"2–3 sentences: who it is for, what they will be able to do","level":"beginner|intermediate|advanced","units":[{"title":"…","summary":"…","objectives":["…"],"lectures":[{"title":"…","summary":"…","lessons":[{"title":"…","summary":"…","objectives":["…"],"concepts":["…"],"estMinutes":15}]}]}],"projects":[{"title":"…","kind":"code|writing|analysis|design|practice|other","brief":"…","skills":["…"],"unitIndex":<0-based unit after which it fits>,"milestones":["…"],"rubric":[{"criterion":"…","points":10}]}]}`)
|
|
2495
|
+
Return {"title":"…","subtitle":"one line promise of the course","emoji":"…","summary":"2–3 sentences: who it is for, what they will be able to do","level":"beginner|intermediate|advanced","units":[{"title":"…","summary":"…","objectives":["…"],"lectures":[{"title":"…","summary":"…","lessons":[{"title":"…","summary":"…","objectives":["…"],"concepts":["…"],"estMinutes":15,"depth":"intuition|definition|derivation|application|extension"}]}]}],"projects":[{"title":"…","kind":"code|writing|analysis|design|practice|other","brief":"…","skills":["…"],"unitIndex":<0-based unit after which it fits>,"milestones":["…"],"rubric":[{"criterion":"…","points":10}]}]}`)
|
|
2421
2496
|
};
|
|
2422
2497
|
}
|
|
2423
2498
|
function revisePrompt({ blueprint, instruction, language }) {
|
|
@@ -2427,10 +2502,16 @@ function revisePrompt({ blueprint, instruction, language }) {
|
|
|
2427
2502
|
};
|
|
2428
2503
|
}
|
|
2429
2504
|
const ITEM_FIGURE = "When a figure makes a question clearer (a shape to measure, a graph to read, a circuit, a diagram to label), give the item \"svg\": a small diagram following the same rules as the board's svg op (viewBox at most 480×320). For the graph of a formula give \"plot\" instead: an object shaped like the board's plot op, without \"params\". Most items need no figure.";
|
|
2505
|
+
function materialsRevisePrompt({ blueprint, materials, passages, instruction, language }) {
|
|
2506
|
+
return {
|
|
2507
|
+
system: join(PERSONA, `You revise course blueprints to make good use of materials the learner has added, keeping everything else intact. ${languageRule(language)}`, JSON_ONLY),
|
|
2508
|
+
user: join(block("current_blueprint", blueprint), block("new_materials", materials), block("material_passages", passages), block("learner_request", instruction), `The learner added these materials to the course. Revise the blueprint so the course uses them: add lessons, or a unit, for what they cover that the course lacks, placed where the prerequisites allow; take existing lessons deeper where the materials go further; follow any request above. Keep everything else as it is, and remove a lesson only if the materials show it is wrong. Keep the "id" of every unit, lecture, lesson and concept you do not substantially change; new items get no id. Give every new lesson a "depth". Return the complete revised blueprint in the same JSON shape.`)
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2430
2511
|
function reworkPrompt({ outline, unitTitle, lectureTitle, lesson, mode, language }) {
|
|
2431
2512
|
return {
|
|
2432
2513
|
system: join(PERSONA, `You design personal courses with the care of an experienced curriculum designer: clear prerequisites first, one new idea per lesson. ${languageRule(language)}`, JSON_ONLY),
|
|
2433
|
-
user: join(block("course_outline", outline), block("position", `Unit: ${unitTitle}\nLecture: ${lectureTitle}`), block("lesson", lesson), mode === "split" ? "Split this lesson into two consecutive lessons that together cover everything it covers. Each teaches one coherent idea in 10–20 minutes, and the first prepares the second. Stay consistent with the rest of the course." : "Rewrite this lesson to go deeper. Keep its topic and its place in the course, but aim at why it works, its edge cases and its connections to the rest of the course; adjust the objectives, concepts and minutes (at most 25) to match.", "Return {\"lessons\":[{\"title\":\"…\",\"summary\":\"…\",\"objectives\":[\"…\"],\"concepts\":[\"…\"],\"estMinutes\":15}]}")
|
|
2514
|
+
user: join(block("course_outline", outline), block("position", `Unit: ${unitTitle}\nLecture: ${lectureTitle}`), block("lesson", lesson), mode === "split" ? "Split this lesson into two consecutive lessons that together cover everything it covers. Each teaches one coherent idea in 10–20 minutes, and the first prepares the second. Stay consistent with the rest of the course." : "Rewrite this lesson to go deeper. Keep its topic and its place in the course, but aim at why it works, its edge cases and its connections to the rest of the course; adjust the objectives, concepts and minutes (at most 25) to match.", "Return {\"lessons\":[{\"title\":\"…\",\"summary\":\"…\",\"objectives\":[\"…\"],\"concepts\":[\"…\"],\"estMinutes\":15,\"depth\":\"intuition|definition|derivation|application|extension\"}]}")
|
|
2434
2515
|
};
|
|
2435
2516
|
}
|
|
2436
2517
|
function practicePrompt({ course, lecture, lessons, language, passages }) {
|
|
@@ -2460,12 +2541,19 @@ const DIRECTIVES = Object.freeze({
|
|
|
2460
2541
|
challenge: "The learner has mastered the prerequisites easily: move briskly through basics, add depth (why it works, edge cases, connections) and make the checks harder.",
|
|
2461
2542
|
review: "This is a REVIEW lesson: retrieve before re-explaining. Open with a question, then consolidate the key ideas with a compact board and mixed checks."
|
|
2462
2543
|
});
|
|
2544
|
+
const DEPTH_GUIDANCE = Object.freeze({
|
|
2545
|
+
intuition: "This lesson builds intuition: lead with pictures, analogies and concrete cases, keep the formalism light, and leave proofs for later lessons.",
|
|
2546
|
+
definition: "This lesson makes the idea precise: state the definitions and notation carefully, with examples and non-examples.",
|
|
2547
|
+
derivation: "This lesson derives or proves the result: work every step on the board and say why each step holds.",
|
|
2548
|
+
application: "This lesson applies the idea: work through varied problems, from routine to less familiar, and show how to choose the method.",
|
|
2549
|
+
extension: "This lesson goes further: edge cases, generalizations and connections to other topics."
|
|
2550
|
+
});
|
|
2463
2551
|
function lessonPrompt({ course, unit, lecture, lesson, language, learner, passages, previous, directive, style }) {
|
|
2464
2552
|
const steps = Math.min(18, Math.max(7, Math.round((lesson.estMinutes || 15) * .7)));
|
|
2465
2553
|
return {
|
|
2466
2554
|
system: join(PERSONA, BOARD_SPEC, STEP_SPEC, languageRule(language), JSONL_ONLY),
|
|
2467
|
-
user: join(block("course", `${course.title} (${course.level || "mixed level"}) — ${course.summary ?? ""}`), block("position", `Unit: ${unit.title}\nLecture: ${lecture.title}\nLesson: ${lesson.title}\nObjectives: ${lesson.objectives.join("; ")}\nConcepts (id=name): ${lesson.concepts.map((concept) => `${concept.id}=${concept.name}`).join(", ")}`), block("previous_lesson", previous), learnerBlock(learner), block("source_passages", passages), style ? block("style_request", style) : "", DIRECTIVES[directive] ?? "", `Teach this lesson in about ${steps} steps (~${lesson.estMinutes || 15} minutes).
|
|
2468
|
-
- Open with a hook: a question, a surprising fact, or a concrete situation the idea explains. Connect to the previous lesson when there is one.
|
|
2555
|
+
user: join(block("course", `${course.title} (${course.level || "mixed level"}) — ${course.summary ?? ""}`), block("position", `Unit: ${unit.title}\nLecture: ${lecture.title}\nLesson: ${lesson.title}\nObjectives: ${lesson.objectives.join("; ")}\nConcepts (id=name): ${lesson.concepts.map((concept) => `${concept.id}=${concept.name}`).join(", ")}`), block("previous_lesson", previous), learnerBlock(learner), block("source_passages", passages), style ? block("style_request", style) : "", DIRECTIVES[directive] ?? "", DEPTH_GUIDANCE[lesson.depth] ?? "", `Teach this lesson in about ${steps} steps (~${lesson.estMinutes || 15} minutes).
|
|
2556
|
+
- Open with a hook: a question, a surprising fact, or a concrete situation the idea explains. Connect to the previous lesson when there is one.${learner?.profile?.name ? " Call the learner by name once, in the opening step." : ""}
|
|
2469
2557
|
- Build intuition before formalism; then make it precise; then work at least one example fully on the board.
|
|
2470
2558
|
- Draw a diagram, chart or plot whenever a picture makes the idea click; a plot with a slider is often the clearest way to show how a formula behaves.
|
|
2471
2559
|
- Put 2–4 checks after the key ideas, plus one at the end; every check must name its conceptIds.
|
|
@@ -2515,6 +2603,12 @@ function correctionPrompt({ course, lesson, language, boardSoFar, item, response
|
|
|
2515
2603
|
user: join(block("course", course ? course.title : ""), block("lesson", lesson ? `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}` : ""), block("board_so_far", boardSoFar), block("problem", item.prompt), block("model_working", item.answer), block("learner_working", response), block("grader_notes", notes), "The learner just wrote out their working for this problem and went wrong somewhere. Correct it at the board in 2–3 steps, continuing on the current board: (1) write the learner's wrong line as they wrote it, in an op with an \"id\", then {\"t\":\"mark\",\"ref\":\"<that id>\",\"style\":\"box\"}, and say kindly what went wrong there and why; (2) write the corrected line and carry the working on to the right answer; (3) end with a short {\"t\":\"note\",\"tone\":\"key\",\"text\":\"…\"} holding the rule to remember. Speak to the learner directly and keep it brief. Add no check, and do not continue the lesson itself.")
|
|
2516
2604
|
};
|
|
2517
2605
|
}
|
|
2606
|
+
function handoutPrompt({ course, lesson, language, board }) {
|
|
2607
|
+
return {
|
|
2608
|
+
system: join(PERSONA, `You turn a lesson into a one-page handout a learner keeps: faithful to what was taught, short, exact. ${languageRule(language)}`, JSON_ONLY),
|
|
2609
|
+
user: join(block("course", course ? course.title : ""), block("lesson", `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}`), block("what_the_board_taught", board), "Write the handout from what the board taught; add nothing it did not teach. \"points\": the 3–6 key points in order, each with a \"oneLine\" a learner could repeat to a friend. \"example\": one worked example with numbers, solved step by step (one step per line). \"mistakes\": 2–4 common mistakes with the fix. \"glossary\": the terms the lesson introduced. \"questions\": 3 short questions to check oneself, with answers. Use inline math $…$ for formulas.\n\nReturn {\"title\":\"…\",\"summary\":\"2–3 sentences: what this lesson was about\",\"points\":[{\"title\":\"…\",\"oneLine\":\"…\",\"detail\":\"…\"}],\"example\":{\"problem\":\"…\",\"steps\":[\"…\"],\"answer\":\"…\"},\"mistakes\":[{\"mistake\":\"…\",\"fix\":\"…\"}],\"glossary\":[{\"term\":\"…\",\"meaning\":\"…\"}],\"questions\":[{\"question\":\"…\",\"answer\":\"…\"}]}")
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2518
2612
|
function verifyStepPrompt({ course, lesson, language, board, step, stepId, note }) {
|
|
2519
2613
|
return {
|
|
2520
2614
|
system: join(PERSONA, `The learner thinks a step of your lesson is wrong. Check it the way a careful examiner would: recompute every number, re-derive every formula, and check every fact, unit, sign, label, graph and answer key. Never defend a mistake and never invent one; a different but valid method or notation is not a mistake. ${languageRule(language)}`, JSON_ONLY),
|
|
@@ -2647,4 +2741,4 @@ function getRuntime() {
|
|
|
2647
2741
|
return runtime;
|
|
2648
2742
|
}
|
|
2649
2743
|
//#endregion
|
|
2650
|
-
export {
|
|
2744
|
+
export { passageRange as $, verifyItemPrompt as A, plotToText as At, materialImages as B, quizPrompt as C, normalizeLesson as Ct, reworkPrompt as D, outlineForPrompt as Dt, revisePrompt as E, normalizeQuestionnaire as Et, generateText as F, readProgress as G, passagesFor as H, streamCounter as I, masteryPercent as J, applyEvidence as K, EMPTY_PROFILE as L, TutorError as M, newId as Mt, generateJson as N, shortPrompt as O, safeRelativePath as Ot, generateSteps as P, formatPassages as Q, emptyProgress as R, questionnairePrompt as S, normalizeItem as St, researchQueriesPrompt as T, normalizeProject as Tt, readMaterials as U, materialsSummary as V, readProfile as W, buildIndex as X, recommendNext as Y, documentParts as Z, materialsRevisePrompt as _, nextLesson as _t, blueprintPrompt as a, boardToPlainText as at, projectReviewPrompt as b, normalizeCheatsheet as bt, correctionPrompt as c, normalizeOp as ct, digestPrompt as d, courseProgress as dt, search as et, flashcardsPrompt as f, findLesson as ft, materialBriefPrompt as g, lessonAfter as gt, lessonPrompt as h, intakeSummary as ht, assistPrompt as i, BOARD_LIMITS as it, verifyStepPrompt as j, assertId as jt, summaryPrompt as k, text as kt, detectLanguage as l, blueprintOutline as lt, handoutPrompt as m, insertLesson as mt, publishRuntime as n, spreadPassages as nt, challengePrompt as o, estimateSpeechMs as ot, gradePrompt as p, flattenLessons as pt, createConceptState as q, askPrompt as r, extractJson as rt, cheatsheetPrompt as s, normalizeBoard as st, getRuntime as t, snippetOf as tt, digestPartPrompt as u, conceptIndex as ut, practicePrompt as v, normalizeBlueprint as vt, reexplainPrompt as w, normalizePractice as wt, projectsPrompt as x, normalizeHandout as xt, profilePrompt as y, normalizeCards as yt, masteryFor as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-aha-tutor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "开窍 Aha Tutor: a personal AI tutor for DeepSeek Harness that builds a course around you and teaches it on a narrated handwriting whiteboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|