@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.
- package/AGENTS.md +43 -4
- package/DATASETS.md +160 -0
- package/README.md +12 -0
- 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/geometry.d.ts +20 -0
- package/dist/src/geometry.js +22 -0
- package/dist/src/mind/attention.d.ts +6 -0
- package/dist/src/mind/attention.js +44 -4
- package/dist/src/mind/learning.js +134 -50
- package/dist/src/mind/mechanisms/cast.js +45 -1
- package/dist/src/mind/mind.d.ts +7 -2
- package/dist/src/mind/mind.js +15 -3
- package/dist/src/mind/reasoning.js +59 -5
- package/dist/src/mind/recognition.js +29 -3
- package/dist/src/mind/traverse.d.ts +16 -0
- package/dist/src/mind/traverse.js +18 -0
- package/dist/src/store-sqlite.d.ts +4 -0
- package/dist/src/store-sqlite.js +47 -0
- package/dist/src/store.d.ts +7 -0
- 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/geometry.ts +23 -0
- package/src/mind/attention.ts +54 -1
- package/src/mind/learning.ts +137 -43
- package/src/mind/mechanisms/cast.ts +48 -1
- package/src/mind/mind.ts +13 -2
- package/src/mind/reasoning.ts +64 -5
- package/src/mind/recognition.ts +29 -3
- package/src/mind/traverse.ts +19 -0
- package/src/store-sqlite.ts +53 -0
- package/src/store.ts +28 -0
- package/test/13-conversation.test.mjs +1 -1
- package/test/29-counterfactual.test.mjs +43 -6
- package/test/77-company-saturation.test.mjs +302 -0
- package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
- package/test/84-composed-answer-honesty.test.mjs +137 -0
- package/test/85-answered-directly.test.mjs +126 -0
- package/test/86-cast-voices-committed.test.mjs +164 -0
- package/test/87-codominant-commitment.test.mjs +250 -0
- package/test/88-dependency-footprint.test.mjs +99 -0
- package/dist/example/train_base.d.ts +0 -93
- package/dist/example/train_base.js +0 -2269
- package/example/train_base.ts +0 -2735
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
import { env, LOCAL_PATH } from "../config.js";
|
|
8
|
+
import { refineItems } from "../items.js";
|
|
9
|
+
import { hfTree, localFiles } from "../discovery.js";
|
|
10
|
+
import { lines } from "../readers.js";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
// ── google/smol · SmolSent (the first training stage) ──
|
|
13
|
+
// SmolSent is Google's sentence-level translation set: ~863 human sentence pairs
|
|
14
|
+
// per language pair across 100+ low-resource languages, cc-by-4.0 (commercial-
|
|
15
|
+
// friendly). Each row is {sl, tl, src, trg, …} — a source sentence and its
|
|
16
|
+
// translation. A pair is "two names for one meaning", which is exactly the
|
|
17
|
+
// cross-language concept SEMA fuses (see test/05-concepts.test.mjs), so each row
|
|
18
|
+
// becomes FACTS that bind the two phrasings as one concept at recall time.
|
|
19
|
+
//
|
|
20
|
+
// The corpus ships as one plain JSONL file PER language pair under smolsent/ in
|
|
21
|
+
// the HF repo (e.g. smolsent/ha_en.jsonl). We DOWNLOAD each file and stream its
|
|
22
|
+
// lines — far faster and free of the rate-limiting that per-row API paging hit.
|
|
23
|
+
// The file list is discovered from the HF repo tree. SMOLSENT=0 disables the
|
|
24
|
+
// stage; SMOLSENT_PAIRS (comma-separated basenames without .jsonl, e.g.
|
|
25
|
+
// "ha_en,zu_en") restricts to a chosen subset.
|
|
26
|
+
const SMOLSENT = env("SMOLSENT", "1") !== "0";
|
|
27
|
+
const SMOLSENT_DATASET = env("SMOLSENT_DATASET", "google/smol");
|
|
28
|
+
const SMOLSENT_PAIRS = (process.env.SMOLSENT_PAIRS ?? "")
|
|
29
|
+
.split(",").map((s) => s.trim()).filter(Boolean);
|
|
30
|
+
// The resume id PREFIX for the SmolSent stage; one completed-files entry per
|
|
31
|
+
// file (e.g. "smolsent::ha_en.jsonl").
|
|
32
|
+
const SMOLSENT_ID = "smolsent";
|
|
33
|
+
// Which direction(s) of a translation pair to deposit. Was effectively "both",
|
|
34
|
+
// and that is now the default NO longer, for a reason measured rather than
|
|
35
|
+
// assumed.
|
|
36
|
+
//
|
|
37
|
+
// SmolSent's English side is a SHARED POOL translated into every language: row
|
|
38
|
+
// id 0 of smolsent/ha_en.jsonl, zu_en.jsonl and am_en.jsonl all carry the SAME
|
|
39
|
+
// `trg` ("It allows me to work by following my vibes and ..."). The two
|
|
40
|
+
// directions are therefore not symmetric at all:
|
|
41
|
+
//
|
|
42
|
+
// src2trg (foreign -> English) many distinct contexts -> ONE shared
|
|
43
|
+
// continuation. Every language's rendering of
|
|
44
|
+
// a meaning converges on the same English
|
|
45
|
+
// node — the cross-language concept fusion
|
|
46
|
+
// this stage exists for.
|
|
47
|
+
// trg2src (English -> foreign) ONE context -> 100+ DIFFERENT continuations,
|
|
48
|
+
// one per language file. The same English
|
|
49
|
+
// sentence is deposited over and over with a
|
|
50
|
+
// different answer each time.
|
|
51
|
+
//
|
|
52
|
+
// So dropping trg2src is not merely a corpus-size economy (it halves the
|
|
53
|
+
// largest stage, which was 60.9% of all examples in the last trained store); it
|
|
54
|
+
// removes a genuine ambiguity pathology. Set SMOLSENT_DIRECTIONS=both to
|
|
55
|
+
// restore the old behaviour, or trg2src for English->foreign only.
|
|
56
|
+
//
|
|
57
|
+
// WHAT THE CUT DOES NOT DO, measured on a three-pair store: asking the English
|
|
58
|
+
// sentence still ANSWERS with a foreign rendering, because the engine can reach
|
|
59
|
+
// a shared continuation's predecessors on its own. What is removed is the
|
|
60
|
+
// DEPOSITED forward ambiguity — one context carrying ~100 competing
|
|
61
|
+
// continuations — not every reverse association.
|
|
62
|
+
const SMOLSENT_DIRECTIONS = env("SMOLSENT_DIRECTIONS", "src2trg")
|
|
63
|
+
.trim().toLowerCase();
|
|
64
|
+
const SMOLSENT_SRC2TRG = SMOLSENT_DIRECTIONS !== "trg2src";
|
|
65
|
+
const SMOLSENT_TRG2SRC = SMOLSENT_DIRECTIONS === "trg2src" ||
|
|
66
|
+
SMOLSENT_DIRECTIONS === "both";
|
|
67
|
+
// A SmolSent side longer than this is skipped (a sentence pair is short; a huge
|
|
68
|
+
// value is corruption, not a sentence).
|
|
69
|
+
const MAX_SMOLSENT_CHARS = Math.max(2_000, Math.floor(Number(env("MAX_SMOLSENT_KB", "16")) * 1000) || 16_000);
|
|
70
|
+
/** Normalize a raw datasets-server row into a SmolSentRow, or null when it lacks
|
|
71
|
+
* both sides or a side is implausibly large (a dump, not a sentence). */
|
|
72
|
+
export function toSmolSentRow(row, maxChars = MAX_SMOLSENT_CHARS) {
|
|
73
|
+
if (!row || typeof row !== "object")
|
|
74
|
+
return null;
|
|
75
|
+
const r = row;
|
|
76
|
+
const src = typeof r.src === "string" ? r.src.trim() : "";
|
|
77
|
+
// `trg` is a single string in smolsent; tolerate a list form defensively.
|
|
78
|
+
const trgRaw = Array.isArray(r.trgs) ? r.trgs[0] : r.trg;
|
|
79
|
+
const trg = typeof trgRaw === "string" ? trgRaw.trim() : "";
|
|
80
|
+
if (!src || !trg)
|
|
81
|
+
return null;
|
|
82
|
+
if (src.length > maxChars || trg.length > maxChars)
|
|
83
|
+
return null;
|
|
84
|
+
const sl = typeof r.sl === "string" ? r.sl.trim() : "";
|
|
85
|
+
const tl = typeof r.tl === "string" ? r.tl.trim() : "";
|
|
86
|
+
return { src, trg, sl, tl };
|
|
87
|
+
}
|
|
88
|
+
/** Translate ONE SmolSent pair into SEMA facts. The two sentences are one
|
|
89
|
+
* meaning in two languages, but the two BINDINGS are not equally sound —
|
|
90
|
+
* SmolSent's English side is a shared pool translated into every language, so
|
|
91
|
+
* `trg -> src` gives one English context a different answer in every language
|
|
92
|
+
* file. See SMOLSENT_DIRECTIONS. refineItems drops the degenerate case where
|
|
93
|
+
* src === trg. */
|
|
94
|
+
export function smolSentRowToItems(row, dirs = {
|
|
95
|
+
src2trg: SMOLSENT_SRC2TRG,
|
|
96
|
+
trg2src: SMOLSENT_TRG2SRC,
|
|
97
|
+
}) {
|
|
98
|
+
const { src, trg } = row;
|
|
99
|
+
const items = [];
|
|
100
|
+
if (dirs.src2trg)
|
|
101
|
+
items.push({ context: src, continuation: trg });
|
|
102
|
+
if (dirs.trg2src)
|
|
103
|
+
items.push({ context: trg, continuation: src });
|
|
104
|
+
return refineItems(items);
|
|
105
|
+
}
|
|
106
|
+
/** Discover the SmolSent per-pair JSONL files from the HF repo tree, restricted
|
|
107
|
+
* to SMOLSENT_PAIRS (basenames without .jsonl) when set. Each entry is the
|
|
108
|
+
* repo-relative path, e.g. "smolsent/ha_en.jsonl". */
|
|
109
|
+
async function listFiles(ctx) {
|
|
110
|
+
const paths = await hfTree(SMOLSENT_DATASET, "smolsent", /\.jsonl$/i, `GET smol tree`, ctx.http);
|
|
111
|
+
if (!SMOLSENT_PAIRS.length)
|
|
112
|
+
return paths;
|
|
113
|
+
const want = new Set(SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")));
|
|
114
|
+
return paths.filter((p) => want.has(basename(p.path).replace(/\.jsonl$/i, "")));
|
|
115
|
+
}
|
|
116
|
+
const unit = (name) => ({
|
|
117
|
+
key: name,
|
|
118
|
+
// The log names a pair by its language code alone; the panel keeps the file.
|
|
119
|
+
name: name.replace(/\.jsonl$/i, ""),
|
|
120
|
+
display: `SmolSent ${name}`,
|
|
121
|
+
});
|
|
122
|
+
export const smolsent = {
|
|
123
|
+
id: SMOLSENT_ID,
|
|
124
|
+
label: "SmolSent",
|
|
125
|
+
kind: "translation",
|
|
126
|
+
enabled: SMOLSENT,
|
|
127
|
+
unitNoun: "translation file(s)",
|
|
128
|
+
// The *4 is chars-to-a-generous-JSON-envelope: the guard bounds ONE side of
|
|
129
|
+
// a pair, the line carries both plus its keys.
|
|
130
|
+
read: lines({ maxLineChars: MAX_SMOLSENT_CHARS * 4 }),
|
|
131
|
+
toItems: (row) => {
|
|
132
|
+
const r = toSmolSentRow(row);
|
|
133
|
+
return r ? smolSentRowToItems(r) : null;
|
|
134
|
+
},
|
|
135
|
+
async discover(ctx) {
|
|
136
|
+
// Work-list: local *.jsonl in LOCAL_PATH, else the repo's smolsent/ files.
|
|
137
|
+
if (LOCAL_PATH) {
|
|
138
|
+
let names = localFiles(LOCAL_PATH, /\.jsonl$/i);
|
|
139
|
+
if (SMOLSENT_PAIRS.length) {
|
|
140
|
+
const want = new Set(SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")));
|
|
141
|
+
names = names.filter((n) => want.has(n.path.replace(/\.jsonl$/i, "")));
|
|
142
|
+
}
|
|
143
|
+
return names.map((n) => ({
|
|
144
|
+
...unit(n.path),
|
|
145
|
+
local: join(LOCAL_PATH, n.path),
|
|
146
|
+
bytes: n.size,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
return (await listFiles(ctx)).map(({ path, size }) => ({
|
|
150
|
+
...unit(basename(path)),
|
|
151
|
+
// owner/name and the file path are URL PATH segments — do not encode "/".
|
|
152
|
+
url: `https://huggingface.co/datasets/${SMOLSENT_DATASET}/resolve/main/${path}`,
|
|
153
|
+
bytes: size,
|
|
154
|
+
}));
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -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
|
+
};
|