textopt 0.0.0 → 0.2.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 +65 -25
- package/dist/bootstrap-search/index.cjs +159 -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 +150 -66
- package/dist/demos-9v5ts7F3.cjs +244 -0
- package/dist/{demos-B0pVQjYC.d.mts → demos-ASsSXYXA.d.mts} +10 -3
- package/dist/demos-Brobjfuc.mjs +215 -0
- package/dist/{demos-BTuzFNsp.d.cts → demos-ByaLZy-Z.d.cts} +10 -3
- package/dist/file-cache.cjs +27 -8
- package/dist/file-cache.d.cts +13 -0
- package/dist/file-cache.d.mts +13 -0
- package/dist/file-cache.mjs +27 -8
- package/dist/gepa/index.cjs +128 -80
- package/dist/gepa/index.d.cts +15 -7
- package/dist/gepa/index.d.mts +15 -7
- package/dist/gepa/index.mjs +101 -55
- package/dist/index.cjs +157 -30
- package/dist/index.d.cts +177 -7
- package/dist/index.d.mts +177 -7
- package/dist/index.mjs +139 -18
- package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
- package/dist/math-Dqme4rYz.mjs +123 -0
- package/dist/mipro/index.cjs +104 -70
- package/dist/mipro/index.d.cts +17 -14
- package/dist/mipro/index.d.mts +17 -14
- package/dist/mipro/index.mjs +90 -58
- package/dist/opro/index.cjs +136 -51
- package/dist/opro/index.d.cts +17 -9
- package/dist/opro/index.d.mts +17 -9
- package/dist/opro/index.mjs +121 -38
- package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-4Zv-Zt2t.d.cts} +90 -5
- package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ds5mzYjz.d.mts} +90 -5
- package/dist/random-search/index.cjs +99 -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 +89 -41
- package/dist/{reflection-Cr_upzU0.d.mts → reflection-CMezGu6u.d.mts} +38 -14
- package/dist/{reflection-CQToe-5B.d.cts → reflection-D0A7eahD.d.cts} +38 -14
- package/dist/reporting-bq007_2z.d.cts +294 -0
- package/dist/reporting-bq007_2z.d.mts +294 -0
- package/dist/simba/index.cjs +216 -83
- package/dist/simba/index.d.cts +53 -13
- package/dist/simba/index.d.mts +53 -13
- package/dist/simba/index.mjs +206 -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/dist/{evaluation-OZOp6TB7.cjs → warnings-CWRJF-jA.cjs} +228 -5
- package/dist/{evaluation-BV0nSZVx.mjs → warnings-OxvDi9kN.mjs} +175 -6
- package/docs/adapters.md +169 -0
- package/docs/benchmark.md +90 -0
- package/docs/data-prep.md +113 -0
- package/docs/distillation.md +128 -0
- package/docs/evaluation.md +87 -0
- package/docs/metric-preflight.md +132 -0
- package/docs/optimizers.md +293 -0
- package/docs/tuning.md +130 -0
- package/package.json +6 -4
- 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_warnings = require("./warnings-CWRJF-jA.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_warnings.createBudget({ maxMetricCalls });
|
|
21
|
+
const evaluator = require_warnings.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_warnings.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
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as UsageTotals, f as Adapter, p as Candidate } from "./reporting-bq007_2z.mjs";
|
|
2
2
|
import { t as Rng } from "./rng-BR5MOedA.mjs";
|
|
3
3
|
//#region src/demos.d.ts
|
|
4
4
|
/**
|
|
@@ -23,6 +23,8 @@ interface BootstrapResult<Datum, Output> {
|
|
|
23
23
|
block: string;
|
|
24
24
|
/** Rollouts this cost. Bootstrapping is cheap, not free. */
|
|
25
25
|
metricCalls: number;
|
|
26
|
+
/** Tokens and dollars this cost, for a caller that bounds spend. */
|
|
27
|
+
usage: UsageTotals;
|
|
26
28
|
attempted: number;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
@@ -36,7 +38,7 @@ interface BootstrapResult<Datum, Output> {
|
|
|
36
38
|
* examples on what the output should look like — so a seed carrying both
|
|
37
39
|
* starts somewhere neither reaches alone.
|
|
38
40
|
*/
|
|
39
|
-
declare function
|
|
41
|
+
declare function harvestFewShotExamples<Datum, Trajectory, Output, K extends string = string>(args: {
|
|
40
42
|
adapter: Adapter<Datum, Trajectory, Output, K>;
|
|
41
43
|
/** The candidate to run. Usually the seed, sometimes a run's winner. */
|
|
42
44
|
candidate: Candidate<K>;
|
|
@@ -61,6 +63,11 @@ declare function bootstrapDemos<Datum, Trajectory, Output, K extends string = st
|
|
|
61
63
|
batchSize?: number;
|
|
62
64
|
/** Ceiling on rollouts. Defaults to one pass over the trainingSet. */
|
|
63
65
|
maxMetricCalls?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Ceiling on dollars this pass may spend, checked between batches. Harvesting
|
|
68
|
+
* runs on its own evaluator, so a caller bounding spend has to say so here.
|
|
69
|
+
*/
|
|
70
|
+
maxCostUsd?: number;
|
|
64
71
|
/** Shuffles the trainingSet first, so demos are not all drawn from its head. */
|
|
65
72
|
rng?: Rng;
|
|
66
73
|
renderDemo?: DemoRenderer<Datum, Output>;
|
|
@@ -85,4 +92,4 @@ declare function formatDemos<Datum, Output>(demos: readonly Demo<Datum, Output>[
|
|
|
85
92
|
*/
|
|
86
93
|
declare function parseDemos(text: string): Demo[];
|
|
87
94
|
//#endregion
|
|
88
|
-
export {
|
|
95
|
+
export { harvestFewShotExamples as a, formatDemos as i, Demo as n, parseDemos as o, DemoRenderer as r, BootstrapResult as t };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { S as createBudget, l as costExhausted, u as createEvaluator } from "./warnings-OxvDi9kN.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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as UsageTotals, f as Adapter, p as Candidate } from "./reporting-bq007_2z.cjs";
|
|
2
2
|
import { t as Rng } from "./rng-BR5MOedA.cjs";
|
|
3
3
|
//#region src/demos.d.ts
|
|
4
4
|
/**
|
|
@@ -23,6 +23,8 @@ interface BootstrapResult<Datum, Output> {
|
|
|
23
23
|
block: string;
|
|
24
24
|
/** Rollouts this cost. Bootstrapping is cheap, not free. */
|
|
25
25
|
metricCalls: number;
|
|
26
|
+
/** Tokens and dollars this cost, for a caller that bounds spend. */
|
|
27
|
+
usage: UsageTotals;
|
|
26
28
|
attempted: number;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
@@ -36,7 +38,7 @@ interface BootstrapResult<Datum, Output> {
|
|
|
36
38
|
* examples on what the output should look like — so a seed carrying both
|
|
37
39
|
* starts somewhere neither reaches alone.
|
|
38
40
|
*/
|
|
39
|
-
declare function
|
|
41
|
+
declare function harvestFewShotExamples<Datum, Trajectory, Output, K extends string = string>(args: {
|
|
40
42
|
adapter: Adapter<Datum, Trajectory, Output, K>;
|
|
41
43
|
/** The candidate to run. Usually the seed, sometimes a run's winner. */
|
|
42
44
|
candidate: Candidate<K>;
|
|
@@ -61,6 +63,11 @@ declare function bootstrapDemos<Datum, Trajectory, Output, K extends string = st
|
|
|
61
63
|
batchSize?: number;
|
|
62
64
|
/** Ceiling on rollouts. Defaults to one pass over the trainingSet. */
|
|
63
65
|
maxMetricCalls?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Ceiling on dollars this pass may spend, checked between batches. Harvesting
|
|
68
|
+
* runs on its own evaluator, so a caller bounding spend has to say so here.
|
|
69
|
+
*/
|
|
70
|
+
maxCostUsd?: number;
|
|
64
71
|
/** Shuffles the trainingSet first, so demos are not all drawn from its head. */
|
|
65
72
|
rng?: Rng;
|
|
66
73
|
renderDemo?: DemoRenderer<Datum, Output>;
|
|
@@ -85,4 +92,4 @@ declare function formatDemos<Datum, Output>(demos: readonly Demo<Datum, Output>[
|
|
|
85
92
|
*/
|
|
86
93
|
declare function parseDemos(text: string): Demo[];
|
|
87
94
|
//#endregion
|
|
88
|
-
export {
|
|
95
|
+
export { harvestFewShotExamples as a, formatDemos as i, Demo as n, parseDemos as o, DemoRenderer as r, BootstrapResult as t };
|
package/dist/file-cache.cjs
CHANGED
|
@@ -13,20 +13,33 @@ let node_path = require("node:path");
|
|
|
13
13
|
* Append-only rather than rewritten: a score is never invalidated (the key
|
|
14
14
|
* names the candidate, the instance, and the environment), and a log survives
|
|
15
15
|
* a process killed mid-write, which a file rewritten in place does not.
|
|
16
|
+
*
|
|
17
|
+
* `namespace` is what makes that invariant true. A cached score measures a
|
|
18
|
+
* whole system, not a candidate, and this log outlives every part of that
|
|
19
|
+
* system a run does not pass through the key: the model id behind an alias the
|
|
20
|
+
* provider upgraded, the decoding settings, the scorer's own version. It is
|
|
21
|
+
* required rather than optional because the failure it prevents is silent —
|
|
22
|
+
* scores from one system served to a run of another, with a normal-looking
|
|
23
|
+
* result and no way to read afterwards that it happened.
|
|
16
24
|
*/
|
|
17
25
|
function createFileCache(args) {
|
|
18
|
-
const { path, maxEntries = 1e6 } = args;
|
|
26
|
+
const { path, namespace, maxEntries = 1e6 } = args;
|
|
27
|
+
if (namespace.trim() === "") throw new Error("createFileCache requires a non-empty namespace naming the system these scores measure");
|
|
19
28
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
20
|
-
const
|
|
29
|
+
const log = readLog(path);
|
|
30
|
+
const entries = log.entries;
|
|
31
|
+
const scope = (key) => `${namespace}\u0000${key}`;
|
|
32
|
+
if (log.unterminated) (0, node_fs.appendFileSync)(path, "\n");
|
|
21
33
|
return {
|
|
22
|
-
get: (key) => entries.get(key),
|
|
34
|
+
get: (key) => entries.get(scope(key)),
|
|
23
35
|
set: (key, cached) => {
|
|
24
|
-
|
|
36
|
+
const scoped = scope(key);
|
|
37
|
+
if (entries.size >= maxEntries && !entries.has(scoped)) {
|
|
25
38
|
const oldest = entries.keys().next();
|
|
26
39
|
if (!oldest.done) entries.delete(oldest.value);
|
|
27
40
|
}
|
|
28
|
-
entries.set(
|
|
29
|
-
(0, node_fs.appendFileSync)(path, `${JSON.stringify([
|
|
41
|
+
entries.set(scoped, cached);
|
|
42
|
+
(0, node_fs.appendFileSync)(path, `${JSON.stringify([scoped, cached])}\n`);
|
|
30
43
|
}
|
|
31
44
|
};
|
|
32
45
|
}
|
|
@@ -42,14 +55,20 @@ function readLog(path) {
|
|
|
42
55
|
try {
|
|
43
56
|
contents = (0, node_fs.readFileSync)(path, "utf8");
|
|
44
57
|
} catch {
|
|
45
|
-
return
|
|
58
|
+
return {
|
|
59
|
+
entries,
|
|
60
|
+
unterminated: false
|
|
61
|
+
};
|
|
46
62
|
}
|
|
47
63
|
for (const line of contents.split("\n")) {
|
|
48
64
|
if (line.length === 0) continue;
|
|
49
65
|
const entry = parseEntry(line);
|
|
50
66
|
if (entry !== void 0) entries.set(entry[0], entry[1]);
|
|
51
67
|
}
|
|
52
|
-
return
|
|
68
|
+
return {
|
|
69
|
+
entries,
|
|
70
|
+
unterminated: contents.length > 0 && !contents.endsWith("\n")
|
|
71
|
+
};
|
|
53
72
|
}
|
|
54
73
|
function parseEntry(line) {
|
|
55
74
|
let parsed;
|
package/dist/file-cache.d.cts
CHANGED
|
@@ -11,9 +11,22 @@ import { n as EvaluationCache } from "./cache-CuSo0NJ8.cjs";
|
|
|
11
11
|
* Append-only rather than rewritten: a score is never invalidated (the key
|
|
12
12
|
* names the candidate, the instance, and the environment), and a log survives
|
|
13
13
|
* a process killed mid-write, which a file rewritten in place does not.
|
|
14
|
+
*
|
|
15
|
+
* `namespace` is what makes that invariant true. A cached score measures a
|
|
16
|
+
* whole system, not a candidate, and this log outlives every part of that
|
|
17
|
+
* system a run does not pass through the key: the model id behind an alias the
|
|
18
|
+
* provider upgraded, the decoding settings, the scorer's own version. It is
|
|
19
|
+
* required rather than optional because the failure it prevents is silent —
|
|
20
|
+
* scores from one system served to a run of another, with a normal-looking
|
|
21
|
+
* result and no way to read afterwards that it happened.
|
|
14
22
|
*/
|
|
15
23
|
declare function createFileCache(args: {
|
|
16
24
|
path: string;
|
|
25
|
+
/**
|
|
26
|
+
* Names the system these scores measure — model id, decoding settings,
|
|
27
|
+
* scorer version. Change it whenever any of them changes.
|
|
28
|
+
*/
|
|
29
|
+
namespace: string;
|
|
17
30
|
/** Entries kept in memory. The file itself is never trimmed. */
|
|
18
31
|
maxEntries?: number;
|
|
19
32
|
}): EvaluationCache;
|
package/dist/file-cache.d.mts
CHANGED
|
@@ -11,9 +11,22 @@ import { n as EvaluationCache } from "./cache-CuSo0NJ8.mjs";
|
|
|
11
11
|
* Append-only rather than rewritten: a score is never invalidated (the key
|
|
12
12
|
* names the candidate, the instance, and the environment), and a log survives
|
|
13
13
|
* a process killed mid-write, which a file rewritten in place does not.
|
|
14
|
+
*
|
|
15
|
+
* `namespace` is what makes that invariant true. A cached score measures a
|
|
16
|
+
* whole system, not a candidate, and this log outlives every part of that
|
|
17
|
+
* system a run does not pass through the key: the model id behind an alias the
|
|
18
|
+
* provider upgraded, the decoding settings, the scorer's own version. It is
|
|
19
|
+
* required rather than optional because the failure it prevents is silent —
|
|
20
|
+
* scores from one system served to a run of another, with a normal-looking
|
|
21
|
+
* result and no way to read afterwards that it happened.
|
|
14
22
|
*/
|
|
15
23
|
declare function createFileCache(args: {
|
|
16
24
|
path: string;
|
|
25
|
+
/**
|
|
26
|
+
* Names the system these scores measure — model id, decoding settings,
|
|
27
|
+
* scorer version. Change it whenever any of them changes.
|
|
28
|
+
*/
|
|
29
|
+
namespace: string;
|
|
17
30
|
/** Entries kept in memory. The file itself is never trimmed. */
|
|
18
31
|
maxEntries?: number;
|
|
19
32
|
}): EvaluationCache;
|