oracular 0.8.1 → 0.10.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 +26 -0
- package/README.md +90 -85
- package/dist/cli.js +102 -30
- package/dist/index.cjs +98 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -10
- package/dist/index.d.ts +30 -10
- package/dist/index.js +98 -30
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +11 -4
- package/dist/presets.cjs.map +1 -1
- package/dist/presets.d.cts +0 -17
- package/dist/presets.d.ts +0 -17
- package/dist/presets.js +11 -4
- package/dist/presets.js.map +1 -1
- package/package.json +10 -3
- package/schema/config.schema.json +18 -4
package/dist/index.d.cts
CHANGED
|
@@ -62,6 +62,8 @@ interface RevealOptions {
|
|
|
62
62
|
*/
|
|
63
63
|
bound?: 'normal' | 'exact';
|
|
64
64
|
}
|
|
65
|
+
/** The association measure converted to a signed, bounded score when learning signal weights. */
|
|
66
|
+
type LiftType = 'absolute-difference' | 'relative-lift' | 'risk-ratio' | 'log-lift' | 'odds-ratio' | 'normalized-difference' | 'association-rule';
|
|
65
67
|
/**
|
|
66
68
|
* How the engine learns from the user's own outcomes.
|
|
67
69
|
*
|
|
@@ -74,9 +76,17 @@ interface RevealOptions {
|
|
|
74
76
|
* ```
|
|
75
77
|
*/
|
|
76
78
|
interface LearningConfig {
|
|
79
|
+
/**
|
|
80
|
+
* Optional association measure. Every choice is converted to a score in [−1, 1], neutral at
|
|
81
|
+
* zero, before learning: ratios use (R − 1)/(R + 1), relative/log lift use x/(1 + |x|), and
|
|
82
|
+
* absolute/normalized differences keep their scale. Omit to preserve the existing normalized
|
|
83
|
+
* difference exactly. 'association-rule' uses the overall outcome rate as its baseline; the
|
|
84
|
+
* others use episodes without the signal. Raw ratios are reporting metrics, never weights.
|
|
85
|
+
*/
|
|
86
|
+
lift?: LiftType;
|
|
77
87
|
/** Half-life of learned hit/miss counts, in the same unit as `at`. Omit for no forgetting. */
|
|
78
88
|
halfLife?: number;
|
|
79
|
-
/**
|
|
89
|
+
/** Caps learned magnitude at `gain × |initial weight|` for every lift option. Default 2; at most 1e6. */
|
|
80
90
|
gain?: number;
|
|
81
91
|
/** Pseudo-count behind the developer's initial weight: how many observed episodes it takes to weigh as much as the prior. Default 10; at most 1e6. */
|
|
82
92
|
priorEpisodes?: number;
|
|
@@ -619,7 +629,9 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
619
629
|
* `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
|
|
620
630
|
* That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
|
|
621
631
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
622
|
-
* outcome of: give the
|
|
632
|
+
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
633
|
+
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
634
|
+
* counts nothing), credit
|
|
623
635
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
624
636
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
625
637
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -814,7 +826,9 @@ declare function serialize(state: EngineState): string;
|
|
|
814
826
|
/**
|
|
815
827
|
* Restores a state saved with {@link serialize}.
|
|
816
828
|
*
|
|
817
|
-
* Always pass the app's current `config`: it is validated and replaces the stored snapshot
|
|
829
|
+
* Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
|
|
830
|
+
* stored one is read without the checks that refuse a config that cannot work, so a state saved
|
|
831
|
+
* before those checks existed still loads),
|
|
818
832
|
* intents and signals no longer declared are dropped (α and β are recomputed from the kept
|
|
819
833
|
* contributions), and newly declared intents start at their prior. States saved by oracular
|
|
820
834
|
* 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
|
|
@@ -847,15 +861,19 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
847
861
|
* Builds a fresh engine state from a config. Every intent starts at its prior and no episode is
|
|
848
862
|
* open. The config is validated and a normalized copy is stored inside the state, so a persisted
|
|
849
863
|
* state remembers the weights it was built with; pass your current config to {@link deserialize}
|
|
850
|
-
* when loading.
|
|
864
|
+
* when loading. `config.learning.lift` selects the lift formula; omitting it preserves the
|
|
865
|
+
* normalized difference used by existing configs and states.
|
|
851
866
|
*
|
|
852
867
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
853
868
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
854
869
|
* @returns A new, JSON-serializable state. `config` is not mutated.
|
|
855
|
-
* @throws `TypeError` when a value has the wrong type
|
|
870
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
871
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on (the reset would wipe what
|
|
872
|
+
* the signal just added).
|
|
856
873
|
* @throws `RangeError` when a number is out of range, for example a `prior` outside (0, 1), a
|
|
857
|
-
* signal weight of 0,
|
|
858
|
-
* `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
874
|
+
* signal weight of 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight,
|
|
875
|
+
* `priorStrength`, `learning.gain` or `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
876
|
+
* overflow the arithmetic).
|
|
859
877
|
* @example
|
|
860
878
|
* ```ts
|
|
861
879
|
* const state = createState({
|
|
@@ -918,9 +936,11 @@ declare const ms: Units;
|
|
|
918
936
|
*
|
|
919
937
|
* @param config - The config to check. Typed `unknown` so parsed JSON can be passed as it is.
|
|
920
938
|
* @returns A fresh, normalized {@link EngineConfig}. `config` is not mutated.
|
|
921
|
-
* @throws `TypeError` when a value has the wrong type
|
|
939
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
940
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on.
|
|
922
941
|
* @throws `RangeError` when a number is out of range: a `prior` outside (0, 1), a signal weight of
|
|
923
|
-
* 0,
|
|
942
|
+
* 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight, `priorStrength`,
|
|
943
|
+
* `learning.gain` or `learning.priorEpisodes` beyond 1e6.
|
|
924
944
|
* @example
|
|
925
945
|
* ```ts
|
|
926
946
|
* import { readFileSync } from 'node:fs';
|
|
@@ -932,4 +952,4 @@ declare const ms: Units;
|
|
|
932
952
|
*/
|
|
933
953
|
declare function validateConfig(config: unknown): EngineConfig;
|
|
934
954
|
|
|
935
|
-
export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
|
|
955
|
+
export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ interface RevealOptions {
|
|
|
62
62
|
*/
|
|
63
63
|
bound?: 'normal' | 'exact';
|
|
64
64
|
}
|
|
65
|
+
/** The association measure converted to a signed, bounded score when learning signal weights. */
|
|
66
|
+
type LiftType = 'absolute-difference' | 'relative-lift' | 'risk-ratio' | 'log-lift' | 'odds-ratio' | 'normalized-difference' | 'association-rule';
|
|
65
67
|
/**
|
|
66
68
|
* How the engine learns from the user's own outcomes.
|
|
67
69
|
*
|
|
@@ -74,9 +76,17 @@ interface RevealOptions {
|
|
|
74
76
|
* ```
|
|
75
77
|
*/
|
|
76
78
|
interface LearningConfig {
|
|
79
|
+
/**
|
|
80
|
+
* Optional association measure. Every choice is converted to a score in [−1, 1], neutral at
|
|
81
|
+
* zero, before learning: ratios use (R − 1)/(R + 1), relative/log lift use x/(1 + |x|), and
|
|
82
|
+
* absolute/normalized differences keep their scale. Omit to preserve the existing normalized
|
|
83
|
+
* difference exactly. 'association-rule' uses the overall outcome rate as its baseline; the
|
|
84
|
+
* others use episodes without the signal. Raw ratios are reporting metrics, never weights.
|
|
85
|
+
*/
|
|
86
|
+
lift?: LiftType;
|
|
77
87
|
/** Half-life of learned hit/miss counts, in the same unit as `at`. Omit for no forgetting. */
|
|
78
88
|
halfLife?: number;
|
|
79
|
-
/**
|
|
89
|
+
/** Caps learned magnitude at `gain × |initial weight|` for every lift option. Default 2; at most 1e6. */
|
|
80
90
|
gain?: number;
|
|
81
91
|
/** Pseudo-count behind the developer's initial weight: how many observed episodes it takes to weigh as much as the prior. Default 10; at most 1e6. */
|
|
82
92
|
priorEpisodes?: number;
|
|
@@ -619,7 +629,9 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
|
|
|
619
629
|
* `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
|
|
620
630
|
* That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
|
|
621
631
|
* it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
|
|
622
|
-
* outcome of: give the
|
|
632
|
+
* outcome of: give the signals remembered since the episode started or the outcome last arrived a
|
|
633
|
+
* hit and count the episode toward the base rate (a repeated outcome with nothing seen since
|
|
634
|
+
* counts nothing), credit
|
|
623
635
|
* the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
|
|
624
636
|
* each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
|
|
625
637
|
* or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
|
|
@@ -814,7 +826,9 @@ declare function serialize(state: EngineState): string;
|
|
|
814
826
|
/**
|
|
815
827
|
* Restores a state saved with {@link serialize}.
|
|
816
828
|
*
|
|
817
|
-
* Always pass the app's current `config`: it is validated and replaces the stored snapshot
|
|
829
|
+
* Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
|
|
830
|
+
* stored one is read without the checks that refuse a config that cannot work, so a state saved
|
|
831
|
+
* before those checks existed still loads),
|
|
818
832
|
* intents and signals no longer declared are dropped (α and β are recomputed from the kept
|
|
819
833
|
* contributions), and newly declared intents start at their prior. States saved by oracular
|
|
820
834
|
* 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
|
|
@@ -847,15 +861,19 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
847
861
|
* Builds a fresh engine state from a config. Every intent starts at its prior and no episode is
|
|
848
862
|
* open. The config is validated and a normalized copy is stored inside the state, so a persisted
|
|
849
863
|
* state remembers the weights it was built with; pass your current config to {@link deserialize}
|
|
850
|
-
* when loading.
|
|
864
|
+
* when loading. `config.learning.lift` selects the lift formula; omitting it preserves the
|
|
865
|
+
* normalized difference used by existing configs and states.
|
|
851
866
|
*
|
|
852
867
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
853
868
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
854
869
|
* @returns A new, JSON-serializable state. `config` is not mutated.
|
|
855
|
-
* @throws `TypeError` when a value has the wrong type
|
|
870
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
871
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on (the reset would wipe what
|
|
872
|
+
* the signal just added).
|
|
856
873
|
* @throws `RangeError` when a number is out of range, for example a `prior` outside (0, 1), a
|
|
857
|
-
* signal weight of 0,
|
|
858
|
-
* `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
874
|
+
* signal weight of 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight,
|
|
875
|
+
* `priorStrength`, `learning.gain` or `learning.priorEpisodes` beyond 1e6 (larger values could
|
|
876
|
+
* overflow the arithmetic).
|
|
859
877
|
* @example
|
|
860
878
|
* ```ts
|
|
861
879
|
* const state = createState({
|
|
@@ -918,9 +936,11 @@ declare const ms: Units;
|
|
|
918
936
|
*
|
|
919
937
|
* @param config - The config to check. Typed `unknown` so parsed JSON can be passed as it is.
|
|
920
938
|
* @returns A fresh, normalized {@link EngineConfig}. `config` is not mutated.
|
|
921
|
-
* @throws `TypeError` when a value has the wrong type
|
|
939
|
+
* @throws `TypeError` when a value has the wrong type, a key is not recognised, or an intent's
|
|
940
|
+
* `outcome` is also one of its signals while `resetOnOutcome` is on.
|
|
922
941
|
* @throws `RangeError` when a number is out of range: a `prior` outside (0, 1), a signal weight of
|
|
923
|
-
* 0,
|
|
942
|
+
* 0, a reveal `threshold` of 0 or 1, a `z` above 10, or a weight, `priorStrength`,
|
|
943
|
+
* `learning.gain` or `learning.priorEpisodes` beyond 1e6.
|
|
924
944
|
* @example
|
|
925
945
|
* ```ts
|
|
926
946
|
* import { readFileSync } from 'node:fs';
|
|
@@ -932,4 +952,4 @@ declare const ms: Units;
|
|
|
932
952
|
*/
|
|
933
953
|
declare function validateConfig(config: unknown): EngineConfig;
|
|
934
954
|
|
|
935
|
-
export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
|
|
955
|
+
export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type LiftType, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/math.ts
|
|
2
2
|
function decayFactor(elapsed, halfLife) {
|
|
3
|
-
if (!(elapsed > 0)
|
|
3
|
+
if (!(elapsed > 0)) return 1;
|
|
4
4
|
return 2 ** (-elapsed / halfLife);
|
|
5
5
|
}
|
|
6
6
|
function betaMean(alpha, beta) {
|
|
@@ -80,10 +80,39 @@ function repeatFactor(n) {
|
|
|
80
80
|
function laplace(successes, total) {
|
|
81
81
|
return (successes + 1) / (total + 2);
|
|
82
82
|
}
|
|
83
|
-
function lift(q, b) {
|
|
83
|
+
function lift(q, b, type = "normalized-difference") {
|
|
84
|
+
if (type === "absolute-difference") return q - b;
|
|
85
|
+
if (type !== "normalized-difference") {
|
|
86
|
+
const rate = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, q));
|
|
87
|
+
const base = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, b));
|
|
88
|
+
switch (type) {
|
|
89
|
+
case "relative-lift":
|
|
90
|
+
return (rate - base) / base;
|
|
91
|
+
case "log-lift":
|
|
92
|
+
return Math.log(rate / base);
|
|
93
|
+
case "odds-ratio":
|
|
94
|
+
return rate / (1 - rate) / (base / (1 - base));
|
|
95
|
+
case "risk-ratio":
|
|
96
|
+
case "association-rule":
|
|
97
|
+
return rate / base;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
84
100
|
if (q >= b) return b >= 1 ? 0 : (q - b) / (1 - b);
|
|
85
101
|
return b <= 0 ? 0 : (q - b) / b;
|
|
86
102
|
}
|
|
103
|
+
function learningScore(value, type = "normalized-difference") {
|
|
104
|
+
switch (type) {
|
|
105
|
+
case "risk-ratio":
|
|
106
|
+
case "odds-ratio":
|
|
107
|
+
case "association-rule":
|
|
108
|
+
return (value - 1) / (value + 1);
|
|
109
|
+
case "relative-lift":
|
|
110
|
+
case "log-lift":
|
|
111
|
+
return value / (1 + Math.abs(value));
|
|
112
|
+
default:
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
87
116
|
|
|
88
117
|
// src/util.ts
|
|
89
118
|
function isRecord(value) {
|
|
@@ -107,12 +136,11 @@ function mapRecord(record2, fn) {
|
|
|
107
136
|
var NONE = { hits: 0, misses: 0 };
|
|
108
137
|
function decayLearned(state, learning, at) {
|
|
109
138
|
if (state.learnedAt === null) return { ...state, learnedAt: at };
|
|
110
|
-
const learnedAt = Math.max(state.learnedAt, at);
|
|
111
139
|
const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
|
|
112
|
-
if (d === 1) return
|
|
140
|
+
if (d === 1) return at === state.learnedAt ? state : { ...state, learnedAt: at };
|
|
113
141
|
return {
|
|
114
142
|
...state,
|
|
115
|
-
learnedAt,
|
|
143
|
+
learnedAt: at,
|
|
116
144
|
learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
|
|
117
145
|
episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
|
|
118
146
|
};
|
|
@@ -129,7 +157,9 @@ function signalWeights(state, intent, learning, signal) {
|
|
|
129
157
|
const nWithout = Math.max(0, totalEpisodes - nWith);
|
|
130
158
|
const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
|
|
131
159
|
const qWithout = laplace(outcomesWithout, nWithout);
|
|
132
|
-
const
|
|
160
|
+
const baseline = learning.lift === "association-rule" ? (record2.hits + outcomesWithout + 2) / (nWith + nWithout + 4) : qWithout;
|
|
161
|
+
const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
|
|
162
|
+
const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
|
|
133
163
|
const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
|
|
134
164
|
const k = learning.priorEpisodes;
|
|
135
165
|
const effective = (k * initial + nEff * learned) / (k + nEff);
|
|
@@ -219,13 +249,12 @@ function withContributions(state, intent, contributions) {
|
|
|
219
249
|
function decayIntent(state, intent, at) {
|
|
220
250
|
if (state.updatedAt === null) return { ...state, updatedAt: at };
|
|
221
251
|
const d = decayFactor(at - state.updatedAt, intent.halfLife);
|
|
222
|
-
|
|
223
|
-
if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
|
|
252
|
+
if (d === 1) return at === state.updatedAt ? state : { ...state, updatedAt: at };
|
|
224
253
|
const contributions = mapRecord(state.contributions, (c) => ({
|
|
225
254
|
alpha: c.alpha * d,
|
|
226
255
|
beta: c.beta * d
|
|
227
256
|
}));
|
|
228
|
-
return withContributions({ ...state, updatedAt }, intent, contributions);
|
|
257
|
+
return withContributions({ ...state, updatedAt: at }, intent, contributions);
|
|
229
258
|
}
|
|
230
259
|
function applySignal(state, intent, signal, weight) {
|
|
231
260
|
const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
|
|
@@ -242,6 +271,7 @@ var DEFAULT_Z = 1.28;
|
|
|
242
271
|
var DEFAULT_GAIN = 2;
|
|
243
272
|
var DEFAULT_PRIOR_EPISODES = 10;
|
|
244
273
|
var MAX_MAGNITUDE = 1e6;
|
|
274
|
+
var MAX_Z = 10;
|
|
245
275
|
var CONFIG_KEYS = ["intents", "episodeTimeout", "learning", "reveal"];
|
|
246
276
|
var INTENT_KEYS = [
|
|
247
277
|
"prior",
|
|
@@ -255,7 +285,16 @@ var INTENT_KEYS = [
|
|
|
255
285
|
"reveal"
|
|
256
286
|
];
|
|
257
287
|
var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
|
|
258
|
-
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
|
|
288
|
+
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes", "lift"];
|
|
289
|
+
var LIFT_TYPES = [
|
|
290
|
+
"absolute-difference",
|
|
291
|
+
"relative-lift",
|
|
292
|
+
"risk-ratio",
|
|
293
|
+
"log-lift",
|
|
294
|
+
"odds-ratio",
|
|
295
|
+
"normalized-difference",
|
|
296
|
+
"association-rule"
|
|
297
|
+
];
|
|
259
298
|
var EXPLAIN_KEYS = ["nonZero", "limit"];
|
|
260
299
|
function typeError(path, message) {
|
|
261
300
|
throw new TypeError(`oracular: ${path} ${message}`);
|
|
@@ -317,14 +356,17 @@ function expectKeyName(name, path) {
|
|
|
317
356
|
if (name === "__proto__") typeError(path, 'must not use the reserved name "__proto__"');
|
|
318
357
|
return name;
|
|
319
358
|
}
|
|
320
|
-
function normalizeRevealOptions(value, path) {
|
|
359
|
+
function normalizeRevealOptions(value, path, strict = true) {
|
|
321
360
|
const record2 = expectRecord(value, path);
|
|
322
361
|
expectKeys(record2, REVEAL_KEYS, path);
|
|
323
362
|
const out = {};
|
|
324
363
|
if (record2.threshold !== void 0) {
|
|
325
|
-
out.threshold = expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
364
|
+
out.threshold = strict ? expectOpenUnit(record2.threshold, `${path}.threshold`) : expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
365
|
+
}
|
|
366
|
+
if (record2.z !== void 0) {
|
|
367
|
+
out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
368
|
+
if (strict && out.z > MAX_Z) rangeError(`${path}.z`, `must be at most ${MAX_Z}`);
|
|
326
369
|
}
|
|
327
|
-
if (record2.z !== void 0) out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
328
370
|
if (record2.cooldown !== void 0) {
|
|
329
371
|
out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
|
|
330
372
|
}
|
|
@@ -340,6 +382,12 @@ function normalizeLearning(value, path) {
|
|
|
340
382
|
const record2 = expectRecord(value, path);
|
|
341
383
|
expectKeys(record2, LEARNING_KEYS, path);
|
|
342
384
|
const out = {};
|
|
385
|
+
if (record2.lift !== void 0) {
|
|
386
|
+
if (!LIFT_TYPES.includes(record2.lift)) {
|
|
387
|
+
typeError(`${path}.lift`, `must be one of: ${LIFT_TYPES.join(", ")}`);
|
|
388
|
+
}
|
|
389
|
+
out.lift = record2.lift;
|
|
390
|
+
}
|
|
343
391
|
if (record2.halfLife !== void 0)
|
|
344
392
|
out.halfLife = expectPositive(record2.halfLife, `${path}.halfLife`);
|
|
345
393
|
if (record2.gain !== void 0) out.gain = expectBoundedPositive(record2.gain, `${path}.gain`);
|
|
@@ -348,7 +396,7 @@ function normalizeLearning(value, path) {
|
|
|
348
396
|
}
|
|
349
397
|
return out;
|
|
350
398
|
}
|
|
351
|
-
function normalizeIntentConfig(value, path) {
|
|
399
|
+
function normalizeIntentConfig(value, path, strict = true) {
|
|
352
400
|
const record2 = expectRecord(value, path);
|
|
353
401
|
expectKeys(record2, INTENT_KEYS, path);
|
|
354
402
|
const signalsRecord = expectRecord(record2.signals, `${path}.signals`);
|
|
@@ -390,11 +438,18 @@ function normalizeIntentConfig(value, path) {
|
|
|
390
438
|
if (record2.attribution !== void 0) {
|
|
391
439
|
out.attribution = expectPositive(record2.attribution, `${path}.attribution`);
|
|
392
440
|
}
|
|
393
|
-
if (record2.reveal !== void 0)
|
|
394
|
-
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal
|
|
441
|
+
if (record2.reveal !== void 0) {
|
|
442
|
+
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal`, strict);
|
|
443
|
+
}
|
|
444
|
+
if (strict && out.outcome !== void 0 && hasOwn(signals, out.outcome) && out.resetOnOutcome !== false) {
|
|
445
|
+
typeError(
|
|
446
|
+
`${path}.outcome`,
|
|
447
|
+
"is also one of its signals: the reset on outcome would wipe what it adds; set resetOnOutcome: false to keep it"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
395
450
|
return out;
|
|
396
451
|
}
|
|
397
|
-
function normalizeConfig(value) {
|
|
452
|
+
function normalizeConfig(value, strict = true) {
|
|
398
453
|
const record2 = expectRecord(value, "config");
|
|
399
454
|
expectKeys(record2, CONFIG_KEYS, "config");
|
|
400
455
|
const intentsRecord = expectRecord(record2.intents, "config.intents");
|
|
@@ -405,7 +460,8 @@ function normalizeConfig(value) {
|
|
|
405
460
|
expectKeyName(name, "config.intents");
|
|
406
461
|
intents[name] = normalizeIntentConfig(
|
|
407
462
|
intentsRecord[name],
|
|
408
|
-
`config.intents[${JSON.stringify(name)}]
|
|
463
|
+
`config.intents[${JSON.stringify(name)}]`,
|
|
464
|
+
strict
|
|
409
465
|
);
|
|
410
466
|
}
|
|
411
467
|
const out = { intents };
|
|
@@ -414,8 +470,9 @@ function normalizeConfig(value) {
|
|
|
414
470
|
}
|
|
415
471
|
if (record2.learning !== void 0)
|
|
416
472
|
out.learning = normalizeLearning(record2.learning, "config.learning");
|
|
417
|
-
if (record2.reveal !== void 0)
|
|
418
|
-
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal");
|
|
473
|
+
if (record2.reveal !== void 0) {
|
|
474
|
+
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal", strict);
|
|
475
|
+
}
|
|
419
476
|
return out;
|
|
420
477
|
}
|
|
421
478
|
function normalizeExplainOptions(value, path) {
|
|
@@ -445,12 +502,12 @@ function splitEvaluateOptions(value, path) {
|
|
|
445
502
|
else explain2[key] = v;
|
|
446
503
|
}
|
|
447
504
|
return {
|
|
448
|
-
reveal: normalizeRevealOptions(reveal, path),
|
|
505
|
+
reveal: normalizeRevealOptions(reveal, path, false),
|
|
449
506
|
explain: normalizeExplainOptions(explain2, path)
|
|
450
507
|
};
|
|
451
508
|
}
|
|
452
509
|
function resolveReveal(config, intent, opts, normalized = false) {
|
|
453
|
-
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options");
|
|
510
|
+
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options", false);
|
|
454
511
|
return {
|
|
455
512
|
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
456
513
|
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
@@ -460,6 +517,7 @@ function resolveReveal(config, intent, opts, normalized = false) {
|
|
|
460
517
|
}
|
|
461
518
|
function resolveLearning(config) {
|
|
462
519
|
return {
|
|
520
|
+
lift: config.learning?.lift ?? "normalized-difference",
|
|
463
521
|
halfLife: config.learning?.halfLife ?? null,
|
|
464
522
|
gain: config.learning?.gain ?? DEFAULT_GAIN,
|
|
465
523
|
priorEpisodes: config.learning?.priorEpisodes ?? DEFAULT_PRIOR_EPISODES
|
|
@@ -487,11 +545,7 @@ function timedOutAt(episode, timeout, at) {
|
|
|
487
545
|
return at - episode.lastEventAt > timeout ? episode.lastEventAt + timeout : null;
|
|
488
546
|
}
|
|
489
547
|
function extendEpisode(episode, at) {
|
|
490
|
-
return {
|
|
491
|
-
startedAt: episode.startedAt ?? at,
|
|
492
|
-
lastEventAt: episode.lastEventAt === null ? at : Math.max(episode.lastEventAt, at),
|
|
493
|
-
intents: episode.intents
|
|
494
|
-
};
|
|
548
|
+
return { startedAt: episode.startedAt ?? at, lastEventAt: at, intents: episode.intents };
|
|
495
549
|
}
|
|
496
550
|
function episodeRecord(episode, intent) {
|
|
497
551
|
return getOwn(episode.intents, intent) ?? { seen: [], counts: {}, resolved: false };
|
|
@@ -549,7 +603,7 @@ function assessIntent(state, name, intent, learning, at) {
|
|
|
549
603
|
}
|
|
550
604
|
function cooldownEnd(since, cooldown, at) {
|
|
551
605
|
if (since === null) return null;
|
|
552
|
-
const end = since + cooldown;
|
|
606
|
+
const end = Math.min(since, at) + cooldown;
|
|
553
607
|
return at < end ? end : null;
|
|
554
608
|
}
|
|
555
609
|
function buildPrediction(is, name, reveal, at) {
|
|
@@ -677,6 +731,7 @@ function observe(state, event) {
|
|
|
677
731
|
let is = getIntentState(s, intentName, intent);
|
|
678
732
|
is = decayIntent(is, intent, at);
|
|
679
733
|
is = settlePending(is, intent, learning, at);
|
|
734
|
+
is = pinAnchors(is, at);
|
|
680
735
|
if (isSignal) {
|
|
681
736
|
const { effective } = signalWeights(is, intent, learning, name);
|
|
682
737
|
const n = occurrences(episode, intentName, name) + 1;
|
|
@@ -686,7 +741,8 @@ function observe(state, event) {
|
|
|
686
741
|
if (!isOutcome) episode = markSeen(episode, intentName, name);
|
|
687
742
|
}
|
|
688
743
|
if (isOutcome) {
|
|
689
|
-
|
|
744
|
+
const record2 = episodeRecord(episode, intentName);
|
|
745
|
+
if (!record2.resolved || record2.seen.length > 0) is = recordEpisode(is, record2.seen, 1);
|
|
690
746
|
is = creditPending(is, intent, at);
|
|
691
747
|
episode = markResolved(episode, intentName);
|
|
692
748
|
is = { ...is, lastOutcomeAt: at };
|
|
@@ -696,6 +752,16 @@ function observe(state, event) {
|
|
|
696
752
|
}
|
|
697
753
|
return { ...s, intents, episode };
|
|
698
754
|
}
|
|
755
|
+
function pinAnchors(is, at) {
|
|
756
|
+
const reveal = is.lastRevealedAt !== null && is.lastRevealedAt > at;
|
|
757
|
+
const outcome = is.lastOutcomeAt !== null && is.lastOutcomeAt > at;
|
|
758
|
+
if (!reveal && !outcome) return is;
|
|
759
|
+
return {
|
|
760
|
+
...is,
|
|
761
|
+
lastRevealedAt: reveal ? at : is.lastRevealedAt,
|
|
762
|
+
lastOutcomeAt: outcome ? at : is.lastOutcomeAt
|
|
763
|
+
};
|
|
764
|
+
}
|
|
699
765
|
function endEpisode(state, at) {
|
|
700
766
|
const s = requireState(state);
|
|
701
767
|
const t = expectTimestamp(at, "at");
|
|
@@ -1013,7 +1079,9 @@ function parseState(value) {
|
|
|
1013
1079
|
}
|
|
1014
1080
|
return {
|
|
1015
1081
|
version: STATE_VERSION,
|
|
1016
|
-
|
|
1082
|
+
// Without the strict checks: a config this release would refuse at createState still loads
|
|
1083
|
+
// when it is only stored, and the config passed in replaces it anyway.
|
|
1084
|
+
config: normalizeConfig(r.config, false),
|
|
1017
1085
|
intents: mapOwn(r.intents, "state.intents", intentState),
|
|
1018
1086
|
episode: episodeState(r.episode, "state.episode")
|
|
1019
1087
|
};
|