@intellectif/lk-core 1.4.0 → 1.5.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 +45 -0
- package/README.md +2 -0
- package/dist/{ai-CnmLPMzd.d.cts → ai-BYryFkF9.d.cts} +199 -5
- package/dist/ai-BYryFkF9.d.cts.map +1 -0
- package/dist/{ai-CnmLPMzd.d.ts → ai-BYryFkF9.d.ts} +199 -5
- package/dist/ai-BYryFkF9.d.ts.map +1 -0
- package/dist/ai-check.cjs +175 -17
- package/dist/ai-check.cjs.map +1 -1
- package/dist/ai-check.d.cts +13 -5
- package/dist/ai-check.d.cts.map +1 -1
- package/dist/ai-check.d.ts +13 -5
- package/dist/ai-check.d.ts.map +1 -1
- package/dist/ai-check.js +175 -17
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-writing-G6tnCGzw.cjs → ai-writing-Cen7-3qs.cjs} +412 -61
- package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
- package/dist/{ai-writing-CPpH9Qxq.js → ai-writing-D8PdhJJz.js} +395 -62
- package/dist/ai-writing-D8PdhJJz.js.map +1 -0
- package/dist/{index-BAfEHCnD.d.cts → index-Bg0aeWtM.d.cts} +327 -3
- package/dist/index-Bg0aeWtM.d.cts.map +1 -0
- package/dist/{index-2nigSf4N.d.cts → index-CxpiWedW.d.ts} +6 -2
- package/dist/index-CxpiWedW.d.ts.map +1 -0
- package/dist/{index-BqMXWuNC.d.cts → index-D7R3Ll8R.d.cts} +2 -2
- package/dist/{index-BqMXWuNC.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
- package/dist/{index-CVWvymVb.d.ts → index-DUaW1Tj5.d.ts} +327 -3
- package/dist/index-DUaW1Tj5.d.ts.map +1 -0
- package/dist/{index-B54baFUA.d.ts → index-qMbxed1B.d.cts} +6 -2
- package/dist/index-qMbxed1B.d.cts.map +1 -0
- package/dist/{index-CcHDTxIZ.d.ts → index-wMc1E_aM.d.ts} +2 -2
- package/dist/{index-CcHDTxIZ.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
- package/dist/index.cjs +31 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -95
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +85 -95
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -117
- package/dist/index.js.map +1 -1
- package/dist/{item-group-DroFXgLb.cjs → item-group-DEB7nLID.cjs} +2 -2
- package/dist/{item-group-DroFXgLb.cjs.map → item-group-DEB7nLID.cjs.map} +1 -1
- package/dist/{item-group-DXiR8nOr.js → item-group-DdSUCUjM.js} +2 -2
- package/dist/{item-group-DXiR8nOr.js.map → item-group-DdSUCUjM.js.map} +1 -1
- package/dist/{registry-DVi0LR_i.cjs → registry-Ce00A_OW.cjs} +184 -4
- package/dist/registry-Ce00A_OW.cjs.map +1 -0
- package/dist/{registry-Dsx9mNsS.js → registry-DaZRpqjW.js} +155 -5
- package/dist/registry-DaZRpqjW.js.map +1 -0
- package/dist/{schemas-Bdw0nGPo.js → schemas-BioKFf3F.js} +3 -3
- package/dist/{schemas-Bdw0nGPo.js.map → schemas-BioKFf3F.js.map} +1 -1
- package/dist/{schemas-DVbAOrtd.cjs → schemas-DksueYsu.cjs} +3 -3
- package/dist/{schemas-DVbAOrtd.cjs.map → schemas-DksueYsu.cjs.map} +1 -1
- package/dist/schemas.cjs +2 -2
- package/dist/schemas.d.cts +1 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +2 -2
- package/dist/{scoring-BZqUkuDF.js → scoring-BKNNTz-s.js} +657 -3
- package/dist/scoring-BKNNTz-s.js.map +1 -0
- package/dist/{scoring-B2_HEhMT.cjs → scoring-lKQVYsxy.cjs} +722 -2
- package/dist/scoring-lKQVYsxy.cjs.map +1 -0
- package/dist/scoring.cjs +8 -2
- package/dist/scoring.d.cts +3 -3
- package/dist/scoring.d.ts +3 -3
- package/dist/scoring.js +3 -3
- package/dist/{xapi-DNzkSJ0S.cjs → xapi-NYrTlpID.cjs} +2 -2
- package/dist/{xapi-DNzkSJ0S.cjs.map → xapi-NYrTlpID.cjs.map} +1 -1
- package/dist/{xapi-CMTph7wG.js → xapi-wR6gu4qS.js} +2 -2
- package/dist/{xapi-CMTph7wG.js.map → xapi-wR6gu4qS.js.map} +1 -1
- package/dist/xapi.cjs +1 -1
- package/dist/xapi.d.cts +1 -1
- package/dist/xapi.d.ts +1 -1
- package/dist/xapi.js +1 -2
- package/package.json +2 -2
- package/vectors/README.md +24 -1
- package/vectors/scoring.json +18182 -1
- package/dist/ai-CnmLPMzd.d.cts.map +0 -1
- package/dist/ai-CnmLPMzd.d.ts.map +0 -1
- package/dist/ai-writing-CPpH9Qxq.js.map +0 -1
- package/dist/ai-writing-G6tnCGzw.cjs.map +0 -1
- package/dist/index-2nigSf4N.d.cts.map +0 -1
- package/dist/index-B54baFUA.d.ts.map +0 -1
- package/dist/index-BAfEHCnD.d.cts.map +0 -1
- package/dist/index-CVWvymVb.d.ts.map +0 -1
- package/dist/registry-DVi0LR_i.cjs.map +0 -1
- package/dist/registry-Dsx9mNsS.js.map +0 -1
- package/dist/scoring-B2_HEhMT.cjs.map +0 -1
- package/dist/scoring-BZqUkuDF.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const require_registry = require("./registry-
|
|
2
|
-
const require_scoring = require("./scoring-
|
|
3
|
-
const require_item_group = require("./item-group-
|
|
1
|
+
const require_registry = require("./registry-Ce00A_OW.cjs");
|
|
2
|
+
const require_scoring = require("./scoring-lKQVYsxy.cjs");
|
|
3
|
+
const require_item_group = require("./item-group-DEB7nLID.cjs");
|
|
4
4
|
//#region src/ai-material.ts
|
|
5
5
|
/**
|
|
6
6
|
* What a model is told about the group a question sits in: the material the
|
|
@@ -2289,65 +2289,19 @@ function interactiveVideoFromDrafts(input) {
|
|
|
2289
2289
|
};
|
|
2290
2290
|
}
|
|
2291
2291
|
//#endregion
|
|
2292
|
-
//#region src/ai-
|
|
2292
|
+
//#region src/ai-written.ts
|
|
2293
|
+
/**
|
|
2294
|
+
* What the AI features about a written response share: reading a model's text
|
|
2295
|
+
* fields, and finding each correction's quote in the learner's text.
|
|
2296
|
+
*
|
|
2297
|
+
* Internal. `ai-writing.ts` (feedback on a draft) and `ai-grading.ts` (a
|
|
2298
|
+
* grade) are their public faces, and hold every correction to the same
|
|
2299
|
+
* anchoring.
|
|
2300
|
+
*/
|
|
2293
2301
|
/** The most corrections one piece of writing feedback may carry. More is refused, not cut. */
|
|
2294
2302
|
const AI_WRITING_MAX_CORRECTIONS = 20;
|
|
2295
2303
|
/** The longest quote, correction, explanation or criterion comment, in code points. */
|
|
2296
2304
|
const AI_WRITING_MAX_FIELD_LENGTH = 500;
|
|
2297
|
-
/** The longest `category` or `band`: a tag, not a sentence. */
|
|
2298
|
-
const MAX_TAG_LENGTH = 40;
|
|
2299
|
-
/**
|
|
2300
|
-
* The request for feedback on a draft, or `null` when there is none to give:
|
|
2301
|
-
* not a written response, an author who switched explanations off (writing
|
|
2302
|
-
* feedback reads `ai.explanations`), a redacted item — feedback is for
|
|
2303
|
-
* practice, where the item is whole — or a draft with nothing in it.
|
|
2304
|
-
*
|
|
2305
|
-
* The facts are the task as the learner saw it, the draft verbatim with its
|
|
2306
|
-
* word count recomputed as the grader will count it, and the rubric the answer
|
|
2307
|
-
* will be graded on. `previousFeedback` is the text of the feedback already
|
|
2308
|
-
* shown on earlier drafts, so a model can speak to what changed. `group` is the
|
|
2309
|
-
* item group the task sits in: its material, the text a summary is of, goes
|
|
2310
|
-
* with the request as `material`.
|
|
2311
|
-
*/
|
|
2312
|
-
function aiWritingFeedbackRequest(input) {
|
|
2313
|
-
const { data, response, learnerLocale } = input;
|
|
2314
|
-
if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) return null;
|
|
2315
|
-
const text = response?.type === "written-response" ? response.text : "";
|
|
2316
|
-
if (typeof text !== "string" || text.trim() === "") return null;
|
|
2317
|
-
const item = data;
|
|
2318
|
-
const wordCount = require_registry.countWords(text);
|
|
2319
|
-
const minWords = typeof item.minWords === "number" ? item.minWords : 0;
|
|
2320
|
-
const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
|
|
2321
|
-
const criteria = Array.isArray(item.rubric?.criteria) ? item.rubric.criteria : null;
|
|
2322
|
-
const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
|
|
2323
|
-
name: criterion.name,
|
|
2324
|
-
...typeof criterion.description === "string" ? { description: criterion.description } : {},
|
|
2325
|
-
weight: criterion.weight
|
|
2326
|
-
}));
|
|
2327
|
-
const previousFeedback = [...input.previousFeedback ?? []];
|
|
2328
|
-
const material = materialFacts(input.group);
|
|
2329
|
-
return {
|
|
2330
|
-
feature: "writing-feedback",
|
|
2331
|
-
facts: {
|
|
2332
|
-
activityType: "written-response",
|
|
2333
|
-
activityId: data.id,
|
|
2334
|
-
title: data.title,
|
|
2335
|
-
...typeof data.locale === "string" ? { locale: data.locale } : {},
|
|
2336
|
-
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
|
2337
|
-
text,
|
|
2338
|
-
wordCount,
|
|
2339
|
-
minWords,
|
|
2340
|
-
maxWords,
|
|
2341
|
-
withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
|
|
2342
|
-
rubric,
|
|
2343
|
-
...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
|
|
2344
|
-
},
|
|
2345
|
-
draftNumber: previousFeedback.length + 1,
|
|
2346
|
-
previousFeedback,
|
|
2347
|
-
...material !== null ? { material } : {},
|
|
2348
|
-
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
2349
|
-
};
|
|
2350
|
-
}
|
|
2351
2305
|
/**
|
|
2352
2306
|
* Where `quote` sits in the draft: at `claimed`, when the model named a range —
|
|
2353
2307
|
* which must then hold the quote — or else at its first occurrence not already
|
|
@@ -2446,7 +2400,7 @@ function checkCorrections(raw, text) {
|
|
|
2446
2400
|
refusal: "malformed"
|
|
2447
2401
|
};
|
|
2448
2402
|
const explanation = textField(entry.explanation, 500);
|
|
2449
|
-
const category = textField(entry.category,
|
|
2403
|
+
const category = textField(entry.category, 40);
|
|
2450
2404
|
if (!explanation.ok) return explanation;
|
|
2451
2405
|
if (!category.ok) return category;
|
|
2452
2406
|
let claimed;
|
|
@@ -2478,6 +2432,385 @@ function checkCorrections(raw, text) {
|
|
|
2478
2432
|
corrections
|
|
2479
2433
|
};
|
|
2480
2434
|
}
|
|
2435
|
+
//#endregion
|
|
2436
|
+
//#region src/ai-grading.ts
|
|
2437
|
+
/**
|
|
2438
|
+
* How far a model's level probabilities may sum from 1 and still be a
|
|
2439
|
+
* distribution: rounding in a model's output, not a different judgement.
|
|
2440
|
+
*/
|
|
2441
|
+
const AI_GRADING_PROBABILITY_TOLERANCE = require_registry.LEVEL_PROBABILITY_TOLERANCE;
|
|
2442
|
+
/**
|
|
2443
|
+
* The request for a model's grade of a submitted written response, or why there
|
|
2444
|
+
* is none to make.
|
|
2445
|
+
*
|
|
2446
|
+
* Grading happens on the server, from the whole item: the rubric is what a
|
|
2447
|
+
* model is held to, so an item without one — or one whose criteria are not each
|
|
2448
|
+
* a name and a weight — gives no request, nor does a `redact()` projection or
|
|
2449
|
+
* an empty response. A criterion's `levels`, when there are any, must be ones
|
|
2450
|
+
* the grading functions can read; levels they cannot read give no request
|
|
2451
|
+
* rather than a grade on a scale the author did not mean. `group` is the item
|
|
2452
|
+
* group the task sits in: its material goes with the request as `material`, as
|
|
2453
|
+
* on feedback on writing.
|
|
2454
|
+
*
|
|
2455
|
+
* The author's AI switch (`ai.explanations`) is not read: grading is the
|
|
2456
|
+
* deployment's decision, made on the server, not help a learner asks for.
|
|
2457
|
+
*/
|
|
2458
|
+
function aiGradingRequest(input) {
|
|
2459
|
+
const { data, response, learnerLocale } = input;
|
|
2460
|
+
if (data.type !== "written-response") return {
|
|
2461
|
+
ok: false,
|
|
2462
|
+
problem: "not-written-response"
|
|
2463
|
+
};
|
|
2464
|
+
if (data.redacted === true) return {
|
|
2465
|
+
ok: false,
|
|
2466
|
+
problem: "redacted"
|
|
2467
|
+
};
|
|
2468
|
+
const item = data;
|
|
2469
|
+
const rubric = rubricFacts(item.rubric);
|
|
2470
|
+
if (!rubric.ok) return rubric;
|
|
2471
|
+
const text = response?.type === "written-response" ? response.text : void 0;
|
|
2472
|
+
if (typeof text !== "string" || text.trim() === "") return {
|
|
2473
|
+
ok: false,
|
|
2474
|
+
problem: "empty-response"
|
|
2475
|
+
};
|
|
2476
|
+
const wordCount = require_registry.countWords(text);
|
|
2477
|
+
const minWords = typeof item.minWords === "number" ? item.minWords : 0;
|
|
2478
|
+
const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
|
|
2479
|
+
const material = materialFacts(input.group);
|
|
2480
|
+
return {
|
|
2481
|
+
ok: true,
|
|
2482
|
+
request: {
|
|
2483
|
+
feature: "grading",
|
|
2484
|
+
facts: {
|
|
2485
|
+
activityType: "written-response",
|
|
2486
|
+
activityId: data.id,
|
|
2487
|
+
title: data.title,
|
|
2488
|
+
...typeof data.locale === "string" ? { locale: data.locale } : {},
|
|
2489
|
+
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
|
2490
|
+
text,
|
|
2491
|
+
wordCount,
|
|
2492
|
+
minWords,
|
|
2493
|
+
maxWords,
|
|
2494
|
+
withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
|
|
2495
|
+
rubric: rubric.criteria,
|
|
2496
|
+
...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
|
|
2497
|
+
},
|
|
2498
|
+
...material !== null ? { material } : {},
|
|
2499
|
+
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
/** The rubric as a model is given it, each criterion's levels read. */
|
|
2504
|
+
function rubricFacts(rubric) {
|
|
2505
|
+
const listed = isRecord(rubric) ? rubric.criteria : void 0;
|
|
2506
|
+
if (!Array.isArray(listed) || listed.length === 0) return {
|
|
2507
|
+
ok: false,
|
|
2508
|
+
problem: "no-rubric"
|
|
2509
|
+
};
|
|
2510
|
+
const criteria = [];
|
|
2511
|
+
const names = /* @__PURE__ */ new Set();
|
|
2512
|
+
for (const [index, criterion] of listed.entries()) {
|
|
2513
|
+
if (!isRecord(criterion)) return {
|
|
2514
|
+
ok: false,
|
|
2515
|
+
problem: "no-rubric",
|
|
2516
|
+
path: [
|
|
2517
|
+
"rubric",
|
|
2518
|
+
"criteria",
|
|
2519
|
+
index
|
|
2520
|
+
]
|
|
2521
|
+
};
|
|
2522
|
+
const { name, description, weight } = criterion;
|
|
2523
|
+
if (typeof name !== "string" || name.trim() === "" || typeof weight !== "number") return {
|
|
2524
|
+
ok: false,
|
|
2525
|
+
problem: "no-rubric",
|
|
2526
|
+
path: [
|
|
2527
|
+
"rubric",
|
|
2528
|
+
"criteria",
|
|
2529
|
+
index
|
|
2530
|
+
]
|
|
2531
|
+
};
|
|
2532
|
+
if (names.has(require_registry.levelKey(name))) return {
|
|
2533
|
+
ok: false,
|
|
2534
|
+
problem: "duplicate-criteria",
|
|
2535
|
+
path: [
|
|
2536
|
+
"rubric",
|
|
2537
|
+
"criteria",
|
|
2538
|
+
index,
|
|
2539
|
+
"name"
|
|
2540
|
+
]
|
|
2541
|
+
};
|
|
2542
|
+
names.add(require_registry.levelKey(name));
|
|
2543
|
+
const levels = require_registry.readRubricLevels(criterion.levels);
|
|
2544
|
+
if (!levels.ok) return {
|
|
2545
|
+
ok: false,
|
|
2546
|
+
problem: "unreadable-levels",
|
|
2547
|
+
path: [
|
|
2548
|
+
"rubric",
|
|
2549
|
+
"criteria",
|
|
2550
|
+
index,
|
|
2551
|
+
"levels"
|
|
2552
|
+
]
|
|
2553
|
+
};
|
|
2554
|
+
criteria.push({
|
|
2555
|
+
name,
|
|
2556
|
+
...typeof description === "string" ? { description } : {},
|
|
2557
|
+
weight,
|
|
2558
|
+
levels: levels.levels
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
return {
|
|
2562
|
+
ok: true,
|
|
2563
|
+
criteria
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* The rubric's criteria by name, folded as level labels are: composed, trimmed,
|
|
2568
|
+
* spaced once, lower-cased. A model that writes "task achievement" for "Task
|
|
2569
|
+
* achievement" judged that criterion. The request refused a rubric with two
|
|
2570
|
+
* names alike once folded, so a folded name is one criterion's alone.
|
|
2571
|
+
*/
|
|
2572
|
+
function criterionIndex(rubric) {
|
|
2573
|
+
const folded = new Map(rubric.map((criterion, index) => [require_registry.levelKey(criterion.name), index]));
|
|
2574
|
+
return (name) => folded.get(require_registry.levelKey(name));
|
|
2575
|
+
}
|
|
2576
|
+
/** A finite number. */
|
|
2577
|
+
const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
2578
|
+
/**
|
|
2579
|
+
* One criterion's judgement, on that criterion's scale: a level it has, with
|
|
2580
|
+
* any probabilities a distribution over its levels whose likeliest is the
|
|
2581
|
+
* level chosen; or, without levels, a score from 0 to its `maxScore` (1 by
|
|
2582
|
+
* default).
|
|
2583
|
+
*/
|
|
2584
|
+
function judgeCriterion(entry, criterion) {
|
|
2585
|
+
const { level, score, maxScore, probabilities } = entry;
|
|
2586
|
+
const levels = criterion.levels;
|
|
2587
|
+
if (levels === null) {
|
|
2588
|
+
if (level !== void 0 || probabilities !== void 0) return {
|
|
2589
|
+
ok: false,
|
|
2590
|
+
refusal: "out-of-scale"
|
|
2591
|
+
};
|
|
2592
|
+
if (score === void 0) return {
|
|
2593
|
+
ok: false,
|
|
2594
|
+
refusal: "misses-criteria"
|
|
2595
|
+
};
|
|
2596
|
+
if (!isNumber(score) || maxScore !== void 0 && !isNumber(maxScore)) return {
|
|
2597
|
+
ok: false,
|
|
2598
|
+
refusal: "malformed"
|
|
2599
|
+
};
|
|
2600
|
+
const max = maxScore ?? 1;
|
|
2601
|
+
if (!(max > 0) || score < 0 || score - max > max * 1e-9) return {
|
|
2602
|
+
ok: false,
|
|
2603
|
+
refusal: "out-of-scale"
|
|
2604
|
+
};
|
|
2605
|
+
return {
|
|
2606
|
+
ok: true,
|
|
2607
|
+
judged: {
|
|
2608
|
+
name: criterion.name,
|
|
2609
|
+
score: Math.min(1, score / max)
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
if (score !== void 0 || maxScore !== void 0) return {
|
|
2614
|
+
ok: false,
|
|
2615
|
+
refusal: "out-of-scale"
|
|
2616
|
+
};
|
|
2617
|
+
if (level === void 0) return {
|
|
2618
|
+
ok: false,
|
|
2619
|
+
refusal: "misses-criteria"
|
|
2620
|
+
};
|
|
2621
|
+
if (typeof level !== "string") return {
|
|
2622
|
+
ok: false,
|
|
2623
|
+
refusal: "malformed"
|
|
2624
|
+
};
|
|
2625
|
+
const chosen = levels.findIndex((one) => require_registry.levelKey(one.label) === require_registry.levelKey(level));
|
|
2626
|
+
if (chosen === -1) return {
|
|
2627
|
+
ok: false,
|
|
2628
|
+
refusal: "out-of-scale"
|
|
2629
|
+
};
|
|
2630
|
+
const judged = {
|
|
2631
|
+
name: criterion.name,
|
|
2632
|
+
level: levels[chosen].label,
|
|
2633
|
+
score: chosen / (levels.length - 1)
|
|
2634
|
+
};
|
|
2635
|
+
if (probabilities === void 0) return {
|
|
2636
|
+
ok: true,
|
|
2637
|
+
judged
|
|
2638
|
+
};
|
|
2639
|
+
if (!Array.isArray(probabilities)) return {
|
|
2640
|
+
ok: false,
|
|
2641
|
+
refusal: "malformed"
|
|
2642
|
+
};
|
|
2643
|
+
const values = require_registry.levelDistribution(probabilities, levels.length, chosen);
|
|
2644
|
+
if (values === null) return {
|
|
2645
|
+
ok: false,
|
|
2646
|
+
refusal: "out-of-scale"
|
|
2647
|
+
};
|
|
2648
|
+
return {
|
|
2649
|
+
ok: true,
|
|
2650
|
+
judged: {
|
|
2651
|
+
...judged,
|
|
2652
|
+
probabilities: values
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Checks a grade a port returned for `request`. Whole or nothing, as every AI
|
|
2658
|
+
* text is: a reply the SDK doubts in any part is refused, and is no grade —
|
|
2659
|
+
* re-ask the model or send the response to a person; never record a 0.
|
|
2660
|
+
*
|
|
2661
|
+
* - **Every criterion of the rubric, once** (`misses-criteria`). Names match as
|
|
2662
|
+
* written, or folded when the folded name is one criterion's alone. Leaving
|
|
2663
|
+
* one out, inventing or renaming one, judging one twice or calling one not
|
|
2664
|
+
* applicable refuses the reply: a total renormalised over fewer criteria
|
|
2665
|
+
* is a grade of a different task.
|
|
2666
|
+
* - **On each criterion's own scale** (`out-of-scale`). With levels: a `level`
|
|
2667
|
+
* the criterion has, as labelled (case and spacing aside), and any
|
|
2668
|
+
* `probabilities` a distribution over its levels, lowest first, whose
|
|
2669
|
+
* likeliest is the level chosen. Without levels: a `score` from 0 to its
|
|
2670
|
+
* `maxScore`, 1 by default.
|
|
2671
|
+
* - **Every correction quotes the response** (`misquotes-answer`), anchored as
|
|
2672
|
+
* feedback on writing anchors it.
|
|
2673
|
+
* - **Text fields** — a criterion's `comment` (at most
|
|
2674
|
+
* `AI_WRITING_MAX_FIELD_LENGTH` code points), `feedback` and `rationale`
|
|
2675
|
+
* (at most `AI_TEXT_MAX_LENGTH`) — are plain text, cleaned; longer is
|
|
2676
|
+
* `too-long`. `requiresHumanReview` is a boolean.
|
|
2677
|
+
*
|
|
2678
|
+
* The total is not computed here: `gradeFromJudgement` computes it, as
|
|
2679
|
+
* `gradeFromRubric` computes every grade in the SDK.
|
|
2680
|
+
*/
|
|
2681
|
+
function checkAiGrading(raw, request) {
|
|
2682
|
+
if (!isRecord(raw)) return {
|
|
2683
|
+
ok: false,
|
|
2684
|
+
refusal: "malformed"
|
|
2685
|
+
};
|
|
2686
|
+
const { criteria: listed, feedback, corrections, rationale, requiresHumanReview } = raw;
|
|
2687
|
+
if (listed === void 0) return {
|
|
2688
|
+
ok: false,
|
|
2689
|
+
refusal: "misses-criteria"
|
|
2690
|
+
};
|
|
2691
|
+
if (!Array.isArray(listed)) return {
|
|
2692
|
+
ok: false,
|
|
2693
|
+
refusal: "malformed"
|
|
2694
|
+
};
|
|
2695
|
+
const rubric = request.facts.rubric;
|
|
2696
|
+
const indexOf = criterionIndex(rubric);
|
|
2697
|
+
const judged = rubric.map(() => void 0);
|
|
2698
|
+
for (const entry of listed) {
|
|
2699
|
+
if (!isRecord(entry)) return {
|
|
2700
|
+
ok: false,
|
|
2701
|
+
refusal: "malformed"
|
|
2702
|
+
};
|
|
2703
|
+
const name = entry.name;
|
|
2704
|
+
const notApplicable = entry.notApplicable;
|
|
2705
|
+
const written = entry.comment;
|
|
2706
|
+
const scale = {
|
|
2707
|
+
level: entry.level,
|
|
2708
|
+
score: entry.score,
|
|
2709
|
+
maxScore: entry.maxScore,
|
|
2710
|
+
probabilities: entry.probabilities
|
|
2711
|
+
};
|
|
2712
|
+
if (typeof name !== "string" || notApplicable !== void 0 && typeof notApplicable !== "boolean") return {
|
|
2713
|
+
ok: false,
|
|
2714
|
+
refusal: "malformed"
|
|
2715
|
+
};
|
|
2716
|
+
const index = indexOf(name);
|
|
2717
|
+
if (index === void 0 || judged[index] !== void 0 || notApplicable === true) return {
|
|
2718
|
+
ok: false,
|
|
2719
|
+
refusal: "misses-criteria"
|
|
2720
|
+
};
|
|
2721
|
+
const criterion = rubric[index];
|
|
2722
|
+
const scaled = judgeCriterion(scale, criterion);
|
|
2723
|
+
if (!scaled.ok) return scaled;
|
|
2724
|
+
const comment = textField(written, 500);
|
|
2725
|
+
if (!comment.ok) return comment;
|
|
2726
|
+
judged[index] = {
|
|
2727
|
+
...scaled.judged,
|
|
2728
|
+
...comment.value !== void 0 ? { comment: comment.value } : {}
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
if (judged.some((one) => one === void 0)) return {
|
|
2732
|
+
ok: false,
|
|
2733
|
+
refusal: "misses-criteria"
|
|
2734
|
+
};
|
|
2735
|
+
const anchored = checkCorrections(corrections, request.facts.text);
|
|
2736
|
+
if (!anchored.ok) return anchored;
|
|
2737
|
+
const said = textField(feedback, AI_TEXT_MAX_LENGTH);
|
|
2738
|
+
if (!said.ok) return said;
|
|
2739
|
+
const why = textField(rationale, AI_TEXT_MAX_LENGTH);
|
|
2740
|
+
if (!why.ok) return why;
|
|
2741
|
+
if (requiresHumanReview !== void 0 && typeof requiresHumanReview !== "boolean") return {
|
|
2742
|
+
ok: false,
|
|
2743
|
+
refusal: "malformed"
|
|
2744
|
+
};
|
|
2745
|
+
const provenance = provenanceOf(raw.provenance);
|
|
2746
|
+
const usage = usageOf(raw.usage);
|
|
2747
|
+
return {
|
|
2748
|
+
ok: true,
|
|
2749
|
+
judgement: {
|
|
2750
|
+
criteria: judged,
|
|
2751
|
+
feedback: said.value ?? null,
|
|
2752
|
+
corrections: anchored.corrections,
|
|
2753
|
+
...why.value !== void 0 ? { rationale: why.value } : {},
|
|
2754
|
+
requiresHumanReview: requiresHumanReview === true,
|
|
2755
|
+
...provenance !== void 0 ? { provenance } : {},
|
|
2756
|
+
...usage !== void 0 ? { usage } : {}
|
|
2757
|
+
}
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
//#endregion
|
|
2761
|
+
//#region src/ai-writing.ts
|
|
2762
|
+
/**
|
|
2763
|
+
* The request for feedback on a draft, or `null` when there is none to give:
|
|
2764
|
+
* not a written response, an author who switched explanations off (writing
|
|
2765
|
+
* feedback reads `ai.explanations`), a redacted item — feedback is for
|
|
2766
|
+
* practice, where the item is whole — or a draft with nothing in it.
|
|
2767
|
+
*
|
|
2768
|
+
* The facts are the task as the learner saw it, the draft verbatim with its
|
|
2769
|
+
* word count recomputed as the grader will count it, and the rubric the answer
|
|
2770
|
+
* will be graded on. `previousFeedback` is the text of the feedback already
|
|
2771
|
+
* shown on earlier drafts, so a model can speak to what changed. `group` is the
|
|
2772
|
+
* item group the task sits in: its material, the text a summary is of, goes
|
|
2773
|
+
* with the request as `material`.
|
|
2774
|
+
*/
|
|
2775
|
+
function aiWritingFeedbackRequest(input) {
|
|
2776
|
+
const { data, response, learnerLocale } = input;
|
|
2777
|
+
if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) return null;
|
|
2778
|
+
const text = response?.type === "written-response" ? response.text : "";
|
|
2779
|
+
if (typeof text !== "string" || text.trim() === "") return null;
|
|
2780
|
+
const item = data;
|
|
2781
|
+
const wordCount = require_registry.countWords(text);
|
|
2782
|
+
const minWords = typeof item.minWords === "number" ? item.minWords : 0;
|
|
2783
|
+
const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
|
|
2784
|
+
const criteria = Array.isArray(item.rubric?.criteria) ? item.rubric.criteria : null;
|
|
2785
|
+
const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
|
|
2786
|
+
name: criterion.name,
|
|
2787
|
+
...typeof criterion.description === "string" ? { description: criterion.description } : {},
|
|
2788
|
+
weight: criterion.weight
|
|
2789
|
+
}));
|
|
2790
|
+
const previousFeedback = [...input.previousFeedback ?? []];
|
|
2791
|
+
const material = materialFacts(input.group);
|
|
2792
|
+
return {
|
|
2793
|
+
feature: "writing-feedback",
|
|
2794
|
+
facts: {
|
|
2795
|
+
activityType: "written-response",
|
|
2796
|
+
activityId: data.id,
|
|
2797
|
+
title: data.title,
|
|
2798
|
+
...typeof data.locale === "string" ? { locale: data.locale } : {},
|
|
2799
|
+
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
|
2800
|
+
text,
|
|
2801
|
+
wordCount,
|
|
2802
|
+
minWords,
|
|
2803
|
+
maxWords,
|
|
2804
|
+
withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
|
|
2805
|
+
rubric,
|
|
2806
|
+
...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
|
|
2807
|
+
},
|
|
2808
|
+
draftNumber: previousFeedback.length + 1,
|
|
2809
|
+
previousFeedback,
|
|
2810
|
+
...material !== null ? { material } : {},
|
|
2811
|
+
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2481
2814
|
/** A criterion score a grade could be computed from: finite, 0 or more, and not above its maximum. */
|
|
2482
2815
|
function isJudgedScore(score, maxScore) {
|
|
2483
2816
|
if (score === void 0) return maxScore === void 0 || typeof maxScore === "number" && maxScore > 0;
|
|
@@ -2524,7 +2857,7 @@ function checkCriteria(raw, rubric) {
|
|
|
2524
2857
|
ok: false,
|
|
2525
2858
|
refusal: "malformed"
|
|
2526
2859
|
};
|
|
2527
|
-
const band = textField(entry.band,
|
|
2860
|
+
const band = textField(entry.band, 40);
|
|
2528
2861
|
const comment = textField(entry.comment, 500);
|
|
2529
2862
|
if (!band.ok) return band;
|
|
2530
2863
|
if (!comment.ok) return comment;
|
|
@@ -2638,6 +2971,12 @@ Object.defineProperty(exports, "AI_EXPLANATION_TYPES", {
|
|
|
2638
2971
|
return AI_EXPLANATION_TYPES;
|
|
2639
2972
|
}
|
|
2640
2973
|
});
|
|
2974
|
+
Object.defineProperty(exports, "AI_GRADING_PROBABILITY_TOLERANCE", {
|
|
2975
|
+
enumerable: true,
|
|
2976
|
+
get: function() {
|
|
2977
|
+
return AI_GRADING_PROBABILITY_TOLERANCE;
|
|
2978
|
+
}
|
|
2979
|
+
});
|
|
2641
2980
|
Object.defineProperty(exports, "AI_HINT_TYPES", {
|
|
2642
2981
|
enumerable: true,
|
|
2643
2982
|
get: function() {
|
|
@@ -2710,6 +3049,12 @@ Object.defineProperty(exports, "aiGradeOf", {
|
|
|
2710
3049
|
return aiGradeOf;
|
|
2711
3050
|
}
|
|
2712
3051
|
});
|
|
3052
|
+
Object.defineProperty(exports, "aiGradingRequest", {
|
|
3053
|
+
enumerable: true,
|
|
3054
|
+
get: function() {
|
|
3055
|
+
return aiGradingRequest;
|
|
3056
|
+
}
|
|
3057
|
+
});
|
|
2713
3058
|
Object.defineProperty(exports, "aiHintRequest", {
|
|
2714
3059
|
enumerable: true,
|
|
2715
3060
|
get: function() {
|
|
@@ -2758,6 +3103,12 @@ Object.defineProperty(exports, "checkAiExplanation", {
|
|
|
2758
3103
|
return checkAiExplanation;
|
|
2759
3104
|
}
|
|
2760
3105
|
});
|
|
3106
|
+
Object.defineProperty(exports, "checkAiGrading", {
|
|
3107
|
+
enumerable: true,
|
|
3108
|
+
get: function() {
|
|
3109
|
+
return checkAiGrading;
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
2761
3112
|
Object.defineProperty(exports, "checkAiHint", {
|
|
2762
3113
|
enumerable: true,
|
|
2763
3114
|
get: function() {
|
|
@@ -2843,4 +3194,4 @@ Object.defineProperty(exports, "validateItemGroupDraft", {
|
|
|
2843
3194
|
}
|
|
2844
3195
|
});
|
|
2845
3196
|
|
|
2846
|
-
//# sourceMappingURL=ai-writing-
|
|
3197
|
+
//# sourceMappingURL=ai-writing-Cen7-3qs.cjs.map
|