Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions services/registration/src/models/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export interface Application extends mongoose.Document {
createdAt: Date;
updatedAt: Date;
finalScore: number;
finalAiScore?: number;
}

const applicationSchema = new Schema<Application>(
Expand Down Expand Up @@ -309,6 +310,11 @@ const applicationSchema = new Schema<Application>(
finalScore: {
type: Number,
},
// Average of the graders' AI likelihood ratings across this application's reviews.
// Recorded for data collection only; nothing currently acts on it.
finalAiScore: {
type: Number,
},
},
{
timestamps: true,
Expand Down
8 changes: 8 additions & 0 deletions services/registration/src/models/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Review extends mongoose.Document {
essayId: Types.ObjectId;
score: number;
adjustedScore: number;
aiScore?: number;
timestamp: Date;
}

Expand Down Expand Up @@ -40,6 +41,13 @@ const reviewSchema = new Schema<Review>({
type: Number,
required: true,
},
// Grader's rating of how likely the essay was written by AI. Optional so that reviews
// submitted before this field existed (and any client not yet sending it) remain valid.
aiScore: {
type: Number,
min: 1,
max: 3,
},
timestamp: {
type: Date,
required: true,
Expand Down
29 changes: 29 additions & 0 deletions services/registration/src/routes/grading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import { getCalibrationMapping } from "../common/adjustScores";
const MAX_REVIEWS_PER_ESSAY = 2;
// NOTE: No. of essays for each application. As such, will need to be updated whenever we add/remove essays.
const ESSAY_COUNT = 4;
// Graders rate how likely an essay was written by AI alongside the normal score, so that they can
// grade the essay on its merits and record the suspicion separately.
// 1 = no chance of AI, 2 = some hints and sounds kinda like AI, 3 = fairly confident this is AI.
const AI_SCORE_MIN = 1;
const AI_SCORE_MAX = 3;

type AggregatedEssay = {
applicationId: string;
Expand Down Expand Up @@ -224,6 +229,18 @@ gradingRouter.route("/actions/submit-review").post(
throw new BadRequestError("One or more of the method body arguments is missing.");
}

// Optional so clients that haven't been updated yet can still submit reviews.
const { aiScore } = req.body;
if (
aiScore !== undefined &&
aiScore !== null &&
(!Number.isInteger(aiScore) || aiScore < AI_SCORE_MIN || aiScore > AI_SCORE_MAX)
) {
throw new BadRequestError(
`AI score must be an integer between ${AI_SCORE_MIN} and ${AI_SCORE_MAX}`
);
}

const gradingGroup = req.body.gradingGroup as GradingGroupType;

let criteriaScores = grader.calibrationScores.find(
Expand Down Expand Up @@ -294,6 +311,7 @@ gradingRouter.route("/actions/submit-review").post(
score: req.body.score,
timestamp: new Date(),
adjustedScore,
aiScore,
});

const allEssayReviews = await ReviewModel.find({
Expand All @@ -305,6 +323,17 @@ gradingRouter.route("/actions/submit-review").post(
application.gradingComplete = true;
const sumScores = allEssayReviews.reduce((prev, review) => prev + review.adjustedScore, 0);
application.finalScore = sumScores / allEssayReviews.length;

// Average only over the reviews that actually submitted an AI score, so reviews from
// before this field existed don't pull the average toward zero.
const aiScores = allEssayReviews
.map(review => review.aiScore)
.filter((score): score is number => score !== undefined && score !== null);
if (aiScores.length > 0) {
application.finalAiScore =
aiScores.reduce((prev, score) => prev + score, 0) / aiScores.length;
}

await application.save();
}

Expand Down