oracular 0.1.1 → 0.1.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/CHANGELOG.md +6 -0
- package/README.md +4 -1
- package/dist/index.cjs +262 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -5
- package/dist/index.d.ts +103 -5
- package/dist/index.js +260 -162
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#6](https://github.com/Yuribenjamin/oracular/pull/6) [`57d121f`](https://github.com/Yuribenjamin/oracular/commit/57d121f107f4ed92f2e59bf354cc6e19f705de1b) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - Docs corrections from the first production integration; new evaluate(), evidenceToReveal(), episodeClosed() and explain() filtering; default export condition.
|
|
8
|
+
|
|
3
9
|
## 0.1.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -223,7 +223,10 @@ endEpisode(state, at: number): EngineState
|
|
|
223
223
|
predict(state, intent: string, at: number, opts?: RevealOptions): Prediction
|
|
224
224
|
shouldReveal(state, intent: string, at: number, opts?: RevealOptions): boolean
|
|
225
225
|
markRevealed(state, intent: string, at: number): EngineState
|
|
226
|
-
explain(state, intent: string, at: number): SignalExplanation[]
|
|
226
|
+
explain(state, intent: string, at: number, opts?: { nonZero?: boolean; limit?: number }): SignalExplanation[]
|
|
227
|
+
evaluate(state, intent: string, at: number, opts?): { prediction, reveal, explanation } // all three, one decay pass
|
|
228
|
+
evidenceToReveal(intentConfig, opts?): number // pseudo-counts at which shouldReveal first becomes true
|
|
229
|
+
episodeClosed(prev, next): boolean // did an episode close between two states?
|
|
227
230
|
serialize(state): string
|
|
228
231
|
deserialize(json: string, opts?: { config?: EngineConfig; migrate?: Migrate }): EngineState
|
|
229
232
|
STATE_VERSION // 1
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/math.ts
|
|
4
|
+
function decayFactor(elapsed, halfLife) {
|
|
5
|
+
if (!(elapsed > 0) || !Number.isFinite(elapsed)) return 1;
|
|
6
|
+
return 2 ** (-elapsed / halfLife);
|
|
7
|
+
}
|
|
8
|
+
function betaMean(alpha, beta) {
|
|
9
|
+
return alpha / (alpha + beta);
|
|
10
|
+
}
|
|
11
|
+
function betaVariance(alpha, beta) {
|
|
12
|
+
const n = alpha + beta;
|
|
13
|
+
return alpha * beta / (n * n * (n + 1));
|
|
14
|
+
}
|
|
15
|
+
function betaSd(alpha, beta) {
|
|
16
|
+
return Math.sqrt(betaVariance(alpha, beta));
|
|
17
|
+
}
|
|
18
|
+
function clamp01(x) {
|
|
19
|
+
if (x < 0) return 0;
|
|
20
|
+
if (x > 1) return 1;
|
|
21
|
+
return x;
|
|
22
|
+
}
|
|
23
|
+
function laplace(successes, total) {
|
|
24
|
+
return (successes + 1) / (total + 2);
|
|
25
|
+
}
|
|
26
|
+
function lift(q, b) {
|
|
27
|
+
if (q >= b) return b >= 1 ? 0 : (q - b) / (1 - b);
|
|
28
|
+
return b <= 0 ? 0 : (q - b) / b;
|
|
29
|
+
}
|
|
30
|
+
|
|
3
31
|
// src/util.ts
|
|
4
32
|
function isRecord(value) {
|
|
5
33
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -18,6 +46,96 @@ function mapRecord(record2, fn) {
|
|
|
18
46
|
return out;
|
|
19
47
|
}
|
|
20
48
|
|
|
49
|
+
// src/intent.ts
|
|
50
|
+
function priorCounts(intent) {
|
|
51
|
+
return {
|
|
52
|
+
alpha0: intent.prior * intent.priorStrength,
|
|
53
|
+
beta0: (1 - intent.prior) * intent.priorStrength
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function initIntent(intent) {
|
|
57
|
+
const { alpha0, beta0 } = priorCounts(intent);
|
|
58
|
+
return {
|
|
59
|
+
alpha: alpha0,
|
|
60
|
+
beta: beta0,
|
|
61
|
+
updatedAt: null,
|
|
62
|
+
contributions: {},
|
|
63
|
+
learned: {},
|
|
64
|
+
episodes: { hits: 0, misses: 0 },
|
|
65
|
+
learnedAt: null,
|
|
66
|
+
lastRevealedAt: null,
|
|
67
|
+
lastOutcomeAt: null,
|
|
68
|
+
revealCount: 0
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function withContributions(state, intent, contributions) {
|
|
72
|
+
const { alpha0, beta0 } = priorCounts(intent);
|
|
73
|
+
let alpha = alpha0;
|
|
74
|
+
let beta = beta0;
|
|
75
|
+
for (const c of Object.values(contributions)) {
|
|
76
|
+
alpha += c.alpha;
|
|
77
|
+
beta += c.beta;
|
|
78
|
+
}
|
|
79
|
+
return { ...state, alpha, beta, contributions };
|
|
80
|
+
}
|
|
81
|
+
function decayIntent(state, intent, at) {
|
|
82
|
+
if (state.updatedAt === null) return { ...state, updatedAt: at };
|
|
83
|
+
const d = decayFactor(at - state.updatedAt, intent.halfLife);
|
|
84
|
+
const updatedAt = Math.max(state.updatedAt, at);
|
|
85
|
+
if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
|
|
86
|
+
const contributions = mapRecord(state.contributions, (c) => ({
|
|
87
|
+
alpha: c.alpha * d,
|
|
88
|
+
beta: c.beta * d
|
|
89
|
+
}));
|
|
90
|
+
return withContributions({ ...state, updatedAt }, intent, contributions);
|
|
91
|
+
}
|
|
92
|
+
function applySignal(state, intent, signal, weight) {
|
|
93
|
+
const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
|
|
94
|
+
const next = weight >= 0 ? { alpha: prev.alpha + weight, beta: prev.beta } : { alpha: prev.alpha, beta: prev.beta - weight };
|
|
95
|
+
return withContributions(state, intent, { ...state.contributions, [signal]: next });
|
|
96
|
+
}
|
|
97
|
+
function resetToPrior(state, intent) {
|
|
98
|
+
return withContributions(state, intent, {});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/learning.ts
|
|
102
|
+
var NONE = { hits: 0, misses: 0 };
|
|
103
|
+
function decayLearned(state, learning, at) {
|
|
104
|
+
if (state.learnedAt === null) return { ...state, learnedAt: at };
|
|
105
|
+
const learnedAt = Math.max(state.learnedAt, at);
|
|
106
|
+
const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
|
|
107
|
+
if (d === 1) return learnedAt === state.learnedAt ? state : { ...state, learnedAt };
|
|
108
|
+
return {
|
|
109
|
+
...state,
|
|
110
|
+
learnedAt,
|
|
111
|
+
learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
|
|
112
|
+
episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function signalWeights(state, intent, learning, signal) {
|
|
116
|
+
const initial = getOwn(intent.signals, signal);
|
|
117
|
+
if (initial === void 0) {
|
|
118
|
+
throw new RangeError(`oracular: ${JSON.stringify(signal)} is not a signal of this intent`);
|
|
119
|
+
}
|
|
120
|
+
const record2 = getOwn(state.learned, signal) ?? NONE;
|
|
121
|
+
const n = record2.hits + record2.misses;
|
|
122
|
+
const q = laplace(record2.hits, n);
|
|
123
|
+
const b = laplace(state.episodes.hits, state.episodes.hits + state.episodes.misses);
|
|
124
|
+
const learned = n === 0 ? 0 : learning.gain * Math.abs(initial) * lift(q, b);
|
|
125
|
+
const k = learning.priorEpisodes;
|
|
126
|
+
const effective = (k * initial + n * learned) / (k + n);
|
|
127
|
+
return { initial, learned, effective, hits: record2.hits, misses: record2.misses };
|
|
128
|
+
}
|
|
129
|
+
function recordEpisode(state, signals, outcome) {
|
|
130
|
+
const learned = { ...state.learned };
|
|
131
|
+
for (const signal of signals) {
|
|
132
|
+
const r = getOwn(learned, signal) ?? NONE;
|
|
133
|
+
learned[signal] = outcome ? { hits: r.hits + 1, misses: r.misses } : { hits: r.hits, misses: r.misses + 1 };
|
|
134
|
+
}
|
|
135
|
+
const episodes = outcome ? { hits: state.episodes.hits + 1, misses: state.episodes.misses } : { hits: state.episodes.hits, misses: state.episodes.misses + 1 };
|
|
136
|
+
return { ...state, learned, episodes };
|
|
137
|
+
}
|
|
138
|
+
|
|
21
139
|
// src/config.ts
|
|
22
140
|
var DEFAULT_THRESHOLD = 0.5;
|
|
23
141
|
var DEFAULT_Z = 1.28;
|
|
@@ -35,6 +153,7 @@ var INTENT_KEYS = [
|
|
|
35
153
|
];
|
|
36
154
|
var REVEAL_KEYS = ["threshold", "z", "cooldown"];
|
|
37
155
|
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
|
|
156
|
+
var EXPLAIN_KEYS = ["nonZero", "limit"];
|
|
38
157
|
function typeError(path, message) {
|
|
39
158
|
throw new TypeError(`oracular: ${path} ${message}`);
|
|
40
159
|
}
|
|
@@ -115,7 +234,7 @@ function normalizeLearning(value, path) {
|
|
|
115
234
|
}
|
|
116
235
|
return out;
|
|
117
236
|
}
|
|
118
|
-
function
|
|
237
|
+
function normalizeIntentConfig(value, path) {
|
|
119
238
|
const record2 = expectRecord(value, path);
|
|
120
239
|
expectKeys(record2, INTENT_KEYS, path);
|
|
121
240
|
const signalsRecord = expectRecord(record2.signals, `${path}.signals`);
|
|
@@ -157,7 +276,10 @@ function normalizeConfig(value) {
|
|
|
157
276
|
const intents = {};
|
|
158
277
|
for (const name of names) {
|
|
159
278
|
expectKeyName(name, "config.intents");
|
|
160
|
-
intents[name] =
|
|
279
|
+
intents[name] = normalizeIntentConfig(
|
|
280
|
+
intentsRecord[name],
|
|
281
|
+
`config.intents[${JSON.stringify(name)}]`
|
|
282
|
+
);
|
|
161
283
|
}
|
|
162
284
|
const out = { intents };
|
|
163
285
|
if (record2.episodeTimeout !== void 0) {
|
|
@@ -169,12 +291,43 @@ function normalizeConfig(value) {
|
|
|
169
291
|
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal");
|
|
170
292
|
return out;
|
|
171
293
|
}
|
|
172
|
-
function
|
|
173
|
-
const
|
|
294
|
+
function normalizeExplainOptions(value, path) {
|
|
295
|
+
const record2 = expectRecord(value, path);
|
|
296
|
+
expectKeys(record2, EXPLAIN_KEYS, path);
|
|
297
|
+
const out = {};
|
|
298
|
+
if (record2.nonZero !== void 0) {
|
|
299
|
+
if (typeof record2.nonZero !== "boolean") typeError(`${path}.nonZero`, "must be a boolean");
|
|
300
|
+
out.nonZero = record2.nonZero;
|
|
301
|
+
}
|
|
302
|
+
if (record2.limit !== void 0) {
|
|
303
|
+
const n = expectNumber(record2.limit, `${path}.limit`);
|
|
304
|
+
if (!Number.isInteger(n) || n < 0)
|
|
305
|
+
rangeError(`${path}.limit`, "must be a non-negative integer");
|
|
306
|
+
out.limit = n;
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
function splitEvaluateOptions(value, path) {
|
|
311
|
+
if (value === void 0) return { reveal: {}, explain: {} };
|
|
312
|
+
const record2 = expectRecord(value, path);
|
|
313
|
+
expectKeys(record2, [...REVEAL_KEYS, ...EXPLAIN_KEYS], path);
|
|
314
|
+
const reveal = {};
|
|
315
|
+
const explain2 = {};
|
|
316
|
+
for (const [key, v] of Object.entries(record2)) {
|
|
317
|
+
if (REVEAL_KEYS.includes(key)) reveal[key] = v;
|
|
318
|
+
else explain2[key] = v;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
reveal: normalizeRevealOptions(reveal, path),
|
|
322
|
+
explain: normalizeExplainOptions(explain2, path)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function resolveReveal(config, intent, opts, normalized = false) {
|
|
326
|
+
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options");
|
|
174
327
|
return {
|
|
175
|
-
threshold: o.threshold ?? intent.reveal?.threshold ?? config
|
|
176
|
-
z: o.z ?? intent.reveal?.z ?? config
|
|
177
|
-
cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config
|
|
328
|
+
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
329
|
+
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
330
|
+
cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife
|
|
178
331
|
};
|
|
179
332
|
}
|
|
180
333
|
function resolveLearning(config) {
|
|
@@ -197,124 +350,6 @@ function validateEvent(event) {
|
|
|
197
350
|
return { name, at };
|
|
198
351
|
}
|
|
199
352
|
|
|
200
|
-
// src/math.ts
|
|
201
|
-
function decayFactor(elapsed, halfLife) {
|
|
202
|
-
if (!(elapsed > 0) || !Number.isFinite(elapsed)) return 1;
|
|
203
|
-
return 2 ** (-elapsed / halfLife);
|
|
204
|
-
}
|
|
205
|
-
function betaMean(alpha, beta) {
|
|
206
|
-
return alpha / (alpha + beta);
|
|
207
|
-
}
|
|
208
|
-
function betaVariance(alpha, beta) {
|
|
209
|
-
const n = alpha + beta;
|
|
210
|
-
return alpha * beta / (n * n * (n + 1));
|
|
211
|
-
}
|
|
212
|
-
function betaSd(alpha, beta) {
|
|
213
|
-
return Math.sqrt(betaVariance(alpha, beta));
|
|
214
|
-
}
|
|
215
|
-
function clamp01(x) {
|
|
216
|
-
if (x < 0) return 0;
|
|
217
|
-
if (x > 1) return 1;
|
|
218
|
-
return x;
|
|
219
|
-
}
|
|
220
|
-
function laplace(successes, total) {
|
|
221
|
-
return (successes + 1) / (total + 2);
|
|
222
|
-
}
|
|
223
|
-
function lift(q, b) {
|
|
224
|
-
if (q >= b) return b >= 1 ? 0 : (q - b) / (1 - b);
|
|
225
|
-
return b <= 0 ? 0 : (q - b) / b;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// src/intent.ts
|
|
229
|
-
function priorCounts(intent) {
|
|
230
|
-
return {
|
|
231
|
-
alpha0: intent.prior * intent.priorStrength,
|
|
232
|
-
beta0: (1 - intent.prior) * intent.priorStrength
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function initIntent(intent) {
|
|
236
|
-
const { alpha0, beta0 } = priorCounts(intent);
|
|
237
|
-
return {
|
|
238
|
-
alpha: alpha0,
|
|
239
|
-
beta: beta0,
|
|
240
|
-
updatedAt: null,
|
|
241
|
-
contributions: {},
|
|
242
|
-
learned: {},
|
|
243
|
-
episodes: { hits: 0, misses: 0 },
|
|
244
|
-
learnedAt: null,
|
|
245
|
-
lastRevealedAt: null,
|
|
246
|
-
lastOutcomeAt: null,
|
|
247
|
-
revealCount: 0
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function withContributions(state, intent, contributions) {
|
|
251
|
-
const { alpha0, beta0 } = priorCounts(intent);
|
|
252
|
-
let alpha = alpha0;
|
|
253
|
-
let beta = beta0;
|
|
254
|
-
for (const c of Object.values(contributions)) {
|
|
255
|
-
alpha += c.alpha;
|
|
256
|
-
beta += c.beta;
|
|
257
|
-
}
|
|
258
|
-
return { ...state, alpha, beta, contributions };
|
|
259
|
-
}
|
|
260
|
-
function decayIntent(state, intent, at) {
|
|
261
|
-
if (state.updatedAt === null) return { ...state, updatedAt: at };
|
|
262
|
-
const d = decayFactor(at - state.updatedAt, intent.halfLife);
|
|
263
|
-
const updatedAt = Math.max(state.updatedAt, at);
|
|
264
|
-
if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
|
|
265
|
-
const contributions = mapRecord(state.contributions, (c) => ({
|
|
266
|
-
alpha: c.alpha * d,
|
|
267
|
-
beta: c.beta * d
|
|
268
|
-
}));
|
|
269
|
-
return withContributions({ ...state, updatedAt }, intent, contributions);
|
|
270
|
-
}
|
|
271
|
-
function applySignal(state, intent, signal, weight) {
|
|
272
|
-
const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
|
|
273
|
-
const next = weight >= 0 ? { alpha: prev.alpha + weight, beta: prev.beta } : { alpha: prev.alpha, beta: prev.beta - weight };
|
|
274
|
-
return withContributions(state, intent, { ...state.contributions, [signal]: next });
|
|
275
|
-
}
|
|
276
|
-
function resetToPrior(state, intent) {
|
|
277
|
-
return withContributions(state, intent, {});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// src/learning.ts
|
|
281
|
-
var NONE = { hits: 0, misses: 0 };
|
|
282
|
-
function decayLearned(state, learning, at) {
|
|
283
|
-
if (state.learnedAt === null) return { ...state, learnedAt: at };
|
|
284
|
-
const learnedAt = Math.max(state.learnedAt, at);
|
|
285
|
-
const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
|
|
286
|
-
if (d === 1) return learnedAt === state.learnedAt ? state : { ...state, learnedAt };
|
|
287
|
-
return {
|
|
288
|
-
...state,
|
|
289
|
-
learnedAt,
|
|
290
|
-
learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
|
|
291
|
-
episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
function signalWeights(state, intent, learning, signal) {
|
|
295
|
-
const initial = getOwn(intent.signals, signal);
|
|
296
|
-
if (initial === void 0) {
|
|
297
|
-
throw new RangeError(`oracular: ${JSON.stringify(signal)} is not a signal of this intent`);
|
|
298
|
-
}
|
|
299
|
-
const record2 = getOwn(state.learned, signal) ?? NONE;
|
|
300
|
-
const n = record2.hits + record2.misses;
|
|
301
|
-
const q = laplace(record2.hits, n);
|
|
302
|
-
const b = laplace(state.episodes.hits, state.episodes.hits + state.episodes.misses);
|
|
303
|
-
const learned = n === 0 ? 0 : learning.gain * Math.abs(initial) * lift(q, b);
|
|
304
|
-
const k = learning.priorEpisodes;
|
|
305
|
-
const effective = (k * initial + n * learned) / (k + n);
|
|
306
|
-
return { initial, learned, effective, hits: record2.hits, misses: record2.misses };
|
|
307
|
-
}
|
|
308
|
-
function recordEpisode(state, signals, outcome) {
|
|
309
|
-
const learned = { ...state.learned };
|
|
310
|
-
for (const signal of signals) {
|
|
311
|
-
const r = getOwn(learned, signal) ?? NONE;
|
|
312
|
-
learned[signal] = outcome ? { hits: r.hits + 1, misses: r.misses } : { hits: r.hits, misses: r.misses + 1 };
|
|
313
|
-
}
|
|
314
|
-
const episodes = outcome ? { hits: state.episodes.hits + 1, misses: state.episodes.misses } : { hits: state.episodes.hits, misses: state.episodes.misses + 1 };
|
|
315
|
-
return { ...state, learned, episodes };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
353
|
// src/episode.ts
|
|
319
354
|
function emptyEpisode() {
|
|
320
355
|
return { startedAt: null, lastEventAt: null, intents: {} };
|
|
@@ -374,20 +409,42 @@ function getIntentState(state, name, intent) {
|
|
|
374
409
|
return getOwn(state.intents, name) ?? initIntent(intent);
|
|
375
410
|
}
|
|
376
411
|
|
|
377
|
-
// src/
|
|
378
|
-
function
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
412
|
+
// src/assess.ts
|
|
413
|
+
function assessIntent(state, name, intent, learning, at) {
|
|
414
|
+
return decayLearned(decayIntent(getIntentState(state, name, intent), intent, at), learning, at);
|
|
415
|
+
}
|
|
416
|
+
function cooldownEnd(since, cooldown, at) {
|
|
417
|
+
if (since === null) return null;
|
|
418
|
+
const end = since + cooldown;
|
|
419
|
+
return at < end ? end : null;
|
|
420
|
+
}
|
|
421
|
+
function buildPrediction(is, name, reveal, at) {
|
|
422
|
+
const p = betaMean(is.alpha, is.beta);
|
|
423
|
+
const sd = betaSd(is.alpha, is.beta);
|
|
424
|
+
const revealEnd = cooldownEnd(is.lastRevealedAt, reveal.cooldown, at);
|
|
425
|
+
const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.cooldown, at);
|
|
426
|
+
const suppressedUntil = revealEnd === null ? outcomeEnd : outcomeEnd === null ? revealEnd : Math.max(revealEnd, outcomeEnd);
|
|
427
|
+
return {
|
|
428
|
+
intent: name,
|
|
429
|
+
p,
|
|
430
|
+
sd,
|
|
431
|
+
lower: clamp01(p - reveal.z * sd),
|
|
432
|
+
confidence: clamp01(1 - 2 * sd),
|
|
433
|
+
evidence: is.alpha + is.beta,
|
|
434
|
+
revealed: revealEnd !== null,
|
|
435
|
+
revealedAt: is.lastRevealedAt,
|
|
436
|
+
suppressedUntil
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function decideReveal(prediction, reveal) {
|
|
440
|
+
return prediction.suppressedUntil === null && prediction.p >= reveal.threshold && prediction.lower >= reveal.threshold;
|
|
441
|
+
}
|
|
442
|
+
function buildExplanation(is, intent, learning, opts) {
|
|
386
443
|
const p = betaMean(is.alpha, is.beta);
|
|
387
444
|
const out = [];
|
|
388
|
-
for (const signal of Object.keys(
|
|
445
|
+
for (const signal of Object.keys(intent.signals)) {
|
|
389
446
|
const c = getOwn(is.contributions, signal) ?? { alpha: 0, beta: 0 };
|
|
390
|
-
const w = signalWeights(is,
|
|
447
|
+
const w = signalWeights(is, intent, learning, signal);
|
|
391
448
|
const without = betaMean(is.alpha - c.alpha, is.beta - c.beta);
|
|
392
449
|
out.push({
|
|
393
450
|
signal,
|
|
@@ -401,7 +458,8 @@ function explain(state, intent, at) {
|
|
|
401
458
|
});
|
|
402
459
|
}
|
|
403
460
|
out.sort((a, b) => Math.abs(b.impact) - Math.abs(a.impact) || compare(a.signal, b.signal));
|
|
404
|
-
|
|
461
|
+
const filtered = opts.nonZero ? out.filter((row) => row.contribution !== 0) : out;
|
|
462
|
+
return opts.limit === void 0 ? filtered : filtered.slice(0, opts.limit);
|
|
405
463
|
}
|
|
406
464
|
function compare(a, b) {
|
|
407
465
|
if (a < b) return -1;
|
|
@@ -409,6 +467,62 @@ function compare(a, b) {
|
|
|
409
467
|
return 0;
|
|
410
468
|
}
|
|
411
469
|
|
|
470
|
+
// src/evaluate.ts
|
|
471
|
+
function evaluate(state, intent, at, opts) {
|
|
472
|
+
const s = requireState(state);
|
|
473
|
+
const name = expectName(intent, "intent");
|
|
474
|
+
const t = expectTimestamp(at, "at");
|
|
475
|
+
const config = requireIntentConfig(s, name);
|
|
476
|
+
const split = splitEvaluateOptions(opts, "options");
|
|
477
|
+
const reveal = resolveReveal(s.config, config, split.reveal, true);
|
|
478
|
+
const learning = resolveLearning(s.config);
|
|
479
|
+
const is = assessIntent(s, name, config, learning, t);
|
|
480
|
+
const prediction = buildPrediction(is, name, reveal, t);
|
|
481
|
+
return {
|
|
482
|
+
prediction,
|
|
483
|
+
reveal: decideReveal(prediction, reveal),
|
|
484
|
+
explanation: buildExplanation(is, config, learning, split.explain)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/evidence.ts
|
|
489
|
+
function evidenceToReveal(intent, opts) {
|
|
490
|
+
const config = normalizeIntentConfig(intent, "intent");
|
|
491
|
+
const { threshold, z } = resolveReveal(void 0, config, opts);
|
|
492
|
+
const { alpha0, beta0 } = priorCounts(config);
|
|
493
|
+
const open = (w) => {
|
|
494
|
+
const alpha = alpha0 + w;
|
|
495
|
+
const p = betaMean(alpha, beta0);
|
|
496
|
+
return p >= threshold && clamp01(p - z * betaSd(alpha, beta0)) >= threshold;
|
|
497
|
+
};
|
|
498
|
+
if (threshold >= 1) return Number.POSITIVE_INFINITY;
|
|
499
|
+
if (open(0)) return 0;
|
|
500
|
+
let lo = 0;
|
|
501
|
+
let hi = 1;
|
|
502
|
+
while (!open(hi)) {
|
|
503
|
+
lo = hi;
|
|
504
|
+
hi *= 2;
|
|
505
|
+
if (hi > 1e15) return Number.POSITIVE_INFINITY;
|
|
506
|
+
}
|
|
507
|
+
for (let i = 0; i < 200 && hi - lo > 1e-9; i++) {
|
|
508
|
+
const mid = (lo + hi) / 2;
|
|
509
|
+
if (open(mid)) hi = mid;
|
|
510
|
+
else lo = mid;
|
|
511
|
+
}
|
|
512
|
+
return hi;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/explain.ts
|
|
516
|
+
function explain(state, intent, at, opts) {
|
|
517
|
+
const s = requireState(state);
|
|
518
|
+
const name = expectName(intent, "intent");
|
|
519
|
+
const t = expectTimestamp(at, "at");
|
|
520
|
+
const config = requireIntentConfig(s, name);
|
|
521
|
+
const filters = opts === void 0 ? {} : normalizeExplainOptions(opts, "options");
|
|
522
|
+
const learning = resolveLearning(s.config);
|
|
523
|
+
return buildExplanation(assessIntent(s, name, config, learning, t), config, learning, filters);
|
|
524
|
+
}
|
|
525
|
+
|
|
412
526
|
// src/observe.ts
|
|
413
527
|
function observe(state, event) {
|
|
414
528
|
let s = requireState(state);
|
|
@@ -460,6 +574,11 @@ function closeEpisode(state, at) {
|
|
|
460
574
|
}
|
|
461
575
|
return { ...state, intents, episode: emptyEpisode() };
|
|
462
576
|
}
|
|
577
|
+
function episodeClosed(prev, next) {
|
|
578
|
+
const a = requireState(prev);
|
|
579
|
+
const b = requireState(next);
|
|
580
|
+
return a.episode.startedAt !== null && b.episode.startedAt !== a.episode.startedAt;
|
|
581
|
+
}
|
|
463
582
|
|
|
464
583
|
// src/predict.ts
|
|
465
584
|
function predict(state, intent, at, opts) {
|
|
@@ -469,35 +588,14 @@ function predict(state, intent, at, opts) {
|
|
|
469
588
|
const config = requireIntentConfig(s, name);
|
|
470
589
|
const reveal = resolveReveal(s.config, config, opts);
|
|
471
590
|
const is = decayIntent(getIntentState(s, name, config), config, t);
|
|
472
|
-
|
|
473
|
-
const sd = betaSd(is.alpha, is.beta);
|
|
474
|
-
const revealEnd = cooldownEnd(is.lastRevealedAt, reveal.cooldown, t);
|
|
475
|
-
const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.cooldown, t);
|
|
476
|
-
const suppressedUntil = revealEnd === null ? outcomeEnd : outcomeEnd === null ? revealEnd : Math.max(revealEnd, outcomeEnd);
|
|
477
|
-
return {
|
|
478
|
-
intent: name,
|
|
479
|
-
p,
|
|
480
|
-
sd,
|
|
481
|
-
lower: clamp01(p - reveal.z * sd),
|
|
482
|
-
confidence: clamp01(1 - 2 * sd),
|
|
483
|
-
evidence: is.alpha + is.beta,
|
|
484
|
-
revealed: revealEnd !== null,
|
|
485
|
-
revealedAt: is.lastRevealedAt,
|
|
486
|
-
suppressedUntil
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
function cooldownEnd(since, cooldown, at) {
|
|
490
|
-
if (since === null) return null;
|
|
491
|
-
const end = since + cooldown;
|
|
492
|
-
return at < end ? end : null;
|
|
591
|
+
return buildPrediction(is, name, reveal, t);
|
|
493
592
|
}
|
|
494
593
|
function shouldReveal(state, intent, at, opts) {
|
|
495
594
|
const s = requireState(state);
|
|
496
595
|
const name = expectName(intent, "intent");
|
|
497
596
|
const config = requireIntentConfig(s, name);
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
return prediction.suppressedUntil === null && prediction.p >= threshold && prediction.lower >= threshold;
|
|
597
|
+
const reveal = resolveReveal(s.config, config, opts);
|
|
598
|
+
return decideReveal(predict(s, name, at, opts), reveal);
|
|
501
599
|
}
|
|
502
600
|
function markRevealed(state, intent, at) {
|
|
503
601
|
const s = requireState(state);
|
|
@@ -688,6 +786,9 @@ exports.STATE_VERSION = STATE_VERSION;
|
|
|
688
786
|
exports.createState = createState;
|
|
689
787
|
exports.deserialize = deserialize;
|
|
690
788
|
exports.endEpisode = endEpisode;
|
|
789
|
+
exports.episodeClosed = episodeClosed;
|
|
790
|
+
exports.evaluate = evaluate;
|
|
791
|
+
exports.evidenceToReveal = evidenceToReveal;
|
|
691
792
|
exports.explain = explain;
|
|
692
793
|
exports.markRevealed = markRevealed;
|
|
693
794
|
exports.observe = observe;
|