Skip to content

Commit 7bad56b

Browse files
CP3108 SR AI-powered marking (#3126)
* Add Grading comment selector basic UI * Connect to backend * Content changes not found but are showing up as changed * Hide generate comment button if no LLM prompt * Input field for API key during Course creation * Add input field for enabling LLM Grading * Disable comment selector if llm grading not enabled * Added LLM API Key input and integration with backend * Add to the course editing panel for llm grading and api key * Update localStorage.ts Fix redux state not saving issue * Add save chosen and final comment feature Comment selector can now handle selecting multiple suggestions * Add Spinner when generating comments * Update to show API Key as password field * Fix format * Revert "Content changes not found but are showing up as changed" This reverts commit 780a269. * Edit frontend to load previous AI comments by default * Moves api key, api url, llm model and course prompt to course level * Add tooltip about formatting instructions * style * Add view "composed prompt" in the grading editor view * Remove un-used 'save comments' * typo * Add copy prompt to clipboard * Fix typo * Fix bug preventing avengers from generating ai comments * Fix TS errors * Fix more TS errors! * fix some style issues * Update src/pages/academy/grading/subcomponents/GradingEditor.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/commons/dropdown/DropdownCreateCourse.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Work on comments * Small style fix * Abstract styles to scss modules * Remove llmApiKey from any kind of storage on FE * Re-format ai_comments to reference answer_id instead * Merge all prompts into :prompts --------- Co-authored-by: tkaixiang <tkaixiang@gmail.com>
1 parent b8bd3ca commit 7bad56b

File tree

20 files changed

+607
-80
lines changed

20 files changed

+607
-80
lines changed

src/commons/application/actions/__tests__/SessionActions.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,8 @@ test('updateGrading generates correct action object', async () => {
605605
const grading: GradingQuery = {
606606
answers: [
607607
{
608+
id: 0,
609+
prompts: [],
608610
question: await vi.importMock('../../../../features/grading/GradingTypes'),
609611
student: {
610612
name: 'test student',
@@ -620,9 +622,12 @@ test('updateGrading generates correct action object', async () => {
620622
id: 100
621623
},
622624
gradedAt: '2019-08-16T13:26:32+00:00'
623-
}
625+
},
626+
autogradingResults: [],
627+
autoGradingStatus: 'N/A'
624628
}
625629
],
630+
enable_llm_grading: false,
626631
assessment: {
627632
coverPicture: 'https://i.imgur.com/dR7zBPI.jpeg',
628633
id: 1,

src/commons/application/reducers/__tests__/SessionReducer.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,13 @@ test('UPDATE_ASSESSMENT_OVERVIEWS works correctly in updating assessment overvie
402402

403403
// Test data for UPDATE_GRADING
404404
const gradingTest1: GradingQuery = {
405+
enable_llm_grading: false,
405406
answers: [
406407
{
408+
id: 0,
409+
prompts: [],
410+
autogradingResults: [],
411+
autoGradingStatus: 'N/A',
407412
question: await vi.importMock('../../../../features/grading/GradingTypes'),
408413
student: {
409414
name: 'test student',
@@ -430,8 +435,13 @@ const gradingTest1: GradingQuery = {
430435
};
431436

432437
const gradingTest2: GradingQuery = {
438+
enable_llm_grading: false,
433439
answers: [
434440
{
441+
id: 0,
442+
prompts: [],
443+
autogradingResults: [],
444+
autoGradingStatus: 'N/A',
435445
question: await vi.importMock('../../../../features/grading/GradingTypes'),
436446
student: {
437447
name: 'another test student',

src/commons/application/types/SessionTypes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export type SessionState = {
3939
readonly enableAchievements?: boolean;
4040
readonly enableSourcecast?: boolean;
4141
readonly enableStories?: boolean;
42+
readonly enableLlmGrading?: boolean;
43+
readonly llmModel?: string;
44+
readonly llmApiUrl?: string;
45+
readonly llmCourseLevelPrompt?: string;
4246
readonly enableOverallLeaderboard?: boolean;
4347
readonly enableContestLeaderboard?: boolean;
4448
readonly topLeaderboardDisplay?: number;
@@ -109,6 +113,7 @@ export type CourseConfiguration = {
109113
enableAchievements: boolean;
110114
enableSourcecast: boolean;
111115
enableStories: boolean;
116+
enableLlmGrading?: boolean;
112117
enableOverallLeaderboard: boolean;
113118
enableContestLeaderboard: boolean;
114119
topLeaderboardDisplay: number;
@@ -117,6 +122,9 @@ export type CourseConfiguration = {
117122
sourceVariant: Variant;
118123
moduleHelpText: string;
119124
assetsPrefix: string;
125+
llmModel?: string;
126+
llmApiUrl?: string;
127+
llmCourseLevelPrompt?: string;
120128
};
121129

122130
export type AdminPanelCourseRegistration = {
@@ -128,4 +136,4 @@ export type AdminPanelCourseRegistration = {
128136
group?: string;
129137
};
130138

131-
export type UpdateCourseConfiguration = Partial<CourseConfiguration>;
139+
export type UpdateCourseConfiguration = Partial<CourseConfiguration & { llmApiKey: string }>;

src/commons/assessment/AssessmentTypes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ export interface IContestVotingQuestion extends BaseQuestion {
143143
type: 'voting';
144144
}
145145

146+
export type LLMPrompt = {
147+
role: string;
148+
content: string;
149+
};
150+
146151
export type BaseQuestion = {
147152
answer: string | number | ContestEntry[] | null;
148153
comments?: string;

src/commons/dropdown/DropdownCreateCourse.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ const DropdownCreateCourse: React.FC<Props> = props => {
4040
enableAchievements: true,
4141
enableSourcecast: true,
4242
enableStories: false,
43+
enableLlmGrading: false,
4344
sourceChapter: Chapter.SOURCE_1,
4445
sourceVariant: Variant.DEFAULT,
45-
moduleHelpText: ''
46+
moduleHelpText: '',
47+
llmApiKey: ''
4648
});
4749

4850
const [courseHelpTextSelectedTab, setCourseHelpTextSelectedTab] =
@@ -222,7 +224,8 @@ const DropdownCreateCourse: React.FC<Props> = props => {
222224
})
223225
}
224226
/>
225-
227+
</div>
228+
<div>
226229
<Switch
227230
checked={courseConfig.enableStories}
228231
inline
@@ -234,6 +237,18 @@ const DropdownCreateCourse: React.FC<Props> = props => {
234237
})
235238
}
236239
/>
240+
241+
<Switch
242+
checked={courseConfig.enableLlmGrading}
243+
inline
244+
label="Enable LLM Grading"
245+
onChange={e =>
246+
setCourseConfig({
247+
...courseConfig,
248+
enableLlmGrading: (e.target as HTMLInputElement).checked
249+
})
250+
}
251+
/>
237252
</div>
238253
</div>
239254
<div>
@@ -273,6 +288,24 @@ const DropdownCreateCourse: React.FC<Props> = props => {
273288
fill
274289
/>
275290
</FormGroup>
291+
<FormGroup
292+
helperText="API Key for LLM endpoint. This key will be encrypted and will not be retrievable on the frontend after."
293+
label={'LLM API Key'}
294+
labelInfo="(optional)"
295+
labelFor="llmApiKey"
296+
>
297+
<InputGroup
298+
id="llmApiKey"
299+
type="password"
300+
value={courseConfig.llmApiKey}
301+
onChange={e =>
302+
setCourseConfig({
303+
...courseConfig,
304+
llmApiKey: e.target.value
305+
})
306+
}
307+
/>
308+
</FormGroup>
276309
</div>
277310
<div className="create-course-button-container">
278311
<Button text="Create Course" onClick={submitHandler} />

src/commons/mocks/BackendMocks.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,11 @@ export function* mockBackendSaga(): SagaIterator {
343343
return gradingQuestion;
344344
});
345345
yield put(
346-
actions.updateGrading(submissionId, { answers: newGrading, assessment: grading.assessment })
346+
actions.updateGrading(submissionId, {
347+
answers: newGrading,
348+
assessment: grading.assessment,
349+
enable_llm_grading: false
350+
})
347351
);
348352
yield call(showSuccessMessage, 'Submitted!', 1000);
349353
};

src/commons/mocks/GradingMocks.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ export const mockTestcases: Testcase[] = [
128128

129129
export const mockGradingAnswer: GradingAnswer = [
130130
{
131+
id: 0,
132+
prompts: [],
133+
autogradingResults: [],
134+
autoGradingStatus: 'N/A',
131135
question: {
132136
answer: `function remainder(n, d) {
133137
return (n - d) < 0 ? n : remainder(n - d, d);
@@ -244,6 +248,10 @@ _italics_
244248
}
245249
},
246250
{
251+
id: 1,
252+
prompts: [],
253+
autogradingResults: [],
254+
autoGradingStatus: 'N/A',
247255
question: {
248256
prepend: '',
249257
postpend: '',
@@ -336,6 +344,10 @@ New message from **Avenger**!
336344
}
337345
},
338346
{
347+
id: 2,
348+
prompts: [],
349+
autogradingResults: [],
350+
autoGradingStatus: 'N/A',
339351
question: {
340352
// C is the answer
341353
prepend: '',
@@ -434,7 +446,8 @@ Starring: Source Academy`,
434446

435447
export const mockGradingQuery: GradingQuery = {
436448
answers: mockGradingAnswer,
437-
assessment: mockGradingAssessment
449+
assessment: mockGradingAssessment,
450+
enable_llm_grading: false
438451
};
439452

440453
/**

src/commons/sagas/BackendSaga.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,11 @@ function* sendGrade(
637637
});
638638

639639
yield put(
640-
actions.updateGrading(submissionId, { answers: newGrading, assessment: grading.assessment })
640+
actions.updateGrading(submissionId, {
641+
answers: newGrading,
642+
assessment: grading.assessment,
643+
enable_llm_grading: false
644+
})
641645
);
642646
}
643647

src/commons/sagas/RequestsSaga.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,8 +1030,9 @@ export const getGrading = async (
10301030

10311031
const gradingResult = await resp.json();
10321032
const grading: GradingAnswer = gradingResult.answers.map((gradingQuestion: any) => {
1033-
const { student, question, grade, team } = gradingQuestion;
1033+
const { id, student, question, grade, team } = gradingQuestion;
10341034
const result = {
1035+
id,
10351036
question: {
10361037
answer: question.answer,
10371038
lastModifiedAt: question.lastModifiedAt,
@@ -1054,7 +1055,9 @@ export const getGrading = async (
10541055
xp: grade.xp,
10551056
xpAdjustment: grade.xpAdjustment,
10561057
comments: grade.comments
1057-
}
1058+
},
1059+
ai_comments: gradingQuestion.ai_comments?.response?.split('|||') || [],
1060+
prompts: gradingQuestion.prompts
10581061
} as GradingQuestion;
10591062

10601063
if (gradingQuestion.grade.grader !== null) {
@@ -1064,7 +1067,11 @@ export const getGrading = async (
10641067
return result;
10651068
});
10661069

1067-
return { answers: grading, assessment: gradingResult.assessment };
1070+
return {
1071+
enable_llm_grading: gradingResult.enable_llm_grading,
1072+
answers: grading,
1073+
assessment: gradingResult.assessment
1074+
};
10681075
};
10691076

10701077
/**
@@ -1548,6 +1555,36 @@ export const removeAssessmentConfig = async (
15481555
return resp;
15491556
};
15501557

1558+
/**
1559+
* POST /courses/{courseId}/admin/generate-comments/{answer_id}/
1560+
*/
1561+
export const postGenerateComments = async (
1562+
tokens: Tokens,
1563+
answer_id: number
1564+
): Promise<{ comments: string[] } | null> => {
1565+
const resp = await request(`${courseId()}/admin/generate-comments/${answer_id}`, 'POST', {
1566+
...tokens
1567+
});
1568+
if (!resp || !resp.ok) {
1569+
return null;
1570+
}
1571+
1572+
return await resp.json();
1573+
};
1574+
1575+
export const saveFinalComment = async (
1576+
tokens: Tokens,
1577+
answer_id: number,
1578+
comment: string
1579+
): Promise<Response | null> => {
1580+
const resp = await request(`${courseId()}/admin/save-final-comment/${answer_id}`, 'POST', {
1581+
body: { comment: comment },
1582+
...tokens
1583+
});
1584+
1585+
return resp;
1586+
};
1587+
15511588
/**
15521589
* GET /courses/{courseId}/admin/users
15531590
*/

src/features/grading/GradingTypes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
AssessmentStatus,
55
AssessmentType,
66
AutogradingResult,
7+
LLMPrompt,
78
MCQChoice,
89
ProgressStatus,
910
Question,
@@ -99,6 +100,7 @@ export type GradingAssessment = {
99100
export type GradingQuery = {
100101
answers: GradingAnswer;
101102
assessment: GradingAssessment;
103+
enable_llm_grading: boolean | null;
102104
};
103105

104106
export type GradingSubmissionTableProps = {
@@ -168,6 +170,7 @@ export type IGradingTableProperties = {
168170
* particular question in a submission.
169171
*/
170172
export type GradingQuestion = {
173+
id: number;
171174
question: AnsweredQuestion;
172175
team?: Array<{
173176
username: any;
@@ -189,6 +192,10 @@ export type GradingQuestion = {
189192
};
190193
gradedAt?: string;
191194
};
195+
autogradingResults: AutogradingResult[];
196+
autoGradingStatus: string;
197+
ai_comments?: string[];
198+
prompts: LLMPrompt[];
192199
};
193200

194201
/**

0 commit comments

Comments
 (0)