1/**
2 * Defines decisions that a `DecisionModel` answers over a single input.
3 * Pair an input schema with named classification, rating, or probability
4 * decisions using `make`, then answer them with `DecisionModel.decide`.
5 *
6 * @see {@link make} for building a definition from an input schema and decisions
7 *
8 * @stability unstable
9 * @since 4.0.0
10 */
11import type * as Schema from "../Schema.ts"
12
13/**
14 * Brand type for decision definitions.
15 *
16 * @stability unstable
17 * @category type IDs
18 * @since 4.0.0
19 */
20export type TypeId = "~effect/ai/Decision"
21
22/**
23 * Brand for decision definitions.
24 *
25 * @stability unstable
26 * @category type IDs
27 * @since 4.0.0
28 */
29export const TypeId: TypeId = "~effect/ai/Decision"
30
31/**
32 * Decision that assigns the input one label out of a set of criteria.
33 * `criteria` maps labels to descriptions.
34 *
35 * @see {@link classify} for the constructor
36 * @see {@link ClassifyAnswer} for the answer produced by this decision
37 *
38 * @stability unstable
39 * @category models
40 * @since 4.0.0
41 */
42export interface Classify<Label extends string> {
43 readonly _tag: "Classify"
44 readonly instructions: string
45 readonly criteria: { readonly [L in Label]: string }
46}
47
48/**
49 * Decision that places the input on an ordered scale of criteria.
50 * `criteria` lists levels from lowest to highest.
51 *
52 * @see {@link rate} for the constructor
53 * @see {@link RateAnswer} for the answer produced by this decision
54 *
55 * @stability unstable
56 * @category models
57 * @since 4.0.0
58 */
59export interface Rate<Level extends string> {
60 readonly _tag: "Rate"
61 readonly instructions: string
62 readonly criteria: ReadonlyArray<Level>
63}
64
65/**
66 * Decision that estimates how likely a statement about the input is to hold.
67 * Optional `criteria` describes both outcomes when supplied.
68 * The answer is the probability of `true`.
69 *
70 * @see {@link probability} for the constructor
71 * @see {@link ProbabilityAnswer} for the answer produced by this decision
72 *
73 * @stability unstable
74 * @category models
75 * @since 4.0.0
76 */
77export interface Probability {
78 readonly _tag: "Probability"
79 readonly instructions: string
80 readonly criteria?: {
81 readonly false: string
82 readonly true: string
83 } | undefined
84}
85
86/**
87 * Union of every decision kind.
88 *
89 * @stability unstable
90 * @category models
91 * @since 4.0.0
92 */
93export type Any = Classify<string> | Rate<string> | Probability
94
95/**
96 * Answer to a {@link Classify} decision.
97 * `label` is chosen by the provider and need not have the highest probability.
98 * `confidence` is an optional, provider-defined measure in `[0, 1]`.
99 *
100 * @stability unstable
101 * @category models
102 * @since 4.0.0
103 */
104export interface ClassifyAnswer<Label extends string> {
105 readonly label: Label
106 readonly probabilities: { readonly [L in Label]: number }
107 readonly confidence?: number | undefined
108}
109
110/**
111 * Answer to a {@link Rate} decision.
112 * `rating` is the probability-weighted position on the scale and may fall
113 * between two levels, within `[0, criteria.length - 1]`. `label` is the level
114 * with the highest probability, choosing the first criteria entry on ties.
115 * `confidence` is an optional, provider-defined measure in `[0, 1]`.
116 *
117 * @stability unstable
118 * @category models
119 * @since 4.0.0
120 */
121export interface RateAnswer<Level extends string> {
122 readonly rating: number
123 readonly label: Level
124 readonly probabilities: { readonly [L in Level]: number }
125 readonly confidence?: number | undefined
126}
127
128/**
129 * Answer to a {@link Probability} decision.
130 *
131 * @stability unstable
132 * @category models
133 * @since 4.0.0
134 */
135export interface ProbabilityAnswer {
136 readonly probability: number
137}
138
139/**
140 * Answer type for a single decision.
141 *
142 * @stability unstable
143 * @category utility types
144 * @since 4.0.0
145 */
146export type Answer<D extends Any> = D extends Classify<infer Label> ? ClassifyAnswer<Label>
147 : D extends Rate<infer Level> ? RateAnswer<Level>
148 : D extends Probability ? ProbabilityAnswer
149 : never
150
151/**
152 * Answers keyed by decision name.
153 *
154 * @stability unstable
155 * @category utility types
156 * @since 4.0.0
157 */
158export type Answers<Decisions extends Record<string, Any>> = {
159 readonly [K in keyof Decisions]: Answer<Decisions[K]>
160}
161
162/**
163 * Input schema and named decisions that run together in one provider call.
164 *
165 * @see {@link make} for the constructor
166 *
167 * @stability unstable
168 * @category models
169 * @since 4.0.0
170 */
171export interface Definition<Input extends Schema.Constraint, Decisions extends Record<string, Any>> {
172 readonly [TypeId]: TypeId
173 readonly input: Input
174 readonly decisions: Decisions
175}
176
177/**
178 * Creates a classification decision from labelled criteria.
179 * Throws if fewer than two labels are supplied.
180 *
181 * **Example** (Choosing a department)
182 *
183 * ```ts
184 * import { Decision } from "effect/ai"
185 *
186 * const department = Decision.classify({
187 * instructions: "Which team should handle this",
188 * criteria: {
189 * billing: "payments",
190 * technical: "bugs",
191 * sales: "pricing"
192 * }
193 * })
194 * ```
195 *
196 * @see {@link rate} for an ordered scale
197 * @see {@link probability} for a single yes or no likelihood
198 *
199 * @stability unstable
200 * @category constructors
201 * @since 4.0.0
202 */
203export const classify = <Label extends string>(options: {
204 readonly instructions: string
205 readonly criteria: { readonly [L in Label]: string }
206}): Classify<Label> => {
207 if (Object.keys(options.criteria).length < 2) {
208 throw new Error("Decision.classify: criteria must contain at least two labels")
209 }
210 return {
211 _tag: "Classify",
212 instructions: options.instructions,
213 criteria: options.criteria
214 }
215}
216
217/**
218 * Creates a rating decision from an ordered list of criteria.
219 * Throws if fewer than two levels or duplicate levels are supplied.
220 *
221 * **Example** (Rating frustration)
222 *
223 * ```ts
224 * import { Decision } from "effect/ai"
225 *
226 * const frustration = Decision.rate({
227 * instructions: "How frustrated",
228 * criteria: ["calm", "frustrated", "angry"]
229 * })
230 * ```
231 *
232 * @see {@link classify} for unordered labels
233 *
234 * @stability unstable
235 * @category constructors
236 * @since 4.0.0
237 */
238export const rate = <const Level extends string>(options: {
239 readonly instructions: string
240 readonly criteria: ReadonlyArray<Level>
241}): Rate<Level> => {
242 if (options.criteria.length < 2) {
243 throw new Error("Decision.rate: criteria must contain at least two levels")
244 }
245 if (new Set(options.criteria).size !== options.criteria.length) {
246 throw new Error("Decision.rate: criteria must contain distinct levels")
247 }
248 return {
249 _tag: "Rate",
250 instructions: options.instructions,
251 criteria: options.criteria
252 }
253}
254
255/**
256 * Creates a probability decision from instructions and optional outcome descriptions.
257 * When `criteria` is supplied, descriptions for both `false` and `true` are required.
258 *
259 * **Example** (Estimating urgency)
260 *
261 * ```ts
262 * import { Decision } from "effect/ai"
263 *
264 * const urgent = Decision.probability({
265 * instructions: "The message is time-sensitive"
266 * })
267 * ```
268 *
269 * **Example** (Providing outcome descriptions)
270 *
271 * ```ts
272 * import { Decision } from "effect/ai"
273 *
274 * const urgent = Decision.probability({
275 * instructions: "The message is time-sensitive",
276 * criteria: {
277 * false: "The message can wait",
278 * true: "The message needs immediate attention"
279 * }
280 * })
281 * ```
282 *
283 * @see {@link classify} for more than two outcomes
284 *
285 * @stability unstable
286 * @category constructors
287 * @since 4.0.0
288 */
289export const probability = (options: {
290 readonly instructions: string
291 readonly criteria?: {
292 readonly false: string
293 readonly true: string
294 } | undefined
295}): Probability => ({
296 _tag: "Probability",
297 instructions: options.instructions,
298 criteria: options.criteria
299})
300
301/**
302 * Creates a decision definition from an input schema and named decisions.
303 * `DecisionModel.decide` encodes the input with `Schema.toCodecJson` before
304 * calling the provider. Answer keys and types are inferred from the decisions.
305 * Throws if `decisions` is empty.
306 *
307 * **Example** (Defining ticket triage)
308 *
309 * ```ts
310 * import { Schema } from "effect"
311 * import { Decision } from "effect/ai"
312 *
313 * const Ticket = Schema.Struct({
314 * subject: Schema.String,
315 * body: Schema.String
316 * })
317 *
318 * const TicketTriage = Decision.make({
319 * input: Ticket,
320 * decisions: {
321 * department: Decision.classify({
322 * instructions: "Which team should handle this",
323 * criteria: { billing: "payments", technical: "bugs" }
324 * }),
325 * urgent: Decision.probability({
326 * instructions: "The message is time-sensitive",
327 * criteria: { false: "No time pressure", true: "Needs action now" }
328 * })
329 * }
330 * })
331 * ```
332 *
333 * @see {@link Definition} for the returned shape
334 *
335 * @stability unstable
336 * @category constructors
337 * @since 4.0.0
338 */
339export const make = <Input extends Schema.Constraint, Decisions extends Record<string, Any>>(options: {
340 readonly input: Input
341 readonly decisions: Decisions
342}): Definition<Input, Decisions> => {
343 if (Object.keys(options.decisions).length === 0) {
344 throw new Error("Decision.make: decisions must not be empty")
345 }
346 return {
347 [TypeId]: TypeId,
348 input: options.input,
349 decisions: options.decisions
350 }
351}