@swapai/core 0.2.0 → 0.2.2
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 +8 -0
- package/dist/index.js +285 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/python/swapai_worker.py +31 -1
package/README.md
CHANGED
|
@@ -95,6 +95,14 @@ init({
|
|
|
95
95
|
|
|
96
96
|
The string result is inferred as `"rabbit" | "fish" | "pig"`.
|
|
97
97
|
|
|
98
|
+
Needle is a classifier, not a regression engine. For bounded numbers, SwapAI
|
|
99
|
+
converts reference scores into a small, closed set of internal labels, then
|
|
100
|
+
converts the selected label back to a number. When the training set contains
|
|
101
|
+
only a few scores, those scores stay exact. Larger score sets are quantized to
|
|
102
|
+
a bounded set based on `acceptableError`. The held-out test still compares the
|
|
103
|
+
decoded number with the original reference score, so quantization consumes the
|
|
104
|
+
same error budget and cannot bypass the accuracy gate.
|
|
105
|
+
|
|
98
106
|
## Effect
|
|
99
107
|
|
|
100
108
|
Effect is optional and lives in a separate import:
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,9 @@ function validateResultConfig(result) {
|
|
|
46
46
|
if (result.max <= result.min) {
|
|
47
47
|
invalidConfiguration("number result max must be greater than min");
|
|
48
48
|
}
|
|
49
|
+
if (!Number.isFinite(result.max - result.min)) {
|
|
50
|
+
invalidConfiguration("number result range must be finite");
|
|
51
|
+
}
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
if (result.type === "string") {
|
|
@@ -122,6 +125,7 @@ function averageError(config, comparisons) {
|
|
|
122
125
|
// src/runtime.ts
|
|
123
126
|
import { createHash, randomUUID } from "crypto";
|
|
124
127
|
import { spawn } from "child_process";
|
|
128
|
+
import { readFileSync, statSync } from "fs";
|
|
125
129
|
import {
|
|
126
130
|
chmod,
|
|
127
131
|
copyFile,
|
|
@@ -137,16 +141,66 @@ import { dirname, join, resolve, sep } from "path";
|
|
|
137
141
|
import { fileURLToPath } from "url";
|
|
138
142
|
|
|
139
143
|
// src/needle.ts
|
|
140
|
-
|
|
144
|
+
var MAX_NEEDLE_NUMBER_LABELS = 64;
|
|
145
|
+
function createNeedleNumberLabels(values, config, acceptableError = 0) {
|
|
146
|
+
if (typeof acceptableError !== "number" || !Number.isFinite(acceptableError) || acceptableError < 0 || acceptableError > 1) {
|
|
147
|
+
throw new TypeError("acceptableError must be between 0 and 1.");
|
|
148
|
+
}
|
|
149
|
+
const valid = values.map(
|
|
150
|
+
(value) => validateNeedleResult(value, config)
|
|
151
|
+
);
|
|
152
|
+
const unique = [...new Set(valid)].sort((left, right) => left - right);
|
|
153
|
+
const desiredLabelCount = acceptableError === 0 ? MAX_NEEDLE_NUMBER_LABELS : Math.min(
|
|
154
|
+
MAX_NEEDLE_NUMBER_LABELS,
|
|
155
|
+
Math.max(2, Math.floor(1 / (2 * acceptableError)) + 1)
|
|
156
|
+
);
|
|
157
|
+
if (unique.length <= desiredLabelCount) return { values: unique };
|
|
158
|
+
const width = (config.max - config.min) / desiredLabelCount;
|
|
159
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const value of unique) {
|
|
161
|
+
occupied.add(Math.min(
|
|
162
|
+
desiredLabelCount - 1,
|
|
163
|
+
Math.floor((value - config.min) / width)
|
|
164
|
+
));
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
values: [...occupied].sort((left, right) => left - right).map((index) => config.min + (index + 0.5) * width)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function numberLabel(index, count) {
|
|
171
|
+
if (count === 1) return "only_score";
|
|
172
|
+
if (count === 2) return index === 0 ? "lower_score" : "higher_score";
|
|
173
|
+
return `score_bucket_${index}`;
|
|
174
|
+
}
|
|
175
|
+
function encodeNumberResult(value, labels) {
|
|
176
|
+
if (labels.values.length === 0) {
|
|
177
|
+
throw new TypeError("Numeric result has no Needle label.");
|
|
178
|
+
}
|
|
179
|
+
let closestIndex = 0;
|
|
180
|
+
let closestDistance = Math.abs(value - labels.values[0]);
|
|
181
|
+
for (let index = 1; index < labels.values.length; index += 1) {
|
|
182
|
+
const distance = Math.abs(value - labels.values[index]);
|
|
183
|
+
if (distance < closestDistance) {
|
|
184
|
+
closestIndex = index;
|
|
185
|
+
closestDistance = distance;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return numberLabel(closestIndex, labels.values.length);
|
|
189
|
+
}
|
|
190
|
+
function createNeedleTool(config, numberLabels) {
|
|
141
191
|
const description = "The classification result.";
|
|
142
192
|
let result;
|
|
143
193
|
switch (config.type) {
|
|
144
194
|
case "number":
|
|
195
|
+
if (!numberLabels || numberLabels.values.length === 0) {
|
|
196
|
+
throw new TypeError("Numeric Needle tools require at least one result label.");
|
|
197
|
+
}
|
|
145
198
|
result = {
|
|
146
|
-
type: "
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
199
|
+
type: "string",
|
|
200
|
+
enum: numberLabels.values.map(
|
|
201
|
+
(_, index) => numberLabel(index, numberLabels.values.length)
|
|
202
|
+
),
|
|
203
|
+
description: `The numeric classification bucket from ${config.min} to ${config.max}.`
|
|
150
204
|
};
|
|
151
205
|
break;
|
|
152
206
|
case "boolean":
|
|
@@ -167,12 +221,13 @@ function createNeedleTool(config) {
|
|
|
167
221
|
}
|
|
168
222
|
};
|
|
169
223
|
}
|
|
170
|
-
function createTrainingLine(input, value, config) {
|
|
224
|
+
function createTrainingLine(input, value, config, numberLabels) {
|
|
171
225
|
const result = validateNeedleResult(value, config);
|
|
226
|
+
const needleResult = config.type === "number" ? encodeNumberResult(result, numberLabels ?? { values: [] }) : result;
|
|
172
227
|
return JSON.stringify({
|
|
173
228
|
query: input,
|
|
174
|
-
tools: [createNeedleTool(config)],
|
|
175
|
-
answers: [{ name: "classify", arguments: { result } }]
|
|
229
|
+
tools: [createNeedleTool(config, numberLabels)],
|
|
230
|
+
answers: [{ name: "classify", arguments: { result: needleResult } }]
|
|
176
231
|
});
|
|
177
232
|
}
|
|
178
233
|
function validateNeedleResult(value, config) {
|
|
@@ -196,11 +251,62 @@ function validateNeedleResult(value, config) {
|
|
|
196
251
|
return value;
|
|
197
252
|
}
|
|
198
253
|
}
|
|
254
|
+
function decodeNeedleResult(result, config, numberLabels) {
|
|
255
|
+
if (config.type === "number") {
|
|
256
|
+
if (typeof result !== "string") {
|
|
257
|
+
throw new TypeError(
|
|
258
|
+
`Needle returned a number outside ${config.min} to ${config.max}.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const index = numberLabels?.values.findIndex(
|
|
262
|
+
(_, candidateIndex) => result === numberLabel(candidateIndex, numberLabels.values.length)
|
|
263
|
+
) ?? -1;
|
|
264
|
+
const decoded = numberLabels?.values[index];
|
|
265
|
+
if (index < 0 || decoded === void 0) {
|
|
266
|
+
throw new TypeError(
|
|
267
|
+
`Needle returned a number outside ${config.min} to ${config.max}.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return validateNeedleResult(decoded, config);
|
|
271
|
+
}
|
|
272
|
+
return validateNeedleResult(result, config);
|
|
273
|
+
}
|
|
199
274
|
|
|
200
275
|
// src/runtime.ts
|
|
201
276
|
var NEEDLE_VERSION = "2.0.14";
|
|
277
|
+
var NEEDLE_NUMBER_MODEL_VERSION = `${NEEDLE_VERSION}/number-buckets-v1`;
|
|
202
278
|
var UV_VERSION = "0.11.4";
|
|
203
279
|
var PYTHON_VERSION = "3.12";
|
|
280
|
+
function needleModelVersion(resultConfig) {
|
|
281
|
+
return resultConfig.type === "number" ? NEEDLE_NUMBER_MODEL_VERSION : NEEDLE_VERSION;
|
|
282
|
+
}
|
|
283
|
+
var NeedleModelArtifactError = class extends SwapAIError {
|
|
284
|
+
constructor(message, cause) {
|
|
285
|
+
super(
|
|
286
|
+
"service_unavailable",
|
|
287
|
+
message,
|
|
288
|
+
cause === void 0 ? void 0 : { cause }
|
|
289
|
+
);
|
|
290
|
+
this.name = "NeedleModelArtifactError";
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
function hasNeedleModelArtifacts(options) {
|
|
294
|
+
try {
|
|
295
|
+
if (!statSync(options.modelPath).isFile()) return false;
|
|
296
|
+
if (options.resultConfig.type !== "number") return true;
|
|
297
|
+
const saved = JSON.parse(
|
|
298
|
+
readFileSync(`${options.modelPath}.numbers.json`, "utf8")
|
|
299
|
+
);
|
|
300
|
+
if (saved.format !== 1 || !Array.isArray(saved.values) || saved.values.length === 0) return false;
|
|
301
|
+
const savedValues = saved.values;
|
|
302
|
+
const labels = createNeedleNumberLabels(savedValues, options.resultConfig);
|
|
303
|
+
return labels.values.length === savedValues.length && labels.values.every(
|
|
304
|
+
(value, index) => value === savedValues[index]
|
|
305
|
+
);
|
|
306
|
+
} catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
204
310
|
var NeedleRuntimeError = class extends SwapAIError {
|
|
205
311
|
constructor(code, message, cause) {
|
|
206
312
|
super(code, message, cause === void 0 ? void 0 : { cause });
|
|
@@ -285,8 +391,18 @@ var ManagedNeedleRuntime = class {
|
|
|
285
391
|
const modelPath = join(candidateDirectory, "model.cact");
|
|
286
392
|
await mkdir(checkpointDirectory, { recursive: true, mode: 448 });
|
|
287
393
|
await mkdir(candidateDirectory, { recursive: true, mode: 448 });
|
|
394
|
+
const numberLabels = options.resultConfig.type === "number" ? createNeedleNumberLabels(
|
|
395
|
+
options.examples.map((example) => example.result),
|
|
396
|
+
options.resultConfig,
|
|
397
|
+
options.acceptableError
|
|
398
|
+
) : void 0;
|
|
288
399
|
const lines = options.examples.map(
|
|
289
|
-
(example) => createTrainingLine(
|
|
400
|
+
(example) => createTrainingLine(
|
|
401
|
+
example.input,
|
|
402
|
+
example.result,
|
|
403
|
+
options.resultConfig,
|
|
404
|
+
numberLabels
|
|
405
|
+
)
|
|
290
406
|
);
|
|
291
407
|
const lockPath = join(
|
|
292
408
|
this.#dataDirectory,
|
|
@@ -294,33 +410,39 @@ var ManagedNeedleRuntime = class {
|
|
|
294
410
|
`${createHash("sha256").update(options.classifierName).digest("hex")}.sqlite`
|
|
295
411
|
);
|
|
296
412
|
try {
|
|
413
|
+
const trainArguments = [
|
|
414
|
+
this.#workerPath,
|
|
415
|
+
"train",
|
|
416
|
+
"--training-data",
|
|
417
|
+
trainingPath,
|
|
418
|
+
"--output",
|
|
419
|
+
modelPath,
|
|
420
|
+
"--checkpoint-dir",
|
|
421
|
+
checkpointDirectory,
|
|
422
|
+
"--epochs",
|
|
423
|
+
String(options.epochs ?? 10),
|
|
424
|
+
"--artifact-lock-database",
|
|
425
|
+
lockPath,
|
|
426
|
+
"--main-database",
|
|
427
|
+
join(this.#dataDirectory, "swapai.sqlite"),
|
|
428
|
+
"--classifier-name",
|
|
429
|
+
options.classifierName,
|
|
430
|
+
"--expected-epoch",
|
|
431
|
+
String(options.expectedEpoch)
|
|
432
|
+
];
|
|
433
|
+
if (numberLabels) {
|
|
434
|
+
trainArguments.push("--numeric-labels-from-stdin");
|
|
435
|
+
}
|
|
297
436
|
await this.#runCommand(
|
|
298
437
|
this.#pythonPath,
|
|
299
|
-
|
|
300
|
-
this.#workerPath,
|
|
301
|
-
"train",
|
|
302
|
-
"--training-data",
|
|
303
|
-
trainingPath,
|
|
304
|
-
"--output",
|
|
305
|
-
modelPath,
|
|
306
|
-
"--checkpoint-dir",
|
|
307
|
-
checkpointDirectory,
|
|
308
|
-
"--epochs",
|
|
309
|
-
String(options.epochs ?? 10),
|
|
310
|
-
"--artifact-lock-database",
|
|
311
|
-
lockPath,
|
|
312
|
-
"--main-database",
|
|
313
|
-
join(this.#dataDirectory, "swapai.sqlite"),
|
|
314
|
-
"--classifier-name",
|
|
315
|
-
options.classifierName,
|
|
316
|
-
"--expected-epoch",
|
|
317
|
-
String(options.expectedEpoch)
|
|
318
|
-
],
|
|
438
|
+
trainArguments,
|
|
319
439
|
{
|
|
320
440
|
cwd: candidateDirectory,
|
|
321
441
|
env: this.#environment,
|
|
322
442
|
timeoutMs: 6 * 60 * 60 * 1e3,
|
|
323
|
-
stdin: `${
|
|
443
|
+
stdin: numberLabels ? `${JSON.stringify({ format: 1, values: numberLabels.values })}
|
|
444
|
+
${lines.join("\n")}
|
|
445
|
+
` : `${lines.join("\n")}
|
|
324
446
|
`
|
|
325
447
|
}
|
|
326
448
|
);
|
|
@@ -328,19 +450,51 @@ var ManagedNeedleRuntime = class {
|
|
|
328
450
|
} catch (error) {
|
|
329
451
|
throw toRuntimeError("Needle training failed.", error);
|
|
330
452
|
}
|
|
331
|
-
return {
|
|
453
|
+
return {
|
|
454
|
+
modelPath,
|
|
455
|
+
needleVersion: needleModelVersion(options.resultConfig)
|
|
456
|
+
};
|
|
332
457
|
}
|
|
333
458
|
async loadModel(options) {
|
|
334
459
|
await this.ready();
|
|
335
460
|
try {
|
|
336
461
|
await stat(options.modelPath);
|
|
337
462
|
} catch (error) {
|
|
338
|
-
throw
|
|
463
|
+
throw new NeedleModelArtifactError(
|
|
464
|
+
`Needle model does not exist: ${options.modelPath}`,
|
|
465
|
+
error
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
let numberLabels;
|
|
469
|
+
if (options.resultConfig.type === "number") {
|
|
470
|
+
try {
|
|
471
|
+
const saved = JSON.parse(
|
|
472
|
+
await readFile(`${options.modelPath}.numbers.json`, "utf8")
|
|
473
|
+
);
|
|
474
|
+
if (saved.format !== 1 || !Array.isArray(saved.values) || saved.values.length === 0) {
|
|
475
|
+
throw new TypeError("Numeric label map has an invalid format.");
|
|
476
|
+
}
|
|
477
|
+
const savedValues = saved.values;
|
|
478
|
+
numberLabels = createNeedleNumberLabels(
|
|
479
|
+
savedValues,
|
|
480
|
+
options.resultConfig
|
|
481
|
+
);
|
|
482
|
+
if (numberLabels.values.length !== savedValues.length || numberLabels.values.some(
|
|
483
|
+
(value, index) => value !== savedValues[index]
|
|
484
|
+
)) {
|
|
485
|
+
throw new TypeError("Numeric label map is not sorted and unique.");
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new NeedleModelArtifactError(
|
|
489
|
+
"Could not load Needle numeric labels.",
|
|
490
|
+
error
|
|
491
|
+
);
|
|
492
|
+
}
|
|
339
493
|
}
|
|
340
494
|
const schemaPath = `${options.modelPath}.schema.json`;
|
|
341
495
|
await writeFile(
|
|
342
496
|
schemaPath,
|
|
343
|
-
JSON.stringify([createNeedleTool(options.resultConfig)]),
|
|
497
|
+
JSON.stringify([createNeedleTool(options.resultConfig, numberLabels)]),
|
|
344
498
|
"utf8"
|
|
345
499
|
);
|
|
346
500
|
const process2 = this.#spawnProcess(
|
|
@@ -359,6 +513,7 @@ var ManagedNeedleRuntime = class {
|
|
|
359
513
|
const model = new NeedleModelProcess(
|
|
360
514
|
process2,
|
|
361
515
|
options.resultConfig,
|
|
516
|
+
numberLabels,
|
|
362
517
|
(error) => this.#onBackgroundError?.(error),
|
|
363
518
|
() => this.#models.delete(model)
|
|
364
519
|
);
|
|
@@ -599,6 +754,7 @@ var ManagedNeedleRuntime = class {
|
|
|
599
754
|
var NeedleModelProcess = class {
|
|
600
755
|
#process;
|
|
601
756
|
#resultConfig;
|
|
757
|
+
#numberLabels;
|
|
602
758
|
#onBackgroundError;
|
|
603
759
|
#onClose;
|
|
604
760
|
#pending = /* @__PURE__ */ new Map();
|
|
@@ -612,9 +768,10 @@ var NeedleModelProcess = class {
|
|
|
612
768
|
#exited = false;
|
|
613
769
|
#closing = false;
|
|
614
770
|
#stderr = "";
|
|
615
|
-
constructor(process2, resultConfig, onBackgroundError, onClose) {
|
|
771
|
+
constructor(process2, resultConfig, numberLabels, onBackgroundError, onClose) {
|
|
616
772
|
this.#process = process2;
|
|
617
773
|
this.#resultConfig = resultConfig;
|
|
774
|
+
this.#numberLabels = numberLabels;
|
|
618
775
|
this.#onBackgroundError = onBackgroundError;
|
|
619
776
|
this.#onClose = onClose;
|
|
620
777
|
this.#readyPromise = new Promise((resolve2, reject) => {
|
|
@@ -765,7 +922,13 @@ var NeedleModelProcess = class {
|
|
|
765
922
|
return;
|
|
766
923
|
}
|
|
767
924
|
try {
|
|
768
|
-
pending.resolve(
|
|
925
|
+
pending.resolve(
|
|
926
|
+
decodeNeedleResult(
|
|
927
|
+
message.result,
|
|
928
|
+
this.#resultConfig,
|
|
929
|
+
this.#numberLabels
|
|
930
|
+
)
|
|
931
|
+
);
|
|
769
932
|
} catch (error) {
|
|
770
933
|
pending.reject(
|
|
771
934
|
new NeedleRuntimeError(
|
|
@@ -1359,7 +1522,7 @@ function openStorage(options) {
|
|
|
1359
1522
|
WHERE name = ? AND training_lease_owner = ?
|
|
1360
1523
|
`).run(Date.now(), options.name, owner);
|
|
1361
1524
|
},
|
|
1362
|
-
archiveAndReset(expectedDataEpoch) {
|
|
1525
|
+
archiveAndReset(expectedDataEpoch, resetOptions) {
|
|
1363
1526
|
assertOpen(closed);
|
|
1364
1527
|
let nextGeneration = 0;
|
|
1365
1528
|
const changedAt = Date.now();
|
|
@@ -1385,11 +1548,19 @@ function openStorage(options) {
|
|
|
1385
1548
|
classifier_name, generation, status, created_at
|
|
1386
1549
|
) VALUES (?, ?, 'active', ?)
|
|
1387
1550
|
`).run(options.name, nextGeneration, changedAt);
|
|
1551
|
+
let retainedExampleCount = 0;
|
|
1552
|
+
if (resetOptions?.retainExamples) {
|
|
1553
|
+
retainedExampleCount = Number(database.prepare(`
|
|
1554
|
+
UPDATE examples
|
|
1555
|
+
SET generation = ?
|
|
1556
|
+
WHERE classifier_name = ? AND generation = ?
|
|
1557
|
+
`).run(nextGeneration, options.name, currentGeneration).changes);
|
|
1558
|
+
}
|
|
1388
1559
|
database.prepare(`
|
|
1389
1560
|
UPDATE classifiers
|
|
1390
1561
|
SET active_generation = ?,
|
|
1391
1562
|
data_epoch = data_epoch + 1,
|
|
1392
|
-
new_examples_since_training =
|
|
1563
|
+
new_examples_since_training = ?,
|
|
1393
1564
|
training_attempts = 0,
|
|
1394
1565
|
examples_used_for_training = 0,
|
|
1395
1566
|
training_lease_owner = NULL,
|
|
@@ -1399,7 +1570,12 @@ function openStorage(options) {
|
|
|
1399
1570
|
consecutive_retest_failures = 0,
|
|
1400
1571
|
updated_at = ?
|
|
1401
1572
|
WHERE name = ?
|
|
1402
|
-
`).run(
|
|
1573
|
+
`).run(
|
|
1574
|
+
nextGeneration,
|
|
1575
|
+
retainedExampleCount,
|
|
1576
|
+
changedAt,
|
|
1577
|
+
options.name
|
|
1578
|
+
);
|
|
1403
1579
|
reset = true;
|
|
1404
1580
|
});
|
|
1405
1581
|
return reset ? generationByNumber(database, options.name, nextGeneration) : null;
|
|
@@ -1804,8 +1980,11 @@ function createClassifier(config, storage, runtime) {
|
|
|
1804
1980
|
let dataEpoch = initialState.dataEpoch;
|
|
1805
1981
|
let clearedEpoch = initialState.clearPending ? initialState.dataEpoch - 1 : initialState.dataEpoch;
|
|
1806
1982
|
const saved = initialState;
|
|
1807
|
-
if (!saved.clearPending && saved.trained && (saved.modelPath === null || saved.needleVersion !==
|
|
1808
|
-
|
|
1983
|
+
if (!saved.clearPending && saved.trained && (saved.modelPath === null || saved.needleVersion !== needleModelVersion(config.result) || !hasNeedleModelArtifacts({
|
|
1984
|
+
modelPath: saved.modelPath,
|
|
1985
|
+
resultConfig: config.result
|
|
1986
|
+
}))) {
|
|
1987
|
+
if (storage.archiveAndReset(saved.dataEpoch, { retainExamples: true }) !== null) {
|
|
1809
1988
|
const reset = storage.snapshot();
|
|
1810
1989
|
dataEpoch = reset.dataEpoch;
|
|
1811
1990
|
clearedEpoch = reset.dataEpoch;
|
|
@@ -1827,13 +2006,22 @@ function createClassifier(config, storage, runtime) {
|
|
|
1827
2006
|
}
|
|
1828
2007
|
try {
|
|
1829
2008
|
await runtime.ready();
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
rememberBackgroundFailure(toSwapAIError(
|
|
2011
|
+
error,
|
|
2012
|
+
"service_unavailable",
|
|
2013
|
+
"Needle could not start"
|
|
2014
|
+
));
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const snapshot = storage.snapshot();
|
|
2018
|
+
if (snapshot.clearPending) {
|
|
2019
|
+
trained = false;
|
|
2020
|
+
dataEpoch = snapshot.dataEpoch;
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (snapshot.trained && snapshot.modelPath !== null) {
|
|
2024
|
+
try {
|
|
1837
2025
|
const restoredModel = await runtime.loadModel({
|
|
1838
2026
|
modelPath: snapshot.modelPath,
|
|
1839
2027
|
resultConfig: config.result
|
|
@@ -1846,15 +2034,28 @@ function createClassifier(config, storage, runtime) {
|
|
|
1846
2034
|
} else {
|
|
1847
2035
|
await restoredModel.close();
|
|
1848
2036
|
}
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
if (error instanceof NeedleModelArtifactError && storage.archiveAndReset(snapshot.dataEpoch, {
|
|
2039
|
+
retainExamples: true
|
|
2040
|
+
}) !== null) {
|
|
2041
|
+
const reset = refreshStoredState();
|
|
2042
|
+
dataEpoch = reset.dataEpoch;
|
|
2043
|
+
clearedEpoch = reset.dataEpoch;
|
|
2044
|
+
trained = false;
|
|
2045
|
+
}
|
|
2046
|
+
const loadError = toSwapAIError(
|
|
2047
|
+
error,
|
|
2048
|
+
"service_unavailable",
|
|
2049
|
+
error instanceof NeedleModelArtifactError ? `Could not load classifier "${config.name}"; retraining from saved examples` : `Could not load classifier "${config.name}"`
|
|
2050
|
+
);
|
|
2051
|
+
if (error instanceof NeedleModelArtifactError) {
|
|
2052
|
+
reportBackgroundError(config, loadError);
|
|
2053
|
+
} else {
|
|
2054
|
+
rememberBackgroundFailure(loadError);
|
|
2055
|
+
}
|
|
1849
2056
|
}
|
|
1850
|
-
} catch (error) {
|
|
1851
|
-
const swapAIError = toSwapAIError(
|
|
1852
|
-
error,
|
|
1853
|
-
"service_unavailable",
|
|
1854
|
-
"Needle could not start"
|
|
1855
|
-
);
|
|
1856
|
-
rememberBackgroundFailure(swapAIError);
|
|
1857
2057
|
}
|
|
2058
|
+
scheduleTraining();
|
|
1858
2059
|
}
|
|
1859
2060
|
function rememberBackgroundFailure(error) {
|
|
1860
2061
|
queuedFailure = error;
|
|
@@ -1971,7 +2172,8 @@ function createClassifier(config, storage, runtime) {
|
|
|
1971
2172
|
generation: snapshot.activeGeneration,
|
|
1972
2173
|
expectedEpoch: trainingEpoch,
|
|
1973
2174
|
examples: trainingExamples.map(({ input, result }) => ({ input, result })),
|
|
1974
|
-
resultConfig: config.result
|
|
2175
|
+
resultConfig: config.result,
|
|
2176
|
+
acceptableError: config.acceptableError
|
|
1975
2177
|
});
|
|
1976
2178
|
const candidateModel = await runtime.loadModel({
|
|
1977
2179
|
modelPath: candidate.modelPath,
|
|
@@ -1980,10 +2182,18 @@ function createClassifier(config, storage, runtime) {
|
|
|
1980
2182
|
try {
|
|
1981
2183
|
const comparisons = [];
|
|
1982
2184
|
for (const example of heldOutExamples) {
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2185
|
+
let candidateResult;
|
|
2186
|
+
try {
|
|
2187
|
+
candidateResult = validateResult(
|
|
2188
|
+
config.result,
|
|
2189
|
+
await candidateModel.classify(example.input)
|
|
2190
|
+
);
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
if (error instanceof SwapAIError && error.code === "classification_failed") {
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
throw error;
|
|
2196
|
+
}
|
|
1987
2197
|
comparisons.push({
|
|
1988
2198
|
reference: validateResult(config.result, example.result),
|
|
1989
2199
|
candidate: candidateResult
|
|
@@ -2062,10 +2272,22 @@ function createClassifier(config, storage, runtime) {
|
|
|
2062
2272
|
);
|
|
2063
2273
|
}
|
|
2064
2274
|
await runtime.ready();
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2275
|
+
let restored;
|
|
2276
|
+
try {
|
|
2277
|
+
restored = await runtime.loadModel({
|
|
2278
|
+
modelPath: current.modelPath,
|
|
2279
|
+
resultConfig: config.result
|
|
2280
|
+
});
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
if (error instanceof NeedleModelArtifactError && storage.archiveAndReset(expectedEpoch, { retainExamples: true }) !== null) {
|
|
2283
|
+
const reset = refreshStoredState();
|
|
2284
|
+
dataEpoch = reset.dataEpoch;
|
|
2285
|
+
clearedEpoch = reset.dataEpoch;
|
|
2286
|
+
trained = false;
|
|
2287
|
+
scheduleTraining();
|
|
2288
|
+
}
|
|
2289
|
+
throw error;
|
|
2290
|
+
}
|
|
2069
2291
|
const afterLoad = refreshStoredState();
|
|
2070
2292
|
if (afterLoad.dataEpoch !== expectedEpoch || afterLoad.clearPending || !afterLoad.trained) {
|
|
2071
2293
|
await restored.close();
|