@hviana/sema 0.5.8 → 0.6.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.
Files changed (108) hide show
  1. package/AGENTS.md +43 -4
  2. package/DATASETS.md +160 -0
  3. package/README.md +12 -0
  4. package/dist/example/train_base/cache.d.ts +35 -0
  5. package/dist/example/train_base/cache.js +211 -0
  6. package/dist/example/train_base/config.d.ts +21 -0
  7. package/dist/example/train_base/config.js +94 -0
  8. package/dist/example/train_base/corpora/aya.d.ts +19 -0
  9. package/dist/example/train_base/corpora/aya.js +76 -0
  10. package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
  11. package/dist/example/train_base/corpora/converted-parquet.js +44 -0
  12. package/dist/example/train_base/corpora/genknow.d.ts +14 -0
  13. package/dist/example/train_base/corpora/genknow.js +83 -0
  14. package/dist/example/train_base/corpora/index.d.ts +29 -0
  15. package/dist/example/train_base/corpora/index.js +81 -0
  16. package/dist/example/train_base/corpora/massive.d.ts +7 -0
  17. package/dist/example/train_base/corpora/massive.js +98 -0
  18. package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
  19. package/dist/example/train_base/corpora/oasst2.js +120 -0
  20. package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
  21. package/dist/example/train_base/corpora/smolsent.js +156 -0
  22. package/dist/example/train_base/corpora/soda.d.ts +12 -0
  23. package/dist/example/train_base/corpora/soda.js +113 -0
  24. package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
  25. package/dist/example/train_base/corpora/taskmaster.js +144 -0
  26. package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
  27. package/dist/example/train_base/corpora/wiki2.js +132 -0
  28. package/dist/example/train_base/corpus.d.ts +88 -0
  29. package/dist/example/train_base/corpus.js +65 -0
  30. package/dist/example/train_base/discovery.d.ts +48 -0
  31. package/dist/example/train_base/discovery.js +143 -0
  32. package/dist/example/train_base/http.d.ts +82 -0
  33. package/dist/example/train_base/http.js +219 -0
  34. package/dist/example/train_base/items.d.ts +46 -0
  35. package/dist/example/train_base/items.js +98 -0
  36. package/dist/example/train_base/main.d.ts +4 -0
  37. package/dist/example/train_base/main.js +207 -0
  38. package/dist/example/train_base/progress.d.ts +34 -0
  39. package/dist/example/train_base/progress.js +114 -0
  40. package/dist/example/train_base/readers.d.ts +125 -0
  41. package/dist/example/train_base/readers.js +391 -0
  42. package/dist/example/train_base/runtime.d.ts +115 -0
  43. package/dist/example/train_base/runtime.js +637 -0
  44. package/dist/example/train_base/stage.d.ts +3 -0
  45. package/dist/example/train_base/stage.js +246 -0
  46. package/dist/example/train_base/ui.d.ts +88 -0
  47. package/dist/example/train_base/ui.js +272 -0
  48. package/dist/src/geometry.d.ts +20 -0
  49. package/dist/src/geometry.js +22 -0
  50. package/dist/src/mind/attention.d.ts +6 -0
  51. package/dist/src/mind/attention.js +44 -4
  52. package/dist/src/mind/learning.js +134 -50
  53. package/dist/src/mind/mechanisms/cast.js +45 -1
  54. package/dist/src/mind/mind.d.ts +7 -2
  55. package/dist/src/mind/mind.js +15 -3
  56. package/dist/src/mind/reasoning.js +59 -5
  57. package/dist/src/mind/recognition.js +29 -3
  58. package/dist/src/mind/traverse.d.ts +16 -0
  59. package/dist/src/mind/traverse.js +18 -0
  60. package/dist/src/store-sqlite.d.ts +4 -0
  61. package/dist/src/store-sqlite.js +47 -0
  62. package/dist/src/store.d.ts +7 -0
  63. package/example/train_base/cache.ts +251 -0
  64. package/example/train_base/config.ts +128 -0
  65. package/example/train_base/corpora/aya.ts +106 -0
  66. package/example/train_base/corpora/converted-parquet.ts +64 -0
  67. package/example/train_base/corpora/genknow.ts +114 -0
  68. package/example/train_base/corpora/index.ts +88 -0
  69. package/example/train_base/corpora/massive.ts +111 -0
  70. package/example/train_base/corpora/oasst2.ts +163 -0
  71. package/example/train_base/corpora/smolsent.ts +203 -0
  72. package/example/train_base/corpora/soda.ts +130 -0
  73. package/example/train_base/corpora/taskmaster.ts +217 -0
  74. package/example/train_base/corpora/wiki2.ts +190 -0
  75. package/example/train_base/corpus.ts +150 -0
  76. package/example/train_base/discovery.ts +203 -0
  77. package/example/train_base/http.ts +284 -0
  78. package/example/train_base/items.ts +118 -0
  79. package/example/train_base/main.ts +240 -0
  80. package/example/train_base/progress.ts +149 -0
  81. package/example/train_base/readers.ts +505 -0
  82. package/example/train_base/runtime.ts +894 -0
  83. package/example/train_base/stage.ts +276 -0
  84. package/example/train_base/ui.ts +333 -0
  85. package/jsr.json +1 -1
  86. package/package.json +2 -4
  87. package/src/geometry.ts +23 -0
  88. package/src/mind/attention.ts +54 -1
  89. package/src/mind/learning.ts +137 -43
  90. package/src/mind/mechanisms/cast.ts +48 -1
  91. package/src/mind/mind.ts +13 -2
  92. package/src/mind/reasoning.ts +64 -5
  93. package/src/mind/recognition.ts +29 -3
  94. package/src/mind/traverse.ts +19 -0
  95. package/src/store-sqlite.ts +53 -0
  96. package/src/store.ts +28 -0
  97. package/test/13-conversation.test.mjs +1 -1
  98. package/test/29-counterfactual.test.mjs +43 -6
  99. package/test/77-company-saturation.test.mjs +302 -0
  100. package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
  101. package/test/84-composed-answer-honesty.test.mjs +137 -0
  102. package/test/85-answered-directly.test.mjs +126 -0
  103. package/test/86-cast-voices-committed.test.mjs +164 -0
  104. package/test/87-codominant-commitment.test.mjs +250 -0
  105. package/test/88-dependency-footprint.test.mjs +99 -0
  106. package/dist/example/train_base.d.ts +0 -93
  107. package/dist/example/train_base.js +0 -2269
  108. package/example/train_base.ts +0 -2735
