textopt 0.0.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/LICENSE +21 -0
- package/README.md +509 -0
- package/dist/bootstrap-search/index.cjs +308 -0
- package/dist/bootstrap-search/index.d.cts +162 -0
- package/dist/bootstrap-search/index.d.mts +162 -0
- package/dist/bootstrap-search/index.mjs +307 -0
- package/dist/cache-CuSo0NJ8.d.cts +24 -0
- package/dist/cache-CuSo0NJ8.d.mts +24 -0
- package/dist/concurrency-C-cFzWW2.cjs +44 -0
- package/dist/concurrency-D58PWeSk.mjs +39 -0
- package/dist/demos-B0pVQjYC.d.mts +88 -0
- package/dist/demos-B9BJiNKz.cjs +143 -0
- package/dist/demos-BTuzFNsp.d.cts +88 -0
- package/dist/demos-Degx6UmP.mjs +126 -0
- package/dist/evaluation-BV0nSZVx.mjs +521 -0
- package/dist/evaluation-OZOp6TB7.cjs +598 -0
- package/dist/file-cache.cjs +70 -0
- package/dist/file-cache.d.cts +21 -0
- package/dist/file-cache.d.mts +21 -0
- package/dist/file-cache.mjs +69 -0
- package/dist/gepa/index.cjs +1671 -0
- package/dist/gepa/index.d.cts +385 -0
- package/dist/gepa/index.d.mts +385 -0
- package/dist/gepa/index.mjs +1652 -0
- package/dist/index.cjs +266 -0
- package/dist/index.d.cts +221 -0
- package/dist/index.d.mts +221 -0
- package/dist/index.mjs +245 -0
- package/dist/math-COOofUyv.cjs +101 -0
- package/dist/math-DhrDmpFS.mjs +78 -0
- package/dist/mipro/index.cjs +739 -0
- package/dist/mipro/index.d.cts +372 -0
- package/dist/mipro/index.d.mts +372 -0
- package/dist/mipro/index.mjs +736 -0
- package/dist/opro/index.cjs +487 -0
- package/dist/opro/index.d.cts +230 -0
- package/dist/opro/index.d.mts +230 -0
- package/dist/opro/index.mjs +485 -0
- package/dist/optimizer-B7SpRwl7.d.cts +288 -0
- package/dist/optimizer-DqCoth_w.d.mts +288 -0
- package/dist/random-search/index.cjs +321 -0
- package/dist/random-search/index.d.cts +156 -0
- package/dist/random-search/index.d.mts +156 -0
- package/dist/random-search/index.mjs +319 -0
- package/dist/reflection-CQToe-5B.d.cts +283 -0
- package/dist/reflection-Cr_upzU0.d.mts +283 -0
- package/dist/reflection-DRfbk6hu.cjs +249 -0
- package/dist/reflection-mwMhrjs_.mjs +214 -0
- package/dist/rng-BR5MOedA.d.cts +22 -0
- package/dist/rng-BR5MOedA.d.mts +22 -0
- package/dist/rng-DbA_rPIo.cjs +67 -0
- package/dist/rng-Dtc5eZ_W.mjs +62 -0
- package/dist/sampling-CfHt7Gue.mjs +59 -0
- package/dist/sampling-DFo_7RNJ.d.mts +23 -0
- package/dist/sampling-Dars7ctR.cjs +64 -0
- package/dist/sampling-axOwfZf5.d.cts +23 -0
- package/dist/simba/index.cjs +709 -0
- package/dist/simba/index.d.cts +289 -0
- package/dist/simba/index.d.mts +289 -0
- package/dist/simba/index.mjs +700 -0
- package/dist/testing.cjs +155 -0
- package/dist/testing.d.cts +53 -0
- package/dist/testing.d.mts +53 -0
- package/dist/testing.mjs +148 -0
- package/dist/text--v4Ffbus.mjs +21 -0
- package/dist/text-CK_HB3su.cjs +26 -0
- package/dist/types-CWv4IQFF.d.cts +129 -0
- package/dist/types-CWv4IQFF.d.mts +129 -0
- package/package.json +135 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { a as requireMeasuredMean, d as createMemoryCache, f as stableHash, i as measuredMean, l as runFingerprint, n as costExhausted, o as createDeadline, p as createBudget, r as createEvaluator, s as assertResumable, t as BudgetExhausted, u as componentNames } from "../evaluation-BV0nSZVx.mjs";
|
|
2
|
+
import { t as mapWithConcurrency } from "../concurrency-D58PWeSk.mjs";
|
|
3
|
+
import { t as parseProposedText } from "../text--v4Ffbus.mjs";
|
|
4
|
+
import { t as createSeededRng } from "../rng-Dtc5eZ_W.mjs";
|
|
5
|
+
//#region src/opro/optimize.ts
|
|
6
|
+
/**
|
|
7
|
+
* Rounds that may pass without a proposal nobody has tried before, per
|
|
8
|
+
* component, before the run gives up.
|
|
9
|
+
*
|
|
10
|
+
* Such a round spends no rollouts, so the budget guard — the only thing
|
|
11
|
+
* bounding a run at the default round limit — never fires, and a proposal
|
|
12
|
+
* model that has settled on one answer would loop forever. Waiting a few
|
|
13
|
+
* rounds costs one prompt each and leaves room for a stochastic model to find
|
|
14
|
+
* something new; waiting indefinitely is a hang.
|
|
15
|
+
*/
|
|
16
|
+
const BARREN_ROUNDS = 3;
|
|
17
|
+
const DEFAULT_PROPOSALS_PER_ROUND = 8;
|
|
18
|
+
const DEFAULT_HISTORY_SIZE = 20;
|
|
19
|
+
const DEFAULT_EXEMPLARS = 3;
|
|
20
|
+
const DEFAULT_SCORE_SCALE = 100;
|
|
21
|
+
const DEFAULT_FULL_EVAL_INTERVAL = 3;
|
|
22
|
+
/**
|
|
23
|
+
* Optimization by prompting: show the model what has been tried and what each
|
|
24
|
+
* attempt scored, and ask for something better.
|
|
25
|
+
*
|
|
26
|
+
* The regime this is for is the one GEPA cannot serve. GEPA's advantage over
|
|
27
|
+
* blind search is the per-instance feedback string — a diagnosis of *why* a
|
|
28
|
+
* rollout failed. Plenty of metrics cannot produce one: a reward model, a
|
|
29
|
+
* preference score, a classifier's accuracy over a closed label set. Handed
|
|
30
|
+
* those, reflection is asked to diagnose a failure it has no evidence about,
|
|
31
|
+
* and pays a frontier-model call to guess.
|
|
32
|
+
*
|
|
33
|
+
* This search asks for less and needs less. One scalar per candidate is the
|
|
34
|
+
* entire signal, and the model reasons over the *trajectory* of scores rather
|
|
35
|
+
* than the anatomy of a single failure. Where feedback does exist, use GEPA:
|
|
36
|
+
* a score history is a much thinner channel than a paragraph saying what broke.
|
|
37
|
+
*/
|
|
38
|
+
var OproOptimizer = class {
|
|
39
|
+
#config;
|
|
40
|
+
constructor(config = {}) {
|
|
41
|
+
assertConfig(config);
|
|
42
|
+
this.#config = config;
|
|
43
|
+
}
|
|
44
|
+
async optimize(task) {
|
|
45
|
+
return runOpro({
|
|
46
|
+
config: this.#config,
|
|
47
|
+
task
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Adapted from the meta-prompt in *Large Language Models as Optimizers* (Yang
|
|
53
|
+
* et al., 2023). Ascending order is load-bearing: the paper finds the model
|
|
54
|
+
* attends most to what is nearest the end of the prompt, so the best attempt
|
|
55
|
+
* has to be last.
|
|
56
|
+
*
|
|
57
|
+
* Exemplars carry an `<INS>` marker for the same reason the paper's do: an
|
|
58
|
+
* instruction written against the task in the abstract reads differently from
|
|
59
|
+
* one written to sit in a particular slot. The marker is placed ahead of the
|
|
60
|
+
* input, which is where a component's text usually goes but which this
|
|
61
|
+
* optimizer cannot actually know — only the adapter composes the real prompt.
|
|
62
|
+
* The paper also shows each exemplar's gold answer. Nothing here needs to know
|
|
63
|
+
* what that is: `renderDatum` is handed the whole datum, so a caller holding
|
|
64
|
+
* labels can render them alongside the input.
|
|
65
|
+
*/
|
|
66
|
+
function buildOproPrompt(args) {
|
|
67
|
+
const { componentName, history, exemplars } = args;
|
|
68
|
+
return [
|
|
69
|
+
`I am tuning the "${componentName}" component of a larger system. Below are the instructions I have tried, each with the score it achieved. Higher scores are better.`,
|
|
70
|
+
"",
|
|
71
|
+
"<attempts>",
|
|
72
|
+
history.map((attempt) => `score: ${attempt.score}\n<instruction>\n${attempt.text}\n</instruction>`).join("\n\n"),
|
|
73
|
+
"</attempts>",
|
|
74
|
+
...exemplars.length === 0 ? [] : [
|
|
75
|
+
"",
|
|
76
|
+
"Here are examples of the inputs this component receives. `<INS>` marks where your instruction is read:",
|
|
77
|
+
"",
|
|
78
|
+
"<inputs>",
|
|
79
|
+
exemplars.map((exemplar) => `input:\n<INS>\n${exemplar}`).join("\n\n"),
|
|
80
|
+
"</inputs>"
|
|
81
|
+
],
|
|
82
|
+
"",
|
|
83
|
+
"Write a new instruction that will score higher than every instruction above.",
|
|
84
|
+
"Work out what the higher-scoring instructions do that the lower-scoring ones do not, and push further in that direction.",
|
|
85
|
+
"Do not repeat an instruction that has already been tried.",
|
|
86
|
+
"",
|
|
87
|
+
"Return only the new instruction, inside a ``` block."
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
async function runOpro(args) {
|
|
91
|
+
const { config, task } = args;
|
|
92
|
+
const { proposalsPerRound = DEFAULT_PROPOSALS_PER_ROUND, concurrency = 1, maxRounds = Number.POSITIVE_INFINITY, maxReflectionCalls = Number.POSITIVE_INFINITY, seed = 0, historySize = DEFAULT_HISTORY_SIZE, exemplars = DEFAULT_EXEMPLARS, scoringSetSize, fullEvalInterval = DEFAULT_FULL_EVAL_INTERVAL, scoreScale = DEFAULT_SCORE_SCALE, buildPrompt = buildOproPrompt, trackBestOutputs = false, checkpointCache = true } = config;
|
|
93
|
+
const { seedCandidate, trainingSet, validationSet = trainingSet, testSet, adapter, reflect, maxMetricCalls, renderDatum = renderDefault, cache, cacheNamespace, retry, maxCostUsd, maxWallClockMs, instanceId = defaultInstanceId, onEvent, onCheckpoint, resumeFrom, signal } = task;
|
|
94
|
+
const deadline = createDeadline({ maxWallClockMs });
|
|
95
|
+
const components = componentNames(seedCandidate);
|
|
96
|
+
if (trainingSet.length === 0) throw new Error("optimize requires a non-empty trainingSet");
|
|
97
|
+
if (validationSet.length === 0) throw new Error("optimize requires a non-empty validationSet");
|
|
98
|
+
if (components.length === 0) throw new Error("optimize requires a seed candidate with at least one component");
|
|
99
|
+
if (testSet !== void 0 && testSet.length === 0) throw new Error("optimize requires a non-empty testSet when one is given; omit it to skip held-out evaluation");
|
|
100
|
+
const validationIds = validationSet.map((datum, index) => instanceId({
|
|
101
|
+
datum,
|
|
102
|
+
index
|
|
103
|
+
}));
|
|
104
|
+
const testIds = testSet?.map((datum, index) => instanceId({
|
|
105
|
+
datum,
|
|
106
|
+
index
|
|
107
|
+
})) ?? [];
|
|
108
|
+
const fingerprint = runFingerprint({
|
|
109
|
+
seedCandidate,
|
|
110
|
+
trainingIds: trainingSet.map((datum, index) => instanceId({
|
|
111
|
+
datum,
|
|
112
|
+
index
|
|
113
|
+
})),
|
|
114
|
+
validationIds,
|
|
115
|
+
seed,
|
|
116
|
+
...cacheNamespace === void 0 ? {} : { cacheNamespace }
|
|
117
|
+
});
|
|
118
|
+
assertResumable({
|
|
119
|
+
fingerprint,
|
|
120
|
+
...resumeFrom === void 0 ? {} : { snapshot: resumeFrom }
|
|
121
|
+
});
|
|
122
|
+
const rng = createSeededRng(seed, resumeFrom?.rngState);
|
|
123
|
+
const budget = createBudget({
|
|
124
|
+
maxMetricCalls,
|
|
125
|
+
spent: resumeFrom?.metricCalls ?? 0
|
|
126
|
+
});
|
|
127
|
+
const evaluationCache = cache === false ? void 0 : cache ?? createMemoryCache();
|
|
128
|
+
const evaluator = createEvaluator({
|
|
129
|
+
adapter,
|
|
130
|
+
budget,
|
|
131
|
+
...retry === void 0 ? {} : { retry },
|
|
132
|
+
...cacheNamespace === void 0 ? {} : { cacheNamespace },
|
|
133
|
+
...evaluationCache === void 0 ? {} : { cache: evaluationCache },
|
|
134
|
+
trackOutputs: trackBestOutputs,
|
|
135
|
+
cacheHits: resumeFrom?.cacheHits ?? 0,
|
|
136
|
+
...signal === void 0 ? {} : { signal },
|
|
137
|
+
onEvaluation: (event) => onEvent?.({
|
|
138
|
+
type: "evaluation",
|
|
139
|
+
...event
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
evaluator.restore(resumeFrom?.cache ?? []);
|
|
143
|
+
function drawExemplars() {
|
|
144
|
+
return rng.sample(trainingSet, Math.min(exemplars, trainingSet.length)).map(renderDatum);
|
|
145
|
+
}
|
|
146
|
+
const scoringIndices = resumeFrom?.scoringIndices ?? (scoringSetSize === void 0 ? void 0 : rng.sample(trainingSet.map((_, index) => index), Math.min(scoringSetSize, trainingSet.length)));
|
|
147
|
+
const scoringSet = scoringIndices?.map((index) => trainingSet[index]);
|
|
148
|
+
const scoringIds = scoringIndices?.map((index) => instanceId({
|
|
149
|
+
datum: trainingSet[index],
|
|
150
|
+
index
|
|
151
|
+
}));
|
|
152
|
+
const histories = new Map(components.map((name) => [name, [...resumeFrom?.histories[name] ?? []]]));
|
|
153
|
+
function contextOf(candidate, component) {
|
|
154
|
+
const rest = {};
|
|
155
|
+
for (const name of components) if (name !== component) rest[name] = candidate[name];
|
|
156
|
+
return stableHash(rest);
|
|
157
|
+
}
|
|
158
|
+
const trajectory = [];
|
|
159
|
+
let round = resumeFrom?.round ?? 0;
|
|
160
|
+
let reflectionCalls = resumeFrom?.reflectionCalls ?? 0;
|
|
161
|
+
let stopReason = "maxRounds";
|
|
162
|
+
onEvent?.({
|
|
163
|
+
type: "start",
|
|
164
|
+
components,
|
|
165
|
+
validationSetSize: validationSet.length
|
|
166
|
+
});
|
|
167
|
+
async function sweep(candidate, phase) {
|
|
168
|
+
return evaluator.evaluate({
|
|
169
|
+
candidate,
|
|
170
|
+
batch: validationSet,
|
|
171
|
+
ids: validationIds,
|
|
172
|
+
split: "val",
|
|
173
|
+
phase,
|
|
174
|
+
candidateId: null,
|
|
175
|
+
iteration: round
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Screens a candidate. With a scoring set that is a slice of the training set;
|
|
180
|
+
* without one it is the full validation set, and screening and reporting are the
|
|
181
|
+
* same measurement.
|
|
182
|
+
*/
|
|
183
|
+
async function screen(candidate, phase) {
|
|
184
|
+
if (scoringSet === void 0) return sweep(candidate, phase);
|
|
185
|
+
return evaluator.evaluate({
|
|
186
|
+
candidate,
|
|
187
|
+
batch: scoringSet,
|
|
188
|
+
ids: scoringIds,
|
|
189
|
+
split: "train",
|
|
190
|
+
phase: "minibatch",
|
|
191
|
+
candidateId: null,
|
|
192
|
+
iteration: round
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const seedEvaluation = resumeFrom === void 0 ? await sweep(seedCandidate, "seed") : void 0;
|
|
196
|
+
const seedScore = seedEvaluation === void 0 ? resumeFrom.seedScore : requireMeasuredMean({
|
|
197
|
+
batch: seedEvaluation,
|
|
198
|
+
phase: "seed"
|
|
199
|
+
});
|
|
200
|
+
let best = resumeFrom?.best ?? seedCandidate;
|
|
201
|
+
let bestScore = resumeFrom?.bestScore ?? seedScore;
|
|
202
|
+
/** Absent on a resumed run until a sweep wins: outputs are not checkpointed. */
|
|
203
|
+
let bestOutputs = seedEvaluation?.outputs;
|
|
204
|
+
let reported = resumeFrom?.reported ?? seedCandidate;
|
|
205
|
+
let lastSwept = resumeFrom === void 0 ? seedCandidate : resumeFrom.incumbentSwept ? best : {};
|
|
206
|
+
let bestSearchScore = resumeFrom?.bestSearchScore ?? seedScore;
|
|
207
|
+
if (resumeFrom === void 0 && scoringSet !== void 0) bestSearchScore = requireMeasuredMean({
|
|
208
|
+
batch: await screen(seedCandidate, "seed"),
|
|
209
|
+
phase: "seed"
|
|
210
|
+
});
|
|
211
|
+
/**
|
|
212
|
+
* Measures the incumbent on the whole validation set. The search chooses by subset
|
|
213
|
+
* score and can therefore chase something that only works on the subset, so
|
|
214
|
+
* what gets reported is the best candidate a full sweep has actually seen —
|
|
215
|
+
* never a subset number wearing a validation set label.
|
|
216
|
+
*/
|
|
217
|
+
async function refreshIncumbent() {
|
|
218
|
+
if (best === lastSwept || !budget.canAfford(validationSet.length)) return "ok";
|
|
219
|
+
try {
|
|
220
|
+
const evaluation = await sweep(best, "validation");
|
|
221
|
+
const full = measuredMean(evaluation);
|
|
222
|
+
lastSwept = best;
|
|
223
|
+
if (full !== void 0 && full > bestScore) {
|
|
224
|
+
reported = best;
|
|
225
|
+
bestScore = full;
|
|
226
|
+
bestOutputs = evaluation.outputs;
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (err instanceof BudgetExhausted || signal?.aborted) return "stop";
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
return "ok";
|
|
233
|
+
}
|
|
234
|
+
if (resumeFrom === void 0) {
|
|
235
|
+
for (const name of components) histories.get(name)?.push({
|
|
236
|
+
text: seedCandidate[name],
|
|
237
|
+
score: scaleScore(bestSearchScore, scoreScale),
|
|
238
|
+
context: contextOf(seedCandidate, name)
|
|
239
|
+
});
|
|
240
|
+
trajectory.push({
|
|
241
|
+
round: 0,
|
|
242
|
+
component: components[0],
|
|
243
|
+
candidate: seedCandidate,
|
|
244
|
+
score: seedScore
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function takeSnapshot() {
|
|
248
|
+
const cached = checkpointCache ? evaluationCache?.entries?.() : void 0;
|
|
249
|
+
return {
|
|
250
|
+
version: 1,
|
|
251
|
+
fingerprint,
|
|
252
|
+
best,
|
|
253
|
+
reported,
|
|
254
|
+
incumbentSwept: best === lastSwept,
|
|
255
|
+
bestScore,
|
|
256
|
+
bestSearchScore,
|
|
257
|
+
seedScore,
|
|
258
|
+
round,
|
|
259
|
+
reflectionCalls,
|
|
260
|
+
metricCalls: budget.spent(),
|
|
261
|
+
cacheHits: evaluator.cacheHits(),
|
|
262
|
+
rngState: rng.state(),
|
|
263
|
+
histories: Object.fromEntries([...histories].map(([name, attempts]) => [name, [...attempts]])),
|
|
264
|
+
...scoringIndices === void 0 ? {} : { scoringIndices },
|
|
265
|
+
...cached === void 0 ? {} : { cache: cached }
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function checkpoint() {
|
|
269
|
+
if (onCheckpoint === void 0) return;
|
|
270
|
+
await onCheckpoint(takeSnapshot());
|
|
271
|
+
}
|
|
272
|
+
await checkpoint();
|
|
273
|
+
let barrenRounds = 0;
|
|
274
|
+
while (round < maxRounds) {
|
|
275
|
+
if (signal?.aborted) {
|
|
276
|
+
stopReason = "aborted";
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
if (barrenRounds >= BARREN_ROUNDS * components.length) {
|
|
280
|
+
stopReason = "proposalsExhausted";
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (costExhausted({
|
|
284
|
+
usage: evaluator.usage(),
|
|
285
|
+
maxCostUsd
|
|
286
|
+
})) {
|
|
287
|
+
stopReason = "costExhausted";
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (deadline.exceeded()) {
|
|
291
|
+
stopReason = "deadlineReached";
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
if (reflectionCalls >= maxReflectionCalls) {
|
|
295
|
+
stopReason = "reflectionBudgetExhausted";
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
if (!budget.canAfford(validationSet.length)) {
|
|
299
|
+
stopReason = "budgetExhausted";
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
const component = components[round % components.length];
|
|
303
|
+
const history = histories.get(component);
|
|
304
|
+
const context = contextOf(best, component);
|
|
305
|
+
const comparable = history.filter((attempt) => attempt.context === context);
|
|
306
|
+
onEvent?.({
|
|
307
|
+
type: "roundStart",
|
|
308
|
+
round,
|
|
309
|
+
component,
|
|
310
|
+
historySize: comparable.length
|
|
311
|
+
});
|
|
312
|
+
const affordableProposals = Math.min(proposalsPerRound, maxReflectionCalls - reflectionCalls);
|
|
313
|
+
const prompt = buildPrompt({
|
|
314
|
+
componentName: component,
|
|
315
|
+
history: topAttempts({
|
|
316
|
+
history: comparable,
|
|
317
|
+
keep: historySize
|
|
318
|
+
}),
|
|
319
|
+
exemplars: drawExemplars()
|
|
320
|
+
});
|
|
321
|
+
reflectionCalls += affordableProposals;
|
|
322
|
+
const drawn = await mapWithConcurrency({
|
|
323
|
+
items: Array.from({ length: affordableProposals }, (_, index) => index),
|
|
324
|
+
limit: concurrency,
|
|
325
|
+
signal,
|
|
326
|
+
task: async () => parseProposedText(await reflect({
|
|
327
|
+
prompt,
|
|
328
|
+
...signal === void 0 ? {} : { signal }
|
|
329
|
+
}))
|
|
330
|
+
});
|
|
331
|
+
const tried = new Set(comparable.map((attempt) => attempt.text));
|
|
332
|
+
const unique = [...new Set(drawn)].filter((text) => text.length > 0 && !tried.has(text));
|
|
333
|
+
barrenRounds = unique.length === 0 ? barrenRounds + 1 : 0;
|
|
334
|
+
let roundStop;
|
|
335
|
+
for (const text of unique) {
|
|
336
|
+
const candidate = {
|
|
337
|
+
...best,
|
|
338
|
+
[component]: text
|
|
339
|
+
};
|
|
340
|
+
let evaluation;
|
|
341
|
+
try {
|
|
342
|
+
evaluation = await screen(candidate, "validation");
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (err instanceof BudgetExhausted) {
|
|
345
|
+
roundStop = "budgetExhausted";
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (signal?.aborted) {
|
|
349
|
+
roundStop = "aborted";
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
const score = measuredMean(evaluation);
|
|
355
|
+
if (score === void 0) continue;
|
|
356
|
+
const accepted = score > bestSearchScore;
|
|
357
|
+
history.push({
|
|
358
|
+
text,
|
|
359
|
+
score: scaleScore(score, scoreScale),
|
|
360
|
+
context
|
|
361
|
+
});
|
|
362
|
+
trajectory.push({
|
|
363
|
+
round: round + 1,
|
|
364
|
+
component,
|
|
365
|
+
candidate,
|
|
366
|
+
score
|
|
367
|
+
});
|
|
368
|
+
onEvent?.({
|
|
369
|
+
type: "attempt",
|
|
370
|
+
round,
|
|
371
|
+
component,
|
|
372
|
+
score,
|
|
373
|
+
accepted
|
|
374
|
+
});
|
|
375
|
+
if (accepted) {
|
|
376
|
+
best = candidate;
|
|
377
|
+
bestSearchScore = score;
|
|
378
|
+
if (scoringSet === void 0) {
|
|
379
|
+
reported = candidate;
|
|
380
|
+
lastSwept = candidate;
|
|
381
|
+
bestScore = score;
|
|
382
|
+
bestOutputs = evaluation.outputs;
|
|
383
|
+
}
|
|
384
|
+
const scaled = scaleScore(score, scoreScale);
|
|
385
|
+
for (const name of components) {
|
|
386
|
+
if (name === component) continue;
|
|
387
|
+
histories.get(name)?.push({
|
|
388
|
+
text: candidate[name],
|
|
389
|
+
score: scaled,
|
|
390
|
+
context: contextOf(candidate, name)
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
round += 1;
|
|
396
|
+
await checkpoint();
|
|
397
|
+
if (roundStop !== void 0) {
|
|
398
|
+
stopReason = roundStop;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
if (scoringSet !== void 0 && round % fullEvalInterval === 0) {
|
|
402
|
+
if (await refreshIncumbent() === "stop") {
|
|
403
|
+
stopReason = signal?.aborted ? "aborted" : "budgetExhausted";
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (scoringSet !== void 0 && !signal?.aborted) await refreshIncumbent();
|
|
409
|
+
if (signal?.aborted) stopReason = "aborted";
|
|
410
|
+
const testScore = testSet === void 0 ? void 0 : measuredMean(await evaluator.evaluate({
|
|
411
|
+
candidate: best,
|
|
412
|
+
batch: testSet,
|
|
413
|
+
ids: testIds,
|
|
414
|
+
split: "test",
|
|
415
|
+
phase: "test",
|
|
416
|
+
candidateId: null,
|
|
417
|
+
iteration: round,
|
|
418
|
+
charge: false
|
|
419
|
+
}));
|
|
420
|
+
onEvent?.({
|
|
421
|
+
type: "finish",
|
|
422
|
+
reason: stopReason,
|
|
423
|
+
bestScore,
|
|
424
|
+
metricCalls: budget.spent(),
|
|
425
|
+
...testScore === void 0 ? {} : { testScore }
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
snapshot: takeSnapshot(),
|
|
429
|
+
bestCandidate: reported,
|
|
430
|
+
bestScore,
|
|
431
|
+
usage: evaluator.usage(),
|
|
432
|
+
seedScore,
|
|
433
|
+
...trackBestOutputs ? { bestOutputs } : {},
|
|
434
|
+
...testScore === void 0 ? {} : {
|
|
435
|
+
testScore,
|
|
436
|
+
testMetricCalls: evaluator.unchargedCalls()
|
|
437
|
+
},
|
|
438
|
+
rounds: round,
|
|
439
|
+
trajectory,
|
|
440
|
+
metricCalls: budget.spent(),
|
|
441
|
+
reflectionCalls,
|
|
442
|
+
cacheHits: evaluator.cacheHits(),
|
|
443
|
+
stopReason
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* The strongest `keep` attempts, ordered weakest first. Dropping the weak tail
|
|
448
|
+
* rather than the old one is deliberate: the prompt's job is to show a
|
|
449
|
+
* gradient, and an attempt that scored badly a hundred rounds ago still marks
|
|
450
|
+
* the bottom of it.
|
|
451
|
+
*/
|
|
452
|
+
function topAttempts(args) {
|
|
453
|
+
const { history, keep } = args;
|
|
454
|
+
return [...history].sort((a, b) => b.score - a.score).slice(0, keep).sort((a, b) => a.score - b.score);
|
|
455
|
+
}
|
|
456
|
+
function scaleScore(score, scale) {
|
|
457
|
+
return Math.round(score * scale);
|
|
458
|
+
}
|
|
459
|
+
function renderDefault(datum) {
|
|
460
|
+
if (typeof datum === "string") return datum;
|
|
461
|
+
try {
|
|
462
|
+
return JSON.stringify(datum, null, 2) ?? String(datum);
|
|
463
|
+
} catch {
|
|
464
|
+
return String(datum);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function assertConfig(config) {
|
|
468
|
+
const positive = [
|
|
469
|
+
["proposalsPerRound", config.proposalsPerRound],
|
|
470
|
+
["concurrency", config.concurrency],
|
|
471
|
+
["maxRounds", config.maxRounds],
|
|
472
|
+
["maxReflectionCalls", config.maxReflectionCalls],
|
|
473
|
+
["historySize", config.historySize],
|
|
474
|
+
["scoringSetSize", config.scoringSetSize],
|
|
475
|
+
["fullEvalInterval", config.fullEvalInterval]
|
|
476
|
+
];
|
|
477
|
+
for (const [name, value] of positive) if (value !== void 0 && (!Number.isInteger(value) || value < 1)) throw new Error(`${name} must be a positive integer, received ${value}`);
|
|
478
|
+
if (config.exemplars !== void 0 && (!Number.isInteger(config.exemplars) || config.exemplars < 0)) throw new Error(`exemplars must be a non-negative integer, received ${config.exemplars}`);
|
|
479
|
+
}
|
|
480
|
+
function defaultInstanceId(args) {
|
|
481
|
+
const hash = stableHash(args.datum);
|
|
482
|
+
return hash === "" ? String(args.index) : hash;
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
485
|
+
export { OproOptimizer, buildOproPrompt };
|