@effect-uai/core 0.16.0 → 0.18.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/README.md +1 -1
- package/dist/decision-model/Decision.d.mts +155 -0
- package/dist/decision-model/Decision.d.mts.map +1 -0
- package/dist/decision-model/Decision.mjs +118 -0
- package/dist/decision-model/Decision.mjs.map +1 -0
- package/dist/decision-model/Decision.test.d.mts +1 -0
- package/dist/decision-model/Decision.test.mjs +164 -0
- package/dist/decision-model/Decision.test.mjs.map +1 -0
- package/dist/decision-model/DecisionModel.d.mts +45 -0
- package/dist/decision-model/DecisionModel.d.mts.map +1 -0
- package/dist/decision-model/DecisionModel.mjs +25 -0
- package/dist/decision-model/DecisionModel.mjs.map +1 -0
- package/dist/dist-C8RmtoR8.mjs +172 -0
- package/dist/dist-C8RmtoR8.mjs.map +1 -0
- package/dist/dist-CorQTQL-.mjs +1533 -0
- package/dist/dist-CorQTQL-.mjs.map +1 -0
- package/dist/dist-DNeuh84d.mjs +7 -0
- package/dist/dist-DNeuh84d.mjs.map +1 -0
- package/dist/dist-DzeSyeYF.mjs +10253 -0
- package/dist/dist-DzeSyeYF.mjs.map +1 -0
- package/dist/domain/Transcript.test.mjs +3 -3
- package/dist/domain/Turn.test.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.test.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.C4tKZ0yW-LuFnBtmu.mjs +7042 -0
- package/dist/index.C4tKZ0yW-LuFnBtmu.mjs.map +1 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +4 -1
- package/dist/language-model/LanguageModel.test.mjs +2 -2
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +3 -3
- package/dist/math/Probability.d.mts +21 -0
- package/dist/math/Probability.d.mts.map +1 -0
- package/dist/math/Probability.mjs +31 -0
- package/dist/math/Probability.mjs.map +1 -0
- package/dist/math/Probability.test.d.mts +1 -0
- package/dist/math/Probability.test.mjs +95 -0
- package/dist/math/Probability.test.mjs.map +1 -0
- package/dist/messenger/MessengerAdapter.test.mjs +3 -3
- package/dist/music-generator/MusicGenerator.test.mjs +3 -3
- package/dist/observability/Metrics.test.mjs +3 -3
- package/dist/observability/Telemetry.d.mts +1 -1
- package/dist/observability/Telemetry.mjs +1 -1
- package/dist/observability/Telemetry.mjs.map +1 -1
- package/dist/observability/Telemetry.test.mjs +4 -4
- package/dist/observability/Telemetry.test.mjs.map +1 -1
- package/dist/realtime/RealtimeSession.test.mjs +4 -4
- package/dist/retry/Retry.test.mjs +2 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +3 -3
- package/dist/streaming/JSONL.test.mjs +2 -2
- package/dist/streaming/Multipart.d.mts +1 -1
- package/dist/streaming/Multipart.mjs +1 -1
- package/dist/streaming/Multipart.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +1 -1
- package/dist/streaming/SSE.mjs +2 -2
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.mjs +2 -2
- package/dist/streaming/Settle.test.mjs +3 -3
- package/dist/streaming/WebSocketSession.d.mts +5 -3
- package/dist/streaming/WebSocketSession.d.mts.map +1 -1
- package/dist/streaming/WebSocketSession.mjs +22 -10
- package/dist/streaming/WebSocketSession.mjs.map +1 -1
- package/dist/streaming/WebSocketSession.test.mjs +41 -5
- package/dist/streaming/WebSocketSession.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +2 -2
- package/dist/testing/FakeWebSocket.d.mts +5 -1
- package/dist/testing/FakeWebSocket.d.mts.map +1 -1
- package/dist/testing/FakeWebSocket.mjs +12 -6
- package/dist/testing/FakeWebSocket.mjs.map +1 -1
- package/dist/tool/Approval.test.mjs +2 -2
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +3 -3
- package/dist/tool/Toolkit.test.mjs +3 -3
- package/dist/transcriber/Transcriber.test.mjs +3 -3
- package/package.json +15 -3
- package/src/decision-model/Decision.test.ts +146 -0
- package/src/decision-model/Decision.ts +273 -0
- package/src/decision-model/DecisionModel.ts +53 -0
- package/src/embedding-model/EmbeddingModel.test.ts +2 -2
- package/src/index.ts +3 -0
- package/src/loop/Loop.ts +1 -1
- package/src/math/Probability.test.ts +61 -0
- package/src/math/Probability.ts +44 -0
- package/src/observability/Telemetry.test.ts +1 -1
- package/src/observability/Telemetry.ts +2 -2
- package/src/streaming/Multipart.ts +1 -1
- package/src/streaming/SSE.ts +2 -2
- package/src/streaming/WebSocketSession.test.ts +54 -2
- package/src/streaming/WebSocketSession.ts +27 -6
- package/src/structured-format/StructuredFormat.ts +1 -1
- package/src/testing/FakeWebSocket.ts +21 -6
- package/src/tool/Tool.ts +1 -1
- package/dist/dist-BaenoU1A.mjs +0 -145
- package/dist/dist-BaenoU1A.mjs.map +0 -1
- package/dist/dist-nohcnkD6.mjs +0 -182
- package/dist/dist-nohcnkD6.mjs.map +0 -1
- package/dist/magic-string.es-CB2hOw9v.mjs +0 -1014
- package/dist/magic-string.es-CB2hOw9v.mjs.map +0 -1
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs +0 -10254
- package/dist/test.DNmyFkvJ-YPzDR0Co.mjs.map +0 -1
- package/dist/utils-CiH_DjFW.mjs +0 -3918
- package/dist/utils-CiH_DjFW.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { t as AiError } from "../AiError-DvhLIbmd.mjs";
|
|
2
|
+
import { Effect, Option, Schema } from "effect";
|
|
3
|
+
declare namespace Decision_d_exports {
|
|
4
|
+
export { Answer, AnswerFor, Answers, Boundary, ClassifyAnswer, ClassifyDecision, Decision, Decisions, Definition, Labels, Levels, ProbabilityAnswer, ProbabilityDecision, RateAnswer, RateDecision, assertKinds, classify, confidence, expectedLevel, make, margin, probabilitiesOf, probability, ranked, rate, topLevel, winner };
|
|
5
|
+
}
|
|
6
|
+
/** Labels mapped to what each one means. */
|
|
7
|
+
type Labels = Readonly<Record<string, string>>;
|
|
8
|
+
/** An ordered rubric, lowest level first. */
|
|
9
|
+
type Levels = readonly [string, string, ...ReadonlyArray<string>];
|
|
10
|
+
/** What a true and a false answer mean, when the boundary is subtle. */
|
|
11
|
+
type Boundary = {
|
|
12
|
+
readonly true?: string;
|
|
13
|
+
readonly false?: string;
|
|
14
|
+
};
|
|
15
|
+
type ClassifyDecision<L extends Labels = Labels> = {
|
|
16
|
+
readonly _tag: "Classify";
|
|
17
|
+
readonly instructions: string;
|
|
18
|
+
readonly criteria: L;
|
|
19
|
+
};
|
|
20
|
+
type RateDecision<V extends Levels = Levels> = {
|
|
21
|
+
readonly _tag: "Rate";
|
|
22
|
+
readonly instructions: string;
|
|
23
|
+
readonly criteria: V;
|
|
24
|
+
};
|
|
25
|
+
type ProbabilityDecision = {
|
|
26
|
+
readonly _tag: "Probability";
|
|
27
|
+
readonly instructions: string;
|
|
28
|
+
readonly criteria?: Boundary;
|
|
29
|
+
};
|
|
30
|
+
type Decision = ClassifyDecision | RateDecision | ProbabilityDecision;
|
|
31
|
+
/** Pick one label. Use {@link rate} when the options form a spectrum. */
|
|
32
|
+
declare const classify: <const L extends Labels>(decision: {
|
|
33
|
+
readonly instructions: string;
|
|
34
|
+
readonly criteria: L;
|
|
35
|
+
}) => ClassifyDecision<L>;
|
|
36
|
+
/** Place the input on an ordered rubric. */
|
|
37
|
+
declare const rate: <const V extends Levels>(decision: {
|
|
38
|
+
readonly instructions: string;
|
|
39
|
+
readonly criteria: V;
|
|
40
|
+
}) => RateDecision<V>;
|
|
41
|
+
/**
|
|
42
|
+
* How likely a statement is to hold. Named for its answer rather than for an
|
|
43
|
+
* act, unlike its siblings, so nobody reads the result as a boolean.
|
|
44
|
+
*/
|
|
45
|
+
declare const probability: (decision: {
|
|
46
|
+
readonly instructions: string;
|
|
47
|
+
readonly criteria?: Boundary;
|
|
48
|
+
}) => ProbabilityDecision;
|
|
49
|
+
/** Decisions keyed by the name their answer comes back under. */
|
|
50
|
+
type Decisions = {
|
|
51
|
+
readonly [name: string]: Decision;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A reusable decision set bound to the shape it reads.
|
|
55
|
+
*
|
|
56
|
+
* `inputSchema` encodes the input on the way out, so a schema that omits a
|
|
57
|
+
* field keeps it from the model. That projection is the supported way to hold
|
|
58
|
+
* irrelevant or sensitive context out of a request.
|
|
59
|
+
*/
|
|
60
|
+
type Definition<A_, D extends Decisions = Decisions> = {
|
|
61
|
+
readonly inputSchema: Schema.Codec<A_, unknown, any, never>;
|
|
62
|
+
readonly decisions: D;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Build a decision set. Declare it once at module scope: the questions and
|
|
66
|
+
* their rubrics are the reviewed artifact, the input changes per call.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const triage = Decision.make({
|
|
71
|
+
* inputSchema: Schema.Struct({ message: Schema.String }),
|
|
72
|
+
* decisions: {
|
|
73
|
+
* department: Decision.classify({
|
|
74
|
+
* instructions: "Which team should handle this?",
|
|
75
|
+
* criteria: { billing: "Payments, invoicing", technical: "Bugs, outages" },
|
|
76
|
+
* }),
|
|
77
|
+
* },
|
|
78
|
+
* })
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
declare const make: <S extends Schema.Codec<any, any, any, never>, const D extends Decisions>(definition: {
|
|
82
|
+
readonly inputSchema: S;
|
|
83
|
+
readonly decisions: D;
|
|
84
|
+
}) => Definition<S["Type"], D>;
|
|
85
|
+
/**
|
|
86
|
+
* Score contract for {@link ClassifyAnswer.labels} and
|
|
87
|
+
* {@link RateAnswer.levels}: probabilities sum to 1 within floating-point
|
|
88
|
+
* error and are comparable within one answer. Whether they are *calibrated*,
|
|
89
|
+
* so a stated 0.8 means right about 80% of the time, is a property of the
|
|
90
|
+
* Layer and is documented per provider. No implementor promises calibration
|
|
91
|
+
* across requests, across decision kinds, or between a statement and its
|
|
92
|
+
* negation: `P(claim) != 1 - P(not claim)`. A threshold fitted against one
|
|
93
|
+
* model does not transfer to another.
|
|
94
|
+
*/
|
|
95
|
+
type ClassifyAnswer<L extends Labels = Labels> = {
|
|
96
|
+
readonly _tag: "Classify";
|
|
97
|
+
readonly labels: Readonly<Record<keyof L & string, number>>;
|
|
98
|
+
};
|
|
99
|
+
type RateAnswer<V extends Levels = Levels> = {
|
|
100
|
+
readonly _tag: "Rate";
|
|
101
|
+
/** Index-aligned with {@link RateAnswer.legend}. */
|
|
102
|
+
readonly levels: { readonly [K in keyof V]: number; };
|
|
103
|
+
/** The rubric, echoed so an index reads back as prose. */
|
|
104
|
+
readonly legend: Readonly<V>;
|
|
105
|
+
};
|
|
106
|
+
type ProbabilityAnswer = {
|
|
107
|
+
readonly _tag: "Probability";
|
|
108
|
+
readonly probability: number;
|
|
109
|
+
};
|
|
110
|
+
type Answer = ClassifyAnswer | RateAnswer | ProbabilityAnswer;
|
|
111
|
+
/** The answer a decision determines, keeping its labels or rubric. */
|
|
112
|
+
type AnswerFor<D> = D extends {
|
|
113
|
+
readonly _tag: "Classify";
|
|
114
|
+
readonly criteria: infer L extends Labels;
|
|
115
|
+
} ? ClassifyAnswer<L> : D extends {
|
|
116
|
+
readonly _tag: "Rate";
|
|
117
|
+
readonly criteria: infer V extends Levels;
|
|
118
|
+
} ? RateAnswer<V> : ProbabilityAnswer;
|
|
119
|
+
/** Answers keyed by their decision's name. */
|
|
120
|
+
type Answers<D extends Decisions> = { readonly [K in keyof D]: AnswerFor<D[K]>; };
|
|
121
|
+
/**
|
|
122
|
+
* Fail `AiError.Unsupported` naming every decision whose kind this provider
|
|
123
|
+
* cannot express. Bucket 1: omitting an answer would leave a hole where
|
|
124
|
+
* {@link Answers} promises a key.
|
|
125
|
+
*
|
|
126
|
+
* Reports all offenders at once, since the definition is known up front.
|
|
127
|
+
*/
|
|
128
|
+
declare const assertKinds: (decisions: Decisions, supported: ReadonlyArray<Decision["_tag"]>, provider: string) => Effect.Effect<void, AiError>;
|
|
129
|
+
/** A `Probability` answer's implied distribution is `[p, 1 - p]`. */
|
|
130
|
+
declare const probabilitiesOf: (answer: Answer) => ReadonlyArray<number>;
|
|
131
|
+
/** See {@link Probability.confidence}. Defined for every kind. */
|
|
132
|
+
declare const confidence: (answer: Answer) => number;
|
|
133
|
+
/** See {@link Probability.margin}. Defined for every kind. */
|
|
134
|
+
declare const margin: (answer: Answer) => number;
|
|
135
|
+
/**
|
|
136
|
+
* Labels best first. Ties keep first-seen order. Also the bridge to rank
|
|
137
|
+
* fusion: map to `label` and pass alongside another retriever's ordering.
|
|
138
|
+
*/
|
|
139
|
+
declare const ranked: <L extends Labels>(answer: ClassifyAnswer<L>) => ReadonlyArray<{
|
|
140
|
+
readonly label: keyof L & string;
|
|
141
|
+
readonly probability: number;
|
|
142
|
+
}>;
|
|
143
|
+
/** Highest-probability label. `None` only when the label set is empty. */
|
|
144
|
+
declare const winner: <L extends Labels>(answer: ClassifyAnswer<L>) => Option.Option<keyof L & string>;
|
|
145
|
+
/** Index of the highest-probability level. Ties resolve to the lowest. */
|
|
146
|
+
declare const topLevel: (answer: RateAnswer) => number;
|
|
147
|
+
/**
|
|
148
|
+
* Probability-weighted mean of the level indices, so it can land between
|
|
149
|
+
* them. Weakly calibrated in practice: a summary, not a magnitude to
|
|
150
|
+
* interpolate. Use {@link topLevel} with `legend` to name a level instead.
|
|
151
|
+
*/
|
|
152
|
+
declare const expectedLevel: (answer: RateAnswer) => number;
|
|
153
|
+
//#endregion
|
|
154
|
+
export { Answer, AnswerFor, Answers, Boundary, ClassifyAnswer, ClassifyDecision, Decision, Decisions, Definition, Labels, Levels, ProbabilityAnswer, ProbabilityDecision, RateAnswer, RateDecision, assertKinds, classify, confidence, expectedLevel, make, margin, probabilitiesOf, probability, ranked, rate, Decision_d_exports as t, topLevel, winner };
|
|
155
|
+
//# sourceMappingURL=Decision.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Decision.d.mts","names":[],"sources":["../../src/decision-model/Decision.ts"],"mappings":";;;;;;KA4BY,SAAS,SAAS;;KAGlB,sCAAsC;;KAGtC;WAAsB;WAAwB;;KAM9C,iBAAiB,UAAU,SAAS;WACrC;WACA;WACA,UAAU;;KAGT,aAAa,UAAU,SAAS;WACjC;WACA;WACA,UAAU;;KAGT;WACD;WACA;WACA,WAAW;;KAGV,WAAW,mBAAmB,eAAe;;cAG5C,iBAAkB,UAAU,QAAM;WACpC;WACA,UAAU;MACjB,iBAAiB;;cAGR,aAAc,UAAU,QAAM;WAChC;WACA,UAAU;MACjB,aAAa;;;;;cAMJ,cAAW;WACb;WACA,WAAW;MAClB;;KAGQ;YAAwB,eAAe;;;;;;;;;KAavC,WAAW,IAAI,UAAU,YAAY;WACtC,aAAa,OAAO,MAAM;WAC1B,WAAW;;;;;;;;;;;;;;;;;;;cAoBT,OACX,UAAU,OAAO,mCACX,UAAU,WAAS;WAEhB,aAAa;WACb,WAAW;MAClB,WAAW,WAAW;;;;;;;;;;;KAgBd,eAAe,UAAU,SAAS;WACnC;WACA,QAAQ,SAAS,aAAa;;KAG7B,WAAW,UAAU,SAAS;WAC/B;;WAEA,oBAAoB,WAAW;;WAE/B,QAAQ,SAAS;;KAGhB;WACD;WACA;;KAGC,SAAS,iBAAiB,aAAa;;KAGvC,UAAU,KAAK;WAChB;WACA,gBAAgB,UAAU;IAEjC,eAAe,KACf;WAAqB;WAAuB,gBAAgB,UAAU;IACpE,WAAW,KACX;;KAGM,QAAQ,UAAU,yBAAyB,WAAW,IAAI,UAAU,EAAE;;;;;;;;cAarE,cAAW,WACX,WAAS,WACT,cAAc,mBAAiB,qBAEzC,OAAO,aAAa;;cAsBV,kBAAkB,QAAQ,WAAW;;cASrC,aAAU,QAAY;;cAItB,SAAM,QAAY;;;;;cAWlB,SAAU,UAAU,QAAM,QAC7B,eAAe,OACtB;WAAyB,aAAa;WAAqB;;;cAQjD,SAAU,UAAU,QAAM,QAC7B,eAAe,OACtB,OAAO,aAAa;;cAQV,WAAQ,QAAY;;;;;;cAcpB,gBAAa,QAAY"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
|
+
import { Unsupported } from "../domain/AiError.mjs";
|
|
3
|
+
import { confidence as confidence$1, margin as margin$1 } from "../math/Probability.mjs";
|
|
4
|
+
import { Array, Effect, Match, Number, Option, Order, Record, pipe } from "effect";
|
|
5
|
+
//#region src/decision-model/Decision.ts
|
|
6
|
+
/**
|
|
7
|
+
* Typed questions about one input, answered with probabilities.
|
|
8
|
+
*
|
|
9
|
+
* A {@link Definition} pairs the shape being read with the questions asked of
|
|
10
|
+
* it. Build one at module scope and apply it per call with
|
|
11
|
+
* `DecisionModel.decide`.
|
|
12
|
+
*
|
|
13
|
+
* @experimental The model class this abstracts is new and the shape may change.
|
|
14
|
+
*/
|
|
15
|
+
var Decision_exports = /* @__PURE__ */ __exportAll({
|
|
16
|
+
assertKinds: () => assertKinds,
|
|
17
|
+
classify: () => classify,
|
|
18
|
+
confidence: () => confidence,
|
|
19
|
+
expectedLevel: () => expectedLevel,
|
|
20
|
+
make: () => make,
|
|
21
|
+
margin: () => margin,
|
|
22
|
+
probabilitiesOf: () => probabilitiesOf,
|
|
23
|
+
probability: () => probability,
|
|
24
|
+
ranked: () => ranked,
|
|
25
|
+
rate: () => rate,
|
|
26
|
+
topLevel: () => topLevel,
|
|
27
|
+
winner: () => winner
|
|
28
|
+
});
|
|
29
|
+
/** Pick one label. Use {@link rate} when the options form a spectrum. */
|
|
30
|
+
const classify = (decision) => ({
|
|
31
|
+
_tag: "Classify",
|
|
32
|
+
...decision
|
|
33
|
+
});
|
|
34
|
+
/** Place the input on an ordered rubric. */
|
|
35
|
+
const rate = (decision) => ({
|
|
36
|
+
_tag: "Rate",
|
|
37
|
+
...decision
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* How likely a statement is to hold. Named for its answer rather than for an
|
|
41
|
+
* act, unlike its siblings, so nobody reads the result as a boolean.
|
|
42
|
+
*/
|
|
43
|
+
const probability = (decision) => ({
|
|
44
|
+
_tag: "Probability",
|
|
45
|
+
...decision
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Build a decision set. Declare it once at module scope: the questions and
|
|
49
|
+
* their rubrics are the reviewed artifact, the input changes per call.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const triage = Decision.make({
|
|
54
|
+
* inputSchema: Schema.Struct({ message: Schema.String }),
|
|
55
|
+
* decisions: {
|
|
56
|
+
* department: Decision.classify({
|
|
57
|
+
* instructions: "Which team should handle this?",
|
|
58
|
+
* criteria: { billing: "Payments, invoicing", technical: "Bugs, outages" },
|
|
59
|
+
* }),
|
|
60
|
+
* },
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
const make = (definition) => definition;
|
|
65
|
+
/**
|
|
66
|
+
* Fail `AiError.Unsupported` naming every decision whose kind this provider
|
|
67
|
+
* cannot express. Bucket 1: omitting an answer would leave a hole where
|
|
68
|
+
* {@link Answers} promises a key.
|
|
69
|
+
*
|
|
70
|
+
* Reports all offenders at once, since the definition is known up front.
|
|
71
|
+
*/
|
|
72
|
+
const assertKinds = (decisions, supported, provider) => {
|
|
73
|
+
const offenders = pipe(Record.toEntries(decisions), Array.filter(([, decision]) => !supported.includes(decision._tag)), Array.map(([name, decision]) => `"${name}" is a ${decision._tag}`));
|
|
74
|
+
return offenders.length === 0 ? Effect.void : Effect.fail(new Unsupported({
|
|
75
|
+
provider,
|
|
76
|
+
capability: "decision",
|
|
77
|
+
reason: `${provider} answers ${supported.join(" / ")} only; ${offenders.join(", ")}.`
|
|
78
|
+
}));
|
|
79
|
+
};
|
|
80
|
+
/** A `Probability` answer's implied distribution is `[p, 1 - p]`. */
|
|
81
|
+
const probabilitiesOf = Match.type().pipe(Match.discriminatorsExhaustive("_tag")({
|
|
82
|
+
Classify: (answer) => Record.values(answer.labels),
|
|
83
|
+
Rate: (answer) => answer.levels,
|
|
84
|
+
Probability: (answer) => [answer.probability, 1 - answer.probability]
|
|
85
|
+
}));
|
|
86
|
+
/** See {@link Probability.confidence}. Defined for every kind. */
|
|
87
|
+
const confidence = (answer) => confidence$1(probabilitiesOf(answer));
|
|
88
|
+
/** See {@link Probability.margin}. Defined for every kind. */
|
|
89
|
+
const margin = (answer) => margin$1(probabilitiesOf(answer));
|
|
90
|
+
const byProbability = Order.mapInput(Order.flip(Order.Number), (outcome) => outcome.probability);
|
|
91
|
+
/**
|
|
92
|
+
* Labels best first. Ties keep first-seen order. Also the bridge to rank
|
|
93
|
+
* fusion: map to `label` and pass alongside another retriever's ordering.
|
|
94
|
+
*/
|
|
95
|
+
const ranked = (answer) => pipe(Record.toEntries(answer.labels), Array.map(([label, probability]) => ({
|
|
96
|
+
label,
|
|
97
|
+
probability
|
|
98
|
+
})), Array.sort(byProbability));
|
|
99
|
+
/** Highest-probability label. `None` only when the label set is empty. */
|
|
100
|
+
const winner = (answer) => pipe(ranked(answer), Array.head, Option.map((outcome) => outcome.label));
|
|
101
|
+
/** Index of the highest-probability level. Ties resolve to the lowest. */
|
|
102
|
+
const topLevel = (answer) => pipe(answer.levels, Array.reduce({
|
|
103
|
+
index: 0,
|
|
104
|
+
probability: -Infinity
|
|
105
|
+
}, (best, probability, index) => probability > best.probability ? {
|
|
106
|
+
index,
|
|
107
|
+
probability
|
|
108
|
+
} : best), (best) => best.index);
|
|
109
|
+
/**
|
|
110
|
+
* Probability-weighted mean of the level indices, so it can land between
|
|
111
|
+
* them. Weakly calibrated in practice: a summary, not a magnitude to
|
|
112
|
+
* interpolate. Use {@link topLevel} with `legend` to name a level instead.
|
|
113
|
+
*/
|
|
114
|
+
const expectedLevel = (answer) => pipe(answer.levels, Array.map((probability, index) => index * probability), Number.sumAll);
|
|
115
|
+
//#endregion
|
|
116
|
+
export { assertKinds, classify, confidence, expectedLevel, make, margin, probabilitiesOf, probability, ranked, rate, Decision_exports as t, topLevel, winner };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=Decision.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Decision.mjs","names":["A","AiError.Unsupported","Probability.confidence","Probability.margin","N"],"sources":["../../src/decision-model/Decision.ts"],"sourcesContent":["/**\n * Typed questions about one input, answered with probabilities.\n *\n * A {@link Definition} pairs the shape being read with the questions asked of\n * it. Build one at module scope and apply it per call with\n * `DecisionModel.decide`.\n *\n * @experimental The model class this abstracts is new and the shape may change.\n */\nimport {\n Array as A,\n Effect,\n Match,\n Number as N,\n Option,\n Order,\n Record,\n type Schema,\n pipe,\n} from \"effect\"\nimport * as AiError from \"../domain/AiError.js\"\nimport * as Probability from \"../math/Probability.js\"\n\n// ---------------------------------------------------------------------------\n// Criteria\n// ---------------------------------------------------------------------------\n\n/** Labels mapped to what each one means. */\nexport type Labels = Readonly<Record<string, string>>\n\n/** An ordered rubric, lowest level first. */\nexport type Levels = readonly [string, string, ...ReadonlyArray<string>]\n\n/** What a true and a false answer mean, when the boundary is subtle. */\nexport type Boundary = { readonly true?: string; readonly false?: string }\n\n// ---------------------------------------------------------------------------\n// Decisions\n// ---------------------------------------------------------------------------\n\nexport type ClassifyDecision<L extends Labels = Labels> = {\n readonly _tag: \"Classify\"\n readonly instructions: string\n readonly criteria: L\n}\n\nexport type RateDecision<V extends Levels = Levels> = {\n readonly _tag: \"Rate\"\n readonly instructions: string\n readonly criteria: V\n}\n\nexport type ProbabilityDecision = {\n readonly _tag: \"Probability\"\n readonly instructions: string\n readonly criteria?: Boundary\n}\n\nexport type Decision = ClassifyDecision | RateDecision | ProbabilityDecision\n\n/** Pick one label. Use {@link rate} when the options form a spectrum. */\nexport const classify = <const L extends Labels>(decision: {\n readonly instructions: string\n readonly criteria: L\n}): ClassifyDecision<L> => ({ _tag: \"Classify\", ...decision })\n\n/** Place the input on an ordered rubric. */\nexport const rate = <const V extends Levels>(decision: {\n readonly instructions: string\n readonly criteria: V\n}): RateDecision<V> => ({ _tag: \"Rate\", ...decision })\n\n/**\n * How likely a statement is to hold. Named for its answer rather than for an\n * act, unlike its siblings, so nobody reads the result as a boolean.\n */\nexport const probability = (decision: {\n readonly instructions: string\n readonly criteria?: Boundary\n}): ProbabilityDecision => ({ _tag: \"Probability\", ...decision })\n\n/** Decisions keyed by the name their answer comes back under. */\nexport type Decisions = { readonly [name: string]: Decision }\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\n/**\n * A reusable decision set bound to the shape it reads.\n *\n * `inputSchema` encodes the input on the way out, so a schema that omits a\n * field keeps it from the model. That projection is the supported way to hold\n * irrelevant or sensitive context out of a request.\n */\nexport type Definition<A_, D extends Decisions = Decisions> = {\n readonly inputSchema: Schema.Codec<A_, unknown, any, never>\n readonly decisions: D\n}\n\n/**\n * Build a decision set. Declare it once at module scope: the questions and\n * their rubrics are the reviewed artifact, the input changes per call.\n *\n * @example\n * ```ts\n * const triage = Decision.make({\n * inputSchema: Schema.Struct({ message: Schema.String }),\n * decisions: {\n * department: Decision.classify({\n * instructions: \"Which team should handle this?\",\n * criteria: { billing: \"Payments, invoicing\", technical: \"Bugs, outages\" },\n * }),\n * },\n * })\n * ```\n */\nexport const make = <\n S extends Schema.Codec<any, any, any, never>,\n const D extends Decisions,\n>(definition: {\n readonly inputSchema: S\n readonly decisions: D\n}): Definition<S[\"Type\"], D> => definition\n\n// ---------------------------------------------------------------------------\n// Answers\n// ---------------------------------------------------------------------------\n\n/**\n * Score contract for {@link ClassifyAnswer.labels} and\n * {@link RateAnswer.levels}: probabilities sum to 1 within floating-point\n * error and are comparable within one answer. Whether they are *calibrated*,\n * so a stated 0.8 means right about 80% of the time, is a property of the\n * Layer and is documented per provider. No implementor promises calibration\n * across requests, across decision kinds, or between a statement and its\n * negation: `P(claim) != 1 - P(not claim)`. A threshold fitted against one\n * model does not transfer to another.\n */\nexport type ClassifyAnswer<L extends Labels = Labels> = {\n readonly _tag: \"Classify\"\n readonly labels: Readonly<Record<keyof L & string, number>>\n}\n\nexport type RateAnswer<V extends Levels = Levels> = {\n readonly _tag: \"Rate\"\n /** Index-aligned with {@link RateAnswer.legend}. */\n readonly levels: { readonly [K in keyof V]: number }\n /** The rubric, echoed so an index reads back as prose. */\n readonly legend: Readonly<V>\n}\n\nexport type ProbabilityAnswer = {\n readonly _tag: \"Probability\"\n readonly probability: number\n}\n\nexport type Answer = ClassifyAnswer | RateAnswer | ProbabilityAnswer\n\n/** The answer a decision determines, keeping its labels or rubric. */\nexport type AnswerFor<D> = D extends {\n readonly _tag: \"Classify\"\n readonly criteria: infer L extends Labels\n}\n ? ClassifyAnswer<L>\n : D extends { readonly _tag: \"Rate\"; readonly criteria: infer V extends Levels }\n ? RateAnswer<V>\n : ProbabilityAnswer\n\n/** Answers keyed by their decision's name. */\nexport type Answers<D extends Decisions> = { readonly [K in keyof D]: AnswerFor<D[K]> }\n\n// ---------------------------------------------------------------------------\n// Guards\n// ---------------------------------------------------------------------------\n\n/**\n * Fail `AiError.Unsupported` naming every decision whose kind this provider\n * cannot express. Bucket 1: omitting an answer would leave a hole where\n * {@link Answers} promises a key.\n *\n * Reports all offenders at once, since the definition is known up front.\n */\nexport const assertKinds = (\n decisions: Decisions,\n supported: ReadonlyArray<Decision[\"_tag\"]>,\n provider: string,\n): Effect.Effect<void, AiError.AiError> => {\n const offenders = pipe(\n Record.toEntries(decisions),\n A.filter(([, decision]) => !supported.includes(decision._tag)),\n A.map(([name, decision]) => `\"${name}\" is a ${decision._tag}`),\n )\n return offenders.length === 0\n ? Effect.void\n : Effect.fail(\n new AiError.Unsupported({\n provider,\n capability: \"decision\",\n reason: `${provider} answers ${supported.join(\" / \")} only; ${offenders.join(\", \")}.`,\n }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// Reading an answer\n// ---------------------------------------------------------------------------\n\n/** A `Probability` answer's implied distribution is `[p, 1 - p]`. */\nexport const probabilitiesOf: (answer: Answer) => ReadonlyArray<number> = Match.type<Answer>().pipe(\n Match.discriminatorsExhaustive(\"_tag\")({\n Classify: (answer) => Record.values(answer.labels),\n Rate: (answer) => answer.levels,\n Probability: (answer) => [answer.probability, 1 - answer.probability],\n }),\n)\n\n/** See {@link Probability.confidence}. Defined for every kind. */\nexport const confidence = (answer: Answer): number =>\n Probability.confidence(probabilitiesOf(answer))\n\n/** See {@link Probability.margin}. Defined for every kind. */\nexport const margin = (answer: Answer): number => Probability.margin(probabilitiesOf(answer))\n\nconst byProbability = Order.mapInput(\n Order.flip(Order.Number),\n (outcome: { readonly probability: number }) => outcome.probability,\n)\n\n/**\n * Labels best first. Ties keep first-seen order. Also the bridge to rank\n * fusion: map to `label` and pass alongside another retriever's ordering.\n */\nexport const ranked = <L extends Labels>(\n answer: ClassifyAnswer<L>,\n): ReadonlyArray<{ readonly label: keyof L & string; readonly probability: number }> =>\n pipe(\n Record.toEntries(answer.labels),\n A.map(([label, probability]) => ({ label, probability })),\n A.sort(byProbability),\n )\n\n/** Highest-probability label. `None` only when the label set is empty. */\nexport const winner = <L extends Labels>(\n answer: ClassifyAnswer<L>,\n): Option.Option<keyof L & string> =>\n pipe(\n ranked(answer),\n A.head,\n Option.map((outcome) => outcome.label),\n )\n\n/** Index of the highest-probability level. Ties resolve to the lowest. */\nexport const topLevel = (answer: RateAnswer): number =>\n pipe(\n answer.levels as ReadonlyArray<number>,\n A.reduce({ index: 0, probability: -Infinity }, (best, probability, index) =>\n probability > best.probability ? { index, probability } : best,\n ),\n (best) => best.index,\n )\n\n/**\n * Probability-weighted mean of the level indices, so it can land between\n * them. Weakly calibrated in practice: a summary, not a magnitude to\n * interpolate. Use {@link topLevel} with `legend` to name a level instead.\n */\nexport const expectedLevel = (answer: RateAnswer): number =>\n pipe(\n answer.levels as ReadonlyArray<number>,\n A.map((probability, index) => index * probability),\n N.sumAll,\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,MAAa,YAAoC,cAGrB;CAAE,MAAM;CAAY,GAAG;AAAS;;AAG5D,MAAa,QAAgC,cAGrB;CAAE,MAAM;CAAQ,GAAG;AAAS;;;;;AAMpD,MAAa,eAAe,cAGA;CAAE,MAAM;CAAe,GAAG;AAAS;;;;;;;;;;;;;;;;;;AAsC/D,MAAa,QAGX,eAG8B;;;;;;;;AA4DhC,MAAa,eACX,WACA,WACA,aACyC;CACzC,MAAM,YAAY,KAChB,OAAO,UAAU,SAAS,GAC1BA,MAAE,QAAQ,GAAG,cAAc,CAAC,UAAU,SAAS,SAAS,IAAI,CAAC,GAC7DA,MAAE,KAAK,CAAC,MAAM,cAAc,IAAI,KAAK,SAAS,SAAS,MAAM,CAC/D;CACA,OAAO,UAAU,WAAW,IACxB,OAAO,OACP,OAAO,KACL,IAAIC,YAAoB;EACtB;EACA,YAAY;EACZ,QAAQ,GAAG,SAAS,WAAW,UAAU,KAAK,KAAK,EAAE,SAAS,UAAU,KAAK,IAAI,EAAE;CACrF,CAAC,CACH;AACN;;AAOA,MAAa,kBAA6D,MAAM,KAAa,CAAC,CAAC,KAC7F,MAAM,yBAAyB,MAAM,CAAC,CAAC;CACrC,WAAW,WAAW,OAAO,OAAO,OAAO,MAAM;CACjD,OAAO,WAAW,OAAO;CACzB,cAAc,WAAW,CAAC,OAAO,aAAa,IAAI,OAAO,WAAW;AACtE,CAAC,CACH;;AAGA,MAAa,cAAc,WACzBC,aAAuB,gBAAgB,MAAM,CAAC;;AAGhD,MAAa,UAAU,WAA2BC,SAAmB,gBAAgB,MAAM,CAAC;AAE5F,MAAM,gBAAgB,MAAM,SAC1B,MAAM,KAAK,MAAM,MAAM,IACtB,YAA8C,QAAQ,WACzD;;;;;AAMA,MAAa,UACX,WAEA,KACE,OAAO,UAAU,OAAO,MAAM,GAC9BH,MAAE,KAAK,CAAC,OAAO,kBAAkB;CAAE;CAAO;AAAY,EAAE,GACxDA,MAAE,KAAK,aAAa,CACtB;;AAGF,MAAa,UACX,WAEA,KACE,OAAO,MAAM,GACbA,MAAE,MACF,OAAO,KAAK,YAAY,QAAQ,KAAK,CACvC;;AAGF,MAAa,YAAY,WACvB,KACE,OAAO,QACPA,MAAE,OAAO;CAAE,OAAO;CAAG,aAAa;AAAU,IAAI,MAAM,aAAa,UACjE,cAAc,KAAK,cAAc;CAAE;CAAO;AAAY,IAAI,IAC5D,IACC,SAAS,KAAK,KACjB;;;;;;AAOF,MAAa,iBAAiB,WAC5B,KACE,OAAO,QACPA,MAAE,KAAK,aAAa,UAAU,QAAQ,WAAW,GACjDI,OAAE,MACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Unsupported } from "../domain/AiError.mjs";
|
|
2
|
+
import { assertKinds, classify, confidence, expectedLevel, make, margin, probabilitiesOf, probability, ranked, rate, topLevel, winner } from "./Decision.mjs";
|
|
3
|
+
import { DecisionModel, decide } from "./DecisionModel.mjs";
|
|
4
|
+
import { m as describe } from "../dist-DzeSyeYF.mjs";
|
|
5
|
+
import { n as globalExpect } from "../index.C4tKZ0yW-LuFnBtmu.mjs";
|
|
6
|
+
import { t as import_dist } from "../dist-DNeuh84d.mjs";
|
|
7
|
+
import { t as it } from "../dist-C8RmtoR8.mjs";
|
|
8
|
+
import { Effect, Layer, Option, Ref, Schema } from "effect";
|
|
9
|
+
//#region src/decision-model/Decision.test.ts
|
|
10
|
+
const triage = make({
|
|
11
|
+
inputSchema: Schema.Struct({ message: Schema.String }),
|
|
12
|
+
decisions: {
|
|
13
|
+
department: classify({
|
|
14
|
+
instructions: "Which team should handle this?",
|
|
15
|
+
criteria: {
|
|
16
|
+
billing: "Payments, invoicing",
|
|
17
|
+
technical: "Bugs, outages"
|
|
18
|
+
}
|
|
19
|
+
}),
|
|
20
|
+
severity: rate({
|
|
21
|
+
instructions: "How severe is it?",
|
|
22
|
+
criteria: [
|
|
23
|
+
"Cosmetic",
|
|
24
|
+
"Degraded",
|
|
25
|
+
"Blocking"
|
|
26
|
+
]
|
|
27
|
+
}),
|
|
28
|
+
urgent: probability({ instructions: "Does this convey urgency?" })
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const fixture = {
|
|
32
|
+
department: {
|
|
33
|
+
_tag: "Classify",
|
|
34
|
+
labels: {
|
|
35
|
+
billing: .62,
|
|
36
|
+
technical: .38
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
severity: {
|
|
40
|
+
_tag: "Rate",
|
|
41
|
+
levels: [
|
|
42
|
+
0,
|
|
43
|
+
.7,
|
|
44
|
+
.3
|
|
45
|
+
],
|
|
46
|
+
legend: [
|
|
47
|
+
"Cosmetic",
|
|
48
|
+
"Degraded",
|
|
49
|
+
"Blocking"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
urgent: {
|
|
53
|
+
_tag: "Probability",
|
|
54
|
+
probability: .94
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A fixed fixture cannot prove it satisfies the caller's `Answers<D>`. This is
|
|
59
|
+
* the boundary a provider casts at too, once it has decoded the wire.
|
|
60
|
+
*/
|
|
61
|
+
const fixed = (answers) => answers;
|
|
62
|
+
/** A `DecisionModel` layer that records what reached it. */
|
|
63
|
+
const recording = Effect.gen(function* () {
|
|
64
|
+
const calls = yield* Ref.make([]);
|
|
65
|
+
return {
|
|
66
|
+
calls,
|
|
67
|
+
layer: Layer.succeed(DecisionModel, { decide: (_definition, request) => Effect.as(Ref.update(calls, (seen) => [...seen, {
|
|
68
|
+
input: request.input,
|
|
69
|
+
model: request.model
|
|
70
|
+
}]), {
|
|
71
|
+
answers: fixed(fixture),
|
|
72
|
+
usage: {}
|
|
73
|
+
}) })
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
describe("the answer map is derived from the definition", () => {
|
|
77
|
+
it.effect("keeps each decision's label keys and rejects labels it never had", () => Effect.gen(function* () {
|
|
78
|
+
const { calls, layer } = yield* recording;
|
|
79
|
+
const result = yield* decide(triage, {
|
|
80
|
+
model: "jev-latest",
|
|
81
|
+
input: { message: "hi" }
|
|
82
|
+
}).pipe(Effect.provide(layer));
|
|
83
|
+
(0, import_dist.expectTypeOf)(result.answers.department.labels).toEqualTypeOf();
|
|
84
|
+
result.answers.department.labels.shipping;
|
|
85
|
+
result.answers.urgent.labels;
|
|
86
|
+
globalExpect(yield* Ref.get(calls)).toEqual([{
|
|
87
|
+
input: { message: "hi" },
|
|
88
|
+
model: "jev-latest"
|
|
89
|
+
}]);
|
|
90
|
+
}));
|
|
91
|
+
it("keeps the rubric as a tuple, so levels are positional rather than parsed", () => {
|
|
92
|
+
(0, import_dist.expectTypeOf)().toEqualTypeOf();
|
|
93
|
+
(0, import_dist.expectTypeOf)().toEqualTypeOf();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("reading an answer", () => {
|
|
97
|
+
it("derives a Probability answer's distribution as [p, 1 - p]", () => {
|
|
98
|
+
const [yes, no] = probabilitiesOf(fixture.urgent);
|
|
99
|
+
globalExpect(yes).toBeCloseTo(.94, 10);
|
|
100
|
+
globalExpect(no).toBeCloseTo(.06, 10);
|
|
101
|
+
});
|
|
102
|
+
it("ranks labels best first and names the winner", () => {
|
|
103
|
+
globalExpect(ranked(fixture.department)).toEqual([{
|
|
104
|
+
label: "billing",
|
|
105
|
+
probability: .62
|
|
106
|
+
}, {
|
|
107
|
+
label: "technical",
|
|
108
|
+
probability: .38
|
|
109
|
+
}]);
|
|
110
|
+
globalExpect(winner(fixture.department)).toStrictEqual(Option.some("billing"));
|
|
111
|
+
});
|
|
112
|
+
it("has no winner for an empty label set", () => {
|
|
113
|
+
globalExpect(winner({
|
|
114
|
+
_tag: "Classify",
|
|
115
|
+
labels: {}
|
|
116
|
+
})).toStrictEqual(Option.none());
|
|
117
|
+
});
|
|
118
|
+
it("reads a rubric by index and by expected value", () => {
|
|
119
|
+
globalExpect(topLevel(fixture.severity)).toBe(1);
|
|
120
|
+
globalExpect(expectedLevel(fixture.severity)).toBeCloseTo(1.3, 10);
|
|
121
|
+
globalExpect(fixture.severity.legend[topLevel(fixture.severity)]).toBe("Degraded");
|
|
122
|
+
});
|
|
123
|
+
it("resolves a level tie to the lowest index", () => {
|
|
124
|
+
globalExpect(topLevel({
|
|
125
|
+
_tag: "Rate",
|
|
126
|
+
levels: [.5, .5],
|
|
127
|
+
legend: ["a", "b"]
|
|
128
|
+
})).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
it("computes certainty for every kind, including Probability", () => {
|
|
131
|
+
globalExpect(confidence(fixture.urgent)).toBeCloseTo(.67, 2);
|
|
132
|
+
globalExpect(margin(fixture.urgent)).toBeCloseTo(.88, 10);
|
|
133
|
+
globalExpect(margin(fixture.department)).toBeCloseTo(.24, 10);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe("assertKinds", () => {
|
|
137
|
+
const supported = ["Classify", "Probability"];
|
|
138
|
+
it.effect("passes when every kind is supported", () => {
|
|
139
|
+
const { department, urgent } = triage.decisions;
|
|
140
|
+
return assertKinds({
|
|
141
|
+
department,
|
|
142
|
+
urgent
|
|
143
|
+
}, supported, "nli");
|
|
144
|
+
});
|
|
145
|
+
it.effect("fails Unsupported and names every offender at once", () => Effect.gen(function* () {
|
|
146
|
+
const decisions = {
|
|
147
|
+
...triage.decisions,
|
|
148
|
+
impact: rate({
|
|
149
|
+
instructions: "How bad?",
|
|
150
|
+
criteria: ["low", "high"]
|
|
151
|
+
})
|
|
152
|
+
};
|
|
153
|
+
const error = yield* Effect.flip(assertKinds(decisions, supported, "nli"));
|
|
154
|
+
globalExpect(error).toBeInstanceOf(Unsupported);
|
|
155
|
+
if (error._tag !== "Unsupported") return;
|
|
156
|
+
globalExpect(error.reason).toContain("\"severity\" is a Rate");
|
|
157
|
+
globalExpect(error.reason).toContain("\"impact\" is a Rate");
|
|
158
|
+
globalExpect(error.reason).not.toContain("department");
|
|
159
|
+
}));
|
|
160
|
+
});
|
|
161
|
+
//#endregion
|
|
162
|
+
export {};
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=Decision.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Decision.test.mjs","names":["Decision.make","Decision.classify","Decision.rate","Decision.probability","Decision.probabilitiesOf","Decision.ranked","Decision.winner","Decision.topLevel","Decision.expectedLevel","Decision.confidence","Decision.margin","Decision.assertKinds","AiError.Unsupported"],"sources":["../../src/decision-model/Decision.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport { Effect, Layer, Option, Ref, Schema } from \"effect\"\nimport { expect, expectTypeOf } from \"vitest\"\nimport * as AiError from \"../domain/AiError.js\"\nimport * as Decision from \"./Decision.js\"\nimport { decide, DecisionModel, type DecisionModelService } from \"./DecisionModel.js\"\n\nconst triage = Decision.make({\n inputSchema: Schema.Struct({ message: Schema.String }),\n decisions: {\n department: Decision.classify({\n instructions: \"Which team should handle this?\",\n criteria: { billing: \"Payments, invoicing\", technical: \"Bugs, outages\" },\n }),\n severity: Decision.rate({\n instructions: \"How severe is it?\",\n criteria: [\"Cosmetic\", \"Degraded\", \"Blocking\"],\n }),\n urgent: Decision.probability({ instructions: \"Does this convey urgency?\" }),\n },\n})\n\nconst fixture = {\n department: { _tag: \"Classify\", labels: { billing: 0.62, technical: 0.38 } },\n severity: { _tag: \"Rate\", levels: [0, 0.7, 0.3], legend: [\"Cosmetic\", \"Degraded\", \"Blocking\"] },\n urgent: { _tag: \"Probability\", probability: 0.94 },\n} as const\n\n/**\n * A fixed fixture cannot prove it satisfies the caller's `Answers<D>`. This is\n * the boundary a provider casts at too, once it has decoded the wire.\n */\nconst fixed = <D extends Decision.Decisions>(\n answers: Readonly<Record<string, Decision.Answer>>,\n): Decision.Answers<D> => answers as Decision.Answers<D>\n\ntype Call = { readonly input: unknown; readonly model: string }\n\n/** A `DecisionModel` layer that records what reached it. */\nconst recording = Effect.gen(function* () {\n const calls = yield* Ref.make<ReadonlyArray<Call>>([])\n const service: DecisionModelService = {\n decide: (_definition, request) =>\n Effect.as(\n Ref.update(calls, (seen) => [...seen, { input: request.input, model: request.model }]),\n { answers: fixed(fixture), usage: {} },\n ),\n }\n return { calls, layer: Layer.succeed(DecisionModel, service) }\n})\n\ndescribe(\"the answer map is derived from the definition\", () => {\n it.effect(\"keeps each decision's label keys and rejects labels it never had\", () =>\n Effect.gen(function* () {\n const { calls, layer } = yield* recording\n const result = yield* decide(triage, {\n model: \"jev-latest\",\n input: { message: \"hi\" },\n }).pipe(Effect.provide(layer))\n\n expectTypeOf(result.answers.department.labels).toEqualTypeOf<{\n readonly billing: number\n readonly technical: number\n }>()\n // @ts-expect-error shipping is not a label of this decision\n result.answers.department.labels.shipping\n // @ts-expect-error a Probability answer has no labels\n result.answers.urgent.labels\n\n // `decide` forwards both arguments rather than dropping either.\n expect(yield* Ref.get(calls)).toEqual([{ input: { message: \"hi\" }, model: \"jev-latest\" }])\n }),\n )\n\n it(\"keeps the rubric as a tuple, so levels are positional rather than parsed\", () => {\n type Severity = (typeof triage)[\"decisions\"][\"severity\"]\n expectTypeOf<Decision.AnswerFor<Severity>[\"levels\"]>().toEqualTypeOf<\n readonly [number, number, number]\n >()\n expectTypeOf<Decision.AnswerFor<Severity>[\"legend\"]>().toEqualTypeOf<\n readonly [\"Cosmetic\", \"Degraded\", \"Blocking\"]\n >()\n })\n})\n\ndescribe(\"reading an answer\", () => {\n it(\"derives a Probability answer's distribution as [p, 1 - p]\", () => {\n const [yes, no] = Decision.probabilitiesOf(fixture.urgent)\n expect(yes).toBeCloseTo(0.94, 10)\n expect(no).toBeCloseTo(0.06, 10)\n })\n\n it(\"ranks labels best first and names the winner\", () => {\n expect(Decision.ranked(fixture.department)).toEqual([\n { label: \"billing\", probability: 0.62 },\n { label: \"technical\", probability: 0.38 },\n ])\n expect(Decision.winner(fixture.department)).toStrictEqual(Option.some(\"billing\"))\n })\n\n it(\"has no winner for an empty label set\", () => {\n expect(Decision.winner({ _tag: \"Classify\", labels: {} })).toStrictEqual(Option.none())\n })\n\n it(\"reads a rubric by index and by expected value\", () => {\n expect(Decision.topLevel(fixture.severity)).toBe(1)\n expect(Decision.expectedLevel(fixture.severity)).toBeCloseTo(1.3, 10)\n expect(fixture.severity.legend[Decision.topLevel(fixture.severity)]).toBe(\"Degraded\")\n })\n\n it(\"resolves a level tie to the lowest index\", () => {\n expect(Decision.topLevel({ _tag: \"Rate\", levels: [0.5, 0.5], legend: [\"a\", \"b\"] })).toBe(0)\n })\n\n it(\"computes certainty for every kind, including Probability\", () => {\n expect(Decision.confidence(fixture.urgent)).toBeCloseTo(0.67, 2)\n expect(Decision.margin(fixture.urgent)).toBeCloseTo(0.88, 10)\n expect(Decision.margin(fixture.department)).toBeCloseTo(0.24, 10)\n })\n})\n\ndescribe(\"assertKinds\", () => {\n const supported = [\"Classify\", \"Probability\"] as const\n\n it.effect(\"passes when every kind is supported\", () => {\n const { department, urgent } = triage.decisions\n return Decision.assertKinds({ department, urgent }, supported, \"nli\")\n })\n\n it.effect(\"fails Unsupported and names every offender at once\", () =>\n Effect.gen(function* () {\n const decisions = {\n ...triage.decisions,\n impact: Decision.rate({ instructions: \"How bad?\", criteria: [\"low\", \"high\"] }),\n }\n const error = yield* Effect.flip(Decision.assertKinds(decisions, supported, \"nli\"))\n\n expect(error).toBeInstanceOf(AiError.Unsupported)\n if (error._tag !== \"Unsupported\") return\n\n expect(error.reason).toContain('\"severity\" is a Rate')\n expect(error.reason).toContain('\"impact\" is a Rate')\n expect(error.reason).not.toContain(\"department\")\n }),\n )\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { t as AiError } from "../AiError-DvhLIbmd.mjs";
|
|
2
|
+
import { Answers, Decisions, Definition } from "./Decision.mjs";
|
|
3
|
+
import { Context, Effect } from "effect";
|
|
4
|
+
declare namespace DecisionModel_d_exports {
|
|
5
|
+
export { CommonDecideRequest, DecideResponse, DecideUsage, DecisionModel, DecisionModelService, decide };
|
|
6
|
+
}
|
|
7
|
+
/** Optional throughout: not every backing reports token counts. */
|
|
8
|
+
type DecideUsage = {
|
|
9
|
+
readonly inputTokens?: number;
|
|
10
|
+
readonly outputTokens?: number;
|
|
11
|
+
};
|
|
12
|
+
type DecideResponse<D extends Decisions = Decisions> = {
|
|
13
|
+
readonly answers: Answers<D>;
|
|
14
|
+
readonly usage: DecideUsage;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Cross-provider decide request. The input is billed once for the whole
|
|
18
|
+
* definition, so batching decisions is cheaper than repeating the input.
|
|
19
|
+
*/
|
|
20
|
+
type CommonDecideRequest<A> = {
|
|
21
|
+
readonly input: A;
|
|
22
|
+
/** Each provider narrows this to its typed literal union. */
|
|
23
|
+
readonly model: string;
|
|
24
|
+
};
|
|
25
|
+
type DecisionModelService = {
|
|
26
|
+
readonly decide: <A, D extends Decisions>(definition: Definition<A, D>, request: CommonDecideRequest<A>) => Effect.Effect<DecideResponse<D>, AiError>;
|
|
27
|
+
};
|
|
28
|
+
declare const DecisionModel_base: Context.ServiceClass<DecisionModel, "@betalyra/effect-uai/DecisionModel", DecisionModelService>;
|
|
29
|
+
/**
|
|
30
|
+
* Answering typed questions about an input with probabilities instead of
|
|
31
|
+
* generating text. The model reports beliefs; the decision stays in the
|
|
32
|
+
* caller's code.
|
|
33
|
+
*
|
|
34
|
+
* Implementor contract: answer every decision or fail. Omitting one leaves a
|
|
35
|
+
* hole where {@link DecideResponse} promises a key, so a kind the backing
|
|
36
|
+
* cannot express is rejected up front with `Decision.assertKinds`.
|
|
37
|
+
*
|
|
38
|
+
* @experimental The model class this abstracts is new and the shape may change.
|
|
39
|
+
*/
|
|
40
|
+
declare class DecisionModel extends DecisionModel_base {}
|
|
41
|
+
/** Answer a definition's decisions about one input in one call. */
|
|
42
|
+
declare const decide: <A, D extends Decisions>(definition: Definition<A, D>, request: CommonDecideRequest<A>) => Effect.Effect<DecideResponse<D>, AiError, DecisionModel>;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { CommonDecideRequest, DecideResponse, DecideUsage, DecisionModel, DecisionModelService, decide, DecisionModel_d_exports as t };
|
|
45
|
+
//# sourceMappingURL=DecisionModel.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DecisionModel.d.mts","names":[],"sources":["../../src/decision-model/DecisionModel.ts"],"mappings":";;;;;;;KAKY;WACD;WACA;;KAGC,eAAe,UAAU,YAAqB;WAC/C,SAAS,QAAiB;WAC1B,OAAO;;;;;;KAON,oBAAoB;WACrB,OAAO;;WAEP;;KAGC;WACD,SAAS,GAAG,UAAU,WAC7B,YAAY,WAAoB,GAAG,IACnC,SAAS,oBAAoB,OAC1B,OAAO,OAAO,eAAe,IAAI;;;;;;;;;;;;;;cAc3B,sBAAsB;;cAKtB,SAAU,GAAG,UAAU,WAAkB,YACxC,WAAoB,GAAG,IAAE,SAC5B,oBAAoB,OAC5B,OAAO,OAAO,eAAe,IAAI,SAAiB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { n as __exportAll } from "../rolldown-runtime-EAsCxrDo.mjs";
|
|
2
|
+
import { Context, Effect } from "effect";
|
|
3
|
+
//#region src/decision-model/DecisionModel.ts
|
|
4
|
+
var DecisionModel_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
DecisionModel: () => DecisionModel,
|
|
6
|
+
decide: () => decide
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Answering typed questions about an input with probabilities instead of
|
|
10
|
+
* generating text. The model reports beliefs; the decision stays in the
|
|
11
|
+
* caller's code.
|
|
12
|
+
*
|
|
13
|
+
* Implementor contract: answer every decision or fail. Omitting one leaves a
|
|
14
|
+
* hole where {@link DecideResponse} promises a key, so a kind the backing
|
|
15
|
+
* cannot express is rejected up front with `Decision.assertKinds`.
|
|
16
|
+
*
|
|
17
|
+
* @experimental The model class this abstracts is new and the shape may change.
|
|
18
|
+
*/
|
|
19
|
+
var DecisionModel = class extends Context.Service()("@betalyra/effect-uai/DecisionModel") {};
|
|
20
|
+
/** Answer a definition's decisions about one input in one call. */
|
|
21
|
+
const decide = (definition, request) => Effect.flatMap(DecisionModel, (m) => m.decide(definition, request));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { DecisionModel, decide, DecisionModel_exports as t };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=DecisionModel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DecisionModel.mjs","names":[],"sources":["../../src/decision-model/DecisionModel.ts"],"sourcesContent":["import { Context, Effect } from \"effect\"\nimport type * as AiError from \"../domain/AiError.js\"\nimport type * as Decision from \"./Decision.js\"\n\n/** Optional throughout: not every backing reports token counts. */\nexport type DecideUsage = {\n readonly inputTokens?: number\n readonly outputTokens?: number\n}\n\nexport type DecideResponse<D extends Decision.Decisions = Decision.Decisions> = {\n readonly answers: Decision.Answers<D>\n readonly usage: DecideUsage\n}\n\n/**\n * Cross-provider decide request. The input is billed once for the whole\n * definition, so batching decisions is cheaper than repeating the input.\n */\nexport type CommonDecideRequest<A> = {\n readonly input: A\n /** Each provider narrows this to its typed literal union. */\n readonly model: string\n}\n\nexport type DecisionModelService = {\n readonly decide: <A, D extends Decision.Decisions>(\n definition: Decision.Definition<A, D>,\n request: CommonDecideRequest<A>,\n ) => Effect.Effect<DecideResponse<D>, AiError.AiError>\n}\n\n/**\n * Answering typed questions about an input with probabilities instead of\n * generating text. The model reports beliefs; the decision stays in the\n * caller's code.\n *\n * Implementor contract: answer every decision or fail. Omitting one leaves a\n * hole where {@link DecideResponse} promises a key, so a kind the backing\n * cannot express is rejected up front with `Decision.assertKinds`.\n *\n * @experimental The model class this abstracts is new and the shape may change.\n */\nexport class DecisionModel extends Context.Service<DecisionModel, DecisionModelService>()(\n \"@betalyra/effect-uai/DecisionModel\",\n) {}\n\n/** Answer a definition's decisions about one input in one call. */\nexport const decide = <A, D extends Decision.Decisions>(\n definition: Decision.Definition<A, D>,\n request: CommonDecideRequest<A>,\n): Effect.Effect<DecideResponse<D>, AiError.AiError, DecisionModel> =>\n Effect.flatMap(DecisionModel, (m) => m.decide(definition, request))\n"],"mappings":";;;;;;;;;;;;;;;;;;AA2CA,IAAa,gBAAb,cAAmC,QAAQ,QAA6C,CAAC,CACvF,oCACF,CAAC,CAAC,CAAC;;AAGH,MAAa,UACX,YACA,YAEA,OAAO,QAAQ,gBAAgB,MAAM,EAAE,OAAO,YAAY,OAAO,CAAC"}
|