@anvia/core 0.23.0 → 0.25.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/dist/agent/index.js +7 -7
- package/dist/{chunk-HYUYZEAJ.js → chunk-2NDQHV7V.js} +5 -5
- package/dist/{chunk-MELYDKWQ.js → chunk-4BGN6PYF.js} +4 -4
- package/dist/{chunk-JI5YZWNT.js → chunk-4HWN7734.js} +3 -3
- package/dist/{chunk-NCPN4SJR.js → chunk-BCA4VZ5W.js} +2 -2
- package/dist/{chunk-YLMDBQO4.js → chunk-KYEBWN3E.js} +4 -4
- package/dist/{chunk-BALBBVI3.js → chunk-VXONXP2G.js} +5 -5
- package/dist/{chunk-DALYP4PX.js → chunk-YEBIVZM2.js} +4 -4
- package/dist/evals/index.d.ts +397 -42
- package/dist/evals/index.js +971 -201
- package/dist/evals/index.js.map +1 -1
- package/dist/extractor/index.js +8 -8
- package/dist/index.js +8 -8
- package/dist/internal/agent.js +6 -6
- package/dist/request/index.js +5 -5
- package/dist/skills/index.js +5 -5
- package/dist/tool/index.js +4 -4
- package/dist/vector-store/index.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-HYUYZEAJ.js.map → chunk-2NDQHV7V.js.map} +0 -0
- /package/dist/{chunk-MELYDKWQ.js.map → chunk-4BGN6PYF.js.map} +0 -0
- /package/dist/{chunk-JI5YZWNT.js.map → chunk-4HWN7734.js.map} +0 -0
- /package/dist/{chunk-NCPN4SJR.js.map → chunk-BCA4VZ5W.js.map} +0 -0
- /package/dist/{chunk-YLMDBQO4.js.map → chunk-KYEBWN3E.js.map} +0 -0
- /package/dist/{chunk-BALBBVI3.js.map → chunk-VXONXP2G.js.map} +0 -0
- /package/dist/{chunk-DALYP4PX.js.map → chunk-YEBIVZM2.js.map} +0 -0
package/dist/evals/index.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ExtractorBuilder
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
6
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-KYEBWN3E.js";
|
|
4
|
+
import "../chunk-4HWN7734.js";
|
|
5
|
+
import "../chunk-YEBIVZM2.js";
|
|
6
|
+
import "../chunk-VXONXP2G.js";
|
|
7
7
|
import "../chunk-YK4WAAS4.js";
|
|
8
8
|
import "../chunk-XUUY2L2D.js";
|
|
9
|
-
import "../chunk-
|
|
10
|
-
import "../chunk-
|
|
9
|
+
import "../chunk-2NDQHV7V.js";
|
|
10
|
+
import "../chunk-4BGN6PYF.js";
|
|
11
11
|
import "../chunk-2ODTMRHP.js";
|
|
12
12
|
import "../chunk-UQX6EXWG.js";
|
|
13
13
|
import "../chunk-MRQLEK2B.js";
|
|
14
|
+
import "../chunk-Q25IWLBT.js";
|
|
15
|
+
import "../chunk-MC3CCKEB.js";
|
|
16
|
+
import {
|
|
17
|
+
Usage
|
|
18
|
+
} from "../chunk-ADH7NNCS.js";
|
|
19
|
+
import "../chunk-WQKHFADH.js";
|
|
14
20
|
import {
|
|
15
21
|
cosineSimilarity,
|
|
16
22
|
embedText
|
|
@@ -18,12 +24,6 @@ import {
|
|
|
18
24
|
import {
|
|
19
25
|
mapWithConcurrency
|
|
20
26
|
} from "../chunk-OIMLU4SF.js";
|
|
21
|
-
import "../chunk-Q25IWLBT.js";
|
|
22
|
-
import "../chunk-MC3CCKEB.js";
|
|
23
|
-
import {
|
|
24
|
-
Usage
|
|
25
|
-
} from "../chunk-ADH7NNCS.js";
|
|
26
|
-
import "../chunk-WQKHFADH.js";
|
|
27
27
|
import "../chunk-CWUJUSOS.js";
|
|
28
28
|
|
|
29
29
|
// src/evals/advanced-metrics.ts
|
|
@@ -100,6 +100,9 @@ var EvalOutcome = {
|
|
|
100
100
|
if (options.metadata !== void 0) {
|
|
101
101
|
outcome.metadata = options.metadata;
|
|
102
102
|
}
|
|
103
|
+
if (options.usage !== void 0) {
|
|
104
|
+
outcome.usage = options.usage;
|
|
105
|
+
}
|
|
103
106
|
return outcome;
|
|
104
107
|
},
|
|
105
108
|
fail(score, options = {}) {
|
|
@@ -115,6 +118,9 @@ var EvalOutcome = {
|
|
|
115
118
|
if (options.metadata !== void 0) {
|
|
116
119
|
outcome.metadata = options.metadata;
|
|
117
120
|
}
|
|
121
|
+
if (options.usage !== void 0) {
|
|
122
|
+
outcome.usage = options.usage;
|
|
123
|
+
}
|
|
118
124
|
return outcome;
|
|
119
125
|
},
|
|
120
126
|
invalid(reason, options = {}) {
|
|
@@ -131,11 +137,20 @@ var EvalOutcome = {
|
|
|
131
137
|
if (options.metadata !== void 0) {
|
|
132
138
|
outcome.metadata = options.metadata;
|
|
133
139
|
}
|
|
140
|
+
if (options.usage !== void 0) {
|
|
141
|
+
outcome.usage = options.usage;
|
|
142
|
+
}
|
|
134
143
|
return outcome;
|
|
135
144
|
}
|
|
136
145
|
};
|
|
137
146
|
|
|
138
147
|
// src/evals/selectors.ts
|
|
148
|
+
function selectPromptOutput(args) {
|
|
149
|
+
if (typeof args.output !== "object" || args.output === null || !("output" in args.output) || typeof args.output.output !== "string") {
|
|
150
|
+
throw new TypeError("selectPromptOutput requires an output object with a string output field.");
|
|
151
|
+
}
|
|
152
|
+
return args.output.output;
|
|
153
|
+
}
|
|
139
154
|
async function resolveActual(selector, args) {
|
|
140
155
|
return selector === void 0 ? defaultOutputValue(args.output) : selector(args);
|
|
141
156
|
}
|
|
@@ -188,9 +203,14 @@ var binaryVerdictSchema = z.object({
|
|
|
188
203
|
reason: z.string()
|
|
189
204
|
});
|
|
190
205
|
var reasonSchema = z.object({ reason: z.string() });
|
|
206
|
+
var abstentionJudgmentSchema = z.object({
|
|
207
|
+
behavior: z.enum(["abstention", "confident_answer"]),
|
|
208
|
+
grounded: z.boolean(),
|
|
209
|
+
reason: z.string()
|
|
210
|
+
});
|
|
191
211
|
function answerRelevancy(options) {
|
|
192
212
|
const config = metricConfig(options, "answer_relevancy");
|
|
193
|
-
return numericMetric(config.name, async (args) => {
|
|
213
|
+
return numericMetric(config.name, config, "higher_is_better", async (args) => {
|
|
194
214
|
try {
|
|
195
215
|
const input = await resolveInput(options.input, args);
|
|
196
216
|
const actual = await resolveActualText(options.actual, args);
|
|
@@ -245,7 +265,7 @@ function promptAlignment(options) {
|
|
|
245
265
|
throw new TypeError("promptAlignment requires at least one prompt instruction.");
|
|
246
266
|
}
|
|
247
267
|
const config = metricConfig(options, "prompt_alignment");
|
|
248
|
-
return numericMetric(config.name, async (args) => {
|
|
268
|
+
return numericMetric(config.name, config, "higher_is_better", async (args) => {
|
|
249
269
|
try {
|
|
250
270
|
const input = await resolveInput(options.input, args);
|
|
251
271
|
const actual = await resolveActualText(options.actual, args);
|
|
@@ -285,56 +305,65 @@ function jsonCorrectness(options) {
|
|
|
285
305
|
const threshold = validateThreshold(options.threshold ?? 0.5);
|
|
286
306
|
const retries = validateRetries(options.retries ?? 0);
|
|
287
307
|
const includeReason = options.includeReason ?? true;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
308
|
+
const strictMode = options.strictMode ?? true;
|
|
309
|
+
return numericMetric(
|
|
310
|
+
options.name ?? "json_correctness",
|
|
311
|
+
{
|
|
312
|
+
threshold: strictMode ? 1 : threshold,
|
|
313
|
+
required: options.required ?? true
|
|
314
|
+
},
|
|
315
|
+
"higher_is_better",
|
|
316
|
+
async (args) => {
|
|
293
317
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
318
|
+
const actual = await resolveActualText(options.actual, args);
|
|
319
|
+
let parsed;
|
|
320
|
+
let validationError;
|
|
321
|
+
try {
|
|
322
|
+
parsed = JSON.parse(actual);
|
|
323
|
+
const result = options.schema.safeParse(parsed);
|
|
324
|
+
if (!result.success) {
|
|
325
|
+
validationError = z.prettifyError(result.error);
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
validationError = errorMessage(error);
|
|
298
329
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
comment = reasonResult.data.reason;
|
|
319
|
-
usage = reasonResult.usage;
|
|
330
|
+
const score = validationError === void 0 ? 1 : 0;
|
|
331
|
+
let comment;
|
|
332
|
+
let usage = Usage.empty();
|
|
333
|
+
if (includeReason) {
|
|
334
|
+
if (score === 1) {
|
|
335
|
+
comment = "The generated JSON is syntactically valid and matches the expected schema.";
|
|
336
|
+
} else if (options.model === void 0) {
|
|
337
|
+
comment = validationError;
|
|
338
|
+
} else {
|
|
339
|
+
const reasonResult = await runJudge({
|
|
340
|
+
model: options.model,
|
|
341
|
+
schema: reasonSchema,
|
|
342
|
+
instructions: "Briefly explain why the generated JSON does not match the expected schema. Focus on actionable syntax, field, and type problems.",
|
|
343
|
+
prompt: jsonPrompt({ actual, validationError }),
|
|
344
|
+
retries
|
|
345
|
+
});
|
|
346
|
+
comment = reasonResult.data.reason;
|
|
347
|
+
usage = reasonResult.usage;
|
|
348
|
+
}
|
|
320
349
|
}
|
|
350
|
+
return higherOutcome({
|
|
351
|
+
score,
|
|
352
|
+
threshold,
|
|
353
|
+
strictMode,
|
|
354
|
+
comment,
|
|
355
|
+
details: validationError === void 0 ? {} : { validationError },
|
|
356
|
+
usage
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return EvalOutcome.invalid(errorMessage(error));
|
|
321
360
|
}
|
|
322
|
-
return higherOutcome({
|
|
323
|
-
score,
|
|
324
|
-
threshold,
|
|
325
|
-
strictMode: options.strictMode ?? true,
|
|
326
|
-
comment,
|
|
327
|
-
details: validationError === void 0 ? {} : { validationError },
|
|
328
|
-
usage
|
|
329
|
-
});
|
|
330
|
-
} catch (error) {
|
|
331
|
-
return EvalOutcome.invalid(errorMessage(error));
|
|
332
361
|
}
|
|
333
|
-
|
|
362
|
+
);
|
|
334
363
|
}
|
|
335
364
|
function hallucination(options) {
|
|
336
365
|
const config = metricConfig(options, "hallucination");
|
|
337
|
-
return numericMetric(config.name, async (args) => {
|
|
366
|
+
return numericMetric(config.name, config, "lower_is_better", async (args) => {
|
|
338
367
|
try {
|
|
339
368
|
const actual = await resolveActualText(options.actual, args);
|
|
340
369
|
const context = await resolveStringList(options.context, args.case.context, args, "context");
|
|
@@ -376,7 +405,7 @@ function faithfulness(options) {
|
|
|
376
405
|
options.truthsExtractionLimit,
|
|
377
406
|
"truthsExtractionLimit"
|
|
378
407
|
);
|
|
379
|
-
return numericMetric(config.name, async (args) => {
|
|
408
|
+
return numericMetric(config.name, config, "higher_is_better", async (args) => {
|
|
380
409
|
try {
|
|
381
410
|
const actual = await resolveActualText(options.actual, args);
|
|
382
411
|
const retrievalContext = await resolveStringList(
|
|
@@ -445,6 +474,70 @@ ${actual}`,
|
|
|
445
474
|
}
|
|
446
475
|
});
|
|
447
476
|
}
|
|
477
|
+
function abstention(options) {
|
|
478
|
+
const retries = validateRetries(options.retries ?? 0);
|
|
479
|
+
return {
|
|
480
|
+
name: options.name ?? "abstention",
|
|
481
|
+
required: options.required ?? true,
|
|
482
|
+
dataType: "CATEGORICAL",
|
|
483
|
+
async evaluate(args) {
|
|
484
|
+
try {
|
|
485
|
+
const actual = await resolveActualText(options.actual, args);
|
|
486
|
+
const shouldAbstain = await resolveExpected(options.shouldAbstain, args);
|
|
487
|
+
if (typeof shouldAbstain !== "boolean") {
|
|
488
|
+
return EvalOutcome.invalid("abstention shouldAbstain must resolve to a boolean.");
|
|
489
|
+
}
|
|
490
|
+
const context = await resolveAbstentionContext(options.context, args);
|
|
491
|
+
if (!shouldAbstain && context.length === 0) {
|
|
492
|
+
return EvalOutcome.invalid(
|
|
493
|
+
"abstention context must be non-empty when shouldAbstain is false."
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
const judgment = await runJudge({
|
|
497
|
+
model: options.model,
|
|
498
|
+
schema: abstentionJudgmentSchema,
|
|
499
|
+
instructions: "Classify whether the answer abstains or gives a confident answer. For a confident answer, grounded is true only when every substantive factual claim is supported by the supplied context. For an abstention, set grounded to false. Return a concise evidence-based reason.",
|
|
500
|
+
prompt: jsonPrompt({ actual, context }),
|
|
501
|
+
retries
|
|
502
|
+
});
|
|
503
|
+
const category = abstentionCategory(
|
|
504
|
+
shouldAbstain,
|
|
505
|
+
judgment.data.behavior,
|
|
506
|
+
judgment.data.grounded
|
|
507
|
+
);
|
|
508
|
+
const outcomeOptions = {
|
|
509
|
+
comment: options.includeReason === false ? void 0 : judgment.data.reason,
|
|
510
|
+
metadata: evaluationMetadata(
|
|
511
|
+
{
|
|
512
|
+
behavior: judgment.data.behavior,
|
|
513
|
+
grounded: judgment.data.grounded,
|
|
514
|
+
shouldAbstain
|
|
515
|
+
},
|
|
516
|
+
judgment.usage
|
|
517
|
+
),
|
|
518
|
+
usage: judgment.usage
|
|
519
|
+
};
|
|
520
|
+
return category === "correct_abstention" || category === "correct_grounded_answer" ? EvalOutcome.pass(category, outcomeOptions) : EvalOutcome.fail(category, outcomeOptions);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return EvalOutcome.invalid(errorMessage(error));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
async function resolveAbstentionContext(selectorOrValue, args) {
|
|
528
|
+
const context = selectorOrValue === void 0 ? args.case.retrievalContext ?? [] : typeof selectorOrValue === "function" ? await selectorOrValue(args) : selectorOrValue;
|
|
529
|
+
if (!Array.isArray(context) || context.some((value) => typeof value !== "string")) {
|
|
530
|
+
throw new TypeError("abstention context must be an array of strings.");
|
|
531
|
+
}
|
|
532
|
+
return context;
|
|
533
|
+
}
|
|
534
|
+
function abstentionCategory(shouldAbstain, behavior, grounded) {
|
|
535
|
+
if (behavior === "abstention") {
|
|
536
|
+
return shouldAbstain ? "correct_abstention" : "unnecessary_abstention";
|
|
537
|
+
}
|
|
538
|
+
if (shouldAbstain || !grounded) return "unsupported_confident_answer";
|
|
539
|
+
return "correct_grounded_answer";
|
|
540
|
+
}
|
|
448
541
|
function summarization(options) {
|
|
449
542
|
const config = metricConfig(options, "summarization");
|
|
450
543
|
const questionCount = validatePositiveInteger(options.questionCount ?? 5, "questionCount");
|
|
@@ -453,7 +546,7 @@ function summarization(options) {
|
|
|
453
546
|
"truthsExtractionLimit"
|
|
454
547
|
);
|
|
455
548
|
const suppliedQuestions = options.assessmentQuestions !== void 0 && options.assessmentQuestions.length > 0 ? [...options.assessmentQuestions] : void 0;
|
|
456
|
-
return numericMetric(config.name, async (args) => {
|
|
549
|
+
return numericMetric(config.name, config, "higher_is_better", async (args) => {
|
|
457
550
|
try {
|
|
458
551
|
const input = await resolveInput(options.input, args);
|
|
459
552
|
const actual = await resolveActualText(options.actual, args);
|
|
@@ -615,7 +708,7 @@ function gEval(options) {
|
|
|
615
708
|
generatedUsageClaimed = true;
|
|
616
709
|
return { steps: result.data.steps, usage };
|
|
617
710
|
}
|
|
618
|
-
return numericMetric(config.name, async (args) => {
|
|
711
|
+
return numericMetric(config.name, config, "higher_is_better", async (args) => {
|
|
619
712
|
try {
|
|
620
713
|
const parameters = await resolveGEvalParameters(options, args);
|
|
621
714
|
const stepsResult = await resolveSteps();
|
|
@@ -662,7 +755,7 @@ function turnRelevancy(options) {
|
|
|
662
755
|
const config = metricConfig(options, "turn_relevancy");
|
|
663
756
|
const windowSize = validatePositiveInteger(options.windowSize ?? 10, "windowSize");
|
|
664
757
|
const concurrency = validatePositiveInteger(options.concurrency ?? 4, "concurrency");
|
|
665
|
-
return numericMetric(config.name, async (args) => {
|
|
758
|
+
return numericMetric(config.name, config, "higher_is_better", async (args) => {
|
|
666
759
|
try {
|
|
667
760
|
const turns = await resolveTurns(options.turns, args);
|
|
668
761
|
const interactions = unitInteractions(turns);
|
|
@@ -710,7 +803,7 @@ function turnRelevancy(options) {
|
|
|
710
803
|
function knowledgeRetention(options) {
|
|
711
804
|
const config = metricConfig(options, "knowledge_retention");
|
|
712
805
|
const concurrency = validatePositiveInteger(options.concurrency ?? 4, "concurrency");
|
|
713
|
-
return numericMetric(config.name, async (args) => {
|
|
806
|
+
return numericMetric(config.name, config, "higher_is_better", async (args) => {
|
|
714
807
|
try {
|
|
715
808
|
const turns = await resolveTurns(options.turns, args);
|
|
716
809
|
const userTurns = turns.map((turn, index) => ({ turn, index })).filter((entry) => entry.turn.role === "user");
|
|
@@ -779,8 +872,15 @@ function knowledgeRetention(options) {
|
|
|
779
872
|
}
|
|
780
873
|
});
|
|
781
874
|
}
|
|
782
|
-
function numericMetric(name, evaluate) {
|
|
783
|
-
return {
|
|
875
|
+
function numericMetric(name, config, direction, evaluate) {
|
|
876
|
+
return {
|
|
877
|
+
name,
|
|
878
|
+
required: config.required,
|
|
879
|
+
direction,
|
|
880
|
+
threshold: config.strictMode === true ? direction === "higher_is_better" ? 1 : 0 : config.threshold,
|
|
881
|
+
dataType: "NUMERIC",
|
|
882
|
+
evaluate
|
|
883
|
+
};
|
|
784
884
|
}
|
|
785
885
|
function metricConfig(options, defaultName) {
|
|
786
886
|
return {
|
|
@@ -788,7 +888,8 @@ function metricConfig(options, defaultName) {
|
|
|
788
888
|
threshold: validateThreshold(options.threshold ?? 0.5),
|
|
789
889
|
strictMode: options.strictMode ?? false,
|
|
790
890
|
includeReason: options.includeReason ?? true,
|
|
791
|
-
retries: validateRetries(options.retries ?? 0)
|
|
891
|
+
retries: validateRetries(options.retries ?? 0),
|
|
892
|
+
required: options.required ?? true
|
|
792
893
|
};
|
|
793
894
|
}
|
|
794
895
|
function higherOutcome(args) {
|
|
@@ -796,6 +897,7 @@ function higherOutcome(args) {
|
|
|
796
897
|
const threshold = args.strictMode ? 1 : args.threshold;
|
|
797
898
|
const options = {
|
|
798
899
|
comment: args.comment,
|
|
900
|
+
usage: args.usage,
|
|
799
901
|
metadata: evaluationMetadata(
|
|
800
902
|
{
|
|
801
903
|
...args.details,
|
|
@@ -813,6 +915,7 @@ function lowerOutcome(args) {
|
|
|
813
915
|
const threshold = args.strictMode ? 0 : args.threshold;
|
|
814
916
|
const options = {
|
|
815
917
|
comment: args.comment,
|
|
918
|
+
usage: args.usage,
|
|
816
919
|
metadata: evaluationMetadata(
|
|
817
920
|
{
|
|
818
921
|
...args.details,
|
|
@@ -1021,122 +1124,9 @@ function agentEvalTarget(agent, options = {}) {
|
|
|
1021
1124
|
};
|
|
1022
1125
|
}
|
|
1023
1126
|
|
|
1024
|
-
// src/evals/metric.ts
|
|
1025
|
-
function defineMetric(metric) {
|
|
1026
|
-
return metric;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// src/evals/metrics.ts
|
|
1030
|
-
import { z as z2 } from "zod";
|
|
1031
|
-
function exactMatch(options = {}) {
|
|
1032
|
-
return {
|
|
1033
|
-
name: options.name ?? "exact_match",
|
|
1034
|
-
async evaluate(args) {
|
|
1035
|
-
const actual = await resolveActual(options.actual, args);
|
|
1036
|
-
const expected = await resolveExpected(options.expected, args);
|
|
1037
|
-
if (expected === void 0) {
|
|
1038
|
-
return EvalOutcome.invalid("No expected value provided for exact match.");
|
|
1039
|
-
}
|
|
1040
|
-
const passed = stableComparable(actual) === stableComparable(expected);
|
|
1041
|
-
return passed ? EvalOutcome.pass(true) : EvalOutcome.fail(false, { comment: `Expected ${formatValue(expected)}.` });
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
function contains(options = {}) {
|
|
1046
|
-
return {
|
|
1047
|
-
name: options.name ?? "contains",
|
|
1048
|
-
async evaluate(args) {
|
|
1049
|
-
const actual = await resolveActualText(options.actual, args);
|
|
1050
|
-
const expected = await resolveExpected(options.expected, args);
|
|
1051
|
-
if (expected === void 0) {
|
|
1052
|
-
return EvalOutcome.invalid("No expected value provided for contains.");
|
|
1053
|
-
}
|
|
1054
|
-
if (typeof expected !== "string" && !(expected instanceof RegExp)) {
|
|
1055
|
-
return EvalOutcome.invalid("Contains expected value must be a string or RegExp.");
|
|
1056
|
-
}
|
|
1057
|
-
const passed = expected instanceof RegExp ? regexMatches(expected, actual) : actual.includes(expected);
|
|
1058
|
-
return passed ? EvalOutcome.pass(true) : EvalOutcome.fail(false, { comment: `Output did not contain ${String(expected)}.` });
|
|
1059
|
-
}
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
function regexMatches(pattern, text) {
|
|
1063
|
-
pattern.lastIndex = 0;
|
|
1064
|
-
const matched = pattern.test(text);
|
|
1065
|
-
pattern.lastIndex = 0;
|
|
1066
|
-
return matched;
|
|
1067
|
-
}
|
|
1068
|
-
function semanticSimilarity(options) {
|
|
1069
|
-
return {
|
|
1070
|
-
name: options.name ?? "semantic_similarity",
|
|
1071
|
-
async evaluate(args) {
|
|
1072
|
-
const actual = await resolveActualText(options.actual, args);
|
|
1073
|
-
const expected = await resolveExpected(options.expected, args);
|
|
1074
|
-
if (expected === void 0) {
|
|
1075
|
-
return EvalOutcome.invalid("No expected value provided for semantic similarity.");
|
|
1076
|
-
}
|
|
1077
|
-
if (typeof expected !== "string") {
|
|
1078
|
-
return EvalOutcome.invalid("Semantic similarity expected value must be a string.");
|
|
1079
|
-
}
|
|
1080
|
-
const [actualEmbedding, expectedEmbedding] = await Promise.all([
|
|
1081
|
-
embedText(options.model, actual),
|
|
1082
|
-
embedText(options.model, expected)
|
|
1083
|
-
]);
|
|
1084
|
-
const score = cosineSimilarity(actualEmbedding.vector, expectedEmbedding.vector);
|
|
1085
|
-
return score >= options.threshold ? EvalOutcome.pass(score) : EvalOutcome.fail(score, { comment: `Similarity below threshold ${options.threshold}.` });
|
|
1086
|
-
}
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
function llmJudge(options) {
|
|
1090
|
-
const extractor = new ExtractorBuilder(options.model, options.schema).instructions(
|
|
1091
|
-
options.instructions ?? "Judge the eval case by the requested schema. Submit the judgment using the schema."
|
|
1092
|
-
).retries(options.retries ?? 0).build();
|
|
1093
|
-
return {
|
|
1094
|
-
name: options.name ?? "llm_judge",
|
|
1095
|
-
async evaluate(args) {
|
|
1096
|
-
try {
|
|
1097
|
-
const judgment = await extractor.extract(await resolveJudgePrompt(options.prompt, args));
|
|
1098
|
-
return options.passes(judgment) ? EvalOutcome.pass(judgment) : EvalOutcome.fail(judgment);
|
|
1099
|
-
} catch (error) {
|
|
1100
|
-
return EvalOutcome.invalid(errorMessage(error));
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
function llmScore(options) {
|
|
1106
|
-
const criteria = Array.isArray(options.criteria) ? options.criteria.join("\n") : options.criteria;
|
|
1107
|
-
const extractor = new ExtractorBuilder(
|
|
1108
|
-
options.model,
|
|
1109
|
-
z2.object({
|
|
1110
|
-
score: z2.number(),
|
|
1111
|
-
feedback: z2.string()
|
|
1112
|
-
})
|
|
1113
|
-
).instructions(
|
|
1114
|
-
options.instructions ?? `Score the eval case against these criteria:
|
|
1115
|
-
${criteria}
|
|
1116
|
-
|
|
1117
|
-
Return a score between 0 and 1 and brief feedback.`
|
|
1118
|
-
).retries(options.retries ?? 0).build();
|
|
1119
|
-
return {
|
|
1120
|
-
name: options.name ?? "llm_score",
|
|
1121
|
-
async evaluate(args) {
|
|
1122
|
-
try {
|
|
1123
|
-
const score = await extractor.extract(await resolveJudgePrompt(options.prompt, args));
|
|
1124
|
-
if (score.score < 0 || score.score > 1) {
|
|
1125
|
-
return EvalOutcome.invalid(`Score ${score.score} outside valid range [0, 1].`, {
|
|
1126
|
-
score
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
return score.score >= options.threshold ? EvalOutcome.pass(score, { comment: score.feedback }) : EvalOutcome.fail(score, { comment: score.feedback });
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
return EvalOutcome.invalid(errorMessage(error));
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
1127
|
// src/evals/reporting.ts
|
|
1138
|
-
function projectEvalOutcome(outcome, dataType) {
|
|
1139
|
-
const value = projectScoreValue(outcome, dataType);
|
|
1128
|
+
function projectEvalOutcome(outcome, dataType, projectScore) {
|
|
1129
|
+
const value = projectScoreValue(outcome, dataType, projectScore);
|
|
1140
1130
|
const projection = {
|
|
1141
1131
|
outcome: outcome.outcome,
|
|
1142
1132
|
value,
|
|
@@ -1158,8 +1148,13 @@ function defaultEvalTraceSelector(args) {
|
|
|
1158
1148
|
metadata: args.case.metadata
|
|
1159
1149
|
});
|
|
1160
1150
|
}
|
|
1161
|
-
function projectScoreValue(outcome, dataType) {
|
|
1151
|
+
function projectScoreValue(outcome, dataType, projectScore) {
|
|
1162
1152
|
const score = outcome.score;
|
|
1153
|
+
if (score !== void 0 && projectScore !== void 0) {
|
|
1154
|
+
const projected = projectScore(score);
|
|
1155
|
+
if (typeof projected === "boolean") return projected ? 1 : 0;
|
|
1156
|
+
return projected;
|
|
1157
|
+
}
|
|
1163
1158
|
if (dataType === "CATEGORICAL") {
|
|
1164
1159
|
if (typeof score === "string") return score;
|
|
1165
1160
|
if (typeof score === "number") return String(score);
|
|
@@ -1211,21 +1206,105 @@ function readTraceRef(value) {
|
|
|
1211
1206
|
|
|
1212
1207
|
// src/evals/runner.ts
|
|
1213
1208
|
async function runEvalSuite(options) {
|
|
1214
|
-
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1209
|
+
validateSuiteOptions(options);
|
|
1210
|
+
const startedAtMs = Date.now();
|
|
1211
|
+
const run = resolveRun(options, startedAtMs);
|
|
1212
|
+
const reporters = options.reporters ?? [];
|
|
1213
|
+
const lifecycle = {
|
|
1214
|
+
run,
|
|
1215
|
+
suiteName: options.name,
|
|
1216
|
+
caseCount: options.cases.length,
|
|
1217
|
+
metricNames: options.metrics.map((metric) => metric.name)
|
|
1218
|
+
};
|
|
1219
|
+
let reporterErrors;
|
|
1220
|
+
try {
|
|
1221
|
+
reporterErrors = await notifyRunStart(
|
|
1222
|
+
reporters,
|
|
1223
|
+
lifecycle,
|
|
1224
|
+
options.failOnReporterError === true
|
|
1225
|
+
);
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
await notifyRunEnd(reporters, {
|
|
1228
|
+
...lifecycle,
|
|
1229
|
+
status: "failed",
|
|
1230
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1231
|
+
durationMs: Date.now() - startedAtMs,
|
|
1232
|
+
error
|
|
1233
|
+
});
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
1236
|
+
let results;
|
|
1237
|
+
let aggregates;
|
|
1238
|
+
try {
|
|
1239
|
+
results = await runEvalCases(options, run);
|
|
1240
|
+
aggregates = await aggregateResult(options, results);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
await notifyRunEnd(reporters, {
|
|
1243
|
+
...lifecycle,
|
|
1244
|
+
status: "failed",
|
|
1245
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1246
|
+
durationMs: Date.now() - startedAtMs,
|
|
1247
|
+
error
|
|
1248
|
+
});
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1252
|
+
const result = {
|
|
1222
1253
|
name: options.name,
|
|
1254
|
+
run: { ...run, completedAt },
|
|
1223
1255
|
results,
|
|
1224
|
-
|
|
1225
|
-
|
|
1256
|
+
metrics: aggregates.metrics,
|
|
1257
|
+
cases: aggregates.cases,
|
|
1258
|
+
usage: aggregates.usage,
|
|
1259
|
+
...aggregates.cost === void 0 ? {} : { cost: aggregates.cost },
|
|
1260
|
+
durationMs: Date.now() - startedAtMs,
|
|
1261
|
+
reporterErrors
|
|
1226
1262
|
};
|
|
1263
|
+
result.reporterErrors.push(
|
|
1264
|
+
...await notifyRunEnd(
|
|
1265
|
+
reporters,
|
|
1266
|
+
{
|
|
1267
|
+
...lifecycle,
|
|
1268
|
+
status: "completed",
|
|
1269
|
+
completedAt,
|
|
1270
|
+
durationMs: result.durationMs,
|
|
1271
|
+
metrics: result.metrics,
|
|
1272
|
+
cases: result.cases,
|
|
1273
|
+
usage: result.usage,
|
|
1274
|
+
...result.cost === void 0 ? {} : { cost: result.cost }
|
|
1275
|
+
},
|
|
1276
|
+
options.failOnReporterError === true
|
|
1277
|
+
)
|
|
1278
|
+
);
|
|
1279
|
+
return result;
|
|
1280
|
+
}
|
|
1281
|
+
async function runEvalCases(options, run) {
|
|
1282
|
+
const concurrency = Math.max(1, Math.trunc(options.concurrency ?? 1));
|
|
1283
|
+
const results = new Array(options.cases.length);
|
|
1284
|
+
let nextIndex = 0;
|
|
1285
|
+
let failure;
|
|
1286
|
+
async function worker() {
|
|
1287
|
+
while (failure === void 0 && nextIndex < options.cases.length) {
|
|
1288
|
+
const index = nextIndex;
|
|
1289
|
+
nextIndex += 1;
|
|
1290
|
+
try {
|
|
1291
|
+
results[index] = await runEvalCase(
|
|
1292
|
+
options,
|
|
1293
|
+
options.cases[index],
|
|
1294
|
+
run
|
|
1295
|
+
);
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
failure ??= { error };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
await Promise.all(
|
|
1302
|
+
Array.from({ length: Math.min(concurrency, options.cases.length) }, () => worker())
|
|
1303
|
+
);
|
|
1304
|
+
if (failure !== void 0) throw failure.error;
|
|
1305
|
+
return results;
|
|
1227
1306
|
}
|
|
1228
|
-
async function runEvalCase(options, testCase) {
|
|
1307
|
+
async function runEvalCase(options, testCase, run) {
|
|
1229
1308
|
let output;
|
|
1230
1309
|
let targetError;
|
|
1231
1310
|
try {
|
|
@@ -1238,6 +1317,7 @@ async function runEvalCase(options, testCase) {
|
|
|
1238
1317
|
for (const metric of options.metrics) {
|
|
1239
1318
|
const outcome = targetError === void 0 ? await safeEvaluate(options.name, testCase, output, metric) : EvalOutcome.invalid(`Target failed: ${errorMessage(targetError)}`);
|
|
1240
1319
|
const reporterErrors = await reportOutcome({
|
|
1320
|
+
run,
|
|
1241
1321
|
suiteName: options.name,
|
|
1242
1322
|
testCase,
|
|
1243
1323
|
output,
|
|
@@ -1249,11 +1329,24 @@ async function runEvalCase(options, testCase) {
|
|
|
1249
1329
|
reporters: options.reporters ?? [],
|
|
1250
1330
|
failOnReporterError: options.failOnReporterError === true
|
|
1251
1331
|
});
|
|
1252
|
-
|
|
1332
|
+
const metricResult = {
|
|
1333
|
+
metricName: metric.name,
|
|
1334
|
+
required: metric.required ?? true,
|
|
1335
|
+
outcome,
|
|
1336
|
+
reporterErrors
|
|
1337
|
+
};
|
|
1338
|
+
if (metric.direction !== void 0) metricResult.direction = metric.direction;
|
|
1339
|
+
if (metric.threshold !== void 0) metricResult.threshold = metric.threshold;
|
|
1340
|
+
metrics.push(metricResult);
|
|
1253
1341
|
}
|
|
1342
|
+
const scores = Object.fromEntries(
|
|
1343
|
+
metrics.map((metric) => [metric.metricName, metric.outcome])
|
|
1344
|
+
);
|
|
1254
1345
|
const result = {
|
|
1255
1346
|
case: testCase,
|
|
1256
|
-
metrics
|
|
1347
|
+
outcome: caseOutcome(targetError, metrics),
|
|
1348
|
+
metrics,
|
|
1349
|
+
scores
|
|
1257
1350
|
};
|
|
1258
1351
|
if (output !== void 0) {
|
|
1259
1352
|
result.output = output;
|
|
@@ -1294,6 +1387,7 @@ async function reportOutcome(args) {
|
|
|
1294
1387
|
for (const reporter of args.reporters) {
|
|
1295
1388
|
try {
|
|
1296
1389
|
await reporter.report({
|
|
1390
|
+
run: args.run,
|
|
1297
1391
|
suiteName: args.suiteName,
|
|
1298
1392
|
case: args.testCase,
|
|
1299
1393
|
output: args.output,
|
|
@@ -1311,26 +1405,695 @@ async function reportOutcome(args) {
|
|
|
1311
1405
|
}
|
|
1312
1406
|
return errors;
|
|
1313
1407
|
}
|
|
1314
|
-
function
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1408
|
+
function resolveRun(options, startedAtMs) {
|
|
1409
|
+
const id = options.run?.id ?? globalThis.crypto.randomUUID();
|
|
1410
|
+
if (id.trim().length === 0 || id.length > 128) {
|
|
1411
|
+
throw new TypeError("Evaluation run id must contain 1 to 128 characters");
|
|
1412
|
+
}
|
|
1413
|
+
for (const [label, value] of [
|
|
1414
|
+
["dataset name", options.run?.datasetName],
|
|
1415
|
+
["dataset version", options.run?.datasetVersion]
|
|
1416
|
+
]) {
|
|
1417
|
+
if (value !== void 0 && (value.trim().length === 0 || value.length > 256)) {
|
|
1418
|
+
throw new TypeError(`Evaluation run ${label} must contain 1 to 256 characters`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
id,
|
|
1423
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
1424
|
+
...options.run?.datasetName === void 0 ? {} : { datasetName: options.run.datasetName },
|
|
1425
|
+
...options.run?.datasetVersion === void 0 ? {} : { datasetVersion: options.run.datasetVersion },
|
|
1426
|
+
...options.run?.metadata === void 0 ? {} : { metadata: options.run.metadata }
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
async function notifyRunStart(reporters, args, failOnReporterError) {
|
|
1430
|
+
const errors = [];
|
|
1431
|
+
for (const reporter of reporters) {
|
|
1432
|
+
if (reporter.onRunStart === void 0) continue;
|
|
1433
|
+
try {
|
|
1434
|
+
await reporter.onRunStart(args);
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
if (failOnReporterError) throw error;
|
|
1437
|
+
errors.push(error);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return errors;
|
|
1441
|
+
}
|
|
1442
|
+
async function notifyRunEnd(reporters, args, failOnReporterError = false) {
|
|
1443
|
+
const errors = [];
|
|
1444
|
+
for (const reporter of reporters) {
|
|
1445
|
+
if (reporter.onRunEnd === void 0) continue;
|
|
1446
|
+
try {
|
|
1447
|
+
await reporter.onRunEnd(args);
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
if (failOnReporterError) throw error;
|
|
1450
|
+
errors.push(error);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return errors;
|
|
1454
|
+
}
|
|
1455
|
+
function countMetricOutcomes(results) {
|
|
1456
|
+
const totals = emptyTotals();
|
|
1318
1457
|
for (const result of results) {
|
|
1319
1458
|
for (const metric of result.metrics) {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1459
|
+
totals.total += 1;
|
|
1460
|
+
totals[statusKey(metric.outcome.outcome)] += 1;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return totals;
|
|
1464
|
+
}
|
|
1465
|
+
function countCaseOutcomes(results) {
|
|
1466
|
+
const totals = emptyTotals();
|
|
1467
|
+
for (const result of results) {
|
|
1468
|
+
totals.total += 1;
|
|
1469
|
+
totals[statusKey(result.outcome)] += 1;
|
|
1470
|
+
}
|
|
1471
|
+
return totals;
|
|
1472
|
+
}
|
|
1473
|
+
function emptyTotals() {
|
|
1474
|
+
return { total: 0, passed: 0, failed: 0, invalid: 0 };
|
|
1475
|
+
}
|
|
1476
|
+
function statusKey(status) {
|
|
1477
|
+
if (status === "pass") return "passed";
|
|
1478
|
+
if (status === "fail") return "failed";
|
|
1479
|
+
return "invalid";
|
|
1480
|
+
}
|
|
1481
|
+
function caseOutcome(targetError, metrics) {
|
|
1482
|
+
if (targetError !== void 0) return "invalid";
|
|
1483
|
+
const required = metrics.filter((metric) => metric.required);
|
|
1484
|
+
if (required.some((metric) => metric.outcome.outcome === "invalid")) return "invalid";
|
|
1485
|
+
if (required.some((metric) => metric.outcome.outcome === "fail")) return "fail";
|
|
1486
|
+
return "pass";
|
|
1487
|
+
}
|
|
1488
|
+
async function aggregateResult(options, results) {
|
|
1489
|
+
let targetUsage = Usage.empty();
|
|
1490
|
+
let evaluationUsage = Usage.empty();
|
|
1491
|
+
let targetCost = 0;
|
|
1492
|
+
let evaluationCost = 0;
|
|
1493
|
+
for (const result of results) {
|
|
1494
|
+
if (result.output !== void 0) {
|
|
1495
|
+
const usage2 = await resolveTargetUsage(options, result.case, result.output);
|
|
1496
|
+
if (usage2 !== void 0) {
|
|
1497
|
+
targetUsage = Usage.add(targetUsage, usage2);
|
|
1498
|
+
if (options.cost !== void 0) {
|
|
1499
|
+
targetCost += await calculateCost(
|
|
1500
|
+
options.cost.calculate({
|
|
1501
|
+
kind: "target",
|
|
1502
|
+
suiteName: options.name,
|
|
1503
|
+
case: result.case,
|
|
1504
|
+
output: result.output,
|
|
1505
|
+
usage: usage2
|
|
1506
|
+
})
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
for (const metricResult of result.metrics) {
|
|
1512
|
+
const usage2 = metricResult.outcome.usage;
|
|
1513
|
+
if (usage2 === void 0) continue;
|
|
1514
|
+
assertUsage(usage2, `Evaluation usage for metric ${metricResult.metricName}`);
|
|
1515
|
+
evaluationUsage = Usage.add(evaluationUsage, usage2);
|
|
1516
|
+
if (options.cost !== void 0 && result.output !== void 0) {
|
|
1517
|
+
const metric = options.metrics.find(
|
|
1518
|
+
(candidate) => candidate.name === metricResult.metricName
|
|
1519
|
+
);
|
|
1520
|
+
if (metric !== void 0) {
|
|
1521
|
+
evaluationCost += await calculateCost(
|
|
1522
|
+
options.cost.calculate({
|
|
1523
|
+
kind: "evaluation",
|
|
1524
|
+
suiteName: options.name,
|
|
1525
|
+
case: result.case,
|
|
1526
|
+
output: result.output,
|
|
1527
|
+
metric,
|
|
1528
|
+
usage: usage2
|
|
1529
|
+
})
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1323
1533
|
}
|
|
1324
1534
|
}
|
|
1325
|
-
|
|
1535
|
+
const usage = {
|
|
1536
|
+
target: targetUsage,
|
|
1537
|
+
evaluation: evaluationUsage,
|
|
1538
|
+
total: Usage.add(targetUsage, evaluationUsage)
|
|
1539
|
+
};
|
|
1540
|
+
const aggregates = {
|
|
1541
|
+
metrics: countMetricOutcomes(results),
|
|
1542
|
+
cases: countCaseOutcomes(results),
|
|
1543
|
+
usage
|
|
1544
|
+
};
|
|
1545
|
+
if (options.cost !== void 0) {
|
|
1546
|
+
aggregates.cost = {
|
|
1547
|
+
currency: options.cost.currency,
|
|
1548
|
+
target: targetCost,
|
|
1549
|
+
evaluation: evaluationCost,
|
|
1550
|
+
total: targetCost + evaluationCost
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
return aggregates;
|
|
1554
|
+
}
|
|
1555
|
+
async function resolveTargetUsage(options, testCase, output) {
|
|
1556
|
+
const usage = options.targetUsage === void 0 ? usageFromOutput(output) : await options.targetUsage({ suiteName: options.name, case: testCase, output });
|
|
1557
|
+
if (usage !== void 0) assertUsage(usage, `Target usage for case ${testCase.id}`);
|
|
1558
|
+
return usage;
|
|
1559
|
+
}
|
|
1560
|
+
function usageFromOutput(output) {
|
|
1561
|
+
if (typeof output !== "object" || output === null || !("usage" in output)) return void 0;
|
|
1562
|
+
return output.usage;
|
|
1563
|
+
}
|
|
1564
|
+
function assertUsage(usage, label) {
|
|
1565
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
1566
|
+
if (key === "details") continue;
|
|
1567
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
1568
|
+
throw new TypeError(`${label} must contain finite, non-negative token counts`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async function calculateCost(value) {
|
|
1573
|
+
const cost = await value;
|
|
1574
|
+
if (!Number.isFinite(cost) || cost < 0) {
|
|
1575
|
+
throw new TypeError("Evaluation cost calculator must return a finite, non-negative number");
|
|
1576
|
+
}
|
|
1577
|
+
return cost;
|
|
1578
|
+
}
|
|
1579
|
+
function validateSuiteOptions(options) {
|
|
1580
|
+
assertUnique(
|
|
1581
|
+
options.cases.map((testCase) => testCase.id),
|
|
1582
|
+
"Evaluation case id"
|
|
1583
|
+
);
|
|
1584
|
+
assertUnique(
|
|
1585
|
+
options.metrics.map((metric) => metric.name),
|
|
1586
|
+
"Evaluation metric name"
|
|
1587
|
+
);
|
|
1588
|
+
if (options.cost !== void 0 && options.cost.currency.trim().length === 0) {
|
|
1589
|
+
throw new TypeError("Evaluation cost currency must not be empty");
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function assertUnique(values, label) {
|
|
1593
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1594
|
+
for (const value of values) {
|
|
1595
|
+
if (seen.has(value)) throw new TypeError(`${label} must be unique: ${value}`);
|
|
1596
|
+
seen.add(value);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/evals/cli.ts
|
|
1601
|
+
var EvalAssertionError = class extends Error {
|
|
1602
|
+
mismatches;
|
|
1603
|
+
constructor(message, mismatches) {
|
|
1604
|
+
super(`${message}
|
|
1605
|
+
${mismatches.map((mismatch) => `- ${mismatch}`).join("\n")}`);
|
|
1606
|
+
this.name = "EvalAssertionError";
|
|
1607
|
+
this.mismatches = mismatches;
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
function printEvalResult(result, options = {}) {
|
|
1611
|
+
const format = options.format ?? "pretty";
|
|
1612
|
+
if (format === "quiet") return;
|
|
1613
|
+
const write = options.output?.stdout ?? ((text) => process.stdout.write(text));
|
|
1614
|
+
write(`${format === "json" ? jsonResult(result) : prettyResult(result)}
|
|
1615
|
+
`);
|
|
1616
|
+
}
|
|
1617
|
+
function evalExitCode(result, expectations) {
|
|
1618
|
+
const hasExpectations = expectations?.totals !== void 0 || expectations?.outcomes !== void 0;
|
|
1619
|
+
const mismatches = hasExpectations ? expectationMismatches(result, expectations) : [];
|
|
1620
|
+
if (hasExpectations && mismatches.length === 0) return 0;
|
|
1621
|
+
if (hasExpectations && hasUnexpectedInvalid(result, expectations)) return 2;
|
|
1622
|
+
if (hasExpectations) return 1;
|
|
1623
|
+
if (requiredMetricCount(result, "invalid") > 0) return 2;
|
|
1624
|
+
return requiredMetricCount(result, "fail") > 0 ? 1 : 0;
|
|
1625
|
+
}
|
|
1626
|
+
function assertEvalTotals(result, expected) {
|
|
1627
|
+
const mismatches = totalMismatches(result, expected);
|
|
1628
|
+
if (mismatches.length > 0) {
|
|
1629
|
+
throw new EvalAssertionError("Evaluation totals did not match expectations.", mismatches);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function assertEvalOutcomes(result, expected) {
|
|
1633
|
+
const mismatches = outcomeMismatches(result, expected);
|
|
1634
|
+
if (mismatches.length > 0) {
|
|
1635
|
+
throw new EvalAssertionError("Evaluation outcomes did not match expectations.", mismatches);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
async function runEvalCli(options) {
|
|
1639
|
+
const { format, exitCode, expectations, output, ...suiteOptions } = options;
|
|
1640
|
+
const result = await runEvalSuite(
|
|
1641
|
+
suiteOptions
|
|
1642
|
+
);
|
|
1643
|
+
printEvalResult(result, { format, output });
|
|
1644
|
+
const code = evalExitCode(result, expectations);
|
|
1645
|
+
const mismatches = expectationMismatches(result, expectations);
|
|
1646
|
+
if (mismatches.length > 0 && format !== "quiet") {
|
|
1647
|
+
const write = output?.stderr ?? ((text) => process.stderr.write(text));
|
|
1648
|
+
write(
|
|
1649
|
+
`Evaluation expectation mismatches:
|
|
1650
|
+
${mismatches.map((value) => `- ${value}`).join("\n")}
|
|
1651
|
+
`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
const currentExitCode = typeof process.exitCode === "number" ? process.exitCode : Number(process.exitCode ?? 0);
|
|
1655
|
+
if (exitCode === true && code > currentExitCode) process.exitCode = code;
|
|
1656
|
+
return result;
|
|
1657
|
+
}
|
|
1658
|
+
function prettyResult(result) {
|
|
1659
|
+
const lines = [
|
|
1660
|
+
`${result.name} (${result.run.id})`,
|
|
1661
|
+
`Cases: ${totalsText(result.cases)}`,
|
|
1662
|
+
`Metrics: ${totalsText(result.metrics)}`
|
|
1663
|
+
];
|
|
1664
|
+
for (const caseResult of result.results) {
|
|
1665
|
+
lines.push(``, `[${caseResult.outcome.toUpperCase()}] ${caseResult.case.id}`);
|
|
1666
|
+
if (caseResult.output !== void 0) lines.push(` output: ${displayValue(caseResult.output)}`);
|
|
1667
|
+
if (caseResult.targetError !== void 0) {
|
|
1668
|
+
lines.push(` target error: ${errorText(caseResult.targetError)}`);
|
|
1669
|
+
}
|
|
1670
|
+
for (const metric of caseResult.metrics) {
|
|
1671
|
+
const parts = [` - ${metric.metricName}: ${metric.outcome.outcome}`];
|
|
1672
|
+
if (metric.outcome.score !== void 0)
|
|
1673
|
+
parts.push(`score=${displayValue(metric.outcome.score)}`);
|
|
1674
|
+
if (metric.threshold !== void 0) parts.push(`threshold=${metric.threshold}`);
|
|
1675
|
+
if (metric.direction !== void 0) parts.push(`direction=${metric.direction}`);
|
|
1676
|
+
if (!metric.required) parts.push("optional");
|
|
1677
|
+
lines.push(parts.join(" | "));
|
|
1678
|
+
const explanation = metric.outcome.comment ?? (metric.outcome.outcome === "invalid" ? metric.outcome.reason : void 0);
|
|
1679
|
+
if (explanation !== void 0) lines.push(` ${explanation}`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
lines.push(
|
|
1683
|
+
``,
|
|
1684
|
+
`Usage: target=${result.usage.target.totalTokens} evaluation=${result.usage.evaluation.totalTokens} total=${result.usage.total.totalTokens} tokens`
|
|
1685
|
+
);
|
|
1686
|
+
if (result.cost !== void 0) {
|
|
1687
|
+
lines.push(
|
|
1688
|
+
`Cost: target=${result.cost.target} evaluation=${result.cost.evaluation} total=${result.cost.total} ${result.cost.currency}`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
lines.push(`Duration: ${result.durationMs}ms`);
|
|
1692
|
+
return lines.join("\n");
|
|
1693
|
+
}
|
|
1694
|
+
function jsonResult(result) {
|
|
1695
|
+
return JSON.stringify(
|
|
1696
|
+
result,
|
|
1697
|
+
(_key, value) => {
|
|
1698
|
+
if (value instanceof Error) {
|
|
1699
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
1700
|
+
}
|
|
1701
|
+
if (value instanceof RegExp) return String(value);
|
|
1702
|
+
return value;
|
|
1703
|
+
},
|
|
1704
|
+
2
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
function expectationMismatches(result, expectations) {
|
|
1708
|
+
if (expectations === void 0) return [];
|
|
1709
|
+
return [
|
|
1710
|
+
...expectations.totals === void 0 ? [] : totalMismatches(result, expectations.totals),
|
|
1711
|
+
...expectations.outcomes === void 0 ? [] : outcomeMismatches(result, expectations.outcomes)
|
|
1712
|
+
];
|
|
1713
|
+
}
|
|
1714
|
+
function totalMismatches(result, expected) {
|
|
1715
|
+
const directMetrics = {
|
|
1716
|
+
...expected.total === void 0 ? {} : { total: expected.total },
|
|
1717
|
+
...expected.passed === void 0 ? {} : { passed: expected.passed },
|
|
1718
|
+
...expected.failed === void 0 ? {} : { failed: expected.failed },
|
|
1719
|
+
...expected.invalid === void 0 ? {} : { invalid: expected.invalid }
|
|
1720
|
+
};
|
|
1721
|
+
return [
|
|
1722
|
+
...totalsGroupMismatches("metrics", result.metrics, {
|
|
1723
|
+
...directMetrics,
|
|
1724
|
+
...expected.metrics
|
|
1725
|
+
}),
|
|
1726
|
+
...totalsGroupMismatches("cases", result.cases, expected.cases)
|
|
1727
|
+
];
|
|
1728
|
+
}
|
|
1729
|
+
function totalsGroupMismatches(label, actual, expected) {
|
|
1730
|
+
if (expected === void 0) return [];
|
|
1731
|
+
const mismatches = [];
|
|
1732
|
+
for (const key of ["total", "passed", "failed", "invalid"]) {
|
|
1733
|
+
if (expected[key] !== void 0 && actual[key] !== expected[key]) {
|
|
1734
|
+
mismatches.push(`${label}.${key}: expected ${expected[key]}, received ${actual[key]}`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return mismatches;
|
|
1738
|
+
}
|
|
1739
|
+
function outcomeMismatches(result, expected) {
|
|
1740
|
+
const mismatches = [];
|
|
1741
|
+
const actualCases = new Map(result.results.map((caseResult) => [caseResult.case.id, caseResult]));
|
|
1742
|
+
for (const [caseId, metrics] of Object.entries(expected)) {
|
|
1743
|
+
const caseResult = actualCases.get(caseId);
|
|
1744
|
+
if (caseResult === void 0) {
|
|
1745
|
+
mismatches.push(`${caseId}: expected case was not present`);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
for (const [metricName, expectedOutcome] of Object.entries(metrics)) {
|
|
1749
|
+
const metric = caseResult.metrics.find((candidate) => candidate.metricName === metricName);
|
|
1750
|
+
if (metric === void 0) {
|
|
1751
|
+
mismatches.push(`${caseId}.${metricName}: expected metric was not present`);
|
|
1752
|
+
} else if (metric.outcome.outcome !== expectedOutcome) {
|
|
1753
|
+
mismatches.push(
|
|
1754
|
+
`${caseId}.${metricName}: expected ${expectedOutcome}, received ${metric.outcome.outcome}`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
for (const caseResult of result.results) {
|
|
1760
|
+
for (const metric of caseResult.metrics) {
|
|
1761
|
+
if (!metric.required) continue;
|
|
1762
|
+
const expectedOutcome = expected[caseResult.case.id]?.[metric.metricName] ?? "pass";
|
|
1763
|
+
if (metric.outcome.outcome !== expectedOutcome) {
|
|
1764
|
+
const message = `${caseResult.case.id}.${metric.metricName}: expected ${expectedOutcome}, received ${metric.outcome.outcome}`;
|
|
1765
|
+
if (!mismatches.includes(message)) mismatches.push(message);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return mismatches;
|
|
1770
|
+
}
|
|
1771
|
+
function hasUnexpectedInvalid(result, expectations) {
|
|
1772
|
+
if (expectations?.outcomes !== void 0) {
|
|
1773
|
+
return result.results.some(
|
|
1774
|
+
(caseResult) => caseResult.metrics.some(
|
|
1775
|
+
(metric) => metric.required && metric.outcome.outcome === "invalid" && expectations.outcomes?.[caseResult.case.id]?.[metric.metricName] !== "invalid"
|
|
1776
|
+
)
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
const expectedInvalid = expectations?.totals?.metrics?.invalid ?? expectations?.totals?.invalid;
|
|
1780
|
+
return result.metrics.invalid > (expectedInvalid ?? 0);
|
|
1781
|
+
}
|
|
1782
|
+
function requiredMetricCount(result, status) {
|
|
1783
|
+
return result.results.reduce(
|
|
1784
|
+
(total, caseResult) => total + caseResult.metrics.filter((metric) => metric.required && metric.outcome.outcome === status).length,
|
|
1785
|
+
0
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
function totalsText(totals) {
|
|
1789
|
+
return `${totals.total} total / ${totals.passed} pass / ${totals.failed} fail / ${totals.invalid} invalid`;
|
|
1790
|
+
}
|
|
1791
|
+
function displayValue(value) {
|
|
1792
|
+
if (typeof value === "string") return value;
|
|
1793
|
+
try {
|
|
1794
|
+
return JSON.stringify(value);
|
|
1795
|
+
} catch {
|
|
1796
|
+
return String(value);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function errorText(error) {
|
|
1800
|
+
return error instanceof Error ? error.message : displayValue(error);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// src/evals/metric.ts
|
|
1804
|
+
function defineMetric(metric) {
|
|
1805
|
+
return metric;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// src/evals/metrics.ts
|
|
1809
|
+
import { z as z2 } from "zod";
|
|
1810
|
+
function exactMatch(options = {}) {
|
|
1811
|
+
return {
|
|
1812
|
+
name: options.name ?? "exact_match",
|
|
1813
|
+
required: options.required ?? true,
|
|
1814
|
+
dataType: "BOOLEAN",
|
|
1815
|
+
direction: "higher_is_better",
|
|
1816
|
+
threshold: 1,
|
|
1817
|
+
async evaluate(args) {
|
|
1818
|
+
const actual = await resolveActual(options.actual, args);
|
|
1819
|
+
const expected = await resolveExpected(options.expected, args);
|
|
1820
|
+
if (expected === void 0) {
|
|
1821
|
+
return EvalOutcome.invalid("No expected value provided for exact match.");
|
|
1822
|
+
}
|
|
1823
|
+
const passed = stableComparable(actual) === stableComparable(expected);
|
|
1824
|
+
return passed ? EvalOutcome.pass(true) : EvalOutcome.fail(false, { comment: `Expected ${formatValue(expected)}.` });
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function contains(options = {}) {
|
|
1829
|
+
return {
|
|
1830
|
+
name: options.name ?? "contains",
|
|
1831
|
+
required: options.required ?? true,
|
|
1832
|
+
dataType: "BOOLEAN",
|
|
1833
|
+
direction: "higher_is_better",
|
|
1834
|
+
threshold: 1,
|
|
1835
|
+
async evaluate(args) {
|
|
1836
|
+
const actual = await resolveActualText(options.actual, args);
|
|
1837
|
+
const expected = await resolveExpected(options.expected, args);
|
|
1838
|
+
if (expected === void 0) {
|
|
1839
|
+
return EvalOutcome.invalid("No expected value provided for contains.");
|
|
1840
|
+
}
|
|
1841
|
+
if (typeof expected !== "string" && !(expected instanceof RegExp)) {
|
|
1842
|
+
return EvalOutcome.invalid("Contains expected value must be a string or RegExp.");
|
|
1843
|
+
}
|
|
1844
|
+
const passed = expected instanceof RegExp ? regexMatches(expected, actual) : actual.includes(expected);
|
|
1845
|
+
return passed ? EvalOutcome.pass(true) : EvalOutcome.fail(false, { comment: `Output did not contain ${String(expected)}.` });
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
function notContains(options = {}) {
|
|
1850
|
+
return {
|
|
1851
|
+
name: options.name ?? "not_contains",
|
|
1852
|
+
required: options.required ?? true,
|
|
1853
|
+
dataType: "BOOLEAN",
|
|
1854
|
+
direction: "higher_is_better",
|
|
1855
|
+
threshold: 1,
|
|
1856
|
+
async evaluate(args) {
|
|
1857
|
+
const actual = await resolveActualText(options.actual, args);
|
|
1858
|
+
const expected = await resolveExpected(options.expected, args);
|
|
1859
|
+
if (expected === void 0) {
|
|
1860
|
+
return EvalOutcome.invalid("No expected value provided for notContains.");
|
|
1861
|
+
}
|
|
1862
|
+
if (typeof expected !== "string" && !(expected instanceof RegExp)) {
|
|
1863
|
+
return EvalOutcome.invalid("notContains expected value must be a string or RegExp.");
|
|
1864
|
+
}
|
|
1865
|
+
const found = textExpectationMatches(expected, actual);
|
|
1866
|
+
return found ? EvalOutcome.fail(false, {
|
|
1867
|
+
comment: `Output contained forbidden value ${String(expected)}.`
|
|
1868
|
+
}) : EvalOutcome.pass(true);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function containsAll(options) {
|
|
1873
|
+
return containsListMetric("contains_all", "all", options);
|
|
1874
|
+
}
|
|
1875
|
+
function containsAny(options) {
|
|
1876
|
+
return containsListMetric("contains_any", "any", options);
|
|
1877
|
+
}
|
|
1878
|
+
function containsListMetric(defaultName, mode, options) {
|
|
1879
|
+
return {
|
|
1880
|
+
name: options.name ?? defaultName,
|
|
1881
|
+
required: options.required ?? true,
|
|
1882
|
+
dataType: "BOOLEAN",
|
|
1883
|
+
direction: "higher_is_better",
|
|
1884
|
+
threshold: 1,
|
|
1885
|
+
async evaluate(args) {
|
|
1886
|
+
const actual = await resolveActualText(options.actual, args);
|
|
1887
|
+
const expected = await resolveExpected(options.expected, args);
|
|
1888
|
+
if (!Array.isArray(expected) || expected.length === 0) {
|
|
1889
|
+
return EvalOutcome.invalid(`${defaultName} expected value must be a non-empty array.`);
|
|
1890
|
+
}
|
|
1891
|
+
if (expected.some((value) => typeof value !== "string" && !(value instanceof RegExp))) {
|
|
1892
|
+
return EvalOutcome.invalid(`${defaultName} expected values must be strings or RegExp.`);
|
|
1893
|
+
}
|
|
1894
|
+
const matches2 = expected.map((value) => textExpectationMatches(value, actual));
|
|
1895
|
+
const passed = mode === "all" ? matches2.every(Boolean) : matches2.some(Boolean);
|
|
1896
|
+
if (passed) return EvalOutcome.pass(true);
|
|
1897
|
+
const missing = expected.filter((_, index) => !matches2[index]).map(String);
|
|
1898
|
+
const comment = mode === "all" ? `Output was missing: ${missing.join(", ")}.` : `Output matched none of: ${expected.map(String).join(", ")}.`;
|
|
1899
|
+
return EvalOutcome.fail(false, { comment });
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function matches(options) {
|
|
1904
|
+
return regexMetric("matches", false, options);
|
|
1905
|
+
}
|
|
1906
|
+
function doesNotMatch(options) {
|
|
1907
|
+
return regexMetric("does_not_match", true, options);
|
|
1908
|
+
}
|
|
1909
|
+
function regexMetric(defaultName, negate, options) {
|
|
1910
|
+
return {
|
|
1911
|
+
name: options.name ?? defaultName,
|
|
1912
|
+
required: options.required ?? true,
|
|
1913
|
+
dataType: "BOOLEAN",
|
|
1914
|
+
direction: "higher_is_better",
|
|
1915
|
+
threshold: 1,
|
|
1916
|
+
async evaluate(args) {
|
|
1917
|
+
const actual = await resolveActualText(options.actual, args);
|
|
1918
|
+
const expected = await resolveExpected(options.expected, args);
|
|
1919
|
+
if (!(expected instanceof RegExp)) {
|
|
1920
|
+
return EvalOutcome.invalid(`${defaultName} expected value must be a RegExp.`);
|
|
1921
|
+
}
|
|
1922
|
+
const matched = regexMatches(expected, actual);
|
|
1923
|
+
const passed = negate ? !matched : matched;
|
|
1924
|
+
return passed ? EvalOutcome.pass(true) : EvalOutcome.fail(false, {
|
|
1925
|
+
comment: negate ? `Output matched forbidden pattern ${String(expected)}.` : `Output did not match ${String(expected)}.`
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
function maxLength(options) {
|
|
1931
|
+
return {
|
|
1932
|
+
name: options.name ?? "max_length",
|
|
1933
|
+
required: options.required ?? true,
|
|
1934
|
+
dataType: "BOOLEAN",
|
|
1935
|
+
direction: "higher_is_better",
|
|
1936
|
+
threshold: 1,
|
|
1937
|
+
async evaluate(args) {
|
|
1938
|
+
const actual = await resolveActualText(options.actual, args);
|
|
1939
|
+
const max = await resolveOption(options.max, args);
|
|
1940
|
+
if (!Number.isInteger(max) || max < 0) {
|
|
1941
|
+
return EvalOutcome.invalid("maxLength max must be a non-negative integer.");
|
|
1942
|
+
}
|
|
1943
|
+
const length = Array.from(actual).length;
|
|
1944
|
+
return length <= max ? EvalOutcome.pass(true) : EvalOutcome.fail(false, { comment: `Output length ${length} exceeded maximum ${max}.` });
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
function requiredFields(options) {
|
|
1949
|
+
return {
|
|
1950
|
+
name: options.name ?? "required_fields",
|
|
1951
|
+
required: options.required ?? true,
|
|
1952
|
+
dataType: "BOOLEAN",
|
|
1953
|
+
direction: "higher_is_better",
|
|
1954
|
+
threshold: 1,
|
|
1955
|
+
async evaluate(args) {
|
|
1956
|
+
const actual = await resolveActual(options.actual, args);
|
|
1957
|
+
const expected = await resolveOption(options.expected, args);
|
|
1958
|
+
if (!Array.isArray(expected) || expected.length === 0 || expected.some((field) => typeof field !== "string" || field.length === 0)) {
|
|
1959
|
+
return EvalOutcome.invalid(
|
|
1960
|
+
"requiredFields expected value must be a non-empty string array."
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
if (typeof actual !== "object" || actual === null || Array.isArray(actual)) {
|
|
1964
|
+
return EvalOutcome.invalid("requiredFields actual value must be an object.");
|
|
1965
|
+
}
|
|
1966
|
+
const missing = expected.filter((field) => !Object.hasOwn(actual, field));
|
|
1967
|
+
return missing.length === 0 ? EvalOutcome.pass(true) : EvalOutcome.fail(false, { comment: `Missing required fields: ${missing.join(", ")}.` });
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
function regexMatches(pattern, text) {
|
|
1972
|
+
pattern.lastIndex = 0;
|
|
1973
|
+
const matched = pattern.test(text);
|
|
1974
|
+
pattern.lastIndex = 0;
|
|
1975
|
+
return matched;
|
|
1976
|
+
}
|
|
1977
|
+
function textExpectationMatches(expected, actual) {
|
|
1978
|
+
return expected instanceof RegExp ? regexMatches(expected, actual) : actual.includes(expected);
|
|
1979
|
+
}
|
|
1980
|
+
async function resolveOption(value, args) {
|
|
1981
|
+
return typeof value === "function" ? value(args) : value;
|
|
1982
|
+
}
|
|
1983
|
+
function semanticSimilarity(options) {
|
|
1984
|
+
return {
|
|
1985
|
+
name: options.name ?? "semantic_similarity",
|
|
1986
|
+
required: options.required ?? true,
|
|
1987
|
+
dataType: "NUMERIC",
|
|
1988
|
+
direction: "higher_is_better",
|
|
1989
|
+
threshold: options.threshold,
|
|
1990
|
+
async evaluate(args) {
|
|
1991
|
+
const actual = await resolveActualText(options.actual, args);
|
|
1992
|
+
const expected = await resolveExpected(options.expected, args);
|
|
1993
|
+
if (expected === void 0) {
|
|
1994
|
+
return EvalOutcome.invalid("No expected value provided for semantic similarity.");
|
|
1995
|
+
}
|
|
1996
|
+
if (typeof expected !== "string") {
|
|
1997
|
+
return EvalOutcome.invalid("Semantic similarity expected value must be a string.");
|
|
1998
|
+
}
|
|
1999
|
+
const [actualEmbedding, expectedEmbedding] = await Promise.all([
|
|
2000
|
+
embedText(options.model, actual),
|
|
2001
|
+
embedText(options.model, expected)
|
|
2002
|
+
]);
|
|
2003
|
+
const score = cosineSimilarity(actualEmbedding.vector, expectedEmbedding.vector);
|
|
2004
|
+
return score >= options.threshold ? EvalOutcome.pass(score) : EvalOutcome.fail(score, { comment: `Similarity below threshold ${options.threshold}.` });
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function llmJudge(options) {
|
|
2009
|
+
const extractor = new ExtractorBuilder(options.model, options.schema).instructions(
|
|
2010
|
+
options.instructions ?? "Judge the eval case by the requested schema. Submit the judgment using the schema."
|
|
2011
|
+
).retries(options.retries ?? 0).build();
|
|
2012
|
+
return {
|
|
2013
|
+
name: options.name ?? "llm_judge",
|
|
2014
|
+
required: options.required ?? true,
|
|
2015
|
+
async evaluate(args) {
|
|
2016
|
+
try {
|
|
2017
|
+
const result = await extractor.extractWithUsage(
|
|
2018
|
+
await resolveJudgePrompt(options.prompt, args)
|
|
2019
|
+
);
|
|
2020
|
+
return options.passes(result.data) ? EvalOutcome.pass(result.data, { usage: result.usage }) : EvalOutcome.fail(result.data, { usage: result.usage });
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
return EvalOutcome.invalid(errorMessage(error));
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function llmScore(options) {
|
|
2028
|
+
const criteria = Array.isArray(options.criteria) ? options.criteria.join("\n") : options.criteria;
|
|
2029
|
+
const extractor = new ExtractorBuilder(
|
|
2030
|
+
options.model,
|
|
2031
|
+
z2.object({
|
|
2032
|
+
score: z2.number(),
|
|
2033
|
+
feedback: z2.string()
|
|
2034
|
+
})
|
|
2035
|
+
).instructions(
|
|
2036
|
+
options.instructions ?? `Score the eval case against these criteria:
|
|
2037
|
+
${criteria}
|
|
2038
|
+
|
|
2039
|
+
Return a score between 0 and 1 and brief feedback.`
|
|
2040
|
+
).retries(options.retries ?? 0).build();
|
|
2041
|
+
return {
|
|
2042
|
+
name: options.name ?? "llm_score",
|
|
2043
|
+
required: options.required ?? true,
|
|
2044
|
+
dataType: "NUMERIC",
|
|
2045
|
+
projectScore: (score) => score.score,
|
|
2046
|
+
direction: "higher_is_better",
|
|
2047
|
+
threshold: options.threshold,
|
|
2048
|
+
async evaluate(args) {
|
|
2049
|
+
try {
|
|
2050
|
+
const result = await extractor.extractWithUsage(
|
|
2051
|
+
await resolveJudgePrompt(options.prompt, args)
|
|
2052
|
+
);
|
|
2053
|
+
const score = result.data;
|
|
2054
|
+
if (score.score < 0 || score.score > 1) {
|
|
2055
|
+
return EvalOutcome.invalid(`Score ${score.score} outside valid range [0, 1].`, {
|
|
2056
|
+
score,
|
|
2057
|
+
usage: result.usage
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
return score.score >= options.threshold ? EvalOutcome.pass(score, { comment: score.feedback, usage: result.usage }) : EvalOutcome.fail(score, { comment: score.feedback, usage: result.usage });
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
return EvalOutcome.invalid(errorMessage(error));
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/evals/suite.ts
|
|
2069
|
+
function defineEvalCases(cases) {
|
|
2070
|
+
return cases;
|
|
2071
|
+
}
|
|
2072
|
+
function defineEvalSuite(options) {
|
|
2073
|
+
if (options !== void 0) return options;
|
|
2074
|
+
return {
|
|
2075
|
+
defineMetric(metric) {
|
|
2076
|
+
return metric;
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
1326
2079
|
}
|
|
1327
2080
|
export {
|
|
2081
|
+
EvalAssertionError,
|
|
1328
2082
|
EvalOutcome,
|
|
2083
|
+
abstention,
|
|
1329
2084
|
agentEvalTarget,
|
|
1330
2085
|
answerRelevancy,
|
|
2086
|
+
assertEvalOutcomes,
|
|
2087
|
+
assertEvalTotals,
|
|
1331
2088
|
contains,
|
|
2089
|
+
containsAll,
|
|
2090
|
+
containsAny,
|
|
1332
2091
|
defaultEvalTraceSelector,
|
|
2092
|
+
defineEvalCases,
|
|
2093
|
+
defineEvalSuite,
|
|
1333
2094
|
defineMetric,
|
|
2095
|
+
doesNotMatch,
|
|
2096
|
+
evalExitCode,
|
|
1334
2097
|
exactMatch,
|
|
1335
2098
|
faithfulness,
|
|
1336
2099
|
gEval,
|
|
@@ -1339,10 +2102,17 @@ export {
|
|
|
1339
2102
|
knowledgeRetention,
|
|
1340
2103
|
llmJudge,
|
|
1341
2104
|
llmScore,
|
|
2105
|
+
matches,
|
|
2106
|
+
maxLength,
|
|
2107
|
+
notContains,
|
|
2108
|
+
printEvalResult,
|
|
1342
2109
|
projectEvalOutcome,
|
|
1343
2110
|
promptAlignment,
|
|
2111
|
+
requiredFields,
|
|
1344
2112
|
resolveEvalTraceRef,
|
|
2113
|
+
runEvalCli,
|
|
1345
2114
|
runEvalSuite,
|
|
2115
|
+
selectPromptOutput,
|
|
1346
2116
|
semanticSimilarity,
|
|
1347
2117
|
summarization,
|
|
1348
2118
|
turnRelevancy
|