@klhapp/skillmux 1.0.1 → 1.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/CHANGELOG.md +53 -0
- package/README.md +83 -29
- package/config.remote.example.toml +6 -4
- package/docs/calibration.md +106 -0
- package/docs/configuration.md +51 -7
- package/docs/schema.json +13 -5
- package/package.json +2 -1
- package/src/adapters.ts +111 -38
- package/src/calibrate.ts +623 -125
- package/src/cli.ts +56 -6
- package/src/clients.ts +264 -48
- package/src/config-service.ts +25 -8
- package/src/config-watcher.ts +7 -0
- package/src/config.ts +106 -25
- package/src/dataset-generator.ts +75 -96
- package/src/decision.ts +4 -1
- package/src/doctor.ts +16 -5
- package/src/eval.ts +2 -1
- package/src/router-core.ts +73 -42
- package/src/server.ts +9 -66
- package/src/types.ts +3 -3
package/src/adapters.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { applyCalibrationRun, getCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, type CalibrationResult } from "./calibrate";
|
|
3
|
+
import { applyCalibrationRun, getCalibrationRun, insertCalibrationRun, listCalibrationRuns, loadDecisionCasesFromFile, openCalibrateDb, runCalibration, summarizeDatasetProvenance, type CalibrationResult } from "./calibrate";
|
|
4
4
|
import { createClients } from "./clients";
|
|
5
|
-
import { DEFAULT_CONFIG_PATH, expandHome, loadConfig } from "./config";
|
|
5
|
+
import { DEFAULT_CONFIG_PATH, embeddingFingerprint, expandHome, loadConfig, rerankerFingerprint } from "./config";
|
|
6
|
+
import { openIndex } from "./db";
|
|
6
7
|
import { CliError } from "./output";
|
|
7
8
|
import {
|
|
8
9
|
computeHash,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
type SetConfigResult,
|
|
19
20
|
} from "./config-service";
|
|
20
21
|
import type { ResolvedTarget } from "./context";
|
|
21
|
-
import {
|
|
22
|
+
import { configure, retrieveAndRerank } from "./router-core";
|
|
22
23
|
import type { Config } from "./types";
|
|
23
24
|
|
|
24
25
|
export interface Capabilities {
|
|
@@ -43,7 +44,13 @@ export interface TargetAdapter {
|
|
|
43
44
|
configDiff(): Promise<{ diff: Record<string, { prior: unknown; resulting: unknown }> }>;
|
|
44
45
|
configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult>;
|
|
45
46
|
configStatus(): Promise<ConfigStatusResponse>;
|
|
46
|
-
calibrateRun(opts?: {
|
|
47
|
+
calibrateRun(opts?: {
|
|
48
|
+
datasetPath?: string;
|
|
49
|
+
minAutoMatchPrecision?: number;
|
|
50
|
+
minRetrievalRecallAtK?: number;
|
|
51
|
+
minDeliveredShortlistRecallAtK?: number;
|
|
52
|
+
minAutoMatchCount?: number;
|
|
53
|
+
}): Promise<{ run_id?: string; result?: CalibrationResult }>;
|
|
47
54
|
calibrateList(): Promise<any[]>;
|
|
48
55
|
calibrateShow(runId: string): Promise<any>;
|
|
49
56
|
calibrateApply(runId: string): Promise<any>;
|
|
@@ -123,26 +130,89 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
123
130
|
return getLocalConfigStatus(this.configPath);
|
|
124
131
|
}
|
|
125
132
|
|
|
126
|
-
async calibrateRun(opts?: {
|
|
133
|
+
async calibrateRun(opts?: {
|
|
134
|
+
datasetPath?: string;
|
|
135
|
+
minAutoMatchPrecision?: number;
|
|
136
|
+
minRetrievalRecallAtK?: number;
|
|
137
|
+
minDeliveredShortlistRecallAtK?: number;
|
|
138
|
+
minAutoMatchCount?: number;
|
|
139
|
+
}): Promise<{ run_id?: string; result?: CalibrationResult }> {
|
|
127
140
|
const config = await loadConfig(this.configPath);
|
|
128
141
|
const datasetFile = opts?.datasetPath ?? join(expandHome(config.state_dir), "queries.json");
|
|
129
|
-
const
|
|
142
|
+
const indexDb = openIndex(expandHome(config.state_dir));
|
|
143
|
+
let indexedSkills: Array<{ skill_id: string; content_sha256: string }>;
|
|
144
|
+
try {
|
|
145
|
+
indexedSkills = indexDb
|
|
146
|
+
.query("SELECT skill_id, content_sha256 FROM skills ORDER BY skill_id")
|
|
147
|
+
.all() as Array<{ skill_id: string; content_sha256: string }>;
|
|
148
|
+
} finally {
|
|
149
|
+
indexDb.close();
|
|
150
|
+
}
|
|
151
|
+
const cases = loadDecisionCasesFromFile(
|
|
152
|
+
datasetFile,
|
|
153
|
+
indexedSkills.map((skill) => skill.skill_id),
|
|
154
|
+
);
|
|
130
155
|
const clients = createClients(config);
|
|
156
|
+
configure({ config, clients });
|
|
131
157
|
const result = await runCalibration({
|
|
132
158
|
cases,
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
|
|
159
|
+
getRankedCandidates: async (query: string) => {
|
|
160
|
+
const result = await retrieveAndRerank({ query, forceLexical: false });
|
|
161
|
+
if (result.retrieval !== "reranked") {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"Calibration requires successful hybrid retrieval and reranking for every query.",
|
|
164
|
+
);
|
|
137
165
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
166
|
+
return result.candidates.map((candidate) => ({
|
|
167
|
+
skill_id: candidate.skill_id,
|
|
168
|
+
score: candidate.score ?? 0,
|
|
169
|
+
}));
|
|
142
170
|
},
|
|
143
171
|
reranker: clients.rerank,
|
|
172
|
+
candidateLimit: config.thresholds.candidate_limit,
|
|
173
|
+
minAutoMatchPrecision: opts?.minAutoMatchPrecision,
|
|
174
|
+
minRetrievalRecallAtK: opts?.minRetrievalRecallAtK,
|
|
175
|
+
minDeliveredShortlistRecallAtK: opts?.minDeliveredShortlistRecallAtK,
|
|
176
|
+
minAutoMatchCount: opts?.minAutoMatchCount,
|
|
144
177
|
});
|
|
145
|
-
|
|
178
|
+
const fingerprint = rerankerFingerprint(config);
|
|
179
|
+
if (!fingerprint) {
|
|
180
|
+
throw new Error("A configured remote reranker is required to record calibration.");
|
|
181
|
+
}
|
|
182
|
+
const datasetText = await Bun.file(datasetFile).text();
|
|
183
|
+
const corpusFingerprint =
|
|
184
|
+
"vault:" +
|
|
185
|
+
createHash("sha256").update(JSON.stringify(indexedSkills)).digest("hex");
|
|
186
|
+
const runId = `run_${crypto.randomUUID()}`;
|
|
187
|
+
const db = openCalibrateDb(expandHome(config.state_dir));
|
|
188
|
+
try {
|
|
189
|
+
insertCalibrationRun(db, {
|
|
190
|
+
run_id: runId,
|
|
191
|
+
created_at: new Date().toISOString(),
|
|
192
|
+
status: result.status,
|
|
193
|
+
reranker_fingerprint: fingerprint,
|
|
194
|
+
embedding_fingerprint: embeddingFingerprint(config),
|
|
195
|
+
corpus_fingerprint: corpusFingerprint,
|
|
196
|
+
dataset_hash: createHash("sha256").update(datasetText).digest("hex"),
|
|
197
|
+
dataset_provenance: summarizeDatasetProvenance(cases),
|
|
198
|
+
candidate_limit: config.thresholds.candidate_limit,
|
|
199
|
+
min_auto_match_precision: opts?.minAutoMatchPrecision ?? 0.99,
|
|
200
|
+
min_auto_match_count: opts?.minAutoMatchCount ?? 30,
|
|
201
|
+
min_delivered_shortlist_recall_at_k:
|
|
202
|
+
opts?.minDeliveredShortlistRecallAtK ??
|
|
203
|
+
opts?.minRetrievalRecallAtK ??
|
|
204
|
+
0.95,
|
|
205
|
+
min_shortlist_recall_at_5: opts?.minRetrievalRecallAtK ?? 0.95,
|
|
206
|
+
failed_reason: result.failed_reason,
|
|
207
|
+
selected_thresholds: result.selected_thresholds,
|
|
208
|
+
tune_metrics: result.tune_metrics,
|
|
209
|
+
test_metrics: result.test_metrics,
|
|
210
|
+
observations: result.observations,
|
|
211
|
+
});
|
|
212
|
+
} finally {
|
|
213
|
+
db.close();
|
|
214
|
+
}
|
|
215
|
+
return { run_id: runId, result };
|
|
146
216
|
}
|
|
147
217
|
|
|
148
218
|
async calibrateList(): Promise<any[]> {
|
|
@@ -173,7 +243,9 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
173
243
|
try {
|
|
174
244
|
const run = getCalibrationRun(db, runId);
|
|
175
245
|
if (!run) throw new Error(`Calibration run "${runId}" not found`);
|
|
176
|
-
await applyCalibrationRun(db, runId, expandHome(this.configPath), {
|
|
246
|
+
await applyCalibrationRun(db, runId, expandHome(this.configPath), {
|
|
247
|
+
currentRerankerFingerprint: rerankerFingerprint(config),
|
|
248
|
+
});
|
|
177
249
|
return { ok: true, run_id: runId };
|
|
178
250
|
} finally {
|
|
179
251
|
db.close();
|
|
@@ -345,36 +417,37 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
345
417
|
return data.runtime;
|
|
346
418
|
}
|
|
347
419
|
|
|
348
|
-
async calibrateRun(opts?: {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return data;
|
|
420
|
+
async calibrateRun(opts?: {
|
|
421
|
+
datasetPath?: string;
|
|
422
|
+
minAutoMatchPrecision?: number;
|
|
423
|
+
minRetrievalRecallAtK?: number;
|
|
424
|
+
minDeliveredShortlistRecallAtK?: number;
|
|
425
|
+
minAutoMatchCount?: number;
|
|
426
|
+
}): Promise<{ run_id?: string; result?: CalibrationResult }> {
|
|
427
|
+
void opts;
|
|
428
|
+
throw this.remoteCalibrationNotImplemented();
|
|
358
429
|
}
|
|
359
430
|
|
|
360
431
|
async calibrateList(): Promise<any[]> {
|
|
361
|
-
|
|
362
|
-
if (status !== 200) throw new Error(`Remote calibration list failed (${status}): ${data?.message || data}`);
|
|
363
|
-
return data;
|
|
432
|
+
throw this.remoteCalibrationNotImplemented();
|
|
364
433
|
}
|
|
365
434
|
|
|
366
435
|
async calibrateShow(runId: string): Promise<any> {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return data;
|
|
436
|
+
void runId;
|
|
437
|
+
throw this.remoteCalibrationNotImplemented();
|
|
370
438
|
}
|
|
371
439
|
|
|
372
440
|
async calibrateApply(runId: string): Promise<any> {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
441
|
+
void runId;
|
|
442
|
+
throw this.remoteCalibrationNotImplemented();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private remoteCalibrationNotImplemented(): CliError {
|
|
446
|
+
return new CliError(
|
|
447
|
+
`Remote calibration is not implemented for target ${this.serverUrl}; ` +
|
|
448
|
+
"run `skillmux calibrate` against a local target.",
|
|
449
|
+
2,
|
|
450
|
+
);
|
|
378
451
|
}
|
|
379
452
|
}
|
|
380
453
|
|