typia 14.0.6 → 15.0.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 +3 -0
- package/lib/internal/_createLlmEvaluation.d.ts +1 -0
- package/lib/internal/_createLlmEvaluation.js +486 -0
- package/lib/internal/_createLlmEvaluation.js.map +1 -0
- package/lib/internal/_createLlmEvaluation.mjs +382 -0
- package/lib/internal/_createLlmEvaluation.mjs.map +1 -0
- package/lib/internal/_stringLengthGte.d.ts +1 -0
- package/lib/internal/_stringLengthGte.js +14 -0
- package/lib/internal/_stringLengthGte.js.map +1 -0
- package/lib/internal/_stringLengthGte.mjs +11 -0
- package/lib/internal/_stringLengthGte.mjs.map +1 -0
- package/lib/internal/_stringLengthLte.d.ts +1 -0
- package/lib/internal/_stringLengthLte.js +14 -0
- package/lib/internal/_stringLengthLte.js.map +1 -0
- package/lib/internal/_stringLengthLte.mjs +11 -0
- package/lib/internal/_stringLengthLte.mjs.map +1 -0
- package/lib/llm.d.ts +54 -1
- package/lib/llm.js +5 -0
- package/lib/llm.js.map +1 -1
- package/lib/llm.mjs +6 -1
- package/lib/llm.mjs.map +1 -1
- package/lib/re-exports.d.ts +1 -1
- package/lib/re-exports.js.map +1 -1
- package/native/cmd/ttsc-typia/build.go +2 -0
- package/native/cmd/ttsc-typia/dynamic_key_tags_transform_test.go +1 -1
- package/native/cmd/ttsc-typia/llm_evaluation_accepts_property_probability_test.go +27 -0
- package/native/cmd/ttsc-typia/llm_evaluation_accepts_unselected_source_probability_test.go +26 -0
- package/native/cmd/ttsc-typia/llm_evaluation_complete_object_shape_test.go +96 -0
- package/native/cmd/ttsc-typia/llm_evaluation_ignores_declaration_probability_without_call_test.go +18 -0
- package/native/cmd/ttsc-typia/llm_evaluation_plan_emission_transform_test.go +90 -0
- package/native/cmd/ttsc-typia/llm_evaluation_program_scan_dependencies_test.go +69 -0
- package/native/cmd/ttsc-typia/llm_evaluation_rejects_alias_comment_test.go +44 -0
- package/native/cmd/ttsc-typia/llm_evaluation_rejects_alias_refinement_test.go +45 -0
- package/native/cmd/ttsc-typia/llm_evaluation_rejects_computed_method_probability_test.go +30 -0
- package/native/cmd/ttsc-typia/llm_evaluation_rejects_imported_declaration_probability_test.go +48 -0
- package/native/cmd/ttsc-typia/llm_evaluation_rejects_invalid_probability_test.go +199 -0
- package/native/cmd/ttsc-typia/llm_evaluation_rejects_refinements_test.go +94 -0
- package/native/cmd/ttsc-typia/llm_evaluation_rejects_undecidable_types_test.go +232 -0
- package/native/cmd/ttsc-typia/metadata_type_tag_sole_malformed_diagnostic_test.go +94 -0
- package/native/cmd/ttsc-typia/template_literal_type_tags_transform_test.go +3 -2
- package/native/cmd/ttsc-typia/transform.go +2 -0
- package/native/cmd/ttsc-typia/transform_helpers_test.go +17 -3
- package/native/core/context/ITypiaContext.go +4 -0
- package/native/core/factories/IdentifierFactory.go +10 -0
- package/native/core/factories/LiteralFactory.go +16 -0
- package/native/core/factories/MetadataCommentTagFactory.go +12 -10
- package/native/core/factories/MetadataTypeTagFactory.go +97 -24
- package/native/core/factories/MetadataTypeTagSchemaFactory.go +5 -0
- package/native/core/factories/internal/metadata/IMetadataIteratorProps.go +6 -2
- package/native/core/factories/internal/metadata/emplace_metadata_object.go +49 -15
- package/native/core/factories/literal_factory_null_survives_objects_test.go +43 -0
- package/native/core/factories/metadata_comment_tag_factory_string_length_helpers_test.go +55 -0
- package/native/core/factories/metadata_type_tag_factory_reports_all_invalid_candidates_test.go +73 -0
- package/native/core/programmers/llm/LlmApplicationProgrammer.go +1 -0
- package/native/core/programmers/llm/LlmEvaluationProgrammer.go +555 -0
- package/native/core/programmers/llm/LlmParametersProgrammer.go +6 -5
- package/native/core/programmers/llm/LlmReferenceDescription.go +113 -0
- package/native/core/programmers/llm/llm_reference_description_test.go +134 -0
- package/native/core/schemas/metadata/MetadataDependency.go +9 -0
- package/native/transform/CallExpressionTransformer.go +1 -0
- package/native/transform/FileTransformer.go +5 -0
- package/native/transform/features/llm/LlmEvaluationProbabilityDeclarations.go +94 -0
- package/native/transform/features/llm/LlmEvaluationProbabilityDeclarations_test.go +50 -0
- package/native/transform/features/llm/LlmEvaluationTransformer.go +59 -0
- package/native/transform/features/llm/LlmSchemaTransformer.go +14 -11
- package/native/transform/features/llm/llm_transformer_entry_coverage_test.go +29 -27
- package/package.json +3 -3
- package/src/internal/_createLlmEvaluation.ts +678 -0
- package/src/internal/_stringLengthGte.ts +6 -0
- package/src/internal/_stringLengthLte.ts +6 -0
- package/src/llm.ts +64 -0
- package/src/re-exports.ts +1 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { ILlmEvaluation, IValidation } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
import { _accessExpressionAsString } from "./_accessExpressionAsString";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compile-time plan of `typia.llm.evaluation<T>()`, one entry per decision leaf
|
|
7
|
+
* of `T`.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export type _ILlmEvaluationPlan =
|
|
12
|
+
| _ILlmEvaluationPlan.IBoolean
|
|
13
|
+
| _ILlmEvaluationPlan.IChoice
|
|
14
|
+
| _ILlmEvaluationPlan.IScore
|
|
15
|
+
| _ILlmEvaluationPlan.ISet;
|
|
16
|
+
|
|
17
|
+
/** @internal */
|
|
18
|
+
export namespace _ILlmEvaluationPlan {
|
|
19
|
+
interface IBase {
|
|
20
|
+
/** Property names from the root of `T` to the leaf. */
|
|
21
|
+
path: string[];
|
|
22
|
+
instructions: string;
|
|
23
|
+
}
|
|
24
|
+
export interface IBoolean extends IBase {
|
|
25
|
+
kind: "boolean";
|
|
26
|
+
threshold: number;
|
|
27
|
+
}
|
|
28
|
+
export interface IChoice extends IBase {
|
|
29
|
+
kind: "choice";
|
|
30
|
+
options: IMember<string>[];
|
|
31
|
+
}
|
|
32
|
+
export interface IScore extends IBase {
|
|
33
|
+
kind: "score";
|
|
34
|
+
/** Sorted in ascending value order; level `i` is `levels[i]`. */
|
|
35
|
+
levels: IMember<number>[];
|
|
36
|
+
}
|
|
37
|
+
export interface ISet extends IBase {
|
|
38
|
+
kind: "set";
|
|
39
|
+
members: ISetMember[];
|
|
40
|
+
}
|
|
41
|
+
export interface IMember<Value extends string | number> {
|
|
42
|
+
value: Value;
|
|
43
|
+
description?: string;
|
|
44
|
+
/** Acceptance minimum of the member, when it has one. */
|
|
45
|
+
minimum?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ISetMember {
|
|
48
|
+
value: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
threshold: number;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates the `ILlmEvaluation` of `typia.llm.evaluation<T>()` from its plan.
|
|
56
|
+
*
|
|
57
|
+
* Both the question map and the decoder derive their keys from here, so the
|
|
58
|
+
* readable path encoding has a single owner.
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export const _createLlmEvaluation = <T>(
|
|
63
|
+
plan: _ILlmEvaluationPlan[],
|
|
64
|
+
): ILlmEvaluation<T> => {
|
|
65
|
+
const questions: Record<string, ILlmEvaluation.IQuestion> = {};
|
|
66
|
+
for (const leaf of plan) {
|
|
67
|
+
if (leaf.kind === "set")
|
|
68
|
+
for (const member of leaf.members)
|
|
69
|
+
assign(questions, key([...leaf.path, member.value]), {
|
|
70
|
+
type: "boolean",
|
|
71
|
+
instructions: setInstructions(leaf, member),
|
|
72
|
+
});
|
|
73
|
+
else assign(questions, key(leaf.path), question(leaf));
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
questions,
|
|
77
|
+
decode: (
|
|
78
|
+
answers: unknown,
|
|
79
|
+
rounding?: ILlmEvaluation.IRounding,
|
|
80
|
+
): IValidation<T> => decode(plan, answers, rounding),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/* -----------------------------------------------------------
|
|
85
|
+
QUESTIONS
|
|
86
|
+
----------------------------------------------------------- */
|
|
87
|
+
/**
|
|
88
|
+
* Encodes a property path as a question key, like `refund.requested`.
|
|
89
|
+
*
|
|
90
|
+
* Every segment after the first uses typia's accessor notation, so the key is
|
|
91
|
+
* the validation path without its `$input` root. The first segment is quoted
|
|
92
|
+
* when it is not an identifier, and always when it is `__proto__`, so no key
|
|
93
|
+
* can collide with another or reach an object's prototype.
|
|
94
|
+
*/
|
|
95
|
+
const key = (path: string[]): string => {
|
|
96
|
+
const head: string = path[0]!;
|
|
97
|
+
const postfix: string = _accessExpressionAsString(head);
|
|
98
|
+
const first: string =
|
|
99
|
+
head === "__proto__"
|
|
100
|
+
? `[${JSON.stringify(head)}]`
|
|
101
|
+
: postfix.startsWith(".")
|
|
102
|
+
? head
|
|
103
|
+
: postfix;
|
|
104
|
+
return first + path.slice(1).map(_accessExpressionAsString).join("");
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const question = (
|
|
108
|
+
leaf:
|
|
109
|
+
| _ILlmEvaluationPlan.IBoolean
|
|
110
|
+
| _ILlmEvaluationPlan.IChoice
|
|
111
|
+
| _ILlmEvaluationPlan.IScore,
|
|
112
|
+
): ILlmEvaluation.IQuestion => {
|
|
113
|
+
if (leaf.kind === "boolean")
|
|
114
|
+
return { type: "boolean", instructions: leaf.instructions };
|
|
115
|
+
else if (leaf.kind === "choice") {
|
|
116
|
+
const criteria: Record<string, string | null> = {};
|
|
117
|
+
for (const option of leaf.options)
|
|
118
|
+
assign(criteria, option.value, option.description ?? null);
|
|
119
|
+
return { type: "choice", instructions: leaf.instructions, criteria };
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
type: "score",
|
|
123
|
+
instructions: leaf.instructions,
|
|
124
|
+
// the transform rejects fewer than two levels
|
|
125
|
+
criteria: leaf.levels.map(
|
|
126
|
+
(level) => level.description ?? String(level.value),
|
|
127
|
+
) as [string, string, ...string[]],
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const setInstructions = (
|
|
132
|
+
leaf: _ILlmEvaluationPlan.ISet,
|
|
133
|
+
member: _ILlmEvaluationPlan.ISetMember,
|
|
134
|
+
): string =>
|
|
135
|
+
[
|
|
136
|
+
leaf.instructions,
|
|
137
|
+
"",
|
|
138
|
+
`Does the option ${JSON.stringify(member.value)} apply?`,
|
|
139
|
+
...(member.description !== undefined ? [member.description] : []),
|
|
140
|
+
].join("\n");
|
|
141
|
+
|
|
142
|
+
/* -----------------------------------------------------------
|
|
143
|
+
VALIDATION
|
|
144
|
+
----------------------------------------------------------- */
|
|
145
|
+
const decode = <T>(
|
|
146
|
+
plan: _ILlmEvaluationPlan[],
|
|
147
|
+
answers: unknown,
|
|
148
|
+
rounding?: ILlmEvaluation.IRounding,
|
|
149
|
+
): IValidation<T> => {
|
|
150
|
+
const precision: IPrecision | null = (() => {
|
|
151
|
+
try {
|
|
152
|
+
return readPrecision(rounding);
|
|
153
|
+
} catch {
|
|
154
|
+
// A trapping rounding object is an invalid declaration.
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
158
|
+
if (precision === null)
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
data: answers,
|
|
162
|
+
errors: [
|
|
163
|
+
{
|
|
164
|
+
path: "$input",
|
|
165
|
+
expected: "rounding decimals in [0, 15]",
|
|
166
|
+
value: rounding,
|
|
167
|
+
description:
|
|
168
|
+
"Evaluation rounding decimals must be integers between 0 and 15.",
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
const map: Record<string, unknown> | null = object(answers);
|
|
173
|
+
if (map === null)
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
data: answers,
|
|
177
|
+
errors: [
|
|
178
|
+
{
|
|
179
|
+
path: "$input",
|
|
180
|
+
expected: "Record<string, ILlmEvaluation answer>",
|
|
181
|
+
value: answers,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const errors: IValidation.IError[] = [];
|
|
187
|
+
const expected: Set<string> = new Set();
|
|
188
|
+
const output: Record<string, unknown> = {};
|
|
189
|
+
const read = (id: string): unknown => {
|
|
190
|
+
expected.add(id);
|
|
191
|
+
try {
|
|
192
|
+
return Object.prototype.hasOwnProperty.call(map, id)
|
|
193
|
+
? map[id]
|
|
194
|
+
: undefined;
|
|
195
|
+
} catch {
|
|
196
|
+
// The decision reader reports the inaccessible answer on its own path.
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
for (const leaf of plan) {
|
|
202
|
+
const path: string = accessor(leaf.path);
|
|
203
|
+
if (leaf.kind === "set") {
|
|
204
|
+
const values: string[] = [];
|
|
205
|
+
for (const member of leaf.members) {
|
|
206
|
+
const memberPath: string = [
|
|
207
|
+
path,
|
|
208
|
+
_accessExpressionAsString(member.value),
|
|
209
|
+
].join("");
|
|
210
|
+
const answer: unknown = read(key([...leaf.path, member.value]));
|
|
211
|
+
const probability: number | null = inspect(
|
|
212
|
+
memberPath,
|
|
213
|
+
answer,
|
|
214
|
+
errors,
|
|
215
|
+
() => booleanProbability(answer, memberPath, errors),
|
|
216
|
+
);
|
|
217
|
+
if (probability !== null && probability >= member.threshold)
|
|
218
|
+
values.push(member.value);
|
|
219
|
+
}
|
|
220
|
+
place(output, leaf.path, values);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const answer: unknown = read(key(leaf.path));
|
|
224
|
+
if (leaf.kind === "boolean") {
|
|
225
|
+
const probability: number | null = inspect(path, answer, errors, () =>
|
|
226
|
+
booleanProbability(answer, path, errors),
|
|
227
|
+
);
|
|
228
|
+
if (probability !== null)
|
|
229
|
+
place(output, leaf.path, probability >= leaf.threshold);
|
|
230
|
+
} else if (leaf.kind === "choice") {
|
|
231
|
+
const value: string | null = inspect(path, answer, errors, () =>
|
|
232
|
+
choice(leaf, answer, path, errors, precision),
|
|
233
|
+
);
|
|
234
|
+
if (value !== null) place(output, leaf.path, value);
|
|
235
|
+
} else {
|
|
236
|
+
const value: number | null = inspect(path, answer, errors, () =>
|
|
237
|
+
score(leaf, answer, path, errors, precision),
|
|
238
|
+
);
|
|
239
|
+
if (value !== null) place(output, leaf.path, value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let received: string[];
|
|
243
|
+
try {
|
|
244
|
+
received = Object.keys(map);
|
|
245
|
+
} catch {
|
|
246
|
+
errors.push({
|
|
247
|
+
path: "$input",
|
|
248
|
+
expected: "readable evaluation answer map",
|
|
249
|
+
value: answers,
|
|
250
|
+
description: "Evaluation answer keys could not be inspected.",
|
|
251
|
+
});
|
|
252
|
+
received = [];
|
|
253
|
+
}
|
|
254
|
+
for (const id of received)
|
|
255
|
+
if (expected.has(id) === false)
|
|
256
|
+
try {
|
|
257
|
+
errors.push({
|
|
258
|
+
path: `$input${_accessExpressionAsString(id)}`,
|
|
259
|
+
expected: "undefined",
|
|
260
|
+
value: map[id],
|
|
261
|
+
description: "The answer does not belong to any question.",
|
|
262
|
+
});
|
|
263
|
+
} catch {
|
|
264
|
+
errors.push({
|
|
265
|
+
path: `$input${_accessExpressionAsString(id)}`,
|
|
266
|
+
expected: "readable evaluation answer",
|
|
267
|
+
value: undefined,
|
|
268
|
+
description: "The extra answer could not be inspected.",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return errors.length === 0
|
|
272
|
+
? { success: true, data: output as T }
|
|
273
|
+
: { success: false, data: answers, errors };
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/** Contain exceptions from getters and proxies at the decision path. */
|
|
277
|
+
const inspect = <T>(
|
|
278
|
+
path: string,
|
|
279
|
+
answer: unknown,
|
|
280
|
+
errors: IValidation.IError[],
|
|
281
|
+
read: () => T,
|
|
282
|
+
): T | null => {
|
|
283
|
+
try {
|
|
284
|
+
return read();
|
|
285
|
+
} catch {
|
|
286
|
+
errors.push({
|
|
287
|
+
path,
|
|
288
|
+
expected: "readable evaluation answer",
|
|
289
|
+
value: answer,
|
|
290
|
+
description: "The answer could not be inspected.",
|
|
291
|
+
});
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const booleanProbability = (
|
|
297
|
+
answer: unknown,
|
|
298
|
+
path: string,
|
|
299
|
+
errors: IValidation.IError[],
|
|
300
|
+
): number | null => {
|
|
301
|
+
const record: Record<string, unknown> | null = object(answer);
|
|
302
|
+
const type: unknown = record?.type;
|
|
303
|
+
const probability: unknown =
|
|
304
|
+
record === null
|
|
305
|
+
? undefined
|
|
306
|
+
: type === "boolean"
|
|
307
|
+
? record.probability
|
|
308
|
+
: type === "noul"
|
|
309
|
+
? record.noul
|
|
310
|
+
: undefined;
|
|
311
|
+
if (isProbability(probability)) return probability;
|
|
312
|
+
errors.push({
|
|
313
|
+
path,
|
|
314
|
+
expected:
|
|
315
|
+
'{ type: "boolean"; probability: number } | { type: "noul"; noul: number }',
|
|
316
|
+
value: answer,
|
|
317
|
+
description:
|
|
318
|
+
record === null
|
|
319
|
+
? "Missing boolean answer."
|
|
320
|
+
: type !== "boolean" && type !== "noul"
|
|
321
|
+
? `Answer type must be "boolean" or "noul", but got ${label(type)}.`
|
|
322
|
+
: "Boolean answer needs a probability in [0, 1].",
|
|
323
|
+
});
|
|
324
|
+
return null;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const choice = (
|
|
328
|
+
leaf: _ILlmEvaluationPlan.IChoice,
|
|
329
|
+
answer: unknown,
|
|
330
|
+
path: string,
|
|
331
|
+
errors: IValidation.IError[],
|
|
332
|
+
precision: IPrecision,
|
|
333
|
+
): string | null => {
|
|
334
|
+
const expected: string = `{ type: "choice"; choice: ${leaf.options
|
|
335
|
+
.map((option) => JSON.stringify(option.value))
|
|
336
|
+
.join(" | ")}; probabilities?: Record<string, number> }`;
|
|
337
|
+
const record: Record<string, unknown> | null = object(answer);
|
|
338
|
+
const type: unknown = record?.type;
|
|
339
|
+
const selected: unknown = type === "choice" ? record?.choice : undefined;
|
|
340
|
+
const option: _ILlmEvaluationPlan.IMember<string> | undefined =
|
|
341
|
+
record !== null && type === "choice"
|
|
342
|
+
? leaf.options.find((o) => o.value === selected)
|
|
343
|
+
: undefined;
|
|
344
|
+
if (record === null || option === undefined) {
|
|
345
|
+
errors.push({
|
|
346
|
+
path,
|
|
347
|
+
expected,
|
|
348
|
+
value: answer,
|
|
349
|
+
description:
|
|
350
|
+
record === null
|
|
351
|
+
? "Missing choice answer."
|
|
352
|
+
: type !== "choice"
|
|
353
|
+
? `Answer type must be "choice", but got ${label(type)}.`
|
|
354
|
+
: "Choice answer must select one of the declared options.",
|
|
355
|
+
});
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const probabilities: Record<string, number> | null | undefined = distribution(
|
|
359
|
+
record.probabilities,
|
|
360
|
+
leaf.options.map((o) => o.value),
|
|
361
|
+
precision.probability,
|
|
362
|
+
);
|
|
363
|
+
if (probabilities === null) {
|
|
364
|
+
errors.push({
|
|
365
|
+
path,
|
|
366
|
+
expected,
|
|
367
|
+
value: answer,
|
|
368
|
+
description:
|
|
369
|
+
"Choice probabilities must contain every declared option exactly once, use numbers in [0, 1], and sum to 1.",
|
|
370
|
+
});
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
if (probabilities !== undefined) {
|
|
374
|
+
const selected: number = own(probabilities, option.value)!;
|
|
375
|
+
if (
|
|
376
|
+
Object.values(probabilities).some(
|
|
377
|
+
(probability) => probability > selected + PROBABILITY_TOLERANCE,
|
|
378
|
+
)
|
|
379
|
+
) {
|
|
380
|
+
errors.push({
|
|
381
|
+
path,
|
|
382
|
+
expected,
|
|
383
|
+
value: answer,
|
|
384
|
+
description:
|
|
385
|
+
"Choice answer must select an option with maximum probability.",
|
|
386
|
+
});
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return accept(
|
|
391
|
+
option,
|
|
392
|
+
option.value,
|
|
393
|
+
own(probabilities, option.value),
|
|
394
|
+
path,
|
|
395
|
+
answer,
|
|
396
|
+
expected,
|
|
397
|
+
errors,
|
|
398
|
+
)
|
|
399
|
+
? option.value
|
|
400
|
+
: null;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const score = (
|
|
404
|
+
leaf: _ILlmEvaluationPlan.IScore,
|
|
405
|
+
answer: unknown,
|
|
406
|
+
path: string,
|
|
407
|
+
errors: IValidation.IError[],
|
|
408
|
+
precision: IPrecision,
|
|
409
|
+
): number | null => {
|
|
410
|
+
const last: number = leaf.levels.length - 1;
|
|
411
|
+
const expected: string = `{ type: "score"; score: number; probabilities?: Record<string, number> }`;
|
|
412
|
+
const record: Record<string, unknown> | null = object(answer);
|
|
413
|
+
const type: unknown = record?.type;
|
|
414
|
+
const scored: unknown = type === "score" ? record?.score : undefined;
|
|
415
|
+
if (
|
|
416
|
+
record === null ||
|
|
417
|
+
type !== "score" ||
|
|
418
|
+
typeof scored !== "number" ||
|
|
419
|
+
Number.isFinite(scored) === false ||
|
|
420
|
+
scored < 0 ||
|
|
421
|
+
scored > last
|
|
422
|
+
) {
|
|
423
|
+
errors.push({
|
|
424
|
+
path,
|
|
425
|
+
expected,
|
|
426
|
+
value: answer,
|
|
427
|
+
description:
|
|
428
|
+
record === null
|
|
429
|
+
? "Missing score answer."
|
|
430
|
+
: type !== "score"
|
|
431
|
+
? `Answer type must be "score", but got ${label(type)}.`
|
|
432
|
+
: `Score answer needs a score in [0, ${last}].`,
|
|
433
|
+
});
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const probabilities: Record<string, number> | null | undefined = distribution(
|
|
437
|
+
record.probabilities,
|
|
438
|
+
leaf.levels.map((_, i) => String(i)),
|
|
439
|
+
precision.probability,
|
|
440
|
+
);
|
|
441
|
+
if (probabilities === null) {
|
|
442
|
+
errors.push({
|
|
443
|
+
path,
|
|
444
|
+
expected,
|
|
445
|
+
value: answer,
|
|
446
|
+
description:
|
|
447
|
+
"Score probabilities must contain every level index exactly once, use numbers in [0, 1], and sum to 1.",
|
|
448
|
+
});
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
if (probabilities !== undefined) {
|
|
452
|
+
const mean: number = leaf.levels.reduce(
|
|
453
|
+
(total, _, index) => total + index * own(probabilities, String(index))!,
|
|
454
|
+
0,
|
|
455
|
+
);
|
|
456
|
+
const meanRoundingError: number = leaf.levels.reduce(
|
|
457
|
+
(total, _, index) => total + index * precision.probability,
|
|
458
|
+
0,
|
|
459
|
+
);
|
|
460
|
+
if (
|
|
461
|
+
Math.abs(mean - scored) >
|
|
462
|
+
PROBABILITY_TOLERANCE + meanRoundingError + precision.score
|
|
463
|
+
) {
|
|
464
|
+
errors.push({
|
|
465
|
+
path,
|
|
466
|
+
expected,
|
|
467
|
+
value: answer,
|
|
468
|
+
description:
|
|
469
|
+
"Score must equal the probability-weighted mean of its distribution.",
|
|
470
|
+
});
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// the most probable level when a distribution exists, where a tie resolves
|
|
476
|
+
// to the lower level; otherwise the level nearest to the fractional score,
|
|
477
|
+
// where a half rounds up
|
|
478
|
+
let index: number = Math.round(scored);
|
|
479
|
+
if (probabilities !== undefined && Object.keys(probabilities).length !== 0) {
|
|
480
|
+
index = -1;
|
|
481
|
+
leaf.levels.forEach((_, i) => {
|
|
482
|
+
const p: number | undefined = own(probabilities, String(i));
|
|
483
|
+
if (
|
|
484
|
+
p !== undefined &&
|
|
485
|
+
(index === -1 || p > own(probabilities, String(index))!)
|
|
486
|
+
)
|
|
487
|
+
index = i;
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const level: _ILlmEvaluationPlan.IMember<number> = leaf.levels[index]!;
|
|
491
|
+
return accept(
|
|
492
|
+
level,
|
|
493
|
+
level.value,
|
|
494
|
+
own(probabilities, String(index)),
|
|
495
|
+
path,
|
|
496
|
+
answer,
|
|
497
|
+
expected,
|
|
498
|
+
errors,
|
|
499
|
+
)
|
|
500
|
+
? level.value
|
|
501
|
+
: null;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/** Enforces the acceptance minimum of the selected member, if any. */
|
|
505
|
+
const accept = (
|
|
506
|
+
member: { minimum?: number },
|
|
507
|
+
value: string | number,
|
|
508
|
+
probability: number | undefined,
|
|
509
|
+
path: string,
|
|
510
|
+
answer: unknown,
|
|
511
|
+
expected: string,
|
|
512
|
+
errors: IValidation.IError[],
|
|
513
|
+
): boolean => {
|
|
514
|
+
if (member.minimum === undefined) return true;
|
|
515
|
+
if (probability === undefined) {
|
|
516
|
+
errors.push({
|
|
517
|
+
path,
|
|
518
|
+
expected,
|
|
519
|
+
value: answer,
|
|
520
|
+
description: `${JSON.stringify(value)} requires probability >= ${member.minimum}, but the answer has no probability for it.`,
|
|
521
|
+
});
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
if (probability < member.minimum) {
|
|
525
|
+
errors.push({
|
|
526
|
+
path,
|
|
527
|
+
expected,
|
|
528
|
+
value: answer,
|
|
529
|
+
description: `${JSON.stringify(value)} has probability ${probability} < ${member.minimum}.`,
|
|
530
|
+
});
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
return true;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Reads optional `probabilities`: `undefined` when absent, `null` when
|
|
538
|
+
* malformed.
|
|
539
|
+
*/
|
|
540
|
+
const distribution = (
|
|
541
|
+
input: unknown,
|
|
542
|
+
keys: string[],
|
|
543
|
+
roundingError: number,
|
|
544
|
+
): Record<string, number> | null | undefined => {
|
|
545
|
+
if (input === undefined) return undefined;
|
|
546
|
+
const record: Record<string, unknown> | null = object(input);
|
|
547
|
+
if (record === null) return null;
|
|
548
|
+
if (Object.keys(record).length !== keys.length) return null;
|
|
549
|
+
const output: Record<string, number> = {};
|
|
550
|
+
for (const key of keys) {
|
|
551
|
+
if (Object.prototype.hasOwnProperty.call(record, key) === false)
|
|
552
|
+
return null;
|
|
553
|
+
const probability: unknown = record[key];
|
|
554
|
+
if (isProbability(probability) === false) return null;
|
|
555
|
+
assign(output, key, probability);
|
|
556
|
+
}
|
|
557
|
+
if (
|
|
558
|
+
Math.abs(
|
|
559
|
+
Object.values(output).reduce((sum, probability) => sum + probability, 0) -
|
|
560
|
+
1,
|
|
561
|
+
) >
|
|
562
|
+
PROBABILITY_TOLERANCE + keys.length * roundingError
|
|
563
|
+
)
|
|
564
|
+
return null;
|
|
565
|
+
return output;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/** AI SDK's absolute tolerance for probability sums and derived values. */
|
|
569
|
+
const PROBABILITY_TOLERANCE = 1e-6;
|
|
570
|
+
|
|
571
|
+
interface IPrecision {
|
|
572
|
+
probability: number;
|
|
573
|
+
score: number;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** AI SDK's declared decimal-precision rule, without an AI SDK dependency. */
|
|
577
|
+
const readPrecision = (
|
|
578
|
+
rounding: ILlmEvaluation.IRounding | undefined,
|
|
579
|
+
): IPrecision | null => {
|
|
580
|
+
if (rounding === undefined) return { probability: 0, score: 0 };
|
|
581
|
+
if (
|
|
582
|
+
typeof rounding !== "object" ||
|
|
583
|
+
rounding === null ||
|
|
584
|
+
Array.isArray(rounding)
|
|
585
|
+
)
|
|
586
|
+
return null;
|
|
587
|
+
const record: Record<string, unknown> = rounding as Record<string, unknown>;
|
|
588
|
+
const decimalError = (value: unknown): number | null =>
|
|
589
|
+
value === undefined
|
|
590
|
+
? 0
|
|
591
|
+
: typeof value === "number" &&
|
|
592
|
+
Number.isInteger(value) &&
|
|
593
|
+
value >= 0 &&
|
|
594
|
+
value <= 15
|
|
595
|
+
? 0.5 * 10 ** -value
|
|
596
|
+
: null;
|
|
597
|
+
const probability: number | null = decimalError(record.probabilityDecimals);
|
|
598
|
+
const score: number | null = decimalError(record.scoreDecimals);
|
|
599
|
+
return probability === null || score === null ? null : { probability, score };
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
/* -----------------------------------------------------------
|
|
603
|
+
HELPERS
|
|
604
|
+
----------------------------------------------------------- */
|
|
605
|
+
/**
|
|
606
|
+
* Renders an untrusted answer `type` for a message; unlike `JSON.stringify`, it
|
|
607
|
+
* never throws on a bigint or circular value.
|
|
608
|
+
*/
|
|
609
|
+
const label = (value: unknown): string =>
|
|
610
|
+
typeof value === "string" ? JSON.stringify(value) : typeof value;
|
|
611
|
+
|
|
612
|
+
const accessor = (path: string[]): string =>
|
|
613
|
+
"$input" + path.map(_accessExpressionAsString).join("");
|
|
614
|
+
|
|
615
|
+
/** Match AI SDK's JSON-record boundary, including null-prototype dictionaries. */
|
|
616
|
+
const object = (input: unknown): Record<string, unknown> | null => {
|
|
617
|
+
if (typeof input !== "object" || input === null) return null;
|
|
618
|
+
try {
|
|
619
|
+
if (Array.isArray(input)) return null;
|
|
620
|
+
const prototype: object | null = Object.getPrototypeOf(input);
|
|
621
|
+
return prototype === Object.prototype || prototype === null
|
|
622
|
+
? (input as Record<string, unknown>)
|
|
623
|
+
: null;
|
|
624
|
+
} catch {
|
|
625
|
+
// A revoked or trapping proxy is not a usable answer record.
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Reads an own probability, so an option named like an `Object.prototype`
|
|
632
|
+
* member (`constructor`, `toString`, ...) never inherits a value.
|
|
633
|
+
*/
|
|
634
|
+
const own = (
|
|
635
|
+
probabilities: Record<string, number> | undefined,
|
|
636
|
+
key: string,
|
|
637
|
+
): number | undefined =>
|
|
638
|
+
probabilities !== undefined &&
|
|
639
|
+
Object.prototype.hasOwnProperty.call(probabilities, key)
|
|
640
|
+
? probabilities[key]
|
|
641
|
+
: undefined;
|
|
642
|
+
|
|
643
|
+
const isProbability = (value: unknown): value is number =>
|
|
644
|
+
typeof value === "number" &&
|
|
645
|
+
Number.isFinite(value) &&
|
|
646
|
+
value >= 0 &&
|
|
647
|
+
value <= 1;
|
|
648
|
+
|
|
649
|
+
/** Writes the leaf value, creating the intermediate objects of its path. */
|
|
650
|
+
const place = (
|
|
651
|
+
output: Record<string, unknown>,
|
|
652
|
+
path: string[],
|
|
653
|
+
value: unknown,
|
|
654
|
+
): void => {
|
|
655
|
+
let target: Record<string, unknown> = output;
|
|
656
|
+
path.slice(0, -1).forEach((name) => {
|
|
657
|
+
if (Object.prototype.hasOwnProperty.call(target, name) === false)
|
|
658
|
+
assign(target, name, {});
|
|
659
|
+
target = target[name] as Record<string, unknown>;
|
|
660
|
+
});
|
|
661
|
+
assign(target, path[path.length - 1]!, value);
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
/** Own-property assignment that cannot reach the prototype via `__proto__`. */
|
|
665
|
+
const assign = (
|
|
666
|
+
target: Record<string, unknown>,
|
|
667
|
+
name: string,
|
|
668
|
+
value: unknown,
|
|
669
|
+
): void => {
|
|
670
|
+
if (name === "__proto__")
|
|
671
|
+
Object.defineProperty(target, name, {
|
|
672
|
+
value,
|
|
673
|
+
enumerable: true,
|
|
674
|
+
writable: true,
|
|
675
|
+
configurable: true,
|
|
676
|
+
});
|
|
677
|
+
else target[name] = value;
|
|
678
|
+
};
|