@intellectif/lk-core 1.3.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 +59 -0
- package/README.md +2 -0
- package/dist/{ai-B222dtHH.d.ts → ai-BYryFkF9.d.cts} +462 -5
- package/dist/ai-BYryFkF9.d.cts.map +1 -0
- package/dist/{ai-B222dtHH.d.cts → ai-BYryFkF9.d.ts} +462 -5
- package/dist/ai-BYryFkF9.d.ts.map +1 -0
- package/dist/ai-check.cjs +274 -23
- 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 +274 -23
- package/dist/ai-check.js.map +1 -1
- package/dist/{ai-writing-DCVkv4Zj.cjs → ai-writing-Cen7-3qs.cjs} +520 -58
- package/dist/ai-writing-Cen7-3qs.cjs.map +1 -0
- package/dist/{ai-writing-CCBjTOM3.js → ai-writing-D8PdhJJz.js} +513 -75
- package/dist/ai-writing-D8PdhJJz.js.map +1 -0
- package/dist/{index-Dyu66R4-.d.cts → index-Bg0aeWtM.d.cts} +327 -3
- package/dist/index-Bg0aeWtM.d.cts.map +1 -0
- package/dist/{index-BWPRPIXL.d.ts → index-CxpiWedW.d.ts} +7 -217
- package/dist/index-CxpiWedW.d.ts.map +1 -0
- package/dist/{index-CXyOV8sm.d.cts → index-D7R3Ll8R.d.cts} +2 -2
- package/dist/{index-CXyOV8sm.d.cts.map → index-D7R3Ll8R.d.cts.map} +1 -1
- package/dist/{index-UGxRSopE.d.ts → index-DUaW1Tj5.d.ts} +327 -3
- package/dist/index-DUaW1Tj5.d.ts.map +1 -0
- package/dist/{index-O4_ExlAD.d.cts → index-qMbxed1B.d.cts} +7 -217
- package/dist/index-qMbxed1B.d.cts.map +1 -0
- package/dist/{index-CegM8o15.d.ts → index-wMc1E_aM.d.ts} +2 -2
- package/dist/{index-CegM8o15.d.ts.map → index-wMc1E_aM.d.ts.map} +1 -1
- package/dist/index.cjs +32 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -97
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +115 -97
- 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-B222dtHH.d.cts.map +0 -1
- package/dist/ai-B222dtHH.d.ts.map +0 -1
- package/dist/ai-writing-CCBjTOM3.js.map +0 -1
- package/dist/ai-writing-DCVkv4Zj.cjs.map +0 -1
- package/dist/index-BWPRPIXL.d.ts.map +0 -1
- package/dist/index-Dyu66R4-.d.cts.map +0 -1
- package/dist/index-O4_ExlAD.d.cts.map +0 -1
- package/dist/index-UGxRSopE.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,95 @@
|
|
|
1
|
-
import { B as
|
|
2
|
-
import {
|
|
3
|
-
import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-
|
|
1
|
+
import { B as covers, F as readRubricLevels, G as issue, H as isRecord$5, I as countWords, K as pathKey, L as DRAFT_ISSUE_SEVERITY, M as LEVEL_PROBABILITY_TOLERANCE, N as levelDistribution, O as keyPositionFinding, P as levelKey, R as checkMedia, U as isUnset, V as isMissingId, W as isUnwritten, X as groupCaptionsField, Y as GROUP_CAPTIONS_REVEAL_DICTATION, _t as fold, a as readSchema, bt as revealsAnswer, et as UnknownActivityTypeError, ft as withValidationScope, gt as containsWords, k as normaliseFinding, nt as alignDictation, q as refusesEmpty, t as getActivityTypeDescriptor, vt as numberedPassage, yt as placeholderOrder, z as coveredPathsOf } from "./registry-DaZRpqjW.js";
|
|
2
|
+
import { F as isGradeInRange, _ as gradeFromRubric, c as validateSpeechAssessment, r as score, s as alignReadAloud } from "./scoring-BKNNTz-s.js";
|
|
3
|
+
import { a as INTERACTIVE_VIDEO_ITEM_TYPES, i as validateItemGroup, t as ItemGroupSchema, u as isInteractiveVideoItemType } from "./item-group-DdSUCUjM.js";
|
|
4
|
+
//#region src/ai-material.ts
|
|
5
|
+
/**
|
|
6
|
+
* What a model is told about the group a question sits in: the material the
|
|
7
|
+
* learner reads and sees beside it.
|
|
8
|
+
*
|
|
9
|
+
* Internal: the request builders in `ai.ts` and `ai-writing.ts` are its public
|
|
10
|
+
* face, through their `group` input.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* The longest material text a model is given, in code points. A longer one is
|
|
14
|
+
* cut and marked `truncated`: a long reading passage on an exam paper runs to
|
|
15
|
+
* 4,000–7,000.
|
|
16
|
+
*/
|
|
17
|
+
const AI_MATERIAL_MAX_LENGTH = 12e3;
|
|
18
|
+
const KINDS$2 = {
|
|
19
|
+
text: true,
|
|
20
|
+
audio: true,
|
|
21
|
+
video: true,
|
|
22
|
+
image: true,
|
|
23
|
+
mixed: true
|
|
24
|
+
};
|
|
25
|
+
const MEDIA_TYPES = {
|
|
26
|
+
image: true,
|
|
27
|
+
audio: true,
|
|
28
|
+
video: true,
|
|
29
|
+
embed: true
|
|
30
|
+
};
|
|
31
|
+
const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
+
/** A string with something in it, as written; otherwise nothing. */
|
|
33
|
+
const filled = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
34
|
+
/**
|
|
35
|
+
* `text` cut to {@link AI_MATERIAL_MAX_LENGTH} code points. The cut falls at
|
|
36
|
+
* the last space in its second half, so no word is broken; in a script written
|
|
37
|
+
* without spaces, at the limit itself.
|
|
38
|
+
*/
|
|
39
|
+
function cut(text) {
|
|
40
|
+
const points = [...text];
|
|
41
|
+
if (points.length <= 12e3) return {
|
|
42
|
+
text,
|
|
43
|
+
truncated: false
|
|
44
|
+
};
|
|
45
|
+
let end = AI_MATERIAL_MAX_LENGTH;
|
|
46
|
+
for (let at = AI_MATERIAL_MAX_LENGTH; at >= AI_MATERIAL_MAX_LENGTH / 2; at -= 1) if (/\s/u.test(points[at])) {
|
|
47
|
+
end = at;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
text: points.slice(0, end).join("").trimEnd(),
|
|
52
|
+
truncated: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The facts of `group`'s material, or `null` without a group or a stimulus it
|
|
57
|
+
* can read: not an object, or of a kind the SDK does not know.
|
|
58
|
+
*
|
|
59
|
+
* Only what the learner sees reaches a model. The `transcript` is author-only
|
|
60
|
+
* and never read, so the facts are the same for a whole group and a redacted
|
|
61
|
+
* one; a media address is never read either, since a model cannot open one
|
|
62
|
+
* and a signed address must not end up in a prompt. Each field is read once.
|
|
63
|
+
*/
|
|
64
|
+
function materialFacts(group) {
|
|
65
|
+
if (!isRecord$4(group)) return null;
|
|
66
|
+
const stimulus = group.stimulus;
|
|
67
|
+
if (!isRecord$4(stimulus)) return null;
|
|
68
|
+
const kind = stimulus.kind;
|
|
69
|
+
if (typeof kind !== "string" || !Object.hasOwn(KINDS$2, kind)) return null;
|
|
70
|
+
const title = filled(stimulus.title) ?? filled(group.title);
|
|
71
|
+
const body = filled(stimulus.body);
|
|
72
|
+
const text = body === void 0 ? void 0 : cut(body);
|
|
73
|
+
const rawMedia = stimulus.media;
|
|
74
|
+
const mediaType = isRecord$4(rawMedia) ? rawMedia.type : void 0;
|
|
75
|
+
const alt = isRecord$4(rawMedia) ? filled(rawMedia.alt) : void 0;
|
|
76
|
+
const media = typeof mediaType === "string" && Object.hasOwn(MEDIA_TYPES, mediaType) ? {
|
|
77
|
+
type: mediaType,
|
|
78
|
+
...alt !== void 0 ? { alt } : {}
|
|
79
|
+
} : void 0;
|
|
80
|
+
const attribution = filled(stimulus.attribution);
|
|
81
|
+
const locale = filled(stimulus.locale);
|
|
82
|
+
return {
|
|
83
|
+
kind,
|
|
84
|
+
...title !== void 0 ? { title } : {},
|
|
85
|
+
...text !== void 0 ? { text: text.text } : {},
|
|
86
|
+
...text?.truncated === true ? { truncated: true } : {},
|
|
87
|
+
...media !== void 0 ? { media } : {},
|
|
88
|
+
...attribution !== void 0 ? { attribution } : {},
|
|
89
|
+
...locale !== void 0 ? { locale } : {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
4
93
|
//#region src/ai-text.ts
|
|
5
94
|
/**
|
|
6
95
|
* How the SDK reads text a model wrote, before a learner does. Internal: shared
|
|
@@ -341,6 +430,10 @@ function gradeLocally(data, response) {
|
|
|
341
430
|
* positive and finite, `score` finite from 0 to it) gets no explanation at all:
|
|
342
431
|
* explaining 85 "out of 1" would tell the model a wrong answer was correct, and
|
|
343
432
|
* grading locally instead would speak to a grade the screen does not show.
|
|
433
|
+
*
|
|
434
|
+
* `group` is the item group the question sits in: its material — the passage,
|
|
435
|
+
* or the instructions the group's questions share — goes with the request as
|
|
436
|
+
* `material`, as the learner sees it.
|
|
344
437
|
*/
|
|
345
438
|
function aiExplanationRequest(input) {
|
|
346
439
|
const { data, response, outcome, learnerLocale } = input;
|
|
@@ -366,10 +459,12 @@ function aiExplanationRequest(input) {
|
|
|
366
459
|
if (grade === null) return null;
|
|
367
460
|
const facts = buildAiFacts(data, response, details);
|
|
368
461
|
if (facts === null) return null;
|
|
462
|
+
const material = materialFacts(input.group);
|
|
369
463
|
return {
|
|
370
464
|
feature: "explanation",
|
|
371
465
|
facts,
|
|
372
466
|
grade,
|
|
467
|
+
...material !== null ? { material } : {},
|
|
373
468
|
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
374
469
|
};
|
|
375
470
|
}
|
|
@@ -380,7 +475,11 @@ function aiExplanationRequest(input) {
|
|
|
380
475
|
*
|
|
381
476
|
* The facts carry the key, and which parts the learner has right so far, so a
|
|
382
477
|
* model can hint where it helps. The learner never sees them; a hint that
|
|
383
|
-
* contains an answer is refused by {@link checkAiHint}
|
|
478
|
+
* contains an answer is refused by {@link checkAiHint}, whatever it quotes —
|
|
479
|
+
* the group's material included.
|
|
480
|
+
*
|
|
481
|
+
* `group` is the item group the question sits in, as for
|
|
482
|
+
* {@link aiExplanationRequest}.
|
|
384
483
|
*/
|
|
385
484
|
function aiHintRequest(input) {
|
|
386
485
|
const { data, previousHints, learnerLocale } = input;
|
|
@@ -388,11 +487,13 @@ function aiHintRequest(input) {
|
|
|
388
487
|
const response = input.response ?? emptyResponse(data.type);
|
|
389
488
|
const facts = buildAiFacts(data, response, (response === null ? null : gradeLocally(data, response))?.details ?? null);
|
|
390
489
|
if (facts === null) return null;
|
|
490
|
+
const material = materialFacts(input.group);
|
|
391
491
|
return {
|
|
392
492
|
feature: "hint",
|
|
393
493
|
facts,
|
|
394
494
|
hintNumber: previousHints.length + 1,
|
|
395
495
|
previousHints: [...previousHints],
|
|
496
|
+
...material !== null ? { material } : {},
|
|
396
497
|
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
397
498
|
};
|
|
398
499
|
}
|
|
@@ -860,7 +961,7 @@ function createInteractiveVideoDraft(context) {
|
|
|
860
961
|
* {@link validateItemGroupDraft} already reports it.
|
|
861
962
|
*/
|
|
862
963
|
function critiqueItemGroupDraft(draft) {
|
|
863
|
-
if (!isRecord$
|
|
964
|
+
if (!isRecord$5(draft)) return [];
|
|
864
965
|
const items = Array.isArray(draft.items) ? draft.items : [];
|
|
865
966
|
return withValidationScope(() => [...items.flatMap((item, index) => repathed(critiqueItem(item), ["items", index])), ...keyPositionFinding(items, ["items"])]);
|
|
866
967
|
}
|
|
@@ -872,11 +973,11 @@ function critiqueItemGroupDraft(draft) {
|
|
|
872
973
|
* skipped.
|
|
873
974
|
*/
|
|
874
975
|
function critiqueDrafts(entries) {
|
|
875
|
-
return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$
|
|
976
|
+
return withValidationScope(() => [...entries.flatMap((entry, index) => repathed(isRecord$5(entry) && entry.type === "item-group" ? critiqueItemGroupDraft(entry) : critiqueItem(entry), [index])), ...keyPositionFinding(entries, [])]);
|
|
876
977
|
}
|
|
877
978
|
/** One item's findings, or none for an item the critic cannot place. */
|
|
878
979
|
function critiqueItem(item) {
|
|
879
|
-
if (!isRecord$
|
|
980
|
+
if (!isRecord$5(item) || typeof item.type !== "string") return [];
|
|
880
981
|
if (getActivityTypeDescriptor(item.type) === void 0) return [];
|
|
881
982
|
return critiqueDraft(item.type, item);
|
|
882
983
|
}
|
|
@@ -889,7 +990,7 @@ function validateItemGroupDraft(draft) {
|
|
|
889
990
|
}
|
|
890
991
|
function validateItemGroupDraftInScope(draft) {
|
|
891
992
|
const issues = [];
|
|
892
|
-
if (!isRecord$
|
|
993
|
+
if (!isRecord$5(draft)) {
|
|
893
994
|
issues.push(issue("ig_not_an_object", [], "A group must be an object with a stimulus and its items."));
|
|
894
995
|
return {
|
|
895
996
|
status: "invalid",
|
|
@@ -897,7 +998,7 @@ function validateItemGroupDraftInScope(draft) {
|
|
|
897
998
|
};
|
|
898
999
|
}
|
|
899
1000
|
issues.push(...checkGroupIdentity(draft));
|
|
900
|
-
if (isRecord$
|
|
1001
|
+
if (isRecord$5(draft.stimulus)) issues.push(...checkStimulus(draft.stimulus));
|
|
901
1002
|
else if (isUnset(draft.stimulus)) issues.push(issue("ig_stimulus_required", ["stimulus"], "Add the passage, recording or image the questions are about."));
|
|
902
1003
|
issues.push(...checkItems(draft));
|
|
903
1004
|
if (draft.timeline !== void 0 && draft.timeline !== null) issues.push(...checkTimeline(draft));
|
|
@@ -947,10 +1048,10 @@ function validateItemGroupDraftInScope(draft) {
|
|
|
947
1048
|
function checkTimeline(draft) {
|
|
948
1049
|
const issues = [];
|
|
949
1050
|
const timeline = draft.timeline;
|
|
950
|
-
if (!isRecord$
|
|
951
|
-
const stimulus = isRecord$
|
|
1051
|
+
if (!isRecord$5(timeline)) return issues;
|
|
1052
|
+
const stimulus = isRecord$5(draft.stimulus) ? draft.stimulus : void 0;
|
|
952
1053
|
if (stimulus !== void 0 && !isUnwritten(stimulus.kind) && stimulus.kind !== "video") issues.push(issue("ig_timeline_stimulus_kind", ["stimulus", "kind"], "An interactive video needs a video: quizzes open at moments of it."));
|
|
953
|
-
else if (stimulus !== void 0 && isRecord$
|
|
1054
|
+
else if (stimulus !== void 0 && isRecord$5(stimulus.media) && !isUnwritten(stimulus.media.type) && stimulus.media.type !== "video") issues.push(issue("ig_timeline_stimulus_kind", [
|
|
954
1055
|
"stimulus",
|
|
955
1056
|
"media",
|
|
956
1057
|
"type"
|
|
@@ -959,7 +1060,7 @@ function checkTimeline(draft) {
|
|
|
959
1060
|
const items = Array.isArray(draft.items) ? draft.items : [];
|
|
960
1061
|
const itemIndexById = /* @__PURE__ */ new Map();
|
|
961
1062
|
items.forEach((item, index) => {
|
|
962
|
-
if (!isRecord$
|
|
1063
|
+
if (!isRecord$5(item)) return;
|
|
963
1064
|
if (typeof item.id === "string" && item.id !== "") itemIndexById.set(item.id, index);
|
|
964
1065
|
const type = item.type;
|
|
965
1066
|
if (typeof type === "string" && type !== "" && type !== "item-group" && !isInteractiveVideoItemType(type) && getActivityTypeDescriptor(type) !== void 0) issues.push(issue("ig_timeline_item_type", [
|
|
@@ -977,7 +1078,7 @@ function checkTimeline(draft) {
|
|
|
977
1078
|
const quizIds = /* @__PURE__ */ new Set();
|
|
978
1079
|
const placed = /* @__PURE__ */ new Set();
|
|
979
1080
|
cues.forEach((cue, cueIndex) => {
|
|
980
|
-
if (!isRecord$
|
|
1081
|
+
if (!isRecord$5(cue)) return;
|
|
981
1082
|
const at = (field, ...rest) => [
|
|
982
1083
|
"timeline",
|
|
983
1084
|
"cues",
|
|
@@ -1008,7 +1109,7 @@ function checkTimeline(draft) {
|
|
|
1008
1109
|
const chapters = Array.isArray(timeline.chapters) ? timeline.chapters : [];
|
|
1009
1110
|
let previousAt;
|
|
1010
1111
|
chapters.forEach((chapter, index) => {
|
|
1011
|
-
if (!isRecord$
|
|
1112
|
+
if (!isRecord$5(chapter)) return;
|
|
1012
1113
|
if (isUnwritten(chapter.title)) issues.push(issue("ig_timeline_chapter_title", [
|
|
1013
1114
|
"timeline",
|
|
1014
1115
|
"chapters",
|
|
@@ -1069,14 +1170,14 @@ function checkStimulus(stimulus) {
|
|
|
1069
1170
|
if (isUnwritten(kind)) issues.push(issue("ig_stimulus_kind_required", at("kind"), "Choose what the questions are about: a text, a recording, a video, an image, or a mix."));
|
|
1070
1171
|
else if (typeof kind !== "string" || !KINDS$1.has(kind)) {
|
|
1071
1172
|
if (typeof kind === "string") issues.push(issue("ig_stimulus_kind_invalid", at("kind"), `"${kind}" is not a stimulus kind. Use text, audio, video, image or mixed.`));
|
|
1072
|
-
if (isRecord$
|
|
1173
|
+
if (isRecord$5(stimulus.media)) issues.push(...checkMedia(stimulus.media, at("media")));
|
|
1073
1174
|
return issues;
|
|
1074
1175
|
}
|
|
1075
1176
|
const hasBody = typeof stimulus.body === "string" && stimulus.body.trim() !== "";
|
|
1076
1177
|
const htmlPresent = !isUnset(stimulus.bodyHtml);
|
|
1077
1178
|
if (!hasBody && (kind === "text" || kind === "mixed" || htmlPresent)) issues.push(issue("ig_stimulus_body_required", at("body"), isUnwritten(stimulus.bodyHtml) ? "Write the passage." : "Write the passage as plain text too. The rich-text version is shown only where a sanitiser is supplied."));
|
|
1078
1179
|
if (kind !== "text" && isUnset(stimulus.media)) issues.push(issue("ig_stimulus_media_required", at("media"), "Add the recording, video or image."));
|
|
1079
|
-
else if (isRecord$
|
|
1180
|
+
else if (isRecord$5(stimulus.media)) {
|
|
1080
1181
|
const allowed = MEDIA_FOR_KIND[kind];
|
|
1081
1182
|
const mediaType = stimulus.media.type;
|
|
1082
1183
|
if (allowed !== void 0 && typeof mediaType === "string" && !allowed.includes(mediaType)) issues.push(issue("ig_stimulus_media_kind", at("media", "type"), `A "${kind}" stimulus cannot carry ${mediaType} media. Use ${allowed.join(" or ")}.`));
|
|
@@ -1103,7 +1204,7 @@ function checkItems(draft) {
|
|
|
1103
1204
|
const seen = /* @__PURE__ */ new Set();
|
|
1104
1205
|
const repeated = /* @__PURE__ */ new Set();
|
|
1105
1206
|
for (const [index, item] of items.entries()) {
|
|
1106
|
-
if (!isRecord$
|
|
1207
|
+
if (!isRecord$5(item)) continue;
|
|
1107
1208
|
const ordinal = index + 1;
|
|
1108
1209
|
if (isMissingId(item.id)) issues.push(issue("ig_item_id_required", [
|
|
1109
1210
|
"items",
|
|
@@ -1200,7 +1301,7 @@ function validateDraft(type, draft) {
|
|
|
1200
1301
|
function validateDraftInScope(type, draft) {
|
|
1201
1302
|
const descriptor = getActivityTypeDescriptor(type);
|
|
1202
1303
|
if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
|
|
1203
|
-
const reported = isRecord$
|
|
1304
|
+
const reported = isRecord$5(draft) && descriptor.authoring?.checkDraft !== void 0 ? descriptor.authoring.checkDraft(draft).map(normalise) : [];
|
|
1204
1305
|
const issues = [...reported];
|
|
1205
1306
|
const parsed = readSchema(descriptor.schema, draft, `Activity type "${descriptor.type}"`, { reportInput: true });
|
|
1206
1307
|
if (!parsed.success) {
|
|
@@ -1310,7 +1411,7 @@ function critiqueDraft(type, draft) {
|
|
|
1310
1411
|
const descriptor = getActivityTypeDescriptor(type);
|
|
1311
1412
|
if (descriptor === void 0) throw new UnknownActivityTypeError(String(type));
|
|
1312
1413
|
const critique = descriptor.authoring?.critique;
|
|
1313
|
-
if (!isRecord$
|
|
1414
|
+
if (!isRecord$5(draft) || critique === void 0) return [];
|
|
1314
1415
|
return withValidationScope(() => critique(draft)).map(normaliseFinding);
|
|
1315
1416
|
}
|
|
1316
1417
|
//#endregion
|
|
@@ -2188,61 +2289,19 @@ function interactiveVideoFromDrafts(input) {
|
|
|
2188
2289
|
};
|
|
2189
2290
|
}
|
|
2190
2291
|
//#endregion
|
|
2191
|
-
//#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
|
+
*/
|
|
2192
2301
|
/** The most corrections one piece of writing feedback may carry. More is refused, not cut. */
|
|
2193
2302
|
const AI_WRITING_MAX_CORRECTIONS = 20;
|
|
2194
2303
|
/** The longest quote, correction, explanation or criterion comment, in code points. */
|
|
2195
2304
|
const AI_WRITING_MAX_FIELD_LENGTH = 500;
|
|
2196
|
-
/** The longest `category` or `band`: a tag, not a sentence. */
|
|
2197
|
-
const MAX_TAG_LENGTH = 40;
|
|
2198
|
-
/**
|
|
2199
|
-
* The request for feedback on a draft, or `null` when there is none to give:
|
|
2200
|
-
* not a written response, an author who switched explanations off (writing
|
|
2201
|
-
* feedback reads `ai.explanations`), a redacted item — feedback is for
|
|
2202
|
-
* practice, where the item is whole — or a draft with nothing in it.
|
|
2203
|
-
*
|
|
2204
|
-
* The facts are the task as the learner saw it, the draft verbatim with its
|
|
2205
|
-
* word count recomputed as the grader will count it, and the rubric the answer
|
|
2206
|
-
* will be graded on. `previousFeedback` is the text of the feedback already
|
|
2207
|
-
* shown on earlier drafts, so a model can speak to what changed.
|
|
2208
|
-
*/
|
|
2209
|
-
function aiWritingFeedbackRequest(input) {
|
|
2210
|
-
const { data, response, learnerLocale } = input;
|
|
2211
|
-
if (!aiSupports(data.type, "writing-feedback") || !aiAllowedByContent(data, "writing-feedback") || data.redacted === true) return null;
|
|
2212
|
-
const text = response?.type === "written-response" ? response.text : "";
|
|
2213
|
-
if (typeof text !== "string" || text.trim() === "") return null;
|
|
2214
|
-
const item = data;
|
|
2215
|
-
const wordCount = countWords(text);
|
|
2216
|
-
const minWords = typeof item.minWords === "number" ? item.minWords : 0;
|
|
2217
|
-
const maxWords = typeof item.maxWords === "number" ? item.maxWords : Number.POSITIVE_INFINITY;
|
|
2218
|
-
const criteria = Array.isArray(item.rubric?.criteria) ? item.rubric.criteria : null;
|
|
2219
|
-
const rubric = criteria === null || criteria.length === 0 ? null : criteria.map((criterion) => ({
|
|
2220
|
-
name: criterion.name,
|
|
2221
|
-
...typeof criterion.description === "string" ? { description: criterion.description } : {},
|
|
2222
|
-
weight: criterion.weight
|
|
2223
|
-
}));
|
|
2224
|
-
const previousFeedback = [...input.previousFeedback ?? []];
|
|
2225
|
-
return {
|
|
2226
|
-
feature: "writing-feedback",
|
|
2227
|
-
facts: {
|
|
2228
|
-
activityType: "written-response",
|
|
2229
|
-
activityId: data.id,
|
|
2230
|
-
title: data.title,
|
|
2231
|
-
...typeof data.locale === "string" ? { locale: data.locale } : {},
|
|
2232
|
-
prompt: typeof item.prompt === "string" ? item.prompt : "",
|
|
2233
|
-
text,
|
|
2234
|
-
wordCount,
|
|
2235
|
-
minWords,
|
|
2236
|
-
maxWords,
|
|
2237
|
-
withinWordBounds: wordCount >= minWords && wordCount <= maxWords,
|
|
2238
|
-
rubric,
|
|
2239
|
-
...typeof item.languageTarget === "string" ? { languageTarget: item.languageTarget } : {}
|
|
2240
|
-
},
|
|
2241
|
-
draftNumber: previousFeedback.length + 1,
|
|
2242
|
-
previousFeedback,
|
|
2243
|
-
...learnerLocale !== void 0 ? { learnerLocale } : {}
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
2305
|
/**
|
|
2247
2306
|
* Where `quote` sits in the draft: at `claimed`, when the model named a range —
|
|
2248
2307
|
* which must then hold the quote — or else at its first occurrence not already
|
|
@@ -2341,7 +2400,7 @@ function checkCorrections(raw, text) {
|
|
|
2341
2400
|
refusal: "malformed"
|
|
2342
2401
|
};
|
|
2343
2402
|
const explanation = textField(entry.explanation, 500);
|
|
2344
|
-
const category = textField(entry.category,
|
|
2403
|
+
const category = textField(entry.category, 40);
|
|
2345
2404
|
if (!explanation.ok) return explanation;
|
|
2346
2405
|
if (!category.ok) return category;
|
|
2347
2406
|
let claimed;
|
|
@@ -2373,6 +2432,385 @@ function checkCorrections(raw, text) {
|
|
|
2373
2432
|
corrections
|
|
2374
2433
|
};
|
|
2375
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 = 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 = 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(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(levelKey(name));
|
|
2543
|
+
const levels = 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) => [levelKey(criterion.name), index]));
|
|
2574
|
+
return (name) => folded.get(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) => levelKey(one.label) === 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 = 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 = 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
|
+
}
|
|
2376
2814
|
/** A criterion score a grade could be computed from: finite, 0 or more, and not above its maximum. */
|
|
2377
2815
|
function isJudgedScore(score, maxScore) {
|
|
2378
2816
|
if (score === void 0) return maxScore === void 0 || typeof maxScore === "number" && maxScore > 0;
|
|
@@ -2419,7 +2857,7 @@ function checkCriteria(raw, rubric) {
|
|
|
2419
2857
|
ok: false,
|
|
2420
2858
|
refusal: "malformed"
|
|
2421
2859
|
};
|
|
2422
|
-
const band = textField(entry.band,
|
|
2860
|
+
const band = textField(entry.band, 40);
|
|
2423
2861
|
const comment = textField(entry.comment, 500);
|
|
2424
2862
|
if (!band.ok) return band;
|
|
2425
2863
|
if (!comment.ok) return comment;
|
|
@@ -2491,6 +2929,6 @@ function checkAiWritingFeedback(raw, request) {
|
|
|
2491
2929
|
};
|
|
2492
2930
|
}
|
|
2493
2931
|
//#endregion
|
|
2494
|
-
export {
|
|
2932
|
+
export { checkAiCoaching as A, checkAiExplanation as B, createItemGroupDraft as C, AI_COACHING_MAX_TIP_LENGTH as D, validateItemGroupDraft as E, aiExplanationRequest as F, hintRevealsAnswer as H, aiGradeOf as I, aiHintRequest as L, AI_HINT_TYPES as M, AI_TEXT_MAX_LENGTH as N, AI_COACHING_MAX_WORDS as O, aiAllowedByContent as P, aiSupports as R, createInteractiveVideoDraft as S, critiqueItemGroupDraft as T, readAiTextResult as U, checkAiHint as V, AI_MATERIAL_MAX_LENGTH as W, aiCritiqueRequest as _, checkAiGrading as a, critiqueDraft as b, AI_DRAFTS_MAX_COUNT as c, aiDraftsRequest as d, checkAiDrafts as f, AI_CRITIQUE_MAX_FINDINGS as g, AI_CRITIQUE_MAX_FIELD_LENGTH as h, aiGradingRequest as i, AI_EXPLANATION_TYPES as j, aiCoachingRequest as k, AI_DRAFTS_MAX_REPAIRS as l, interactiveVideoFromDrafts as m, checkAiWritingFeedback as n, AI_WRITING_MAX_CORRECTIONS as o, generateDrafts as p, AI_GRADING_PROBABILITY_TOLERANCE as r, AI_WRITING_MAX_FIELD_LENGTH as s, aiWritingFeedbackRequest as t, aiDraftsRepairRequest as u, checkAiCritique as v, critiqueDrafts as w, validateDraft as x, createDraft as y, buildAiFacts as z };
|
|
2495
2933
|
|
|
2496
|
-
//# sourceMappingURL=ai-writing-
|
|
2934
|
+
//# sourceMappingURL=ai-writing-D8PdhJJz.js.map
|