@@ -0,0 +1,203 @@
1
+ // train_base/corpora/smolsent.ts — google/smol sentence pairs
2
+ //
3
+ // Knobs, the row adapter, and the stage descriptor for ONE corpus, together
4
+ // with the evidence that fixed each default. A corpus file owns everything
5
+ // source-specific; the contract it fills is ../corpus.ts and the loop that runs
6
+ // it is ../stage.ts.
7
+
8
+ import { env, LOCAL_PATH } from "../config.js";
9
+ import { refineItems, type TrainingItem } from "../items.js";
10
+ import { hfTree, type Listed, localFiles } from "../discovery.js";
11
+ import { lines } from "../readers.js";
12
+ import type { Corpus, Unit } from "../corpus.js";
13
+ import type { TrainCtx } from "../runtime.js";
14
+ import { basename, join } from "node:path";
15
+
16
+ // ── google/smol · SmolSent (the first training stage) ──
17
+ // SmolSent is Google's sentence-level translation set: ~863 human sentence pairs
18
+ // per language pair across 100+ low-resource languages, cc-by-4.0 (commercial-
19
+ // friendly). Each row is {sl, tl, src, trg, …} — a source sentence and its
20
+ // translation. A pair is "two names for one meaning", which is exactly the
21
+ // cross-language concept SEMA fuses (see test/05-concepts.test.mjs), so each row
22
+ // becomes FACTS that bind the two phrasings as one concept at recall time.
23
+ //
24
+ // The corpus ships as one plain JSONL file PER language pair under smolsent/ in
25
+ // the HF repo (e.g. smolsent/ha_en.jsonl). We DOWNLOAD each file and stream its
26
+ // lines — far faster and free of the rate-limiting that per-row API paging hit.
27
+ // The file list is discovered from the HF repo tree. SMOLSENT=0 disables the
28
+ // stage; SMOLSENT_PAIRS (comma-separated basenames without .jsonl, e.g.
29
+ // "ha_en,zu_en") restricts to a chosen subset.
30
+ const SMOLSENT = env("SMOLSENT", "1") !== "0";
31
+ const SMOLSENT_DATASET = env("SMOLSENT_DATASET", "google/smol");
32
+ const SMOLSENT_PAIRS = (process.env.SMOLSENT_PAIRS ?? "")
33
+ .split(",").map((s) => s.trim()).filter(Boolean);
34
+ // The resume id PREFIX for the SmolSent stage; one completed-files entry per
35
+ // file (e.g. "smolsent::ha_en.jsonl").
36
+ const SMOLSENT_ID = "smolsent";
37
+ // Which direction(s) of a translation pair to deposit. Was effectively "both",
38
+ // and that is now the default NO longer, for a reason measured rather than
39
+ // assumed.
40
+ //
41
+ // SmolSent's English side is a SHARED POOL translated into every language: row
42
+ // id 0 of smolsent/ha_en.jsonl, zu_en.jsonl and am_en.jsonl all carry the SAME
43
+ // `trg` ("It allows me to work by following my vibes and ..."). The two
44
+ // directions are therefore not symmetric at all:
45
+ //
46
+ // src2trg (foreign -> English) many distinct contexts -> ONE shared
47
+ // continuation. Every language's rendering of
48
+ // a meaning converges on the same English
49
+ // node — the cross-language concept fusion
50
+ // this stage exists for.
51
+ // trg2src (English -> foreign) ONE context -> 100+ DIFFERENT continuations,
52
+ // one per language file. The same English
53
+ // sentence is deposited over and over with a
54
+ // different answer each time.
55
+ //
56
+ // So dropping trg2src is not merely a corpus-size economy (it halves the
57
+ // largest stage, which was 60.9% of all examples in the last trained store); it
58
+ // removes a genuine ambiguity pathology. Set SMOLSENT_DIRECTIONS=both to
59
+ // restore the old behaviour, or trg2src for English->foreign only.
60
+ //
61
+ // WHAT THE CUT DOES NOT DO, measured on a three-pair store: asking the English
62
+ // sentence still ANSWERS with a foreign rendering, because the engine can reach
63
+ // a shared continuation's predecessors on its own. What is removed is the
64
+ // DEPOSITED forward ambiguity — one context carrying ~100 competing
65
+ // continuations — not every reverse association.
66
+ const SMOLSENT_DIRECTIONS = env("SMOLSENT_DIRECTIONS", "src2trg")
67
+ .trim().toLowerCase();
68
+ const SMOLSENT_SRC2TRG = SMOLSENT_DIRECTIONS !== "trg2src";
69
+ const SMOLSENT_TRG2SRC = SMOLSENT_DIRECTIONS === "trg2src" ||
70
+ SMOLSENT_DIRECTIONS === "both";
71
+ // A SmolSent side longer than this is skipped (a sentence pair is short; a huge
72
+ // value is corruption, not a sentence).
73
+ const MAX_SMOLSENT_CHARS = Math.max(
74
+ 2_000,
75
+ Math.floor(Number(env("MAX_SMOLSENT_KB", "16")) * 1000) || 16_000,
76
+ );
77
+
78
+ // ═══════════════════════════════════════════════════════════════════════
79
+ // §6c SmolSent parsing — a translation pair → SEMA facts
80
+ //
81
+ // Each SmolSent row is {sl, tl, src, trg, …}: a source sentence and its
82
+ // translation into another language — "two names for one meaning". This is the
83
+ // cross-language concept SEMA fuses (see test/05-concepts.test.mjs: "ice" and
84
+ // "hielo" become one concept because they share company, so a fact about one
85
+ // transfers to the other). So a pair is rendered as BIDIRECTIONAL translation
86
+ // FACTS — (src → trg) and (trg → src) — binding the two phrasings as one
87
+ // concept at recall time, in both directions. Two facts, no experiences and no
88
+ // cumulative walk: a sentence pair is not multi-turn, and a bare sentence on its
89
+ // own carries no relation to point at.
90
+ // ═══════════════════════════════════════════════════════════════════════
91
+
92
+ /** One normalized SmolSent row. */
93
+ export interface SmolSentRow {
94
+ src: string; // source sentence
95
+ trg: string; // its translation
96
+ sl: string; // source language code
97
+ tl: string; // target language code
98
+ }
99
+
100
+ /** Normalize a raw datasets-server row into a SmolSentRow, or null when it lacks
101
+ * both sides or a side is implausibly large (a dump, not a sentence). */
102
+ export function toSmolSentRow(
103
+ row: unknown,
104
+ maxChars = MAX_SMOLSENT_CHARS,
105
+ ): SmolSentRow | null {
106
+ if (!row || typeof row !== "object") return null;
107
+ const r = row as Record<string, unknown>;
108
+ const src = typeof r.src === "string" ? r.src.trim() : "";
109
+ // `trg` is a single string in smolsent; tolerate a list form defensively.
110
+ const trgRaw = Array.isArray(r.trgs) ? r.trgs[0] : r.trg;
111
+ const trg = typeof trgRaw === "string" ? trgRaw.trim() : "";
112
+ if (!src || !trg) return null;
113
+ if (
114
+ src.length > maxChars || trg.length > maxChars
115
+ ) return null;
116
+ const sl = typeof r.sl === "string" ? r.sl.trim() : "";
117
+ const tl = typeof r.tl === "string" ? r.tl.trim() : "";
118
+ return { src, trg, sl, tl };
119
+ }
120
+
121
+ /** Translate ONE SmolSent pair into SEMA facts. The two sentences are one
122
+ * meaning in two languages, but the two BINDINGS are not equally sound —
123
+ * SmolSent's English side is a shared pool translated into every language, so
124
+ * `trg -> src` gives one English context a different answer in every language
125
+ * file. See SMOLSENT_DIRECTIONS. refineItems drops the degenerate case where
126
+ * src === trg. */
127
+ export function smolSentRowToItems(
128
+ row: SmolSentRow,
129
+ dirs: { src2trg: boolean; trg2src: boolean } = {
130
+ src2trg: SMOLSENT_SRC2TRG,
131
+ trg2src: SMOLSENT_TRG2SRC,
132
+ },
133
+ ): TrainingItem[] {
134
+ const { src, trg } = row;
135
+ const items: TrainingItem[] = [];
136
+ if (dirs.src2trg) items.push({ context: src, continuation: trg });
137
+ if (dirs.trg2src) items.push({ context: trg, continuation: src });
138
+ return refineItems(items);
139
+ }
140
+
141
+ /** Discover the SmolSent per-pair JSONL files from the HF repo tree, restricted
142
+ * to SMOLSENT_PAIRS (basenames without .jsonl) when set. Each entry is the
143
+ * repo-relative path, e.g. "smolsent/ha_en.jsonl". */
144
+ async function listFiles(ctx: TrainCtx): Promise<Listed[]> {
145
+ const paths = await hfTree(
146
+ SMOLSENT_DATASET,
147
+ "smolsent",
148
+ /\.jsonl$/i,
149
+ `GET smol tree`,
150
+ ctx.http,
151
+ );
152
+ if (!SMOLSENT_PAIRS.length) return paths;
153
+ const want = new Set(SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")));
154
+ return paths.filter((p) =>
155
+ want.has(basename(p.path).replace(/\.jsonl$/i, ""))
156
+ );
157
+ }
158
+
159
+ const unit = (name: string): Unit => ({
160
+ key: name,
161
+ // The log names a pair by its language code alone; the panel keeps the file.
162
+ name: name.replace(/\.jsonl$/i, ""),
163
+ display: `SmolSent ${name}`,
164
+ });
165
+
166
+ export const smolsent: Corpus = {
167
+ id: SMOLSENT_ID,
168
+ label: "SmolSent",
169
+ kind: "translation",
170
+ enabled: SMOLSENT,
171
+ unitNoun: "translation file(s)",
172
+ // The *4 is chars-to-a-generous-JSON-envelope: the guard bounds ONE side of
173
+ // a pair, the line carries both plus its keys.
174
+ read: lines({ maxLineChars: MAX_SMOLSENT_CHARS * 4 }),
175
+ toItems: (row) => {
176
+ const r = toSmolSentRow(row);
177
+ return r ? smolSentRowToItems(r) : null;
178
+ },
179
+ async discover(ctx) {
180
+ // Work-list: local *.jsonl in LOCAL_PATH, else the repo's smolsent/ files.
181
+ if (LOCAL_PATH) {
182
+ let names = localFiles(LOCAL_PATH, /\.jsonl$/i);
183
+ if (SMOLSENT_PAIRS.length) {
184
+ const want = new Set(
185
+ SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")),
186
+ );
187
+ names = names.filter((n) => want.has(n.path.replace(/\.jsonl$/i, "")));
188
+ }
189
+ return names.map((n) => ({
190
+ ...unit(n.path),
191
+ local: join(LOCAL_PATH, n.path),
192
+ bytes: n.size,
193
+ }));
194
+ }
195
+ return (await listFiles(ctx)).map(({ path, size }) => ({
196
+ ...unit(basename(path)),
197
+ // owner/name and the file path are URL PATH segments — do not encode "/".
198
+ url:
199
+ `https://huggingface.co/datasets/${SMOLSENT_DATASET}/resolve/main/${path}`,
200
+ bytes: size,
201
+ }));
202
+ },
203
+ };
@@ -0,0 +1,130 @@
1
+ // train_base/corpora/soda.ts — allenai/soda social dialogue
2
+ //
3
+ // Knobs, the row adapter, and the stage descriptor for ONE corpus, together
4
+ // with the evidence that fixed each default. A corpus file owns everything
5
+ // source-specific; the contract it fills is ../corpus.ts and the loop that runs
6
+ // it is ../stage.ts.
7
+
8
+ import { env } from "../config.js";
9
+ import {
10
+ accumulate,
11
+ mergeSpeakerTurns,
12
+ refineItems,
13
+ type TrainingItem,
14
+ } from "../items.js";
15
+ import { parquet } from "../readers.js";
16
+ import type { Corpus } from "../corpus.js";
17
+ import { convertedParquetUnits } from "./converted-parquet.js";
18
+ // SODA turns are the same shape as Taskmaster's, and merge by the same rule.
19
+ import type { TaskmasterTurn } from "./taskmaster.js";
20
+
21
+ // ── allenai/soda (social dialogue) and AmazonScience/massive (short intents) ──
22
+ // Both are read from Hugging Face's auto-converted `refs/convert/parquet`
23
+ // branch. For SODA that is mandatory, not cosmetic: its main-branch
24
+ // train.parquet is ONE 1,191,582-row group (1.19 GB uncompressed), and a
25
+ // Parquet column chunk is per-group, so any read of it materialises the whole
26
+ // file — measured at 100% of a 689 MB file and 2 GB of heap for a 500-row read.
27
+ // The converted branch uses uniform 10,000-row groups.
28
+ //
29
+ // BOTH STAGES ARE BUDGETED, and that is a curriculum decision rather than an
30
+ // algorithmic cap. SODA's train split holds 1,191,582 dialogues which the
31
+ // cumulative walk would turn into ~8 MILLION episodes — against the 662,221
32
+ // deposits of the entire current corpus. Trained whole it would not join the
33
+ // mix, it would BE the mix, and corpus size is the quantity every scale problem
34
+ // in this engine is measured against. The default takes the first
35
+ // SODA_MAX_DIALOGS of them; set it to 0 to lift the budget.
36
+ const SODA = env("SODA", "1") !== "0";
37
+ const SODA_DATASET = env("SODA_DATASET", "allenai/soda");
38
+ const SODA_SPLITS = env("SODA_SPLITS", "train")
39
+ .split(",").map((s) => s.trim()).filter(Boolean);
40
+ // ~6.3 episodes per dialogue, so this budgets ~750k episodes — comparable to
41
+ // the Taskmaster stage and to Aya, which is the intended balance. 0 = no budget.
42
+ const SODA_MAX_DIALOGS = Math.max(
43
+ 0,
44
+ Math.floor(Number(env("SODA_MAX_DIALOGS", "120000"))) || 0,
45
+ );
46
+ const MAX_SODA_TURN_CHARS = Math.max(
47
+ 1_000,
48
+ Math.floor(Number(env("MAX_SODA_TURN_KB", "32")) * 1000) || 32_000,
49
+ );
50
+
51
+ // ═══════════════════════════════════════════════════════════════════════
52
+ // §6e‴ SODA parsing — a social dialogue row → SEMA items
53
+ //
54
+ // Each row carries `dialogue` (an array of turn strings) and `speakers` (the
55
+ // speaker name per turn). The deposit is the cumulative walk over speaker-merged
56
+ // turns, identical in shape to Taskmaster and oasst2 — turns are short (mean
57
+ // 87 B) and dialogues average 7.3 turns, so the accumulated context stays well
58
+ // inside the healthy range.
59
+ //
60
+ // `narrative`, `literal` and the ATOMIC-style `head`/`relation`/`tail` columns
61
+ // are NOT deposited: they are the generation scaffolding SODA was distilled
62
+ // from, they restate the dialogue in the third person, and depositing both a
63
+ // dialogue and its paraphrased summary gives one meaning two shapes — which is
64
+ // measured to SUPPRESS composition rather than help it.
65
+ // ═══════════════════════════════════════════════════════════════════════
66
+
67
+ /** Normalize a SODA row into its turns, or null when it carries no usable
68
+ * dialogue. Speakers are optional (they only drive merging); an implausibly
69
+ * long turn rejects the dialogue as corrupt. */
70
+ export function toSodaTurns(
71
+ row: unknown,
72
+ maxChars = MAX_SODA_TURN_CHARS,
73
+ ): TaskmasterTurn[] | null {
74
+ if (!row || typeof row !== "object") return null;
75
+ const r = row as Record<string, unknown>;
76
+ const dialogue = r.dialogue;
77
+ if (!Array.isArray(dialogue)) return null;
78
+ const speakers = Array.isArray(r.speakers) ? r.speakers : [];
79
+ const turns: TaskmasterTurn[] = [];
80
+ for (let i = 0; i < dialogue.length; i++) {
81
+ const text = typeof dialogue[i] === "string"
82
+ ? (dialogue[i] as string).trim()
83
+ : "";
84
+ if (!text) continue;
85
+ if (text.length > maxChars) return null;
86
+ turns.push({
87
+ speaker: String(speakers[i] ?? "").trim().toUpperCase(),
88
+ text,
89
+ });
90
+ }
91
+ return turns.length ? turns : null;
92
+ }
93
+
94
+ /** Translate ONE SODA dialogue into SEMA items: the cumulative walk over its
95
+ * speaker-merged turns. Shares `mergeSpeakerTurns` because the rule is the
96
+ * same one — consecutive turns by one speaker are one contribution. */
97
+ export function sodaDialogueToItems(turns: TaskmasterTurn[]): TrainingItem[] {
98
+ const texts = mergeSpeakerTurns(turns);
99
+ if (texts.length < 2) return []; // not an exchange
100
+ return refineItems(accumulate(texts));
101
+ }
102
+
103
+ export const soda: Corpus = {
104
+ id: "soda",
105
+ label: "SODA",
106
+ kind: "social dialogue",
107
+ enabled: SODA,
108
+ maxRows: SODA_MAX_DIALOGS,
109
+ // Two of sixteen columns. The `narrative`/`literal`/`head`/`relation`/`tail`
110
+ // scaffolding the note above declines to deposit is now also never decoded:
111
+ // measured on the converted train shard, 449 MB uncompressed across all
112
+ // sixteen against 306 MB for these two (68.1%).
113
+ read: parquet({ columns: ["dialogue", "speakers"] }),
114
+ toItems: (row) => {
115
+ const turns = toSodaTurns(row);
116
+ if (!turns) return null;
117
+ const items = sodaDialogueToItems(turns);
118
+ return items.length ? items : null;
119
+ },
120
+ unitNoun: "shard(s)",
121
+ log: { rows: "row(s)" },
122
+ discover: convertedParquetUnits({
123
+ id: "soda",
124
+ label: "SODA",
125
+ dataset: SODA_DATASET,
126
+ config: "default",
127
+ splits: SODA_SPLITS,
128
+ localSub: "soda",
129
+ }),
130
+ };
@@ -0,0 +1,217 @@
1
+ // train_base/corpora/taskmaster.ts — google-research-datasets/Taskmaster 1–4 dialogue
2
+ //
3
+ // Knobs, the row adapter, and the stage descriptor for ONE corpus, together
4
+ // with the evidence that fixed each default. A corpus file owns everything
5
+ // source-specific; the contract it fills is ../corpus.ts and the loop that runs
6
+ // it is ../stage.ts.
7
+
8
+ import { env, LOCAL_PATH } from "../config.js";
9
+ import {
10
+ accumulate,
11
+ mergeSpeakerTurns,
12
+ refineItems,
13
+ type SpeakerTurn,
14
+ type TrainingItem,
15
+ } from "../items.js";
16
+ import { githubContents, localFiles } from "../discovery.js";
17
+ import { jsonArray } from "../readers.js";
18
+ import { type Corpus, localDir, type Unit } from "../corpus.js";
19
+ import type { TrainCtx } from "../runtime.js";
20
+ import { DIM, R } from "../ui.js";
21
+ import { basename, join } from "node:path";
22
+
23
+ // ── google-research-datasets/Taskmaster 1–4 (the dialogue stages) ──
24
+ // Four corpora of task-oriented dialogue, one shape between them: each file is a
25
+ // JSON ARRAY of conversations and each conversation carries
26
+ // `utterances: [{speaker, text, …}]`. TM-1 ships two files directly under its
27
+ // directory (self-dialogs, woz-dialogs); TM-2/3/4 ship theirs under `<set>/data`.
28
+ // They are the best-scoring corpora on the fold-unit recurrence benchmark that
29
+ // selects for halo health (TM-3 85.1%, TM-4 78.8%, TM-2 68.7%, TM-1 51.8%,
30
+ // against 23.2% for the incumbent SmolSent), and they are genuinely multi-turn
31
+ // where the incumbent multi-turn stage is not (TM-3 median 20 turns of ~43 B,
32
+ // against oasst2's median turn of 529 B).
33
+ //
34
+ // Served from GitHub raw, not Hugging Face: the HF mirrors are loading-script
35
+ // repos with no data files, and the official copies carry the CC BY 4.0 notice.
36
+ const TASKMASTER = env("TASKMASTER", "1") !== "0";
37
+ // Which sets to train, in order. Each is a directory in the Taskmaster repo.
38
+ const TASKMASTER_SETS = env(
39
+ "TASKMASTER_SETS",
40
+ "TM-1-2019,TM-2-2020,TM-3-2020,TM-4-2024",
41
+ ).split(",").map((s) => s.trim()).filter(Boolean);
42
+ const TASKMASTER_REPO = env(
43
+ "TASKMASTER_REPO",
44
+ "google-research-datasets/Taskmaster",
45
+ );
46
+ const TASKMASTER_RAW =
47
+ `https://raw.githubusercontent.com/${TASKMASTER_REPO}/master`;
48
+ // A conversation must have at least this many turns AFTER same-speaker merging.
49
+ // The default of 2 keeps every real exchange: unlike oasst2 — where a lone Q→A
50
+ // tree merely replicates the Aya stage's shape and is dropped — a two-turn
51
+ // task-oriented exchange is still task-oriented dialogue, and TM-4's dialogues
52
+ // are short by design (median 3.7 turns), so a higher bar would discard most of
53
+ // that set.
54
+ const TASKMASTER_MIN_TURNS = Math.max(
55
+ 2,
56
+ Math.floor(Number(env("TASKMASTER_MIN_TURNS", "2"))) || 2,
57
+ );
58
+ // Skip a conversation carrying an implausibly long utterance (corruption). The
59
+ // measured maximum across TM-1/2/3/4 is 1,897 bytes, so this only guards.
60
+ const MAX_TASKMASTER_TURN_CHARS = Math.max(
61
+ 1_000,
62
+ Math.floor(Number(env("MAX_TASKMASTER_TURN_KB", "32")) * 1000) || 32_000,
63
+ );
64
+
65
+ // ═══════════════════════════════════════════════════════════════════════
66
+ // §6e′ Taskmaster 1–4 parsing — a conversation ARRAY ELEMENT → SEMA items
67
+ //
68
+ // One adapter serves all four sets: every Taskmaster conversation, in every
69
+ // set, is `{conversation_id, …, utterances: [{speaker, text, …}]}`.
70
+ //
71
+ // ONLY `utterances[].text` IS READ, and that is a licence-adjacent correctness
72
+ // property, not a stylistic one. TM-3 and TM-4 also carry an `instructions`
73
+ // field holding the crowd-worker's task template — page after page of
74
+ // `{{HIDE movie_1 name.movie No Time To Die}}`, `{{CHECK confirm_natural …}}`
75
+ // and `var_theater_1` placeholders. That is authoring scaffolding, not
76
+ // dialogue, and depositing it would teach the store template noise as prose.
77
+ // Reading only `utterances[].text` excludes it structurally. Verified against
78
+ // the real files: across TM-2 (13,953 turns), TM-3 (24,059) and TM-4 (786),
79
+ // utterance text contains ZERO `var_*` placeholders and ZERO `{{ }}` markers —
80
+ // the scaffolding never leaks out of `instructions`.
81
+ //
82
+ // CONSECUTIVE SAME-SPEAKER TURNS ARE MERGED. Taskmaster splits one speaker's
83
+ // contribution across several indexed utterances ("I can help you with your
84
+ // movie search." / "Where are you located?" are two ASSISTANT rows), which is
85
+ // an artifact of the collection UI. Left unmerged, the cumulative walk deposits
86
+ // a turn boundary in the middle of one speaker's contribution and teaches it as
87
+ // a hand-off. Measured share of turns absorbed by merging: TM-1 17.7%,
88
+ // TM-2 11.9%, TM-3 0.8%, TM-4 0.0% — so this is load-bearing for the older sets
89
+ // and a no-op for the newer ones. Speaker names are compared case-insensitively
90
+ // because TM-1/2 use USER/ASSISTANT and TM-3/4 use user/assistant.
91
+ //
92
+ // The deposit shape is the cumulative walk (§6e's `accumulate`), identical to
93
+ // oasst2: each turn is the continuation of ALL prior turns, bare text, no role
94
+ // labels. It is the right shape here for the same reason and at a far healthier
95
+ // size — merged turns run p50 34–45 B (p90 ~100 B) and the accumulated context
96
+ // p50 301–532 B (p90 ~1.1 KB), against oasst2's median SINGLE turn of 529 B.
97
+ // ═══════════════════════════════════════════════════════════════════════
98
+
99
+ /** One utterance of a Taskmaster conversation — the shared dialogue-turn shape,
100
+ * under the name this corpus's adapters have always used. */
101
+ export type TaskmasterTurn = SpeakerTurn;
102
+
103
+ /** Normalize ONE element of a Taskmaster data file into its turns, or null when
104
+ * it carries no usable utterance. Empty/whitespace-only utterances are dropped
105
+ * (TM-3 has a few); a single implausibly long utterance rejects the whole
106
+ * conversation as corrupt rather than depositing a dump. */
107
+ export function toTaskmasterTurns(
108
+ row: unknown,
109
+ maxChars = MAX_TASKMASTER_TURN_CHARS,
110
+ ): TaskmasterTurn[] | null {
111
+ if (!row || typeof row !== "object") return null;
112
+ const utterances = (row as Record<string, unknown>).utterances;
113
+ if (!Array.isArray(utterances)) return null;
114
+ const turns: TaskmasterTurn[] = [];
115
+ for (const u of utterances) {
116
+ if (!u || typeof u !== "object") continue;
117
+ const r = u as Record<string, unknown>;
118
+ const text = typeof r.text === "string" ? r.text.trim() : "";
119
+ if (!text) continue;
120
+ if (text.length > maxChars) return null;
121
+ turns.push({
122
+ speaker: String(r.speaker ?? "").trim().toUpperCase(),
123
+ text,
124
+ });
125
+ }
126
+ return turns.length ? turns : null;
127
+ }
128
+
129
+ /** Translate ONE Taskmaster conversation into SEMA training items: the
130
+ * cumulative walk over its merged turns. Returns [] for a conversation below
131
+ * TASKMASTER_MIN_TURNS, so callers can simply skip empties. */
132
+ export function taskmasterConversationToItems(
133
+ turns: TaskmasterTurn[],
134
+ minTurns = TASKMASTER_MIN_TURNS,
135
+ ): TrainingItem[] {
136
+ const texts = mergeSpeakerTurns(turns);
137
+ if (texts.length < minTurns) return [];
138
+ return refineItems(accumulate(texts));
139
+ }
140
+
141
+ /** List the Taskmaster data files to train, in TASKMASTER_SETS order. Returns
142
+ * repo-relative paths, e.g. "TM-3-2020/data/data_00.json".
143
+ *
144
+ * TM-2/3/4 keep their dialogue files under `<set>/data`, so everything there is
145
+ * fair game. TM-1 has no `data` directory: its two dialogue files sit at the
146
+ * set root NEXT TO `ontology.json` (a slot schema) and `sample.json` (a small
147
+ * excerpt of self-dialogs). Neither is an array of conversations, and training
148
+ * the excerpt would deposit a subset of TM-1 twice, so TM-1 is filtered to the
149
+ * `*-dialogs.json` pair (self-dialogs, woz-dialogs). */
150
+ async function listFiles(
151
+ ctx: TrainCtx,
152
+ ): Promise<Array<{ set: string; path: string; size: number }>> {
153
+ const out: Array<{ set: string; path: string; size: number }> = [];
154
+ for (const set of TASKMASTER_SETS) {
155
+ const rootOnly = /^TM-1\b/i.test(set);
156
+ const dir = rootOnly ? set : `${set}/data`;
157
+ const names = await githubContents(
158
+ TASKMASTER_REPO,
159
+ dir,
160
+ /\.json$/i,
161
+ `GET Taskmaster ${dir}`,
162
+ ctx.http,
163
+ );
164
+ for (const { path: name, size } of names) {
165
+ if (rootOnly && !/-dialogs\.json$/i.test(name)) continue;
166
+ out.push({ set, path: `${dir}/${name}`, size });
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+
172
+ const unit = (key: string, name: string): Unit => ({
173
+ key,
174
+ name,
175
+ display: `Taskmaster ${name}`,
176
+ });
177
+
178
+ export const taskmaster: Corpus = {
179
+ id: "taskmaster",
180
+ label: "Taskmaster",
181
+ kind: "task dialogue",
182
+ enabled: TASKMASTER,
183
+ unitNoun: "dialogue file(s)",
184
+ read: jsonArray(),
185
+ toItems: (row) => {
186
+ const turns = toTaskmasterTurns(row);
187
+ if (!turns) return null;
188
+ const items = taskmasterConversationToItems(turns); // [] when too short
189
+ return items.length ? items : null;
190
+ },
191
+ log: { bad: "unusable conversation(s)" },
192
+ async discover(ctx) {
193
+ // LOCAL_PATH/taskmaster/ — a subdirectory, because these share the .json
194
+ // extension with the General-Knowledge source and must not be confused
195
+ // with it.
196
+ if (LOCAL_PATH) {
197
+ const dir = localDir("taskmaster");
198
+ const names = localFiles(dir, /\.json$/i);
199
+ if (names.length === 0) {
200
+ ctx.progress.log(
201
+ ` ${DIM}· no Taskmaster *.json in ${dir} — skipping${R}`,
202
+ );
203
+ return null;
204
+ }
205
+ return names.map((n) => ({
206
+ ...unit(n.path, n.path),
207
+ local: join(dir, n.path),
208
+ bytes: n.size,
209
+ }));
210
+ }
211
+ return (await listFiles(ctx)).map((f) => ({
212
+ ...unit(f.path, `${f.set}/${basename(f.path)}`),
213
+ url: `${TASKMASTER_RAW}/${f.path}`,
214
+ bytes: f.size,
215
+ }));
216
+ },
217
+ };