1/**
2 * Cloudflare Clef implementation of DecisionModel over Workers AI REST.
3 *
4 * @stability unstable
5 * @since 4.0.0
6 */
7import * as DecisionModel from "effect/ai/DecisionModel"
8import * as AiModel from "effect/ai/Model"
9import * as Effect from "effect/Effect"
10import * as Layer from "effect/Layer"
11import { CloudflareClient } from "./CloudflareClient.ts"
12import type * as CloudflareSchema from "./CloudflareSchema.ts"
13
14/**
15 * Cloudflare Clef model identifiers.
16 *
17 * @stability unstable
18 * @category models
19 * @since 4.0.0
20 */
21export type Model = typeof CloudflareSchema.DecisionsRequest.Encoded["model"]
22
23/**
24 * Creates a decision model with Cloudflare provider metadata.
25 *
26 * @stability unstable
27 * @category constructors
28 * @since 4.0.0
29 */
30export const model = (
31 model: Model
32): AiModel.Model<"cloudflare", DecisionModel.DecisionModel, CloudflareClient> =>
33 AiModel.make("cloudflare", model, layer({ model }))
34
35/**
36 * Builds a decision service with tolerance for four-decimal probability rounding.
37 *
38 * @stability unstable
39 * @category constructors
40 * @since 4.0.0
41 */
42export const make = Effect.fnUntraced(
43 function*(
44 options: { readonly model: Model }
45 ): Effect.fn.Return<DecisionModel.DecisionModel, never, CloudflareClient> {
46 const client = yield* CloudflareClient
47 return yield* DecisionModel.make({
48 probabilityPrecision: 4,
49 decide: Effect.fnUntraced(function*({ state, decisions }) {
50 const questions: Record<string, typeof CloudflareSchema.Question.Encoded> = Object.create(null)
51 for (const [key, decision] of Object.entries(decisions)) {
52 switch (decision._tag) {
53 case "Classify":
54 questions[key] = { type: "choice", instructions: decision.instructions, criteria: decision.criteria }
55 break
56 case "Rate":
57 questions[key] = { type: "score", instructions: decision.instructions, criteria: decision.criteria }
58 break
59 case "Probability":
60 questions[key] = { type: "noul", instructions: decision.instructions, criteria: decision.criteria }
61 break
62 }
63 }
64 const response = yield* client.createDecisions({ model: options.model, state, questions })
65 const answers: Record<string, DecisionModel.ProviderAnswer> = Object.create(null)
66 for (const [key, decision] of Object.entries(decisions)) {
67 const answer = response.answers[key]
68 switch (answer?.type) {
69 case "choice":
70 answers[key] = {
71 _tag: "Classify",
72 label: answer.choice,
73 probabilities: answer.probabilities,
74 confidence: answer.confidence
75 }
76 break
77 case "score": {
78 const levels = decision._tag === "Rate" ? decision.criteria : []
79 const probabilities: Record<string, number> = Object.create(null)
80 for (let index = 0; index < levels.length; index++) {
81 const probability = answer.probabilities[String(index)]
82 if (probability !== undefined) probabilities[levels[index]] = probability
83 }
84 answers[key] = { _tag: "Rate", rating: answer.score, probabilities, confidence: answer.confidence }
85 break
86 }
87 case "noul":
88 answers[key] = { _tag: "Probability", probability: answer.noul }
89 break
90 }
91 }
92 return {
93 answers,
94 usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens }
95 }
96 })
97 })
98 }
99)
100
101/**
102 * Provides DecisionModel using an existing CloudflareClient.
103 *
104 * @stability unstable
105 * @category layers
106 * @since 4.0.0
107 */
108export const layer = (
109 options: { readonly model: Model }
110): Layer.Layer<DecisionModel.DecisionModel, never, CloudflareClient> =>
111 Layer.effect(DecisionModel.DecisionModel, make(options))