oracular 0.8.1 → 0.9.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 +8 -0
- package/README.md +88 -83
- package/dist/cli.js +52 -9
- package/dist/index.cjs +50 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +50 -3
- package/dist/index.js.map +1 -1
- package/package.json +10 -3
- package/schema/config.schema.json +14 -1
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;
|
|
@@ -847,7 +857,8 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
847
857
|
* Builds a fresh engine state from a config. Every intent starts at its prior and no episode is
|
|
848
858
|
* open. The config is validated and a normalized copy is stored inside the state, so a persisted
|
|
849
859
|
* state remembers the weights it was built with; pass your current config to {@link deserialize}
|
|
850
|
-
* when loading.
|
|
860
|
+
* when loading. `config.learning.lift` selects the lift formula; omitting it preserves the
|
|
861
|
+
* normalized difference used by existing configs and states.
|
|
851
862
|
*
|
|
852
863
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
853
864
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
@@ -932,4 +943,4 @@ declare const ms: Units;
|
|
|
932
943
|
*/
|
|
933
944
|
declare function validateConfig(config: unknown): EngineConfig;
|
|
934
945
|
|
|
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 };
|
|
946
|
+
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;
|
|
@@ -847,7 +857,8 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
|
|
|
847
857
|
* Builds a fresh engine state from a config. Every intent starts at its prior and no episode is
|
|
848
858
|
* open. The config is validated and a normalized copy is stored inside the state, so a persisted
|
|
849
859
|
* state remembers the weights it was built with; pass your current config to {@link deserialize}
|
|
850
|
-
* when loading.
|
|
860
|
+
* when loading. `config.learning.lift` selects the lift formula; omitting it preserves the
|
|
861
|
+
* normalized difference used by existing configs and states.
|
|
851
862
|
*
|
|
852
863
|
* @param config - Intents with their priors and signals, plus engine-wide options. Validated
|
|
853
864
|
* strictly: unknown keys, non-finite numbers and out-of-range values throw.
|
|
@@ -932,4 +943,4 @@ declare const ms: Units;
|
|
|
932
943
|
*/
|
|
933
944
|
declare function validateConfig(config: unknown): EngineConfig;
|
|
934
945
|
|
|
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 };
|
|
946
|
+
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
|
@@ -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) {
|
|
@@ -129,7 +158,9 @@ function signalWeights(state, intent, learning, signal) {
|
|
|
129
158
|
const nWithout = Math.max(0, totalEpisodes - nWith);
|
|
130
159
|
const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
|
|
131
160
|
const qWithout = laplace(outcomesWithout, nWithout);
|
|
132
|
-
const
|
|
161
|
+
const baseline = learning.lift === "association-rule" ? (record2.hits + outcomesWithout + 2) / (nWith + nWithout + 4) : qWithout;
|
|
162
|
+
const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
|
|
163
|
+
const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
|
|
133
164
|
const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
|
|
134
165
|
const k = learning.priorEpisodes;
|
|
135
166
|
const effective = (k * initial + nEff * learned) / (k + nEff);
|
|
@@ -255,7 +286,16 @@ var INTENT_KEYS = [
|
|
|
255
286
|
"reveal"
|
|
256
287
|
];
|
|
257
288
|
var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
|
|
258
|
-
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
|
|
289
|
+
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes", "lift"];
|
|
290
|
+
var LIFT_TYPES = [
|
|
291
|
+
"absolute-difference",
|
|
292
|
+
"relative-lift",
|
|
293
|
+
"risk-ratio",
|
|
294
|
+
"log-lift",
|
|
295
|
+
"odds-ratio",
|
|
296
|
+
"normalized-difference",
|
|
297
|
+
"association-rule"
|
|
298
|
+
];
|
|
259
299
|
var EXPLAIN_KEYS = ["nonZero", "limit"];
|
|
260
300
|
function typeError(path, message) {
|
|
261
301
|
throw new TypeError(`oracular: ${path} ${message}`);
|
|
@@ -340,6 +380,12 @@ function normalizeLearning(value, path) {
|
|
|
340
380
|
const record2 = expectRecord(value, path);
|
|
341
381
|
expectKeys(record2, LEARNING_KEYS, path);
|
|
342
382
|
const out = {};
|
|
383
|
+
if (record2.lift !== void 0) {
|
|
384
|
+
if (!LIFT_TYPES.includes(record2.lift)) {
|
|
385
|
+
typeError(`${path}.lift`, `must be one of: ${LIFT_TYPES.join(", ")}`);
|
|
386
|
+
}
|
|
387
|
+
out.lift = record2.lift;
|
|
388
|
+
}
|
|
343
389
|
if (record2.halfLife !== void 0)
|
|
344
390
|
out.halfLife = expectPositive(record2.halfLife, `${path}.halfLife`);
|
|
345
391
|
if (record2.gain !== void 0) out.gain = expectBoundedPositive(record2.gain, `${path}.gain`);
|
|
@@ -460,6 +506,7 @@ function resolveReveal(config, intent, opts, normalized = false) {
|
|
|
460
506
|
}
|
|
461
507
|
function resolveLearning(config) {
|
|
462
508
|
return {
|
|
509
|
+
lift: config.learning?.lift ?? "normalized-difference",
|
|
463
510
|
halfLife: config.learning?.halfLife ?? null,
|
|
464
511
|
gain: config.learning?.gain ?? DEFAULT_GAIN,
|
|
465
512
|
priorEpisodes: config.learning?.priorEpisodes ?? DEFAULT_PRIOR_EPISODES
|