@arclabs561/ai-visual-test 0.5.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.
- package/.secretsignore.example +20 -0
- package/CHANGELOG.md +360 -0
- package/CONTRIBUTING.md +63 -0
- package/DEPLOYMENT.md +80 -0
- package/LICENSE +22 -0
- package/README.md +142 -0
- package/SECURITY.md +108 -0
- package/api/health.js +34 -0
- package/api/validate.js +252 -0
- package/index.d.ts +1221 -0
- package/package.json +112 -0
- package/public/index.html +149 -0
- package/src/batch-optimizer.mjs +451 -0
- package/src/bias-detector.mjs +370 -0
- package/src/bias-mitigation.mjs +233 -0
- package/src/cache.mjs +433 -0
- package/src/config.mjs +268 -0
- package/src/constants.mjs +80 -0
- package/src/context-compressor.mjs +350 -0
- package/src/convenience.mjs +617 -0
- package/src/cost-tracker.mjs +257 -0
- package/src/cross-modal-consistency.mjs +170 -0
- package/src/data-extractor.mjs +232 -0
- package/src/dynamic-few-shot.mjs +140 -0
- package/src/dynamic-prompts.mjs +361 -0
- package/src/ensemble/index.mjs +53 -0
- package/src/ensemble-judge.mjs +366 -0
- package/src/error-handler.mjs +67 -0
- package/src/errors.mjs +167 -0
- package/src/experience-propagation.mjs +128 -0
- package/src/experience-tracer.mjs +487 -0
- package/src/explanation-manager.mjs +299 -0
- package/src/feedback-aggregator.mjs +248 -0
- package/src/game-goal-prompts.mjs +478 -0
- package/src/game-player.mjs +548 -0
- package/src/hallucination-detector.mjs +155 -0
- package/src/helpers/playwright.mjs +80 -0
- package/src/human-validation-manager.mjs +516 -0
- package/src/index.mjs +364 -0
- package/src/judge.mjs +929 -0
- package/src/latency-aware-batch-optimizer.mjs +192 -0
- package/src/load-env.mjs +159 -0
- package/src/logger.mjs +55 -0
- package/src/metrics.mjs +187 -0
- package/src/model-tier-selector.mjs +221 -0
- package/src/multi-modal/index.mjs +36 -0
- package/src/multi-modal-fusion.mjs +190 -0
- package/src/multi-modal.mjs +524 -0
- package/src/natural-language-specs.mjs +1071 -0
- package/src/pair-comparison.mjs +277 -0
- package/src/persona/index.mjs +42 -0
- package/src/persona-enhanced.mjs +200 -0
- package/src/persona-experience.mjs +572 -0
- package/src/position-counterbalance.mjs +140 -0
- package/src/prompt-composer.mjs +375 -0
- package/src/render-change-detector.mjs +583 -0
- package/src/research-enhanced-validation.mjs +436 -0
- package/src/retry.mjs +152 -0
- package/src/rubrics.mjs +231 -0
- package/src/score-tracker.mjs +277 -0
- package/src/smart-validator.mjs +447 -0
- package/src/spec-config.mjs +106 -0
- package/src/spec-templates.mjs +347 -0
- package/src/specs/index.mjs +38 -0
- package/src/temporal/index.mjs +102 -0
- package/src/temporal-adaptive.mjs +163 -0
- package/src/temporal-batch-optimizer.mjs +222 -0
- package/src/temporal-constants.mjs +69 -0
- package/src/temporal-context.mjs +49 -0
- package/src/temporal-decision-manager.mjs +271 -0
- package/src/temporal-decision.mjs +669 -0
- package/src/temporal-errors.mjs +58 -0
- package/src/temporal-note-pruner.mjs +173 -0
- package/src/temporal-preprocessor.mjs +543 -0
- package/src/temporal-prompt-formatter.mjs +219 -0
- package/src/temporal-validation.mjs +159 -0
- package/src/temporal.mjs +415 -0
- package/src/type-guards.mjs +311 -0
- package/src/uncertainty-reducer.mjs +470 -0
- package/src/utils/index.mjs +175 -0
- package/src/validation-framework.mjs +321 -0
- package/src/validation-result-normalizer.mjs +64 -0
- package/src/validation.mjs +243 -0
- package/src/validators/accessibility-programmatic.mjs +345 -0
- package/src/validators/accessibility-validator.mjs +223 -0
- package/src/validators/batch-validator.mjs +143 -0
- package/src/validators/hybrid-validator.mjs +268 -0
- package/src/validators/index.mjs +34 -0
- package/src/validators/prompt-builder.mjs +218 -0
- package/src/validators/rubric.mjs +85 -0
- package/src/validators/state-programmatic.mjs +260 -0
- package/src/validators/state-validator.mjs +291 -0
- package/vercel.json +27 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explanation Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides late interaction capabilities for explaining VLLM judgments.
|
|
5
|
+
* Allows humans to ask questions about judgments after they've been made.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { VLLMJudge } from './judge.mjs';
|
|
9
|
+
import { getCached, setCached } from './cache.mjs';
|
|
10
|
+
import { log, warn } from './logger.mjs';
|
|
11
|
+
import { formatNotesForPrompt } from './temporal.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Explanation Manager
|
|
15
|
+
*
|
|
16
|
+
* Manages interactive explanations of VLLM judgments
|
|
17
|
+
*/
|
|
18
|
+
export class ExplanationManager {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.judge = options.judge || new VLLMJudge(options);
|
|
21
|
+
this.cacheEnabled = options.cacheEnabled !== false;
|
|
22
|
+
this.explanations = new Map(); // In-memory cache of explanations
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get explanation for a judgment
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} vllmJudgment - VLLM judgment to explain
|
|
29
|
+
* @param {string} question - Question about the judgment (optional)
|
|
30
|
+
* @param {Object} options - Explanation options
|
|
31
|
+
* @returns {Promise<Object>} Explanation response
|
|
32
|
+
*/
|
|
33
|
+
async explainJudgment(vllmJudgment, question = null, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
screenshotPath = vllmJudgment.screenshot,
|
|
36
|
+
prompt = vllmJudgment.prompt,
|
|
37
|
+
context = vllmJudgment.context || {},
|
|
38
|
+
useCache = true,
|
|
39
|
+
// NEW: Temporal and experience context
|
|
40
|
+
temporalNotes = vllmJudgment.temporalNotes || context.temporalNotes || null,
|
|
41
|
+
aggregatedNotes = vllmJudgment.aggregatedNotes || context.aggregatedNotes || null,
|
|
42
|
+
experienceTrace = vllmJudgment.experienceTrace || context.experienceTrace || null
|
|
43
|
+
} = options;
|
|
44
|
+
|
|
45
|
+
// Build explanation prompt with temporal context
|
|
46
|
+
const explanationPrompt = this._buildExplanationPrompt(
|
|
47
|
+
vllmJudgment,
|
|
48
|
+
question,
|
|
49
|
+
{ temporalNotes, aggregatedNotes, experienceTrace }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Check cache
|
|
53
|
+
if (useCache && this.cacheEnabled) {
|
|
54
|
+
const cacheKey = `explain-${vllmJudgment.id}-${question || 'default'}`;
|
|
55
|
+
const cached = getCached(cacheKey, explanationPrompt, context);
|
|
56
|
+
if (cached) {
|
|
57
|
+
return cached;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get explanation from VLLM
|
|
62
|
+
try {
|
|
63
|
+
const result = await this.judge.judgeScreenshot(
|
|
64
|
+
screenshotPath,
|
|
65
|
+
explanationPrompt,
|
|
66
|
+
{
|
|
67
|
+
...context,
|
|
68
|
+
useCache: false, // Don't cache explanation requests
|
|
69
|
+
enableHumanValidation: false // Don't collect explanations for validation
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const explanation = {
|
|
74
|
+
question: question || 'Why did you score this the way you did?',
|
|
75
|
+
answer: result.reasoning || result.assessment || 'No explanation available',
|
|
76
|
+
confidence: this._extractConfidence(result),
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
judgmentId: vllmJudgment.id
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Cache explanation
|
|
82
|
+
if (useCache && this.cacheEnabled) {
|
|
83
|
+
const cacheKey = `explain-${vllmJudgment.id}-${question || 'default'}`;
|
|
84
|
+
setCached(cacheKey, explanationPrompt, context, explanation);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.explanations.set(vllmJudgment.id, explanation);
|
|
88
|
+
return explanation;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
warn('Failed to get explanation:', error.message);
|
|
91
|
+
return {
|
|
92
|
+
question: question || 'Why did you score this the way you did?',
|
|
93
|
+
answer: 'Unable to generate explanation at this time.',
|
|
94
|
+
error: error.message,
|
|
95
|
+
timestamp: new Date().toISOString()
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build explanation prompt with temporal and experience context
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} vllmJudgment - VLLM judgment
|
|
104
|
+
* @param {string|null} question - Optional question
|
|
105
|
+
* @param {Object} temporalContext - Temporal and experience context
|
|
106
|
+
* @param {Object|null} temporalContext.temporalNotes - Raw temporal notes
|
|
107
|
+
* @param {Object|null} temporalContext.aggregatedNotes - Aggregated temporal notes
|
|
108
|
+
* @param {Object|null} temporalContext.experienceTrace - Experience trace data
|
|
109
|
+
*/
|
|
110
|
+
_buildExplanationPrompt(vllmJudgment, question, temporalContext = {}) {
|
|
111
|
+
const { temporalNotes, aggregatedNotes, experienceTrace } = temporalContext;
|
|
112
|
+
|
|
113
|
+
let prompt = '';
|
|
114
|
+
|
|
115
|
+
// Base judgment context
|
|
116
|
+
if (question) {
|
|
117
|
+
prompt = `You previously evaluated this screenshot and gave it a score of ${vllmJudgment.vllmScore}/10.
|
|
118
|
+
|
|
119
|
+
Your previous judgment:
|
|
120
|
+
- Score: ${vllmJudgment.vllmScore}/10
|
|
121
|
+
- Issues: ${vllmJudgment.vllmIssues.join(', ') || 'None'}
|
|
122
|
+
- Reasoning: ${vllmJudgment.vllmReasoning}
|
|
123
|
+
|
|
124
|
+
Question: ${question}
|
|
125
|
+
|
|
126
|
+
Please provide a clear, detailed explanation addressing this question.`;
|
|
127
|
+
} else {
|
|
128
|
+
prompt = `You previously evaluated this screenshot and gave it a score of ${vllmJudgment.vllmScore}/10.
|
|
129
|
+
|
|
130
|
+
Your previous judgment:
|
|
131
|
+
- Score: ${vllmJudgment.vllmScore}/10
|
|
132
|
+
- Issues: ${vllmJudgment.vllmIssues.join(', ') || 'None'}
|
|
133
|
+
- Reasoning: ${vllmJudgment.vllmReasoning}
|
|
134
|
+
|
|
135
|
+
Please provide a detailed explanation of:
|
|
136
|
+
1. Why you scored it ${vllmJudgment.vllmScore}/10
|
|
137
|
+
2. What specific evidence in the screenshot led to this score
|
|
138
|
+
3. What the main issues are and why they matter
|
|
139
|
+
4. What would need to change to improve the score
|
|
140
|
+
|
|
141
|
+
Be specific and reference visual elements in the screenshot.`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add temporal context if available
|
|
145
|
+
if (aggregatedNotes) {
|
|
146
|
+
prompt += `\n\nTEMPORAL CONTEXT:\n`;
|
|
147
|
+
prompt += formatNotesForPrompt(aggregatedNotes);
|
|
148
|
+
prompt += `\n\nWhen explaining, reference specific time points, trends, and temporal relationships (before/after/during).`;
|
|
149
|
+
prompt += ` Explain how the current judgment relates to previous observations and temporal patterns.`;
|
|
150
|
+
} else if (temporalNotes && Array.isArray(temporalNotes) && temporalNotes.length > 0) {
|
|
151
|
+
// Format raw temporal notes if aggregated notes not available
|
|
152
|
+
prompt += `\n\nTEMPORAL CONTEXT:\n`;
|
|
153
|
+
const recentNotes = temporalNotes.slice(-5);
|
|
154
|
+
recentNotes.forEach((note, i) => {
|
|
155
|
+
const time = note.elapsed ? `${(note.elapsed / 1000).toFixed(1)}s` : `step ${i + 1}`;
|
|
156
|
+
prompt += ` ${time}: ${note.observation || note.step || 'step'}\n`;
|
|
157
|
+
if (note.score !== null && note.score !== undefined) {
|
|
158
|
+
prompt += ` Score: ${note.score}/10\n`;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
prompt += `\n\nWhen explaining, reference these temporal observations and explain how they relate to the current judgment.`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add experience trace context if available
|
|
165
|
+
if (experienceTrace) {
|
|
166
|
+
prompt += `\n\nEXPERIENCE TRACE CONTEXT:\n`;
|
|
167
|
+
prompt += `Session ID: ${experienceTrace.sessionId || 'unknown'}\n`;
|
|
168
|
+
|
|
169
|
+
if (experienceTrace.persona) {
|
|
170
|
+
prompt += `Persona: ${experienceTrace.persona.name || 'unknown'}\n`;
|
|
171
|
+
if (experienceTrace.persona.goals) {
|
|
172
|
+
prompt += `Persona Goals: ${experienceTrace.persona.goals.join(', ')}\n`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (experienceTrace.events && experienceTrace.events.length > 0) {
|
|
177
|
+
prompt += `\nRecent Events (last 5):\n`;
|
|
178
|
+
const recentEvents = experienceTrace.events.slice(-5);
|
|
179
|
+
recentEvents.forEach(event => {
|
|
180
|
+
const time = event.elapsed ? `${(event.elapsed / 1000).toFixed(1)}s` : 'unknown';
|
|
181
|
+
prompt += ` ${time}: ${event.type} - ${event.data.observation || event.data.action || ''}\n`;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (experienceTrace.validations && experienceTrace.validations.length > 0) {
|
|
186
|
+
prompt += `\nPrevious Validations (last 3):\n`;
|
|
187
|
+
const recentValidations = experienceTrace.validations.slice(-3);
|
|
188
|
+
recentValidations.forEach(validation => {
|
|
189
|
+
const time = validation.elapsed ? `${(validation.elapsed / 1000).toFixed(1)}s` : 'unknown';
|
|
190
|
+
prompt += ` ${time}: Score ${validation.validation.score}/10 - ${validation.validation.reasoning?.substring(0, 100) || ''}\n`;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
prompt += `\n\nWhen explaining, consider the user's journey, previous states, and how the current judgment fits into the overall experience.`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add VLLM-specific guidance for temporal explanations
|
|
198
|
+
if (aggregatedNotes || temporalNotes || experienceTrace) {
|
|
199
|
+
prompt += `\n\nIMPORTANT: As a Vision-Language Model, when explaining temporal aspects:
|
|
200
|
+
1. Visual citations: Reference specific image regions (coordinates, descriptions) when mentioning visual evidence
|
|
201
|
+
2. Temporal citations: Reference specific time points (e.g., "at t=5s", "after 12 seconds")
|
|
202
|
+
3. Temporal relationships: Explain before/after/during relationships and transitions
|
|
203
|
+
4. Experience context: Reference the user's journey and how previous states influenced the judgment
|
|
204
|
+
5. Trends: Explain improvement/decline trends and temporal coherence`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return prompt;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract confidence from result
|
|
212
|
+
*/
|
|
213
|
+
_extractConfidence(result) {
|
|
214
|
+
// Try to extract confidence from various sources
|
|
215
|
+
if (result.semantic?.confidence !== undefined) {
|
|
216
|
+
return result.semantic.confidence;
|
|
217
|
+
}
|
|
218
|
+
if (result.raw?.confidence !== undefined) {
|
|
219
|
+
return result.raw.confidence;
|
|
220
|
+
}
|
|
221
|
+
// Estimate from uncertainty if available
|
|
222
|
+
if (result.uncertainty !== undefined) {
|
|
223
|
+
return 1.0 - result.uncertainty;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get confidence breakdown for different aspects
|
|
230
|
+
*/
|
|
231
|
+
async getConfidenceBreakdown(vllmJudgment) {
|
|
232
|
+
const questions = [
|
|
233
|
+
'How confident are you in the overall score?',
|
|
234
|
+
'How confident are you in the issues you identified?',
|
|
235
|
+
'Are there any aspects you are uncertain about?'
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
const breakdown = {
|
|
239
|
+
overall: null,
|
|
240
|
+
issues: null,
|
|
241
|
+
uncertainty: null,
|
|
242
|
+
aspects: []
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Get explanations for each question
|
|
246
|
+
for (const question of questions) {
|
|
247
|
+
const explanation = await this.explainJudgment(vllmJudgment, question, { useCache: true });
|
|
248
|
+
// Parse explanation to extract confidence info
|
|
249
|
+
// This is a simplified version - could be enhanced with structured output
|
|
250
|
+
breakdown.aspects.push({
|
|
251
|
+
question,
|
|
252
|
+
explanation: explanation.answer
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return breakdown;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Explain disagreement between human and VLLM
|
|
261
|
+
*/
|
|
262
|
+
async explainDisagreement(vllmJudgment, humanJudgment) {
|
|
263
|
+
const question = `A human reviewer scored this ${humanJudgment.humanScore}/10, but you scored it ${vllmJudgment.vllmScore}/10.
|
|
264
|
+
The human identified these issues: ${humanJudgment.humanIssues.join(', ') || 'None'}.
|
|
265
|
+
You identified these issues: ${vllmJudgment.vllmIssues.join(', ') || 'None'}.
|
|
266
|
+
|
|
267
|
+
Please explain:
|
|
268
|
+
1. Why there might be a difference in scores
|
|
269
|
+
2. Whether you think the human's assessment is valid
|
|
270
|
+
3. What you might have missed or over-emphasized
|
|
271
|
+
4. How this could help improve future evaluations`;
|
|
272
|
+
|
|
273
|
+
return await this.explainJudgment(vllmJudgment, question);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get cached explanation if available
|
|
278
|
+
*/
|
|
279
|
+
getCachedExplanation(judgmentId, question = null) {
|
|
280
|
+
const key = question ? `${judgmentId}-${question}` : judgmentId;
|
|
281
|
+
return this.explanations.get(key) || null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Global explanation manager instance
|
|
287
|
+
*/
|
|
288
|
+
let globalExplanationManager = null;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get or create global explanation manager
|
|
292
|
+
*/
|
|
293
|
+
export function getExplanationManager(options = {}) {
|
|
294
|
+
if (!globalExplanationManager) {
|
|
295
|
+
globalExplanationManager = new ExplanationManager(options);
|
|
296
|
+
}
|
|
297
|
+
return globalExplanationManager;
|
|
298
|
+
}
|
|
299
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Aggregator
|
|
3
|
+
*
|
|
4
|
+
* Aggregates judge feedback across multiple tests for iterative improvement.
|
|
5
|
+
*
|
|
6
|
+
* General-purpose utility - no domain-specific logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Aggregate judge feedback from multiple test runs
|
|
11
|
+
*
|
|
12
|
+
* @param {import('./index.mjs').ValidationResult[]} judgeResults - Array of validation results
|
|
13
|
+
* @returns {import('./index.mjs').AggregatedFeedback} Aggregated feedback with statistics and recommendations
|
|
14
|
+
*/
|
|
15
|
+
export function aggregateFeedback(judgeResults) {
|
|
16
|
+
const aggregated = {
|
|
17
|
+
scores: [],
|
|
18
|
+
issues: {},
|
|
19
|
+
recommendations: {},
|
|
20
|
+
strengths: {},
|
|
21
|
+
weaknesses: {},
|
|
22
|
+
actionableItems: {},
|
|
23
|
+
categories: {
|
|
24
|
+
visual: [],
|
|
25
|
+
functional: [],
|
|
26
|
+
performance: [],
|
|
27
|
+
accessibility: [],
|
|
28
|
+
gameplay: [],
|
|
29
|
+
ux: [],
|
|
30
|
+
other: []
|
|
31
|
+
},
|
|
32
|
+
priority: {
|
|
33
|
+
critical: [],
|
|
34
|
+
high: [],
|
|
35
|
+
medium: [],
|
|
36
|
+
low: []
|
|
37
|
+
},
|
|
38
|
+
trends: {
|
|
39
|
+
score: [],
|
|
40
|
+
issues: [],
|
|
41
|
+
recommendations: []
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
judgeResults.forEach(result => {
|
|
46
|
+
// Aggregate scores
|
|
47
|
+
if (result.score !== null && result.score !== undefined) {
|
|
48
|
+
aggregated.scores.push(result.score);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Aggregate semantic information
|
|
52
|
+
if (result.semantic) {
|
|
53
|
+
const sem = result.semantic;
|
|
54
|
+
|
|
55
|
+
// Aggregate issues by frequency
|
|
56
|
+
if (sem.issues) {
|
|
57
|
+
sem.issues.forEach(issue => {
|
|
58
|
+
aggregated.issues[issue] = (aggregated.issues[issue] || 0) + 1;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Aggregate recommendations
|
|
63
|
+
if (sem.recommendations) {
|
|
64
|
+
sem.recommendations.forEach(rec => {
|
|
65
|
+
aggregated.recommendations[rec] = (aggregated.recommendations[rec] || 0) + 1;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Aggregate strengths
|
|
70
|
+
if (sem.strengths) {
|
|
71
|
+
sem.strengths.forEach(strength => {
|
|
72
|
+
aggregated.strengths[strength] = (aggregated.strengths[strength] || 0) + 1;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Aggregate weaknesses
|
|
77
|
+
if (sem.weaknesses) {
|
|
78
|
+
sem.weaknesses.forEach(weakness => {
|
|
79
|
+
aggregated.weaknesses[weakness] = (aggregated.weaknesses[weakness] || 0) + 1;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Aggregate actionable items
|
|
84
|
+
if (sem.actionableItems) {
|
|
85
|
+
sem.actionableItems.forEach(item => {
|
|
86
|
+
aggregated.actionableItems[item] = (aggregated.actionableItems[item] || 0) + 1;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Aggregate by category
|
|
91
|
+
if (sem.semanticCategories) {
|
|
92
|
+
Object.entries(sem.semanticCategories).forEach(([category, items]) => {
|
|
93
|
+
if (items && items.length > 0) {
|
|
94
|
+
aggregated.categories[category] = aggregated.categories[category] || [];
|
|
95
|
+
aggregated.categories[category].push(...items);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Aggregate by priority
|
|
101
|
+
if (sem.priority) {
|
|
102
|
+
Object.entries(sem.priority).forEach(([level, items]) => {
|
|
103
|
+
if (items && items.length > 0) {
|
|
104
|
+
aggregated.priority[level] = aggregated.priority[level] || [];
|
|
105
|
+
aggregated.priority[level].push(...items);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Calculate statistics
|
|
113
|
+
const stats = {
|
|
114
|
+
totalJudgments: judgeResults.length,
|
|
115
|
+
averageScore: aggregated.scores.length > 0
|
|
116
|
+
? aggregated.scores.reduce((a, b) => a + b, 0) / aggregated.scores.length
|
|
117
|
+
: null,
|
|
118
|
+
minScore: aggregated.scores.length > 0 ? Math.min(...aggregated.scores) : null,
|
|
119
|
+
maxScore: aggregated.scores.length > 0 ? Math.max(...aggregated.scores) : null,
|
|
120
|
+
mostCommonIssues: Object.entries(aggregated.issues)
|
|
121
|
+
.sort((a, b) => b[1] - a[1])
|
|
122
|
+
.slice(0, 10)
|
|
123
|
+
.map(([issue, count]) => ({ issue, count })),
|
|
124
|
+
mostCommonRecommendations: Object.entries(aggregated.recommendations)
|
|
125
|
+
.sort((a, b) => b[1] - a[1])
|
|
126
|
+
.slice(0, 10)
|
|
127
|
+
.map(([rec, count]) => ({ rec, count })),
|
|
128
|
+
mostCommonStrengths: Object.entries(aggregated.strengths)
|
|
129
|
+
.sort((a, b) => b[1] - a[1])
|
|
130
|
+
.slice(0, 10)
|
|
131
|
+
.map(([strength, count]) => ({ strength, count })),
|
|
132
|
+
mostCommonWeaknesses: Object.entries(aggregated.weaknesses)
|
|
133
|
+
.sort((a, b) => b[1] - a[1])
|
|
134
|
+
.slice(0, 10)
|
|
135
|
+
.map(([weakness, count]) => ({ weakness, count })),
|
|
136
|
+
mostCommonActionableItems: Object.entries(aggregated.actionableItems)
|
|
137
|
+
.sort((a, b) => b[1] - a[1])
|
|
138
|
+
.slice(0, 10)
|
|
139
|
+
.map(([item, count]) => ({ item, count })),
|
|
140
|
+
categoryCounts: Object.entries(aggregated.categories)
|
|
141
|
+
.map(([category, items]) => ({ category, count: items.length }))
|
|
142
|
+
.sort((a, b) => b.count - a.count),
|
|
143
|
+
priorityCounts: Object.entries(aggregated.priority)
|
|
144
|
+
.map(([level, items]) => ({ level, count: items.length }))
|
|
145
|
+
.sort((a, b) => {
|
|
146
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
147
|
+
return (order[a.level] || 99) - (order[b.level] || 99);
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
aggregated,
|
|
153
|
+
stats,
|
|
154
|
+
summary: generateSummary(aggregated, stats)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate human-readable summary
|
|
160
|
+
*/
|
|
161
|
+
function generateSummary(aggregated, stats) {
|
|
162
|
+
const parts = [];
|
|
163
|
+
|
|
164
|
+
parts.push(`Aggregated ${stats.totalJudgments} judge results.`);
|
|
165
|
+
|
|
166
|
+
if (stats.averageScore !== null) {
|
|
167
|
+
parts.push(`Average score: ${stats.averageScore.toFixed(1)}/10 (range: ${stats.minScore}-${stats.maxScore}).`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (stats.mostCommonIssues.length > 0) {
|
|
171
|
+
parts.push(`Most common issues: ${stats.mostCommonIssues.slice(0, 3).map(i => i.issue).join(', ')}.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (stats.mostCommonRecommendations.length > 0) {
|
|
175
|
+
parts.push(`Most common recommendations: ${stats.mostCommonRecommendations.slice(0, 3).map(r => r.rec).join(', ')}.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (stats.priorityCounts.length > 0) {
|
|
179
|
+
const critical = stats.priorityCounts.find(p => p.level === 'critical');
|
|
180
|
+
if (critical && critical.count > 0) {
|
|
181
|
+
parts.push(`Critical issues: ${critical.count}.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return parts.join(' ');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate recommendations from aggregated feedback
|
|
190
|
+
*
|
|
191
|
+
* @param {import('./index.mjs').AggregatedFeedback} aggregated - Aggregated feedback
|
|
192
|
+
* @returns {string[]} Array of recommendation strings
|
|
193
|
+
*/
|
|
194
|
+
export function generateRecommendations(aggregated) {
|
|
195
|
+
const recommendations = [];
|
|
196
|
+
|
|
197
|
+
// Critical priority items
|
|
198
|
+
if (aggregated.priority.critical && aggregated.priority.critical.length > 0) {
|
|
199
|
+
recommendations.push({
|
|
200
|
+
priority: 'critical',
|
|
201
|
+
category: 'all',
|
|
202
|
+
items: aggregated.priority.critical.slice(0, 5),
|
|
203
|
+
description: 'Critical issues that must be addressed immediately'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// High priority items
|
|
208
|
+
if (aggregated.priority.high && aggregated.priority.high.length > 0) {
|
|
209
|
+
recommendations.push({
|
|
210
|
+
priority: 'high',
|
|
211
|
+
category: 'all',
|
|
212
|
+
items: aggregated.priority.high.slice(0, 5),
|
|
213
|
+
description: 'High priority issues that should be addressed soon'
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Category-specific recommendations
|
|
218
|
+
Object.entries(aggregated.categories).forEach(([category, items]) => {
|
|
219
|
+
if (items && items.length > 0) {
|
|
220
|
+
const uniqueItems = [...new Set(items)];
|
|
221
|
+
if (uniqueItems.length > 0) {
|
|
222
|
+
recommendations.push({
|
|
223
|
+
priority: 'medium',
|
|
224
|
+
category,
|
|
225
|
+
items: uniqueItems.slice(0, 5),
|
|
226
|
+
description: `${category} improvements`
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Most common actionable items
|
|
233
|
+
const actionableEntries = Object.entries(aggregated.actionableItems || {})
|
|
234
|
+
.sort((a, b) => b[1] - a[1])
|
|
235
|
+
.slice(0, 10);
|
|
236
|
+
|
|
237
|
+
if (actionableEntries.length > 0) {
|
|
238
|
+
recommendations.push({
|
|
239
|
+
priority: 'medium',
|
|
240
|
+
category: 'actionable',
|
|
241
|
+
items: actionableEntries.map(([item, count]) => `${item} (mentioned ${count} times)`),
|
|
242
|
+
description: 'Most frequently mentioned actionable improvements'
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return recommendations;
|
|
247
|
+
}
|
|
248
|
+
|