@learning-commons/evaluators 0.2.0 → 0.4.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/CHANGELOG.md +44 -0
- package/README.md +86 -4
- package/dist/base-Ced9oKKa.d.cts +331 -0
- package/dist/base-Ced9oKKa.d.ts +331 -0
- package/dist/batch/cli.js +3940 -0
- package/dist/batch/cli.js.map +1 -0
- package/dist/batch/index.cjs +3602 -0
- package/dist/batch/index.cjs.map +1 -0
- package/dist/batch/index.d.cts +145 -0
- package/dist/batch/index.d.ts +145 -0
- package/dist/batch/index.js +3572 -0
- package/dist/batch/index.js.map +1 -0
- package/dist/index.cjs +225 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -329
- package/dist/index.d.ts +93 -329
- package/dist/index.js +224 -8
- package/dist/index.js.map +1 -1
- package/package.json +28 -9
- package/src/batch/README.md +166 -0
|
@@ -0,0 +1,3940 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs2 from 'fs';
|
|
3
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import prompts from 'prompts';
|
|
8
|
+
import pLimit from 'p-limit';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { generateText, Output } from 'ai';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import nlp from 'compromise';
|
|
15
|
+
import { syllable } from 'syllable';
|
|
16
|
+
import { parse } from 'csv-parse/sync';
|
|
17
|
+
|
|
18
|
+
// src/telemetry/client.ts
|
|
19
|
+
var TelemetryClient = class {
|
|
20
|
+
config;
|
|
21
|
+
logger;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.logger = config.logger;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Send telemetry event to analytics service
|
|
28
|
+
*
|
|
29
|
+
* Fire-and-forget: Errors are logged but don't throw.
|
|
30
|
+
*/
|
|
31
|
+
async send(event) {
|
|
32
|
+
if (!this.config.enabled) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const headers = {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"X-Client-ID": this.config.clientId
|
|
39
|
+
};
|
|
40
|
+
if (this.config.partnerKey) {
|
|
41
|
+
headers["X-API-Key"] = this.config.partnerKey;
|
|
42
|
+
}
|
|
43
|
+
const response = await fetch(this.config.endpoint, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers,
|
|
46
|
+
body: JSON.stringify(event),
|
|
47
|
+
// Don't block SDK operations on slow networks
|
|
48
|
+
signal: AbortSignal.timeout(5e3)
|
|
49
|
+
// 5 second timeout
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
this.logger.warn(
|
|
53
|
+
`[Telemetry] Failed to send event: ${response.status} ${response.statusText}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof Error) {
|
|
58
|
+
if (error.name !== "TimeoutError" && error.name !== "AbortError") {
|
|
59
|
+
this.logger.warn(`[Telemetry] Error sending event: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
66
|
+
var __dirname$1 = dirname(__filename$1);
|
|
67
|
+
var cachedClientId;
|
|
68
|
+
function generateClientId() {
|
|
69
|
+
if (cachedClientId) {
|
|
70
|
+
return cachedClientId;
|
|
71
|
+
}
|
|
72
|
+
const configFile = getConfigFilePath();
|
|
73
|
+
try {
|
|
74
|
+
const data = JSON.parse(readFileSync(configFile, "utf-8"));
|
|
75
|
+
if (data?.telemetry?.clientId) {
|
|
76
|
+
cachedClientId = data.telemetry.clientId;
|
|
77
|
+
return cachedClientId;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
const clientId = randomUUID();
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
84
|
+
writeFileSync(configFile, JSON.stringify({ telemetry: { clientId } }, null, 2));
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
cachedClientId = clientId;
|
|
88
|
+
return cachedClientId;
|
|
89
|
+
}
|
|
90
|
+
function getConfigFilePath() {
|
|
91
|
+
const configDir = process.platform === "win32" ? join(process.env.APPDATA ?? homedir(), "learning-commons") : join(homedir(), ".config", "learning-commons");
|
|
92
|
+
return join(configDir, "config.json");
|
|
93
|
+
}
|
|
94
|
+
var cachedVersion;
|
|
95
|
+
function getSDKVersion() {
|
|
96
|
+
if (cachedVersion) {
|
|
97
|
+
return cachedVersion;
|
|
98
|
+
}
|
|
99
|
+
const possiblePaths = [
|
|
100
|
+
join(__dirname$1, "../../package.json"),
|
|
101
|
+
// From src/
|
|
102
|
+
join(__dirname$1, "../package.json")
|
|
103
|
+
// From dist/
|
|
104
|
+
];
|
|
105
|
+
for (const path2 of possiblePaths) {
|
|
106
|
+
try {
|
|
107
|
+
const pkg = JSON.parse(readFileSync(path2, "utf-8"));
|
|
108
|
+
cachedVersion = pkg.version || "0.0.0";
|
|
109
|
+
return cachedVersion;
|
|
110
|
+
} catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
cachedVersion = "0.0.0";
|
|
115
|
+
return cachedVersion;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/errors.ts
|
|
119
|
+
var EvaluatorError = class extends Error {
|
|
120
|
+
constructor(message, code) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.code = code;
|
|
123
|
+
this.name = "EvaluatorError";
|
|
124
|
+
if (Error.captureStackTrace) {
|
|
125
|
+
Error.captureStackTrace(this, this.constructor);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var ConfigurationError = class extends EvaluatorError {
|
|
130
|
+
constructor(message) {
|
|
131
|
+
super(message, "CONFIGURATION_ERROR");
|
|
132
|
+
this.name = "ConfigurationError";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var ValidationError = class extends EvaluatorError {
|
|
136
|
+
constructor(message) {
|
|
137
|
+
super(message, "VALIDATION_ERROR");
|
|
138
|
+
this.name = "ValidationError";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var APIError = class extends EvaluatorError {
|
|
142
|
+
constructor(message, statusCode, retryable = false, code) {
|
|
143
|
+
super(message, code);
|
|
144
|
+
this.statusCode = statusCode;
|
|
145
|
+
this.retryable = retryable;
|
|
146
|
+
this.name = "APIError";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var AuthenticationError = class extends APIError {
|
|
150
|
+
constructor(message, statusCode) {
|
|
151
|
+
super(message, statusCode, false, "AUTHENTICATION_ERROR");
|
|
152
|
+
this.name = "AuthenticationError";
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var RateLimitError = class extends APIError {
|
|
156
|
+
constructor(message, retryAfter) {
|
|
157
|
+
super(message, 429, true, "RATE_LIMIT_ERROR");
|
|
158
|
+
this.retryAfter = retryAfter;
|
|
159
|
+
this.name = "RateLimitError";
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var NetworkError = class extends APIError {
|
|
163
|
+
constructor(message, retryable = true) {
|
|
164
|
+
super(message, void 0, retryable, "NETWORK_ERROR");
|
|
165
|
+
this.name = "NetworkError";
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
var TimeoutError = class extends APIError {
|
|
169
|
+
constructor(message = "Request timed out") {
|
|
170
|
+
super(message, 408, true, "TIMEOUT_ERROR");
|
|
171
|
+
this.name = "TimeoutError";
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
function parseProviderError(error) {
|
|
175
|
+
if (error instanceof Error) {
|
|
176
|
+
const message = error.message;
|
|
177
|
+
const statusMatch = message.match(/\b(4\d{2}|5\d{2})\b/);
|
|
178
|
+
const statusCode = statusMatch ? parseInt(statusMatch[1]) : void 0;
|
|
179
|
+
return {
|
|
180
|
+
message,
|
|
181
|
+
statusCode,
|
|
182
|
+
code: error.name !== "Error" ? error.name : void 0
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
message: String(error)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function wrapProviderError(error, defaultMessage = "API request failed") {
|
|
190
|
+
const { message, statusCode, code } = parseProviderError(error);
|
|
191
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
192
|
+
return new AuthenticationError(
|
|
193
|
+
message.includes("API key") ? message : "Invalid API key",
|
|
194
|
+
statusCode
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (statusCode === 429) {
|
|
198
|
+
const retryAfterMatch = message.match(/retry[- ]after[:\s]+(\d+)/i);
|
|
199
|
+
const retryAfter = retryAfterMatch ? parseInt(retryAfterMatch[1]) * 1e3 : void 0;
|
|
200
|
+
return new RateLimitError(
|
|
201
|
+
message.includes("rate limit") ? message : "Rate limit exceeded",
|
|
202
|
+
retryAfter
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("ETIMEDOUT") || message.includes("network") || message.includes("Network")) {
|
|
206
|
+
return new NetworkError(message);
|
|
207
|
+
}
|
|
208
|
+
if (message.includes("timeout") || message.includes("timed out")) {
|
|
209
|
+
return new TimeoutError(message);
|
|
210
|
+
}
|
|
211
|
+
return new APIError(
|
|
212
|
+
message || defaultMessage,
|
|
213
|
+
statusCode,
|
|
214
|
+
statusCode ? statusCode >= 500 : false,
|
|
215
|
+
// 5xx errors are retryable
|
|
216
|
+
code
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/logger.ts
|
|
221
|
+
var ConsoleLogger = class {
|
|
222
|
+
constructor(level = 2 /* WARN */) {
|
|
223
|
+
this.level = level;
|
|
224
|
+
}
|
|
225
|
+
debug(message, context) {
|
|
226
|
+
if (this.level <= 0 /* DEBUG */) {
|
|
227
|
+
console.debug(`[DEBUG] ${message}`, context || "");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
info(message, context) {
|
|
231
|
+
if (this.level <= 1 /* INFO */) {
|
|
232
|
+
console.info(`[INFO] ${message}`, context || "");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
warn(message, context) {
|
|
236
|
+
if (this.level <= 2 /* WARN */) {
|
|
237
|
+
console.warn(`[WARN] ${message}`, context || "");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
error(message, context) {
|
|
241
|
+
if (this.level <= 3 /* ERROR */) {
|
|
242
|
+
console.error(`[ERROR] ${message}`, context || "");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
var SilentLogger = class {
|
|
247
|
+
debug() {
|
|
248
|
+
}
|
|
249
|
+
info() {
|
|
250
|
+
}
|
|
251
|
+
warn() {
|
|
252
|
+
}
|
|
253
|
+
error() {
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function createLogger(customLogger, level = 2 /* WARN */) {
|
|
257
|
+
if (customLogger) {
|
|
258
|
+
return customLogger;
|
|
259
|
+
}
|
|
260
|
+
if (level === 4 /* SILENT */) {
|
|
261
|
+
return new SilentLogger();
|
|
262
|
+
}
|
|
263
|
+
return new ConsoleLogger(level);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/evaluators/base.ts
|
|
267
|
+
var VALIDATION_LIMITS = {
|
|
268
|
+
/** Minimum text length in characters */
|
|
269
|
+
MIN_TEXT_LENGTH: 10,
|
|
270
|
+
/** Maximum text length in characters (100K chars ≈ 25K tokens) */
|
|
271
|
+
MAX_TEXT_LENGTH: 1e5
|
|
272
|
+
};
|
|
273
|
+
var BaseEvaluator = class {
|
|
274
|
+
telemetryClient;
|
|
275
|
+
logger;
|
|
276
|
+
config;
|
|
277
|
+
/**
|
|
278
|
+
* Static metadata for the evaluator
|
|
279
|
+
*
|
|
280
|
+
* Concrete evaluators MUST define this property.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* class MyEvaluator extends BaseEvaluator {
|
|
285
|
+
* static readonly metadata = {
|
|
286
|
+
* id: 'my-evaluator',
|
|
287
|
+
* name: 'My Evaluator',
|
|
288
|
+
* description: 'Does something useful',
|
|
289
|
+
* supportedGrades: ['3', '4', '5'],
|
|
290
|
+
* requiresGoogleKey: true,
|
|
291
|
+
* requiresOpenAIKey: false,
|
|
292
|
+
* };
|
|
293
|
+
* }
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
static metadata;
|
|
297
|
+
constructor(config) {
|
|
298
|
+
this.logger = createLogger(config.logger, config.logLevel ?? 2 /* WARN */);
|
|
299
|
+
this.validateApiKeys(config);
|
|
300
|
+
const telemetryConfig = this.normalizeTelemetryConfig(config.telemetry);
|
|
301
|
+
this.config = {
|
|
302
|
+
maxRetries: config.maxRetries ?? 2,
|
|
303
|
+
telemetry: telemetryConfig
|
|
304
|
+
};
|
|
305
|
+
if (this.config.telemetry.enabled) {
|
|
306
|
+
this.telemetryClient = new TelemetryClient({
|
|
307
|
+
endpoint: "https://api.learningcommons.org/evaluators-telemetry/v1/events",
|
|
308
|
+
partnerKey: config.partnerKey,
|
|
309
|
+
clientId: generateClientId(),
|
|
310
|
+
enabled: true,
|
|
311
|
+
logger: this.logger
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Get metadata for this evaluator instance
|
|
317
|
+
* @throws {ConfigurationError} If the subclass has not defined static metadata
|
|
318
|
+
*/
|
|
319
|
+
get metadata() {
|
|
320
|
+
const meta = this.constructor.metadata;
|
|
321
|
+
if (!meta) {
|
|
322
|
+
throw new ConfigurationError(
|
|
323
|
+
`${this.constructor.name} must define a static readonly metadata block.`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return meta;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Validate that required API keys are provided based on metadata
|
|
330
|
+
* @throws {ConfigurationError} If required API keys are missing
|
|
331
|
+
*/
|
|
332
|
+
validateApiKeys(config) {
|
|
333
|
+
if (this.metadata.requiresGoogleKey && !config.googleApiKey) {
|
|
334
|
+
throw new ConfigurationError(
|
|
335
|
+
`Google API key is required for ${this.metadata.name} evaluator. Pass googleApiKey in config.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (this.metadata.requiresOpenAIKey && !config.openaiApiKey) {
|
|
339
|
+
throw new ConfigurationError(
|
|
340
|
+
`OpenAI API key is required for ${this.metadata.name} evaluator. Pass openaiApiKey in config.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Normalize telemetry config to standard format
|
|
346
|
+
*/
|
|
347
|
+
normalizeTelemetryConfig(telemetry) {
|
|
348
|
+
if (telemetry === false) {
|
|
349
|
+
return {
|
|
350
|
+
enabled: false,
|
|
351
|
+
recordInputs: false
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (telemetry === true || telemetry === void 0) {
|
|
355
|
+
return {
|
|
356
|
+
enabled: true,
|
|
357
|
+
recordInputs: false
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
enabled: telemetry.enabled ?? true,
|
|
362
|
+
recordInputs: telemetry.recordInputs ?? false
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get the evaluator type identifier from metadata
|
|
367
|
+
* @returns The evaluator type ID (e.g., "vocabulary", "sentence-structure")
|
|
368
|
+
*/
|
|
369
|
+
getEvaluatorType() {
|
|
370
|
+
return this.metadata.id;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Validate text meets requirements
|
|
374
|
+
* Default implementation - can be overridden by concrete evaluators
|
|
375
|
+
*
|
|
376
|
+
* @throws {ValidationError} If text is invalid
|
|
377
|
+
*/
|
|
378
|
+
validateText(text) {
|
|
379
|
+
this.logger.debug("Validating text input", {
|
|
380
|
+
evaluator: this.getEvaluatorType(),
|
|
381
|
+
operation: "validateText",
|
|
382
|
+
textLength: text.length
|
|
383
|
+
});
|
|
384
|
+
const trimmedText = text.trim();
|
|
385
|
+
if (!trimmedText) {
|
|
386
|
+
throw new ValidationError("Text cannot be empty or contain only whitespace");
|
|
387
|
+
}
|
|
388
|
+
if (trimmedText.length < VALIDATION_LIMITS.MIN_TEXT_LENGTH) {
|
|
389
|
+
throw new ValidationError(
|
|
390
|
+
`Text is too short. Minimum length is ${VALIDATION_LIMITS.MIN_TEXT_LENGTH} characters, received ${trimmedText.length} characters`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (trimmedText.length > VALIDATION_LIMITS.MAX_TEXT_LENGTH) {
|
|
394
|
+
throw new ValidationError(
|
|
395
|
+
`Text is too long. Maximum length is ${VALIDATION_LIMITS.MAX_TEXT_LENGTH.toLocaleString()} characters, received ${trimmedText.length.toLocaleString()} characters`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Validate grade is in supported range
|
|
401
|
+
* Default implementation - can be overridden by concrete evaluators
|
|
402
|
+
*
|
|
403
|
+
* @param grade - Grade level to validate
|
|
404
|
+
* @param validGrades - Set of valid grades for this evaluator
|
|
405
|
+
* @throws {ValidationError} If grade is invalid
|
|
406
|
+
*/
|
|
407
|
+
validateGrade(grade, validGrades) {
|
|
408
|
+
this.logger.debug("Validating grade input", {
|
|
409
|
+
evaluator: this.getEvaluatorType(),
|
|
410
|
+
operation: "validateGrade",
|
|
411
|
+
grade
|
|
412
|
+
});
|
|
413
|
+
if (!validGrades.has(grade)) {
|
|
414
|
+
const validList = Array.from(validGrades).sort((a, b) => {
|
|
415
|
+
if (a === "K") return -1;
|
|
416
|
+
if (b === "K") return 1;
|
|
417
|
+
return parseInt(a, 10) - parseInt(b, 10);
|
|
418
|
+
}).join(", ");
|
|
419
|
+
throw new ValidationError(
|
|
420
|
+
`Invalid grade "${grade}". Supported grades for this evaluator: ${validList}`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Send telemetry event to analytics service
|
|
426
|
+
* Common helper for all evaluators
|
|
427
|
+
*/
|
|
428
|
+
async sendTelemetry(params) {
|
|
429
|
+
if (!this.telemetryClient) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
await this.telemetryClient.send({
|
|
433
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
434
|
+
sdk_version: getSDKVersion(),
|
|
435
|
+
evaluator_type: this.getEvaluatorType(),
|
|
436
|
+
grade: params.grade,
|
|
437
|
+
status: params.status,
|
|
438
|
+
error_code: params.errorCode,
|
|
439
|
+
latency_ms: params.latencyMs,
|
|
440
|
+
text_length_chars: params.textLength,
|
|
441
|
+
provider: params.provider,
|
|
442
|
+
token_usage: params.tokenUsage,
|
|
443
|
+
metadata: params.metadata,
|
|
444
|
+
// Include input text only if recording is enabled
|
|
445
|
+
input_text: this.config.telemetry.recordInputs ? params.inputText : void 0
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
var DEFAULT_MODELS = {
|
|
450
|
+
openai: "gpt-4o",
|
|
451
|
+
anthropic: "claude-sonnet-4-5-20250929",
|
|
452
|
+
google: "gemini-2.5-pro"
|
|
453
|
+
};
|
|
454
|
+
var VercelAIProvider = class {
|
|
455
|
+
constructor(config) {
|
|
456
|
+
this.config = config;
|
|
457
|
+
if (config.type === "custom") {
|
|
458
|
+
throw new Error(
|
|
459
|
+
"VercelAIProvider does not support custom type. Use config.customProvider directly."
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Generate structured output using Vercel AI SDK's generateText with output
|
|
465
|
+
*/
|
|
466
|
+
async generateStructured(request) {
|
|
467
|
+
const model = await this.getModel(request.model);
|
|
468
|
+
const startTime = Date.now();
|
|
469
|
+
const { output, usage } = await generateText({
|
|
470
|
+
model,
|
|
471
|
+
messages: request.messages,
|
|
472
|
+
output: Output.object({ schema: request.schema }),
|
|
473
|
+
temperature: request.temperature ?? 0,
|
|
474
|
+
maxRetries: this.config.maxRetries ?? 0,
|
|
475
|
+
...request.maxTokens !== void 0 ? { maxTokens: request.maxTokens } : {}
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
data: output,
|
|
479
|
+
model: request.model || this.getDefaultModel(),
|
|
480
|
+
usage: {
|
|
481
|
+
inputTokens: usage.inputTokens || 0,
|
|
482
|
+
outputTokens: usage.outputTokens || 0
|
|
483
|
+
},
|
|
484
|
+
latencyMs: Date.now() - startTime
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Generate plain text using Vercel AI SDK's generateText
|
|
489
|
+
*/
|
|
490
|
+
async generateText(messages, temperature) {
|
|
491
|
+
const model = await this.getModel();
|
|
492
|
+
const startTime = Date.now();
|
|
493
|
+
const { text, usage } = await generateText({
|
|
494
|
+
model,
|
|
495
|
+
messages,
|
|
496
|
+
temperature: temperature ?? this.config.temperature ?? 0,
|
|
497
|
+
maxRetries: this.config.maxRetries ?? 0
|
|
498
|
+
});
|
|
499
|
+
return {
|
|
500
|
+
text,
|
|
501
|
+
usage: {
|
|
502
|
+
inputTokens: usage.inputTokens || 0,
|
|
503
|
+
outputTokens: usage.outputTokens || 0
|
|
504
|
+
},
|
|
505
|
+
latencyMs: Date.now() - startTime
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get the configured language model.
|
|
510
|
+
* Uses dynamic imports so consumers only need to install the provider packages they use.
|
|
511
|
+
*/
|
|
512
|
+
async getModel(requestModel) {
|
|
513
|
+
const modelId = requestModel || this.config.model || this.getDefaultModel();
|
|
514
|
+
const apiKey = this.config.apiKey;
|
|
515
|
+
switch (this.config.type) {
|
|
516
|
+
case "openai": {
|
|
517
|
+
const { createOpenAI } = await import('@ai-sdk/openai').catch(() => {
|
|
518
|
+
throw new Error(
|
|
519
|
+
"To use the OpenAI provider, install its adapter: npm install @ai-sdk/openai"
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
return createOpenAI(apiKey ? { apiKey } : {})(modelId);
|
|
523
|
+
}
|
|
524
|
+
case "anthropic": {
|
|
525
|
+
const { createAnthropic } = await import('@ai-sdk/anthropic').catch(() => {
|
|
526
|
+
throw new Error(
|
|
527
|
+
"To use the Anthropic provider, install its adapter: npm install @ai-sdk/anthropic"
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
return createAnthropic(apiKey ? { apiKey } : {})(modelId);
|
|
531
|
+
}
|
|
532
|
+
case "google": {
|
|
533
|
+
const { createGoogleGenerativeAI } = await import('@ai-sdk/google').catch(() => {
|
|
534
|
+
throw new Error(
|
|
535
|
+
"To use the Google provider, install its adapter: npm install @ai-sdk/google"
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
return createGoogleGenerativeAI(apiKey ? { apiKey } : {})(modelId);
|
|
539
|
+
}
|
|
540
|
+
default:
|
|
541
|
+
throw new Error(`Unsupported provider type: ${this.config.type}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Get default model for the configured provider
|
|
546
|
+
*/
|
|
547
|
+
getDefaultModel() {
|
|
548
|
+
const providerType = this.config.type;
|
|
549
|
+
if (providerType === "custom") {
|
|
550
|
+
throw new Error("Cannot get default model for custom provider type");
|
|
551
|
+
}
|
|
552
|
+
return DEFAULT_MODELS[providerType];
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
function createProvider(config) {
|
|
556
|
+
if (config.type === "custom" && config.customProvider) {
|
|
557
|
+
return config.customProvider;
|
|
558
|
+
}
|
|
559
|
+
return new VercelAIProvider(config);
|
|
560
|
+
}
|
|
561
|
+
var TextComplexityLevel = z.enum([
|
|
562
|
+
"Slightly complex",
|
|
563
|
+
"Moderately complex",
|
|
564
|
+
"Very complex",
|
|
565
|
+
"Exceedingly complex"
|
|
566
|
+
]);
|
|
567
|
+
|
|
568
|
+
// src/schemas/vocabulary.ts
|
|
569
|
+
var VocabularyComplexitySchema = z.object({
|
|
570
|
+
tier_2_words: z.string().describe("List of Tier 2 words (academic words)"),
|
|
571
|
+
tier_3_words: z.string().describe("List of Tier 3 words (domain-specific)"),
|
|
572
|
+
archaic_words: z.string().describe("List of Archaic words"),
|
|
573
|
+
other_complex_words: z.string().describe("List of Other Complex words"),
|
|
574
|
+
complexity_score: TextComplexityLevel.describe(
|
|
575
|
+
"The complexity of the text vocabulary"
|
|
576
|
+
),
|
|
577
|
+
reasoning: z.string().describe("Detailed reasoning for the complexity rating")
|
|
578
|
+
});
|
|
579
|
+
function calculateFleschKincaidGrade(text) {
|
|
580
|
+
return calculateReadabilityMetrics(text).fleschKincaidGrade;
|
|
581
|
+
}
|
|
582
|
+
function calculateReadabilityMetrics(text) {
|
|
583
|
+
const doc = nlp(text);
|
|
584
|
+
const sentences = doc.sentences().length;
|
|
585
|
+
const terms = doc.terms();
|
|
586
|
+
const words = terms.length;
|
|
587
|
+
const characters = text.replace(/\s/g, "").length;
|
|
588
|
+
const allWords = terms.out("array");
|
|
589
|
+
const totalSyllables = allWords.reduce((sum, word) => sum + syllable(word), 0);
|
|
590
|
+
const avgWordsPerSentence = sentences > 0 ? words / sentences : 0;
|
|
591
|
+
const avgSyllablesPerWord = words > 0 ? totalSyllables / words : 0;
|
|
592
|
+
const fkGrade = 0.39 * avgWordsPerSentence + 11.8 * avgSyllablesPerWord - 15.59;
|
|
593
|
+
return {
|
|
594
|
+
sentenceCount: sentences,
|
|
595
|
+
wordCount: words,
|
|
596
|
+
characterCount: characters,
|
|
597
|
+
syllableCount: totalSyllables,
|
|
598
|
+
avgWordsPerSentence,
|
|
599
|
+
avgSyllablesPerWord,
|
|
600
|
+
fleschKincaidGrade: Math.round(Math.max(0, fkGrade) * 100) / 100
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/features/sentence-features.ts
|
|
605
|
+
function safeDivision(numerator, denominator) {
|
|
606
|
+
return denominator === 0 ? 0 : numerator / denominator;
|
|
607
|
+
}
|
|
608
|
+
function standardDeviation(values) {
|
|
609
|
+
if (values.length <= 1) return 0;
|
|
610
|
+
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
611
|
+
const squaredDiffs = values.map((val) => Math.pow(val - mean, 2));
|
|
612
|
+
const variance = squaredDiffs.reduce((sum, val) => sum + val, 0) / values.length;
|
|
613
|
+
return Math.sqrt(variance);
|
|
614
|
+
}
|
|
615
|
+
function categorizeSentenceLengths(wordCounts) {
|
|
616
|
+
if (!wordCounts || wordCounts.length === 0) {
|
|
617
|
+
return {
|
|
618
|
+
percent_short_sentences: 0,
|
|
619
|
+
percent_medium_sentences: 0,
|
|
620
|
+
percent_long_sentences: 0,
|
|
621
|
+
percent_very_long_sentences: 0
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
let short = 0, medium = 0, long = 0, veryLong = 0;
|
|
625
|
+
for (const count of wordCounts) {
|
|
626
|
+
if (count <= 10) short++;
|
|
627
|
+
else if (count <= 20) medium++;
|
|
628
|
+
else if (count <= 30) long++;
|
|
629
|
+
else veryLong++;
|
|
630
|
+
}
|
|
631
|
+
const total = wordCounts.length;
|
|
632
|
+
return {
|
|
633
|
+
percent_short_sentences: short / total * 100,
|
|
634
|
+
percent_medium_sentences: medium / total * 100,
|
|
635
|
+
percent_long_sentences: long / total * 100,
|
|
636
|
+
percent_very_long_sentences: veryLong / total * 100
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function addEngineeredFeatures(analysis) {
|
|
640
|
+
const numSentences = analysis.num_sentences;
|
|
641
|
+
const numWords = analysis.num_words;
|
|
642
|
+
const avg_words_per_sentence = safeDivision(numWords, numSentences);
|
|
643
|
+
const sentence_length_variation = standardDeviation(analysis.sentence_word_counts);
|
|
644
|
+
const lengthCategories = categorizeSentenceLengths(analysis.sentence_word_counts);
|
|
645
|
+
const percent_simple_sentences = safeDivision(analysis.num_simple_sentences, numSentences) * 100;
|
|
646
|
+
const percent_compound_sentences = safeDivision(analysis.num_compound_sentences, numSentences) * 100;
|
|
647
|
+
const percent_complex_sentences = safeDivision(analysis.num_complex_sentences, numSentences) * 100;
|
|
648
|
+
const percent_compound_complex_sentences = safeDivision(analysis.num_compound_complex_sentences, numSentences) * 100;
|
|
649
|
+
const percent_other_sentences = safeDivision(analysis.num_other_sentences, numSentences) * 100;
|
|
650
|
+
const percent_words_in_simple_sentences = safeDivision(analysis.words_in_simple_sentences, numWords) * 100;
|
|
651
|
+
const percent_words_in_compound_sentences = safeDivision(analysis.words_in_compound_sentences, numWords) * 100;
|
|
652
|
+
const percent_words_in_complex_sentences = safeDivision(analysis.words_in_complex_sentences, numWords) * 100;
|
|
653
|
+
const percent_words_in_compound_complex_sentences = safeDivision(analysis.words_in_compound_complex_sentences, numWords) * 100;
|
|
654
|
+
const percent_words_in_other_sentences = safeDivision(analysis.words_in_other_sentences, numWords) * 100;
|
|
655
|
+
const avg_subordinates_per_sentence = safeDivision(analysis.num_subordinate_clauses, numSentences);
|
|
656
|
+
const avg_clauses_per_sentence = safeDivision(analysis.num_total_clauses, numSentences);
|
|
657
|
+
const percent_sentences_with_subordinate = safeDivision(analysis.num_sentences_with_subordinate, numSentences) * 100;
|
|
658
|
+
const percent_sentences_with_multiple_subordinates = safeDivision(analysis.num_sentences_with_multiple_subordinates, numSentences) * 100;
|
|
659
|
+
const percent_sentences_with_embedded_clauses = safeDivision(analysis.num_sentences_with_embedded_clauses, numSentences) * 100;
|
|
660
|
+
const prep_phrase_density = safeDivision(analysis.num_prepositional_phrases, numWords) * 100;
|
|
661
|
+
const participle_phrase_density = safeDivision(analysis.num_participle_phrases, numWords) * 100;
|
|
662
|
+
const appositive_phrase_density = safeDivision(analysis.num_appositive_phrases, numWords) * 100;
|
|
663
|
+
const total_transitions = analysis.num_simple_transitions + analysis.num_sophisticated_transitions;
|
|
664
|
+
const avg_transitions_per_sentence = safeDivision(total_transitions, numSentences);
|
|
665
|
+
const percent_sophisticated_transitions = safeDivision(analysis.num_sophisticated_transitions, total_transitions) * 100;
|
|
666
|
+
const percent_sentences_w_one_concept = safeDivision(analysis.num_one_concept_sentences, numSentences) * 100;
|
|
667
|
+
const percent_sentences_w_multi_concept = safeDivision(analysis.num_multi_concept_sentences, numSentences) * 100;
|
|
668
|
+
const percent_cleft_sentences = safeDivision(analysis.num_cleft_sentences, numSentences) * 100;
|
|
669
|
+
return {
|
|
670
|
+
...analysis,
|
|
671
|
+
avg_words_per_sentence,
|
|
672
|
+
sentence_length_variation,
|
|
673
|
+
...lengthCategories,
|
|
674
|
+
percent_simple_sentences,
|
|
675
|
+
percent_compound_sentences,
|
|
676
|
+
percent_complex_sentences,
|
|
677
|
+
percent_compound_complex_sentences,
|
|
678
|
+
percent_other_sentences,
|
|
679
|
+
percent_words_in_simple_sentences,
|
|
680
|
+
percent_words_in_compound_sentences,
|
|
681
|
+
percent_words_in_complex_sentences,
|
|
682
|
+
percent_words_in_compound_complex_sentences,
|
|
683
|
+
percent_words_in_other_sentences,
|
|
684
|
+
avg_subordinates_per_sentence,
|
|
685
|
+
avg_clauses_per_sentence,
|
|
686
|
+
percent_sentences_with_subordinate,
|
|
687
|
+
percent_sentences_with_multiple_subordinates,
|
|
688
|
+
percent_sentences_with_embedded_clauses,
|
|
689
|
+
prep_phrase_density,
|
|
690
|
+
participle_phrase_density,
|
|
691
|
+
appositive_phrase_density,
|
|
692
|
+
avg_transitions_per_sentence,
|
|
693
|
+
percent_sophisticated_transitions,
|
|
694
|
+
percent_sentences_w_one_concept,
|
|
695
|
+
percent_sentences_w_multi_concept,
|
|
696
|
+
percent_cleft_sentences
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
var FEATURE_COLS = [
|
|
700
|
+
// Foundational & Distributional
|
|
701
|
+
"avg_words_per_sentence",
|
|
702
|
+
"sentence_length_variation",
|
|
703
|
+
"percent_short_sentences",
|
|
704
|
+
"percent_medium_sentences",
|
|
705
|
+
"percent_long_sentences",
|
|
706
|
+
"percent_very_long_sentences",
|
|
707
|
+
"flesch_kincaid_grade",
|
|
708
|
+
// Sentence Structure (Grammatical Type)
|
|
709
|
+
"percent_simple_sentences",
|
|
710
|
+
"percent_compound_sentences",
|
|
711
|
+
"percent_complex_sentences",
|
|
712
|
+
"percent_compound_complex_sentences",
|
|
713
|
+
"percent_other_sentences",
|
|
714
|
+
// Word Distribution
|
|
715
|
+
"percent_words_in_simple_sentences",
|
|
716
|
+
"percent_words_in_complex_sentences",
|
|
717
|
+
"percent_words_in_compound_sentences",
|
|
718
|
+
"percent_words_in_compound_complex_sentences",
|
|
719
|
+
"percent_words_in_other_sentences",
|
|
720
|
+
// Clausal & Subordination
|
|
721
|
+
"avg_subordinates_per_sentence",
|
|
722
|
+
"avg_clauses_per_sentence",
|
|
723
|
+
"percent_sentences_with_subordinate",
|
|
724
|
+
"percent_sentences_with_multiple_subordinates",
|
|
725
|
+
"percent_sentences_with_embedded_clauses",
|
|
726
|
+
// Phrase Density
|
|
727
|
+
"prep_phrase_density",
|
|
728
|
+
"participle_phrase_density",
|
|
729
|
+
"appositive_phrase_density",
|
|
730
|
+
// Cohesion & Transitions
|
|
731
|
+
"avg_transitions_per_sentence",
|
|
732
|
+
"percent_sophisticated_transitions",
|
|
733
|
+
// Conceptual & Other
|
|
734
|
+
"percent_sentences_w_one_concept",
|
|
735
|
+
"percent_sentences_w_multi_concept",
|
|
736
|
+
"percent_cleft_sentences",
|
|
737
|
+
"max_clauses_in_any_sentence",
|
|
738
|
+
// Grades 5-12
|
|
739
|
+
"num_sentences",
|
|
740
|
+
"num_simple_sentences",
|
|
741
|
+
"num_compound",
|
|
742
|
+
"num_basic_complex",
|
|
743
|
+
"num_advanced_complex",
|
|
744
|
+
"percentage_simple",
|
|
745
|
+
"percentage_compound",
|
|
746
|
+
"percentage_basic_complex",
|
|
747
|
+
"percentage_advanced_complex"
|
|
748
|
+
];
|
|
749
|
+
function featuresToJSON(features, decimals = 1, castToInt = true) {
|
|
750
|
+
const payload = {};
|
|
751
|
+
for (const col of FEATURE_COLS) {
|
|
752
|
+
const value = features[col];
|
|
753
|
+
if (typeof value === "number") {
|
|
754
|
+
const rounded = Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
755
|
+
payload[col] = castToInt ? Math.round(rounded) : rounded;
|
|
756
|
+
} else {
|
|
757
|
+
payload[col] = null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return JSON.stringify(payload, null, 2);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ../../evals/prompts/vocabulary/background-knowledge.txt
|
|
764
|
+
var background_knowledge_default = `
|
|
765
|
+
Review the following text, which is an educational text written for students in the following grade band: {grade}.
|
|
766
|
+
|
|
767
|
+
Your job is to give me a background knowledge assumption; that is: what topics, if any, from the text students are likely to be familiar with based on a standard progression of topics in US public school education, as well as topics, if any the student is not likely to be familiar with.
|
|
768
|
+
|
|
769
|
+
Make sure your response is concise (between 1 - 3 lines max) and is about the topics themselves, not about any other aspect of the text (e.g. flowery language, complicated sentence structure, etc.).
|
|
770
|
+
|
|
771
|
+
Here's an example:
|
|
772
|
+
[START EXAMPLE]
|
|
773
|
+
Grade Band: 11th
|
|
774
|
+
Text: I went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could not
|
|
775
|
+
learn what it had to teach, and not, when I came to die, discover that I had not lived. I did not wish to live what was
|
|
776
|
+
not life, living is so dear; nor did I wish to practise resignation, unless it was quite necessary. I wanted to live deep and suck out all the marrow of life, to live so sturdily and Spartan-like as to put to rout all that was not life, to cut a broad swath and shave close, to drive life into a corner, and reduce it to its lowest terms, and, if it proved to be mean, why then to get the whole and genuine meanness of it, and publish its meanness to the world; or if it were sublime, to
|
|
777
|
+
know it by experience, and be able to give a true account of it in my next excursion. For most men, it appears to me,
|
|
778
|
+
are in a strange uncertainty about it, whether it is of the devil or of God, and have somewhat hastily concluded that it
|
|
779
|
+
is the chief end of man here to "glorify God and enjoy him forever."
|
|
780
|
+
|
|
781
|
+
Background Knowledge Assumption: Assume they've studied American Transcendentalists like Thoreau and Emerson, including the mid-19th-century context of nature-focused philosophy.
|
|
782
|
+
[END EXAMPLE]
|
|
783
|
+
|
|
784
|
+
You should assume that the student is an average US public school who is learning from common core curriculum. When you respond, just respond with the background knowledge assumption and nothing else.
|
|
785
|
+
|
|
786
|
+
You can use the following list of topics that we know are covered for each grade level, although use your best judgement if you know there are other topics out there that students are likely to have covered. And this doesn't cover higher grade levels, so you'll have to again use your judgement for, say, what background knowledge a 9th grader is likely to have:
|
|
787
|
+
[BEGIN TOPICS]
|
|
788
|
+
[
|
|
789
|
+
K: [
|
|
790
|
+
"Toys and Play", "Weather Wonders", "Trees are Alive", "Enjoying and Appreciating Trees",
|
|
791
|
+
"The Five Senses: How do our senses help us learn?", "Once Upon a Farm: What makes a good story?",
|
|
792
|
+
"America, Then and Now: How has life in America changed over time?", "The Continents: What makes the world fascinating?",
|
|
793
|
+
"Needs of Plants and Animals", "Pushes and Pulls", "Sunlight and Weather", "Learning and Working Together",
|
|
794
|
+
"How Do People Learn and Work Together?", "Where Do We Live?", "What Does it Mean to Be an American?",
|
|
795
|
+
"How Has Our World Changed?", "Why Do People Have Jobs?"
|
|
796
|
+
],
|
|
797
|
+
1: [
|
|
798
|
+
"Tools and Work", "A Study of the Sun, Moon, and Stars", "Birds' Amazing Bodies", "Caring for Birds",
|
|
799
|
+
"A World of Books: How do books change lives around the world?", "Creature Features: What can we discover about animals' unique features?",
|
|
800
|
+
"Powerful Forces: How do people respond to the powerful force of the wind?", "Cinderella Stories: Why do people around the world admire Cinderella?",
|
|
801
|
+
"Animal and Plant Defenses", "Light and Sounds", "Spinning Earth", "Our Place in the World",
|
|
802
|
+
"What Are the Rights and Responsibilities of Citizens?", "How Can We Describe Where We Live?",
|
|
803
|
+
"How Do We Celebrate Our Country?", "How Does the Past Shape Our Lives?", "Why Do People Work?"
|
|
804
|
+
],
|
|
805
|
+
2: [
|
|
806
|
+
"Schools and Community", "Fossils Tell of Earth's Changes", "The Secret World of Pollination", "Providing for Pollinators",
|
|
807
|
+
"A Season of Change: How does change impact people and nature?", "The American West: What was life like in the West for early Americans?",
|
|
808
|
+
"Civil Rights Heroes: How can people respond to injustice?", "Good Eating: How does food nourish us?",
|
|
809
|
+
"Plant and Animal Relationships", "Properties of Matter", "Changing Landforms", "Exploring Who We Are",
|
|
810
|
+
"Why Is It Important to Learn About the Past?", "How Does Geography Help Us Understand Our World?",
|
|
811
|
+
"How Do We Get What We Want and Need?", "Why Do We Need Government?", "How Can People Make a Difference in Our World?"
|
|
812
|
+
],
|
|
813
|
+
"3": [
|
|
814
|
+
"Overcoming Learning Challenges Near and Far", "Adaptations and the Wide World of Frogs", "Exploring Literary Classics",
|
|
815
|
+
"Water Around the World", "Ocean/Sea Exploration", "Outer Space", "Immigration", "Art/Being an Artist",
|
|
816
|
+
"Balancing Forces", "Inheritance and Traits", "Environments and Survival", "Weather and Climate",
|
|
817
|
+
"Communities", "Why Does It Matter Where We Live?", "What Is Our Relationship With Our Environment?",
|
|
818
|
+
"What Makes a Community Unique?", "How Does the Past Impact the Present?", "Why Do Governments and Citizens Need Each Other?",
|
|
819
|
+
"How Do People in a Community Meet Their Wants and Needs?"
|
|
820
|
+
],
|
|
821
|
+
4: [
|
|
822
|
+
"Poetry", "Animal Defense Mechanisms", "The American Revolution",
|
|
823
|
+
"Responding to Inequality: Ratifying the 19th Amendment (covers gender and racial inequality)",
|
|
824
|
+
"A Great Heart: What does it mean to have a great heart, literally and figuratively?",
|
|
825
|
+
"Extreme Settings: How does a challenging setting or physical environment change a person?",
|
|
826
|
+
"American Revolution/Multiple Perspectives", "Myths/Myth Making", "Energy Conversions", "Vision and Light",
|
|
827
|
+
"Earth's Features", "Waves, Energy, and Information", "Regions of the United States",
|
|
828
|
+
"How Does America Use Its Strengths and Face Its Challenges?", "Why Have People Moved to and From the Northeast?",
|
|
829
|
+
"How Has the Southeast Changed Over Time?", "How Does the Midwest Reflect the Spirit of America?",
|
|
830
|
+
"How Does the Southwest Reflect Its Diverse Past and Unique Environment?", "What Draws People to the West?"
|
|
831
|
+
],
|
|
832
|
+
5: [
|
|
833
|
+
"Human Rights", "Biodiversity in the Rainforest", "Athlete Leaders of Social Change",
|
|
834
|
+
"Impact of Natural Disasters", "Cultures in Conflict: How do cultural beliefs and values guide people?",
|
|
835
|
+
"Word Play: How and why do writers play with words?", "A War Between Us: How did the Civil War impact people?",
|
|
836
|
+
"Breaking Barriers: How can sports influence individuals and societies?", "Patterns of Earth and Sky",
|
|
837
|
+
"Modeling Matter", "The Earth System", "Ecosystem Restoration", "U.S. History: Making a New Nation",
|
|
838
|
+
"How Were the Lives of Native Peoples Influenced by Where They Lived?",
|
|
839
|
+
"What Happened When Diverse Cultures Crossed Paths?", "What Is the Impact of People Settling in a New Place?",
|
|
840
|
+
"Why Would a Nation Want to Become Independent?", "What Does the Revolutionary Era Tell Us About Our Nation Today?",
|
|
841
|
+
"How Does the Constitution Help Us Understand What It Means to Be an American?",
|
|
842
|
+
"What Do the Early Years of the United States Reveal About the Character of the Nation?",
|
|
843
|
+
"What Was the Effect of the Civil War on U.S. Society?"
|
|
844
|
+
],
|
|
845
|
+
6: [
|
|
846
|
+
"Greek Mythology", "Critical Problems and Design Solutions", "American Indian Boarding Schools",
|
|
847
|
+
"Remarkable Accomplishments in Space Science", "Resilience in the Great Depression: How can enduring tremendous hardship contribute to personal transformation?",
|
|
848
|
+
"A Hero's Journey: What is the significance and power of the hero's journey?",
|
|
849
|
+
"Narrating the Unknown: How did the social and environmental factors in the unknown world of Jamestown shape its development and decline?",
|
|
850
|
+
"Courage in Crisis: How can the challenges of a hostile environment inspire heroism?",
|
|
851
|
+
"Microbiome", "Metabolism", "Metabolism Engineering", "Traits and Reproduction", "Thermal Energy",
|
|
852
|
+
"Ocean, Atmosphere, and Climate", "Weather Patterns", "Earth's Changing Climate",
|
|
853
|
+
"Earth's Changing Climate: Engineering Internship", "The First Americans (up to 1492)",
|
|
854
|
+
"Exploration and Colonization", "English Colonies", "American Revolution", "First Governments and the Constitution",
|
|
855
|
+
"The Early American Republic", "Political and Geographic Changes (1828-1850)", "Life in the North and South (1820-1860)",
|
|
856
|
+
"Division and Civil War (1821-1865)", "Reconstruction (1865-1896)", "The West (1858-1896)",
|
|
857
|
+
"New Industry and a Changing Society", "Expansion and War", "The 1920s and 1930s", "World War II",
|
|
858
|
+
"The Cold War", "Civil Rights and American Society", "America Since the 1970s"
|
|
859
|
+
],
|
|
860
|
+
7: [
|
|
861
|
+
"The Lost Children of Sudan (Genocide, Genocide in Sudan)", "Epidemics", "Harlem Renaissance", "Plastic Pollution",
|
|
862
|
+
"Identity in the Middle Ages: How does society both support and limit the development of identity?",
|
|
863
|
+
"Americans All: How did World War II affect individuals?", "Language and Power: What is the power of language?",
|
|
864
|
+
"Fever: How can times of crisis affect citizens and society?", "Geology on Mars", "Plane Motion", "Plane Motion Engineering",
|
|
865
|
+
"Rock Formations", "Phase Change", "Phase Change Engineering", "Chemical Reactions", "Populations and Resources",
|
|
866
|
+
"Matter and Energy in Ecosystems", "Early Humans and Agricultural Revolution", "Fertile Crescent",
|
|
867
|
+
"Ancient Egypt and Kush", "The Israelites", "Ancient Greece", "Ancient South Asia", "Early China, Korea, and Japan",
|
|
868
|
+
"Ancient Rome", "Rise of Christian Kingdoms", "The Americas", "Medieval Europe", "The Rise of Islamic Empires",
|
|
869
|
+
"China in the Middle Ages", "Korea and Japan in the Middle Ages", "African Civilizations", "New Ways of Thinking",
|
|
870
|
+
"Age of Exploration and Trade", "Revolutions and Empires", "The Modern World"
|
|
871
|
+
],
|
|
872
|
+
8: [
|
|
873
|
+
"Folklore of Latin America", "Food Choices", "The Holocaust", "Japanese American Internment",
|
|
874
|
+
"The Poetics and Power of Storytelling: What is the power of storytelling?",
|
|
875
|
+
"The Great War: How do literature and art illuminate the effects of World War I?", "What Is Love?",
|
|
876
|
+
"Teens as Change Agents: How do people effect social change?", "Harnessing Human Energy",
|
|
877
|
+
"Force and Motion", "Force and Motion Engineering", "Magnetic Fields", "Light Waves", "Earth, Moon, and Sun",
|
|
878
|
+
"Natural Selection", "Natural Selection Engineering", "Evolutionary History", "The World in Spatial Terms",
|
|
879
|
+
"Places and Regions", "Physical Geography", "Population Geography", "Economic Geography",
|
|
880
|
+
"Political Geography", "Human-Environment Geography", "What is Economics?", "Markets, Money, and Businesses",
|
|
881
|
+
"Government and the Economy", "The Global Economy"
|
|
882
|
+
]
|
|
883
|
+
]
|
|
884
|
+
[END TOPICS]
|
|
885
|
+
|
|
886
|
+
Here is the text:
|
|
887
|
+
[BEGIN TEXT]
|
|
888
|
+
{text}
|
|
889
|
+
[END TEXT]
|
|
890
|
+
`;
|
|
891
|
+
|
|
892
|
+
// src/prompts/vocabulary/background-knowledge.ts
|
|
893
|
+
function getBackgroundKnowledgePrompt(text, grade) {
|
|
894
|
+
return background_knowledge_default.replaceAll("{grade}", grade).replaceAll("{text}", text);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ../../evals/prompts/vocabulary/grades-3-4-system.txt
|
|
898
|
+
var grades_3_4_system_default = "\nYou are an expert curriculum designer. Your job is to rate the complexity of a text's vocabulary relative to the grade level.\n\nYou will be given a rubric (with levels from least to most complex: slightly complex, moderately complex, very complex, exceedingly complex) as well as guidelines for interpreting the rubric.\nIMPORTANT: You should only pay attention to the vocabulary. Do not evaluate any other element of the text's complexity (e.g. sentence structure, meaning, etc.)\n\n**Resource 1: Qualitative Text Complexity rubric (SAP)**\n1. **Level 1: Slightly complex**\n * Original Definition: Vocabulary that is almost entirely not complex: contemporary, conversational, and/or familiar. A very low proportion of complex words (archaic, subject-specific, academic) is OK -- i.e. doesn't need to be 0.\n * Summary definition: Overall, vocabulary is easy to understand and does not impede comprehension of the bulk of the text (including main idea and supporting claims). 1-2 quick pauses for processing by the student are ok here!\n2. **Level 2: Moderately complex**\n * Original Definition: Vocabulary that is mostly not complex: contemporary, conversational, and/or familiar. A low proportion of complex words (archaic, subject-specific, academic) is OK\n * Summary definition: Overall, vocabulary generally allows students to comprehend the bulk of the text with little difficulty, though there may be occasional pauses for clarification. Several quick pauses or occasional prolonged pauses may occur.\n3. **Level 3: Very complex**\n * Original Definition: Vocabulary that is often complex: unfamiliar, archaic, subject-specific, and/or overly academic\n * Summary definition: Overall, vocabulary often presents challenges that may slow down comprehension but does not completely block the comprehension of the bulk of the text.\n4. **Level 4: Exceedingly complex**\n * Original Definition: Vocabulary that is mostly complex: unfamiliar, archaic, subject-specific, and/or overly academic. May be ambiguous or purposefully misleading.\n * Summary definition: Overall, vocabulary is so complex that it makes comprehension of the bulk of the text very challenging and requires careful effort to interpret.\n\n**Resource 2: Flesch-Kincaid Grade Level**\nUse the Flesch-Kincaid (FK) Grade Level as light guidance of the approximate grade level based on readability. The metric alone does not provide final information of vocabulary complexity, but a ballpark of the difficulty of the entire text.\n* grade 2-3: 1.98-5.34\n* grade 4-5: 4.51-7.73\n* grade 6-8: 6.51-10.34\n* grade 9-10: 8.32-12.12\n* grade 11-College: 10.34-14.2\n\n**Guidelines for Interpretation and Reasoning**\n\nYour reasoning is the most critical part of your analysis. It's not enough to simply count complex words. You must analyze their impact on a student at the specified grade level. Use the following principles to guide your judgment:\n\n1. **Density and Cumulative Effect:** Do not just count complex words; evaluate their concentration. A short text with a high density of challenging Tier 2 words (e.g., `peculiar`, `mischievous`, `courageous` for a 4th grader) can be more overwhelming than a longer text with a few scattered Tier 3 words. A constant barrage of unfamiliar words can elevate complexity from `very` to `exceedingly`.\n2. **Contextual Scaffolding:** Assess how the text supports new vocabulary.\n * Are new, complex terms explicitly defined or explained with simple examples (e.g., \"volume... to see if it is big enough to hold a liter of food\")?\n * Is the surrounding language simple and conversational, making the meaning of new words easier to infer?\n * Strong scaffolding can lower the complexity rating. A text with many Tier 3 words that are well-explained might only be `moderately complex`.\n3. **Abstract vs. Concrete Vocabulary:** Differentiate between words for abstract concepts and words for concrete things. A text built on abstract Tier 2 words (e.g., `relationships`, `performance`, `non-physical`) can be more challenging than a text that introduces Tier 3 labels for concrete things or people (e.g., `Sumerians`, `polonium`).\n4. **Conceptual Load:** Consider the cognitive load of the vocabulary. A list of many new, multi-syllabic, conceptually-heavy terms (e.g., `Paleolithic`, `Mesolithic`, `Neolithic` for a 3rd grader) can be `very complex` even if the terms are briefly defined, because the student must process multiple new concepts at once.\n5. **Calibrating the Top Levels:** Be precise in your use of `very complex` vs. `exceedingly complex`.\n * **Very complex:** The vocabulary creates significant hurdles and slows the reader down, but the main ideas of the text are still accessible with effort.\n * **Exceedingly complex:** The vocabulary is so dense, technical, or abstract that it acts as a barrier, making it nearly impossible for the target student to grasp the bulk of the text's meaning without extensive outside help. Reserve this for texts saturated with advanced terminology.\n6. **Consider Background Knowledge:** Pay close attention to the provided `student_background_knowledge`. Do not classify a word as complex if the student is likely to be familiar with it (e.g., 'oxygen' for a 3rd grader who has learned about the human body).\n\n**Final Analysis Format**\n\nProvide these information as your final analysis:\n1. **Complex vocabulary:**\n * Tier 2 words: Words that are commonly used in academic settings and more complex than colloquial, or everyday language and often have multiple meanings.\n * Tier 3 words: Overly academic or domain-specific words.\n * Archaic words: Words, or uses of words that are not commonly used in modern conversational language. E.g., \"The jury retired to deliberate on their verdict.\" The use of \"retire\" to mean withdrawing to a private place is an archaic use.\n * Other complex words: All other words that can increase complexity of the text (e.g., idioms, unfamiliar proper nouns that function as vocabulary).\n2. **Vocabulary complexity:** one of: slightly complex, moderately complex, very complex, exceedingly complex\n3. **Your reasoning of the complexity:** A detailed explanation of your rating, referencing the principles above.\n";
|
|
899
|
+
|
|
900
|
+
// ../../evals/prompts/vocabulary/other-grades-system.txt
|
|
901
|
+
var other_grades_system_default = "\nYou are an expert curriculum designer. Your job involves reading text snippets intended for students in K-12 and evaluating the complexity of the vocabulary in each text.\n\nYou will be given a rubric (with options 1, 2, 3, 4) as well as guidelines for interpreting the rubric.\n\nIMPORTANT: You should only pay attention to the vocabulary. Do not evaluate any other element of the text's complexity (e.g. sentence structure, meainng, etc.)\nIMPORTANT: Rely on the supplied rubric and annotation guidelines along. Do not introduce any new crtieria for evaluating the complexity of a text's vocabulary.\n\nPlease first reason out loud about the vocabulary complexity of the text and then provide an answer between 1 and 4 (whole numbers only). Provide the answer as an integer (not a float).\n";
|
|
902
|
+
|
|
903
|
+
// src/prompts/vocabulary/system.ts
|
|
904
|
+
function getSystemPrompt(grade) {
|
|
905
|
+
if (grade === "3" || grade === "4") {
|
|
906
|
+
return grades_3_4_system_default;
|
|
907
|
+
}
|
|
908
|
+
return other_grades_system_default;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ../../evals/prompts/vocabulary/grades-3-4-user.txt
|
|
912
|
+
var grades_3_4_user_default = "\nBelow is the text you need to evaluate. Let's think step by step in order to predict the output of the vocabulary complexity task.\n\n- It is intended for grade {student_grade_level}.\n\n- You can assume the student has the following background knowledge about the text \u2014 this background knowledge influences which words from the text are familiar versus unfamiliar for the student: {student_background_knowledge}\n\n- Text Flesch-Kincaid grade level: {fk_level}\n\n- Text to evaluate: [BEGIN TEXT]\n{text}\n[END TEXT]\n";
|
|
913
|
+
|
|
914
|
+
// ../../evals/prompts/vocabulary/other-grades-user.txt
|
|
915
|
+
var other_grades_user_default = `
|
|
916
|
+
Your job is to rate the complexity of a text's vocabulary (relative to the intended level of the text) according to a rubric and annotation guide. Stick to the rubric and annotation guide exactly \u2014 do not introduce any additional criteria or lenses for judging the complexity of the text.
|
|
917
|
+
|
|
918
|
+
[BEGIN ANNOTATION GUIDE AND RUBRIC]
|
|
919
|
+
Instructions
|
|
920
|
+
For the following task, please assume that:
|
|
921
|
+
- The student is on grade level and proficient in all core content areas, including reading fluency, comprehension, science, & social studies. (example).
|
|
922
|
+
- The student is moving through a common progression of topics (detailed here).
|
|
923
|
+
- The student is fluent in speaking English.
|
|
924
|
+
- The student has an "average" amount of background knowledge on topics not commonly covered in curriculum.
|
|
925
|
+
- The student will use this material for independent reading/work, without direct instruction.
|
|
926
|
+
- The text is reasonable for the given grade level.
|
|
927
|
+
|
|
928
|
+
Please do not consider the presence of figurative language when scoring Vocabulary. For example: with a phrase like "kicked the bucket," consider only the qualities of the words themselves ("kicked", "the" and "bucket").
|
|
929
|
+
|
|
930
|
+
Please do be sure to consider:
|
|
931
|
+
- all of the different types of vocabulary (listed below)
|
|
932
|
+
- the overall proportion of complex words in the text - including repeated complex words.
|
|
933
|
+
- the resulting holistic complexity of the vocabulary (described in the Summary section below).
|
|
934
|
+
|
|
935
|
+
Level 1:
|
|
936
|
+
Rubric: Vocabulary that is almost entirely not complex: contemporary, conversational, and/or familiar. That said, a very low proportion of complex words (archaic, subject-specific, academic) is OK -- i.e. doesn't need to be 0.
|
|
937
|
+
|
|
938
|
+
Level 2:
|
|
939
|
+
Rubric: Vocabulary that is mostly not complex: contemporary, conversational, and/or familiar. A low proportion of complex words (archaic, subject-specific, academic) is OK, but if it's very low, the text is probably level 1.
|
|
940
|
+
|
|
941
|
+
Level 3:
|
|
942
|
+
Rubric: Vocabulary that is often complex: unfamiliar, archaic, subject-specific, and/or overly academic
|
|
943
|
+
|
|
944
|
+
Level 4:
|
|
945
|
+
Rubric: Vocabulary that is mostly complex: unfamiliar, archaic, subject-specific, and/or overly academic. May be ambiguous or purposefully misleading
|
|
946
|
+
|
|
947
|
+
And here are some relevant definitions:
|
|
948
|
+
- Conversational: Everyday language.
|
|
949
|
+
- Familiar: Words that the student is likely to have seen/heard, from everyday life or their curriculum. Reminder: assume an "average" level of background knowledge.
|
|
950
|
+
- Unfamiliar: Words the student has probably not heard, or are being used in an unfamiliar way.
|
|
951
|
+
- For ex: 4th graders are familiar with the word "table" but may not be familiar with the use of the word with respect to data ("a table of data").
|
|
952
|
+
- Note:
|
|
953
|
+
- Words with in-line definitions (via appositives, or because they can be easily inferred from other parts of the text) should be evaluated as less unfamiliar.
|
|
954
|
+
- For ex: "The pharaoh, a powerful ruler of ancient Egypt, was buried in a grand tomb."
|
|
955
|
+
- The word "pharaoh" might be unfamiliar or subject-specific, but since is defined within the text, you can consider it a more familiar word.
|
|
956
|
+
- Unfamiliar proper nouns:
|
|
957
|
+
- A person's name, even if unfamiliar, generally does not add to complexity.
|
|
958
|
+
- Other unfamiliar proper nouns (eg locations, organizations) do add to complexity.
|
|
959
|
+
|
|
960
|
+
- Subject-specific: Words that are specific to a subject or field of study that are essential for understanding concepts and engaging with the content.
|
|
961
|
+
- Overly-academic: Words that are excessively formal, complex, or specialized.
|
|
962
|
+
- For ex: "The agrarian societal structure of the Neolithic Revolution precipitated a paradigm shift in agriculture"
|
|
963
|
+
- Archaic: A word that was common in the past but is now rarely/almost never used. Could also be a word used in an archaic way.
|
|
964
|
+
- For ex: "After a long day of court proceedings, the jury 'retired' to deliberate on their verdict."
|
|
965
|
+
- The word "retire" meaning to stop working may be familiar to a student, but "retire" meaning "withdrawing to a private place" is an archaic use.
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
Examples
|
|
969
|
+
The student is on-grade-level:
|
|
970
|
+
- Consider a 6th grade passage about earth systems. Per NGSS standards, students are introduced to earth systems starting in 2nd grade. They encounter words like: wind, water, river, lake, solids, and liquids. For our rating purposes, we would assume most students following 2nd have encountered these words. In 5th grade, they dive more fully into earth systems concepts, learning vocabulary words like geosphere, sediment, biosphere, atmosphere, ecosystems, organisms and climate. While rating, we would consider the words listed in the NGSS standards as more familiar following that grade level. If the same passage were intended for 3rd graders, though, then the subject-specific vocabulary is likely to be unfamiliar.
|
|
971
|
+
|
|
972
|
+
Figurative Language
|
|
973
|
+
- Kicked the bucket.
|
|
974
|
+
- The pen is mightier than the sword.
|
|
975
|
+
- The classroom was a zoo.
|
|
976
|
+
- He ran faster than the speed of light.
|
|
977
|
+
[END ANNOTATION GUIDE AND RUBRIC]
|
|
978
|
+
|
|
979
|
+
Here are a couple examples of texts that have already been scored along with justification for their scores, which you can use as exemplars:
|
|
980
|
+
[BEGIN EXAMPLES]
|
|
981
|
+
|
|
982
|
+
*** EXAMPLE 1 ***
|
|
983
|
+
The following text was intended for grade level 11 and received a complexity level of 1.
|
|
984
|
+
|
|
985
|
+
Here is the background knowledge assumption for that text: N/A
|
|
986
|
+
|
|
987
|
+
Here is the text:
|
|
988
|
+
// START TEXT //
|
|
989
|
+
"In a recent lecture, "Is Nothing Sacred?", Salman Rushdie, one of the most censored authors of our time, talked about the importance of books. He grew up in a household in India where books were as sacred as bread. If anyone in the household dropped a piece of bread or a book, the person not only picked it up, but also kissed the object by way of apologizing for clumsy disrespect.
|
|
990
|
+
|
|
991
|
+
He goes on to say that he had kissed many books before he had kissed a girl. Bread and books were for his household, and for many like his, food for the body and the soul. This image of the kissing of the book one had accidentally dropped made an impression on me. It speaks to the love and respect many people have for them.
|
|
992
|
+
|
|
993
|
+
I grew up in a small town in New Mexico, and we had very few books in our household. The first one I remember reading was my catechism book. Before I went to school to learn English, my mother taught me catechism in Spanish.
|
|
994
|
+
|
|
995
|
+
I remember the questions and answers I had to learn, and I remember the well-thumbed, frayed volume which was sacred to me.
|
|
996
|
+
|
|
997
|
+
Growing up with few books in the house created in me a desire and a need for them. When I started school, I remember visiting the one room library of our town and standing in front of the dusty shelves. In reality there were only a few shelves and not over a thousand books, but I wanted to read them all. There was food for my soul in the books, that much I realized."
|
|
998
|
+
// END TEXT //
|
|
999
|
+
|
|
1000
|
+
Here is the reasoning for that complexity level:
|
|
1001
|
+
// START REASONING //
|
|
1002
|
+
This text is a 1 for vocabulary, because the vocabulary that is used is familiar and accessible for a proficient 11th grader. Most of the words used in the text are very common everyday vocabulary for describing growing up, family life, and the importance of reading. A few examples of these very common words are: small town, book, school, learn, food, kissed, image, respect, love, speaks. There are many more in the text. In this text there are only a few "juicier" or more complex words, you can think of those as words that are less familiar, have a more abstract or nuanced meaning, or carry a very large concept. Less commonly spoken words that were used in the text were: frayed, volume, censored, clumsy, sacred. These are still well within reach of a proficient 11th grader, and would still be considered familiar, because they will have encountered them in past reading or academic studies. In the text there are a couple of words that are outliers, but they are not essential to the understanding of the larger text. One of these words or hyphenated compound phrase is well-frayed. A compound phrase is a phrase consisting of multiple words that work together to create a specific meaning or idea, often acting as a single unit in a sentence. If the meaning of individual words is familiar, it is typically quite easy for proficient readers to generalize the larger meaning that the author is implying with their word choice. In this case, proficient students will be accustomed to the phrase well, with the secondary meaning of very, rather than a description of positivity or health; and they will be accustomed to the use frayed, as in worn, aged, or damaged from use. Making the leap to identify the meaning of "well-frayed" as a book that is very used, will take only moments for a proficient 11th grader. Another word that stands out in the text is the word catechism, which might be new for many students based on their personal background or location, but a full understanding of what a catechism book contains is not essential for understanding the paragraph or whole text. The reader can make it through using minimum context clues to know that the catechism must be something important to his family. The type of book he learned to read before going to school is not critical for comprehension, it's enough to understand that reading was so important in his family, his mother started instruction before he even started school. Additionally, it's important to know that having one unknown word for an 11th grade reading, does not merit a rating higher than one.
|
|
1003
|
+
|
|
1004
|
+
It is worth noting that another reason this text is a 1, is that the content or topic of the passage is so familiar and covered extensively in K-12 education, i.e. reading is important, loving books, growing up; that coupled with the simple vocabulary choices, getting to the meaning of the overall text, and even the paragraphs, would be incredibly easy for a proficient 11th grader.
|
|
1005
|
+
// END REASONING //
|
|
1006
|
+
*** EXAMPLE 2 ***
|
|
1007
|
+
The following text was intended for grade level 5 and received a complexity level of 2.
|
|
1008
|
+
|
|
1009
|
+
Here is the background knowledge assumption for that text: Background Knowledge Assumption: Students are likely familiar with the concept of natural disasters, including hurricanes, and basic atmospheric concepts like high and low pressure from their studies on weather and climate. They may not be familiar with the specific formation processes of hurricanes or the global terminology differences (hurricane, typhoon, cyclone).
|
|
1010
|
+
|
|
1011
|
+
Here is the text:
|
|
1012
|
+
// START TEXT //
|
|
1013
|
+
Great whirling storms roar out of the oceans in many parts of the world. They are called by several names\u2014hurricane, typhoon, and cyclone are the three most familiar ones. But no matter what they are called, they are all the same sort of storm. They are born in the same way, in tropical waters. They develop the same way, feeding on warm, moist air. And they do the same kind of damage, both ashore and at sea. Other storms may cover a bigger area or have higher winds, but none can match both the size and the fury of hurricanes. They are earth's mightiest storms.
|
|
1014
|
+
|
|
1015
|
+
Like all storms, they take place in the atmosphere, the envelope of air that surrounds the earth and presses on its surface. The pressure at any one place is always changing. There are days when air is sinking and the atmosphere presses harder on the surface. These are the times of high pressure. There are days when a lot of air is rising and the atmosphere does not press down as hard. These are times of low pressure. Low-pressure areas over warm oceans give birth to hurricanes.
|
|
1016
|
+
// END TEXT //
|
|
1017
|
+
|
|
1018
|
+
Here is the reasoning for that complexity level:
|
|
1019
|
+
// START REASONING //
|
|
1020
|
+
I scored this a 2 because of the density of subject-specific vocabulary related to weather and climate, which is often covered in lower grade levels. This adds to the complexity above a 1, but it is not a level 3 because of the familiarity with the topic, which implies some familiarity with the vocabulary as well. The specific formation process and the vocabulary used to explain the processes are also subject-specfiic but not famliar, which would make the second paragraph a level 3 in the rubric language, but when considering the language used in the overall SUMMARY below the rubric, this new content and vocabulary would cause quick pauses and/or occasional prolonged pauses but would not cause the reader to slow down to due to challenging overall comprehension of the key ideas and supporting claims. This is especially the case because the second paragraph builds upon prior knowledge and familiar vocabulary use, so it is not entirely new information and vocabulary. While there is subject-specific vocabulary used, overly academic vocabulary is NOT used and is more conversational in nature, such as "great whiring storms" and "born" / "giving birth" to storm (although this is the way storms are described!) rather than more technical terms which made comprehension easier due to the accessibility of the vocabulary (even if used in other contexts before reading this text). Words such as "a lot" and "bigger" are more conversational, and while technical, unfamiliar words are provided, such as "hurricane," "typhoon," and "cyclone," knowing and understanding their differences is not necessary to grasp the main idea. The processes by which they are formed are what need to be retained while reading the entire text, and familiarity with the bulk of the vocabulary used would allow for that to happen without too much struggle to make meaning of it. Additionally, the text does not contain any archaic vocabulary or ambiguous words, which prevents it from reaching a rating of 4, although it is not necessary that they text have such vocabulary to meet a level 4, the frequent inclusion of such vocabulary makes it more likely to land at least a 3 or 4.
|
|
1021
|
+
// END REASONING //
|
|
1022
|
+
|
|
1023
|
+
*** EXAMPLE 3 ***
|
|
1024
|
+
The following text was intended for grade level 6 and received a complexity level of 3.
|
|
1025
|
+
|
|
1026
|
+
Here is the background knowledge assumption for that text: Background Knowledge Assumption: Students are likely familiar with basic Earth science concepts such as rocks, minerals, and fossils, as well as natural processes like volcanic eruptions and earthquakes. They may not be familiar with more advanced topics like plate tectonics or the specific branches of geology such as mineralogy, petrology, and seismology.
|
|
1027
|
+
|
|
1028
|
+
Here is the text:
|
|
1029
|
+
// START TEXT //
|
|
1030
|
+
Geology is the scientific study of Earth. Geologists study the planet\u2014its formation, its internal structure, its materials, its chemical and physical processes, and its history. Mountains, valleys, plains, sea floors, minerals, rocks, fossils, and the processes that create and destroy each of these are all the domain of the geologist. Geology is divided into two broad categories of study: physical geology and historical geology.
|
|
1031
|
+
|
|
1032
|
+
Physical geology is concerned with the processes occurring on or below the surface of Earth and the materials on which they operate. These processes include volcanic eruptions, landslides, earthquakes, and floods. Materials include rocks, air, seawater, soils, and sediment. Physical geology further divides into more specific branches, each of which deals with its own part of Earth's materials, landforms, and processes. Mineralogy and petrology investigate the composition and origin of minerals and rocks. Volcanologists study lava, rocks, and gases on live, dormant, and extinct volcanoes. Seismologists use instruments to monitor and predict earthquakes and volcanic eruptions.
|
|
1033
|
+
|
|
1034
|
+
Historical geology is concerned with the chronology of events, both physical and biological, that have taken place in Earth's history. Paleontologists study fossils (remains of ancient life) for evidence of the evolution of life on Earth. Fossils not only relate evolution, but also speak of the environment in which the organism lived. Corals in rocks at the top of the Grand Canyon in Arizona, for example, show a shallow sea flooded the area around 290 million years ago. In addition, by determining the ages and types of rocks around the world, geologists piece together continental and oceanic history over the past few billion years. Plate tectonics (the study of the movement of the sections of Earth's crust) adds to Earth's story with details of the changing configuration of the continents and oceans.
|
|
1035
|
+
// END TEXT //
|
|
1036
|
+
|
|
1037
|
+
Here is the reasoning for that complexity level:
|
|
1038
|
+
// START REASONING //
|
|
1039
|
+
To determine the complexity rating of this text based on the vocabulary present, I used the annotation guide, scoring rubric, and examples to set the expectations for rating. During the first read of the text, I "bolded" and categorized the more challenging vocabulary words according to the following complexity groupings: archaic, unfamiliar, archaic, subject-specific, and/or overly academic. On the second read, I considered the main idea or "gist" that students need to acquire understanding of. I then referenced the previously mentioned tools\u2013annotation guide, scoring rubric, and examples to remind myself of the expectations for rating. I agreed that readers would have familiarity with basic concepts of geology; however, I also considered the definitions provided for words such as Geology, Geologists, Physical Geology, Historical Geology, Mineralogy, and Petrology. I considered how students might pause for clarification and for how long. After reviewing the Annotation Guide while considering, I narrowed the rating down because the definitions provided throughout the text of more complex words should make the meaning of the text more accessible for readers, which is why although the words are subject-specific, I rated this text as a 3 instead of a 2-less complex or a 4\u2013more complex. I read the text one final time to ensure clarity around my rating, scored and wrote the justification.
|
|
1040
|
+
// END REASONING //
|
|
1041
|
+
[END EXAMPLES]
|
|
1042
|
+
|
|
1043
|
+
Below is the text you need to evaluate. It is intended for grade {student_grade_level}.
|
|
1044
|
+
|
|
1045
|
+
As you read the text, you can assume the student has the following background knowledge about the text \u2014 this background knowledge influences which words from the text are familiar versus unfamiliar for the student: {student_background_knowledge}
|
|
1046
|
+
|
|
1047
|
+
[BEGIN TEXT]
|
|
1048
|
+
{text}
|
|
1049
|
+
[END TEXT]
|
|
1050
|
+
|
|
1051
|
+
In your response, when specifying the level of complexity, be sure to use only a single integer (e.g. 2) and don't include any other text (e.g. don't say "level 2").
|
|
1052
|
+
`;
|
|
1053
|
+
|
|
1054
|
+
// src/prompts/vocabulary/user.ts
|
|
1055
|
+
function getUserPrompt(text, studentGradeLevel, studentBackgroundKnowledge, fkLevel) {
|
|
1056
|
+
const template = studentGradeLevel === "3" || studentGradeLevel === "4" ? grades_3_4_user_default : other_grades_user_default;
|
|
1057
|
+
return template.replaceAll("{student_grade_level}", studentGradeLevel).replaceAll("{student_background_knowledge}", studentBackgroundKnowledge).replaceAll("{fk_level}", fkLevel.toString()).replaceAll("{text}", text);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/evaluators/vocabulary.ts
|
|
1061
|
+
var VocabularyEvaluator = class _VocabularyEvaluator extends BaseEvaluator {
|
|
1062
|
+
static metadata = {
|
|
1063
|
+
id: "vocabulary",
|
|
1064
|
+
name: "Vocabulary",
|
|
1065
|
+
description: "Evaluates vocabulary complexity of educational texts relative to grade level",
|
|
1066
|
+
supportedGrades: ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
|
1067
|
+
requiresGoogleKey: true,
|
|
1068
|
+
requiresOpenAIKey: true
|
|
1069
|
+
};
|
|
1070
|
+
grades34ComplexityProvider;
|
|
1071
|
+
otherGradesComplexityProvider;
|
|
1072
|
+
backgroundKnowledgeProvider;
|
|
1073
|
+
constructor(config) {
|
|
1074
|
+
super(config);
|
|
1075
|
+
this.grades34ComplexityProvider = createProvider({
|
|
1076
|
+
type: "google",
|
|
1077
|
+
model: "gemini-2.5-pro",
|
|
1078
|
+
apiKey: config.googleApiKey,
|
|
1079
|
+
maxRetries: this.config.maxRetries
|
|
1080
|
+
});
|
|
1081
|
+
this.otherGradesComplexityProvider = createProvider({
|
|
1082
|
+
type: "openai",
|
|
1083
|
+
model: "gpt-4.1-2025-04-14",
|
|
1084
|
+
apiKey: config.openaiApiKey,
|
|
1085
|
+
maxRetries: this.config.maxRetries
|
|
1086
|
+
});
|
|
1087
|
+
this.backgroundKnowledgeProvider = createProvider({
|
|
1088
|
+
type: "openai",
|
|
1089
|
+
model: "gpt-4o-2024-11-20",
|
|
1090
|
+
apiKey: config.openaiApiKey,
|
|
1091
|
+
maxRetries: this.config.maxRetries
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Evaluate vocabulary complexity for a given text and grade level
|
|
1096
|
+
*
|
|
1097
|
+
* @param text - The text to evaluate
|
|
1098
|
+
* @param grade - The target grade level (3-12)
|
|
1099
|
+
* @returns Evaluation result with complexity score and detailed analysis
|
|
1100
|
+
* @throws {ValidationError} If text is empty, too short/long, or grade is invalid
|
|
1101
|
+
* @throws {APIError} If LLM API calls fail (includes AuthenticationError, RateLimitError, NetworkError, TimeoutError)
|
|
1102
|
+
*/
|
|
1103
|
+
async evaluate(text, grade) {
|
|
1104
|
+
this.logger.info("Starting vocabulary evaluation", {
|
|
1105
|
+
evaluator: "vocabulary",
|
|
1106
|
+
operation: "evaluate",
|
|
1107
|
+
grade,
|
|
1108
|
+
textLength: text.length
|
|
1109
|
+
});
|
|
1110
|
+
const startTime = Date.now();
|
|
1111
|
+
const stageDetails = [];
|
|
1112
|
+
const complexityProviderName = grade === "3" || grade === "4" ? "google:gemini-2.5-pro" : "openai:gpt-4.1-2025-04-14";
|
|
1113
|
+
try {
|
|
1114
|
+
this.validateText(text);
|
|
1115
|
+
this.validateGrade(grade, new Set(_VocabularyEvaluator.metadata.supportedGrades));
|
|
1116
|
+
this.logger.debug("Stage 1: Generating background knowledge", {
|
|
1117
|
+
evaluator: "vocabulary",
|
|
1118
|
+
operation: "background_knowledge"
|
|
1119
|
+
});
|
|
1120
|
+
const bgResponse = await this.getBackgroundKnowledgeAssumption(text, grade);
|
|
1121
|
+
stageDetails.push({
|
|
1122
|
+
stage: "background_knowledge",
|
|
1123
|
+
provider: "openai:gpt-4o-2024-11-20",
|
|
1124
|
+
latency_ms: bgResponse.latencyMs,
|
|
1125
|
+
token_usage: {
|
|
1126
|
+
input_tokens: bgResponse.usage.inputTokens,
|
|
1127
|
+
output_tokens: bgResponse.usage.outputTokens
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
const fkLevel = calculateFleschKincaidGrade(text);
|
|
1131
|
+
const complexityResponse = await this.evaluateComplexity(
|
|
1132
|
+
text,
|
|
1133
|
+
grade,
|
|
1134
|
+
bgResponse.knowledge.assumption,
|
|
1135
|
+
fkLevel
|
|
1136
|
+
);
|
|
1137
|
+
stageDetails.push({
|
|
1138
|
+
stage: "complexity_evaluation",
|
|
1139
|
+
provider: complexityProviderName,
|
|
1140
|
+
latency_ms: complexityResponse.latencyMs,
|
|
1141
|
+
token_usage: {
|
|
1142
|
+
input_tokens: complexityResponse.usage.inputTokens,
|
|
1143
|
+
output_tokens: complexityResponse.usage.outputTokens
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
const latencyMs = Date.now() - startTime;
|
|
1147
|
+
const totalTokenUsage = {
|
|
1148
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
1149
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
1150
|
+
};
|
|
1151
|
+
const result = {
|
|
1152
|
+
score: complexityResponse.data.complexity_score,
|
|
1153
|
+
reasoning: complexityResponse.data.reasoning,
|
|
1154
|
+
metadata: {
|
|
1155
|
+
model: `openai:gpt-4o-2024-11-20 + ${complexityProviderName}`,
|
|
1156
|
+
processingTimeMs: latencyMs
|
|
1157
|
+
},
|
|
1158
|
+
_internal: complexityResponse.data
|
|
1159
|
+
};
|
|
1160
|
+
this.sendTelemetry({
|
|
1161
|
+
status: "success",
|
|
1162
|
+
latencyMs,
|
|
1163
|
+
textLength: text.length,
|
|
1164
|
+
grade,
|
|
1165
|
+
provider: `openai:gpt-4o-2024-11-20 + ${complexityProviderName}`,
|
|
1166
|
+
tokenUsage: totalTokenUsage,
|
|
1167
|
+
metadata: {
|
|
1168
|
+
stage_details: stageDetails
|
|
1169
|
+
},
|
|
1170
|
+
inputText: text
|
|
1171
|
+
}).catch(() => {
|
|
1172
|
+
});
|
|
1173
|
+
this.logger.info("Vocabulary evaluation completed successfully", {
|
|
1174
|
+
evaluator: "vocabulary",
|
|
1175
|
+
operation: "evaluate",
|
|
1176
|
+
grade,
|
|
1177
|
+
score: result.score,
|
|
1178
|
+
processingTimeMs: latencyMs
|
|
1179
|
+
});
|
|
1180
|
+
return result;
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
const latencyMs = Date.now() - startTime;
|
|
1183
|
+
this.logger.error("Vocabulary evaluation failed", {
|
|
1184
|
+
evaluator: "vocabulary",
|
|
1185
|
+
operation: "evaluate",
|
|
1186
|
+
grade,
|
|
1187
|
+
error: error instanceof Error ? error : void 0,
|
|
1188
|
+
processingTimeMs: latencyMs,
|
|
1189
|
+
completedStages: stageDetails.length
|
|
1190
|
+
});
|
|
1191
|
+
const totalTokenUsage = stageDetails.length > 0 ? {
|
|
1192
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
1193
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
1194
|
+
} : void 0;
|
|
1195
|
+
this.sendTelemetry({
|
|
1196
|
+
status: "error",
|
|
1197
|
+
latencyMs,
|
|
1198
|
+
textLength: text.length,
|
|
1199
|
+
grade,
|
|
1200
|
+
provider: `openai:gpt-4o-2024-11-20 + ${complexityProviderName}`,
|
|
1201
|
+
tokenUsage: totalTokenUsage,
|
|
1202
|
+
errorCode: error instanceof Error ? error.name : "UnknownError",
|
|
1203
|
+
metadata: stageDetails.length > 0 ? { stage_details: stageDetails } : void 0,
|
|
1204
|
+
inputText: text
|
|
1205
|
+
}).catch(() => {
|
|
1206
|
+
});
|
|
1207
|
+
if (error instanceof ValidationError) {
|
|
1208
|
+
throw error;
|
|
1209
|
+
}
|
|
1210
|
+
throw wrapProviderError(error, "Vocabulary evaluation failed");
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Stage 1: Generate background knowledge assumption
|
|
1215
|
+
*
|
|
1216
|
+
* Estimates what topics the student at the given grade level would be familiar with
|
|
1217
|
+
* based on Common Core curriculum progression.
|
|
1218
|
+
*/
|
|
1219
|
+
async getBackgroundKnowledgeAssumption(text, grade) {
|
|
1220
|
+
const prompt = getBackgroundKnowledgePrompt(text, grade);
|
|
1221
|
+
const response = await this.backgroundKnowledgeProvider.generateText(
|
|
1222
|
+
[{ role: "user", content: prompt }],
|
|
1223
|
+
0
|
|
1224
|
+
// temperature = 0 for consistency
|
|
1225
|
+
);
|
|
1226
|
+
return {
|
|
1227
|
+
knowledge: {
|
|
1228
|
+
assumption: response.text.trim(),
|
|
1229
|
+
grade
|
|
1230
|
+
},
|
|
1231
|
+
usage: response.usage,
|
|
1232
|
+
latencyMs: response.latencyMs
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Stage 2: Evaluate vocabulary complexity
|
|
1237
|
+
*
|
|
1238
|
+
* Uses the Qual Text Complexity rubric (SAP) and background knowledge to evaluate vocabulary complexity.
|
|
1239
|
+
* Grades 3-4 use Gemini 2.5 Pro; grades 5-12 use GPT-4.1.
|
|
1240
|
+
*/
|
|
1241
|
+
async evaluateComplexity(text, grade, backgroundKnowledge, fkLevel) {
|
|
1242
|
+
const systemPrompt = getSystemPrompt(grade);
|
|
1243
|
+
const userPrompt = getUserPrompt(text, grade, backgroundKnowledge, fkLevel);
|
|
1244
|
+
const provider = grade === "3" || grade === "4" ? this.grades34ComplexityProvider : this.otherGradesComplexityProvider;
|
|
1245
|
+
const response = await provider.generateStructured({
|
|
1246
|
+
messages: [
|
|
1247
|
+
{ role: "system", content: systemPrompt },
|
|
1248
|
+
{ role: "user", content: userPrompt }
|
|
1249
|
+
],
|
|
1250
|
+
schema: VocabularyComplexitySchema,
|
|
1251
|
+
temperature: 0
|
|
1252
|
+
});
|
|
1253
|
+
return {
|
|
1254
|
+
data: response.data,
|
|
1255
|
+
usage: response.usage,
|
|
1256
|
+
latencyMs: response.latencyMs
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
var SentenceAnalysisSchema = z.object({
|
|
1261
|
+
reasoning: z.string().describe("Step-by-step reasoning for the analysis"),
|
|
1262
|
+
// Foundational
|
|
1263
|
+
num_sentences: z.number().int().describe("Total number of sentences in the text"),
|
|
1264
|
+
num_words: z.number().int().describe("Total number of words in the text"),
|
|
1265
|
+
flesch_kincaid_grade: z.number().describe("Flesch-Kincaid Grade Level number"),
|
|
1266
|
+
// Sentence Type
|
|
1267
|
+
num_simple_sentences: z.number().int().describe("Number of simple sentences"),
|
|
1268
|
+
num_compound_sentences: z.number().int().describe("Number of compound sentences"),
|
|
1269
|
+
num_complex_sentences: z.number().int().describe("Number of complex sentences"),
|
|
1270
|
+
num_compound_complex_sentences: z.number().int().describe("Number of compound-complex sentences"),
|
|
1271
|
+
num_other_sentences: z.number().int().describe("Number of other sentence types"),
|
|
1272
|
+
// Subordination
|
|
1273
|
+
num_independent_clauses: z.number().int(),
|
|
1274
|
+
num_subordinate_clauses: z.number().int(),
|
|
1275
|
+
num_total_clauses: z.number().int(),
|
|
1276
|
+
num_sentences_with_subordinate: z.number().int(),
|
|
1277
|
+
num_sentences_with_multiple_subordinates: z.number().int(),
|
|
1278
|
+
num_sentences_with_embedded_clauses: z.number().int(),
|
|
1279
|
+
// Informational Phrases
|
|
1280
|
+
num_prepositional_phrases: z.number().int(),
|
|
1281
|
+
num_participle_phrases: z.number().int(),
|
|
1282
|
+
num_appositive_phrases: z.number().int(),
|
|
1283
|
+
// Cohesion
|
|
1284
|
+
num_simple_transitions: z.number().int(),
|
|
1285
|
+
num_sophisticated_transitions: z.number().int(),
|
|
1286
|
+
// Sentence Type Density
|
|
1287
|
+
words_in_simple_sentences: z.number().int(),
|
|
1288
|
+
words_in_compound_sentences: z.number().int(),
|
|
1289
|
+
words_in_complex_sentences: z.number().int(),
|
|
1290
|
+
words_in_compound_complex_sentences: z.number().int(),
|
|
1291
|
+
words_in_other_sentences: z.number().int(),
|
|
1292
|
+
// Additional Features
|
|
1293
|
+
sentence_word_counts: z.array(z.number().int()),
|
|
1294
|
+
num_one_concept_sentences: z.number().int(),
|
|
1295
|
+
num_multi_concept_sentences: z.number().int(),
|
|
1296
|
+
num_cleft_sentences: z.number().int(),
|
|
1297
|
+
max_clauses_in_any_sentence: z.number().int(),
|
|
1298
|
+
// Grades 5-12 specific
|
|
1299
|
+
num_compound: z.number().int().describe("Number of compound sentences"),
|
|
1300
|
+
num_basic_complex: z.number().int().describe("Number of basic complex sentences"),
|
|
1301
|
+
num_advanced_complex: z.number().int().describe("Number of advanced complex sentences"),
|
|
1302
|
+
percentage_simple: z.number().describe("Percentage of simple sentences"),
|
|
1303
|
+
percentage_compound: z.number().describe("Percentage of compound sentences"),
|
|
1304
|
+
percentage_basic_complex: z.number().describe("Percentage of basic complex sentences"),
|
|
1305
|
+
percentage_advanced_complex: z.number().describe("Percentage of advanced complex sentences")
|
|
1306
|
+
});
|
|
1307
|
+
var ComplexityClassificationSchema = z.object({
|
|
1308
|
+
reasoning: z.string().describe("Detailed pedagogically appropriate reasoning"),
|
|
1309
|
+
answer: TextComplexityLevel
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
// ../../evals/prompts/sentence-structure/analysis-system.txt
|
|
1313
|
+
var analysis_system_default = "You are an expert in grammar and literacy.";
|
|
1314
|
+
|
|
1315
|
+
// ../../evals/prompts/sentence-structure/analysis-user.txt
|
|
1316
|
+
var analysis_user_default = `
|
|
1317
|
+
# Task
|
|
1318
|
+
I am going to give you a text, and I need you to look through the text sentence-by-sentence to perform a comprehensive grammatical analysis. Use the computational counts as a reference; they can be incorrect in ambiguous cases.
|
|
1319
|
+
|
|
1320
|
+
# Definitions
|
|
1321
|
+
* Sentences: Count a complete grammatical unit ending in a terminal punctuation mark.
|
|
1322
|
+
* Words: Count any sequence of characters separated by a space as one word. Treat hyphenated words (e.g., "state-of-the-art") and numbers (e.g., "2025") as single words.
|
|
1323
|
+
* Independent Clauses: Clauses that can stand alone as a complete sentence.
|
|
1324
|
+
* Subordinate Clauses: Clauses that are dependent on the main clause and cannot stand alone as a complete sentence.
|
|
1325
|
+
* Simple Sentences: Sentences with one independent clause and no subordinate clauses.
|
|
1326
|
+
* Compound Sentences: Sentences with two or more independent clauses and no subordinate clauses.
|
|
1327
|
+
* Complex Sentences: Sentences with one independent clause and at least one subordinate clause.
|
|
1328
|
+
* Compound-Complex Sentences: Sentences with two or more independent clauses and at least one subordinate clause.
|
|
1329
|
+
* Other / Non-Canonical Sentences: Sentences that cannot be reliably classified as simple, compound, complex, or compound-complex (e.g., sentence fragments, run-ons, elliptical responses, headlines, imperatives lacking an explicit subject, or stylized dialogue tags).
|
|
1330
|
+
* Subordinate Clauses: Clauses that are dependent on the main clause and cannot stand alone as a complete sentence.
|
|
1331
|
+
* Embedded Clauses: Clauses that are nested within another clause.
|
|
1332
|
+
* Prepositional Phrases: Phrases that begin with a preposition and end with a noun phrase.
|
|
1333
|
+
* Participle Phrases: Phrases that begin with a participle and end with a noun phrase.
|
|
1334
|
+
* Appositive Phrases: Phrases that rename or identify a noun phrase.
|
|
1335
|
+
* Simple Transitions: Basic coordinating conjunctions and chronological adverbs. Examples: 'and', 'but', 'or', 'so', 'then', 'next', 'first'.
|
|
1336
|
+
* Sophisticated Transitions: Conjunctive adverbs and phrases signaling logical relationships. Examples: 'however', 'therefore', 'consequently', 'as a result', 'for example', 'although'.
|
|
1337
|
+
* One-Concept Sentence: A sentence with ZERO subordinate clauses AND ZERO transition words/phrases (neither simple nor sophisticated).
|
|
1338
|
+
* Multi-Concept Sentence: Any sentence that has \u22651 subordinate clause OR \u22651 transition word/phrase (or both).
|
|
1339
|
+
* Basic Complex Sentences: Sentences with exactly one independent clause and at one dependent (subordinate) clause.
|
|
1340
|
+
* Advanced Complex Sentences: Sentences with two or more of any of those following (can include a mix, doesn't have to be two of the same type) subordinate phrases, clauses, transition words, or any other meaningful "interruptions" to the flow of the sentence (like not-only-but-also constructions, dashes, semicolons, and lengthy appositives). A sentence can be advanced complex if it has just one subordinate phrase or clause alongside a transition phrase, like: "For example, the British favored trade with Hong Kong, assuming favorable trade conditions.
|
|
1341
|
+
|
|
1342
|
+
# Computational Counts
|
|
1343
|
+
Use these as reference, your internal heuristics can be more reliable.
|
|
1344
|
+
{ground_truth_counts}
|
|
1345
|
+
|
|
1346
|
+
# Text to Analyze
|
|
1347
|
+
[BEGIN TEXT]
|
|
1348
|
+
{text}
|
|
1349
|
+
[END TEXT]
|
|
1350
|
+
|
|
1351
|
+
IMPORTANT: Your response should be a single JSON object with the following structure. Do not produce anything outside of the JSON object.
|
|
1352
|
+
|
|
1353
|
+
{format_instructions}
|
|
1354
|
+
`;
|
|
1355
|
+
|
|
1356
|
+
// src/prompts/sentence-structure/analysis.ts
|
|
1357
|
+
function getSystemPromptAnalysis() {
|
|
1358
|
+
return analysis_system_default;
|
|
1359
|
+
}
|
|
1360
|
+
function getUserPromptAnalysis(text, groundTruthCounts) {
|
|
1361
|
+
return analysis_user_default.replace("{text}", text).replace("{ground_truth_counts}", groundTruthCounts).replace("{format_instructions}", "");
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// ../../evals/prompts/sentence-structure/complexity-system.txt
|
|
1365
|
+
var complexity_system_default = "You are an expert in grammar and literacy, and understand K-12 and Qualitative Text Complexity rubric (SAP).";
|
|
1366
|
+
|
|
1367
|
+
// ../../evals/prompts/sentence-structure/complexity-user.txt
|
|
1368
|
+
var complexity_user_default = '\nYour task is to perform a text complexity analysis for a Grade {grade} student. You will be given a text excerpt and a set of quantitative sentence-level statistics for that text.\n\nYou must integrate both the qualitative aspects of the text and the quantitative statistics to make your final judgment. Do not rely on the numbers alone.\n\n1. Read the TEXT EXCERPT to understand its topic, conceptual load, and overall structure.\n2. Review the TEXT STATISTICS as a guide for complexity level.\n3. Synthesize your findings in your reasoning. Explain how the structure (qualitative) interact with the text statistics (quantitative) to determine the complexity. For example, a text with simple sentences might still be complex if the topic is very dense or abstract.\n\nYour final answer must be one of ["Slightly Complex," "Moderately Complex," "Very Complex", "Exceedingly Complex"].\n\n# GRADE {grade} RUBRIC\n{rubric}\n\n# TEXT EXCERPT\n[BEGIN TEXT]\n{excerpt}\n[END TEXT]\n\n# TEXT STATISTICS\n{sentence_features}\n\n# OUTPUT FORMAT\n{format_instructions}\n';
|
|
1369
|
+
|
|
1370
|
+
// ../../evals/prompts/sentence-structure/rubric-grade-3.txt
|
|
1371
|
+
var rubric_grade_3_default = '\n **Instructions for Analysis:** First, evaluate if the text meets the criteria for "Slightly Complex" or "Exceedingly Complex". If it does not fit into these categories, then decide between "Moderately Complex" and "Very Complex".\n\n **Slightly Complex:**\n * **Description:** The text consists of simple, straightforward language and sentence structures.\n * **Statistical Guidelines:** The text is likely "Slightly Complex" if it meets at least TWO of the following criteria:\n * **Sentence Type:** Primarily simple sentences. (`percent_simple_sentences` is typically > 60%).\n * **Sentence Length:** Short sentences. (`avg_sentence_length` is typically < 12 words).\n * **Subordination:** Very low use of clauses. (`percent_sentences_with_subordinate` is typically < 25%).\n\n **Moderately Complex:**\n * **Description:** The text shows a mix of simple and more complex sentences, introducing some variety in structure without being overly demanding.\n * **Statistical Guidelines:** If the text is not "Slightly Complex", consider "Moderately Complex" if it generally aligns with these ranges:\n * **Sentence Type:** A balanced mix of sentence types. (`percent_simple_sentences` is typically between 40% and 60%).\n * **Sentence Length:** Medium length sentences. (`avg_sentence_length` is typically between 12 and 16 words).\n * **Subordination:** A moderate use of clauses. (`percent_sentences_with_subordinate` is typically between 25% and 45%).\n\n **Very Complex:**\n * **Description:** The text features more elaborate sentences with multiple clauses and ideas, requiring more effort from the reader to parse. This is often the default category for grade-level text that isn\'t simple or exceptionally difficult.\n * **Statistical Guidelines:** If the text is more complex than "Moderately" but does not meet the "Exceedingly" criteria, it is likely "Very Complex". Key indicators include:\n * **Sentence Type:** Complex structures are common. (`percent_simple_sentences` is a minority, typically < 40%).\n * **Sentence Length:** Longer sentences are frequent. (`avg_sentence_length` is typically between 16 and 19 words).\n * **Subordination:** Subordinate clauses are a key feature. (`percent_sentences_with_subordinate` is typically > 45%).\n\n **Exceedingly Complex:**\n * **Description:** The text is dense with very long, intricate sentences and a high degree of subordination, making it exceptionally challenging for this grade level.\n * **Statistical Guidelines:** The text is "Exceedingly Complex" if it shows an extreme combination of sentence length and structural density. It should meet at least **TWO** of the following criteria, including at least **ONE** from the "Structural Density" group.\n * **Structural Density Indicators:**\n * High Subordination: `percent_sentences_with_subordinate` is extensive (typically > 50%).\n * Multiple Subordinates: `percent_sentences_with_multiple_subordinates` is consistently present (typically > 12%).\n * High Syntactic Complexity: `percent_compound_complex_sentences` is significant (typically > 15%).\n * **Length Indicators:**\n * Extreme Sentence Length: `avg_sentence_length` is very long (typically > 19 words).\n * Low Simplicity: `percent_simple_sentences` is very low (typically < 30%).\n * Concentrated Length: `percent_very_long_sentences` is notable (typically > 10%).\n';
|
|
1372
|
+
|
|
1373
|
+
// ../../evals/prompts/sentence-structure/rubric-grade-4.txt
|
|
1374
|
+
var rubric_grade_4_default = '\n **Instructions for Analysis:** First, evaluate if the text meets the criteria for "Slightly Complex" or "Exceedingly Complex". If it does not fit into these categories, then decide between "Moderately Complex" and "Very Complex".\n\n **Slightly Complex:**\n * **Description:** The text uses clear, direct language with basic sentence structures appropriate for developing readers.\n * **Statistical Guidelines:** The text is likely "Slightly Complex" if it meets at least TWO of the following criteria:\n * **Sentence Type:** Dominated by simple sentences. (`percent_simple_sentences` is typically > 55%).\n * **Sentence Length:** Short to medium sentences. (`avg_sentence_length` is typically < 13 words).\n * **Subordination:** Infrequent use of clauses. (`percent_sentences_with_subordinate` is typically < 30%).\n\n **Moderately Complex:**\n * **Description:** The text contains a variety of sentence structures, including compound and complex sentences, but remains accessible.\n * **Statistical Guidelines:** If the text is not "Slightly Complex", consider "Moderately Complex" if it generally aligns with these ranges:\n * **Sentence Type:** A healthy mix of sentence types. (`percent_simple_sentences` is typically between 40% and 55%).\n * **Sentence Length:** Medium length sentences. (`avg_sentence_length` is typically between 13 and 17 words).\n * **Subordination:** A moderate number of clauses. (`percent_sentences_with_subordinate` is typically between 30% and 50%).\n\n **Very Complex:**\n * **Description:** The text is characterized by longer sentences and the regular use of dependent clauses, requiring readers to track multiple ideas. This is the default for challenging, on-grade-level texts.\n * **Statistical Guidelines:** If the text is more complex than "Moderately" but does not meet the "Exceedingly" criteria, it is likely "Very Complex". Key indicators include:\n * **Sentence Type:** Simple sentences are a clear minority. (`percent_simple_sentences` is typically < 40%).\n * **Sentence Length:** Sentences are consistently long. (`avg_sentence_length` is typically between 17 and 22 words).\n * **Subordination:** Subordination is a major feature. (`percent_sentences_with_subordinate` is typically > 50%).\n * **Multiple Subordination:** Sentences with multiple clauses appear more often. (`percent_sentences_with_multiple_subordinates` is typically > 8%).\n\n **Exceedingly Complex:**\n * **Description:** The text\'s structure is highly sophisticated and dense, marked by extensive use of embedded clauses and long, flowing sentences that are well above grade-level expectations.\n * **Statistical Guidelines:** A text is "Exceedingly Complex" if its structure is highly sophisticated and dense. It should meet at least **TWO** of the following criteria, including at least **ONE** from the "Structural Density" group.\n * **Structural Density Indicators:**\n * High Subordination: `percent_sentences_with_subordinate` is very high (typically > 60%).\n * Multiple Subordinates: `percent_sentences_with_multiple_subordinates` is high and consistent (typically > 15%).\n * High Syntactic Complexity: `percent_compound_complex_sentences` is a notable feature (typically > 20%).\n * **Length Indicators:**\n * Extreme Sentence Length: `avg_sentence_length` is exceptionally long (typically > 22 words).\n * Low Simplicity: `percent_simple_sentences` is very low (typically < 25%).\n * Concentrated Length: `percent_very_long_sentences` is significant (typically > 15%).\n';
|
|
1375
|
+
|
|
1376
|
+
// ../../evals/prompts/sentence-structure/rubric-grades-5-12.txt
|
|
1377
|
+
var rubric_grades_5_12_default = "\n **Slightly Complex:** A text is in the Slightly Complex bucket if it has at least 50% simple sentences. If it doesn't, the text is a higher level of complexity. If the % of simple sentences is >= 50% and the % of compound sentences is >= 20%, the text is Moderately Complex, otherwise, the text is Slightly Complex. Slightly Complex texts NEVER have advanced complex sentences \u2014 the presence of an advanced complex sentence always leads to a higher level of complexity than Slightly.\n **For Moderately Complex:** These texts can take on any distribution of sentence types as long as there aren't more than 2 advanced complex sentences and as long as there aren't so many simple sentences that the text becomes Slightly Complex. That means Moderately Complex texts may have many simple sentences (although not so many that the text is Slightly Complex), compound sentences, and/or basic complex sentences. It's also possible for a moderately complex text to contain one or two advanced complex sentences, as long as there aren't more than 2. If there are more than 2, then the text is either Very or Exceedingly complex.\n **Very Complex:** These texts contain 3 or more advanced complex sentences (unless the percentage of advanced complex sentences is >= 65)%, in which case the text becomes Exceedingly Complex). They may still contain many simple, compound, and basic complex sentences, but a text is not Very Complex unless there are 3 or more advanced complex sentences.\n **Exceedingly Complex:** These texts have 65%+ of their sentences being advanced complex sentences.\n";
|
|
1378
|
+
|
|
1379
|
+
// src/prompts/sentence-structure/complexity.ts
|
|
1380
|
+
function getSystemPromptComplexity() {
|
|
1381
|
+
return complexity_system_default;
|
|
1382
|
+
}
|
|
1383
|
+
function getRubricForGrade(grade) {
|
|
1384
|
+
if (grade === "3") {
|
|
1385
|
+
return rubric_grade_3_default;
|
|
1386
|
+
} else if (grade === "4") {
|
|
1387
|
+
return rubric_grade_4_default;
|
|
1388
|
+
} else {
|
|
1389
|
+
return rubric_grades_5_12_default;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function getUserPromptComplexity(sentenceFeatures, grade, excerpt) {
|
|
1393
|
+
const rubric = getRubricForGrade(grade);
|
|
1394
|
+
return complexity_user_default.replace("{sentence_features}", sentenceFeatures).replace("{grade}", grade).replace("{rubric}", rubric).replace("{excerpt}", excerpt).replace("{format_instructions}", "");
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/evaluators/sentence-structure.ts
|
|
1398
|
+
function normalizeLabel(label) {
|
|
1399
|
+
if (!label) {
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
const normalized = label.trim().toLowerCase().replace(/_/g, " ");
|
|
1403
|
+
const mapping = {
|
|
1404
|
+
"slightly complex": "Slightly complex",
|
|
1405
|
+
"moderately complex": "Moderately complex",
|
|
1406
|
+
"very complex": "Very complex",
|
|
1407
|
+
"exceedingly complex": "Exceedingly complex",
|
|
1408
|
+
"extremely complex": "Exceedingly complex"
|
|
1409
|
+
};
|
|
1410
|
+
return mapping[normalized] ?? null;
|
|
1411
|
+
}
|
|
1412
|
+
var SentenceStructureEvaluator = class _SentenceStructureEvaluator extends BaseEvaluator {
|
|
1413
|
+
static metadata = {
|
|
1414
|
+
id: "sentence-structure",
|
|
1415
|
+
name: "Sentence Structure",
|
|
1416
|
+
description: "Evaluates sentence structure complexity based on grammatical features",
|
|
1417
|
+
supportedGrades: ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
|
1418
|
+
requiresGoogleKey: false,
|
|
1419
|
+
requiresOpenAIKey: true
|
|
1420
|
+
};
|
|
1421
|
+
analysisProvider;
|
|
1422
|
+
complexityProvider;
|
|
1423
|
+
constructor(config) {
|
|
1424
|
+
super(config);
|
|
1425
|
+
this.analysisProvider = createProvider({
|
|
1426
|
+
type: "openai",
|
|
1427
|
+
model: "gpt-4o",
|
|
1428
|
+
apiKey: config.openaiApiKey,
|
|
1429
|
+
maxRetries: this.config.maxRetries
|
|
1430
|
+
});
|
|
1431
|
+
this.complexityProvider = createProvider({
|
|
1432
|
+
type: "openai",
|
|
1433
|
+
model: "gpt-4o",
|
|
1434
|
+
apiKey: config.openaiApiKey,
|
|
1435
|
+
maxRetries: this.config.maxRetries
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Evaluate sentence structure complexity for a given text and grade level
|
|
1440
|
+
*
|
|
1441
|
+
* @param text - The text to evaluate
|
|
1442
|
+
* @param grade - The target grade level (3-12)
|
|
1443
|
+
* @returns Evaluation result with complexity score and detailed analysis
|
|
1444
|
+
* @throws {ValidationError} If text is empty, too short/long, or grade is invalid
|
|
1445
|
+
* @throws {APIError} If LLM API calls fail (includes AuthenticationError, RateLimitError, NetworkError, TimeoutError)
|
|
1446
|
+
*/
|
|
1447
|
+
async evaluate(text, grade) {
|
|
1448
|
+
this.logger.info("Starting sentence structure evaluation", {
|
|
1449
|
+
evaluator: "sentence-structure",
|
|
1450
|
+
operation: "evaluate",
|
|
1451
|
+
grade,
|
|
1452
|
+
textLength: text.length
|
|
1453
|
+
});
|
|
1454
|
+
const startTime = Date.now();
|
|
1455
|
+
const stageDetails = [];
|
|
1456
|
+
try {
|
|
1457
|
+
this.validateText(text);
|
|
1458
|
+
this.validateGrade(grade, new Set(_SentenceStructureEvaluator.metadata.supportedGrades));
|
|
1459
|
+
this.logger.debug("Stage 1: Analyzing sentence structure", {
|
|
1460
|
+
evaluator: "sentence-structure",
|
|
1461
|
+
operation: "sentence_analysis"
|
|
1462
|
+
});
|
|
1463
|
+
const analysisResponse = await this.analyzeSentenceStructure(text);
|
|
1464
|
+
stageDetails.push({
|
|
1465
|
+
stage: "sentence_analysis",
|
|
1466
|
+
provider: "openai:gpt-4o",
|
|
1467
|
+
latency_ms: analysisResponse.latencyMs,
|
|
1468
|
+
token_usage: {
|
|
1469
|
+
input_tokens: analysisResponse.usage.inputTokens,
|
|
1470
|
+
output_tokens: analysisResponse.usage.outputTokens
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
const features = addEngineeredFeatures(analysisResponse.data);
|
|
1474
|
+
this.logger.debug("Stage 2: Classifying complexity", {
|
|
1475
|
+
evaluator: "sentence-structure",
|
|
1476
|
+
operation: "complexity_classification"
|
|
1477
|
+
});
|
|
1478
|
+
const complexityResponse = await this.classifyComplexity(features, grade, text);
|
|
1479
|
+
stageDetails.push({
|
|
1480
|
+
stage: "complexity_classification",
|
|
1481
|
+
provider: "openai:gpt-4o",
|
|
1482
|
+
latency_ms: complexityResponse.latencyMs,
|
|
1483
|
+
token_usage: {
|
|
1484
|
+
input_tokens: complexityResponse.usage.inputTokens,
|
|
1485
|
+
output_tokens: complexityResponse.usage.outputTokens
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
const latencyMs = Date.now() - startTime;
|
|
1489
|
+
const totalTokenUsage = {
|
|
1490
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
1491
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
1492
|
+
};
|
|
1493
|
+
const result = {
|
|
1494
|
+
score: complexityResponse.data.answer,
|
|
1495
|
+
reasoning: complexityResponse.data.reasoning,
|
|
1496
|
+
metadata: {
|
|
1497
|
+
model: "openai:gpt-4o",
|
|
1498
|
+
processingTimeMs: latencyMs
|
|
1499
|
+
},
|
|
1500
|
+
_internal: {
|
|
1501
|
+
sentenceAnalysis: analysisResponse.data,
|
|
1502
|
+
features,
|
|
1503
|
+
complexity: complexityResponse.data
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
this.sendTelemetry({
|
|
1507
|
+
status: "success",
|
|
1508
|
+
latencyMs,
|
|
1509
|
+
textLength: text.length,
|
|
1510
|
+
grade,
|
|
1511
|
+
provider: "openai:gpt-4o",
|
|
1512
|
+
tokenUsage: totalTokenUsage,
|
|
1513
|
+
metadata: {
|
|
1514
|
+
stage_details: stageDetails
|
|
1515
|
+
},
|
|
1516
|
+
inputText: text
|
|
1517
|
+
}).catch(() => {
|
|
1518
|
+
});
|
|
1519
|
+
this.logger.info("Sentence structure evaluation completed successfully", {
|
|
1520
|
+
evaluator: "sentence-structure",
|
|
1521
|
+
operation: "evaluate",
|
|
1522
|
+
grade,
|
|
1523
|
+
score: result.score,
|
|
1524
|
+
processingTimeMs: latencyMs
|
|
1525
|
+
});
|
|
1526
|
+
return result;
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
const latencyMs = Date.now() - startTime;
|
|
1529
|
+
this.logger.error("Sentence structure evaluation failed", {
|
|
1530
|
+
evaluator: "sentence-structure",
|
|
1531
|
+
operation: "evaluate",
|
|
1532
|
+
grade,
|
|
1533
|
+
error: error instanceof Error ? error : void 0,
|
|
1534
|
+
processingTimeMs: latencyMs,
|
|
1535
|
+
completedStages: stageDetails.length
|
|
1536
|
+
});
|
|
1537
|
+
const totalTokenUsage = stageDetails.length > 0 ? {
|
|
1538
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
1539
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
1540
|
+
} : void 0;
|
|
1541
|
+
this.sendTelemetry({
|
|
1542
|
+
status: "error",
|
|
1543
|
+
latencyMs,
|
|
1544
|
+
textLength: text.length,
|
|
1545
|
+
grade,
|
|
1546
|
+
provider: "openai:gpt-4o",
|
|
1547
|
+
tokenUsage: totalTokenUsage,
|
|
1548
|
+
errorCode: error instanceof Error ? error.name : "UnknownError",
|
|
1549
|
+
metadata: stageDetails.length > 0 ? { stage_details: stageDetails } : void 0,
|
|
1550
|
+
inputText: text
|
|
1551
|
+
}).catch(() => {
|
|
1552
|
+
});
|
|
1553
|
+
if (error instanceof ValidationError) {
|
|
1554
|
+
throw error;
|
|
1555
|
+
}
|
|
1556
|
+
throw wrapProviderError(error, "Sentence structure evaluation failed");
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Stage 1: Analyze sentence grammatical structure
|
|
1561
|
+
*
|
|
1562
|
+
* Analyzes sentence types, clauses, phrases, transitions, and other grammatical features
|
|
1563
|
+
*/
|
|
1564
|
+
async analyzeSentenceStructure(text) {
|
|
1565
|
+
const metrics = calculateReadabilityMetrics(text);
|
|
1566
|
+
const gtCountsStr = [
|
|
1567
|
+
`num_sentences: ${metrics.sentenceCount}`,
|
|
1568
|
+
`num_words: ${metrics.wordCount}`,
|
|
1569
|
+
`num_char: ${metrics.characterCount}`,
|
|
1570
|
+
`num_syllable: ${metrics.syllableCount}`,
|
|
1571
|
+
`flesch_kincaid_grade: ${metrics.fleschKincaidGrade}`
|
|
1572
|
+
].join("\n");
|
|
1573
|
+
const userPrompt = getUserPromptAnalysis(text, gtCountsStr);
|
|
1574
|
+
const response = await this.analysisProvider.generateStructured({
|
|
1575
|
+
messages: [
|
|
1576
|
+
{ role: "system", content: getSystemPromptAnalysis() },
|
|
1577
|
+
{ role: "user", content: userPrompt }
|
|
1578
|
+
],
|
|
1579
|
+
schema: SentenceAnalysisSchema,
|
|
1580
|
+
temperature: 0
|
|
1581
|
+
});
|
|
1582
|
+
return {
|
|
1583
|
+
data: response.data,
|
|
1584
|
+
usage: response.usage,
|
|
1585
|
+
latencyMs: response.latencyMs
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Stage 2: Classify sentence structure complexity
|
|
1590
|
+
*
|
|
1591
|
+
* Uses engineered features and grade-specific rubric to classify complexity level
|
|
1592
|
+
*/
|
|
1593
|
+
async classifyComplexity(features, grade, excerpt) {
|
|
1594
|
+
const featuresJSON = featuresToJSON(features, 1, true);
|
|
1595
|
+
const userPrompt = getUserPromptComplexity(featuresJSON, grade, excerpt);
|
|
1596
|
+
const response = await this.complexityProvider.generateStructured({
|
|
1597
|
+
messages: [
|
|
1598
|
+
{ role: "system", content: getSystemPromptComplexity() },
|
|
1599
|
+
{ role: "user", content: userPrompt }
|
|
1600
|
+
],
|
|
1601
|
+
schema: ComplexityClassificationSchema,
|
|
1602
|
+
temperature: 0
|
|
1603
|
+
});
|
|
1604
|
+
const normalizedAnswer = normalizeLabel(response.data.answer);
|
|
1605
|
+
if (!normalizedAnswer) {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
`Failed to normalize complexity label. Received unexpected value: "${response.data.answer}". Expected one of: Slightly Complex, Moderately Complex, Very Complex, Exceedingly Complex, Extremely Complex.`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
return {
|
|
1611
|
+
data: {
|
|
1612
|
+
...response.data,
|
|
1613
|
+
answer: normalizedAnswer
|
|
1614
|
+
},
|
|
1615
|
+
usage: response.usage,
|
|
1616
|
+
latencyMs: response.latencyMs
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
var GradeBand = z.enum(["K-1", "2-3", "4-5", "6-8", "9-10", "11-CCR"]);
|
|
1621
|
+
var GradeLevelAppropriatenessSchema = z.object({
|
|
1622
|
+
reasoning: z.string().describe(
|
|
1623
|
+
"Your reasoning for your answer in numbered bullet points for 4 steps with a 4th bullet point for synthesis."
|
|
1624
|
+
),
|
|
1625
|
+
grade: GradeBand.describe("The appropriate grade level for the text"),
|
|
1626
|
+
alternative_grade: GradeBand.describe("An alternative grade level for the text"),
|
|
1627
|
+
scaffolding_needed: z.string().describe("Scaffolding needed for the text to be appropriate for the alternative grade")
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
// ../../evals/prompts/grade-level-appropriateness/system.txt
|
|
1631
|
+
var system_default = "\nYou are an expert in English literature education for K-12.\nYour job is to help evaluate the grade level appropriateness of a given text.\n\nYou will be given a text and you should determine which grade level the text is appropriate for (grade levels include: K-1, 2-3, 4-5, 6-8, 9-10, 11-CCR)\n\nIMPORTANT: You should pay attention to the vocabulary used, topics of the text and readability of text.\n\nPlease first reason out loud about the vocabulary complexity of the text and then provide an answer between grade level options: K-1, 2-3, 4-5, 6-8, 9-10, 11-CCR.\n\n";
|
|
1632
|
+
|
|
1633
|
+
// ../../evals/prompts/grade-level-appropriateness/user.txt
|
|
1634
|
+
var user_default = '\nUse these steps to determine appropriate grade level for a text:\n1. Calculate word count and Flesch-Kincaid Grade Level of the text, and generate a grade band.\nHere are the bands guideline for word count\n\n2-3: 200-800 words\n4-5: 200-800 words\n6-8: 400-1000 words\n9-10: 500-1500 words\n11-12: 1501 words and more\n\nHere is the formula for Flesch-Kincaid Grade Level:\nFlesch-Kincaid Grade Level = 0.39 * (total words / total sentences) + 11.8 * (total syllables / total words) - 15.59\n\n\n2. Determine the qualitative complexity using this text complexity rubric:\nTEXT STRUCTURE\n\nExceedingly Complex\n \u2022 Deep, intricate, often ambiguous connections between many ideas/processes/events\n \u2022 Organization is intricate or discipline-specific\n \u2022 Text features are essential for understanding\n \u2022 Graphics are intricate, extensive, and integral to meaning; may convey unique information\n\nVery Complex\n \u2022 Expanded ideas/processes/events with implicit or subtle connections\n \u2022 Organization may have multiple pathways or discipline-specific traits\n \u2022 Text features directly enhance understanding\n \u2022 Graphics support or are integral to understanding\n\nModerately Complex\n \u2022 Some implicit/subtle connections between ideas/events\n \u2022 Organization is evident and generally sequential or chronological\n \u2022 Text features enhance understanding\n \u2022 Graphics are mostly supplementary\n\nSlightly Complex\n \u2022 Explicit and clear connections between ideas/events\n \u2022 Organization is chronological, sequential, or predictable\n \u2022 Text features help navigation but are not essential\n \u2022 Graphics are simple, not necessary, but may assist understanding\n\n\u2E3B\n\nLANGUAGE FEATURES\n\nExceedingly Complex\n \u2022 Dense, abstract, ironic, and/or figurative language\n \u2022 Complex, unfamiliar, archaic, subject-specific, or ambiguous vocabulary\n \u2022 Mainly complex sentences with multiple subordinate clauses and transitions\n\nVery Complex\n \u2022 Fairly complex; some abstract, ironic, and/or figurative language\n \u2022 Some unfamiliar, archaic, or overly academic vocabulary\n \u2022 Many complex sentences with subordinate phrases/clauses\n\nModerately Complex\n \u2022 Mostly explicit language with some complex meaning\n \u2022 Mostly familiar and conversational vocabulary\n \u2022 Primarily simple and compound sentences, with some complex ones\n\nSlightly Complex\n \u2022 Explicit, literal, straightforward language\n \u2022 Contemporary, familiar, conversational vocabulary\n \u2022 Mainly simple sentences\n\n\u2E3B\n\nPURPOSE\n\nExceedingly Complex\n \u2022 Subtle, intricate, and difficult to determine\n \u2022 Includes many theoretical or abstract elements\n\nVery Complex\n \u2022 Implicit or subtle, fairly easy to infer\n \u2022 More theoretical or abstract than concrete\n\nModerately Complex\n \u2022 Implied but easy to identify based on context or source\n\nSlightly Complex\n \u2022 Explicitly stated, clear, concrete, and narrowly focused\n\n\u2E3B\n\nKNOWLEDGE DEMANDS\n\nExceedingly Complex\n \u2022 Requires extensive discipline-specific or theoretical knowledge\n \u2022 Many references/allusions to other texts or ideas\n\nVery Complex\n \u2022 Requires moderate discipline-specific knowledge\n \u2022 Some references/allusions to other texts or ideas\n\nModerately Complex\n \u2022 Requires common knowledge and some discipline-specific knowledge\n \u2022 Few references/allusions\n\nSlightly Complex\n \u2022 Requires everyday, practical knowledge\n \u2022 No references/allusions\n\n3. Background knowledge:\nAt which grade level would student have enough background knowledge to understand the text?\n\n4. Use your judgement of the above three steps. First use the quantitative signal to get first signal of the appropriate grade level range, then use qualitative analysis to refine your decisions and consider if student at such grade will have enough background knowledge to arrive at a final grade level band. Also consider if the text can be for a lower grade with additional scaffolding.\n\n<begin of text to evaluate>\n<text>{text}</text>\n<end of text to evaluate>\n\nWhen providing your response, first think out loud of your reasoning and then provide your answer from one of the grade band options above. Your reasoning and answer needs to be in JSON format. Strictly follow the following format for your response.\n\nYour final answer should be in the "grade" property for the target grade band for the text aimed for independent reading. If there is alternative appropriate grade students can read and comprehend with scaffold (eg. picture, graph, additional context, etc) or for read-aloud purposes for lower grade, provide it in the "alternative_grade" property and provide the types of scaffolding in the "scaffolding_needed" property.\n\nIn your reasoning, provide numbered bullet points for each of the analyses in each of the 3 steps. At the end, give me the 4th bullet point called "synthesis" to summarize your analysis from the above 3 steps that help you arrive at the final decision.\n\n{format_instructions}\n';
|
|
1635
|
+
|
|
1636
|
+
// src/prompts/grade-level-appropriateness/index.ts
|
|
1637
|
+
function getSystemPrompt2() {
|
|
1638
|
+
return system_default;
|
|
1639
|
+
}
|
|
1640
|
+
function getUserPrompt2(text) {
|
|
1641
|
+
return user_default.replace("{text}", text).replace("{format_instructions}", "");
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// src/evaluators/grade-level-appropriateness.ts
|
|
1645
|
+
var GradeLevelAppropriatenessEvaluator = class extends BaseEvaluator {
|
|
1646
|
+
static metadata = {
|
|
1647
|
+
id: "grade-level-appropriateness",
|
|
1648
|
+
name: "Grade Level Appropriateness",
|
|
1649
|
+
description: "Determines appropriate grade level for text with scaffolding recommendations",
|
|
1650
|
+
supportedGrades: [],
|
|
1651
|
+
// No grade parameter required - evaluates what grade the text is appropriate for
|
|
1652
|
+
requiresGoogleKey: true,
|
|
1653
|
+
requiresOpenAIKey: false
|
|
1654
|
+
};
|
|
1655
|
+
provider;
|
|
1656
|
+
constructor(config) {
|
|
1657
|
+
super(config);
|
|
1658
|
+
this.provider = createProvider({
|
|
1659
|
+
type: "google",
|
|
1660
|
+
model: "gemini-2.5-pro",
|
|
1661
|
+
apiKey: config.googleApiKey,
|
|
1662
|
+
maxRetries: this.config.maxRetries
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Evaluate grade level appropriateness for a given text
|
|
1667
|
+
*
|
|
1668
|
+
* @param text - The text to evaluate
|
|
1669
|
+
* @returns Evaluation result with grade recommendations and scaffolding suggestions
|
|
1670
|
+
* @throws {ValidationError} If text is empty or too short/long
|
|
1671
|
+
* @throws {APIError} If LLM API calls fail (includes AuthenticationError, RateLimitError, NetworkError, TimeoutError)
|
|
1672
|
+
*/
|
|
1673
|
+
async evaluate(text) {
|
|
1674
|
+
this.logger.info("Starting grade level appropriateness evaluation", {
|
|
1675
|
+
evaluator: "grade-level-appropriateness",
|
|
1676
|
+
operation: "evaluate",
|
|
1677
|
+
textLength: text.length
|
|
1678
|
+
});
|
|
1679
|
+
const startTime = Date.now();
|
|
1680
|
+
try {
|
|
1681
|
+
this.validateText(text);
|
|
1682
|
+
this.logger.debug("Evaluating grade level appropriateness", {
|
|
1683
|
+
evaluator: "grade-level-appropriateness",
|
|
1684
|
+
operation: "grade_evaluation"
|
|
1685
|
+
});
|
|
1686
|
+
const userPrompt = getUserPrompt2(text);
|
|
1687
|
+
const response = await this.provider.generateStructured({
|
|
1688
|
+
messages: [
|
|
1689
|
+
{ role: "system", content: getSystemPrompt2() },
|
|
1690
|
+
{ role: "user", content: userPrompt }
|
|
1691
|
+
],
|
|
1692
|
+
schema: GradeLevelAppropriatenessSchema,
|
|
1693
|
+
temperature: 0.25
|
|
1694
|
+
});
|
|
1695
|
+
const latencyMs = Date.now() - startTime;
|
|
1696
|
+
const tokenUsage = {
|
|
1697
|
+
input_tokens: response.usage.inputTokens,
|
|
1698
|
+
output_tokens: response.usage.outputTokens
|
|
1699
|
+
};
|
|
1700
|
+
const result = {
|
|
1701
|
+
score: response.data.grade,
|
|
1702
|
+
reasoning: response.data.reasoning,
|
|
1703
|
+
metadata: {
|
|
1704
|
+
model: "google:gemini-2.5-pro",
|
|
1705
|
+
processingTimeMs: latencyMs
|
|
1706
|
+
},
|
|
1707
|
+
_internal: response.data
|
|
1708
|
+
};
|
|
1709
|
+
this.sendTelemetry({
|
|
1710
|
+
status: "success",
|
|
1711
|
+
latencyMs,
|
|
1712
|
+
textLength: text.length,
|
|
1713
|
+
provider: "google:gemini-2.5-pro",
|
|
1714
|
+
tokenUsage,
|
|
1715
|
+
// No metadata.stage_details for single-stage evaluator
|
|
1716
|
+
inputText: text
|
|
1717
|
+
}).catch(() => {
|
|
1718
|
+
});
|
|
1719
|
+
this.logger.info("Grade level appropriateness evaluation completed successfully", {
|
|
1720
|
+
evaluator: "grade-level-appropriateness",
|
|
1721
|
+
operation: "evaluate",
|
|
1722
|
+
grade: result.score,
|
|
1723
|
+
processingTimeMs: latencyMs
|
|
1724
|
+
});
|
|
1725
|
+
return result;
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
const latencyMs = Date.now() - startTime;
|
|
1728
|
+
this.logger.error("Grade level appropriateness evaluation failed", {
|
|
1729
|
+
evaluator: "grade-level-appropriateness",
|
|
1730
|
+
operation: "evaluate",
|
|
1731
|
+
error: error instanceof Error ? error : void 0,
|
|
1732
|
+
processingTimeMs: latencyMs
|
|
1733
|
+
});
|
|
1734
|
+
this.sendTelemetry({
|
|
1735
|
+
status: "error",
|
|
1736
|
+
latencyMs,
|
|
1737
|
+
textLength: text.length,
|
|
1738
|
+
provider: "google:gemini-2.5-pro",
|
|
1739
|
+
errorCode: error instanceof Error ? error.name : "UnknownError",
|
|
1740
|
+
inputText: text
|
|
1741
|
+
}).catch(() => {
|
|
1742
|
+
});
|
|
1743
|
+
if (error instanceof ValidationError) {
|
|
1744
|
+
throw error;
|
|
1745
|
+
}
|
|
1746
|
+
throw wrapProviderError(error, "Grade level appropriateness evaluation failed");
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
var SmkOutputSchema = z.object({
|
|
1751
|
+
identified_topics: z.array(z.string()).describe("List of major subjects/concepts found in the text."),
|
|
1752
|
+
curriculum_check: z.string().describe("Whether the topics are standard K-8 or specialized high school level."),
|
|
1753
|
+
assumptions_and_scaffolding: z.string().describe("What the author assumes the reader knows vs. what is explained."),
|
|
1754
|
+
friction_analysis: z.string().describe("Whether difficulty comes from vocabulary/structure or actual knowledge demands."),
|
|
1755
|
+
complexity_score: TextComplexityLevel.describe("The subject matter knowledge complexity level of the text"),
|
|
1756
|
+
reasoning: z.string().describe("A brief synthesis of why the text fits the chosen complexity level.")
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
// ../../evals/prompts/subject-matter-knowledge/system.txt
|
|
1760
|
+
var system_default2 = `
|
|
1761
|
+
To perform the task of evaluating text complexity based on Subject Matter Knowledge (SMK), strictly adhere to the following instructions.
|
|
1762
|
+
Role
|
|
1763
|
+
You are an expert K-12 Literacy Pedagogue and Text Complexity Evaluator. Your specific focus is analyzing Subject Matter Knowledge (SMK) demands according to the Common Core Qualitative Text Complexity Rubric.
|
|
1764
|
+
Objective
|
|
1765
|
+
Analyze a provided text relative to a target grade_level. You must determine the extent of background knowledge required to comprehend the text. You must distinguish between Common/Standard knowledge (generally lower/moderate complexity) and Specialized/Theoretical knowledge (generally higher complexity).
|
|
1766
|
+
Input Data
|
|
1767
|
+
text: The passage to analyze.
|
|
1768
|
+
grade_level: The target student grade (integer).
|
|
1769
|
+
fk_score: Flesch-Kincaid Grade Level. Note: Use this only as a loose proxy for sentence structure. Do not let a high FK score artificially inflate the Subject Matter Knowledge score if the concepts remain simple.
|
|
1770
|
+
|
|
1771
|
+
1. The Rubric: Subject Matter Knowledge (SMK)
|
|
1772
|
+
1. Slightly Complex
|
|
1773
|
+
Scope: Everyday, practical knowledge, and Introduction to Skills.
|
|
1774
|
+
Concept Type: Concrete, directly observable, and familiar.
|
|
1775
|
+
Key Indicator: "How-to" texts involving familiar objects (e.g., drawing a cupboard, playing a game, family life). Even if specific terms (like "scale" or "measure") are used, if the application is on a common object, it remains Slightly Complex.
|
|
1776
|
+
2. Moderately Complex
|
|
1777
|
+
Scope: Common Discipline-Specific Knowledge or Narrative History.
|
|
1778
|
+
Definition: Topics widely introduced in K-8 curricula (Basic American History, Geography, Earth Science, Biology).
|
|
1779
|
+
Key Characteristic: The text bridges concrete descriptions with abstract themes (e.g., using farming to discuss justice), OR narrates historical events via sensory details.
|
|
1780
|
+
Spatial Reasoning: Texts requiring mental manipulation of maps/routes are generally Moderate, unless the object is a familiar household item (see Slightly Complex).
|
|
1781
|
+
3. Very Complex
|
|
1782
|
+
Scope: Specialized Discipline-Specific, Engineering Mechanics, or Political Theory.
|
|
1783
|
+
Definition: Topics characteristic of High School (9-12) curricula requiring abstract mental models.
|
|
1784
|
+
Key Characteristic: Requires understanding mechanisms (how physics works/propulsion), chemical composition, or undefined political stakes (specific treaties, alliances, or secularization without context).
|
|
1785
|
+
4. Exceedingly Complex
|
|
1786
|
+
Scope: Professional or Academic knowledge.
|
|
1787
|
+
|
|
1788
|
+
2. The Expert Mental Model (Decision Logic)
|
|
1789
|
+
Use these refined rules to categorize cases.
|
|
1790
|
+
Rule A: The "Layers of Meaning" Check
|
|
1791
|
+
Concrete -> Abstract (Moderate): The text describes concrete things (farming) to argue an abstract point (justice, rights).
|
|
1792
|
+
Concrete -> Concrete (Slightly): The text describes concrete things (lines, paper) to achieve a concrete result (drawing a cupboard). Do not over-rank practical instructions.
|
|
1793
|
+
Rule B: The Science & Engineering Boundary
|
|
1794
|
+
Observational (Moderate): Habitats, Water Cycle, observable traits, simple definitions.
|
|
1795
|
+
Mechanistic/Theoretical (Very): Engineering mechanics (how propulsion works via reaction), Instrumentation (using a spectroscope), or Chemical/Atomic theory.
|
|
1796
|
+
Test: Does the text explain how a machine functions using physical principles? If yes, it is Very Complex.
|
|
1797
|
+
Rule C: The History/Social Studies Boundary
|
|
1798
|
+
General/Narrative (Moderate):
|
|
1799
|
+
Sensory: Battle descriptions focusing on sights/sounds (flashes, smoke).
|
|
1800
|
+
Standard Topics: Immigration, Slavery, Government, Geography. Lists of nationalities or religions are "Common Knowledge" for Grades 6-8.
|
|
1801
|
+
Political/Contextual (Very):
|
|
1802
|
+
Implicit Context: Texts assuming knowledge of specific political factions, treaties, or the causes of events without explanation (e.g., "The Allies," "The Front," "The secularization of the clergy").
|
|
1803
|
+
Test: If the reader must know why two groups are fighting or the specific political history of a revolution to understand the text, it is Very Complex.
|
|
1804
|
+
Rule D: The "Technical vs. Practical" Trap
|
|
1805
|
+
Scenario: A text teaches a technical skill (e.g., Technical Drawing/Technology) but applies it to a familiar object (a cupboard).
|
|
1806
|
+
Decision: Slightly Complex.
|
|
1807
|
+
Reasoning: Do not confuse "Technical Vocabulary" (scale, thick lines) with "Theoretical Complexity." If the underlying concept is familiar (furniture), the SMK load is low.
|
|
1808
|
+
|
|
1809
|
+
3. Critical Calibration Examples
|
|
1810
|
+
Text: "Make a rough sketch... How many shelves should the cupboard have?" (Grade 2) -> Slightly Complex.
|
|
1811
|
+
Reasoning: (Rule D/Rule A) Although it mentions "scale" and "technology," the task is concrete and relies on everyday knowledge.
|
|
1812
|
+
Text: "Hydraulic propulsion works by sucking water at the bow and forcing it sternward." (Grade 10) -> Very Complex.
|
|
1813
|
+
Reasoning: (Rule B) Explains a mechanism using physics principles.
|
|
1814
|
+
Text: "The Allies fight the enemy's cavalry; we remember the hospitality to priests during the Revolution." (Grade 6) -> Very Complex.
|
|
1815
|
+
Reasoning: (Rule C) Assumes undefined knowledge of WWI alliances and the specific political history of the French Revolution.
|
|
1816
|
+
Text: "Immigrants from Poland, Italy, and Russia arrived. Most were Catholic or Orthodox." (Grade 7) -> Moderately Complex.
|
|
1817
|
+
Reasoning: (Rule C) Standard K-8 topic. Lists of nationalities are content vocabulary, not specialized theory.
|
|
1818
|
+
|
|
1819
|
+
4. Output Format
|
|
1820
|
+
Return your analysis in a valid JSON object. Do not include markdown formatting.
|
|
1821
|
+
Keys:
|
|
1822
|
+
- identified_topics: List[str] identifying the core subjects.
|
|
1823
|
+
- curriculum_check: String explaining if the topics are "Standard/General" (typical for K-8) or "Specialized/High School" (typical for 9-12).
|
|
1824
|
+
- assumptions_and_scaffolding: String analyzing what the author assumes the reader knows vs what is explained.
|
|
1825
|
+
- friction_analysis: String discussing the gap between Concrete description and Abstract meaning.
|
|
1826
|
+
- complexity_score: String (One of: slightly_complex, moderately_complex, very_complex, exceedingly_complex).
|
|
1827
|
+
- reasoning: String synthesizing the decision.
|
|
1828
|
+
|
|
1829
|
+
`;
|
|
1830
|
+
|
|
1831
|
+
// ../../evals/prompts/subject-matter-knowledge/user.txt
|
|
1832
|
+
var user_default2 = "Analyze:\nText: {text}\nGrade: {grade}\nFK Score: {fk_score}";
|
|
1833
|
+
|
|
1834
|
+
// src/prompts/subject-matter-knowledge/index.ts
|
|
1835
|
+
function getSystemPrompt3() {
|
|
1836
|
+
return system_default2;
|
|
1837
|
+
}
|
|
1838
|
+
function getUserPrompt3(text, grade, fkScore) {
|
|
1839
|
+
return user_default2.replaceAll("{text}", text).replaceAll("{grade}", grade).replaceAll("{fk_score}", fkScore.toString());
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/evaluators/smk.ts
|
|
1843
|
+
var SmkEvaluator = class _SmkEvaluator extends BaseEvaluator {
|
|
1844
|
+
static metadata = {
|
|
1845
|
+
id: "subject-matter-knowledge",
|
|
1846
|
+
name: "Subject Matter Knowledge",
|
|
1847
|
+
description: "Evaluates background knowledge demands of educational texts relative to grade level",
|
|
1848
|
+
supportedGrades: ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
|
1849
|
+
requiresGoogleKey: true,
|
|
1850
|
+
requiresOpenAIKey: false
|
|
1851
|
+
};
|
|
1852
|
+
provider;
|
|
1853
|
+
constructor(config) {
|
|
1854
|
+
super(config);
|
|
1855
|
+
this.provider = createProvider({
|
|
1856
|
+
type: "google",
|
|
1857
|
+
model: "gemini-3-flash-preview",
|
|
1858
|
+
apiKey: config.googleApiKey,
|
|
1859
|
+
maxRetries: this.config.maxRetries
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Evaluate subject matter knowledge complexity for a given text and grade level
|
|
1864
|
+
*
|
|
1865
|
+
* @param text - The text to evaluate
|
|
1866
|
+
* @param grade - The target grade level (3-12)
|
|
1867
|
+
* @returns Evaluation result with complexity score and detailed analysis
|
|
1868
|
+
* @throws {ValidationError} If text is empty, too short/long, or grade is invalid
|
|
1869
|
+
* @throws {APIError} If LLM API calls fail (includes AuthenticationError, RateLimitError, NetworkError, TimeoutError)
|
|
1870
|
+
*/
|
|
1871
|
+
async evaluate(text, grade) {
|
|
1872
|
+
this.logger.info("Starting SMK evaluation", {
|
|
1873
|
+
evaluator: "subject-matter-knowledge",
|
|
1874
|
+
operation: "evaluate",
|
|
1875
|
+
grade,
|
|
1876
|
+
textLength: text.length
|
|
1877
|
+
});
|
|
1878
|
+
const startTime = Date.now();
|
|
1879
|
+
const stageDetails = [];
|
|
1880
|
+
try {
|
|
1881
|
+
this.validateText(text);
|
|
1882
|
+
this.validateGrade(grade, new Set(_SmkEvaluator.metadata.supportedGrades));
|
|
1883
|
+
this.logger.debug("Evaluating subject matter knowledge complexity", {
|
|
1884
|
+
evaluator: "subject-matter-knowledge",
|
|
1885
|
+
operation: "smk_evaluation"
|
|
1886
|
+
});
|
|
1887
|
+
const fkScore = calculateFleschKincaidGrade(text);
|
|
1888
|
+
const response = await this.evaluateSmk(text, grade, fkScore);
|
|
1889
|
+
stageDetails.push({
|
|
1890
|
+
stage: "smk_evaluation",
|
|
1891
|
+
provider: "google:gemini-3-flash-preview",
|
|
1892
|
+
latency_ms: response.latencyMs,
|
|
1893
|
+
token_usage: {
|
|
1894
|
+
input_tokens: response.usage.inputTokens,
|
|
1895
|
+
output_tokens: response.usage.outputTokens
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
const latencyMs = Date.now() - startTime;
|
|
1899
|
+
const totalTokenUsage = {
|
|
1900
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
1901
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
1902
|
+
};
|
|
1903
|
+
const result = {
|
|
1904
|
+
score: response.data.complexity_score,
|
|
1905
|
+
reasoning: response.data.reasoning,
|
|
1906
|
+
metadata: {
|
|
1907
|
+
model: "google:gemini-3-flash-preview",
|
|
1908
|
+
processingTimeMs: latencyMs
|
|
1909
|
+
},
|
|
1910
|
+
_internal: response.data
|
|
1911
|
+
};
|
|
1912
|
+
this.sendTelemetry({
|
|
1913
|
+
status: "success",
|
|
1914
|
+
latencyMs,
|
|
1915
|
+
textLength: text.length,
|
|
1916
|
+
grade,
|
|
1917
|
+
provider: "google:gemini-3-flash-preview",
|
|
1918
|
+
tokenUsage: totalTokenUsage,
|
|
1919
|
+
metadata: {
|
|
1920
|
+
stage_details: stageDetails
|
|
1921
|
+
},
|
|
1922
|
+
inputText: text
|
|
1923
|
+
}).catch(() => {
|
|
1924
|
+
});
|
|
1925
|
+
this.logger.info("SMK evaluation completed successfully", {
|
|
1926
|
+
evaluator: "subject-matter-knowledge",
|
|
1927
|
+
operation: "evaluate",
|
|
1928
|
+
grade,
|
|
1929
|
+
score: result.score,
|
|
1930
|
+
processingTimeMs: latencyMs
|
|
1931
|
+
});
|
|
1932
|
+
return result;
|
|
1933
|
+
} catch (error) {
|
|
1934
|
+
const latencyMs = Date.now() - startTime;
|
|
1935
|
+
this.logger.error("SMK evaluation failed", {
|
|
1936
|
+
evaluator: "subject-matter-knowledge",
|
|
1937
|
+
operation: "evaluate",
|
|
1938
|
+
grade,
|
|
1939
|
+
error: error instanceof Error ? error : void 0,
|
|
1940
|
+
processingTimeMs: latencyMs,
|
|
1941
|
+
completedStages: stageDetails.length
|
|
1942
|
+
});
|
|
1943
|
+
const totalTokenUsage = stageDetails.length > 0 ? {
|
|
1944
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
1945
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
1946
|
+
} : void 0;
|
|
1947
|
+
this.sendTelemetry({
|
|
1948
|
+
status: "error",
|
|
1949
|
+
latencyMs,
|
|
1950
|
+
textLength: text.length,
|
|
1951
|
+
grade,
|
|
1952
|
+
provider: "google:gemini-3-flash-preview",
|
|
1953
|
+
tokenUsage: totalTokenUsage,
|
|
1954
|
+
errorCode: error instanceof Error ? error.name : "UnknownError",
|
|
1955
|
+
metadata: stageDetails.length > 0 ? { stage_details: stageDetails } : void 0,
|
|
1956
|
+
inputText: text
|
|
1957
|
+
}).catch(() => {
|
|
1958
|
+
});
|
|
1959
|
+
if (error instanceof ValidationError) {
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1962
|
+
throw wrapProviderError(error, "SMK evaluation failed");
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Run the SMK evaluation LLM call
|
|
1967
|
+
*/
|
|
1968
|
+
async evaluateSmk(text, grade, fkScore) {
|
|
1969
|
+
const response = await this.provider.generateStructured({
|
|
1970
|
+
messages: [
|
|
1971
|
+
{ role: "system", content: getSystemPrompt3() },
|
|
1972
|
+
{ role: "user", content: getUserPrompt3(text, grade, fkScore) }
|
|
1973
|
+
],
|
|
1974
|
+
schema: SmkOutputSchema,
|
|
1975
|
+
temperature: 0
|
|
1976
|
+
});
|
|
1977
|
+
return {
|
|
1978
|
+
data: response.data,
|
|
1979
|
+
usage: response.usage,
|
|
1980
|
+
latencyMs: response.latencyMs
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
var ConventionalityOutputSchema = z.object({
|
|
1985
|
+
conventionality_features: z.array(z.string()).describe("The specific language features driving the complexity (e.g., literal narrative, concrete actions, sustained irony, abstract qualities) with direct quotes from the text."),
|
|
1986
|
+
grade_context: z.string().describe("How the conventionality demands compare to general expectations for the provided target grade."),
|
|
1987
|
+
instructional_insights: z.string().describe("Actionable pedagogical suggestions for scaffolding the conventionality features in the classroom."),
|
|
1988
|
+
complexity_score: TextComplexityLevel.describe("The conventionality complexity level of the text"),
|
|
1989
|
+
reasoning: z.string().describe("A detailed explanation of the rating, citing specific features in the text and referencing the expert guardrails.")
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
// ../../evals/prompts/conventionality/system.txt
|
|
1993
|
+
var system_default3 = `Role
|
|
1994
|
+
You are an expert reading teacher and text complexity evaluator. Your task is to evaluate the "Conventionality" of a text and assign it a complexity level based on a 4-point scale, carefully factoring in the target grade level.
|
|
1995
|
+
|
|
1996
|
+
Objective
|
|
1997
|
+
Measure how explicit, literal, and straightforward the text's meaning is, versus how abstract, ironic, figurative, or archaic it is. Focus on the hiddenness of the meaning, the use of conceptual framing, the reliance on abstract reasoning, and the familiarity of the expression for the target grade.
|
|
1998
|
+
|
|
1999
|
+
Complexity Levels
|
|
2000
|
+
- Slightly Complex: Explicit, literal, straightforward, easy to understand. Meaning is entirely on the surface. The language is concrete, and the meaning is clear and procedural, mostly referring to observable materials and actions. Contains no symbolic or ironic language, and conceptual interpretation is not required. Contains limited figurative language that is common and easy to comprehend at the target grade level.
|
|
2001
|
+
- Moderately Complex: Largely explicit and easy to understand with some occasions for more complex meaning. May contain a noticeable amount of archaic/dated phrasing, formal historical prose, vocabulary demands, background knowledge requirements, or expressions that are less familiar to the target grade level, which might make the text feel vague or slightly challenging.
|
|
2002
|
+
- Very Complex: Fairly complex; contains sustained abstract language, conceptual framing, rhetorical idealization, ironic comparisons, or central metaphors that drive the meaning of the text. Addresses concepts, beliefs, and abstract qualities rather than just concrete objects. The tone or underlying message requires interpretation, even if the surface message is clear.
|
|
2003
|
+
- Exceedingly Complex: Dense and complex; contains considerable abstract, ironic, and/or figurative language. Meaning is heavily hidden, deeply conceptual, or relies heavily on complex rhetorical devices.
|
|
2004
|
+
|
|
2005
|
+
Essential Evaluation Rules
|
|
2006
|
+
1. Concrete & Procedural Texts: Texts that are highly concrete, clear, and procedural (e.g., describing observable materials, mechanical processes, or physical actions) should typically be rated "Slightly Complex."
|
|
2007
|
+
|
|
2008
|
+
2. Grade-Level Anchoring and Vague Narratives: Always consider the target grade. A literal historical narrative that might be straightforward for older students can be "Moderately Complex" for younger students (e.g., 4th graders) if it involves less familiar expressions, older contexts (e.g., wagon loads, traveling by horseback), vocabulary demands, and background knowledge requirements that make the text feel vague or slightly demanding for that age group.
|
|
2009
|
+
|
|
2010
|
+
3. Rhetorical Idealization and Abstract Qualities: If an entire argument or narrative is built around abstract qualities (e.g., national character, bravery, liberty) and uses repeated figurative language or personification to portray a subject in a certain idealized way, rate the text as "Very Complex." Even if the figurative language is easy to interpret, the need to interpret the rhetorical tone and sustained abstract focus elevates the complexity beyond level two.
|
|
2011
|
+
|
|
2012
|
+
4. Common Idioms and Grade-Level Appropriateness: Do NOT elevate a text to "Moderately Complex" simply because it contains a few common idiomatic expressions. If these expressions are widely known and easy for the target grade to understand without making the text feel vague, the text remains "Slightly Complex."
|
|
2013
|
+
|
|
2014
|
+
5. Conversational and Hypothetical Framing: Using a second-person conversational hook (e.g., "Imagine you are...") to explain a concept is a standard, literal device for engaging readers. It does not constitute complex conceptual framing.
|
|
2015
|
+
|
|
2016
|
+
6. Sustained vs. Occasional Impact: If abstract language, figurative phrasing, irony, or conceptual framing is sustained throughout the text and central to the argument/meaning, the text is Very Complex. Reserve Moderately Complex for texts where the explicit meaning dominates but the expression, vocabulary, or archaic language provides a moderate conventionality challenge.
|
|
2017
|
+
|
|
2018
|
+
7. Central Metaphors and Conceptual Framing: When an author uses a central metaphor to explain a concept or uses figurative phrasing to explain how things "work," this abstract reasoning drives the meaning, elevating the text to Very Complex.
|
|
2019
|
+
|
|
2020
|
+
8. Irony and Abstract Comparisons: Texts that rely on sustained irony, especially through comparative arguments, are inherently Very Complex for younger students.
|
|
2021
|
+
|
|
2022
|
+
9. Isolate Conventionality from Vocabulary: Do not inflate the Conventionality score just because the text uses archaic, dated, or highly academic vocabulary.
|
|
2023
|
+
|
|
2024
|
+
Input Format
|
|
2025
|
+
You will receive:
|
|
2026
|
+
- text: The passage to evaluate.
|
|
2027
|
+
- grade_level: The target student grade level.
|
|
2028
|
+
- fk_score: The Flesch-Kincaid readability score.
|
|
2029
|
+
|
|
2030
|
+
Output Format
|
|
2031
|
+
Provide a JSON object containing ONLY the following keys:
|
|
2032
|
+
- complexity_score: (String) One of the 4 scale levels exactly as formatted: 'slightly_complex', 'moderately_complex', 'very_complex', or 'exceedingly_complex'.
|
|
2033
|
+
- reasoning: (String) A detailed explanation of the rating, citing specific features in the text and referencing the expert guardrails (e.g., noting if the text relies on abstract qualities/rhetorical idealization, if vocabulary/background knowledge demands make a literal text vague for the grade level, or if it is strictly concrete/procedural).
|
|
2034
|
+
- conventionality_features: (List of Strings) The specific language features driving the complexity (e.g., literal narrative, concrete actions, less familiar expressions, sustained irony, abstract qualities, rhetorical idealization, archaic phrasing) with direct quotes from the text.
|
|
2035
|
+
- grade_context: (String) How the conventionality demands compare to general expectations for the provided target grade.
|
|
2036
|
+
- instructional_insights: (String) Actionable pedagogical suggestions for scaffolding the conventionality features in the classroom.`;
|
|
2037
|
+
|
|
2038
|
+
// ../../evals/prompts/conventionality/user.txt
|
|
2039
|
+
var user_default3 = "Analyze:\nText: {text}\nGrade: {grade}\nFK Score: {fk_score}";
|
|
2040
|
+
|
|
2041
|
+
// src/prompts/conventionality/index.ts
|
|
2042
|
+
function getSystemPrompt4() {
|
|
2043
|
+
return system_default3;
|
|
2044
|
+
}
|
|
2045
|
+
function getUserPrompt4(text, grade, fkScore) {
|
|
2046
|
+
return user_default3.replaceAll("{text}", text).replaceAll("{grade}", grade).replaceAll("{fk_score}", fkScore.toString());
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// src/evaluators/conventionality.ts
|
|
2050
|
+
var ConventionalityEvaluator = class _ConventionalityEvaluator extends BaseEvaluator {
|
|
2051
|
+
static metadata = {
|
|
2052
|
+
id: "conventionality",
|
|
2053
|
+
name: "Conventionality",
|
|
2054
|
+
description: "Evaluates how explicit, literal, and straightforward a text's meaning is relative to grade level",
|
|
2055
|
+
supportedGrades: ["3", "4", "5", "6", "7", "8", "9", "10", "11", "12"],
|
|
2056
|
+
requiresGoogleKey: true,
|
|
2057
|
+
requiresOpenAIKey: false
|
|
2058
|
+
};
|
|
2059
|
+
provider;
|
|
2060
|
+
constructor(config) {
|
|
2061
|
+
super(config);
|
|
2062
|
+
this.provider = createProvider({
|
|
2063
|
+
type: "google",
|
|
2064
|
+
model: "gemini-3-flash-preview",
|
|
2065
|
+
apiKey: config.googleApiKey,
|
|
2066
|
+
maxRetries: this.config.maxRetries
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Evaluate conventionality complexity for a given text and grade level
|
|
2071
|
+
*
|
|
2072
|
+
* @param text - The text to evaluate
|
|
2073
|
+
* @param grade - The target grade level (3-12)
|
|
2074
|
+
* @returns Evaluation result with complexity score and detailed analysis
|
|
2075
|
+
* @throws {ValidationError} If text is empty, too short/long, or grade is invalid
|
|
2076
|
+
* @throws {APIError} If LLM API calls fail (includes AuthenticationError, RateLimitError, NetworkError, TimeoutError)
|
|
2077
|
+
*/
|
|
2078
|
+
async evaluate(text, grade) {
|
|
2079
|
+
this.logger.info("Starting Conventionality evaluation", {
|
|
2080
|
+
evaluator: "conventionality",
|
|
2081
|
+
operation: "evaluate",
|
|
2082
|
+
grade,
|
|
2083
|
+
textLength: text.length
|
|
2084
|
+
});
|
|
2085
|
+
const startTime = Date.now();
|
|
2086
|
+
const stageDetails = [];
|
|
2087
|
+
try {
|
|
2088
|
+
this.validateText(text);
|
|
2089
|
+
this.validateGrade(grade, new Set(_ConventionalityEvaluator.metadata.supportedGrades));
|
|
2090
|
+
this.logger.debug("Evaluating conventionality complexity", {
|
|
2091
|
+
evaluator: "conventionality",
|
|
2092
|
+
operation: "conventionality_evaluation"
|
|
2093
|
+
});
|
|
2094
|
+
const fkScore = calculateFleschKincaidGrade(text);
|
|
2095
|
+
const response = await this.evaluateConventionality(text, grade, fkScore);
|
|
2096
|
+
stageDetails.push({
|
|
2097
|
+
stage: "conventionality_evaluation",
|
|
2098
|
+
provider: "google:gemini-3-flash-preview",
|
|
2099
|
+
latency_ms: response.latencyMs,
|
|
2100
|
+
token_usage: {
|
|
2101
|
+
input_tokens: response.usage.inputTokens,
|
|
2102
|
+
output_tokens: response.usage.outputTokens
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
const latencyMs = Date.now() - startTime;
|
|
2106
|
+
const totalTokenUsage = {
|
|
2107
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
2108
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
2109
|
+
};
|
|
2110
|
+
const result = {
|
|
2111
|
+
score: response.data.complexity_score,
|
|
2112
|
+
reasoning: response.data.reasoning,
|
|
2113
|
+
metadata: {
|
|
2114
|
+
model: "google:gemini-3-flash-preview",
|
|
2115
|
+
processingTimeMs: latencyMs
|
|
2116
|
+
},
|
|
2117
|
+
_internal: response.data
|
|
2118
|
+
};
|
|
2119
|
+
this.sendTelemetry({
|
|
2120
|
+
status: "success",
|
|
2121
|
+
latencyMs,
|
|
2122
|
+
textLength: text.length,
|
|
2123
|
+
grade,
|
|
2124
|
+
provider: "google:gemini-3-flash-preview",
|
|
2125
|
+
tokenUsage: totalTokenUsage,
|
|
2126
|
+
metadata: {
|
|
2127
|
+
stage_details: stageDetails
|
|
2128
|
+
},
|
|
2129
|
+
inputText: text
|
|
2130
|
+
}).catch(() => {
|
|
2131
|
+
});
|
|
2132
|
+
this.logger.info("Conventionality evaluation completed successfully", {
|
|
2133
|
+
evaluator: "conventionality",
|
|
2134
|
+
operation: "evaluate",
|
|
2135
|
+
grade,
|
|
2136
|
+
score: result.score,
|
|
2137
|
+
processingTimeMs: latencyMs
|
|
2138
|
+
});
|
|
2139
|
+
return result;
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
const latencyMs = Date.now() - startTime;
|
|
2142
|
+
this.logger.error("Conventionality evaluation failed", {
|
|
2143
|
+
evaluator: "conventionality",
|
|
2144
|
+
operation: "evaluate",
|
|
2145
|
+
grade,
|
|
2146
|
+
error: error instanceof Error ? error : void 0,
|
|
2147
|
+
processingTimeMs: latencyMs,
|
|
2148
|
+
completedStages: stageDetails.length
|
|
2149
|
+
});
|
|
2150
|
+
const totalTokenUsage = stageDetails.length > 0 ? {
|
|
2151
|
+
input_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.input_tokens || 0), 0),
|
|
2152
|
+
output_tokens: stageDetails.reduce((sum, s) => sum + (s.token_usage?.output_tokens || 0), 0)
|
|
2153
|
+
} : void 0;
|
|
2154
|
+
this.sendTelemetry({
|
|
2155
|
+
status: "error",
|
|
2156
|
+
latencyMs,
|
|
2157
|
+
textLength: text.length,
|
|
2158
|
+
grade,
|
|
2159
|
+
provider: "google:gemini-3-flash-preview",
|
|
2160
|
+
tokenUsage: totalTokenUsage,
|
|
2161
|
+
errorCode: error instanceof Error ? error.name : "UnknownError",
|
|
2162
|
+
metadata: stageDetails.length > 0 ? { stage_details: stageDetails } : void 0,
|
|
2163
|
+
inputText: text
|
|
2164
|
+
}).catch(() => {
|
|
2165
|
+
});
|
|
2166
|
+
if (error instanceof ValidationError) {
|
|
2167
|
+
throw error;
|
|
2168
|
+
}
|
|
2169
|
+
throw wrapProviderError(error, "Conventionality evaluation failed");
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Run the Conventionality evaluation LLM call
|
|
2174
|
+
*/
|
|
2175
|
+
async evaluateConventionality(text, grade, fkScore) {
|
|
2176
|
+
const response = await this.provider.generateStructured({
|
|
2177
|
+
messages: [
|
|
2178
|
+
{ role: "system", content: getSystemPrompt4() },
|
|
2179
|
+
{ role: "user", content: getUserPrompt4(text, grade, fkScore) }
|
|
2180
|
+
],
|
|
2181
|
+
schema: ConventionalityOutputSchema,
|
|
2182
|
+
temperature: 0
|
|
2183
|
+
});
|
|
2184
|
+
return {
|
|
2185
|
+
data: response.data,
|
|
2186
|
+
usage: response.usage,
|
|
2187
|
+
latencyMs: response.latencyMs
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
|
|
2192
|
+
// src/batch/evaluator.ts
|
|
2193
|
+
var EVALUATOR_MAP = /* @__PURE__ */ new Map([
|
|
2194
|
+
[GradeLevelAppropriatenessEvaluator.metadata.id, GradeLevelAppropriatenessEvaluator],
|
|
2195
|
+
[SmkEvaluator.metadata.id, SmkEvaluator],
|
|
2196
|
+
[VocabularyEvaluator.metadata.id, VocabularyEvaluator],
|
|
2197
|
+
[SentenceStructureEvaluator.metadata.id, SentenceStructureEvaluator],
|
|
2198
|
+
[ConventionalityEvaluator.metadata.id, ConventionalityEvaluator]
|
|
2199
|
+
]);
|
|
2200
|
+
var EVALUATOR_GROUPS = [
|
|
2201
|
+
{
|
|
2202
|
+
id: "text-complexity",
|
|
2203
|
+
name: "Text Complexity Analysis",
|
|
2204
|
+
description: "Evaluates vocabulary complexity, sentence structure, subject matter knowledge, conventionality, and grade-level appropriateness",
|
|
2205
|
+
evaluatorIds: [
|
|
2206
|
+
GradeLevelAppropriatenessEvaluator.metadata.id,
|
|
2207
|
+
SmkEvaluator.metadata.id,
|
|
2208
|
+
VocabularyEvaluator.metadata.id,
|
|
2209
|
+
SentenceStructureEvaluator.metadata.id,
|
|
2210
|
+
ConventionalityEvaluator.metadata.id
|
|
2211
|
+
],
|
|
2212
|
+
requiresGoogleKey: true,
|
|
2213
|
+
requiresOpenAIKey: true,
|
|
2214
|
+
maxInputRows: 50
|
|
2215
|
+
}
|
|
2216
|
+
];
|
|
2217
|
+
function getAvailableGroups() {
|
|
2218
|
+
return [...EVALUATOR_GROUPS];
|
|
2219
|
+
}
|
|
2220
|
+
var BatchEvaluator = class {
|
|
2221
|
+
config;
|
|
2222
|
+
limit;
|
|
2223
|
+
evaluatorInstances = /* @__PURE__ */ new Map();
|
|
2224
|
+
isCancelled = false;
|
|
2225
|
+
completedResults = [];
|
|
2226
|
+
constructor(config) {
|
|
2227
|
+
this.config = {
|
|
2228
|
+
concurrency: 3,
|
|
2229
|
+
maxRetries: 2,
|
|
2230
|
+
telemetry: false,
|
|
2231
|
+
...config
|
|
2232
|
+
};
|
|
2233
|
+
this.limit = pLimit(this.config.concurrency);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Cancel ongoing evaluation.
|
|
2237
|
+
* Returns partial results collected so far.
|
|
2238
|
+
*/
|
|
2239
|
+
cancel() {
|
|
2240
|
+
this.isCancelled = true;
|
|
2241
|
+
return [...this.completedResults];
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Initialize evaluator instances for the given IDs
|
|
2245
|
+
*/
|
|
2246
|
+
initializeEvaluators(evaluatorIds) {
|
|
2247
|
+
for (const id of evaluatorIds) {
|
|
2248
|
+
if (this.evaluatorInstances.has(id)) continue;
|
|
2249
|
+
const EvaluatorClass = EVALUATOR_MAP.get(id);
|
|
2250
|
+
if (!EvaluatorClass) {
|
|
2251
|
+
throw new Error(`Unknown evaluator: ${id}`);
|
|
2252
|
+
}
|
|
2253
|
+
const evaluator = new EvaluatorClass({
|
|
2254
|
+
googleApiKey: this.config.googleApiKey,
|
|
2255
|
+
openaiApiKey: this.config.openaiApiKey,
|
|
2256
|
+
maxRetries: this.config.maxRetries,
|
|
2257
|
+
telemetry: this.config.telemetry
|
|
2258
|
+
});
|
|
2259
|
+
this.evaluatorInstances.set(id, evaluator);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Create tasks from inputs and evaluator IDs
|
|
2264
|
+
*/
|
|
2265
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2266
|
+
createTasks(inputs, evaluatorIds) {
|
|
2267
|
+
const tasks = [];
|
|
2268
|
+
for (const input of inputs) {
|
|
2269
|
+
for (const evaluatorId of evaluatorIds) {
|
|
2270
|
+
tasks.push({
|
|
2271
|
+
text: input.text,
|
|
2272
|
+
grade: input.grade,
|
|
2273
|
+
evaluatorId,
|
|
2274
|
+
rowIndex: input.rowIndex,
|
|
2275
|
+
originalRow: input.originalRow
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return tasks;
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Execute a single evaluation task
|
|
2283
|
+
*/
|
|
2284
|
+
async executeTask(task, onProgress) {
|
|
2285
|
+
if (this.isCancelled) {
|
|
2286
|
+
const batchResult = {
|
|
2287
|
+
rowIndex: task.rowIndex,
|
|
2288
|
+
text: task.text,
|
|
2289
|
+
grade: task.grade,
|
|
2290
|
+
evaluatorId: task.evaluatorId,
|
|
2291
|
+
status: "error",
|
|
2292
|
+
error: "Cancelled by user",
|
|
2293
|
+
processingTimeMs: 0,
|
|
2294
|
+
originalRow: task.originalRow
|
|
2295
|
+
};
|
|
2296
|
+
return batchResult;
|
|
2297
|
+
}
|
|
2298
|
+
const startTime = Date.now();
|
|
2299
|
+
const evaluator = this.evaluatorInstances.get(task.evaluatorId);
|
|
2300
|
+
if (!evaluator) {
|
|
2301
|
+
const batchResult = {
|
|
2302
|
+
rowIndex: task.rowIndex,
|
|
2303
|
+
text: task.text,
|
|
2304
|
+
grade: task.grade,
|
|
2305
|
+
evaluatorId: task.evaluatorId,
|
|
2306
|
+
status: "error",
|
|
2307
|
+
error: `Evaluator not initialized: ${task.evaluatorId}`,
|
|
2308
|
+
processingTimeMs: 0,
|
|
2309
|
+
originalRow: task.originalRow
|
|
2310
|
+
};
|
|
2311
|
+
this.completedResults.push(batchResult);
|
|
2312
|
+
if (onProgress) onProgress(batchResult);
|
|
2313
|
+
return batchResult;
|
|
2314
|
+
}
|
|
2315
|
+
try {
|
|
2316
|
+
const result = await evaluator.evaluate(task.text, task.grade);
|
|
2317
|
+
const batchResult = {
|
|
2318
|
+
rowIndex: task.rowIndex,
|
|
2319
|
+
text: task.text,
|
|
2320
|
+
grade: task.grade,
|
|
2321
|
+
evaluatorId: task.evaluatorId,
|
|
2322
|
+
status: "success",
|
|
2323
|
+
score: result.score,
|
|
2324
|
+
reasoning: result.reasoning,
|
|
2325
|
+
processingTimeMs: Date.now() - startTime,
|
|
2326
|
+
originalRow: task.originalRow
|
|
2327
|
+
};
|
|
2328
|
+
this.completedResults.push(batchResult);
|
|
2329
|
+
if (onProgress) onProgress(batchResult);
|
|
2330
|
+
return batchResult;
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
const batchResult = {
|
|
2333
|
+
rowIndex: task.rowIndex,
|
|
2334
|
+
text: task.text,
|
|
2335
|
+
grade: task.grade,
|
|
2336
|
+
evaluatorId: task.evaluatorId,
|
|
2337
|
+
status: "error",
|
|
2338
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2339
|
+
processingTimeMs: Date.now() - startTime,
|
|
2340
|
+
originalRow: task.originalRow
|
|
2341
|
+
};
|
|
2342
|
+
this.completedResults.push(batchResult);
|
|
2343
|
+
if (onProgress) onProgress(batchResult);
|
|
2344
|
+
return batchResult;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Calculate summary statistics
|
|
2349
|
+
*/
|
|
2350
|
+
calculateSummary(results, durationMs) {
|
|
2351
|
+
const summary = {
|
|
2352
|
+
totalTasks: results.length,
|
|
2353
|
+
successful: results.filter((r) => r.status === "success").length,
|
|
2354
|
+
failed: results.filter((r) => r.status === "error").length,
|
|
2355
|
+
durationMs,
|
|
2356
|
+
resultsPerEvaluator: {}
|
|
2357
|
+
};
|
|
2358
|
+
const evaluatorIds = Array.from(new Set(results.map((r) => r.evaluatorId)));
|
|
2359
|
+
for (const id of evaluatorIds) {
|
|
2360
|
+
const evalResults = results.filter((r) => r.evaluatorId === id);
|
|
2361
|
+
summary.resultsPerEvaluator[id] = {
|
|
2362
|
+
successful: evalResults.filter((r) => r.status === "success").length,
|
|
2363
|
+
failed: evalResults.filter((r) => r.status === "error").length
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
return summary;
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Run batch evaluation for an evaluator group.
|
|
2370
|
+
*
|
|
2371
|
+
* @param inputs - Array of input rows
|
|
2372
|
+
* @param groupId - The evaluator group to run (see getAvailableGroups())
|
|
2373
|
+
* @param onProgress - Optional callback invoked after each task completes
|
|
2374
|
+
* @returns Batch evaluation results and summary
|
|
2375
|
+
*/
|
|
2376
|
+
async evaluate(inputs, groupId, onProgress) {
|
|
2377
|
+
const startTime = Date.now();
|
|
2378
|
+
const group = EVALUATOR_GROUPS.find((g) => g.id === groupId);
|
|
2379
|
+
if (!group) {
|
|
2380
|
+
throw new Error(
|
|
2381
|
+
`Unknown evaluator group: "${groupId}". Available: ${EVALUATOR_GROUPS.map((g) => g.id).join(", ")}`
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
if (inputs.length > group.maxInputRows) {
|
|
2385
|
+
throw new Error(
|
|
2386
|
+
`Input exceeds limit for "${group.id}": ${inputs.length} rows (max ${group.maxInputRows}). Split into smaller batches.`
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
this.isCancelled = false;
|
|
2390
|
+
this.completedResults = [];
|
|
2391
|
+
this.initializeEvaluators(group.evaluatorIds);
|
|
2392
|
+
const tasks = this.createTasks(inputs, group.evaluatorIds);
|
|
2393
|
+
const settledResults = await Promise.allSettled(
|
|
2394
|
+
tasks.map((task) => this.limit(() => this.executeTask(task, onProgress)))
|
|
2395
|
+
);
|
|
2396
|
+
const results = settledResults.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
2397
|
+
const durationMs = Date.now() - startTime;
|
|
2398
|
+
const summary = this.calculateSummary(results, durationMs);
|
|
2399
|
+
return { results, summary };
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
function findColumn(row, columnName) {
|
|
2403
|
+
const normalizedTarget = columnName.toLowerCase().trim();
|
|
2404
|
+
for (const key of Object.keys(row)) {
|
|
2405
|
+
if (key.toLowerCase().trim() === normalizedTarget) {
|
|
2406
|
+
return key;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return void 0;
|
|
2410
|
+
}
|
|
2411
|
+
function parseCSV(csvPath) {
|
|
2412
|
+
if (!fs2.existsSync(csvPath)) {
|
|
2413
|
+
throw new Error(`CSV file not found: ${csvPath}`);
|
|
2414
|
+
}
|
|
2415
|
+
const records = parse(fs2.readFileSync(csvPath, "utf-8"), {
|
|
2416
|
+
columns: true,
|
|
2417
|
+
skip_empty_lines: true,
|
|
2418
|
+
trim: true
|
|
2419
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2420
|
+
});
|
|
2421
|
+
if (records.length === 0) {
|
|
2422
|
+
throw new Error("CSV file is empty");
|
|
2423
|
+
}
|
|
2424
|
+
const firstRow = records[0];
|
|
2425
|
+
const textColumn = findColumn(firstRow, "text");
|
|
2426
|
+
const gradeColumn = findColumn(firstRow, "grade");
|
|
2427
|
+
if (!textColumn) {
|
|
2428
|
+
throw new Error('CSV must have a "text" column (case-insensitive)');
|
|
2429
|
+
}
|
|
2430
|
+
if (!gradeColumn) {
|
|
2431
|
+
throw new Error('CSV must have a "grade" column (case-insensitive)');
|
|
2432
|
+
}
|
|
2433
|
+
const inputs = [];
|
|
2434
|
+
for (let i = 0; i < records.length; i++) {
|
|
2435
|
+
const row = records[i];
|
|
2436
|
+
const text = row[textColumn];
|
|
2437
|
+
const grade = row[gradeColumn];
|
|
2438
|
+
if (!text || !grade) {
|
|
2439
|
+
console.warn(`Warning: skipping row ${i + 2} \u2014 missing text or grade`);
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
inputs.push({
|
|
2443
|
+
text: String(text).trim(),
|
|
2444
|
+
grade: String(grade).trim(),
|
|
2445
|
+
rowIndex: i + 2,
|
|
2446
|
+
// 1-based, offset by 1 for the header row
|
|
2447
|
+
originalRow: row
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
return inputs;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/batch/report-template.html
|
|
2454
|
+
var report_template_default = `<!DOCTYPE html>
|
|
2455
|
+
<html lang="en">
|
|
2456
|
+
<head>
|
|
2457
|
+
<meta charset="UTF-8">
|
|
2458
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2459
|
+
<title>Evaluation Report</title>
|
|
2460
|
+
<style>
|
|
2461
|
+
:root {
|
|
2462
|
+
--primary: #242423;
|
|
2463
|
+
--secondary: #6B6A64;
|
|
2464
|
+
--informational: #125B3A;
|
|
2465
|
+
--informational-bg: #E0F5EC;
|
|
2466
|
+
--border: #e2e8f0;
|
|
2467
|
+
--neutral-bg: #f1f5f9;
|
|
2468
|
+
--neutral-muted: #64748b;
|
|
2469
|
+
--on-band: #177A4D;
|
|
2470
|
+
--within-reach: #B79F15;
|
|
2471
|
+
--off-target: #C4352D;
|
|
2472
|
+
--card-bg: rgba(255,255,255,0.6);
|
|
2473
|
+
--card-shadow: 0 4px 10px 0 rgba(36,36,35,0.1);
|
|
2474
|
+
--radius: 8px;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2478
|
+
|
|
2479
|
+
body {
|
|
2480
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
2481
|
+
background: #F4F6F8;
|
|
2482
|
+
color: var(--primary);
|
|
2483
|
+
font-size: 14px;
|
|
2484
|
+
line-height: 1.5;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 32px 40px; }
|
|
2488
|
+
|
|
2489
|
+
/* \u2500\u2500 Header \u2500\u2500 */
|
|
2490
|
+
header.report-header { margin-bottom: 24px; }
|
|
2491
|
+
header.report-header h1 { font-size: 20px; font-weight: 600; margin: 0 0 4px 0; color: var(--primary); }
|
|
2492
|
+
header.report-header .subtitle { font-size: 14px; color: var(--secondary); }
|
|
2493
|
+
|
|
2494
|
+
/* \u2500\u2500 Cards \u2500\u2500 */
|
|
2495
|
+
.card { background: var(--card-bg); border-radius: var(--radius); box-shadow: var(--card-shadow); margin-bottom: 20px; overflow: hidden; }
|
|
2496
|
+
.card-body { padding: 16px 20px; }
|
|
2497
|
+
.card-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--secondary); margin-bottom: 16px; }
|
|
2498
|
+
.card-label-sm { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--secondary); }
|
|
2499
|
+
|
|
2500
|
+
/* \u2500\u2500 Tabs \u2500\u2500 */
|
|
2501
|
+
.tabs { display: flex; border-bottom: 1px solid var(--border); }
|
|
2502
|
+
.tab-btn { padding: 12px 20px; font-size: 14px; font-weight: 500; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; color: var(--secondary); transition: color 0.15s, border-color 0.15s; }
|
|
2503
|
+
.tab-btn:hover { color: var(--primary); }
|
|
2504
|
+
.tab-btn.active { color: var(--primary); border-bottom-color: var(--on-band); }
|
|
2505
|
+
.tab-panel { display: none; }
|
|
2506
|
+
.tab-panel.active { display: block; }
|
|
2507
|
+
.tab-content { padding: 20px 28px; }
|
|
2508
|
+
|
|
2509
|
+
/* \u2500\u2500 Layout grids \u2500\u2500 */
|
|
2510
|
+
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
|
|
2511
|
+
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
|
|
2512
|
+
.grid-4 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px 32px; }
|
|
2513
|
+
.flex-col { display: flex; flex-direction: column; gap: 4px; }
|
|
2514
|
+
|
|
2515
|
+
/* \u2500\u2500 Tags \u2500\u2500 */
|
|
2516
|
+
.tag { display: inline-block; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
|
2517
|
+
.tag-on-band { background: var(--on-band); color: #fff; }
|
|
2518
|
+
.tag-within-reach { background: var(--within-reach); color: #fff; }
|
|
2519
|
+
.tag-off-target { background: var(--off-target); color: #fff; }
|
|
2520
|
+
.tag-informational{ background: var(--neutral-bg); color: var(--neutral-muted); }
|
|
2521
|
+
|
|
2522
|
+
/* \u2500\u2500 GLA stat cards \u2500\u2500 */
|
|
2523
|
+
.gla-card .top-stripe { height: 4px; }
|
|
2524
|
+
.gla-card.on-band .top-stripe { background: var(--on-band); }
|
|
2525
|
+
.gla-card.within-reach .top-stripe { background: var(--within-reach); }
|
|
2526
|
+
.gla-card.off-target .top-stripe { background: var(--off-target); }
|
|
2527
|
+
.gla-card.on-band .card-label-sm { color: var(--on-band); }
|
|
2528
|
+
.gla-card.within-reach .card-label-sm { color: var(--within-reach); }
|
|
2529
|
+
.gla-card.off-target .card-label-sm { color: var(--off-target); }
|
|
2530
|
+
.gla-card .big-num { font-size: 28px; font-weight: 700; margin: 6px 0; }
|
|
2531
|
+
.gla-card.on-band .big-num { color: var(--on-band); }
|
|
2532
|
+
.gla-card.within-reach .big-num { color: var(--within-reach); }
|
|
2533
|
+
.gla-card.off-target .big-num { color: var(--off-target); }
|
|
2534
|
+
.gla-card .desc { font-size: 13px; color: var(--secondary); }
|
|
2535
|
+
|
|
2536
|
+
/* \u2500\u2500 Complexity dimension summary bars \u2500\u2500 */
|
|
2537
|
+
.cx-dim-track { height: 10px; background: var(--border); border-radius: 99px; overflow: hidden; }
|
|
2538
|
+
.cx-dim-fill { height: 100%; border-radius: 99px; background: var(--neutral-muted); transition: width 0.4s ease; }
|
|
2539
|
+
|
|
2540
|
+
/* \u2500\u2500 Insights \u2500\u2500 */
|
|
2541
|
+
.number-icon { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: var(--primary); color: #fff; font-size: 14px; font-weight: 600; flex-shrink: 0; }
|
|
2542
|
+
.insight-row { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
|
|
2543
|
+
.insight-text { flex: 1; font-size: 14px; color: var(--primary); }
|
|
2544
|
+
.disclaimer { font-size: 13px; color: var(--secondary); margin-top: 12px; }
|
|
2545
|
+
|
|
2546
|
+
/* \u2500\u2500 Grade level distribution (CSS bars) \u2500\u2500 */
|
|
2547
|
+
.dist-chart { margin-top: 8px; }
|
|
2548
|
+
.dist-row { display: flex; align-items: center; margin-bottom: 10px; }
|
|
2549
|
+
.dist-row .band { width: 60px; font-size: 12px; font-weight: 600; color: var(--primary); flex-shrink: 0; }
|
|
2550
|
+
.dist-row .bars { flex: 1; height: 24px; display: flex; }
|
|
2551
|
+
.dist-row .bar-on { background: var(--on-band); }
|
|
2552
|
+
.dist-row .bar-wr { background: var(--within-reach); }
|
|
2553
|
+
.dist-row .bar-off { background: var(--off-target); }
|
|
2554
|
+
.dist-row .bars > .bar-seg:first-child { border-radius: 4px 0 0 4px; }
|
|
2555
|
+
.dist-row .bars > .bar-seg:last-child { border-radius: 0 4px 4px 4px; }
|
|
2556
|
+
.dist-row .bars > .bar-seg.bar-off:last-child { border-radius: 0 4px 4px 0; }
|
|
2557
|
+
.dist-row .bars > .bar-seg.bar-off:only-child { border-radius: 4px; }
|
|
2558
|
+
.dist-row .bars > .bar-seg:only-child { border-radius: 4px; }
|
|
2559
|
+
.dist-legend { display: flex; justify-content: center; gap: 24px; margin-top: 16px; font-size: 12px; color: var(--primary); }
|
|
2560
|
+
.dist-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
2561
|
+
.dist-legend .dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
|
2562
|
+
|
|
2563
|
+
/* \u2500\u2500 Bar segment tooltips \u2500\u2500 */
|
|
2564
|
+
.bar-seg { position: relative; cursor: default; }
|
|
2565
|
+
.bar-seg::after {
|
|
2566
|
+
content: attr(data-tip);
|
|
2567
|
+
position: absolute;
|
|
2568
|
+
bottom: calc(100% + 6px);
|
|
2569
|
+
left: 50%;
|
|
2570
|
+
transform: translateX(-50%);
|
|
2571
|
+
background: var(--primary);
|
|
2572
|
+
color: #fff;
|
|
2573
|
+
padding: 5px 9px;
|
|
2574
|
+
border-radius: 5px;
|
|
2575
|
+
font-size: 12px;
|
|
2576
|
+
white-space: nowrap;
|
|
2577
|
+
pointer-events: none;
|
|
2578
|
+
opacity: 0;
|
|
2579
|
+
transition: opacity 0.12s;
|
|
2580
|
+
z-index: 20;
|
|
2581
|
+
}
|
|
2582
|
+
.bar-seg::before {
|
|
2583
|
+
content: '';
|
|
2584
|
+
position: absolute;
|
|
2585
|
+
bottom: calc(100% + 1px);
|
|
2586
|
+
left: 50%;
|
|
2587
|
+
transform: translateX(-50%);
|
|
2588
|
+
border: 5px solid transparent;
|
|
2589
|
+
border-top-color: var(--primary);
|
|
2590
|
+
opacity: 0;
|
|
2591
|
+
transition: opacity 0.12s;
|
|
2592
|
+
pointer-events: none;
|
|
2593
|
+
z-index: 20;
|
|
2594
|
+
}
|
|
2595
|
+
.bar-seg:hover::after,
|
|
2596
|
+
.bar-seg:hover::before { opacity: 1; }
|
|
2597
|
+
|
|
2598
|
+
/* \u2500\u2500 Heatmap \u2500\u2500 */
|
|
2599
|
+
.heatmap-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
2600
|
+
.heatmap-table th { padding: 10px 16px; text-align: left; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--neutral-muted); border-bottom: 2px solid var(--border); }
|
|
2601
|
+
.heatmap-table td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
|
2602
|
+
.heatmap-table td:first-child { font-weight: 600; color: var(--primary); }
|
|
2603
|
+
.heatmap-table .cell-num { text-align: center; }
|
|
2604
|
+
.heatmap-table tr:last-child td { border-bottom: none; }
|
|
2605
|
+
.heatmap-cell { display: inline-block; padding: 5px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; background: var(--neutral-bg); color: var(--neutral-muted); }
|
|
2606
|
+
|
|
2607
|
+
/* \u2500\u2500 Full results table \u2500\u2500 */
|
|
2608
|
+
.results-scroll { overflow-x: auto; overflow-y: auto; max-height: 600px; }
|
|
2609
|
+
table.data-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 100%; white-space: nowrap; }
|
|
2610
|
+
table.data-table th {
|
|
2611
|
+
padding: 10px 16px; text-align: left; font-size: 12px; font-weight: 600;
|
|
2612
|
+
color: var(--primary); background: var(--neutral-bg);
|
|
2613
|
+
border-bottom: 2px solid var(--border);
|
|
2614
|
+
position: sticky; top: 0; z-index: 3; white-space: nowrap;
|
|
2615
|
+
}
|
|
2616
|
+
table.data-table td {
|
|
2617
|
+
padding: 10px 16px; border-bottom: 1px solid var(--border);
|
|
2618
|
+
vertical-align: top; white-space: normal;
|
|
2619
|
+
color: var(--primary);
|
|
2620
|
+
}
|
|
2621
|
+
table.data-table tr:last-child td { border-bottom: none; }
|
|
2622
|
+
table.data-table tbody tr:hover td { background: rgba(0,0,0,0.02); }
|
|
2623
|
+
table.data-table tbody tr:hover td.frozen { background: var(--neutral-bg); }
|
|
2624
|
+
/* Frozen (sticky-left) columns */
|
|
2625
|
+
table.data-table th.frozen,
|
|
2626
|
+
table.data-table td.frozen { position: sticky; background: var(--neutral-bg); z-index: 2; }
|
|
2627
|
+
table.data-table td.frozen { background: #fff; }
|
|
2628
|
+
table.data-table th.frozen { z-index: 4; }
|
|
2629
|
+
table.data-table th.frozen-last,
|
|
2630
|
+
table.data-table td.frozen-last { border-right: 2px solid var(--border); }
|
|
2631
|
+
table.data-table th.group-start,
|
|
2632
|
+
table.data-table td.group-start { border-left: 2px solid var(--border); }
|
|
2633
|
+
/* Text cell \u2014 3-line clamp, full text on hover */
|
|
2634
|
+
table.data-table .cell-text { cursor: help; }
|
|
2635
|
+
table.data-table .cell-text-inner {
|
|
2636
|
+
display: -webkit-box;
|
|
2637
|
+
-webkit-line-clamp: 3;
|
|
2638
|
+
-webkit-box-orient: vertical;
|
|
2639
|
+
overflow: hidden;
|
|
2640
|
+
white-space: normal;
|
|
2641
|
+
}
|
|
2642
|
+
/* Reasoning cells \u2014 2-line clamp, full text on hover */
|
|
2643
|
+
table.data-table .cell-reasoning { font-size: 12px; color: var(--secondary); cursor: help; }
|
|
2644
|
+
table.data-table .cell-reasoning-inner {
|
|
2645
|
+
display: -webkit-box;
|
|
2646
|
+
-webkit-line-clamp: 2;
|
|
2647
|
+
-webkit-box-orient: vertical;
|
|
2648
|
+
overflow: hidden;
|
|
2649
|
+
white-space: normal;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
/* \u2500\u2500 Empty / no-data \u2500\u2500 */
|
|
2653
|
+
.no-data { padding: 40px; text-align: center; color: var(--secondary); font-size: 15px; }
|
|
2654
|
+
</style>
|
|
2655
|
+
</head>
|
|
2656
|
+
<body>
|
|
2657
|
+
|
|
2658
|
+
<div class="container">
|
|
2659
|
+
<header class="report-header" id="app-header"></header>
|
|
2660
|
+
<div class="card" style="margin-bottom:0; overflow:visible;">
|
|
2661
|
+
<div class="tabs" id="tab-bar"></div>
|
|
2662
|
+
<div id="tab-summary" class="tab-panel active"></div>
|
|
2663
|
+
<div id="tab-results" class="tab-panel"></div>
|
|
2664
|
+
</div>
|
|
2665
|
+
</div>
|
|
2666
|
+
|
|
2667
|
+
<script>
|
|
2668
|
+
// ---------------------------------------------------------------------------
|
|
2669
|
+
// MOCK DATA \u2014 used when the template is opened directly in a browser so
|
|
2670
|
+
// designers can see a realistic preview without running the CLI.
|
|
2671
|
+
// This constant is never referenced when real data has been injected.
|
|
2672
|
+
// ---------------------------------------------------------------------------
|
|
2673
|
+
const MOCK_REPORT_DATA = {
|
|
2674
|
+
meta: {
|
|
2675
|
+
reportId: 'sample_content_batch_20260301T1430',
|
|
2676
|
+
generatedAt: 'Mar 1, 2026 2:30 PM',
|
|
2677
|
+
csvPath: '/Users/designer/Documents/interventionhelper_content_batch_03-01.csv',
|
|
2678
|
+
evaluatorIds: ['grade-level-appropriateness', 'subject-matter-knowledge', 'vocabulary', 'sentence-structure', 'conventionality'],
|
|
2679
|
+
evaluatorNames: ['Grade Level Appropriateness', 'Subject Matter Knowledge', 'Vocabulary', 'Sentence Structure', 'Conventionality'],
|
|
2680
|
+
totalRows: 300,
|
|
2681
|
+
processedRows: 287,
|
|
2682
|
+
erroredRows: 13,
|
|
2683
|
+
},
|
|
2684
|
+
gradeLevelStats: {
|
|
2685
|
+
onBand: 172, adjacent: 85, offTarget: 30,
|
|
2686
|
+
onBandPct: 60, adjacentPct: 30, offTargetPct: 10,
|
|
2687
|
+
hasData: true,
|
|
2688
|
+
},
|
|
2689
|
+
complexityStats: [
|
|
2690
|
+
{
|
|
2691
|
+
evaluatorId: 'subject-matter-knowledge', name: 'Subject Matter Knowledge',
|
|
2692
|
+
average: 2.6, label: 'Moderately complex',
|
|
2693
|
+
distribution: [28, 98, 110, 51],
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
evaluatorId: 'vocabulary', name: 'Vocabulary',
|
|
2697
|
+
average: 2.4, label: 'Moderately complex',
|
|
2698
|
+
distribution: [45, 120, 95, 27],
|
|
2699
|
+
},
|
|
2700
|
+
{
|
|
2701
|
+
evaluatorId: 'sentence-structure', name: 'Sentence Structure',
|
|
2702
|
+
average: 1.9, label: 'Slightly complex',
|
|
2703
|
+
distribution: [88, 105, 72, 22],
|
|
2704
|
+
},
|
|
2705
|
+
{
|
|
2706
|
+
evaluatorId: 'conventionality', name: 'Conventionality',
|
|
2707
|
+
average: 1.7, label: 'Slightly complex',
|
|
2708
|
+
distribution: [112, 118, 45, 12],
|
|
2709
|
+
},
|
|
2710
|
+
],
|
|
2711
|
+
gradeBandDistribution: {
|
|
2712
|
+
bands: ['K-1', '2-3', '4-5', '6-8', '9-10', '11-CCR'],
|
|
2713
|
+
data: [
|
|
2714
|
+
{ onBand: 0, adjacent: 0, offTarget: 0, total: 0 },
|
|
2715
|
+
{ onBand: 32, adjacent: 18, offTarget: 5, total: 55 },
|
|
2716
|
+
{ onBand: 58, adjacent: 22, offTarget: 8, total: 88 },
|
|
2717
|
+
{ onBand: 48, adjacent: 25, offTarget: 10, total: 83 },
|
|
2718
|
+
{ onBand: 22, adjacent: 14, offTarget: 5, total: 41 },
|
|
2719
|
+
{ onBand: 12, adjacent: 6, offTarget: 2, total: 20 },
|
|
2720
|
+
],
|
|
2721
|
+
},
|
|
2722
|
+
complexityHeatmap: {
|
|
2723
|
+
bands: ['K-1', '2-3', '4-5', '6-8', '9-10', '11-CCR'],
|
|
2724
|
+
evaluators: ['Subject Matter Knowledge', 'Vocabulary', 'Sentence Structure', 'Conventionality'],
|
|
2725
|
+
evaluatorIds: ['subject-matter-knowledge', 'vocabulary', 'sentence-structure', 'conventionality'],
|
|
2726
|
+
values: [
|
|
2727
|
+
[null, null, null, null],
|
|
2728
|
+
[1.8, 1.6, 1.4, 1.3 ],
|
|
2729
|
+
[2.3, 2.1, 1.8, 1.7 ],
|
|
2730
|
+
[2.7, 2.5, 2.2, 2.0 ],
|
|
2731
|
+
[3.1, 2.9, 2.6, 2.4 ],
|
|
2732
|
+
[3.4, 3.2, 2.8, 2.6 ],
|
|
2733
|
+
],
|
|
2734
|
+
},
|
|
2735
|
+
insights: [
|
|
2736
|
+
'Review texts marked as Off Target \u2014 they may need content revision or grade-level adjustment before distribution.',
|
|
2737
|
+
'Texts evaluated as Adjacent may benefit from light scaffolding strategies such as vocabulary pre-teaching.',
|
|
2738
|
+
'Higher grade bands tend to show greater text complexity. Consider whether complexity aligns with instructional goals.',
|
|
2739
|
+
],
|
|
2740
|
+
fullResults: {
|
|
2741
|
+
originalColumns: ['row_id', 'text', 'grade', 'source'],
|
|
2742
|
+
hasGLA: true,
|
|
2743
|
+
complexityEvaluators: [
|
|
2744
|
+
{ evaluatorId: 'subject-matter-knowledge', name: 'Subject Matter Knowledge', prefix: 'subject_matter_knowledge' },
|
|
2745
|
+
{ evaluatorId: 'vocabulary', name: 'Vocabulary', prefix: 'vocabulary' },
|
|
2746
|
+
{ evaluatorId: 'sentence-structure', name: 'Sentence Structure', prefix: 'sentence_structure' },
|
|
2747
|
+
{ evaluatorId: 'conventionality', name: 'Conventionality', prefix: 'conventionality' },
|
|
2748
|
+
],
|
|
2749
|
+
rows: [
|
|
2750
|
+
{
|
|
2751
|
+
row_id: '1', grade: '5', source: 'science_unit_3',
|
|
2752
|
+
text: 'The water cycle describes how water evaporates from surfaces, rises into the atmosphere, cools and condenses into clouds, and falls back to the ground as precipitation.',
|
|
2753
|
+
__gla_status: 'On Band', __gla_band: '4-5',
|
|
2754
|
+
__gla_reasoning: 'Uses grade-appropriate science vocabulary with a clear explanatory structure suitable for grades 4\u20135.',
|
|
2755
|
+
__subject_matter_knowledge_score: 'Moderately complex',
|
|
2756
|
+
__subject_matter_knowledge_reasoning: 'Requires familiarity with basic Earth science concepts; grade 5 students may need prior exposure to the water cycle.',
|
|
2757
|
+
__vocabulary_score: 'Moderately complex',
|
|
2758
|
+
__vocabulary_reasoning: 'Contains domain-specific terms (evaporates, condenses, precipitation) that require pre-teaching for grade 5 students.',
|
|
2759
|
+
__sentence_structure_score: 'Slightly complex',
|
|
2760
|
+
__sentence_structure_reasoning: 'Primarily compound sentences with clear connective structure appropriate for grade 5.',
|
|
2761
|
+
__conventionality_score: 'Slightly complex',
|
|
2762
|
+
__conventionality_reasoning: 'Language is largely literal and explicit; no figurative or idiomatic usage that would increase comprehension demand.',
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
row_id: '2', grade: '6', source: 'science_unit_1',
|
|
2766
|
+
text: 'Photosynthesis is the process by which green plants use sunlight, water and carbon dioxide to produce food and oxygen.',
|
|
2767
|
+
__gla_status: 'Adjacent', __gla_band: '4-5',
|
|
2768
|
+
__gla_reasoning: 'Content is accessible but slightly below typical grade 6 complexity expectations.',
|
|
2769
|
+
__subject_matter_knowledge_score: 'Slightly complex',
|
|
2770
|
+
__subject_matter_knowledge_reasoning: 'Core concept of photosynthesis is introduced in upper elementary science; low prior knowledge demand for grade 6.',
|
|
2771
|
+
__vocabulary_score: 'Moderately complex',
|
|
2772
|
+
__vocabulary_reasoning: 'Key scientific terms are present but relatively straightforward for grade 6 readers.',
|
|
2773
|
+
__sentence_structure_score: 'Slightly complex',
|
|
2774
|
+
__sentence_structure_reasoning: 'Single main clause with a relative clause; well within grade 6 reading ability.',
|
|
2775
|
+
__conventionality_score: 'Slightly complex',
|
|
2776
|
+
__conventionality_reasoning: 'Entirely literal and direct; meaning is fully transparent with no figurative language.',
|
|
2777
|
+
},
|
|
2778
|
+
{
|
|
2779
|
+
row_id: '3', grade: '8', source: 'biology_unit_2',
|
|
2780
|
+
text: 'The mitochondria, often described as the powerhouse of the cell, are organelles found in the cytoplasm of eukaryotic cells, where they generate most of the adenosine triphosphate used for cellular energy.',
|
|
2781
|
+
__gla_status: 'Off Target', __gla_band: '11-CCR',
|
|
2782
|
+
__gla_reasoning: 'Text uses advanced biochemical terminology (adenosine triphosphate, eukaryotic) more appropriate for upper secondary or college-level readers.',
|
|
2783
|
+
__subject_matter_knowledge_score: 'Very complex',
|
|
2784
|
+
__subject_matter_knowledge_reasoning: 'Assumes familiarity with cell biology, organelle function, and biochemical energy systems well beyond typical grade 8 expectations.',
|
|
2785
|
+
__vocabulary_score: 'Exceedingly complex',
|
|
2786
|
+
__vocabulary_reasoning: 'High density of Tier 3 domain-specific words significantly exceeds typical grade 8 vocabulary expectations.',
|
|
2787
|
+
__sentence_structure_score: 'Very complex',
|
|
2788
|
+
__sentence_structure_reasoning: 'Long, embedded clauses with multiple modifying phrases create significant syntactic complexity for grade 8.',
|
|
2789
|
+
__conventionality_score: 'Moderately complex',
|
|
2790
|
+
__conventionality_reasoning: '"Powerhouse of the cell" is a well-known metaphor but requires understanding of the underlying analogy; otherwise literal.',
|
|
2791
|
+
},
|
|
2792
|
+
{
|
|
2793
|
+
row_id: '4', grade: '3', source: 'science_unit_3',
|
|
2794
|
+
text: 'Rain falls from clouds when tiny water droplets join together and become heavy enough to fall to the ground.',
|
|
2795
|
+
__gla_status: 'On Band', __gla_band: '2-3',
|
|
2796
|
+
__gla_reasoning: 'Simple vocabulary and sentence structure are appropriate for grades 2\u20133.',
|
|
2797
|
+
__subject_matter_knowledge_score: 'Slightly complex',
|
|
2798
|
+
__subject_matter_knowledge_reasoning: 'Everyday weather phenomenon; no specialized prior knowledge required for grade 3 students.',
|
|
2799
|
+
__vocabulary_score: 'Slightly complex',
|
|
2800
|
+
__vocabulary_reasoning: 'Common everyday vocabulary with no domain-specific terms requiring pre-teaching.',
|
|
2801
|
+
__sentence_structure_score: 'Slightly complex',
|
|
2802
|
+
__sentence_structure_reasoning: 'Short simple sentences with basic connective structure.',
|
|
2803
|
+
__conventionality_score: 'Slightly complex',
|
|
2804
|
+
__conventionality_reasoning: 'Entirely conventional and literal; straightforward causal explanation with no figurative language.',
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
row_id: '5', grade: '9', source: 'biology_unit_1',
|
|
2808
|
+
text: 'Ecosystems are communities of organisms that interact with each other and their physical environment, shaped by both biotic and abiotic factors that influence population dynamics over time.',
|
|
2809
|
+
__gla_status: 'On Band', __gla_band: '9-10',
|
|
2810
|
+
__gla_reasoning: 'Appropriate complexity and terminology for a grade 9\u201310 biology curriculum.',
|
|
2811
|
+
__subject_matter_knowledge_score: 'Very complex',
|
|
2812
|
+
__subject_matter_knowledge_reasoning: 'Requires understanding of ecological concepts (biotic/abiotic factors, population dynamics) typical of secondary-level biology coursework.',
|
|
2813
|
+
__vocabulary_score: 'Very complex',
|
|
2814
|
+
__vocabulary_reasoning: 'Multiple Tier 3 terms (biotic, abiotic, population dynamics) require strong background knowledge.',
|
|
2815
|
+
__sentence_structure_score: 'Moderately complex',
|
|
2816
|
+
__sentence_structure_reasoning: 'Compound-complex sentence with a relative clause; manageable for grade 9 readers.',
|
|
2817
|
+
__conventionality_score: 'Slightly complex',
|
|
2818
|
+
__conventionality_reasoning: 'Technical but literal throughout; no irony or figurative usage that would obscure meaning.',
|
|
2819
|
+
},
|
|
2820
|
+
{
|
|
2821
|
+
row_id: '6', grade: '4', source: 'social_studies_unit_2',
|
|
2822
|
+
text: 'Ancient Egyptians built pyramids as tombs for their pharaohs and used a picture-based writing system called hieroglyphics.',
|
|
2823
|
+
__gla_status: 'On Band', __gla_band: '4-5',
|
|
2824
|
+
__gla_reasoning: 'Vocabulary and sentence length are well-matched to grade 4\u20135 social studies content.',
|
|
2825
|
+
__subject_matter_knowledge_score: 'Moderately complex',
|
|
2826
|
+
__subject_matter_knowledge_reasoning: 'Ancient Egypt is a common grade 4 social studies topic; some prior exposure to civilizations is expected.',
|
|
2827
|
+
__vocabulary_score: 'Moderately complex',
|
|
2828
|
+
__vocabulary_reasoning: 'Domain-specific proper nouns (pharaohs, hieroglyphics) may need brief glossing.',
|
|
2829
|
+
__sentence_structure_score: 'Slightly complex',
|
|
2830
|
+
__sentence_structure_reasoning: 'Two coordinated independent clauses; clear and accessible structure.',
|
|
2831
|
+
__conventionality_score: 'Slightly complex',
|
|
2832
|
+
__conventionality_reasoning: 'Descriptive and informational; meaning is explicit and no non-literal language is used.',
|
|
2833
|
+
},
|
|
2834
|
+
{
|
|
2835
|
+
row_id: '7', grade: '11', source: 'lit_unit_4',
|
|
2836
|
+
text: 'Shakespeare\\'s use of dramatic irony in Othello functions as a mechanism of tragic inevitability, positioning the audience as unwilling witnesses to the protagonist\\'s epistemological collapse.',
|
|
2837
|
+
__gla_status: 'On Band', __gla_band: '11-CCR',
|
|
2838
|
+
__gla_reasoning: 'Sophisticated literary analysis vocabulary and complex syntax are well-suited to grades 11\u2013CCR.',
|
|
2839
|
+
__subject_matter_knowledge_score: 'Exceedingly complex',
|
|
2840
|
+
__subject_matter_knowledge_reasoning: 'Requires familiarity with Shakespearean drama, literary theory, and epistemological concepts; assumes high prior exposure to canonical literature.',
|
|
2841
|
+
__vocabulary_score: 'Exceedingly complex',
|
|
2842
|
+
__vocabulary_reasoning: 'Tier 3 literary and philosophical vocabulary (epistemological, dramatic irony, tragic inevitability) demands high reading proficiency.',
|
|
2843
|
+
__sentence_structure_score: 'Exceedingly complex',
|
|
2844
|
+
__sentence_structure_reasoning: 'Noun phrase and participial phrase stacking creates a dense, highly embedded syntactic structure.',
|
|
2845
|
+
__conventionality_score: 'Very complex',
|
|
2846
|
+
__conventionality_reasoning: 'Analytical prose employs abstract and figurative constructs; "epistemological collapse" and "mechanism of tragic inevitability" are non-literal formulations requiring interpretive inference.',
|
|
2847
|
+
},
|
|
2848
|
+
{
|
|
2849
|
+
row_id: '8', grade: '7', source: 'history_unit_1',
|
|
2850
|
+
text: 'The Industrial Revolution transformed European societies by shifting labor from farms to factories, driving rapid urban growth and fundamentally changing how goods were produced and traded.',
|
|
2851
|
+
__gla_status: 'Adjacent', __gla_band: '9-10',
|
|
2852
|
+
__gla_reasoning: 'Vocabulary and conceptual density exceed typical grade 7 expectations; better suited for grades 9\u201310.',
|
|
2853
|
+
__subject_matter_knowledge_score: 'Moderately complex',
|
|
2854
|
+
__subject_matter_knowledge_reasoning: 'Industrial Revolution is introduced in middle school; grade 7 students likely have foundational context, though economic concepts add demand.',
|
|
2855
|
+
__vocabulary_score: 'Very complex',
|
|
2856
|
+
__vocabulary_reasoning: 'Abstract economic and historical vocabulary (urban growth, fundamentally) adds significant reading demand.',
|
|
2857
|
+
__sentence_structure_score: 'Moderately complex',
|
|
2858
|
+
__sentence_structure_reasoning: 'Participial phrases and coordinated verb phrases add structural complexity but remain readable.',
|
|
2859
|
+
__conventionality_score: 'Slightly complex',
|
|
2860
|
+
__conventionality_reasoning: 'Primarily literal and informational; "transformed" is used in its conventional sense with no figurative layering.',
|
|
2861
|
+
},
|
|
2862
|
+
],
|
|
2863
|
+
},
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
// ---------------------------------------------------------------------------
|
|
2867
|
+
// DATA \u2014 this line is replaced by the formatter at report generation time.
|
|
2868
|
+
// When opening the template directly in a browser, MOCK_REPORT_DATA is used.
|
|
2869
|
+
// ---------------------------------------------------------------------------
|
|
2870
|
+
var REPORT_DATA = null; // __REPLACED_BY_FORMATTER__
|
|
2871
|
+
REPORT_DATA = REPORT_DATA || MOCK_REPORT_DATA;
|
|
2872
|
+
|
|
2873
|
+
// ---------------------------------------------------------------------------
|
|
2874
|
+
// Canonical evaluator order
|
|
2875
|
+
// ---------------------------------------------------------------------------
|
|
2876
|
+
|
|
2877
|
+
const EVALUATOR_ORDER = [
|
|
2878
|
+
'grade-level-appropriateness',
|
|
2879
|
+
'subject-matter-knowledge',
|
|
2880
|
+
'vocabulary',
|
|
2881
|
+
'sentence-structure',
|
|
2882
|
+
'conventionality',
|
|
2883
|
+
];
|
|
2884
|
+
|
|
2885
|
+
function evalSortIndex(id) {
|
|
2886
|
+
const i = EVALUATOR_ORDER.indexOf(id);
|
|
2887
|
+
return i === -1 ? 999 : i;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// Sort meta.evaluatorIds / evaluatorNames in tandem
|
|
2891
|
+
const _metaPairs = REPORT_DATA.meta.evaluatorIds
|
|
2892
|
+
.map((id, i) => ({ id, name: REPORT_DATA.meta.evaluatorNames[i] }))
|
|
2893
|
+
.sort((a, b) => evalSortIndex(a.id) - evalSortIndex(b.id));
|
|
2894
|
+
REPORT_DATA.meta.evaluatorIds = _metaPairs.map(x => x.id);
|
|
2895
|
+
REPORT_DATA.meta.evaluatorNames = _metaPairs.map(x => x.name);
|
|
2896
|
+
|
|
2897
|
+
// Sort complexityStats
|
|
2898
|
+
REPORT_DATA.complexityStats = [...REPORT_DATA.complexityStats]
|
|
2899
|
+
.sort((a, b) => evalSortIndex(a.evaluatorId) - evalSortIndex(b.evaluatorId));
|
|
2900
|
+
|
|
2901
|
+
// Sort complexityHeatmap evaluators and their value columns
|
|
2902
|
+
const _hmPairs = REPORT_DATA.complexityHeatmap.evaluators
|
|
2903
|
+
.map((name, i) => ({ name, id: REPORT_DATA.complexityHeatmap.evaluatorIds[i], i }))
|
|
2904
|
+
.sort((a, b) => evalSortIndex(a.id) - evalSortIndex(b.id));
|
|
2905
|
+
REPORT_DATA.complexityHeatmap.evaluators = _hmPairs.map(x => x.name);
|
|
2906
|
+
REPORT_DATA.complexityHeatmap.evaluatorIds = _hmPairs.map(x => x.id);
|
|
2907
|
+
REPORT_DATA.complexityHeatmap.values = REPORT_DATA.complexityHeatmap.values
|
|
2908
|
+
.map(row => _hmPairs.map(x => row[x.i]));
|
|
2909
|
+
|
|
2910
|
+
// Sort fullResults.complexityEvaluators
|
|
2911
|
+
REPORT_DATA.fullResults.complexityEvaluators = [...REPORT_DATA.fullResults.complexityEvaluators]
|
|
2912
|
+
.sort((a, b) => evalSortIndex(a.evaluatorId) - evalSortIndex(b.evaluatorId));
|
|
2913
|
+
|
|
2914
|
+
// ---------------------------------------------------------------------------
|
|
2915
|
+
// Utilities
|
|
2916
|
+
// ---------------------------------------------------------------------------
|
|
2917
|
+
|
|
2918
|
+
function esc(str) {
|
|
2919
|
+
return String(str ?? '')
|
|
2920
|
+
.replace(/&/g, '&')
|
|
2921
|
+
.replace(/</g, '<')
|
|
2922
|
+
.replace(/>/g, '>')
|
|
2923
|
+
.replace(/"/g, '"');
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function statusBadge(status) {
|
|
2927
|
+
const cls = {
|
|
2928
|
+
'On Band': 'tag-on-band',
|
|
2929
|
+
'Adjacent': 'tag-within-reach',
|
|
2930
|
+
'Off Target':'tag-off-target',
|
|
2931
|
+
}[status] || '';
|
|
2932
|
+
const display = status === 'Adjacent' ? 'Within Reach' : status;
|
|
2933
|
+
return \`<span class="tag \${cls}">\${esc(display)}</span>\`;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// ---------------------------------------------------------------------------
|
|
2937
|
+
// Tab switching
|
|
2938
|
+
// ---------------------------------------------------------------------------
|
|
2939
|
+
|
|
2940
|
+
function switchTab(tab) {
|
|
2941
|
+
document.querySelectorAll('.tab-btn').forEach(b =>
|
|
2942
|
+
b.classList.toggle('active', b.dataset.tab === tab)
|
|
2943
|
+
);
|
|
2944
|
+
document.getElementById('tab-summary').classList.toggle('active', tab === 'summary');
|
|
2945
|
+
document.getElementById('tab-results').classList.toggle('active', tab === 'results');
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// ---------------------------------------------------------------------------
|
|
2949
|
+
// Header
|
|
2950
|
+
// ---------------------------------------------------------------------------
|
|
2951
|
+
|
|
2952
|
+
function renderHeader() {
|
|
2953
|
+
const { meta } = REPORT_DATA;
|
|
2954
|
+
document.getElementById('app-header').innerHTML = \`
|
|
2955
|
+
<h1>Evaluation Report</h1>
|
|
2956
|
+
<p class="subtitle">Generated: \${esc(meta.generatedAt)} • Report ID: \${esc(meta.reportId)}</p>
|
|
2957
|
+
\`;
|
|
2958
|
+
document.title = \`Report: \${meta.reportId}\`;
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// ---------------------------------------------------------------------------
|
|
2962
|
+
// Tab bar
|
|
2963
|
+
// ---------------------------------------------------------------------------
|
|
2964
|
+
|
|
2965
|
+
function renderTabs() {
|
|
2966
|
+
document.getElementById('tab-bar').innerHTML = \`
|
|
2967
|
+
<button class="tab-btn active" data-tab="summary" onclick="switchTab('summary')">Summary</button>
|
|
2968
|
+
<button class="tab-btn" data-tab="results" onclick="switchTab('results')">Full Results</button>
|
|
2969
|
+
\`;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// ---------------------------------------------------------------------------
|
|
2973
|
+
// Summary tab
|
|
2974
|
+
// ---------------------------------------------------------------------------
|
|
2975
|
+
|
|
2976
|
+
function renderSummary() {
|
|
2977
|
+
const { meta, gradeLevelStats: gls, complexityStats, insights,
|
|
2978
|
+
gradeBandDistribution, complexityHeatmap } = REPORT_DATA;
|
|
2979
|
+
|
|
2980
|
+
// \u2500\u2500 Snapshot \u2500\u2500
|
|
2981
|
+
const snapshotHtml = \`
|
|
2982
|
+
<div class="card">
|
|
2983
|
+
<div class="card-body">
|
|
2984
|
+
<div class="card-label">Snapshot</div>
|
|
2985
|
+
<div style="display:flex; flex-direction:column; gap:14px;">
|
|
2986
|
+
<div class="flex-col">
|
|
2987
|
+
<span class="card-label-sm">Evaluators</span>
|
|
2988
|
+
<div style="margin-top:6px; display:flex; flex-wrap:wrap; gap:6px;">
|
|
2989
|
+
\${meta.evaluatorNames.map(n => \`<span class="tag tag-informational">\${esc(n)}</span>\`).join('')}
|
|
2990
|
+
</div>
|
|
2991
|
+
</div>
|
|
2992
|
+
<div style="display:grid; grid-template-columns:1fr 1fr 3fr; gap:12px 32px;">
|
|
2993
|
+
<div class="flex-col">
|
|
2994
|
+
<span class="card-label-sm">Rows Processed</span>
|
|
2995
|
+
<span style="font-weight:500; margin-top:4px;">\${meta.processedRows} of \${meta.totalRows}</span>
|
|
2996
|
+
</div>
|
|
2997
|
+
<div class="flex-col">
|
|
2998
|
+
<span class="card-label-sm">Errors / Skipped</span>
|
|
2999
|
+
<span style="font-weight:500; margin-top:4px;">\${meta.erroredRows}</span>
|
|
3000
|
+
</div>
|
|
3001
|
+
<div class="flex-col">
|
|
3002
|
+
<span class="card-label-sm">Source File</span>
|
|
3003
|
+
<span style="font-size:12px; font-family:monospace; word-break:break-all; color:var(--secondary); margin-top:4px;" title="\${esc(meta.csvPath)}">\${esc(meta.csvPath)}</span>
|
|
3004
|
+
</div>
|
|
3005
|
+
</div>
|
|
3006
|
+
</div>
|
|
3007
|
+
</div>
|
|
3008
|
+
</div>
|
|
3009
|
+
\`;
|
|
3010
|
+
|
|
3011
|
+
// \u2500\u2500 GLA stat cards \u2500\u2500
|
|
3012
|
+
const glsHtml = gls.hasData ? \`
|
|
3013
|
+
<div class="grid-3">
|
|
3014
|
+
<div class="card gla-card on-band" style="margin-bottom:0;">
|
|
3015
|
+
<div class="top-stripe"></div>
|
|
3016
|
+
<div class="card-body">
|
|
3017
|
+
<div class="card-label-sm">On Band</div>
|
|
3018
|
+
<div class="big-num">\${gls.onBandPct}%</div>
|
|
3019
|
+
<div class="desc">\${gls.onBand} of \${meta.processedRows} rows where the evaluated grade band matches intended</div>
|
|
3020
|
+
</div>
|
|
3021
|
+
</div>
|
|
3022
|
+
<div class="card gla-card within-reach" style="margin-bottom:0;">
|
|
3023
|
+
<div class="top-stripe"></div>
|
|
3024
|
+
<div class="card-body">
|
|
3025
|
+
<div class="card-label-sm">Within Reach</div>
|
|
3026
|
+
<div class="big-num">\${gls.adjacentPct}%</div>
|
|
3027
|
+
<div class="desc">\${gls.adjacent} rows where the alternative grade band aligns with intended</div>
|
|
3028
|
+
</div>
|
|
3029
|
+
</div>
|
|
3030
|
+
<div class="card gla-card off-target" style="margin-bottom:0;">
|
|
3031
|
+
<div class="top-stripe"></div>
|
|
3032
|
+
<div class="card-body">
|
|
3033
|
+
<div class="card-label-sm">Off Target</div>
|
|
3034
|
+
<div class="big-num">\${gls.offTargetPct}%</div>
|
|
3035
|
+
<div class="desc">\${gls.offTarget} rows where neither evaluated nor alternative matches intended</div>
|
|
3036
|
+
</div>
|
|
3037
|
+
</div>
|
|
3038
|
+
</div>
|
|
3039
|
+
\` : '';
|
|
3040
|
+
|
|
3041
|
+
// \u2500\u2500 Complexity dimension summary card \u2500\u2500
|
|
3042
|
+
const cxHtml = complexityStats.length > 0 ? (() => {
|
|
3043
|
+
const totalRows = meta.processedRows || meta.totalRows;
|
|
3044
|
+
const items = complexityStats.map(cs => {
|
|
3045
|
+
const pct = Math.round((cs.average / 4.0) * 100);
|
|
3046
|
+
const labelText = cs.average > 0
|
|
3047
|
+
? \`\${esc(cs.label)} (\${cs.average.toFixed(1)} / 4.0)\`
|
|
3048
|
+
: '\u2014';
|
|
3049
|
+
return \`
|
|
3050
|
+
<div style="display:flex; flex-direction:column; gap:8px;">
|
|
3051
|
+
<span style="font-size:13px; font-weight:500; color:var(--primary);">\${esc(cs.name)}</span>
|
|
3052
|
+
<div class="cx-dim-track"><div class="cx-dim-fill" style="width:\${pct}%"></div></div>
|
|
3053
|
+
<span style="font-size:12px; color:var(--secondary);">\${labelText}</span>
|
|
3054
|
+
</div>
|
|
3055
|
+
\`;
|
|
3056
|
+
}).join('');
|
|
3057
|
+
return \`
|
|
3058
|
+
<div class="card">
|
|
3059
|
+
<div class="card-body">
|
|
3060
|
+
<div class="card-label">Text Complexity Dimensions (avg. across \${totalRows} rows)</div>
|
|
3061
|
+
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:16px 24px;">
|
|
3062
|
+
\${items}
|
|
3063
|
+
</div>
|
|
3064
|
+
</div>
|
|
3065
|
+
</div>
|
|
3066
|
+
\`;
|
|
3067
|
+
})() : '';
|
|
3068
|
+
|
|
3069
|
+
// \u2500\u2500 Insights \u2500\u2500
|
|
3070
|
+
const insightsHtml = \`
|
|
3071
|
+
<div class="card" style="display: none;">
|
|
3072
|
+
<div class="card-body">
|
|
3073
|
+
<div class="card-label" style="display:flex; align-items:center; gap:8px;">
|
|
3074
|
+
Insights
|
|
3075
|
+
<span class="tag tag-informational" style="font-size:10px; letter-spacing:0.06em; padding:2px 8px;">Early Access</span>
|
|
3076
|
+
</div>
|
|
3077
|
+
<div style="display:flex; flex-direction:column; gap:10px;">
|
|
3078
|
+
\${insights.map((text, idx) => \`
|
|
3079
|
+
<div class="insight-row">
|
|
3080
|
+
<span class="number-icon" aria-label="Insight \${idx + 1}">\${idx + 1}</span>
|
|
3081
|
+
<span class="insight-text">\${esc(text)}</span>
|
|
3082
|
+
</div>
|
|
3083
|
+
\`).join('')}
|
|
3084
|
+
</div>
|
|
3085
|
+
<p class="disclaimer">These insights are automatically generated and may not reflect the full context of your data.</p>
|
|
3086
|
+
</div>
|
|
3087
|
+
</div>
|
|
3088
|
+
\`;
|
|
3089
|
+
|
|
3090
|
+
// \u2500\u2500 Grade level distribution (CSS stacked bars) \u2500\u2500
|
|
3091
|
+
const distHtml = (() => {
|
|
3092
|
+
const toPct = (n, total) => total > 0 ? Math.round((n / total) * 100) : 0;
|
|
3093
|
+
const activeBands = gradeBandDistribution.bands.filter((_, i) => gradeBandDistribution.data[i].total > 0);
|
|
3094
|
+
const activeData = gradeBandDistribution.data.filter(d => d.total > 0);
|
|
3095
|
+
if (activeBands.length === 0) return '';
|
|
3096
|
+
|
|
3097
|
+
const rows = activeBands.map((band, i) => {
|
|
3098
|
+
const d = activeData[i];
|
|
3099
|
+
const onPct = toPct(d.onBand, d.total);
|
|
3100
|
+
const adjPct = toPct(d.adjacent, d.total);
|
|
3101
|
+
const offPct = toPct(d.offTarget, d.total);
|
|
3102
|
+
const onSeg = d.onBand > 0 ? \`<div class="bar-seg bar-on" style="width:\${onPct}%" data-tip="On Band: \${d.onBand} (\${onPct}%)"></div>\` : '';
|
|
3103
|
+
const adjSeg = d.adjacent > 0 ? \`<div class="bar-seg bar-wr" style="width:\${adjPct}%" data-tip="Within Reach: \${d.adjacent} (\${adjPct}%)"></div>\` : '';
|
|
3104
|
+
const offSeg = d.offTarget > 0 ? \`<div class="bar-seg bar-off" style="width:\${offPct}%" data-tip="Off Target: \${d.offTarget} (\${offPct}%)"></div>\` : '';
|
|
3105
|
+
return \`
|
|
3106
|
+
<div class="dist-row">
|
|
3107
|
+
<span class="band">\${esc(band)}</span>
|
|
3108
|
+
<div class="bars">\${onSeg}\${adjSeg}\${offSeg}</div>
|
|
3109
|
+
</div>
|
|
3110
|
+
\`;
|
|
3111
|
+
}).join('');
|
|
3112
|
+
|
|
3113
|
+
return \`
|
|
3114
|
+
<div class="card">
|
|
3115
|
+
<div class="card-body">
|
|
3116
|
+
<div class="card-label">Grade Band Alignment by Intended Grade</div>
|
|
3117
|
+
<div class="dist-chart">\${rows}</div>
|
|
3118
|
+
<div class="dist-legend">
|
|
3119
|
+
<span><span class="dot" style="background:var(--on-band)"></span>On Band</span>
|
|
3120
|
+
<span><span class="dot" style="background:var(--within-reach)"></span>Within Reach</span>
|
|
3121
|
+
<span><span class="dot" style="background:var(--off-target)"></span>Off Target</span>
|
|
3122
|
+
</div>
|
|
3123
|
+
</div>
|
|
3124
|
+
</div>
|
|
3125
|
+
\`;
|
|
3126
|
+
})();
|
|
3127
|
+
|
|
3128
|
+
// \u2500\u2500 Heatmap \u2500\u2500
|
|
3129
|
+
const heatmapHtml = complexityStats.length > 0 && complexityHeatmap.evaluators.length > 0 ? \`
|
|
3130
|
+
<div class="card">
|
|
3131
|
+
<div class="card-body">
|
|
3132
|
+
<div class="card-label">Text Complexity Dimensions by Intended Grade</div>
|
|
3133
|
+
<div style="overflow-x:auto;">
|
|
3134
|
+
<table class="heatmap-table">
|
|
3135
|
+
<thead>
|
|
3136
|
+
<tr>
|
|
3137
|
+
<th>Dimension</th>
|
|
3138
|
+
\${complexityHeatmap.bands.map(b => \`<th style="text-align:center; padding:8px 10px; white-space:nowrap; min-width:56px;">\${esc(b)}</th>\`).join('')}
|
|
3139
|
+
</tr>
|
|
3140
|
+
</thead>
|
|
3141
|
+
<tbody>
|
|
3142
|
+
\${complexityHeatmap.evaluators.map((evaluator, ei) => \`
|
|
3143
|
+
<tr>
|
|
3144
|
+
<td>\${esc(evaluator)}</td>
|
|
3145
|
+
\${complexityHeatmap.bands.map((band, bi) => {
|
|
3146
|
+
const val = complexityHeatmap.values[bi][ei];
|
|
3147
|
+
const label = val !== null ? val.toFixed(1) : '\u2014';
|
|
3148
|
+
return \`<td class="cell-num" style="padding:8px 10px;"><span class="heatmap-cell" style="padding:4px 8px;">\${label}</span></td>\`;
|
|
3149
|
+
}).join('')}
|
|
3150
|
+
</tr>
|
|
3151
|
+
\`).join('')}
|
|
3152
|
+
</tbody>
|
|
3153
|
+
</table>
|
|
3154
|
+
</div>
|
|
3155
|
+
</div>
|
|
3156
|
+
</div>
|
|
3157
|
+
\` : '';
|
|
3158
|
+
|
|
3159
|
+
document.getElementById('tab-summary').innerHTML = \`
|
|
3160
|
+
<div class="tab-content">
|
|
3161
|
+
\${snapshotHtml}\${glsHtml}\${cxHtml}\${insightsHtml}\${distHtml}\${heatmapHtml}
|
|
3162
|
+
</div>
|
|
3163
|
+
\`;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// ---------------------------------------------------------------------------
|
|
3167
|
+
// Full Results tab
|
|
3168
|
+
// ---------------------------------------------------------------------------
|
|
3169
|
+
|
|
3170
|
+
function renderResults() {
|
|
3171
|
+
const { fullResults } = REPORT_DATA;
|
|
3172
|
+
const { originalColumns, hasGLA, complexityEvaluators, rows } = fullResults;
|
|
3173
|
+
|
|
3174
|
+
const KEPT_COLS = ['row_id', 'text', 'grade'];
|
|
3175
|
+
const visibleColumns = originalColumns.filter(col => KEPT_COLS.includes(col.toLowerCase()));
|
|
3176
|
+
const frozenCount = 1; // only Row # is sticky
|
|
3177
|
+
|
|
3178
|
+
function colLabel(col) {
|
|
3179
|
+
const c = col.toLowerCase();
|
|
3180
|
+
if (c === 'row_id') return 'Row #';
|
|
3181
|
+
if (c === 'text') return 'Text';
|
|
3182
|
+
if (c === 'grade') return 'Grade';
|
|
3183
|
+
return col;
|
|
3184
|
+
}
|
|
3185
|
+
function colWidth(col) {
|
|
3186
|
+
const c = col.toLowerCase();
|
|
3187
|
+
if (c === 'row_id') return 60;
|
|
3188
|
+
if (c === 'text') return 340;
|
|
3189
|
+
if (c === 'grade') return 70;
|
|
3190
|
+
return 160;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
const thOriginal = visibleColumns.map((col, i) => {
|
|
3194
|
+
const isFrozen = i === 0;
|
|
3195
|
+
return \`<th class="\${isFrozen ? 'frozen frozen-last' : ''}" \${isFrozen ? 'data-frozen="0" style="min-width:' + colWidth(col) + 'px; left:0; z-index:4;"' : 'style="min-width:' + colWidth(col) + 'px;"'}>\${colLabel(col)}</th>\`;
|
|
3196
|
+
}).join('');
|
|
3197
|
+
|
|
3198
|
+
const thGLA = hasGLA ? \`
|
|
3199
|
+
<th class="group-start" style="min-width:120px">Grade Level Status</th>
|
|
3200
|
+
<th style="min-width:110px">GLA Grade Band</th>
|
|
3201
|
+
<th style="min-width:260px">GLA Reasoning</th>
|
|
3202
|
+
\` : '';
|
|
3203
|
+
|
|
3204
|
+
const thCX = complexityEvaluators.map(e => \`
|
|
3205
|
+
<th class="group-start" style="min-width:150px">\${esc(e.name)} Score</th>
|
|
3206
|
+
<th style="min-width:260px">\${esc(e.name)} Reasoning</th>
|
|
3207
|
+
\`).join('');
|
|
3208
|
+
|
|
3209
|
+
const bodyRows = rows.map(row => {
|
|
3210
|
+
const tdOriginal = visibleColumns.map((col, i) => {
|
|
3211
|
+
const isFrozen = i === 0;
|
|
3212
|
+
const isTextCol = col.toLowerCase() === 'text';
|
|
3213
|
+
const content = isTextCol
|
|
3214
|
+
? \`<div class="cell-text-inner">\${esc(row[col])}</div>\`
|
|
3215
|
+
: esc(row[col]);
|
|
3216
|
+
const classes = [
|
|
3217
|
+
isFrozen ? 'frozen frozen-last' : '',
|
|
3218
|
+
isTextCol ? 'cell-text' : '',
|
|
3219
|
+
].filter(Boolean).join(' ');
|
|
3220
|
+
const style = \`min-width:\${colWidth(col)}px;\${isFrozen ? ' left:0; z-index:2;' : ''}\`;
|
|
3221
|
+
return \`<td\${classes ? \` class="\${classes}"\` : ''} \${isFrozen ? 'data-frozen="0"' : ''} style="\${style}"\${isTextCol ? \` title="\${esc(row[col])}"\` : ''}>\${content}</td>\`;
|
|
3222
|
+
}).join('');
|
|
3223
|
+
|
|
3224
|
+
const glaStatus = row['__gla_status'] || '';
|
|
3225
|
+
const tdGLA = hasGLA ? \`
|
|
3226
|
+
<td class="group-start">\${statusBadge(glaStatus)}</td>
|
|
3227
|
+
<td>\${esc(row['__gla_band'])}</td>
|
|
3228
|
+
<td class="cell-reasoning" title="\${esc(row['__gla_reasoning'])}"><div class="cell-reasoning-inner">\${esc(row['__gla_reasoning'])}</div></td>
|
|
3229
|
+
\` : '';
|
|
3230
|
+
|
|
3231
|
+
const tdCX = complexityEvaluators.map(e => {
|
|
3232
|
+
const prefix = \`__\${e.prefix}\`;
|
|
3233
|
+
return \`
|
|
3234
|
+
<td class="group-start">\${esc(row[prefix + '_score'])}</td>
|
|
3235
|
+
<td class="cell-reasoning" title="\${esc(row[prefix + '_reasoning'])}"><div class="cell-reasoning-inner">\${esc(row[prefix + '_reasoning'])}</div></td>
|
|
3236
|
+
\`;
|
|
3237
|
+
}).join('');
|
|
3238
|
+
|
|
3239
|
+
return \`<tr>\${tdOriginal}\${tdGLA}\${tdCX}</tr>\`;
|
|
3240
|
+
}).join('');
|
|
3241
|
+
|
|
3242
|
+
document.getElementById('tab-results').innerHTML = \`
|
|
3243
|
+
<div class="tab-content" style="max-width:100%;">
|
|
3244
|
+
<div class="results-scroll">
|
|
3245
|
+
<table class="data-table" id="results-table">
|
|
3246
|
+
<thead><tr>\${thOriginal}\${thGLA}\${thCX}</tr></thead>
|
|
3247
|
+
<tbody>\${bodyRows}</tbody>
|
|
3248
|
+
</table>
|
|
3249
|
+
</div>
|
|
3250
|
+
</div>
|
|
3251
|
+
\`;
|
|
3252
|
+
|
|
3253
|
+
requestAnimationFrame(() => applyFrozenOffsets('results-table', frozenCount));
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* Reads the rendered widths of frozen header cells and applies correct
|
|
3258
|
+
* \`left\` offsets to all frozen <th> and <td> cells in the table.
|
|
3259
|
+
*/
|
|
3260
|
+
function applyFrozenOffsets(tableId, frozenCount) {
|
|
3261
|
+
const table = document.getElementById(tableId);
|
|
3262
|
+
if (!table) return;
|
|
3263
|
+
|
|
3264
|
+
const headerCells = table.querySelectorAll('thead th.frozen');
|
|
3265
|
+
const offsets = [];
|
|
3266
|
+
let cumLeft = 0;
|
|
3267
|
+
headerCells.forEach(th => {
|
|
3268
|
+
offsets.push(cumLeft);
|
|
3269
|
+
cumLeft += th.offsetWidth;
|
|
3270
|
+
});
|
|
3271
|
+
|
|
3272
|
+
table.querySelectorAll('tr').forEach(row => {
|
|
3273
|
+
const cells = row.querySelectorAll('td.frozen, th.frozen');
|
|
3274
|
+
cells.forEach((cell, i) => {
|
|
3275
|
+
if (i < frozenCount) cell.style.left = (offsets[i] ?? 0) + 'px';
|
|
3276
|
+
});
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
// ---------------------------------------------------------------------------
|
|
3281
|
+
// Bootstrap
|
|
3282
|
+
// ---------------------------------------------------------------------------
|
|
3283
|
+
|
|
3284
|
+
renderHeader();
|
|
3285
|
+
renderTabs();
|
|
3286
|
+
renderSummary();
|
|
3287
|
+
renderResults();
|
|
3288
|
+
</script>
|
|
3289
|
+
</body>
|
|
3290
|
+
</html>
|
|
3291
|
+
`;
|
|
3292
|
+
|
|
3293
|
+
// src/batch/formatters.ts
|
|
3294
|
+
var GLA_EVALUATOR_ID = "grade-level-appropriateness";
|
|
3295
|
+
var GRADE_BANDS = ["K-1", "2-3", "4-5", "6-8", "9-10", "11-CCR"];
|
|
3296
|
+
var COMPLEXITY_SCORE_MAP = {
|
|
3297
|
+
"slightly complex": 1,
|
|
3298
|
+
"moderately complex": 2,
|
|
3299
|
+
"very complex": 3,
|
|
3300
|
+
"exceedingly complex": 4
|
|
3301
|
+
};
|
|
3302
|
+
function evaluatorDisplayName(id) {
|
|
3303
|
+
return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
3304
|
+
}
|
|
3305
|
+
function gradeToBandIndex(grade) {
|
|
3306
|
+
const g = String(grade).trim().toUpperCase().replace(/^0+/, "");
|
|
3307
|
+
if (g === "K" || g === "KINDERGARTEN") return 0;
|
|
3308
|
+
if (g === "1") return 0;
|
|
3309
|
+
if (g === "2" || g === "3") return 1;
|
|
3310
|
+
if (g === "4" || g === "5") return 2;
|
|
3311
|
+
if (g === "6" || g === "7" || g === "8") return 3;
|
|
3312
|
+
if (g === "9" || g === "10") return 4;
|
|
3313
|
+
if (g === "11" || g === "12" || g === "CCR") return 5;
|
|
3314
|
+
return -1;
|
|
3315
|
+
}
|
|
3316
|
+
function glaBandToIndex(band) {
|
|
3317
|
+
return GRADE_BANDS.indexOf(band);
|
|
3318
|
+
}
|
|
3319
|
+
function getGLAStatus(inputGrade, glaBand) {
|
|
3320
|
+
const inputIdx = gradeToBandIndex(inputGrade);
|
|
3321
|
+
const glaIdx = glaBandToIndex(glaBand);
|
|
3322
|
+
if (inputIdx === -1 || glaIdx === -1) return "off-target";
|
|
3323
|
+
const diff = Math.abs(inputIdx - glaIdx);
|
|
3324
|
+
if (diff === 0) return "on-band";
|
|
3325
|
+
if (diff === 1) return "adjacent";
|
|
3326
|
+
return "off-target";
|
|
3327
|
+
}
|
|
3328
|
+
function complexityToNumeric(score) {
|
|
3329
|
+
return COMPLEXITY_SCORE_MAP[score.toLowerCase().trim()];
|
|
3330
|
+
}
|
|
3331
|
+
function complexityScoreLabel(avg) {
|
|
3332
|
+
if (avg < 1.5) return "Slightly Complex";
|
|
3333
|
+
if (avg < 2.5) return "Moderately Complex";
|
|
3334
|
+
if (avg < 3.5) return "Very Complex";
|
|
3335
|
+
return "Exceedingly Complex";
|
|
3336
|
+
}
|
|
3337
|
+
function generateInsights() {
|
|
3338
|
+
return [
|
|
3339
|
+
"Review texts marked as Off Target \u2014 they may need content revision or grade-level adjustment before distribution.",
|
|
3340
|
+
"Texts evaluated as Adjacent may benefit from light scaffolding strategies such as vocabulary pre-teaching.",
|
|
3341
|
+
"Higher grade bands tend to show greater text complexity. Consider whether complexity aligns with instructional goals."
|
|
3342
|
+
];
|
|
3343
|
+
}
|
|
3344
|
+
function groupResultsByRow(results) {
|
|
3345
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3346
|
+
for (const result of results) {
|
|
3347
|
+
if (!grouped.has(result.rowIndex)) {
|
|
3348
|
+
grouped.set(result.rowIndex, []);
|
|
3349
|
+
}
|
|
3350
|
+
grouped.get(result.rowIndex).push(result);
|
|
3351
|
+
}
|
|
3352
|
+
return grouped;
|
|
3353
|
+
}
|
|
3354
|
+
function formatEvaluatorPrefix(evaluatorId) {
|
|
3355
|
+
return evaluatorId.replace(/-/g, "_");
|
|
3356
|
+
}
|
|
3357
|
+
function escapeCSV(field) {
|
|
3358
|
+
if (field.includes(",") || field.includes('"') || field.includes("\n")) {
|
|
3359
|
+
return `"${field.replace(/"/g, '""')}"`;
|
|
3360
|
+
}
|
|
3361
|
+
return field;
|
|
3362
|
+
}
|
|
3363
|
+
function formatAsCSV(output) {
|
|
3364
|
+
if (output.results.length === 0) {
|
|
3365
|
+
return "";
|
|
3366
|
+
}
|
|
3367
|
+
const groupedByRow = groupResultsByRow(output.results);
|
|
3368
|
+
const evaluatorIds = Array.from(new Set(output.results.map((r) => r.evaluatorId))).sort();
|
|
3369
|
+
const firstResult = output.results[0];
|
|
3370
|
+
const originalColumns = Object.keys(firstResult.originalRow);
|
|
3371
|
+
const evaluatorColumns = [];
|
|
3372
|
+
for (const evalId of evaluatorIds) {
|
|
3373
|
+
const prefix = formatEvaluatorPrefix(evalId);
|
|
3374
|
+
evaluatorColumns.push(`${prefix}_score`);
|
|
3375
|
+
evaluatorColumns.push(`${prefix}_reasoning`);
|
|
3376
|
+
evaluatorColumns.push(`${prefix}_status`);
|
|
3377
|
+
}
|
|
3378
|
+
const headers = [...originalColumns, ...evaluatorColumns];
|
|
3379
|
+
const rows = [];
|
|
3380
|
+
const sortedRowIndices = Array.from(groupedByRow.keys()).sort((a, b) => a - b);
|
|
3381
|
+
for (const rowIndex of sortedRowIndices) {
|
|
3382
|
+
const resultsForRow = groupedByRow.get(rowIndex);
|
|
3383
|
+
const firstResultForRow = resultsForRow[0];
|
|
3384
|
+
const originalValues = originalColumns.map(
|
|
3385
|
+
(col) => escapeCSV(String(firstResultForRow.originalRow[col] || ""))
|
|
3386
|
+
);
|
|
3387
|
+
const evaluatorValues = [];
|
|
3388
|
+
for (const evalId of evaluatorIds) {
|
|
3389
|
+
const result = resultsForRow.find((r) => r.evaluatorId === evalId);
|
|
3390
|
+
if (result) {
|
|
3391
|
+
evaluatorValues.push(result.status === "success" ? escapeCSV(result.score || "") : "");
|
|
3392
|
+
evaluatorValues.push(result.status === "success" ? escapeCSV(result.reasoning || "") : escapeCSV(result.error || ""));
|
|
3393
|
+
evaluatorValues.push(result.status);
|
|
3394
|
+
} else {
|
|
3395
|
+
evaluatorValues.push("", "", "not_run");
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
rows.push([...originalValues, ...evaluatorValues]);
|
|
3399
|
+
}
|
|
3400
|
+
return [headers, ...rows].map((row) => row.join(",")).join("\n");
|
|
3401
|
+
}
|
|
3402
|
+
function formatAsHTML(output, meta) {
|
|
3403
|
+
const { results } = output;
|
|
3404
|
+
const byRow = groupResultsByRow(results);
|
|
3405
|
+
const allRowIndices = Array.from(byRow.keys()).sort((a, b) => a - b);
|
|
3406
|
+
const allEvaluatorIds = Array.from(new Set(results.map((r) => r.evaluatorId))).sort();
|
|
3407
|
+
const hasGLA = allEvaluatorIds.includes(GLA_EVALUATOR_ID);
|
|
3408
|
+
const complexityIds = allEvaluatorIds.filter((id) => id !== GLA_EVALUATOR_ID);
|
|
3409
|
+
let processedRows = 0;
|
|
3410
|
+
let erroredRows = 0;
|
|
3411
|
+
for (const rowResults of byRow.values()) {
|
|
3412
|
+
if (rowResults.some((r) => r.status === "error")) erroredRows++;
|
|
3413
|
+
else processedRows++;
|
|
3414
|
+
}
|
|
3415
|
+
const glaCounts = { onBand: 0, adjacent: 0, offTarget: 0 };
|
|
3416
|
+
const rowGLAStatus = /* @__PURE__ */ new Map();
|
|
3417
|
+
if (hasGLA) {
|
|
3418
|
+
for (const [rowIndex, rowResults] of byRow) {
|
|
3419
|
+
const glaResult = rowResults.find((r) => r.evaluatorId === GLA_EVALUATOR_ID);
|
|
3420
|
+
if (glaResult && glaResult.status === "success" && glaResult.score) {
|
|
3421
|
+
const status = getGLAStatus(glaResult.grade, glaResult.score);
|
|
3422
|
+
rowGLAStatus.set(rowIndex, { status, band: glaResult.score, reasoning: glaResult.reasoning || "" });
|
|
3423
|
+
if (status === "on-band") glaCounts.onBand++;
|
|
3424
|
+
else if (status === "adjacent") glaCounts.adjacent++;
|
|
3425
|
+
else glaCounts.offTarget++;
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
const glaTotal = glaCounts.onBand + glaCounts.adjacent + glaCounts.offTarget;
|
|
3430
|
+
const pct = (n) => glaTotal > 0 ? Math.round(n / glaTotal * 100) : 0;
|
|
3431
|
+
const complexityStats = complexityIds.map((evalId) => {
|
|
3432
|
+
const scores = [];
|
|
3433
|
+
const distribution = [0, 0, 0, 0];
|
|
3434
|
+
for (const rowResults of byRow.values()) {
|
|
3435
|
+
const r = rowResults.find((x) => x.evaluatorId === evalId);
|
|
3436
|
+
if (r && r.status === "success" && r.score) {
|
|
3437
|
+
const num = complexityToNumeric(r.score);
|
|
3438
|
+
if (num !== void 0) {
|
|
3439
|
+
scores.push(num);
|
|
3440
|
+
distribution[num - 1]++;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
const avg = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0;
|
|
3445
|
+
return {
|
|
3446
|
+
evaluatorId: evalId,
|
|
3447
|
+
name: evaluatorDisplayName(evalId),
|
|
3448
|
+
average: Math.round(avg * 10) / 10,
|
|
3449
|
+
label: avg > 0 ? complexityScoreLabel(avg) : "N/A",
|
|
3450
|
+
distribution
|
|
3451
|
+
};
|
|
3452
|
+
});
|
|
3453
|
+
const bandDist = GRADE_BANDS.map(() => ({ onBand: 0, adjacent: 0, offTarget: 0, total: 0 }));
|
|
3454
|
+
for (const [rowIndex, rowResults] of byRow) {
|
|
3455
|
+
const firstResult = rowResults[0];
|
|
3456
|
+
if (!firstResult) continue;
|
|
3457
|
+
const bandIdx = gradeToBandIndex(firstResult.grade);
|
|
3458
|
+
if (bandIdx === -1) continue;
|
|
3459
|
+
const glaStatus = rowGLAStatus.get(rowIndex);
|
|
3460
|
+
if (glaStatus) {
|
|
3461
|
+
bandDist[bandIdx].total++;
|
|
3462
|
+
if (glaStatus.status === "on-band") bandDist[bandIdx].onBand++;
|
|
3463
|
+
else if (glaStatus.status === "adjacent") bandDist[bandIdx].adjacent++;
|
|
3464
|
+
else bandDist[bandIdx].offTarget++;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
const hmSums = GRADE_BANDS.map(() => complexityIds.map(() => 0));
|
|
3468
|
+
const hmCounts = GRADE_BANDS.map(() => complexityIds.map(() => 0));
|
|
3469
|
+
for (const rowResults of byRow.values()) {
|
|
3470
|
+
const firstResult = rowResults[0];
|
|
3471
|
+
if (!firstResult) continue;
|
|
3472
|
+
const bandIdx = gradeToBandIndex(firstResult.grade);
|
|
3473
|
+
if (bandIdx === -1) continue;
|
|
3474
|
+
complexityIds.forEach((evalId, evalIdx) => {
|
|
3475
|
+
const r = rowResults.find((x) => x.evaluatorId === evalId);
|
|
3476
|
+
if (r && r.status === "success" && r.score) {
|
|
3477
|
+
const num = complexityToNumeric(r.score);
|
|
3478
|
+
if (num !== void 0) {
|
|
3479
|
+
hmSums[bandIdx][evalIdx] += num;
|
|
3480
|
+
hmCounts[bandIdx][evalIdx]++;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
const heatmapValues = GRADE_BANDS.map(
|
|
3486
|
+
(_, bi) => complexityIds.map((_2, ei) => {
|
|
3487
|
+
const count = hmCounts[bi][ei];
|
|
3488
|
+
return count > 0 ? Math.round(hmSums[bi][ei] / count * 10) / 10 : null;
|
|
3489
|
+
})
|
|
3490
|
+
);
|
|
3491
|
+
const firstRowResults = allRowIndices.length > 0 ? byRow.get(allRowIndices[0]) ?? [] : [];
|
|
3492
|
+
const originalColumns = firstRowResults.length > 0 ? Object.keys(firstRowResults[0].originalRow) : [];
|
|
3493
|
+
const fullResultsRows = allRowIndices.map((rowIndex) => {
|
|
3494
|
+
const rowResults = byRow.get(rowIndex);
|
|
3495
|
+
const firstResult = rowResults[0];
|
|
3496
|
+
const row = {};
|
|
3497
|
+
for (const col of originalColumns) {
|
|
3498
|
+
row[col] = String(firstResult.originalRow[col] ?? "");
|
|
3499
|
+
}
|
|
3500
|
+
const glaStatus = rowGLAStatus.get(rowIndex);
|
|
3501
|
+
const glaLabels = { "on-band": "On Band", "adjacent": "Adjacent", "off-target": "Off Target" };
|
|
3502
|
+
row["__gla_status"] = glaStatus ? glaLabels[glaStatus.status] : hasGLA ? "Error" : "";
|
|
3503
|
+
row["__gla_band"] = glaStatus?.band ?? "";
|
|
3504
|
+
row["__gla_reasoning"] = glaStatus?.reasoning ?? "";
|
|
3505
|
+
for (const evalId of complexityIds) {
|
|
3506
|
+
const r = rowResults.find((x) => x.evaluatorId === evalId);
|
|
3507
|
+
const prefix = `__${evalId.replace(/-/g, "_")}`;
|
|
3508
|
+
row[`${prefix}_score`] = r?.status === "success" ? r.score ?? "" : r?.status === "error" ? "Error" : "";
|
|
3509
|
+
row[`${prefix}_reasoning`] = r?.status === "success" ? r.reasoning ?? "" : r?.error ?? "";
|
|
3510
|
+
}
|
|
3511
|
+
return row;
|
|
3512
|
+
});
|
|
3513
|
+
const reportData = {
|
|
3514
|
+
meta: {
|
|
3515
|
+
reportId: meta.reportId,
|
|
3516
|
+
generatedAt: meta.generatedAt.toLocaleString("en-US", {
|
|
3517
|
+
month: "short",
|
|
3518
|
+
day: "numeric",
|
|
3519
|
+
year: "numeric",
|
|
3520
|
+
hour: "numeric",
|
|
3521
|
+
minute: "2-digit",
|
|
3522
|
+
hour12: true
|
|
3523
|
+
}),
|
|
3524
|
+
csvPath: meta.csvPath,
|
|
3525
|
+
groupId: meta.groupId,
|
|
3526
|
+
evaluatorIds: allEvaluatorIds,
|
|
3527
|
+
evaluatorNames: allEvaluatorIds.map(evaluatorDisplayName),
|
|
3528
|
+
totalRows: meta.totalInputRows,
|
|
3529
|
+
processedRows,
|
|
3530
|
+
erroredRows
|
|
3531
|
+
},
|
|
3532
|
+
gradeLevelStats: {
|
|
3533
|
+
onBand: glaCounts.onBand,
|
|
3534
|
+
adjacent: glaCounts.adjacent,
|
|
3535
|
+
offTarget: glaCounts.offTarget,
|
|
3536
|
+
onBandPct: pct(glaCounts.onBand),
|
|
3537
|
+
adjacentPct: pct(glaCounts.adjacent),
|
|
3538
|
+
offTargetPct: pct(glaCounts.offTarget),
|
|
3539
|
+
hasData: glaTotal > 0
|
|
3540
|
+
},
|
|
3541
|
+
complexityStats,
|
|
3542
|
+
gradeBandDistribution: {
|
|
3543
|
+
bands: [...GRADE_BANDS],
|
|
3544
|
+
data: bandDist
|
|
3545
|
+
},
|
|
3546
|
+
complexityHeatmap: {
|
|
3547
|
+
bands: [...GRADE_BANDS],
|
|
3548
|
+
evaluators: complexityIds.map(evaluatorDisplayName),
|
|
3549
|
+
evaluatorIds: complexityIds,
|
|
3550
|
+
values: heatmapValues
|
|
3551
|
+
},
|
|
3552
|
+
insights: generateInsights(),
|
|
3553
|
+
fullResults: {
|
|
3554
|
+
originalColumns,
|
|
3555
|
+
hasGLA,
|
|
3556
|
+
complexityEvaluators: complexityIds.map((id) => ({
|
|
3557
|
+
evaluatorId: id,
|
|
3558
|
+
name: evaluatorDisplayName(id),
|
|
3559
|
+
prefix: id.replace(/-/g, "_")
|
|
3560
|
+
})),
|
|
3561
|
+
rows: fullResultsRows
|
|
3562
|
+
}
|
|
3563
|
+
};
|
|
3564
|
+
const safeJson = JSON.stringify(reportData).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
3565
|
+
const INJECTION_MARKER = "var REPORT_DATA = null; // __REPLACED_BY_FORMATTER__";
|
|
3566
|
+
if (!report_template_default.includes(INJECTION_MARKER)) {
|
|
3567
|
+
throw new Error("Report template injection marker not found \u2014 template may be corrupted");
|
|
3568
|
+
}
|
|
3569
|
+
return report_template_default.replace(INJECTION_MARKER, `var REPORT_DATA = ${safeJson};`);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
// src/batch/progress.ts
|
|
3573
|
+
var ProgressTracker = class {
|
|
3574
|
+
totalTasks;
|
|
3575
|
+
completed = 0;
|
|
3576
|
+
successful = 0;
|
|
3577
|
+
failed = 0;
|
|
3578
|
+
startTime;
|
|
3579
|
+
perEvaluator = /* @__PURE__ */ new Map();
|
|
3580
|
+
constructor(totalTasks) {
|
|
3581
|
+
this.totalTasks = totalTasks;
|
|
3582
|
+
this.startTime = Date.now();
|
|
3583
|
+
}
|
|
3584
|
+
/**
|
|
3585
|
+
* Update progress with a new result
|
|
3586
|
+
*/
|
|
3587
|
+
update(result) {
|
|
3588
|
+
this.completed++;
|
|
3589
|
+
if (result.status === "success") {
|
|
3590
|
+
this.successful++;
|
|
3591
|
+
} else {
|
|
3592
|
+
this.failed++;
|
|
3593
|
+
}
|
|
3594
|
+
if (!this.perEvaluator.has(result.evaluatorId)) {
|
|
3595
|
+
this.perEvaluator.set(result.evaluatorId, { completed: 0, successful: 0, failed: 0 });
|
|
3596
|
+
}
|
|
3597
|
+
const stats = this.perEvaluator.get(result.evaluatorId);
|
|
3598
|
+
stats.completed++;
|
|
3599
|
+
if (result.status === "success") {
|
|
3600
|
+
stats.successful++;
|
|
3601
|
+
} else {
|
|
3602
|
+
stats.failed++;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
/**
|
|
3606
|
+
* Get current progress percentage
|
|
3607
|
+
*/
|
|
3608
|
+
getPercentage() {
|
|
3609
|
+
return Math.round(this.completed / this.totalTasks * 100);
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Get elapsed time in seconds
|
|
3613
|
+
*/
|
|
3614
|
+
getElapsedSeconds() {
|
|
3615
|
+
return Math.round((Date.now() - this.startTime) / 1e3);
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Estimate remaining time in seconds
|
|
3619
|
+
*/
|
|
3620
|
+
getEstimatedRemainingSeconds() {
|
|
3621
|
+
if (this.completed === 0) return 0;
|
|
3622
|
+
const elapsed = Date.now() - this.startTime;
|
|
3623
|
+
const avgTimePerTask = elapsed / this.completed;
|
|
3624
|
+
const remaining = this.totalTasks - this.completed;
|
|
3625
|
+
return Math.round(avgTimePerTask * remaining / 1e3);
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3628
|
+
* Format elapsed time as human-readable string
|
|
3629
|
+
*/
|
|
3630
|
+
formatElapsed() {
|
|
3631
|
+
const seconds = this.getElapsedSeconds();
|
|
3632
|
+
if (seconds < 60) return `${seconds}s`;
|
|
3633
|
+
const minutes = Math.floor(seconds / 60);
|
|
3634
|
+
const remainingSeconds = seconds % 60;
|
|
3635
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Format estimated remaining time as human-readable string
|
|
3639
|
+
*/
|
|
3640
|
+
formatEstimatedRemaining() {
|
|
3641
|
+
const seconds = this.getEstimatedRemainingSeconds();
|
|
3642
|
+
if (seconds < 60) return `${seconds}s`;
|
|
3643
|
+
const minutes = Math.floor(seconds / 60);
|
|
3644
|
+
const remainingSeconds = seconds % 60;
|
|
3645
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Generate progress bar
|
|
3649
|
+
*/
|
|
3650
|
+
getProgressBar(width = 20) {
|
|
3651
|
+
const percentage = this.getPercentage();
|
|
3652
|
+
const filled = Math.round(percentage / 100 * width);
|
|
3653
|
+
const empty = width - filled;
|
|
3654
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Display progress in terminal
|
|
3658
|
+
*/
|
|
3659
|
+
display() {
|
|
3660
|
+
if (this.completed > 1) {
|
|
3661
|
+
const linesToClear = 3 + this.perEvaluator.size;
|
|
3662
|
+
process.stdout.write(`\x1B[${linesToClear}A`);
|
|
3663
|
+
process.stdout.write("\x1B[J");
|
|
3664
|
+
}
|
|
3665
|
+
console.log(
|
|
3666
|
+
`${this.getProgressBar()} ${this.getPercentage()}% (${this.completed}/${this.totalTasks})`
|
|
3667
|
+
);
|
|
3668
|
+
for (const [evalId, stats] of this.perEvaluator.entries()) {
|
|
3669
|
+
const status = stats.completed === stats.successful ? "\u2713" : stats.failed > 0 ? "\u2717" : "\u23F3";
|
|
3670
|
+
console.log(
|
|
3671
|
+
` ${status} ${evalId}: ${stats.successful}/${stats.completed} successful`
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3674
|
+
console.log(
|
|
3675
|
+
`
|
|
3676
|
+
\u23F1 Elapsed: ${this.formatElapsed()} | Estimated remaining: ${this.formatEstimatedRemaining()}`
|
|
3677
|
+
);
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Display final summary
|
|
3681
|
+
*/
|
|
3682
|
+
displaySummary() {
|
|
3683
|
+
const linesToClear = 3 + this.perEvaluator.size + 1;
|
|
3684
|
+
process.stdout.write(`\x1B[${linesToClear}A`);
|
|
3685
|
+
process.stdout.write("\x1B[J");
|
|
3686
|
+
console.log("\n\u2705 Batch evaluation completed!\n");
|
|
3687
|
+
console.log(`Total tasks: ${this.totalTasks}`);
|
|
3688
|
+
console.log(`Successful: ${this.successful} \u2713`);
|
|
3689
|
+
console.log(`Failed: ${this.failed} \u2717`);
|
|
3690
|
+
console.log(`Duration: ${this.formatElapsed()}`);
|
|
3691
|
+
if (this.perEvaluator.size > 1) {
|
|
3692
|
+
console.log("\nResults per evaluator:");
|
|
3693
|
+
for (const [evalId, stats] of this.perEvaluator.entries()) {
|
|
3694
|
+
console.log(
|
|
3695
|
+
` ${evalId}: ${stats.successful} successful, ${stats.failed} failed`
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
console.log();
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
3702
|
+
|
|
3703
|
+
// src/batch/cli.ts
|
|
3704
|
+
function parseArgs() {
|
|
3705
|
+
const args = process.argv.slice(2);
|
|
3706
|
+
const result = {};
|
|
3707
|
+
for (let i = 0; i < args.length; i++) {
|
|
3708
|
+
if (args[i] === "--concurrency" && args[i + 1]) {
|
|
3709
|
+
const v = parseInt(args[++i], 10);
|
|
3710
|
+
if (!isNaN(v) && v > 0) result.concurrency = v;
|
|
3711
|
+
} else if (args[i] === "--max-retries" && args[i + 1]) {
|
|
3712
|
+
const v = parseInt(args[++i], 10);
|
|
3713
|
+
if (!isNaN(v) && v >= 0) result.maxRetries = v;
|
|
3714
|
+
} else if (args[i] === "--no-telemetry") {
|
|
3715
|
+
result.noTelemetry = true;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
return result;
|
|
3719
|
+
}
|
|
3720
|
+
async function main() {
|
|
3721
|
+
const cliArgs = parseArgs();
|
|
3722
|
+
console.log("\n\u{1F4CA} Batch CSV Evaluator\n");
|
|
3723
|
+
console.log("This tool will evaluate multiple texts using one or more evaluators.\n");
|
|
3724
|
+
try {
|
|
3725
|
+
let inputs = [];
|
|
3726
|
+
const { csvPath } = await prompts({
|
|
3727
|
+
type: "text",
|
|
3728
|
+
name: "csvPath",
|
|
3729
|
+
message: "Where is your CSV file?",
|
|
3730
|
+
initial: "./input.csv",
|
|
3731
|
+
validate: (value) => {
|
|
3732
|
+
try {
|
|
3733
|
+
inputs = parseCSV(value);
|
|
3734
|
+
return true;
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
return error instanceof Error ? error.message : "Invalid CSV file";
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
});
|
|
3740
|
+
if (!csvPath) {
|
|
3741
|
+
console.log("No file path provided. Run the command again to start over.");
|
|
3742
|
+
process.exit(0);
|
|
3743
|
+
}
|
|
3744
|
+
console.log(`
|
|
3745
|
+
\u2713 Found ${inputs.length} rows in CSV
|
|
3746
|
+
`);
|
|
3747
|
+
const group = getAvailableGroups()[0];
|
|
3748
|
+
console.log(`\u2713 Evaluator group: ${group.name}`);
|
|
3749
|
+
console.log(` ${group.description}`);
|
|
3750
|
+
console.log(` Row limit: ${group.maxInputRows}
|
|
3751
|
+
`);
|
|
3752
|
+
if (inputs.length > group.maxInputRows) {
|
|
3753
|
+
console.error(`\u274C Too many rows: ${inputs.length} (max ${group.maxInputRows} for this group)
|
|
3754
|
+
`);
|
|
3755
|
+
console.log("Suggestions:");
|
|
3756
|
+
console.log(` \u2022 Trim the CSV to ${group.maxInputRows} rows`);
|
|
3757
|
+
console.log(" \u2022 Split into multiple smaller batches\n");
|
|
3758
|
+
process.exit(1);
|
|
3759
|
+
}
|
|
3760
|
+
let googleApiKey;
|
|
3761
|
+
let openaiApiKey;
|
|
3762
|
+
if (group.requiresGoogleKey) {
|
|
3763
|
+
const result = await prompts({
|
|
3764
|
+
type: "password",
|
|
3765
|
+
name: "key",
|
|
3766
|
+
message: "Google API Key:",
|
|
3767
|
+
initial: process.env.GOOGLE_API_KEY || "",
|
|
3768
|
+
validate: (value) => value ? true : "Google API key is required"
|
|
3769
|
+
});
|
|
3770
|
+
if (!result.key) {
|
|
3771
|
+
console.log("Cancelled.");
|
|
3772
|
+
process.exit(0);
|
|
3773
|
+
}
|
|
3774
|
+
googleApiKey = result.key;
|
|
3775
|
+
}
|
|
3776
|
+
if (group.requiresOpenAIKey) {
|
|
3777
|
+
const result = await prompts({
|
|
3778
|
+
type: "password",
|
|
3779
|
+
name: "key",
|
|
3780
|
+
message: "OpenAI API Key:",
|
|
3781
|
+
initial: process.env.OPENAI_API_KEY || "",
|
|
3782
|
+
validate: (value) => value ? true : "OpenAI API key is required"
|
|
3783
|
+
});
|
|
3784
|
+
if (!result.key) {
|
|
3785
|
+
console.log("Cancelled.");
|
|
3786
|
+
process.exit(0);
|
|
3787
|
+
}
|
|
3788
|
+
openaiApiKey = result.key;
|
|
3789
|
+
}
|
|
3790
|
+
const now = /* @__PURE__ */ new Date();
|
|
3791
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
3792
|
+
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
|
3793
|
+
const defaultOutputDir = path.join(process.cwd(), `batch-results-${timestamp}`);
|
|
3794
|
+
const { outputDir } = await prompts({
|
|
3795
|
+
type: "text",
|
|
3796
|
+
name: "outputDir",
|
|
3797
|
+
message: "Output directory:",
|
|
3798
|
+
initial: defaultOutputDir,
|
|
3799
|
+
validate: (value) => {
|
|
3800
|
+
const parentDir = path.dirname(value);
|
|
3801
|
+
if (!fs2.existsSync(parentDir)) {
|
|
3802
|
+
return `Parent directory does not exist: ${parentDir}`;
|
|
3803
|
+
}
|
|
3804
|
+
try {
|
|
3805
|
+
const testFile = path.join(parentDir, ".write-test");
|
|
3806
|
+
fs2.writeFileSync(testFile, "");
|
|
3807
|
+
fs2.unlinkSync(testFile);
|
|
3808
|
+
return true;
|
|
3809
|
+
} catch (error) {
|
|
3810
|
+
if (error instanceof Error) {
|
|
3811
|
+
if (error.message.includes("EACCES")) return `No write permission for directory: ${parentDir}`;
|
|
3812
|
+
if (error.message.includes("EROFS")) return `Directory is read-only: ${parentDir}`;
|
|
3813
|
+
return `Cannot write to directory: ${error.message}`;
|
|
3814
|
+
}
|
|
3815
|
+
return "Cannot write to directory";
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
});
|
|
3819
|
+
if (!outputDir) {
|
|
3820
|
+
console.log("No output directory provided. Run the command again to start over.");
|
|
3821
|
+
process.exit(0);
|
|
3822
|
+
}
|
|
3823
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
3824
|
+
const csvBasename = path.basename(csvPath, path.extname(csvPath));
|
|
3825
|
+
const reportMeta = {
|
|
3826
|
+
csvPath: path.resolve(csvPath),
|
|
3827
|
+
groupId: group.id,
|
|
3828
|
+
reportId: `${csvBasename.replace(/[^a-zA-Z0-9]/g, "_")}_${timestamp}`,
|
|
3829
|
+
generatedAt: now,
|
|
3830
|
+
totalInputRows: inputs.length
|
|
3831
|
+
};
|
|
3832
|
+
const totalTasks = inputs.length * group.evaluatorIds.length;
|
|
3833
|
+
console.log(`
|
|
3834
|
+
\u{1F4DD} Summary:`);
|
|
3835
|
+
console.log(` Input rows: ${inputs.length}`);
|
|
3836
|
+
console.log(` Evaluators: ${group.evaluatorIds.length}`);
|
|
3837
|
+
console.log(` Total tasks: ${totalTasks}`);
|
|
3838
|
+
console.log(` Concurrency: ${cliArgs.concurrency ?? 3}`);
|
|
3839
|
+
console.log(` Max retries: ${cliArgs.maxRetries ?? 2}`);
|
|
3840
|
+
console.log(` Output: ${outputDir}
|
|
3841
|
+
`);
|
|
3842
|
+
const { confirm } = await prompts({
|
|
3843
|
+
type: "confirm",
|
|
3844
|
+
name: "confirm",
|
|
3845
|
+
message: "Start batch evaluation?",
|
|
3846
|
+
initial: true
|
|
3847
|
+
});
|
|
3848
|
+
if (!confirm) {
|
|
3849
|
+
console.log("Cancelled.");
|
|
3850
|
+
process.exit(0);
|
|
3851
|
+
}
|
|
3852
|
+
console.log("\n" + "=".repeat(60));
|
|
3853
|
+
const tracker = new ProgressTracker(totalTasks);
|
|
3854
|
+
const evaluationStartTime = Date.now();
|
|
3855
|
+
const evaluator = new BatchEvaluator({
|
|
3856
|
+
googleApiKey,
|
|
3857
|
+
openaiApiKey,
|
|
3858
|
+
concurrency: cliArgs.concurrency ?? 3,
|
|
3859
|
+
maxRetries: cliArgs.maxRetries ?? 2,
|
|
3860
|
+
telemetry: !cliArgs.noTelemetry
|
|
3861
|
+
});
|
|
3862
|
+
let isShuttingDown = false;
|
|
3863
|
+
const handleShutdown = () => {
|
|
3864
|
+
if (isShuttingDown) {
|
|
3865
|
+
console.log("\n\n\u26A0\uFE0F Force quit detected. Exiting immediately...");
|
|
3866
|
+
process.exit(1);
|
|
3867
|
+
}
|
|
3868
|
+
isShuttingDown = true;
|
|
3869
|
+
console.log("\n\n\u26A0\uFE0F Shutdown requested. Saving partial results...");
|
|
3870
|
+
console.log(" (Press Ctrl+C again to force quit)\n");
|
|
3871
|
+
const partialResults = evaluator.cancel();
|
|
3872
|
+
if (partialResults.length > 0) {
|
|
3873
|
+
const durationMs = Date.now() - evaluationStartTime;
|
|
3874
|
+
const partialOutput = {
|
|
3875
|
+
results: partialResults,
|
|
3876
|
+
summary: {
|
|
3877
|
+
totalTasks: partialResults.length,
|
|
3878
|
+
successful: partialResults.filter((r) => r.status === "success").length,
|
|
3879
|
+
failed: partialResults.filter((r) => r.status === "error").length,
|
|
3880
|
+
durationMs,
|
|
3881
|
+
resultsPerEvaluator: {}
|
|
3882
|
+
}
|
|
3883
|
+
};
|
|
3884
|
+
try {
|
|
3885
|
+
fs2.writeFileSync(path.join(outputDir, "results-partial.csv"), formatAsCSV(partialOutput));
|
|
3886
|
+
fs2.writeFileSync(path.join(outputDir, "results-partial.html"), formatAsHTML(partialOutput, reportMeta));
|
|
3887
|
+
console.log(`\u2713 Saved ${partialResults.length} results to:`);
|
|
3888
|
+
console.log(` ${outputDir}/`);
|
|
3889
|
+
console.log(` \u251C\u2500\u2500 results-partial.csv`);
|
|
3890
|
+
console.log(` \u2514\u2500\u2500 results-partial.html`);
|
|
3891
|
+
console.log();
|
|
3892
|
+
} catch (error) {
|
|
3893
|
+
console.error("\u274C Error saving partial results:", error instanceof Error ? error.message : String(error));
|
|
3894
|
+
}
|
|
3895
|
+
} else {
|
|
3896
|
+
console.log("No results to save yet.\n");
|
|
3897
|
+
}
|
|
3898
|
+
process.exit(0);
|
|
3899
|
+
};
|
|
3900
|
+
process.on("SIGINT", handleShutdown);
|
|
3901
|
+
process.on("SIGTERM", handleShutdown);
|
|
3902
|
+
let output;
|
|
3903
|
+
try {
|
|
3904
|
+
output = await evaluator.evaluate(inputs, group.id, (result) => {
|
|
3905
|
+
tracker.update(result);
|
|
3906
|
+
tracker.display();
|
|
3907
|
+
});
|
|
3908
|
+
} finally {
|
|
3909
|
+
process.off("SIGINT", handleShutdown);
|
|
3910
|
+
process.off("SIGTERM", handleShutdown);
|
|
3911
|
+
}
|
|
3912
|
+
tracker.displaySummary();
|
|
3913
|
+
try {
|
|
3914
|
+
fs2.writeFileSync(path.join(outputDir, "results.csv"), formatAsCSV(output));
|
|
3915
|
+
fs2.writeFileSync(path.join(outputDir, "results.html"), formatAsHTML(output, reportMeta));
|
|
3916
|
+
console.log("\u{1F4C4} Output files generated:");
|
|
3917
|
+
console.log(` ${outputDir}/`);
|
|
3918
|
+
console.log(` \u251C\u2500\u2500 results.csv`);
|
|
3919
|
+
console.log(` \u2514\u2500\u2500 results.html`);
|
|
3920
|
+
console.log();
|
|
3921
|
+
const htmlPath = path.join(outputDir, "results.html");
|
|
3922
|
+
try {
|
|
3923
|
+
const cmd = process.platform === "win32" ? `start "" "${htmlPath}"` : `open "${htmlPath}"`;
|
|
3924
|
+
exec(cmd);
|
|
3925
|
+
} catch {
|
|
3926
|
+
}
|
|
3927
|
+
} catch (error) {
|
|
3928
|
+
console.error("\n\u274C Error writing output files:");
|
|
3929
|
+
if (error instanceof Error) console.error(` ${error.message}`);
|
|
3930
|
+
console.error("\n\u26A0\uFE0F Evaluation completed but outputs could not be saved.");
|
|
3931
|
+
process.exit(1);
|
|
3932
|
+
}
|
|
3933
|
+
} catch (error) {
|
|
3934
|
+
console.error("\n\u274C Error:", error instanceof Error ? error.message : String(error));
|
|
3935
|
+
process.exit(1);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
main();
|
|
3939
|
+
//# sourceMappingURL=cli.js.map
|
|
3940
|
+
//# sourceMappingURL=cli.js.map
|