textopt 0.0.0 → 0.1.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/README.md +44 -22
- package/dist/bootstrap-search/index.cjs +153 -73
- package/dist/bootstrap-search/index.d.cts +32 -10
- package/dist/bootstrap-search/index.d.mts +32 -10
- package/dist/bootstrap-search/index.mjs +144 -66
- package/dist/{demos-B0pVQjYC.d.mts → demos-1b7JiUue.d.mts} +10 -3
- package/dist/{demos-BTuzFNsp.d.cts → demos-CU9dy2oT.d.cts} +10 -3
- package/dist/demos-D2o0qSSo.cjs +244 -0
- package/dist/demos-DE2oxNWX.mjs +215 -0
- package/dist/file-cache.cjs +11 -3
- package/dist/file-cache.mjs +11 -3
- package/dist/gepa/index.cjs +76 -71
- package/dist/gepa/index.d.cts +12 -6
- package/dist/gepa/index.d.mts +12 -6
- package/dist/gepa/index.mjs +49 -46
- package/dist/index.cjs +129 -27
- package/dist/index.d.cts +145 -7
- package/dist/index.d.mts +145 -7
- package/dist/index.mjs +113 -15
- package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
- package/dist/math-Dqme4rYz.mjs +123 -0
- package/dist/mipro/index.cjs +98 -70
- package/dist/mipro/index.d.cts +17 -14
- package/dist/mipro/index.d.mts +17 -14
- package/dist/mipro/index.mjs +84 -58
- package/dist/opro/index.cjs +130 -51
- package/dist/opro/index.d.cts +17 -9
- package/dist/opro/index.d.mts +17 -9
- package/dist/opro/index.mjs +115 -38
- package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-Bh5DPRMH.d.cts} +50 -4
- package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ck6-e_8o.d.mts} +50 -4
- package/dist/random-search/index.cjs +93 -49
- package/dist/random-search/index.d.cts +15 -13
- package/dist/random-search/index.d.mts +15 -13
- package/dist/random-search/index.mjs +83 -41
- package/dist/{reflection-CQToe-5B.d.cts → reflection-Dt3QrXhM.d.cts} +7 -11
- package/dist/{reflection-Cr_upzU0.d.mts → reflection-LRaAZP4e.d.mts} +7 -11
- package/dist/{evaluation-OZOp6TB7.cjs → reporting-CNHzbJC-.cjs} +165 -5
- package/dist/reporting-DQbAohc9.d.cts +240 -0
- package/dist/reporting-DQbAohc9.d.mts +240 -0
- package/dist/{evaluation-BV0nSZVx.mjs → reporting-DY-DC4HG.mjs} +124 -6
- package/dist/simba/index.cjs +210 -83
- package/dist/simba/index.d.cts +32 -11
- package/dist/simba/index.d.mts +32 -11
- package/dist/simba/index.mjs +200 -75
- package/dist/testing.cjs +1 -0
- package/dist/testing.d.cts +5 -3
- package/dist/testing.d.mts +5 -3
- package/dist/testing.mjs +1 -1
- package/package.json +4 -3
- package/dist/demos-B9BJiNKz.cjs +0 -143
- package/dist/demos-Degx6UmP.mjs +0 -126
- package/dist/math-DhrDmpFS.mjs +0 -78
- package/dist/types-CWv4IQFF.d.cts +0 -129
- package/dist/types-CWv4IQFF.d.mts +0 -129
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const require_reporting = require("./reporting-CNHzbJC-.cjs");
|
|
2
|
+
//#region src/harvest.ts
|
|
3
|
+
/**
|
|
4
|
+
* Run a candidate over data and keep the rollouts the metric rewarded.
|
|
5
|
+
*
|
|
6
|
+
* The library's one paid collection primitive, with two consumers: a few-shot
|
|
7
|
+
* block wants four of these, and a distillation set wants thousands. Both are
|
|
8
|
+
* the same pass — run the candidate, score it, keep what cleared the bar — so
|
|
9
|
+
* both share the budget, retry and transient-failure handling that pass needs.
|
|
10
|
+
*
|
|
11
|
+
* Which data to sweep is the caller's decision and the consequential one. A
|
|
12
|
+
* validation set is the wrong choice: it is the set that selected the candidate,
|
|
13
|
+
* so the rollouts it yields are enriched for the candidate's fit to those
|
|
14
|
+
* instances rather than to the task. Prefer the training set, or a pool held
|
|
15
|
+
* out of the run entirely.
|
|
16
|
+
*/
|
|
17
|
+
async function harvestRollouts(args) {
|
|
18
|
+
const { adapter, candidate, data, minScore, maxRollouts = Number.POSITIVE_INFINITY, batchSize = Math.min(maxRollouts, data.length), maxMetricCalls = data.length, maxCostUsd, rng, signal } = args;
|
|
19
|
+
if (data.length === 0) throw new Error("harvestRollouts requires non-empty data");
|
|
20
|
+
const budget = require_reporting.createBudget({ maxMetricCalls });
|
|
21
|
+
const evaluator = require_reporting.createEvaluator({
|
|
22
|
+
adapter,
|
|
23
|
+
budget,
|
|
24
|
+
...signal === void 0 ? {} : { signal }
|
|
25
|
+
});
|
|
26
|
+
const order = rng === void 0 ? [...data] : rng.shuffle(data);
|
|
27
|
+
const rollouts = [];
|
|
28
|
+
let attempted = 0;
|
|
29
|
+
for (let start = 0; start < order.length; start += batchSize) {
|
|
30
|
+
if (rollouts.length >= maxRollouts || signal?.aborted) break;
|
|
31
|
+
if (require_reporting.costExhausted({
|
|
32
|
+
usage: evaluator.usage(),
|
|
33
|
+
maxCostUsd
|
|
34
|
+
})) break;
|
|
35
|
+
const batch = order.slice(start, start + Math.min(batchSize, budget.remaining()));
|
|
36
|
+
if (batch.length === 0) break;
|
|
37
|
+
const evaluation = await evaluator.evaluateTraced({
|
|
38
|
+
candidate,
|
|
39
|
+
batch,
|
|
40
|
+
split: "train",
|
|
41
|
+
phase: "seed",
|
|
42
|
+
candidateId: null,
|
|
43
|
+
iteration: 0
|
|
44
|
+
});
|
|
45
|
+
if (evaluation === null) break;
|
|
46
|
+
attempted += batch.length;
|
|
47
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
48
|
+
const score = evaluation.scores[index];
|
|
49
|
+
if (!(minScore === void 0 ? score > 0 : score >= minScore) || rollouts.length >= maxRollouts) continue;
|
|
50
|
+
rollouts.push({
|
|
51
|
+
input: batch[index],
|
|
52
|
+
output: evaluation.outputs[index],
|
|
53
|
+
score
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
rollouts,
|
|
59
|
+
metricCalls: budget.spent(),
|
|
60
|
+
usage: evaluator.usage(),
|
|
61
|
+
attempted
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/demos.ts
|
|
66
|
+
const DEMO_OPEN = "<demo>";
|
|
67
|
+
const DEMO_CLOSE = "</demo>";
|
|
68
|
+
const DEMO_BLOCK = /<demo>\s*([\s\S]*?)\s*<\/demo>/g;
|
|
69
|
+
const DEMO_PARTS = /<input>\s*([\s\S]*?)\s*<\/input>\s*<output>\s*([\s\S]*?)\s*<\/output>/;
|
|
70
|
+
const DELIMITER_TAG = /<(\/?)(demo|input|output)>/g;
|
|
71
|
+
const ESCAPED_DELIMITER_TAG = /<(\/?)(demo|input|output)>/g;
|
|
72
|
+
const DEFAULT_MAX_DEMOS = 4;
|
|
73
|
+
/**
|
|
74
|
+
* Harvest demonstrations by running a candidate over the training set and keeping
|
|
75
|
+
* the rollouts the metric rewarded.
|
|
76
|
+
*
|
|
77
|
+
* The cheapest signal in the whole library: a rollout that scored well is
|
|
78
|
+
* already paid for, and turning it into a few-shot block costs one pass over
|
|
79
|
+
* the data rather than a search. Instruction search and demonstrations pull on
|
|
80
|
+
* different parts of a model's behaviour — instructions on what to do,
|
|
81
|
+
* examples on what the output should look like — so a seed carrying both
|
|
82
|
+
* starts somewhere neither reaches alone.
|
|
83
|
+
*/
|
|
84
|
+
async function harvestFewShotExamples(args) {
|
|
85
|
+
const { adapter, candidate, trainingSet, minScore, maxDemos = DEFAULT_MAX_DEMOS, batchSize = maxDemos, maxMetricCalls = trainingSet.length, maxCostUsd, rng, renderDemo, signal } = args;
|
|
86
|
+
if (trainingSet.length === 0) throw new Error("harvestFewShotExamples requires a non-empty trainingSet");
|
|
87
|
+
const harvest = await harvestRollouts({
|
|
88
|
+
adapter,
|
|
89
|
+
candidate,
|
|
90
|
+
data: trainingSet,
|
|
91
|
+
maxRollouts: maxDemos,
|
|
92
|
+
batchSize,
|
|
93
|
+
maxMetricCalls,
|
|
94
|
+
...maxCostUsd === void 0 ? {} : { maxCostUsd },
|
|
95
|
+
...minScore === void 0 ? {} : { minScore },
|
|
96
|
+
...rng === void 0 ? {} : { rng },
|
|
97
|
+
...signal === void 0 ? {} : { signal }
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
demos: harvest.rollouts,
|
|
101
|
+
block: formatDemos(harvest.rollouts, renderDemo === void 0 ? {} : { render: renderDemo }),
|
|
102
|
+
metricCalls: harvest.metricCalls,
|
|
103
|
+
usage: harvest.usage,
|
|
104
|
+
attempted: harvest.attempted
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Render demos as the text a candidate component holds.
|
|
109
|
+
*
|
|
110
|
+
* Delimited rather than free-form so `parseDemos` can read them back: a demo
|
|
111
|
+
* component is edited over the course of a run, and a block that cannot be
|
|
112
|
+
* parsed can only be replaced wholesale, throwing away every example found
|
|
113
|
+
* before it.
|
|
114
|
+
*/
|
|
115
|
+
function formatDemos(demos, options = {}) {
|
|
116
|
+
const { render = renderDefault } = options;
|
|
117
|
+
if (demos.length === 0) return "";
|
|
118
|
+
return demos.map((demo, index) => `${DEMO_OPEN}\n${render({
|
|
119
|
+
demo,
|
|
120
|
+
index
|
|
121
|
+
})}\n${DEMO_CLOSE}`).join("\n");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Rewrite the demos a component holds, leaving everything else it says intact.
|
|
125
|
+
*
|
|
126
|
+
* A component is not always only examples. SIMBA appends advice to the same
|
|
127
|
+
* text it appends demonstrations to, so replacing the component wholesale with
|
|
128
|
+
* a fresh block would delete the instructions the other mutation wrote. The
|
|
129
|
+
* replacement lands where the first demo was, so a block a caller placed after
|
|
130
|
+
* its preamble stays after it.
|
|
131
|
+
*/
|
|
132
|
+
function replaceDemos(args) {
|
|
133
|
+
const { text, demos, render } = args;
|
|
134
|
+
const block = formatDemos(demos, render === void 0 ? {} : { render });
|
|
135
|
+
const [before, ...rest] = text.replace(DEMO_BLOCK, "\0").split("\0");
|
|
136
|
+
if (rest.length === 0) return join([text, block]);
|
|
137
|
+
return join([
|
|
138
|
+
before ?? "",
|
|
139
|
+
block,
|
|
140
|
+
rest.join("")
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Recover the demos from a formatted block, ignoring anything written around
|
|
145
|
+
* them. Text a model rewrote and mangled yields the demos it left intact
|
|
146
|
+
* rather than throwing: a malformed example is worth less than the rest of the
|
|
147
|
+
* block, not more than it.
|
|
148
|
+
*/
|
|
149
|
+
function parseDemos(text) {
|
|
150
|
+
const demos = [];
|
|
151
|
+
for (const match of text.matchAll(DEMO_BLOCK)) {
|
|
152
|
+
const parts = (match[1] ?? "").match(DEMO_PARTS);
|
|
153
|
+
if (parts === null) continue;
|
|
154
|
+
const input = parseValue(parts[1] ?? "");
|
|
155
|
+
const output = parseValue(parts[2] ?? "");
|
|
156
|
+
demos.push({
|
|
157
|
+
input,
|
|
158
|
+
output
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return demos;
|
|
162
|
+
}
|
|
163
|
+
/** Joins what survived a replacement, without leaving blank runs behind. */
|
|
164
|
+
function join(parts) {
|
|
165
|
+
return parts.map((part) => part.trim()).filter((part) => part.length > 0).join("\n\n");
|
|
166
|
+
}
|
|
167
|
+
function renderDefault(args) {
|
|
168
|
+
const { demo } = args;
|
|
169
|
+
return [
|
|
170
|
+
"<input>",
|
|
171
|
+
serialize(demo.input),
|
|
172
|
+
"</input>",
|
|
173
|
+
"<output>",
|
|
174
|
+
serialize(demo.output),
|
|
175
|
+
"</output>"
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
/** Strings stay as they are; anything else is shown as JSON. */
|
|
179
|
+
function serialize(value) {
|
|
180
|
+
if (typeof value === "string") return escapeDelimiters(value);
|
|
181
|
+
try {
|
|
182
|
+
return escapeDelimiters(JSON.stringify(value, null, 2) ?? String(value));
|
|
183
|
+
} catch {
|
|
184
|
+
return escapeDelimiters(String(value));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function parseValue(text) {
|
|
188
|
+
const unescaped = unescapeDelimiters(text);
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(unescaped);
|
|
191
|
+
} catch {
|
|
192
|
+
return unescaped;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Neutralize the tags a demo block is delimited by, so a value carrying one
|
|
197
|
+
* cannot end the block it sits in.
|
|
198
|
+
*
|
|
199
|
+
* A system that quotes its own prompt back produces exactly that: the output
|
|
200
|
+
* worth keeping is a demonstration containing `</demo>`, and appending it raw
|
|
201
|
+
* closes the outer block early. What comes back is not the demo that went in,
|
|
202
|
+
* and SIMBA reparses and rewrites the block at every step, so the damage
|
|
203
|
+
* compounds over a run rather than showing up once.
|
|
204
|
+
*
|
|
205
|
+
* The escape is escaped first, so a value that already reads `<demo>`
|
|
206
|
+
* survives the round trip as itself.
|
|
207
|
+
*/
|
|
208
|
+
function escapeDelimiters(text) {
|
|
209
|
+
return text.replaceAll("<", "&lt;").replace(DELIMITER_TAG, "<$1$2>");
|
|
210
|
+
}
|
|
211
|
+
function unescapeDelimiters(text) {
|
|
212
|
+
return text.replace(ESCAPED_DELIMITER_TAG, "<$1$2>").replaceAll("&lt;", "<");
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
Object.defineProperty(exports, "formatDemos", {
|
|
216
|
+
enumerable: true,
|
|
217
|
+
get: function() {
|
|
218
|
+
return formatDemos;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
Object.defineProperty(exports, "harvestFewShotExamples", {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
get: function() {
|
|
224
|
+
return harvestFewShotExamples;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
Object.defineProperty(exports, "harvestRollouts", {
|
|
228
|
+
enumerable: true,
|
|
229
|
+
get: function() {
|
|
230
|
+
return harvestRollouts;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
Object.defineProperty(exports, "parseDemos", {
|
|
234
|
+
enumerable: true,
|
|
235
|
+
get: function() {
|
|
236
|
+
return parseDemos;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
Object.defineProperty(exports, "replaceDemos", {
|
|
240
|
+
enumerable: true,
|
|
241
|
+
get: function() {
|
|
242
|
+
return replaceDemos;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { b as createBudget, c as createEvaluator, s as costExhausted } from "./reporting-DY-DC4HG.mjs";
|
|
2
|
+
//#region src/harvest.ts
|
|
3
|
+
/**
|
|
4
|
+
* Run a candidate over data and keep the rollouts the metric rewarded.
|
|
5
|
+
*
|
|
6
|
+
* The library's one paid collection primitive, with two consumers: a few-shot
|
|
7
|
+
* block wants four of these, and a distillation set wants thousands. Both are
|
|
8
|
+
* the same pass — run the candidate, score it, keep what cleared the bar — so
|
|
9
|
+
* both share the budget, retry and transient-failure handling that pass needs.
|
|
10
|
+
*
|
|
11
|
+
* Which data to sweep is the caller's decision and the consequential one. A
|
|
12
|
+
* validation set is the wrong choice: it is the set that selected the candidate,
|
|
13
|
+
* so the rollouts it yields are enriched for the candidate's fit to those
|
|
14
|
+
* instances rather than to the task. Prefer the training set, or a pool held
|
|
15
|
+
* out of the run entirely.
|
|
16
|
+
*/
|
|
17
|
+
async function harvestRollouts(args) {
|
|
18
|
+
const { adapter, candidate, data, minScore, maxRollouts = Number.POSITIVE_INFINITY, batchSize = Math.min(maxRollouts, data.length), maxMetricCalls = data.length, maxCostUsd, rng, signal } = args;
|
|
19
|
+
if (data.length === 0) throw new Error("harvestRollouts requires non-empty data");
|
|
20
|
+
const budget = createBudget({ maxMetricCalls });
|
|
21
|
+
const evaluator = createEvaluator({
|
|
22
|
+
adapter,
|
|
23
|
+
budget,
|
|
24
|
+
...signal === void 0 ? {} : { signal }
|
|
25
|
+
});
|
|
26
|
+
const order = rng === void 0 ? [...data] : rng.shuffle(data);
|
|
27
|
+
const rollouts = [];
|
|
28
|
+
let attempted = 0;
|
|
29
|
+
for (let start = 0; start < order.length; start += batchSize) {
|
|
30
|
+
if (rollouts.length >= maxRollouts || signal?.aborted) break;
|
|
31
|
+
if (costExhausted({
|
|
32
|
+
usage: evaluator.usage(),
|
|
33
|
+
maxCostUsd
|
|
34
|
+
})) break;
|
|
35
|
+
const batch = order.slice(start, start + Math.min(batchSize, budget.remaining()));
|
|
36
|
+
if (batch.length === 0) break;
|
|
37
|
+
const evaluation = await evaluator.evaluateTraced({
|
|
38
|
+
candidate,
|
|
39
|
+
batch,
|
|
40
|
+
split: "train",
|
|
41
|
+
phase: "seed",
|
|
42
|
+
candidateId: null,
|
|
43
|
+
iteration: 0
|
|
44
|
+
});
|
|
45
|
+
if (evaluation === null) break;
|
|
46
|
+
attempted += batch.length;
|
|
47
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
48
|
+
const score = evaluation.scores[index];
|
|
49
|
+
if (!(minScore === void 0 ? score > 0 : score >= minScore) || rollouts.length >= maxRollouts) continue;
|
|
50
|
+
rollouts.push({
|
|
51
|
+
input: batch[index],
|
|
52
|
+
output: evaluation.outputs[index],
|
|
53
|
+
score
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
rollouts,
|
|
59
|
+
metricCalls: budget.spent(),
|
|
60
|
+
usage: evaluator.usage(),
|
|
61
|
+
attempted
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/demos.ts
|
|
66
|
+
const DEMO_OPEN = "<demo>";
|
|
67
|
+
const DEMO_CLOSE = "</demo>";
|
|
68
|
+
const DEMO_BLOCK = /<demo>\s*([\s\S]*?)\s*<\/demo>/g;
|
|
69
|
+
const DEMO_PARTS = /<input>\s*([\s\S]*?)\s*<\/input>\s*<output>\s*([\s\S]*?)\s*<\/output>/;
|
|
70
|
+
const DELIMITER_TAG = /<(\/?)(demo|input|output)>/g;
|
|
71
|
+
const ESCAPED_DELIMITER_TAG = /<(\/?)(demo|input|output)>/g;
|
|
72
|
+
const DEFAULT_MAX_DEMOS = 4;
|
|
73
|
+
/**
|
|
74
|
+
* Harvest demonstrations by running a candidate over the training set and keeping
|
|
75
|
+
* the rollouts the metric rewarded.
|
|
76
|
+
*
|
|
77
|
+
* The cheapest signal in the whole library: a rollout that scored well is
|
|
78
|
+
* already paid for, and turning it into a few-shot block costs one pass over
|
|
79
|
+
* the data rather than a search. Instruction search and demonstrations pull on
|
|
80
|
+
* different parts of a model's behaviour — instructions on what to do,
|
|
81
|
+
* examples on what the output should look like — so a seed carrying both
|
|
82
|
+
* starts somewhere neither reaches alone.
|
|
83
|
+
*/
|
|
84
|
+
async function harvestFewShotExamples(args) {
|
|
85
|
+
const { adapter, candidate, trainingSet, minScore, maxDemos = DEFAULT_MAX_DEMOS, batchSize = maxDemos, maxMetricCalls = trainingSet.length, maxCostUsd, rng, renderDemo, signal } = args;
|
|
86
|
+
if (trainingSet.length === 0) throw new Error("harvestFewShotExamples requires a non-empty trainingSet");
|
|
87
|
+
const harvest = await harvestRollouts({
|
|
88
|
+
adapter,
|
|
89
|
+
candidate,
|
|
90
|
+
data: trainingSet,
|
|
91
|
+
maxRollouts: maxDemos,
|
|
92
|
+
batchSize,
|
|
93
|
+
maxMetricCalls,
|
|
94
|
+
...maxCostUsd === void 0 ? {} : { maxCostUsd },
|
|
95
|
+
...minScore === void 0 ? {} : { minScore },
|
|
96
|
+
...rng === void 0 ? {} : { rng },
|
|
97
|
+
...signal === void 0 ? {} : { signal }
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
demos: harvest.rollouts,
|
|
101
|
+
block: formatDemos(harvest.rollouts, renderDemo === void 0 ? {} : { render: renderDemo }),
|
|
102
|
+
metricCalls: harvest.metricCalls,
|
|
103
|
+
usage: harvest.usage,
|
|
104
|
+
attempted: harvest.attempted
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Render demos as the text a candidate component holds.
|
|
109
|
+
*
|
|
110
|
+
* Delimited rather than free-form so `parseDemos` can read them back: a demo
|
|
111
|
+
* component is edited over the course of a run, and a block that cannot be
|
|
112
|
+
* parsed can only be replaced wholesale, throwing away every example found
|
|
113
|
+
* before it.
|
|
114
|
+
*/
|
|
115
|
+
function formatDemos(demos, options = {}) {
|
|
116
|
+
const { render = renderDefault } = options;
|
|
117
|
+
if (demos.length === 0) return "";
|
|
118
|
+
return demos.map((demo, index) => `${DEMO_OPEN}\n${render({
|
|
119
|
+
demo,
|
|
120
|
+
index
|
|
121
|
+
})}\n${DEMO_CLOSE}`).join("\n");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Rewrite the demos a component holds, leaving everything else it says intact.
|
|
125
|
+
*
|
|
126
|
+
* A component is not always only examples. SIMBA appends advice to the same
|
|
127
|
+
* text it appends demonstrations to, so replacing the component wholesale with
|
|
128
|
+
* a fresh block would delete the instructions the other mutation wrote. The
|
|
129
|
+
* replacement lands where the first demo was, so a block a caller placed after
|
|
130
|
+
* its preamble stays after it.
|
|
131
|
+
*/
|
|
132
|
+
function replaceDemos(args) {
|
|
133
|
+
const { text, demos, render } = args;
|
|
134
|
+
const block = formatDemos(demos, render === void 0 ? {} : { render });
|
|
135
|
+
const [before, ...rest] = text.replace(DEMO_BLOCK, "\0").split("\0");
|
|
136
|
+
if (rest.length === 0) return join([text, block]);
|
|
137
|
+
return join([
|
|
138
|
+
before ?? "",
|
|
139
|
+
block,
|
|
140
|
+
rest.join("")
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Recover the demos from a formatted block, ignoring anything written around
|
|
145
|
+
* them. Text a model rewrote and mangled yields the demos it left intact
|
|
146
|
+
* rather than throwing: a malformed example is worth less than the rest of the
|
|
147
|
+
* block, not more than it.
|
|
148
|
+
*/
|
|
149
|
+
function parseDemos(text) {
|
|
150
|
+
const demos = [];
|
|
151
|
+
for (const match of text.matchAll(DEMO_BLOCK)) {
|
|
152
|
+
const parts = (match[1] ?? "").match(DEMO_PARTS);
|
|
153
|
+
if (parts === null) continue;
|
|
154
|
+
const input = parseValue(parts[1] ?? "");
|
|
155
|
+
const output = parseValue(parts[2] ?? "");
|
|
156
|
+
demos.push({
|
|
157
|
+
input,
|
|
158
|
+
output
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return demos;
|
|
162
|
+
}
|
|
163
|
+
/** Joins what survived a replacement, without leaving blank runs behind. */
|
|
164
|
+
function join(parts) {
|
|
165
|
+
return parts.map((part) => part.trim()).filter((part) => part.length > 0).join("\n\n");
|
|
166
|
+
}
|
|
167
|
+
function renderDefault(args) {
|
|
168
|
+
const { demo } = args;
|
|
169
|
+
return [
|
|
170
|
+
"<input>",
|
|
171
|
+
serialize(demo.input),
|
|
172
|
+
"</input>",
|
|
173
|
+
"<output>",
|
|
174
|
+
serialize(demo.output),
|
|
175
|
+
"</output>"
|
|
176
|
+
].join("\n");
|
|
177
|
+
}
|
|
178
|
+
/** Strings stay as they are; anything else is shown as JSON. */
|
|
179
|
+
function serialize(value) {
|
|
180
|
+
if (typeof value === "string") return escapeDelimiters(value);
|
|
181
|
+
try {
|
|
182
|
+
return escapeDelimiters(JSON.stringify(value, null, 2) ?? String(value));
|
|
183
|
+
} catch {
|
|
184
|
+
return escapeDelimiters(String(value));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function parseValue(text) {
|
|
188
|
+
const unescaped = unescapeDelimiters(text);
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(unescaped);
|
|
191
|
+
} catch {
|
|
192
|
+
return unescaped;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Neutralize the tags a demo block is delimited by, so a value carrying one
|
|
197
|
+
* cannot end the block it sits in.
|
|
198
|
+
*
|
|
199
|
+
* A system that quotes its own prompt back produces exactly that: the output
|
|
200
|
+
* worth keeping is a demonstration containing `</demo>`, and appending it raw
|
|
201
|
+
* closes the outer block early. What comes back is not the demo that went in,
|
|
202
|
+
* and SIMBA reparses and rewrites the block at every step, so the damage
|
|
203
|
+
* compounds over a run rather than showing up once.
|
|
204
|
+
*
|
|
205
|
+
* The escape is escaped first, so a value that already reads `<demo>`
|
|
206
|
+
* survives the round trip as itself.
|
|
207
|
+
*/
|
|
208
|
+
function escapeDelimiters(text) {
|
|
209
|
+
return text.replaceAll("<", "&lt;").replace(DELIMITER_TAG, "<$1$2>");
|
|
210
|
+
}
|
|
211
|
+
function unescapeDelimiters(text) {
|
|
212
|
+
return text.replace(ESCAPED_DELIMITER_TAG, "<$1$2>").replaceAll("&lt;", "<");
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
export { harvestRollouts as a, replaceDemos as i, harvestFewShotExamples as n, parseDemos as r, formatDemos as t };
|
package/dist/file-cache.cjs
CHANGED
|
@@ -17,7 +17,9 @@ let node_path = require("node:path");
|
|
|
17
17
|
function createFileCache(args) {
|
|
18
18
|
const { path, maxEntries = 1e6 } = args;
|
|
19
19
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
20
|
-
const
|
|
20
|
+
const log = readLog(path);
|
|
21
|
+
const entries = log.entries;
|
|
22
|
+
if (log.unterminated) (0, node_fs.appendFileSync)(path, "\n");
|
|
21
23
|
return {
|
|
22
24
|
get: (key) => entries.get(key),
|
|
23
25
|
set: (key, cached) => {
|
|
@@ -42,14 +44,20 @@ function readLog(path) {
|
|
|
42
44
|
try {
|
|
43
45
|
contents = (0, node_fs.readFileSync)(path, "utf8");
|
|
44
46
|
} catch {
|
|
45
|
-
return
|
|
47
|
+
return {
|
|
48
|
+
entries,
|
|
49
|
+
unterminated: false
|
|
50
|
+
};
|
|
46
51
|
}
|
|
47
52
|
for (const line of contents.split("\n")) {
|
|
48
53
|
if (line.length === 0) continue;
|
|
49
54
|
const entry = parseEntry(line);
|
|
50
55
|
if (entry !== void 0) entries.set(entry[0], entry[1]);
|
|
51
56
|
}
|
|
52
|
-
return
|
|
57
|
+
return {
|
|
58
|
+
entries,
|
|
59
|
+
unterminated: contents.length > 0 && !contents.endsWith("\n")
|
|
60
|
+
};
|
|
53
61
|
}
|
|
54
62
|
function parseEntry(line) {
|
|
55
63
|
let parsed;
|
package/dist/file-cache.mjs
CHANGED
|
@@ -16,7 +16,9 @@ import { dirname } from "node:path";
|
|
|
16
16
|
function createFileCache(args) {
|
|
17
17
|
const { path, maxEntries = 1e6 } = args;
|
|
18
18
|
mkdirSync(dirname(path), { recursive: true });
|
|
19
|
-
const
|
|
19
|
+
const log = readLog(path);
|
|
20
|
+
const entries = log.entries;
|
|
21
|
+
if (log.unterminated) appendFileSync(path, "\n");
|
|
20
22
|
return {
|
|
21
23
|
get: (key) => entries.get(key),
|
|
22
24
|
set: (key, cached) => {
|
|
@@ -41,14 +43,20 @@ function readLog(path) {
|
|
|
41
43
|
try {
|
|
42
44
|
contents = readFileSync(path, "utf8");
|
|
43
45
|
} catch {
|
|
44
|
-
return
|
|
46
|
+
return {
|
|
47
|
+
entries,
|
|
48
|
+
unterminated: false
|
|
49
|
+
};
|
|
45
50
|
}
|
|
46
51
|
for (const line of contents.split("\n")) {
|
|
47
52
|
if (line.length === 0) continue;
|
|
48
53
|
const entry = parseEntry(line);
|
|
49
54
|
if (entry !== void 0) entries.set(entry[0], entry[1]);
|
|
50
55
|
}
|
|
51
|
-
return
|
|
56
|
+
return {
|
|
57
|
+
entries,
|
|
58
|
+
unterminated: contents.length > 0 && !contents.endsWith("\n")
|
|
59
|
+
};
|
|
52
60
|
}
|
|
53
61
|
function parseEntry(line) {
|
|
54
62
|
let parsed;
|