@hviana/sema 0.5.9 → 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 (79) hide show
  1. package/AGENTS.md +20 -4
  2. package/DATASETS.md +12 -11
  3. package/dist/example/train_base/cache.d.ts +35 -0
  4. package/dist/example/train_base/cache.js +211 -0
  5. package/dist/example/train_base/config.d.ts +21 -0
  6. package/dist/example/train_base/config.js +94 -0
  7. package/dist/example/train_base/corpora/aya.d.ts +19 -0
  8. package/dist/example/train_base/corpora/aya.js +76 -0
  9. package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
  10. package/dist/example/train_base/corpora/converted-parquet.js +44 -0
  11. package/dist/example/train_base/corpora/genknow.d.ts +14 -0
  12. package/dist/example/train_base/corpora/genknow.js +83 -0
  13. package/dist/example/train_base/corpora/index.d.ts +29 -0
  14. package/dist/example/train_base/corpora/index.js +81 -0
  15. package/dist/example/train_base/corpora/massive.d.ts +7 -0
  16. package/dist/example/train_base/corpora/massive.js +98 -0
  17. package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
  18. package/dist/example/train_base/corpora/oasst2.js +120 -0
  19. package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
  20. package/dist/example/train_base/corpora/smolsent.js +156 -0
  21. package/dist/example/train_base/corpora/soda.d.ts +12 -0
  22. package/dist/example/train_base/corpora/soda.js +113 -0
  23. package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
  24. package/dist/example/train_base/corpora/taskmaster.js +144 -0
  25. package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
  26. package/dist/example/train_base/corpora/wiki2.js +132 -0
  27. package/dist/example/train_base/corpus.d.ts +88 -0
  28. package/dist/example/train_base/corpus.js +65 -0
  29. package/dist/example/train_base/discovery.d.ts +48 -0
  30. package/dist/example/train_base/discovery.js +143 -0
  31. package/dist/example/train_base/http.d.ts +82 -0
  32. package/dist/example/train_base/http.js +219 -0
  33. package/dist/example/train_base/items.d.ts +46 -0
  34. package/dist/example/train_base/items.js +98 -0
  35. package/dist/example/train_base/main.d.ts +4 -0
  36. package/dist/example/train_base/main.js +207 -0
  37. package/dist/example/train_base/progress.d.ts +34 -0
  38. package/dist/example/train_base/progress.js +114 -0
  39. package/dist/example/train_base/readers.d.ts +125 -0
  40. package/dist/example/train_base/readers.js +391 -0
  41. package/dist/example/train_base/runtime.d.ts +115 -0
  42. package/dist/example/train_base/runtime.js +637 -0
  43. package/dist/example/train_base/stage.d.ts +3 -0
  44. package/dist/example/train_base/stage.js +246 -0
  45. package/dist/example/train_base/ui.d.ts +88 -0
  46. package/dist/example/train_base/ui.js +272 -0
  47. package/dist/src/mind/mind.d.ts +1 -1
  48. package/dist/src/mind/mind.js +1 -1
  49. package/example/train_base/cache.ts +251 -0
  50. package/example/train_base/config.ts +128 -0
  51. package/example/train_base/corpora/aya.ts +106 -0
  52. package/example/train_base/corpora/converted-parquet.ts +64 -0
  53. package/example/train_base/corpora/genknow.ts +114 -0
  54. package/example/train_base/corpora/index.ts +88 -0
  55. package/example/train_base/corpora/massive.ts +111 -0
  56. package/example/train_base/corpora/oasst2.ts +163 -0
  57. package/example/train_base/corpora/smolsent.ts +203 -0
  58. package/example/train_base/corpora/soda.ts +130 -0
  59. package/example/train_base/corpora/taskmaster.ts +217 -0
  60. package/example/train_base/corpora/wiki2.ts +190 -0
  61. package/example/train_base/corpus.ts +150 -0
  62. package/example/train_base/discovery.ts +203 -0
  63. package/example/train_base/http.ts +284 -0
  64. package/example/train_base/items.ts +118 -0
  65. package/example/train_base/main.ts +240 -0
  66. package/example/train_base/progress.ts +149 -0
  67. package/example/train_base/readers.ts +505 -0
  68. package/example/train_base/runtime.ts +894 -0
  69. package/example/train_base/stage.ts +276 -0
  70. package/example/train_base/ui.ts +333 -0
  71. package/jsr.json +1 -1
  72. package/package.json +2 -4
  73. package/src/mind/mind.ts +1 -1
  74. package/test/13-conversation.test.mjs +1 -1
  75. package/test/84-composed-answer-honesty.test.mjs +2 -1
  76. package/test/88-dependency-footprint.test.mjs +99 -0
  77. package/dist/example/train_base.d.ts +0 -163
  78. package/dist/example/train_base.js +0 -3220
  79. package/example/train_base.ts +0 -3882
