@effect-agent/ai-typesafe 0.1.0-beta.100

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.
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Request and response schemas for TypeSafe's System One HTTP API.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Schema from "effect/Schema";
7
+
8
+ /**
9
+ * Text or JSON objects and arrays used as state and question instructions.
10
+ *
11
+ * @category schemas
12
+ * @since 0.1.0
13
+ */
14
+ export const Content = Schema.Union([Schema.String, Schema.JsonObject, Schema.Array(Schema.Json)]);
15
+
16
+ /** @category models
17
+ * @since 0.1.0
18
+ */
19
+ export type Content = typeof Content.Type;
20
+
21
+ /**
22
+ * A choice between named options. A null rubric uses the option name alone.
23
+ *
24
+ * @category schemas
25
+ * @since 0.1.0
26
+ */
27
+ export const ChoiceQuestion = Schema.Struct({
28
+ type: Schema.Literal("choice"),
29
+ instructions: Content,
30
+ criteria: Schema.Record(Schema.String, Schema.NullOr(Schema.String)).check(
31
+ Schema.isMinProperties(1),
32
+ ),
33
+ });
34
+
35
+ /** @category models
36
+ * @since 0.1.0
37
+ */
38
+ export type ChoiceQuestion = typeof ChoiceQuestion.Type;
39
+
40
+ /**
41
+ * A rating along at least two ordered, zero-indexed level descriptions.
42
+ *
43
+ * @category schemas
44
+ * @since 0.1.0
45
+ */
46
+ export const ScoreQuestion = Schema.Struct({
47
+ type: Schema.Literal("score"),
48
+ instructions: Content,
49
+ criteria: Schema.Array(Schema.String).check(Schema.isMinLength(2)),
50
+ });
51
+
52
+ /** @category models
53
+ * @since 0.1.0
54
+ */
55
+ export type ScoreQuestion = typeof ScoreQuestion.Type;
56
+
57
+ /**
58
+ * A yes/no judgment, with optional descriptions of either outcome.
59
+ *
60
+ * @category schemas
61
+ * @since 0.1.0
62
+ */
63
+ export const NoulQuestion = Schema.Struct({
64
+ type: Schema.Literal("noul"),
65
+ instructions: Content,
66
+ criteria: Schema.optionalKey(
67
+ Schema.Struct({
68
+ true: Schema.optionalKey(Schema.String),
69
+ false: Schema.optionalKey(Schema.String),
70
+ }),
71
+ ),
72
+ });
73
+
74
+ /** @category models
75
+ * @since 0.1.0
76
+ */
77
+ export type NoulQuestion = typeof NoulQuestion.Type;
78
+
79
+ /** @category schemas
80
+ * @since 0.1.0
81
+ */
82
+ export const Question = Schema.Union([ChoiceQuestion, ScoreQuestion, NoulQuestion]);
83
+
84
+ /** @category models
85
+ * @since 0.1.0
86
+ */
87
+ export type Question = typeof Question.Type;
88
+
89
+ /** @category schemas
90
+ * @since 0.1.0
91
+ */
92
+ export const Questions = Schema.Record(Schema.String, Question);
93
+
94
+ /** @category models
95
+ * @since 0.1.0
96
+ */
97
+ export type Questions = typeof Questions.Type;
98
+
99
+ /** @category schemas
100
+ * @since 0.1.0
101
+ */
102
+ export const EvaluateRequest = Schema.Struct({
103
+ model: Schema.String,
104
+ state: Content,
105
+ questions: Questions,
106
+ });
107
+
108
+ /**
109
+ * Evaluation input. Keep question literals with `satisfies Questions` when
110
+ * storing questions separately from the call to `evaluate`.
111
+ *
112
+ * @category models
113
+ * @since 0.1.0
114
+ */
115
+ export type EvaluateRequest<Q extends Questions = Questions> = Omit<
116
+ typeof EvaluateRequest.Type,
117
+ "questions"
118
+ > & { readonly questions: Q };
119
+
120
+ /**
121
+ * A finite probability or confidence in the inclusive range [0, 1].
122
+ *
123
+ * @category schemas
124
+ * @since 0.1.0
125
+ */
126
+ export const Probability = Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 }));
127
+
128
+ /**
129
+ * A selected option and its full distribution. Confidence summarizes the
130
+ * distribution; it does not guarantee correctness.
131
+ *
132
+ * @category schemas
133
+ * @since 0.1.0
134
+ */
135
+ export const ChoiceAnswer = Schema.Struct({
136
+ type: Schema.Literal("choice"),
137
+ choice: Schema.String,
138
+ probabilities: Schema.Record(Schema.String, Probability),
139
+ confidence: Probability,
140
+ });
141
+
142
+ /**
143
+ * Known option unions infer literal choices and required probability keys.
144
+ * Open string or template-pattern option types allow absent dictionary entries.
145
+ *
146
+ * @category models
147
+ * @since 0.1.0
148
+ */
149
+ export type ChoiceAnswer<Choice extends string = string> = Omit<
150
+ typeof ChoiceAnswer.Type,
151
+ "choice" | "probabilities"
152
+ > & {
153
+ readonly choice: Choice;
154
+ readonly probabilities: {
155
+ readonly [K in Choice]: {} extends Pick<Record<Choice, unknown>, K>
156
+ ? number | undefined
157
+ : number;
158
+ };
159
+ };
160
+
161
+ /**
162
+ * A fractional, probability-weighted level, with the supplied rubric as legend.
163
+ *
164
+ * @category schemas
165
+ * @since 0.1.0
166
+ */
167
+ export const ScoreAnswer = Schema.Struct({
168
+ type: Schema.Literal("score"),
169
+ score: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)),
170
+ legend: Schema.Record(Schema.String, Schema.String),
171
+ probabilities: Schema.Record(Schema.String, Probability),
172
+ confidence: Probability,
173
+ });
174
+
175
+ /**
176
+ * Score levels are keyed at runtime; an arbitrary legend or probability lookup
177
+ * may be absent.
178
+ *
179
+ * @category models
180
+ * @since 0.1.0
181
+ */
182
+ export type ScoreAnswer = Omit<typeof ScoreAnswer.Type, "legend" | "probabilities"> & {
183
+ readonly legend: { readonly [level: string]: string | undefined };
184
+ readonly probabilities: { readonly [level: string]: number | undefined };
185
+ };
186
+
187
+ /**
188
+ * The probability of yes. Noul has no separate confidence field.
189
+ *
190
+ * @category schemas
191
+ * @since 0.1.0
192
+ */
193
+ export const NoulAnswer = Schema.Struct({ type: Schema.Literal("noul"), noul: Probability });
194
+
195
+ /** @category models
196
+ * @since 0.1.0
197
+ */
198
+ export type NoulAnswer = typeof NoulAnswer.Type;
199
+
200
+ /** @category schemas
201
+ * @since 0.1.0
202
+ */
203
+ export const Answer = Schema.Union([ChoiceAnswer, ScoreAnswer, NoulAnswer]);
204
+
205
+ /** @category models
206
+ * @since 0.1.0
207
+ */
208
+ export type Answer = ChoiceAnswer | ScoreAnswer | NoulAnswer;
209
+
210
+ /** @category schemas
211
+ * @since 0.1.0
212
+ */
213
+ export const Usage = Schema.Struct({ input_tokens: Schema.Natural, output_tokens: Schema.Natural });
214
+
215
+ /** @category models
216
+ * @since 0.1.0
217
+ */
218
+ export type Usage = typeof Usage.Type;
219
+
220
+ /**
221
+ * The wire response shape. `TypeSafeClient.evaluate` additionally validates
222
+ * answer IDs, question kinds, criteria, distributions, and score correlations.
223
+ *
224
+ * @category schemas
225
+ * @since 0.1.0
226
+ */
227
+ export const EvaluateResponse = Schema.Struct({
228
+ model: Schema.String,
229
+ answers: Schema.Record(Schema.String, Answer),
230
+ usage: Usage,
231
+ });
232
+
233
+ type ChoiceAnswerFor<Criteria> = Criteria extends unknown
234
+ ? Omit<typeof ChoiceAnswer.Type, "choice" | "probabilities"> & {
235
+ readonly choice: `${Extract<keyof Criteria, string | number>}`;
236
+ readonly probabilities: {
237
+ readonly [
238
+ K in keyof Criteria as K extends string | number ? `${K}` : never
239
+ ]: {} extends Pick<Criteria, K> ? number | undefined : number;
240
+ };
241
+ }
242
+ : never;
243
+
244
+ /**
245
+ * Infer a question's answer, preserving optional choice criteria in its probabilities.
246
+ *
247
+ * @category models
248
+ * @since 0.1.0
249
+ */
250
+ export type AnswerFor<Q extends Question> = Q extends ChoiceQuestion
251
+ ? ChoiceAnswerFor<Q["criteria"]>
252
+ : Q extends ScoreQuestion
253
+ ? ScoreAnswer
254
+ : NoulAnswer;
255
+
256
+ /**
257
+ * One answer for each required question. Optional properties and open string,
258
+ * numeric, or template-pattern indexes require checking an entry for absence.
259
+ *
260
+ * @category models
261
+ * @since 0.1.0
262
+ */
263
+ export type Answers<Q extends Questions> = {
264
+ readonly [K in keyof Q as K extends string | number ? `${K}` : never]: {} extends Pick<Q, K>
265
+ ? AnswerFor<NonNullable<Q[K]>> | undefined
266
+ : AnswerFor<Q[K]>;
267
+ };
268
+
269
+ /** @category models
270
+ * @since 0.1.0
271
+ */
272
+ export type EvaluateResponse<Q extends Questions = Questions> = Omit<
273
+ typeof EvaluateResponse.Type,
274
+ "answers"
275
+ > & { readonly answers: Answers<Q> };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * as TypeSafeClient from "./TypeSafeClient.ts";
2
+ export * as TypeSafeSchema from "./TypeSafeSchema.ts";
@@ -0,0 +1,103 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Option from "effect/Option";
3
+ import * as Record from "effect/Record";
4
+ import * as Redacted from "effect/Redacted";
5
+ import type * as Schema from "effect/Schema";
6
+ import * as AiError from "effect/unstable/ai/AiError";
7
+ import * as Headers from "effect/unstable/http/Headers";
8
+ import type * as HttpClientError from "effect/unstable/http/HttpClientError";
9
+
10
+ export const make = (reason: AiError.AiErrorReason): AiError.AiError =>
11
+ AiError.make({ module: "TypeSafeClient", method: "evaluate", reason });
12
+
13
+ export const mapSchemaError = (
14
+ error: Schema.SchemaError,
15
+ redact: (text: string) => string,
16
+ ): AiError.AiError => {
17
+ const reason = AiError.InvalidOutputError.fromSchemaError(error);
18
+
19
+ return make(
20
+ new AiError.InvalidOutputError({ ...reason, description: redact(reason.description) }),
21
+ );
22
+ };
23
+
24
+ const redactRequest = (
25
+ request: typeof AiError.HttpRequestDetails.Type,
26
+ redact: (text: string) => string,
27
+ ): typeof AiError.HttpRequestDetails.Type => ({
28
+ ...request,
29
+ url: redact(request.url),
30
+ urlParams: request.urlParams.map(([key, value]) => [key, redact(value)]),
31
+ hash: request.hash === undefined ? undefined : redact(request.hash),
32
+ headers: Record.map(request.headers, (value) =>
33
+ Redacted.isRedacted(value) ? "<redacted>" : redact(value),
34
+ ),
35
+ });
36
+
37
+ export const mapHttpClientError = Effect.fnUntraced(function* (
38
+ error: HttpClientError.HttpClientError,
39
+ redact: (text: string) => string,
40
+ ): Effect.fn.Return<never, AiError.AiError> {
41
+ const reason = error.reason;
42
+
43
+ switch (reason._tag) {
44
+ case "TransportError":
45
+ case "EncodeError":
46
+ case "InvalidUrlError": {
47
+ const network = AiError.NetworkError.fromRequestError(reason);
48
+
49
+ return yield* make(
50
+ new AiError.NetworkError({
51
+ ...network,
52
+ request: redactRequest(network.request, redact),
53
+ description: network.description === undefined ? undefined : redact(network.description),
54
+ }),
55
+ );
56
+ }
57
+ case "DecodeError":
58
+ case "EmptyBodyError":
59
+ return yield* make(
60
+ new AiError.InvalidOutputError({
61
+ description: redact(reason.description ?? "Could not decode the TypeSafe response body"),
62
+ }),
63
+ );
64
+ case "StatusCodeError": {
65
+ const { request, response } = reason;
66
+ const redactedNames = yield* Headers.CurrentRedactedNames;
67
+
68
+ const headers = (value: Headers.Headers) =>
69
+ Record.map(Headers.redact(value, redactedNames), (value) =>
70
+ Redacted.isRedacted(value) ? "<redacted>" : redact(value),
71
+ );
72
+
73
+ const text = yield* Effect.option(response.text);
74
+ const body = Option.isSome(text) ? redact(text.value) : undefined;
75
+
76
+ const http: typeof AiError.HttpContext.Type = {
77
+ request: {
78
+ method: request.method,
79
+ url: redact(request.url),
80
+ urlParams: Array.from(request.urlParams, ([key, value]) => [key, redact(value)]),
81
+ hash: Option.getOrUndefined(Option.map(request.hash, redact)),
82
+ headers: headers(request.headers),
83
+ },
84
+ response: { status: response.status, headers: headers(response.headers) },
85
+ body,
86
+ };
87
+
88
+ const description = AiError.buildErrorDescription({
89
+ status: response.status,
90
+ method: request.method,
91
+ url: http.request.url,
92
+ message: undefined,
93
+ body,
94
+ });
95
+
96
+ return yield* make(
97
+ response.status === 422
98
+ ? new AiError.InvalidRequestError({ description, http })
99
+ : AiError.reasonFromHttpStatus({ status: response.status, description, http }),
100
+ );
101
+ }
102
+ }
103
+ });
@@ -0,0 +1,86 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ import * as TypeSafeSchema from "../TypeSafeSchema.ts";
4
+
5
+ // Permit floating-point serialization error without changing provider values.
6
+ const tolerance = 1e-6;
7
+
8
+ const distribution = (keys: ReadonlyArray<string>) =>
9
+ Schema.Record(Schema.Literals(keys), TypeSafeSchema.Probability).check(
10
+ Schema.makeFilter(
11
+ (probabilities) =>
12
+ Math.abs(Object.values(probabilities).reduce((sum, value) => sum + value, 0) - 1) <=
13
+ tolerance,
14
+ { expected: "probabilities summing to 1 (within 1e-6)" },
15
+ ),
16
+ );
17
+
18
+ const answerFor = (question: TypeSafeSchema.Question) => {
19
+ switch (question.type) {
20
+ case "choice": {
21
+ const keys = Object.keys(question.criteria);
22
+
23
+ return Schema.Struct({
24
+ ...TypeSafeSchema.ChoiceAnswer.fields,
25
+ choice: Schema.Literals(keys),
26
+ probabilities: distribution(keys),
27
+ }).check(
28
+ Schema.makeFilter(
29
+ ({ choice, probabilities }) =>
30
+ Object.values(probabilities).every((value) => value <= probabilities[choice]),
31
+ { expected: "a highest-probability choice" },
32
+ ),
33
+ );
34
+ }
35
+ case "score": {
36
+ const maxLevel = question.criteria.length - 1;
37
+
38
+ const levels = question.criteria.map(
39
+ (description, index) => [String(index), description] as const,
40
+ );
41
+
42
+ return Schema.Struct({
43
+ ...TypeSafeSchema.ScoreAnswer.fields,
44
+ score: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: maxLevel })),
45
+ legend: Schema.Struct(
46
+ Object.fromEntries(
47
+ levels.map(([key, description]) => [key, Schema.Literal(description)]),
48
+ ),
49
+ ),
50
+ probabilities: distribution(levels.map(([key]) => key)),
51
+ }).check(
52
+ Schema.makeFilter(
53
+ ({ score, probabilities }) =>
54
+ Math.abs(
55
+ score -
56
+ Object.entries(probabilities).reduce(
57
+ (sum, [level, probability]) => sum + Number(level) * probability,
58
+ 0,
59
+ ),
60
+ ) <=
61
+ tolerance * Math.max(1, maxLevel),
62
+ { expected: "the probability-weighted score (within 1e-6 per level)" },
63
+ ),
64
+ );
65
+ }
66
+ case "noul":
67
+ return TypeSafeSchema.NoulAnswer;
68
+ }
69
+ };
70
+
71
+ // The overload describes the dependent type enforced by the literal keys and
72
+ // per-question schemas below. No JSON value is asserted to have that type.
73
+ export function responseFor<const Q extends TypeSafeSchema.Questions>(
74
+ questions: Q,
75
+ ): Schema.Codec<TypeSafeSchema.EvaluateResponse<Q>>;
76
+
77
+ export function responseFor(questions: TypeSafeSchema.Questions): Schema.Top {
78
+ return Schema.Struct({
79
+ ...TypeSafeSchema.EvaluateResponse.fields,
80
+ answers: Schema.Struct(
81
+ Object.fromEntries(
82
+ Object.entries(questions).map(([id, question]) => [id, answerFor(question)]),
83
+ ),
84
+ ),
85
+ });
86
+ }