@klhapp/skillmux 1.7.1 → 1.8.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 +7 -0
- package/README.md +14 -5
- package/docs/assets/architecture-dark.svg +160 -0
- package/docs/assets/{architecture.svg → architecture-light.svg} +40 -34
- package/docs/assets/logo-dark.png +0 -0
- package/docs/assets/logo-light.png +0 -0
- package/docs/cli.md +63 -5
- package/docs/concepts.md +10 -0
- package/docs/configuration.md +20 -20
- package/docs/deployment.md +20 -7
- package/docs/getting-started.md +11 -0
- package/docs/mcp-routing.md +41 -7
- package/docs/schema.json +31 -4
- package/package.json +1 -1
- package/src/audit.ts +1 -0
- package/src/cli.ts +28 -8
- package/src/commands/audit.ts +82 -0
- package/src/commands/eval.ts +81 -0
- package/src/config.ts +6 -0
- package/src/db.ts +152 -45
- package/src/eval.ts +69 -0
- package/src/router-core.ts +95 -24
- package/src/server.ts +21 -6
- package/src/stats.ts +119 -13
- package/src/types.ts +22 -0
- package/docs/assets/logo.png +0 -0
package/src/stats.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
import type { AuditCandidate, AuditRow } from "./types";
|
|
2
|
+
import type { AuditCandidate, AuditRow, FetchAuditRow } from "./types";
|
|
3
3
|
|
|
4
4
|
export const SINCE_PATTERN = /^(\d+[hdwmy]|\d{4}-\d{2}-\d{2}([T ].+)?)$/;
|
|
5
5
|
|
|
@@ -20,6 +20,18 @@ export interface RetrievalTotals {
|
|
|
20
20
|
lexical: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export type AcceptanceSignal =
|
|
24
|
+
| { available: false; uncorrelated_fetch_count: number }
|
|
25
|
+
| {
|
|
26
|
+
available: true;
|
|
27
|
+
resolves_with_candidates: number;
|
|
28
|
+
accepted_count: number;
|
|
29
|
+
acceptance_rate: number;
|
|
30
|
+
observed_mrr: number;
|
|
31
|
+
top1_acceptance_rate: number;
|
|
32
|
+
uncorrelated_fetch_count: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
export interface StatsResponse {
|
|
24
36
|
since: string;
|
|
25
37
|
until: string;
|
|
@@ -31,6 +43,8 @@ export interface StatsResponse {
|
|
|
31
43
|
average_latency_ms: number;
|
|
32
44
|
skills: SkillStat[];
|
|
33
45
|
top_empty_shortlist_queries: EmptyShortlistQuery[];
|
|
46
|
+
acceptance: AcceptanceSignal;
|
|
47
|
+
top_unused_shortlist_queries: EmptyShortlistQuery[];
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
const RELATIVE_WINDOW = /^(\d+)([hdwmy])$/;
|
|
@@ -63,10 +77,16 @@ function compareCodeUnits(a: string, b: string): number {
|
|
|
63
77
|
return 0;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
export function computeStats(
|
|
80
|
+
export function computeStats(
|
|
81
|
+
rows: AuditRow[],
|
|
82
|
+
since: Date,
|
|
83
|
+
until: Date,
|
|
84
|
+
fetchRows: FetchAuditRow[] = [],
|
|
85
|
+
): StatsResponse {
|
|
67
86
|
const retrieval_totals: RetrievalTotals = { exact: 0, reranked: 0, hybrid: 0, lexical: 0 };
|
|
68
87
|
const skillCounts = new Map<string, number>();
|
|
69
88
|
const emptyShortlistCounts = new Map<string, number>();
|
|
89
|
+
const resolvesWithCandidates = new Map<number, AuditRow>();
|
|
70
90
|
let empty_shortlist_count = 0;
|
|
71
91
|
let degraded_count = 0;
|
|
72
92
|
let total_latency_ms = 0;
|
|
@@ -86,6 +106,7 @@ export function computeStats(rows: AuditRow[], since: Date, until: Date): StatsR
|
|
|
86
106
|
empty_shortlist_count++;
|
|
87
107
|
emptyShortlistCounts.set(row.query, (emptyShortlistCounts.get(row.query) ?? 0) + 1);
|
|
88
108
|
} else {
|
|
109
|
+
resolvesWithCandidates.set(row.id, row);
|
|
89
110
|
const seenInRow = new Set<string>();
|
|
90
111
|
for (const candidate of row.candidates) {
|
|
91
112
|
if (!candidate.skill_id) continue;
|
|
@@ -96,6 +117,52 @@ export function computeStats(rows: AuditRow[], since: Date, until: Date): StatsR
|
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
let uncorrelated_fetch_count = 0;
|
|
121
|
+
const firstFetchByResolve = new Map<number, FetchAuditRow>();
|
|
122
|
+
for (const fetch of fetchRows) {
|
|
123
|
+
if (fetch.resolve_audit_id === null) {
|
|
124
|
+
uncorrelated_fetch_count++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!resolvesWithCandidates.has(fetch.resolve_audit_id)) continue;
|
|
128
|
+
const existing = firstFetchByResolve.get(fetch.resolve_audit_id);
|
|
129
|
+
if (!existing || fetch.ts < existing.ts) {
|
|
130
|
+
firstFetchByResolve.set(fetch.resolve_audit_id, fetch);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const acceptedResolveIds = new Set(firstFetchByResolve.keys());
|
|
135
|
+
const accepted_count = acceptedResolveIds.size;
|
|
136
|
+
const acceptance: AcceptanceSignal =
|
|
137
|
+
accepted_count > 0
|
|
138
|
+
? (() => {
|
|
139
|
+
let reciprocalRankSum = 0;
|
|
140
|
+
let top1Count = 0;
|
|
141
|
+
for (const fetch of firstFetchByResolve.values()) {
|
|
142
|
+
const rank = fetch.rank_at_resolve;
|
|
143
|
+
if (rank !== null) {
|
|
144
|
+
reciprocalRankSum += 1 / rank;
|
|
145
|
+
if (rank === 1) top1Count++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
available: true as const,
|
|
150
|
+
resolves_with_candidates: resolvesWithCandidates.size,
|
|
151
|
+
accepted_count,
|
|
152
|
+
acceptance_rate: accepted_count / resolvesWithCandidates.size,
|
|
153
|
+
observed_mrr: reciprocalRankSum / accepted_count,
|
|
154
|
+
top1_acceptance_rate: top1Count / accepted_count,
|
|
155
|
+
uncorrelated_fetch_count,
|
|
156
|
+
};
|
|
157
|
+
})()
|
|
158
|
+
: { available: false as const, uncorrelated_fetch_count };
|
|
159
|
+
|
|
160
|
+
const unusedShortlistCounts = new Map<string, number>();
|
|
161
|
+
for (const [id, row] of resolvesWithCandidates) {
|
|
162
|
+
if (acceptedResolveIds.has(id)) continue;
|
|
163
|
+
unusedShortlistCounts.set(row.query, (unusedShortlistCounts.get(row.query) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
99
166
|
const total_requests = rows.length;
|
|
100
167
|
const empty_shortlist_rate = total_requests > 0 ? empty_shortlist_count / total_requests : 0;
|
|
101
168
|
const average_latency_ms = total_requests > 0 ? total_latency_ms / total_requests : 0;
|
|
@@ -109,15 +176,8 @@ export function computeStats(rows: AuditRow[], since: Date, until: Date): StatsR
|
|
|
109
176
|
return compareCodeUnits(a.skill_id, b.skill_id);
|
|
110
177
|
});
|
|
111
178
|
|
|
112
|
-
const top_empty_shortlist_queries
|
|
113
|
-
|
|
114
|
-
.sort((a, b) => {
|
|
115
|
-
if (b.count !== a.count) {
|
|
116
|
-
return b.count - a.count;
|
|
117
|
-
}
|
|
118
|
-
return compareCodeUnits(a.query, b.query);
|
|
119
|
-
})
|
|
120
|
-
.slice(0, 20);
|
|
179
|
+
const top_empty_shortlist_queries = topQueryCounts(emptyShortlistCounts);
|
|
180
|
+
const top_unused_shortlist_queries = topQueryCounts(unusedShortlistCounts);
|
|
121
181
|
|
|
122
182
|
return {
|
|
123
183
|
since: since.toISOString(),
|
|
@@ -130,12 +190,27 @@ export function computeStats(rows: AuditRow[], since: Date, until: Date): StatsR
|
|
|
130
190
|
average_latency_ms,
|
|
131
191
|
skills,
|
|
132
192
|
top_empty_shortlist_queries,
|
|
193
|
+
acceptance,
|
|
194
|
+
top_unused_shortlist_queries,
|
|
133
195
|
};
|
|
134
196
|
}
|
|
135
197
|
|
|
198
|
+
function topQueryCounts(counts: Map<string, number>): EmptyShortlistQuery[] {
|
|
199
|
+
return [...counts.entries()]
|
|
200
|
+
.map(([query, count]) => ({ query, count }))
|
|
201
|
+
.sort((a, b) => {
|
|
202
|
+
if (b.count !== a.count) {
|
|
203
|
+
return b.count - a.count;
|
|
204
|
+
}
|
|
205
|
+
return compareCodeUnits(a.query, b.query);
|
|
206
|
+
})
|
|
207
|
+
.slice(0, 20);
|
|
208
|
+
}
|
|
209
|
+
|
|
136
210
|
interface AuditTableRow {
|
|
137
211
|
id: number;
|
|
138
212
|
ts: string;
|
|
213
|
+
request_id: string | null;
|
|
139
214
|
query: string;
|
|
140
215
|
retrieval: AuditRow["retrieval"];
|
|
141
216
|
degraded_from: string | null;
|
|
@@ -147,7 +222,7 @@ interface AuditTableRow {
|
|
|
147
222
|
export function queryAuditRows(db: Database, sinceIso: string): AuditRow[] {
|
|
148
223
|
const rows = db
|
|
149
224
|
.query(
|
|
150
|
-
"SELECT id, ts, query, retrieval, degraded_from, degradation_reason, candidates, latency_ms FROM audit WHERE ts >= ? ORDER BY ts ASC",
|
|
225
|
+
"SELECT id, ts, request_id, query, retrieval, degraded_from, degradation_reason, candidates, latency_ms FROM audit WHERE ts >= ? ORDER BY ts ASC",
|
|
151
226
|
)
|
|
152
227
|
.all(sinceIso) as AuditTableRow[];
|
|
153
228
|
|
|
@@ -178,6 +253,7 @@ export function queryAuditRows(db: Database, sinceIso: string): AuditRow[] {
|
|
|
178
253
|
const result: AuditRow = {
|
|
179
254
|
id: row.id,
|
|
180
255
|
ts: row.ts,
|
|
256
|
+
request_id: row.request_id,
|
|
181
257
|
query: row.query,
|
|
182
258
|
retrieval: row.retrieval,
|
|
183
259
|
candidates,
|
|
@@ -193,10 +269,19 @@ export function queryAuditRows(db: Database, sinceIso: string): AuditRow[] {
|
|
|
193
269
|
});
|
|
194
270
|
}
|
|
195
271
|
|
|
272
|
+
export function queryFetchRows(db: Database, sinceIso: string): FetchAuditRow[] {
|
|
273
|
+
return db
|
|
274
|
+
.query(
|
|
275
|
+
"SELECT id, ts, skill_id, request_id, resolve_audit_id, rank_at_resolve FROM fetch WHERE ts >= ? ORDER BY ts ASC",
|
|
276
|
+
)
|
|
277
|
+
.all(sinceIso) as FetchAuditRow[];
|
|
278
|
+
}
|
|
279
|
+
|
|
196
280
|
export function getStats(db: Database, since: string, now: Date = new Date()): StatsResponse {
|
|
197
281
|
const sinceDate = parseSince(since, now);
|
|
198
282
|
const rows = queryAuditRows(db, sinceDate.toISOString());
|
|
199
|
-
|
|
283
|
+
const fetchRows = queryFetchRows(db, sinceDate.toISOString());
|
|
284
|
+
return computeStats(rows, sinceDate, now, fetchRows);
|
|
200
285
|
}
|
|
201
286
|
|
|
202
287
|
export function renderStatsText(stats: StatsResponse): string {
|
|
@@ -230,5 +315,26 @@ export function renderStatsText(stats: StatsResponse): string {
|
|
|
230
315
|
}
|
|
231
316
|
}
|
|
232
317
|
|
|
318
|
+
if (stats.acceptance.available) {
|
|
319
|
+
lines.push(
|
|
320
|
+
`acceptance: acceptance_rate=${stats.acceptance.acceptance_rate.toFixed(3)} ` +
|
|
321
|
+
`observed_mrr=${stats.acceptance.observed_mrr.toFixed(3)} ` +
|
|
322
|
+
`top1_acceptance_rate=${stats.acceptance.top1_acceptance_rate.toFixed(3)} ` +
|
|
323
|
+
`(accepted=${stats.acceptance.accepted_count}/${stats.acceptance.resolves_with_candidates}, ` +
|
|
324
|
+
`uncorrelated_fetch_count=${stats.acceptance.uncorrelated_fetch_count})`,
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
lines.push(`acceptance: unavailable (uncorrelated_fetch_count=${stats.acceptance.uncorrelated_fetch_count})`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
lines.push("top unused shortlist queries:");
|
|
331
|
+
if (stats.top_unused_shortlist_queries.length === 0) {
|
|
332
|
+
lines.push(" (none)");
|
|
333
|
+
} else {
|
|
334
|
+
for (const entry of stats.top_unused_shortlist_queries) {
|
|
335
|
+
lines.push(` "${entry.query}" (${entry.count})`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
233
339
|
return lines.join("\n");
|
|
234
340
|
}
|
package/src/types.ts
CHANGED
|
@@ -107,6 +107,11 @@ export interface ConfigPolicy {
|
|
|
107
107
|
environment_overrides?: boolean;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
export interface AuditConfig {
|
|
111
|
+
/** Age in days beyond which audit rows are pruned. 0 disables pruning. */
|
|
112
|
+
retention_days: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
export interface Config {
|
|
111
116
|
config?: ConfigPolicy;
|
|
112
117
|
vault_path: string;
|
|
@@ -116,6 +121,7 @@ export interface Config {
|
|
|
116
121
|
output: OutputConfig;
|
|
117
122
|
inference: InferenceConfig;
|
|
118
123
|
server?: ServerConfig;
|
|
124
|
+
audit?: AuditConfig;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
export interface RankedCandidate {
|
|
@@ -136,6 +142,7 @@ export type DegradationReason =
|
|
|
136
142
|
| "reranker_protocol_error";
|
|
137
143
|
|
|
138
144
|
export interface ResolveResult {
|
|
145
|
+
request_id: string;
|
|
139
146
|
retrieval: RetrievalCapability;
|
|
140
147
|
degraded_from?: "reranked" | "hybrid";
|
|
141
148
|
degradation_reason?: DegradationReason;
|
|
@@ -151,6 +158,7 @@ export interface ResolveSkillInput {
|
|
|
151
158
|
|
|
152
159
|
export interface FetchSkillInput {
|
|
153
160
|
skill_id: string;
|
|
161
|
+
request_id?: string;
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
export interface FetchSkillResult {
|
|
@@ -169,6 +177,8 @@ export interface AuditCandidate {
|
|
|
169
177
|
export interface AuditRow {
|
|
170
178
|
id: number;
|
|
171
179
|
ts: string;
|
|
180
|
+
/** Null for rows written before request_id existed (AC4). */
|
|
181
|
+
request_id: string | null;
|
|
172
182
|
query: string;
|
|
173
183
|
retrieval: RetrievalCapability;
|
|
174
184
|
degraded_from?: "reranked" | "hybrid" | null;
|
|
@@ -177,6 +187,18 @@ export interface AuditRow {
|
|
|
177
187
|
latency_ms: number;
|
|
178
188
|
}
|
|
179
189
|
|
|
190
|
+
export interface FetchAuditRow {
|
|
191
|
+
id: number;
|
|
192
|
+
ts: string;
|
|
193
|
+
skill_id: string;
|
|
194
|
+
/** Exactly as supplied by the caller, including an unknown value. Null when the caller sent none. */
|
|
195
|
+
request_id: string | null;
|
|
196
|
+
/** Null when the fetch is uncorrelated: no request_id, an unknown/malformed one, or a pruned resolve row. */
|
|
197
|
+
resolve_audit_id: number | null;
|
|
198
|
+
/** Rank of skill_id in the correlated resolve's shortlist. Null when uncorrelated or absent from the shortlist. */
|
|
199
|
+
rank_at_resolve: number | null;
|
|
200
|
+
}
|
|
201
|
+
|
|
180
202
|
export interface Clients {
|
|
181
203
|
embed(texts: string[]): Promise<Float32Array[]>;
|
|
182
204
|
rerank?: (query: string, docs: { skill_id: string; text: string }[]) => Promise<number[]>;
|
package/docs/assets/logo.png
DELETED
|
Binary file
|