tinydecide 0.0.0-stage → 0.1.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 +251 -2
- package/bin/tinydecide.js +253 -0
- package/examples/support-api.js +118 -0
- package/package.json +56 -4
- package/src/compile.js +264 -0
- package/src/features.js +128 -0
- package/src/hash.js +87 -0
- package/src/index.js +5 -0
- package/src/log.js +56 -0
- package/src/model.js +154 -0
- package/src/node.js +155 -0
- package/src/shadow.js +412 -0
- package/src/stats.js +67 -0
- package/src/store.js +21 -0
- package/src/train.js +126 -0
- package/src/worker.js +12 -0
- package/types/index.d.ts +285 -0
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Type definitions for tinydecide.
|
|
2
|
+
|
|
3
|
+
/** learning: every call goes to your API · local: confident calls answered locally · guarded: back to your API after a failed check. */
|
|
4
|
+
export type Mode = 'learning' | 'local' | 'guarded';
|
|
5
|
+
|
|
6
|
+
/** Call it exactly like your API function. */
|
|
7
|
+
export interface Decide<I, R> {
|
|
8
|
+
(input: I): Promise<R>;
|
|
9
|
+
/** The answer plus who gave it. */
|
|
10
|
+
detailed(input: I): Promise<Detailed<R>>;
|
|
11
|
+
/** Runs the local model synchronously; null before the first model. Ignores the cutoff check: see `local` in the result. */
|
|
12
|
+
local(input: I): Decision | null;
|
|
13
|
+
/** Compiles now instead of on the schedule. Resolves to the report, or null if the compile was discarded. */
|
|
14
|
+
compile(): Promise<Report | null>;
|
|
15
|
+
/** Resolves once a saved model and state are loaded. Calls made before then go to your API. */
|
|
16
|
+
readonly ready: Promise<void>;
|
|
17
|
+
/** Stops compiling and closes the store. */
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
stats(): Stats;
|
|
20
|
+
readonly mode: Mode;
|
|
21
|
+
readonly model: Model | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Detailed<R> {
|
|
25
|
+
value: R;
|
|
26
|
+
/** local: the model answered · api: your API answered · fallback: your API threw and `fallback` was on. */
|
|
27
|
+
source: 'local' | 'api' | 'fallback';
|
|
28
|
+
/** The label, or null when none could be read from your API's response. */
|
|
29
|
+
choice: string | null;
|
|
30
|
+
/** The local model's confidence; null when your API answered. */
|
|
31
|
+
confidence: number | null;
|
|
32
|
+
ms: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ShadowOptions<I = any, R = any> {
|
|
36
|
+
/** Agreement with your API to certify, overall across all calls. Default 0.98. */
|
|
37
|
+
target?: number;
|
|
38
|
+
/** One minus the confidence of the certificate. Default 0.05 (95% confidence). */
|
|
39
|
+
delta?: number;
|
|
40
|
+
/** Share of calls your API also answers in the background, to check agreement live. Default 0.02. */
|
|
41
|
+
audit?: number;
|
|
42
|
+
/** Fixes the label set and its order. Needed when your API returns label indexes or probability arrays. */
|
|
43
|
+
labels?: string[];
|
|
44
|
+
/** Starts from a compiled model, such as one built with `tinydecide compile`. */
|
|
45
|
+
model?: Model;
|
|
46
|
+
/** Where answers, the model and its state live. Default: memory; in Node, `.tinydecide/<name>/` for a named decision. */
|
|
47
|
+
store?: Store;
|
|
48
|
+
/** Node only: the folder that holds named decisions. Default `.tinydecide`, or `$TINYDECIDE_DIR`. */
|
|
49
|
+
dir?: string;
|
|
50
|
+
/** Keeps related calls (one conversation, one customer) in the same split, so the certificate covers new ones. */
|
|
51
|
+
group?: (input: I) => string | number | null | undefined;
|
|
52
|
+
/** Reads the label, and optionally probabilities, from your API's response. Default: a string, or `choice` / `label` / `decision` / `answer` with `probs`. */
|
|
53
|
+
choice?: (response: R, labels: string[] | undefined) => { choice: string | null; probs?: Record<string, number> | null } | null | undefined;
|
|
54
|
+
/** Builds a local answer. Default: the label under the same key your API uses, plus `probs` and `confidence` if your API returns them. */
|
|
55
|
+
respond?: (decision: Decision, shape: ResponseShape | null) => R;
|
|
56
|
+
/** Answers locally when your API throws and a model exists. Default false. */
|
|
57
|
+
fallback?: boolean;
|
|
58
|
+
/** One line per lifecycle event (never per call). true uses console.info; false is silent. Default true. */
|
|
59
|
+
log?: boolean | ((line: string, event: ShadowEvent) => void);
|
|
60
|
+
onEvent?: (event: ShadowEvent) => void;
|
|
61
|
+
/** When to compile. `false` compiles only when you call `decide.compile()`. */
|
|
62
|
+
autoCompile?: false | {
|
|
63
|
+
/** Answers before the first compile. Default 2000. */
|
|
64
|
+
min?: number;
|
|
65
|
+
/** Recompile when answers grow by this factor. Default 2. */
|
|
66
|
+
growth?: number;
|
|
67
|
+
/** …but at least every this many answers. Default 20000. */
|
|
68
|
+
maxEvery?: number;
|
|
69
|
+
/** Most recent answers a compile uses. Default 50000. */
|
|
70
|
+
window?: number;
|
|
71
|
+
};
|
|
72
|
+
/** Extra options for each compile. */
|
|
73
|
+
compile?: Partial<CompileOptions>;
|
|
74
|
+
/** Replaces the compile function. Node uses a worker thread by default. */
|
|
75
|
+
compiler?: (rows: AnswerRecord[], options: CompileOptions & { background?: boolean }) => Promise<{ model: Model; report: Report }>;
|
|
76
|
+
/** Source of randomness for the audit sample. Default Math.random. */
|
|
77
|
+
random?: () => number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wraps your decision API. Every call still goes to your API until a local model is certified; then
|
|
82
|
+
* calls the model is sure about are answered in microseconds and the rest still go to your API.
|
|
83
|
+
*
|
|
84
|
+
* @param name names the decision in logs; in Node it is also the folder in `.tinydecide/` that keeps its answers and model.
|
|
85
|
+
* @param api your existing call. Return a label string, or an object with `label` (or `choice`) and optional `probs`.
|
|
86
|
+
*/
|
|
87
|
+
export function shadow<I = string, R = string>(name: string, api: (input: I) => R | Promise<R>, options?: ShadowOptions<I, Awaited<R>>): Decide<I, Awaited<R>>;
|
|
88
|
+
export function shadow<I = string, R = string>(api: (input: I) => R | Promise<R>, options?: ShadowOptions<I, Awaited<R>>): Decide<I, Awaited<R>>;
|
|
89
|
+
|
|
90
|
+
export interface ResponseShape {
|
|
91
|
+
kind: 'string' | 'object';
|
|
92
|
+
key?: string;
|
|
93
|
+
numeric?: boolean;
|
|
94
|
+
probsKey?: string | null;
|
|
95
|
+
probsArray?: boolean;
|
|
96
|
+
confidence?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface Stats {
|
|
100
|
+
name: string | null;
|
|
101
|
+
mode: Mode;
|
|
102
|
+
calls: number;
|
|
103
|
+
local: number;
|
|
104
|
+
api: number;
|
|
105
|
+
fallback: number;
|
|
106
|
+
audits: number;
|
|
107
|
+
auditDisagree: number;
|
|
108
|
+
compiles: number;
|
|
109
|
+
errors: number;
|
|
110
|
+
/** Answers in the store. */
|
|
111
|
+
logged: number;
|
|
112
|
+
/** Share of calls answered locally since start. */
|
|
113
|
+
share: number;
|
|
114
|
+
apiCallsSaved: number;
|
|
115
|
+
/** Agreement on the audit sample since the last install. */
|
|
116
|
+
live: { sampled: number; agreement: number | null; ci: [number, number] | null };
|
|
117
|
+
/** Agreement of the model's guess on calls your API answered, against what calibration measured. */
|
|
118
|
+
deferred: { checked: number; agreement: number | null; baseline: number | null };
|
|
119
|
+
model: { labels: number; cutoff: number; expectedShare: number | null; created: string | null } | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ShadowEvent =
|
|
123
|
+
| { type: 'start'; t: number; mode: Mode; min?: number; logged?: number; share?: number; target?: number; since?: number }
|
|
124
|
+
| { type: 'compile:start'; t: number; rows: number }
|
|
125
|
+
| { type: 'compile'; t: number; report: Report; recovering: boolean }
|
|
126
|
+
| { type: 'install'; t: number; share: number; verdict: Verdict; model: Model; report: Report }
|
|
127
|
+
| { type: 'discard'; t: number; reason: string }
|
|
128
|
+
| { type: 'guard'; t: number; reason: 'drift' | 'schema'; slice?: 'audit' | 'deferred'; agreement?: number; baseline?: number; sampled?: number; upper?: number; target?: number; label?: string }
|
|
129
|
+
| { type: 'audit'; t: number; agree: boolean; local: string; api: string }
|
|
130
|
+
| { type: 'unreadable'; t: number; preview: string }
|
|
131
|
+
| { type: 'error'; t: number; where: string; error: string };
|
|
132
|
+
|
|
133
|
+
/** One logged answer from your API. */
|
|
134
|
+
export interface AnswerRecord {
|
|
135
|
+
t: number;
|
|
136
|
+
input: unknown;
|
|
137
|
+
choice: string;
|
|
138
|
+
probs?: Record<string, number>;
|
|
139
|
+
group?: string;
|
|
140
|
+
/** false when the call was logged only because the model deferred it: used for training, never for certifying. */
|
|
141
|
+
sample?: false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Where shadow() keeps answers, the model and its state. Only append, load and size are required. */
|
|
145
|
+
export interface Store {
|
|
146
|
+
append(record: AnswerRecord): void;
|
|
147
|
+
/** The most recent answers, oldest first. */
|
|
148
|
+
load(): AnswerRecord[] | Promise<AnswerRecord[]>;
|
|
149
|
+
readonly size?: number;
|
|
150
|
+
loadModel?(): Model | null | Promise<Model | null>;
|
|
151
|
+
saveModel?(model: Model, report: Report): void | Promise<void>;
|
|
152
|
+
loadState?(): StoreState | null | Promise<StoreState | null>;
|
|
153
|
+
saveState?(state: StoreState | null): void | Promise<void>;
|
|
154
|
+
close?(): void | Promise<void>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface StoreState { guardedSince: number }
|
|
158
|
+
|
|
159
|
+
/** Keeps answers and the model in memory. The default outside Node. */
|
|
160
|
+
export function memoryStore(options?: { limit?: number; records?: AnswerRecord[] }): Store & { readonly records: AnswerRecord[] };
|
|
161
|
+
|
|
162
|
+
export type Verdict = 'ship' | 'marginal' | 'not-worth-it' | 'need-more-data';
|
|
163
|
+
|
|
164
|
+
export interface CompileOptions {
|
|
165
|
+
/** Default 0.98. */
|
|
166
|
+
target: number;
|
|
167
|
+
/** Default 0.05. */
|
|
168
|
+
delta: number;
|
|
169
|
+
/** overall: agreement across all calls, where your API's own answers count as agreeing (default). local: agreement on local answers alone. */
|
|
170
|
+
guarantee: 'overall' | 'local';
|
|
171
|
+
labels?: string[];
|
|
172
|
+
/** Share of rows held out to certify the cutoff. Default 0.25. */
|
|
173
|
+
calib: number;
|
|
174
|
+
/** Share of rows held out to report on. Default 0.1. */
|
|
175
|
+
test: number;
|
|
176
|
+
seed: number;
|
|
177
|
+
/** The audit rate the model will run with, used to estimate savings. Default 0.02. */
|
|
178
|
+
audit: number;
|
|
179
|
+
onProgress?: (progress: { phase: string; done: number }) => void;
|
|
180
|
+
signal?: AbortSignal;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One line of traffic: `input` (or `text`), the label as `choice` / `label` / `decision` / `answer`
|
|
185
|
+
* (or an index into `labels`), and optionally `probs` and `group`.
|
|
186
|
+
*/
|
|
187
|
+
export type TrafficRecord = Record<string, unknown>;
|
|
188
|
+
|
|
189
|
+
export function compile(records: Iterable<TrafficRecord>, options?: Partial<CompileOptions>): Promise<{ model: Model; report: Report }>;
|
|
190
|
+
|
|
191
|
+
export interface Report {
|
|
192
|
+
verdict: Verdict;
|
|
193
|
+
/** One plain-English sentence. */
|
|
194
|
+
summary: string;
|
|
195
|
+
labels: string[];
|
|
196
|
+
target: number;
|
|
197
|
+
delta: number;
|
|
198
|
+
guarantee: 'overall' | 'local';
|
|
199
|
+
rows: { total: number; train: number; calib: number; test: number; skipped: number };
|
|
200
|
+
features: number;
|
|
201
|
+
epochs: number;
|
|
202
|
+
certificate: Certificate;
|
|
203
|
+
calib: { share: number; localAgreement: number | null; overallAgreement: number | null; rawAgreement: number | null };
|
|
204
|
+
/** What the cutoff did on held-out rows, with 95% Wilson intervals. */
|
|
205
|
+
test: {
|
|
206
|
+
n: number;
|
|
207
|
+
share: number;
|
|
208
|
+
overallAgreement: number;
|
|
209
|
+
overallCI: [number, number];
|
|
210
|
+
localAgreement: number | null;
|
|
211
|
+
localCI: [number, number] | null;
|
|
212
|
+
rawAgreement: number;
|
|
213
|
+
int8Flips: number;
|
|
214
|
+
} | null;
|
|
215
|
+
/** Share answered locally at other targets. */
|
|
216
|
+
whatIf: { target: number; share: number }[];
|
|
217
|
+
savings: { apiCallsAvoided: number; auditRate: number };
|
|
218
|
+
temperature: number;
|
|
219
|
+
bytes: number;
|
|
220
|
+
latencyUs: number;
|
|
221
|
+
ms: { train: number; total: number };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface Certificate {
|
|
225
|
+
target: number;
|
|
226
|
+
delta: number;
|
|
227
|
+
guarantee: 'overall' | 'local';
|
|
228
|
+
/** Calibration rows. */
|
|
229
|
+
n: number;
|
|
230
|
+
local: number;
|
|
231
|
+
errors: number;
|
|
232
|
+
pValue: number;
|
|
233
|
+
/** Answer locally when the top-two logit margin is at least this. Infinity when nothing certified. */
|
|
234
|
+
cutoff: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface Decision {
|
|
238
|
+
choice: string;
|
|
239
|
+
index: number;
|
|
240
|
+
confidence: number;
|
|
241
|
+
/** Gap between the top two logits. */
|
|
242
|
+
margin: number;
|
|
243
|
+
/** Whether the margin clears the certified cutoff. */
|
|
244
|
+
local: boolean;
|
|
245
|
+
probs?: Record<string, number>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface ModelMeta {
|
|
249
|
+
created?: string;
|
|
250
|
+
certificate?: Certificate;
|
|
251
|
+
rows?: { total: number; train: number; calib: number; test: number };
|
|
252
|
+
expected?: { share: number; localAgreement: number | null; deferred: { n: number; agreement: number | null } };
|
|
253
|
+
verdict?: Verdict;
|
|
254
|
+
response?: ResponseShape | null;
|
|
255
|
+
[key: string]: unknown;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export class Model {
|
|
259
|
+
readonly labels: string[];
|
|
260
|
+
readonly cutoff: number;
|
|
261
|
+
/** Temperature for the reported confidence. */
|
|
262
|
+
readonly T: number;
|
|
263
|
+
readonly meta: ModelMeta;
|
|
264
|
+
/** Size of the weights in bytes. */
|
|
265
|
+
readonly bytes: number;
|
|
266
|
+
decide(input: unknown, options?: { probs?: boolean }): Decision;
|
|
267
|
+
/** Raw logits. The array is reused by the next call. */
|
|
268
|
+
logits(input: unknown): Float32Array;
|
|
269
|
+
/** A copy with a different cutoff. */
|
|
270
|
+
withCutoff(cutoff: number): Model;
|
|
271
|
+
/** The .tdm file format. */
|
|
272
|
+
toBytes(): Uint8Array;
|
|
273
|
+
static fromBytes(bytes: Uint8Array | ArrayBuffer): Model;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Node only ───────────────────────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
/** Node only. Keeps one decision in a folder: answers.jsonl, model.tdm, report.json and, while guarded, state.json. */
|
|
279
|
+
export function fileStore(dir: string, options?: { limit?: number }): Store & { readonly dir: string };
|
|
280
|
+
/** Node only. */
|
|
281
|
+
export function loadModel(path: string): Model;
|
|
282
|
+
/** Node only. Writes atomically. */
|
|
283
|
+
export function saveModel(path: string, model: Model): void;
|
|
284
|
+
/** Node only. Reads .jsonl or .jsonl.gz, skipping lines that are not valid JSON. */
|
|
285
|
+
export function readJsonl(path: string, options?: { onSkip?: (line: number) => void }): Record<string, unknown>[];
|