@@ -0,0 +1,12 @@
1
+ import { type TrainingItem } from "../items.js";
2
+ import type { Corpus } from "../corpus.js";
3
+ import type { TaskmasterTurn } from "./taskmaster.js";
4
+ /** Normalize a SODA row into its turns, or null when it carries no usable
5
+ * dialogue. Speakers are optional (they only drive merging); an implausibly
6
+ * long turn rejects the dialogue as corrupt. */
7
+ export declare function toSodaTurns(row: unknown, maxChars?: number): TaskmasterTurn[] | null;
8
+ /** Translate ONE SODA dialogue into SEMA items: the cumulative walk over its
9
+ * speaker-merged turns. Shares `mergeSpeakerTurns` because the rule is the
10
+ * same one — consecutive turns by one speaker are one contribution. */
11
+ export declare function sodaDialogueToItems(turns: TaskmasterTurn[]): TrainingItem[];
12
+ export declare const soda: Corpus;
@@ -0,0 +1,113 @@
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
+ import { env } from "../config.js";
8
+ import { accumulate, mergeSpeakerTurns, refineItems, } from "../items.js";
9
+ import { parquet } from "../readers.js";
10
+ import { convertedParquetUnits } from "./converted-parquet.js";
11
+ // ── allenai/soda (social dialogue) and AmazonScience/massive (short intents) ──
12
+ // Both are read from Hugging Face's auto-converted `refs/convert/parquet`
13
+ // branch. For SODA that is mandatory, not cosmetic: its main-branch
14
+ // train.parquet is ONE 1,191,582-row group (1.19 GB uncompressed), and a
15
+ // Parquet column chunk is per-group, so any read of it materialises the whole
16
+ // file — measured at 100% of a 689 MB file and 2 GB of heap for a 500-row read.
17
+ // The converted branch uses uniform 10,000-row groups.
18
+ //
19
+ // BOTH STAGES ARE BUDGETED, and that is a curriculum decision rather than an
20
+ // algorithmic cap. SODA's train split holds 1,191,582 dialogues which the
21
+ // cumulative walk would turn into ~8 MILLION episodes — against the 662,221
22
+ // deposits of the entire current corpus. Trained whole it would not join the
23
+ // mix, it would BE the mix, and corpus size is the quantity every scale problem
24
+ // in this engine is measured against. The default takes the first
25
+ // SODA_MAX_DIALOGS of them; set it to 0 to lift the budget.
26
+ const SODA = env("SODA", "1") !== "0";
27
+ const SODA_DATASET = env("SODA_DATASET", "allenai/soda");
28
+ const SODA_SPLITS = env("SODA_SPLITS", "train")
29
+ .split(",").map((s) => s.trim()).filter(Boolean);
30
+ // ~6.3 episodes per dialogue, so this budgets ~750k episodes — comparable to
31
+ // the Taskmaster stage and to Aya, which is the intended balance. 0 = no budget.
32
+ const SODA_MAX_DIALOGS = Math.max(0, Math.floor(Number(env("SODA_MAX_DIALOGS", "120000"))) || 0);
33
+ const MAX_SODA_TURN_CHARS = Math.max(1_000, Math.floor(Number(env("MAX_SODA_TURN_KB", "32")) * 1000) || 32_000);
34
+ // ═══════════════════════════════════════════════════════════════════════
35
+ // §6e‴ SODA parsing — a social dialogue row → SEMA items
36
+ //
37
+ // Each row carries `dialogue` (an array of turn strings) and `speakers` (the
38
+ // speaker name per turn). The deposit is the cumulative walk over speaker-merged
39
+ // turns, identical in shape to Taskmaster and oasst2 — turns are short (mean
40
+ // 87 B) and dialogues average 7.3 turns, so the accumulated context stays well
41
+ // inside the healthy range.
42
+ //
43
+ // `narrative`, `literal` and the ATOMIC-style `head`/`relation`/`tail` columns
44
+ // are NOT deposited: they are the generation scaffolding SODA was distilled
45
+ // from, they restate the dialogue in the third person, and depositing both a
46
+ // dialogue and its paraphrased summary gives one meaning two shapes — which is
47
+ // measured to SUPPRESS composition rather than help it.
48
+ // ═══════════════════════════════════════════════════════════════════════
49
+ /** Normalize a SODA row into its turns, or null when it carries no usable
50
+ * dialogue. Speakers are optional (they only drive merging); an implausibly
51
+ * long turn rejects the dialogue as corrupt. */
52
+ export function toSodaTurns(row, maxChars = MAX_SODA_TURN_CHARS) {
53
+ if (!row || typeof row !== "object")
54
+ return null;
55
+ const r = row;
56
+ const dialogue = r.dialogue;
57
+ if (!Array.isArray(dialogue))
58
+ return null;
59
+ const speakers = Array.isArray(r.speakers) ? r.speakers : [];
60
+ const turns = [];
61
+ for (let i = 0; i < dialogue.length; i++) {
62
+ const text = typeof dialogue[i] === "string"
63
+ ? dialogue[i].trim()
64
+ : "";
65
+ if (!text)
66
+ continue;
67
+ if (text.length > maxChars)
68
+ return null;
69
+ turns.push({
70
+ speaker: String(speakers[i] ?? "").trim().toUpperCase(),
71
+ text,
72
+ });
73
+ }
74
+ return turns.length ? turns : null;
75
+ }
76
+ /** Translate ONE SODA dialogue into SEMA items: the cumulative walk over its
77
+ * speaker-merged turns. Shares `mergeSpeakerTurns` because the rule is the
78
+ * same one — consecutive turns by one speaker are one contribution. */
79
+ export function sodaDialogueToItems(turns) {
80
+ const texts = mergeSpeakerTurns(turns);
81
+ if (texts.length < 2)
82
+ return []; // not an exchange
83
+ return refineItems(accumulate(texts));
84
+ }
85
+ export const soda = {
86
+ id: "soda",
87
+ label: "SODA",
88
+ kind: "social dialogue",
89
+ enabled: SODA,
90
+ maxRows: SODA_MAX_DIALOGS,
91
+ // Two of sixteen columns. The `narrative`/`literal`/`head`/`relation`/`tail`
92
+ // scaffolding the note above declines to deposit is now also never decoded:
93
+ // measured on the converted train shard, 449 MB uncompressed across all
94
+ // sixteen against 306 MB for these two (68.1%).
95
+ read: parquet({ columns: ["dialogue", "speakers"] }),
96
+ toItems: (row) => {
97
+ const turns = toSodaTurns(row);
98
+ if (!turns)
99
+ return null;
100
+ const items = sodaDialogueToItems(turns);
101
+ return items.length ? items : null;
102
+ },
103
+ unitNoun: "shard(s)",
104
+ log: { rows: "row(s)" },
105
+ discover: convertedParquetUnits({
106
+ id: "soda",
107
+ label: "SODA",
108
+ dataset: SODA_DATASET,
109
+ config: "default",
110
+ splits: SODA_SPLITS,
111
+ localSub: "soda",
112
+ }),
113
+ };
@@ -0,0 +1,15 @@
1
+ import { type SpeakerTurn, type TrainingItem } from "../items.js";
2
+ import { type Corpus } from "../corpus.js";
3
+ /** One utterance of a Taskmaster conversation — the shared dialogue-turn shape,
4
+ * under the name this corpus's adapters have always used. */
5
+ export type TaskmasterTurn = SpeakerTurn;
6
+ /** Normalize ONE element of a Taskmaster data file into its turns, or null when
7
+ * it carries no usable utterance. Empty/whitespace-only utterances are dropped
8
+ * (TM-3 has a few); a single implausibly long utterance rejects the whole
9
+ * conversation as corrupt rather than depositing a dump. */
10
+ export declare function toTaskmasterTurns(row: unknown, maxChars?: number): TaskmasterTurn[] | null;
11
+ /** Translate ONE Taskmaster conversation into SEMA training items: the
12
+ * cumulative walk over its merged turns. Returns [] for a conversation below
13
+ * TASKMASTER_MIN_TURNS, so callers can simply skip empties. */
14
+ export declare function taskmasterConversationToItems(turns: TaskmasterTurn[], minTurns?: number): TrainingItem[];
15
+ export declare const taskmaster: Corpus;
@@ -0,0 +1,144 @@
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
+ import { env, LOCAL_PATH } from "../config.js";
8
+ import { accumulate, mergeSpeakerTurns, refineItems, } from "../items.js";
9
+ import { githubContents, localFiles } from "../discovery.js";
10
+ import { jsonArray } from "../readers.js";
11
+ import { localDir } from "../corpus.js";
12
+ import { DIM, R } from "../ui.js";
13
+ import { basename, join } from "node:path";
14
+ // ── google-research-datasets/Taskmaster 1–4 (the dialogue stages) ──
15
+ // Four corpora of task-oriented dialogue, one shape between them: each file is a
16
+ // JSON ARRAY of conversations and each conversation carries
17
+ // `utterances: [{speaker, text, …}]`. TM-1 ships two files directly under its
18
+ // directory (self-dialogs, woz-dialogs); TM-2/3/4 ship theirs under `<set>/data`.
19
+ // They are the best-scoring corpora on the fold-unit recurrence benchmark that
20
+ // selects for halo health (TM-3 85.1%, TM-4 78.8%, TM-2 68.7%, TM-1 51.8%,
21
+ // against 23.2% for the incumbent SmolSent), and they are genuinely multi-turn
22
+ // where the incumbent multi-turn stage is not (TM-3 median 20 turns of ~43 B,
23
+ // against oasst2's median turn of 529 B).
24
+ //
25
+ // Served from GitHub raw, not Hugging Face: the HF mirrors are loading-script
26
+ // repos with no data files, and the official copies carry the CC BY 4.0 notice.
27
+ const TASKMASTER = env("TASKMASTER", "1") !== "0";
28
+ // Which sets to train, in order. Each is a directory in the Taskmaster repo.
29
+ const TASKMASTER_SETS = env("TASKMASTER_SETS", "TM-1-2019,TM-2-2020,TM-3-2020,TM-4-2024").split(",").map((s) => s.trim()).filter(Boolean);
30
+ const TASKMASTER_REPO = env("TASKMASTER_REPO", "google-research-datasets/Taskmaster");
31
+ const TASKMASTER_RAW = `https://raw.githubusercontent.com/${TASKMASTER_REPO}/master`;
32
+ // A conversation must have at least this many turns AFTER same-speaker merging.
33
+ // The default of 2 keeps every real exchange: unlike oasst2 — where a lone Q→A
34
+ // tree merely replicates the Aya stage's shape and is dropped — a two-turn
35
+ // task-oriented exchange is still task-oriented dialogue, and TM-4's dialogues
36
+ // are short by design (median 3.7 turns), so a higher bar would discard most of
37
+ // that set.
38
+ const TASKMASTER_MIN_TURNS = Math.max(2, Math.floor(Number(env("TASKMASTER_MIN_TURNS", "2"))) || 2);
39
+ // Skip a conversation carrying an implausibly long utterance (corruption). The
40
+ // measured maximum across TM-1/2/3/4 is 1,897 bytes, so this only guards.
41
+ const MAX_TASKMASTER_TURN_CHARS = Math.max(1_000, Math.floor(Number(env("MAX_TASKMASTER_TURN_KB", "32")) * 1000) || 32_000);
42
+ /** Normalize ONE element of a Taskmaster data file into its turns, or null when
43
+ * it carries no usable utterance. Empty/whitespace-only utterances are dropped
44
+ * (TM-3 has a few); a single implausibly long utterance rejects the whole
45
+ * conversation as corrupt rather than depositing a dump. */
46
+ export function toTaskmasterTurns(row, maxChars = MAX_TASKMASTER_TURN_CHARS) {
47
+ if (!row || typeof row !== "object")
48
+ return null;
49
+ const utterances = row.utterances;
50
+ if (!Array.isArray(utterances))
51
+ return null;
52
+ const turns = [];
53
+ for (const u of utterances) {
54
+ if (!u || typeof u !== "object")
55
+ continue;
56
+ const r = u;
57
+ const text = typeof r.text === "string" ? r.text.trim() : "";
58
+ if (!text)
59
+ continue;
60
+ if (text.length > maxChars)
61
+ return null;
62
+ turns.push({
63
+ speaker: String(r.speaker ?? "").trim().toUpperCase(),
64
+ text,
65
+ });
66
+ }
67
+ return turns.length ? turns : null;
68
+ }
69
+ /** Translate ONE Taskmaster conversation into SEMA training items: the
70
+ * cumulative walk over its merged turns. Returns [] for a conversation below
71
+ * TASKMASTER_MIN_TURNS, so callers can simply skip empties. */
72
+ export function taskmasterConversationToItems(turns, minTurns = TASKMASTER_MIN_TURNS) {
73
+ const texts = mergeSpeakerTurns(turns);
74
+ if (texts.length < minTurns)
75
+ return [];
76
+ return refineItems(accumulate(texts));
77
+ }
78
+ /** List the Taskmaster data files to train, in TASKMASTER_SETS order. Returns
79
+ * repo-relative paths, e.g. "TM-3-2020/data/data_00.json".
80
+ *
81
+ * TM-2/3/4 keep their dialogue files under `<set>/data`, so everything there is
82
+ * fair game. TM-1 has no `data` directory: its two dialogue files sit at the
83
+ * set root NEXT TO `ontology.json` (a slot schema) and `sample.json` (a small
84
+ * excerpt of self-dialogs). Neither is an array of conversations, and training
85
+ * the excerpt would deposit a subset of TM-1 twice, so TM-1 is filtered to the
86
+ * `*-dialogs.json` pair (self-dialogs, woz-dialogs). */
87
+ async function listFiles(ctx) {
88
+ const out = [];
89
+ for (const set of TASKMASTER_SETS) {
90
+ const rootOnly = /^TM-1\b/i.test(set);
91
+ const dir = rootOnly ? set : `${set}/data`;
92
+ const names = await githubContents(TASKMASTER_REPO, dir, /\.json$/i, `GET Taskmaster ${dir}`, ctx.http);
93
+ for (const { path: name, size } of names) {
94
+ if (rootOnly && !/-dialogs\.json$/i.test(name))
95
+ continue;
96
+ out.push({ set, path: `${dir}/${name}`, size });
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ const unit = (key, name) => ({
102
+ key,
103
+ name,
104
+ display: `Taskmaster ${name}`,
105
+ });
106
+ export const taskmaster = {
107
+ id: "taskmaster",
108
+ label: "Taskmaster",
109
+ kind: "task dialogue",
110
+ enabled: TASKMASTER,
111
+ unitNoun: "dialogue file(s)",
112
+ read: jsonArray(),
113
+ toItems: (row) => {
114
+ const turns = toTaskmasterTurns(row);
115
+ if (!turns)
116
+ return null;
117
+ const items = taskmasterConversationToItems(turns); // [] when too short
118
+ return items.length ? items : null;
119
+ },
120
+ log: { bad: "unusable conversation(s)" },
121
+ async discover(ctx) {
122
+ // LOCAL_PATH/taskmaster/ — a subdirectory, because these share the .json
123
+ // extension with the General-Knowledge source and must not be confused
124
+ // with it.
125
+ if (LOCAL_PATH) {
126
+ const dir = localDir("taskmaster");
127
+ const names = localFiles(dir, /\.json$/i);
128
+ if (names.length === 0) {
129
+ ctx.progress.log(` ${DIM}· no Taskmaster *.json in ${dir} — skipping${R}`);
130
+ return null;
131
+ }
132
+ return names.map((n) => ({
133
+ ...unit(n.path, n.path),
134
+ local: join(dir, n.path),
135
+ bytes: n.size,
136
+ }));
137
+ }
138
+ return (await listFiles(ctx)).map((f) => ({
139
+ ...unit(f.path, `${f.set}/${basename(f.path)}`),
140
+ url: `${TASKMASTER_RAW}/${f.path}`,
141
+ bytes: f.size,
142
+ }));
143
+ },
144
+ };
@@ -0,0 +1,23 @@
1
+ import { type TrainingItem } from "../items.js";
2
+ import type { Corpus } from "../corpus.js";
3
+ /** One (subject, relation, object) triple from a 2Wiki `evidences` cell. */
4
+ export interface WikiTriple {
5
+ subject: string;
6
+ relation: string;
7
+ object: string;
8
+ }
9
+ /** Normalize a 2Wiki row into its evidence triples, or null when it carries
10
+ * none usable. `evidences` is a JSON STRING holding an array of 3-element
11
+ * arrays; a row whose cell is absent, unparseable, or empty yields null.
12
+ * Individual malformed or oversized triples are dropped without discarding the
13
+ * row — one bad triple should not cost the others. */
14
+ export declare function toWikiTriples(row: unknown, maxChars?: number): WikiTriple[] | null;
15
+ /** Render ONE triple as the prose fact Sema stores. Kept separate so the two
16
+ * deposits below are guaranteed to share a byte-identical continuation: the
17
+ * pivot fact only works if it leads to the SAME node the relation fact does. */
18
+ export declare function wikiTripleSentence(t: WikiTriple): string;
19
+ /** Translate a row's triples into SEMA items: per triple, the relation fact and
20
+ * the bare-subject PIVOT fact (see the section note above). refineItems drops
21
+ * the duplicates this produces when a row states the same triple twice. */
22
+ export declare function wikiTriplesToItems(triples: WikiTriple[]): TrainingItem[];
23
+ export declare const wiki2: Corpus;
@@ -0,0 +1,132 @@
1
+ // train_base/corpora/wiki2.ts — 2WikiMultihopQA evidence triples
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
+ import { env } from "../config.js";
8
+ import { refineItems } from "../items.js";
9
+ import { parquet } from "../readers.js";
10
+ import { convertedParquetUnits } from "./converted-parquet.js";
11
+ // ── 2WikiMultihopQA — the `evidences` TRIPLES only (the composition stage) ──
12
+ // Each row carries `evidences`: a JSON string of (subject, relation, object)
13
+ // triples that CHAIN — one triple's object is the next's subject. 72.5% of rows
14
+ // carry such a chain (measured over 4,000 rows), and those triples are the only
15
+ // representation measured to make Sema compose a two-hop answer at all.
16
+ //
17
+ // TWO COLUMNS ARE DELIBERATELY NOT READ, one for licence reasons and one for
18
+ // capability reasons:
19
+ // • `context` holds Wikipedia PROSE. The repo is Apache-2.0 but Wikipedia text
20
+ // is CC BY-SA, and a Sema store keeps text verbatim, so ingesting the
21
+ // passages would attach ShareAlike to every distributed store. The triples
22
+ // originate in Wikidata (CC0). See DATASETS.md §3.2/§4.
23
+ // • `question`/`answer` are the composed multi-hop QUESTION. Depositing those
24
+ // teaches the answer to that exact question and nothing else — it memorises
25
+ // rather than composes. They are used to EVALUATE this adapter, never as
26
+ // training input.
27
+ //
28
+ // Read from Hugging Face's auto-converted `refs/convert/parquet` branch, not
29
+ // from main: the main-branch train.parquet is written as ONE 167,454-row
30
+ // group (666 MB uncompressed) and a Parquet column chunk is per-group, so any
31
+ // read of it materialises the whole file. The converted branch uses uniform
32
+ // 10,000-row groups, which `parquetBatchRows` then subdivides by BYTES.
33
+ const WIKI2 = env("WIKI2", "1") !== "0";
34
+ const WIKI2_DATASET = env("WIKI2_DATASET", "xanhho/2WikiMultihopQA");
35
+ // Splits to train, in order. Only `train` by default: `validation`/`test` are
36
+ // the dataset's held-out sets and are what an honest evaluation of this
37
+ // adapter's composition rate has to be measured on.
38
+ const WIKI2_SPLITS = env("WIKI2_SPLITS", "train")
39
+ .split(",").map((s) => s.trim()).filter(Boolean);
40
+ // Reject a triple with an implausibly long field (corruption); real subjects and
41
+ // objects are entity names, and relations are Wikidata property labels.
42
+ // 0 = every row. The train split holds 167,454 rows at ~4.95 deposits each
43
+ // (~830k facts), so this is the knob that keeps 2Wiki proportionate to the rest
44
+ // of the curriculum in the same way SODA_MAX_DIALOGS does.
45
+ const WIKI2_MAX_ROWS = Math.max(0, Math.floor(Number(env("WIKI2_MAX_ROWS", "0"))) || 0);
46
+ const MAX_WIKI2_FIELD_CHARS = Math.max(100, Math.floor(Number(env("MAX_WIKI2_FIELD_KB", "2")) * 1000) || 2_000);
47
+ /** Normalize a 2Wiki row into its evidence triples, or null when it carries
48
+ * none usable. `evidences` is a JSON STRING holding an array of 3-element
49
+ * arrays; a row whose cell is absent, unparseable, or empty yields null.
50
+ * Individual malformed or oversized triples are dropped without discarding the
51
+ * row — one bad triple should not cost the others. */
52
+ export function toWikiTriples(row, maxChars = MAX_WIKI2_FIELD_CHARS) {
53
+ if (!row || typeof row !== "object")
54
+ return null;
55
+ const cell = row.evidences;
56
+ let parsed = cell;
57
+ if (typeof cell === "string") {
58
+ try {
59
+ parsed = JSON.parse(cell);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ if (!Array.isArray(parsed))
66
+ return null;
67
+ const out = [];
68
+ for (const e of parsed) {
69
+ if (!Array.isArray(e) || e.length < 3)
70
+ continue;
71
+ const subject = typeof e[0] === "string" ? e[0].trim() : "";
72
+ const relation = typeof e[1] === "string" ? e[1].trim() : "";
73
+ const object = typeof e[2] === "string" ? e[2].trim() : "";
74
+ if (!subject || !relation || !object)
75
+ continue;
76
+ if (subject.length > maxChars ||
77
+ relation.length > maxChars ||
78
+ object.length > maxChars)
79
+ continue;
80
+ out.push({ subject, relation, object });
81
+ }
82
+ return out.length ? out : null;
83
+ }
84
+ /** Render ONE triple as the prose fact Sema stores. Kept separate so the two
85
+ * deposits below are guaranteed to share a byte-identical continuation: the
86
+ * pivot fact only works if it leads to the SAME node the relation fact does. */
87
+ export function wikiTripleSentence(t) {
88
+ return `The ${t.relation} of ${t.subject} is ${t.object}.`;
89
+ }
90
+ /** Translate a row's triples into SEMA items: per triple, the relation fact and
91
+ * the bare-subject PIVOT fact (see the section note above). refineItems drops
92
+ * the duplicates this produces when a row states the same triple twice. */
93
+ export function wikiTriplesToItems(triples) {
94
+ const items = [];
95
+ for (const t of triples) {
96
+ const fact = wikiTripleSentence(t);
97
+ items.push({ context: `${t.subject} ${t.relation}`, continuation: fact });
98
+ items.push({ context: t.subject, continuation: fact });
99
+ }
100
+ return refineItems(items);
101
+ }
102
+ export const wiki2 = {
103
+ id: "2wiki",
104
+ label: "2Wiki",
105
+ kind: "relation triples",
106
+ enabled: WIKI2,
107
+ maxRows: WIKI2_MAX_ROWS,
108
+ // Read ONE of the file's seven columns. This is where the "context is not
109
+ // read" rule above stops being a property of the adapter and becomes a
110
+ // property of the read: the Wikipedia prose is never decoded at all. It is
111
+ // also almost the whole file — measured on the converted train shard, the
112
+ // seven columns hold 518 MB uncompressed and `evidences` is 18 MB of it
113
+ // (3.5%), so the projection cuts the decode work by ~28x.
114
+ read: parquet({ columns: ["evidences"] }),
115
+ toItems: (row) => {
116
+ const triples = toWikiTriples(row);
117
+ if (!triples)
118
+ return null;
119
+ const items = wikiTriplesToItems(triples);
120
+ return items.length ? items : null;
121
+ },
122
+ unitNoun: "shard(s)",
123
+ log: { rows: "row(s)" },
124
+ discover: convertedParquetUnits({
125
+ id: "2wiki",
126
+ label: "2Wiki",
127
+ dataset: WIKI2_DATASET,
128
+ config: "default",
129
+ splits: WIKI2_SPLITS,
130
+ localSub: "2wiki",
131
+ }),
132
+ };
@@ -0,0 +1,88 @@
1
+ import type { Reader, RowAdapter } from "./readers.js";
2
+ import type { TrainCtx } from "./runtime.js";
3
+ /** One file to train: a shard, a per-language file, or a whole single-file
4
+ * corpus. Exactly one of `url` / `local` is set. */
5
+ export interface Unit {
6
+ /** Resume-id suffix — the corpus id and this form `${id}::${key}`. Part of
7
+ * the store's compatibility surface; see the file header. */
8
+ key: string;
9
+ /** How the run log names this unit once it is read. */
10
+ name: string;
11
+ /** How the live panel names it, and (unless `acquireLabel` overrides) how the
12
+ * download is labelled. Conventionally `${corpus.label} ${name}`. */
13
+ display: string;
14
+ url?: string;
15
+ local?: string;
16
+ /** Size in bytes when the listing said, else 0/absent. Summed BEFORE the
17
+ * stage reads anything, so the corpus progress bar has a denominator that
18
+ * does not grow underneath it. */
19
+ bytes?: number;
20
+ /** Cache filename. Defaults to the resume id with unsafe characters folded. */
21
+ dest?: string;
22
+ /** Download label, when it differs from `display`. */
23
+ acquireLabel?: string;
24
+ }
25
+ /** How one unit's outcome reads in the run log. All optional: the defaults are
26
+ * what every fact-shaped corpus prints. */
27
+ export interface LogStyle {
28
+ /** What one deposit is called. Default "facts". */
29
+ deposits?: string;
30
+ /** When set, the line reports "from N <rows>" — the count of rows that
31
+ * actually produced deposits. */
32
+ rows?: string;
33
+ /** What an unusable record is called. Default "unusable row(s)". */
34
+ bad?: string;
35
+ /** Report only the reader's `skipped` (malformed records), not the rows the
36
+ * adapter declined. For a corpus that DECLINES records by design — oasst2
37
+ * drops every single-turn tree — counting those as damage would be a lie. */
38
+ malformedOnly?: boolean;
39
+ }
40
+ export interface Corpus {
41
+ /** Tally key AND resume-id prefix. Compatibility surface — see the header. */
42
+ id: string;
43
+ /** Human name: the panel, the skip notices, the listing-failure message. */
44
+ label: string;
45
+ /** The dim tag in the log line, e.g. "translation", "social dialogue". */
46
+ kind: string;
47
+ enabled: boolean;
48
+ /** The work-list. Return [] for "nothing found" (the runner says so), or
49
+ * null when the corpus has already logged a more specific reason. */
50
+ discover(ctx: TrainCtx): Promise<Unit[] | null>;
51
+ read: Reader;
52
+ toItems: RowAdapter;
53
+ /** Stage-wide row budget; 0/absent = unbounded. See the budget notes in
54
+ * stage.ts. */
55
+ maxRows?: number;
56
+ /** Noun for the "N/M ___ to train" announcement. Absent ⇒ no announcement,
57
+ * which is what a single-unit corpus has always done. */
58
+ unitNoun?: string;
59
+ /** Keep a file that came from the CACHE after a complete read. Only oasst2
60
+ * does this: every other corpus deletes whatever acquire() handed it. */
61
+ keepCached?: boolean;
62
+ log?: LogStyle;
63
+ }
64
+ /** The string a store records once this unit is finished. ONE rule, no
65
+ * exceptions — see the file header for why there used to be two. */
66
+ export declare const unitIdOf: (corpus: Corpus, unit: Unit) => string;
67
+ /** Local files live under `LOCAL_PATH/<sub>`, or directly in LOCAL_PATH when
68
+ * `sub` is empty. Kept here because the layout is a user-facing convention:
69
+ * the corpora that share an extension (.json, .parquet) are kept apart by a
70
+ * subdirectory so a local run cannot feed one corpus's files to another. */
71
+ export declare const localDir: (sub: string) => string;
72
+ /** A single-unit corpus: one fixed URL, or one local file matched by pattern.
73
+ * Factored out because the three corpora that are ONE file resolve it the same
74
+ * way. `key` is what the store records this corpus under — `aya::dataset` is
75
+ * `key: "dataset"` — so it is required rather than defaulted: a resume id is
76
+ * the one thing here that must never be guessed. */
77
+ export declare function singleUnit(opts: {
78
+ key: string;
79
+ label: string;
80
+ display: string;
81
+ url: string;
82
+ dest: string;
83
+ acquireLabel?: string;
84
+ /** Patterns tried, in order, against LOCAL_PATH. */
85
+ localMatch: RegExp[];
86
+ /** How the "no local copy" notice describes what it looked for. */
87
+ localWhat: string;
88
+ }): (ctx: TrainCtx) => Promise<Unit[] | null>;
@@ -0,0 +1,65 @@
1
+ // train_base/corpus.ts — WHAT A CORPUS IS: the contract every corpus file
2
+ // declares, and the resume identity derived from it.
3
+ //
4
+ // Separated from the loop that consumes it (stage.ts) so a corpus file never
5
+ // imports the loop. A corpus declares three things the loop does not know —
6
+ // where its work-list comes from, what container its bytes are in, and how a row
7
+ // becomes deposits — and everything here exists to state those three.
8
+ //
9
+ // THE RESUME IDS ARE A COMPATIBILITY SURFACE. A store records the units it has
10
+ // finished as strings, and a store trained by an earlier version must keep
11
+ // resuming, so `unitIdOf` below has to reproduce the ids that store already
12
+ // holds. It does, by one rule with no exceptions: `${corpus.id}::${unit.key}`.
13
+ //
14
+ // That single rule is a recent simplification, and the evidence for it is worth
15
+ // keeping. There used to be a second mechanism — a `Corpus.unitId` field
16
+ // carrying a FIXED id for the three single-unit corpora, which were believed
17
+ // irregular — and with it a trap: those corpora emitted `key: ""` on the
18
+ // assumption that the fixed id would always override, so a new single-unit
19
+ // corpus that forgot the field silently got the id "foo::". Reading the ids out
20
+ // of a real 2.5 GB store settled it: the three are `aya::dataset`,
21
+ // `oasst2::trees` and `genknow::qa`, which decompose EXACTLY into corpus id and
22
+ // key. They were never irregular, so the escape hatch and its trap are gone and
23
+ // one rule covers all 239 recorded ids.
24
+ import { LOCAL_PATH } from "./config.js";
25
+ import { localFind } from "./discovery.js";
26
+ import { DIM, R } from "./ui.js";
27
+ import { join } from "node:path";
28
+ /** The string a store records once this unit is finished. ONE rule, no
29
+ * exceptions — see the file header for why there used to be two. */
30
+ export const unitIdOf = (corpus, unit) => `${corpus.id}::${unit.key}`;
31
+ /** Local files live under `LOCAL_PATH/<sub>`, or directly in LOCAL_PATH when
32
+ * `sub` is empty. Kept here because the layout is a user-facing convention:
33
+ * the corpora that share an extension (.json, .parquet) are kept apart by a
34
+ * subdirectory so a local run cannot feed one corpus's files to another. */
35
+ export const localDir = (sub) => sub ? join(LOCAL_PATH, sub) : LOCAL_PATH;
36
+ /** A single-unit corpus: one fixed URL, or one local file matched by pattern.
37
+ * Factored out because the three corpora that are ONE file resolve it the same
38
+ * way. `key` is what the store records this corpus under — `aya::dataset` is
39
+ * `key: "dataset"` — so it is required rather than defaulted: a resume id is
40
+ * the one thing here that must never be guessed. */
41
+ export function singleUnit(opts) {
42
+ return async (ctx) => {
43
+ if (LOCAL_PATH) {
44
+ const hit = localFind(LOCAL_PATH, ...opts.localMatch);
45
+ if (!hit) {
46
+ ctx.progress.log(` ${DIM}· no ${opts.localWhat} in ${LOCAL_PATH} — skipping${R}`);
47
+ return null;
48
+ }
49
+ return [{
50
+ key: opts.key,
51
+ name: opts.label,
52
+ display: opts.display,
53
+ local: join(LOCAL_PATH, hit),
54
+ }];
55
+ }
56
+ return [{
57
+ key: opts.key,
58
+ name: opts.label,
59
+ display: opts.display,
60
+ url: opts.url,
61
+ dest: opts.dest,
62
+ acquireLabel: opts.acquireLabel,
63
+ }];
64
+ };
65
+ }
@@ -0,0 +1,48 @@
1
+ import { type HttpOptions } from "./http.js";
2
+ /** A note a listing needs to make about ITSELF — an accepted-but-unexpected
3
+ * split name, say. Distinct from a failure: the run continues, but silently
4
+ * continuing would hide why fewer units appeared than expected. */
5
+ export type Note = (msg: string) => void;
6
+ /** One listed file: where it is, and how big it is.
7
+ *
8
+ * The SIZE is carried because the panel is otherwise dishonest. Corpus
9
+ * progress used to be measured against a total that GREW as each file was
10
+ * opened, so the bar ran to 100% at the end of every file and then fell back
11
+ * when the next one was added — 100%, 50%, 100%, 66%… Both listing APIs
12
+ * already return the size, so the denominator can simply be known before the
13
+ * first byte is read. */
14
+ export interface Listed {
15
+ path: string;
16
+ /** Bytes, or 0 when the source did not say. */
17
+ size: number;
18
+ }
19
+ /** Files under `path` in a Hugging Face dataset repo's main branch, filtered to
20
+ * an extension. Returns repo-relative paths (e.g. "smolsent/ha_en.jsonl"),
21
+ * sorted, so a run's unit order is stable across machines. */
22
+ export declare function hfTree(dataset: string, path: string, ext: RegExp, label: string, opts: HttpOptions): Promise<Listed[]>;
23
+ /** A dataset's Parquet shards on Hugging Face's auto-converted
24
+ * `refs/convert/parquet` branch, restricted to `config` and to `splits`.
25
+ *
26
+ * The converted branch is used rather than `main` because a dataset's own
27
+ * Parquet may be written as ONE giant row-group (SODA's is 1,191,582 rows),
28
+ * and a column chunk is per-group, so reading any part of it materialises all
29
+ * of it. The converted branch is uniformly 10,000-row groups.
30
+ *
31
+ * Paths look like "<config>/<split>/0000.parquet". The BRANCH name is a single
32
+ * path SEGMENT here, so its "/" is percent-encoded — unlike a dataset id,
33
+ * whose "/" must not be. */
34
+ export declare function hfConvertedParquet(dataset: string, config: string, splits: string[], label: string, opts: HttpOptions, note?: Note): Promise<Listed[]>;
35
+ /** File NAMES (not paths) in one directory of a GitHub repo, filtered to an
36
+ * extension and sorted. Used for corpora served from GitHub raw rather than
37
+ * Hugging Face — where the HF mirrors are loading-script repos with no data
38
+ * files, the official GitHub copy is the one carrying the licence notice. */
39
+ export declare function githubContents(repo: string, dir: string, ext: RegExp, label: string, opts: HttpOptions): Promise<Listed[]>;
40
+ /** Every file in a local directory matching `ext`, sorted. A missing directory
41
+ * is an empty list, not an error: LOCAL_PATH is an offline convenience and a
42
+ * stage with no local copy simply reports that and moves on. */
43
+ export declare function localFiles(dir: string, ext: RegExp): Listed[];
44
+ /** The FIRST file in a local directory matching any of `exts`, in directory
45
+ * order (deliberately NOT sorted — this mirrors the single-file stages, which
46
+ * take whichever copy the filesystem hands back first). Null when none match
47
+ * or the directory is absent. */
48
+ export declare function localFind(dir: string, ...exts: RegExp[]): string | null;