@sanity/ailf 0.3.1 → 0.4.1
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.
|
@@ -25,7 +25,19 @@ export declare function detectFeatureArea(description: string): string;
|
|
|
25
25
|
/**
|
|
26
26
|
* Extract a numeric score (0–100) from a grading component result.
|
|
27
27
|
*
|
|
28
|
-
* Tries
|
|
28
|
+
* Tries (in order):
|
|
29
|
+
* 1. JSON-parsed reason (grader's native 0–100 scale — most reliable)
|
|
30
|
+
* 2. Direct score field (may be Promptfoo-normalized to 0–1)
|
|
31
|
+
* 3. Bare number in reason text
|
|
32
|
+
*
|
|
33
|
+
* Promptfoo's `llm-rubric` assertion normalizes `component.score` to
|
|
34
|
+
* the 0–1 range for some providers (notably GPT models, ~50% of the
|
|
35
|
+
* time) while leaving others in the grader's native 0–100 range. The
|
|
36
|
+
* `reason` field always contains the raw grader JSON, so we prefer it.
|
|
37
|
+
*
|
|
38
|
+
* When falling back to `component.score`, values in (0, 1] are rescaled
|
|
39
|
+
* to 0–100 since the rubric explicitly requests a 0–100 score and a
|
|
40
|
+
* true score of 0 or 1 out of 100 is vanishingly unlikely.
|
|
29
41
|
*/
|
|
30
42
|
export declare function parseRubricScore(component: ComponentResult): number;
|
|
31
43
|
/**
|
|
@@ -85,14 +85,22 @@ export function detectFeatureArea(description) {
|
|
|
85
85
|
/**
|
|
86
86
|
* Extract a numeric score (0–100) from a grading component result.
|
|
87
87
|
*
|
|
88
|
-
* Tries
|
|
88
|
+
* Tries (in order):
|
|
89
|
+
* 1. JSON-parsed reason (grader's native 0–100 scale — most reliable)
|
|
90
|
+
* 2. Direct score field (may be Promptfoo-normalized to 0–1)
|
|
91
|
+
* 3. Bare number in reason text
|
|
92
|
+
*
|
|
93
|
+
* Promptfoo's `llm-rubric` assertion normalizes `component.score` to
|
|
94
|
+
* the 0–1 range for some providers (notably GPT models, ~50% of the
|
|
95
|
+
* time) while leaving others in the grader's native 0–100 range. The
|
|
96
|
+
* `reason` field always contains the raw grader JSON, so we prefer it.
|
|
97
|
+
*
|
|
98
|
+
* When falling back to `component.score`, values in (0, 1] are rescaled
|
|
99
|
+
* to 0–100 since the rubric explicitly requests a 0–100 score and a
|
|
100
|
+
* true score of 0 or 1 out of 100 is vanishingly unlikely.
|
|
89
101
|
*/
|
|
90
102
|
export function parseRubricScore(component) {
|
|
91
|
-
//
|
|
92
|
-
if (typeof component.score === "number") {
|
|
93
|
-
return component.score;
|
|
94
|
-
}
|
|
95
|
-
// Try to extract from reason (LLM rubric returns JSON)
|
|
103
|
+
// 1. Prefer reason-extracted score — always in the grader's native 0–100 scale
|
|
96
104
|
if (component.reason) {
|
|
97
105
|
try {
|
|
98
106
|
const parsed = JSON.parse(component.reason);
|
|
@@ -102,15 +110,38 @@ export function parseRubricScore(component) {
|
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
catch {
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
// Not valid JSON — fall through to direct score or bare number extraction
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 2. Direct score field — may be Promptfoo-normalized to 0–1
|
|
117
|
+
if (typeof component.score === "number") {
|
|
118
|
+
return normalizeScore(component.score);
|
|
119
|
+
}
|
|
120
|
+
// 3. Last resort: bare number in reason text
|
|
121
|
+
if (component.reason) {
|
|
122
|
+
const match = component.reason.match(/(\d+)/);
|
|
123
|
+
if (match) {
|
|
124
|
+
return parseInt(match[1], 10);
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
127
|
return 0;
|
|
113
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Normalize a score that may be in either the 0–1 or 0–100 range.
|
|
131
|
+
*
|
|
132
|
+
* Promptfoo's `llm-rubric` assertion inconsistently normalizes
|
|
133
|
+
* `component.score` to 0–1 for some providers. Since the rubric
|
|
134
|
+
* explicitly requests scores on a 0–100 scale:
|
|
135
|
+
* - Scores in (0, 1] are rescaled to 0–100 (e.g., 0.95 → 95)
|
|
136
|
+
* - Score of exactly 0 stays 0 (genuine zero)
|
|
137
|
+
* - Scores > 1 are already on the 0–100 scale
|
|
138
|
+
*/
|
|
139
|
+
function normalizeScore(score) {
|
|
140
|
+
if (score > 0 && score <= 1) {
|
|
141
|
+
return Math.round(score * 100);
|
|
142
|
+
}
|
|
143
|
+
return score;
|
|
144
|
+
}
|
|
114
145
|
// ---------------------------------------------------------------------------
|
|
115
146
|
// URL metadata extraction
|
|
116
147
|
// ---------------------------------------------------------------------------
|
|
@@ -83,6 +83,14 @@ export declare function extractGraderJudgments(resultsPath: string): GraderJudgm
|
|
|
83
83
|
* Returns a record keyed by feature area with the composite actual score.
|
|
84
84
|
*/
|
|
85
85
|
export declare function scoreAgenticResults(resultsPath: string, weights: Record<string, number>): Record<string, ActualScoreEntry>;
|
|
86
|
+
/**
|
|
87
|
+
* Score agentic results broken down by model.
|
|
88
|
+
*
|
|
89
|
+
* Same logic as `scoreAgenticResults` but groups by provider first,
|
|
90
|
+
* producing a map of model → feature → ActualScoreEntry.
|
|
91
|
+
* Used to enrich the per-model breakdown with actual scores in full mode.
|
|
92
|
+
*/
|
|
93
|
+
export declare function scoreAgenticResultsPerModel(resultsPath: string, weights: Record<string, number>): Record<string, Record<string, ActualScoreEntry>>;
|
|
86
94
|
/** Options for the calculate-scores main() function. */
|
|
87
95
|
export interface CalculateScoresOptions {
|
|
88
96
|
/** Allowed origins for source isolation reporting */
|
|
@@ -648,6 +648,69 @@ export function scoreAgenticResults(resultsPath, weights) {
|
|
|
648
648
|
}
|
|
649
649
|
return entries;
|
|
650
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* Score agentic results broken down by model.
|
|
653
|
+
*
|
|
654
|
+
* Same logic as `scoreAgenticResults` but groups by provider first,
|
|
655
|
+
* producing a map of model → feature → ActualScoreEntry.
|
|
656
|
+
* Used to enrich the per-model breakdown with actual scores in full mode.
|
|
657
|
+
*/
|
|
658
|
+
export function scoreAgenticResultsPerModel(resultsPath, weights) {
|
|
659
|
+
const results = readAndNormalizeResults(resultsPath);
|
|
660
|
+
const wTask = weights["task-completion"] ?? 0.5;
|
|
661
|
+
const wCode = weights["code-correctness"] ?? 0.25;
|
|
662
|
+
const wDoc = weights["doc-coverage"] ?? 0.25;
|
|
663
|
+
// Group by model, then feature
|
|
664
|
+
const byModel = {};
|
|
665
|
+
for (const result of results) {
|
|
666
|
+
const modelId = result.providerId ?? result.providerLabel ?? "unknown";
|
|
667
|
+
const feature = result.vars.__featureArea || detectFeatureArea(result.description);
|
|
668
|
+
if (!byModel[modelId])
|
|
669
|
+
byModel[modelId] = {};
|
|
670
|
+
if (!byModel[modelId][feature])
|
|
671
|
+
byModel[modelId][feature] = [];
|
|
672
|
+
byModel[modelId][feature].push(result);
|
|
673
|
+
}
|
|
674
|
+
const perModel = {};
|
|
675
|
+
for (const [modelId, features] of Object.entries(byModel)) {
|
|
676
|
+
perModel[modelId] = {};
|
|
677
|
+
for (const [feature, featureResults] of Object.entries(features)) {
|
|
678
|
+
let totalTask = 0;
|
|
679
|
+
let totalCode = 0;
|
|
680
|
+
let totalDoc = 0;
|
|
681
|
+
let featureCost = 0;
|
|
682
|
+
const count = featureResults.length || 1;
|
|
683
|
+
for (const test of featureResults) {
|
|
684
|
+
featureCost += test.cost;
|
|
685
|
+
for (const comp of test.gradingResult.componentResults) {
|
|
686
|
+
if (comp.assertion?.type !== "llm-rubric")
|
|
687
|
+
continue;
|
|
688
|
+
const score = parseRubricScore(comp);
|
|
689
|
+
const kind = classifyRubric(comp);
|
|
690
|
+
if (kind === "taskCompletion")
|
|
691
|
+
totalTask += score;
|
|
692
|
+
else if (kind === "codeCorrectness")
|
|
693
|
+
totalCode += score;
|
|
694
|
+
else if (kind === "docCoverage")
|
|
695
|
+
totalDoc += score;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const avgTask = totalTask / count;
|
|
699
|
+
const avgCode = totalCode / count;
|
|
700
|
+
const avgDoc = totalDoc / count;
|
|
701
|
+
const actualScore = Math.round(avgTask * wTask + avgCode * wCode + avgDoc * wDoc);
|
|
702
|
+
perModel[modelId][feature] = {
|
|
703
|
+
actualScore,
|
|
704
|
+
codeCorrectness: Math.round(avgCode),
|
|
705
|
+
docCoverage: Math.round(avgDoc),
|
|
706
|
+
taskCompletion: Math.round(avgTask),
|
|
707
|
+
testCount: featureResults.length,
|
|
708
|
+
totalCost: featureCost,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return perModel;
|
|
713
|
+
}
|
|
651
714
|
// ---------------------------------------------------------------------------
|
|
652
715
|
// Score merging — combine baseline floor/ceiling with agentic actual
|
|
653
716
|
// ---------------------------------------------------------------------------
|
|
@@ -736,6 +799,16 @@ export function calculateAndWriteScores(options) {
|
|
|
736
799
|
});
|
|
737
800
|
scores = mergeScores(baselineScores, agenticScores);
|
|
738
801
|
evaluationMode = "full";
|
|
802
|
+
// Merge agentic actual scores into the per-model breakdown
|
|
803
|
+
if (perModel) {
|
|
804
|
+
const agenticPerModel = scoreAgenticResultsPerModel(agenticResultsPath, rubricConfig.weights);
|
|
805
|
+
for (const entry of perModel) {
|
|
806
|
+
const modelAgentic = agenticPerModel[entry.modelId];
|
|
807
|
+
if (modelAgentic) {
|
|
808
|
+
entry.scores = mergeScores(entry.scores, modelAgentic);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
739
812
|
// Aggregate agent behavior and source isolation from agentic results
|
|
740
813
|
agentBehavior = aggregateAgentBehavior(agenticResultsPath);
|
|
741
814
|
sourceIsolation = aggregateSourceIsolation(agenticResultsPath, options?.allowedOrigins);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ailf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "restricted"
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@types/node": "^22.13.1",
|
|
41
41
|
"tsx": "^4.19.2",
|
|
42
42
|
"typescript": "^5.7.3",
|
|
43
|
-
"@sanity/ailf-core": "0.1.0",
|
|
44
43
|
"@sanity/ailf-tasks": "0.1.4",
|
|
44
|
+
"@sanity/ailf-core": "0.1.0",
|
|
45
45
|
"@sanity/ailf-shared": "0.1.0"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|