@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.
- package/AGENTS.md +20 -4
- package/DATASETS.md +12 -11
- package/dist/example/train_base/cache.d.ts +35 -0
- package/dist/example/train_base/cache.js +211 -0
- package/dist/example/train_base/config.d.ts +21 -0
- package/dist/example/train_base/config.js +94 -0
- package/dist/example/train_base/corpora/aya.d.ts +19 -0
- package/dist/example/train_base/corpora/aya.js +76 -0
- package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
- package/dist/example/train_base/corpora/converted-parquet.js +44 -0
- package/dist/example/train_base/corpora/genknow.d.ts +14 -0
- package/dist/example/train_base/corpora/genknow.js +83 -0
- package/dist/example/train_base/corpora/index.d.ts +29 -0
- package/dist/example/train_base/corpora/index.js +81 -0
- package/dist/example/train_base/corpora/massive.d.ts +7 -0
- package/dist/example/train_base/corpora/massive.js +98 -0
- package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
- package/dist/example/train_base/corpora/oasst2.js +120 -0
- package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
- package/dist/example/train_base/corpora/smolsent.js +156 -0
- package/dist/example/train_base/corpora/soda.d.ts +12 -0
- package/dist/example/train_base/corpora/soda.js +113 -0
- package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
- package/dist/example/train_base/corpora/taskmaster.js +144 -0
- package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
- package/dist/example/train_base/corpora/wiki2.js +132 -0
- package/dist/example/train_base/corpus.d.ts +88 -0
- package/dist/example/train_base/corpus.js +65 -0
- package/dist/example/train_base/discovery.d.ts +48 -0
- package/dist/example/train_base/discovery.js +143 -0
- package/dist/example/train_base/http.d.ts +82 -0
- package/dist/example/train_base/http.js +219 -0
- package/dist/example/train_base/items.d.ts +46 -0
- package/dist/example/train_base/items.js +98 -0
- package/dist/example/train_base/main.d.ts +4 -0
- package/dist/example/train_base/main.js +207 -0
- package/dist/example/train_base/progress.d.ts +34 -0
- package/dist/example/train_base/progress.js +114 -0
- package/dist/example/train_base/readers.d.ts +125 -0
- package/dist/example/train_base/readers.js +391 -0
- package/dist/example/train_base/runtime.d.ts +115 -0
- package/dist/example/train_base/runtime.js +637 -0
- package/dist/example/train_base/stage.d.ts +3 -0
- package/dist/example/train_base/stage.js +246 -0
- package/dist/example/train_base/ui.d.ts +88 -0
- package/dist/example/train_base/ui.js +272 -0
- package/dist/src/mind/mind.d.ts +1 -1
- package/dist/src/mind/mind.js +1 -1
- package/example/train_base/cache.ts +251 -0
- package/example/train_base/config.ts +128 -0
- package/example/train_base/corpora/aya.ts +106 -0
- package/example/train_base/corpora/converted-parquet.ts +64 -0
- package/example/train_base/corpora/genknow.ts +114 -0
- package/example/train_base/corpora/index.ts +88 -0
- package/example/train_base/corpora/massive.ts +111 -0
- package/example/train_base/corpora/oasst2.ts +163 -0
- package/example/train_base/corpora/smolsent.ts +203 -0
- package/example/train_base/corpora/soda.ts +130 -0
- package/example/train_base/corpora/taskmaster.ts +217 -0
- package/example/train_base/corpora/wiki2.ts +190 -0
- package/example/train_base/corpus.ts +150 -0
- package/example/train_base/discovery.ts +203 -0
- package/example/train_base/http.ts +284 -0
- package/example/train_base/items.ts +118 -0
- package/example/train_base/main.ts +240 -0
- package/example/train_base/progress.ts +149 -0
- package/example/train_base/readers.ts +505 -0
- package/example/train_base/runtime.ts +894 -0
- package/example/train_base/stage.ts +276 -0
- package/example/train_base/ui.ts +333 -0
- package/jsr.json +1 -1
- package/package.json +2 -4
- package/src/mind/mind.ts +1 -1
- package/test/13-conversation.test.mjs +1 -1
- package/test/84-composed-answer-honesty.test.mjs +2 -1
- package/test/88-dependency-footprint.test.mjs +99 -0
- package/dist/example/train_base.d.ts +0 -163
- package/dist/example/train_base.js +0 -3220
- package/example/train_base.ts +0 -3882
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
|
|
8
|
+
import { env } from "../config.js";
|
|
9
|
+
import { refineItems, type TrainingItem } from "../items.js";
|
|
10
|
+
import { parquet } from "../readers.js";
|
|
11
|
+
import type { Corpus } from "../corpus.js";
|
|
12
|
+
import { convertedParquetUnits } from "./converted-parquet.js";
|
|
13
|
+
|
|
14
|
+
// ── 2WikiMultihopQA — the `evidences` TRIPLES only (the composition stage) ──
|
|
15
|
+
// Each row carries `evidences`: a JSON string of (subject, relation, object)
|
|
16
|
+
// triples that CHAIN — one triple's object is the next's subject. 72.5% of rows
|
|
17
|
+
// carry such a chain (measured over 4,000 rows), and those triples are the only
|
|
18
|
+
// representation measured to make Sema compose a two-hop answer at all.
|
|
19
|
+
//
|
|
20
|
+
// TWO COLUMNS ARE DELIBERATELY NOT READ, one for licence reasons and one for
|
|
21
|
+
// capability reasons:
|
|
22
|
+
// • `context` holds Wikipedia PROSE. The repo is Apache-2.0 but Wikipedia text
|
|
23
|
+
// is CC BY-SA, and a Sema store keeps text verbatim, so ingesting the
|
|
24
|
+
// passages would attach ShareAlike to every distributed store. The triples
|
|
25
|
+
// originate in Wikidata (CC0). See DATASETS.md §3.2/§4.
|
|
26
|
+
// • `question`/`answer` are the composed multi-hop QUESTION. Depositing those
|
|
27
|
+
// teaches the answer to that exact question and nothing else — it memorises
|
|
28
|
+
// rather than composes. They are used to EVALUATE this adapter, never as
|
|
29
|
+
// training input.
|
|
30
|
+
//
|
|
31
|
+
// Read from Hugging Face's auto-converted `refs/convert/parquet` branch, not
|
|
32
|
+
// from main: the main-branch train.parquet is written as ONE 167,454-row
|
|
33
|
+
// group (666 MB uncompressed) and a Parquet column chunk is per-group, so any
|
|
34
|
+
// read of it materialises the whole file. The converted branch uses uniform
|
|
35
|
+
// 10,000-row groups, which `parquetBatchRows` then subdivides by BYTES.
|
|
36
|
+
const WIKI2 = env("WIKI2", "1") !== "0";
|
|
37
|
+
const WIKI2_DATASET = env("WIKI2_DATASET", "xanhho/2WikiMultihopQA");
|
|
38
|
+
// Splits to train, in order. Only `train` by default: `validation`/`test` are
|
|
39
|
+
// the dataset's held-out sets and are what an honest evaluation of this
|
|
40
|
+
// adapter's composition rate has to be measured on.
|
|
41
|
+
const WIKI2_SPLITS = env("WIKI2_SPLITS", "train")
|
|
42
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
43
|
+
// Reject a triple with an implausibly long field (corruption); real subjects and
|
|
44
|
+
// objects are entity names, and relations are Wikidata property labels.
|
|
45
|
+
// 0 = every row. The train split holds 167,454 rows at ~4.95 deposits each
|
|
46
|
+
// (~830k facts), so this is the knob that keeps 2Wiki proportionate to the rest
|
|
47
|
+
// of the curriculum in the same way SODA_MAX_DIALOGS does.
|
|
48
|
+
const WIKI2_MAX_ROWS = Math.max(
|
|
49
|
+
0,
|
|
50
|
+
Math.floor(Number(env("WIKI2_MAX_ROWS", "0"))) || 0,
|
|
51
|
+
);
|
|
52
|
+
const MAX_WIKI2_FIELD_CHARS = Math.max(
|
|
53
|
+
100,
|
|
54
|
+
Math.floor(Number(env("MAX_WIKI2_FIELD_KB", "2")) * 1000) || 2_000,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
58
|
+
// §6e″ 2WikiMultihopQA parsing — `evidences` TRIPLES → SEMA facts
|
|
59
|
+
//
|
|
60
|
+
// This is the only stage whose purpose is COMPOSITION: answering a question
|
|
61
|
+
// whose answer no single deposited fact contains. Sema composes by grounding
|
|
62
|
+
// hop 1, then pivoting on the longest unconsumed learnt context that the
|
|
63
|
+
// grounded answer CONTAINS (`reason`/`pivotStep`), so the pivot target must
|
|
64
|
+
// itself be a deposited context. Each triple therefore deposits TWO facts:
|
|
65
|
+
//
|
|
66
|
+
// "<subject> <relation>" → "The <relation> of <subject> is <object>."
|
|
67
|
+
// "<subject>" → "The <relation> of <subject> is <object>."
|
|
68
|
+
//
|
|
69
|
+
// The second is the PIVOT FACT. Without it the bare entity naming hop 2's
|
|
70
|
+
// subject is not a learnt context, so the chain is structurally unreachable no
|
|
71
|
+
// matter what the rest of the pipeline does.
|
|
72
|
+
//
|
|
73
|
+
// MEASURED on 200 real chained dev rows, depositing triples only and asking the
|
|
74
|
+
// dataset's own composed questions (D = 1024, seed 7):
|
|
75
|
+
//
|
|
76
|
+
// relation fact only 240 deposits 5/120 ( 4%) pivotStep 0
|
|
77
|
+
// relation + pivot fact 800 deposits 44/200 (22%) pivotStep 31
|
|
78
|
+
//
|
|
79
|
+
// A 5x improvement, and the only variant where the second hop fires at all.
|
|
80
|
+
//
|
|
81
|
+
// REJECTED ALTERNATIVE, so it is not re-tried blind: depositing the pivot fact
|
|
82
|
+
// only for subjects that also appear as an OBJECT within the same row's
|
|
83
|
+
// evidences (a row-local "something can pivot into this" test) cut deposits 25%
|
|
84
|
+
// (800 → 600) but cost composition — 41/200 (20.5%) with pivotStep down to 21,
|
|
85
|
+
// because real chains also run BETWEEN rows. Composition is this stage's entire
|
|
86
|
+
// justification, so the deposits are worth keeping.
|
|
87
|
+
//
|
|
88
|
+
// The residual ~78% is a KNOWN, previously-recorded limitation and not a defect
|
|
89
|
+
// in this adapter: the climb elects a topic rather than a relation, so a
|
|
90
|
+
// question phrased "When did X's father die?" does not align with the Wikidata
|
|
91
|
+
// property label "date of death". Failure is dominated by hop 2 never firing,
|
|
92
|
+
// not by a wrong hop 2. Answer-shape breakdown at N = 120: entity answers
|
|
93
|
+
// 23/106, date answers 2/14 — dates are worse, but not the cliff an earlier
|
|
94
|
+
// note suggested, which is why no object-shape filter is applied here.
|
|
95
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
96
|
+
|
|
97
|
+
/** One (subject, relation, object) triple from a 2Wiki `evidences` cell. */
|
|
98
|
+
export interface WikiTriple {
|
|
99
|
+
subject: string;
|
|
100
|
+
relation: string;
|
|
101
|
+
object: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Normalize a 2Wiki row into its evidence triples, or null when it carries
|
|
105
|
+
* none usable. `evidences` is a JSON STRING holding an array of 3-element
|
|
106
|
+
* arrays; a row whose cell is absent, unparseable, or empty yields null.
|
|
107
|
+
* Individual malformed or oversized triples are dropped without discarding the
|
|
108
|
+
* row — one bad triple should not cost the others. */
|
|
109
|
+
export function toWikiTriples(
|
|
110
|
+
row: unknown,
|
|
111
|
+
maxChars = MAX_WIKI2_FIELD_CHARS,
|
|
112
|
+
): WikiTriple[] | null {
|
|
113
|
+
if (!row || typeof row !== "object") return null;
|
|
114
|
+
const cell = (row as Record<string, unknown>).evidences;
|
|
115
|
+
let parsed: unknown = cell;
|
|
116
|
+
if (typeof cell === "string") {
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(cell);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(parsed)) return null;
|
|
124
|
+
const out: WikiTriple[] = [];
|
|
125
|
+
for (const e of parsed) {
|
|
126
|
+
if (!Array.isArray(e) || e.length < 3) continue;
|
|
127
|
+
const subject = typeof e[0] === "string" ? e[0].trim() : "";
|
|
128
|
+
const relation = typeof e[1] === "string" ? e[1].trim() : "";
|
|
129
|
+
const object = typeof e[2] === "string" ? e[2].trim() : "";
|
|
130
|
+
if (!subject || !relation || !object) continue;
|
|
131
|
+
if (
|
|
132
|
+
subject.length > maxChars ||
|
|
133
|
+
relation.length > maxChars ||
|
|
134
|
+
object.length > maxChars
|
|
135
|
+
) continue;
|
|
136
|
+
out.push({ subject, relation, object });
|
|
137
|
+
}
|
|
138
|
+
return out.length ? out : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Render ONE triple as the prose fact Sema stores. Kept separate so the two
|
|
142
|
+
* deposits below are guaranteed to share a byte-identical continuation: the
|
|
143
|
+
* pivot fact only works if it leads to the SAME node the relation fact does. */
|
|
144
|
+
export function wikiTripleSentence(t: WikiTriple): string {
|
|
145
|
+
return `The ${t.relation} of ${t.subject} is ${t.object}.`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Translate a row's triples into SEMA items: per triple, the relation fact and
|
|
149
|
+
* the bare-subject PIVOT fact (see the section note above). refineItems drops
|
|
150
|
+
* the duplicates this produces when a row states the same triple twice. */
|
|
151
|
+
export function wikiTriplesToItems(triples: WikiTriple[]): TrainingItem[] {
|
|
152
|
+
const items: TrainingItem[] = [];
|
|
153
|
+
for (const t of triples) {
|
|
154
|
+
const fact = wikiTripleSentence(t);
|
|
155
|
+
items.push({ context: `${t.subject} ${t.relation}`, continuation: fact });
|
|
156
|
+
items.push({ context: t.subject, continuation: fact });
|
|
157
|
+
}
|
|
158
|
+
return refineItems(items);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const wiki2: Corpus = {
|
|
162
|
+
id: "2wiki",
|
|
163
|
+
label: "2Wiki",
|
|
164
|
+
kind: "relation triples",
|
|
165
|
+
enabled: WIKI2,
|
|
166
|
+
maxRows: WIKI2_MAX_ROWS,
|
|
167
|
+
// Read ONE of the file's seven columns. This is where the "context is not
|
|
168
|
+
// read" rule above stops being a property of the adapter and becomes a
|
|
169
|
+
// property of the read: the Wikipedia prose is never decoded at all. It is
|
|
170
|
+
// also almost the whole file — measured on the converted train shard, the
|
|
171
|
+
// seven columns hold 518 MB uncompressed and `evidences` is 18 MB of it
|
|
172
|
+
// (3.5%), so the projection cuts the decode work by ~28x.
|
|
173
|
+
read: parquet({ columns: ["evidences"] }),
|
|
174
|
+
toItems: (row) => {
|
|
175
|
+
const triples = toWikiTriples(row);
|
|
176
|
+
if (!triples) return null;
|
|
177
|
+
const items = wikiTriplesToItems(triples);
|
|
178
|
+
return items.length ? items : null;
|
|
179
|
+
},
|
|
180
|
+
unitNoun: "shard(s)",
|
|
181
|
+
log: { rows: "row(s)" },
|
|
182
|
+
discover: convertedParquetUnits({
|
|
183
|
+
id: "2wiki",
|
|
184
|
+
label: "2Wiki",
|
|
185
|
+
dataset: WIKI2_DATASET,
|
|
186
|
+
config: "default",
|
|
187
|
+
splits: WIKI2_SPLITS,
|
|
188
|
+
localSub: "2wiki",
|
|
189
|
+
}),
|
|
190
|
+
};
|