@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.
- package/README.md +126 -0
- package/dist/TypeSafeClient-OS5hTVMz.mjs +189 -0
- package/dist/TypeSafeClient-OS5hTVMz.mjs.map +1 -0
- package/dist/TypeSafeClient.d.mts +65 -0
- package/dist/TypeSafeClient.mjs +3 -0
- package/dist/TypeSafeSchema.d.mts +305 -0
- package/dist/TypeSafeSchema.mjs +168 -0
- package/dist/TypeSafeSchema.mjs.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -0
- package/src/TypeSafeClient.ts +145 -0
- package/src/TypeSafeSchema.ts +275 -0
- package/src/index.ts +2 -0
- package/src/internal/errors.ts +103 -0
- package/src/internal/schema.ts +86 -0
|
@@ -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,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
|
+
}
|