From 6eef08c86b92c848f052a3f336a9a6f95d55c40f Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Wed, 16 Apr 2025 23:11:48 +0100 Subject: [PATCH] feat(*): Add AI functionality to grading endpoint --- src/lib/utils/ai.ts | 89 +++++++++++++++++++++++++++++++++ src/lib/utils/types.ts | 9 ++++ src/routes/api/grade/+server.ts | 51 ++++++++++++------- 3 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 src/lib/utils/ai.ts create mode 100644 src/lib/utils/types.ts diff --git a/src/lib/utils/ai.ts b/src/lib/utils/ai.ts new file mode 100644 index 0000000..79fbf49 --- /dev/null +++ b/src/lib/utils/ai.ts @@ -0,0 +1,89 @@ +import type { OpenAIResponse } from './types'; + +const OPENAI_API_KEY = process.env['AZURE_OPENAI_API_KEY']; +const OPENAI_ENDPOINT = process.env['AZURE_OPENAI_ENDPOINT']; +const OPENAI_DEPLOYMENT = process.env['AZURE_OPENAI_DEPLOYMENT'] || 'gpt-4'; + +// Prompt template for grading and feedback +export function buildGradingPrompt(submission: string) { + return `You are an expert secondary school teacher and AI assessment agent. Assess the following student submission using the following steps: +1. Grade the work out of 10, using clear, objective criteria. +2. Identify specific strengths, referencing the success criteria. +3. Identify misconceptions or areas for improvement, using formative assessment language. +4. Design an individualized activity or exercise for the student to address their misconceptions or extend their learning. This activity should be: + - Appropriately scaffolded for the student's current level. + - Specific and actionable (e.g., a short written task, a practical exercise, or a quiz question). + - Aligned with the curriculum and learning objectives. +5. Write a reflection question for the student to encourage metacognition. +6. Suggest to the teacher one way to support this student in the next lesson. +7. Show your reasoning step by step (chain-of-thought). + +STUDENT SUBMISSION: +${submission} + +RESPONSE FORMAT: +Grade: /10 +Strengths: +Areas for Improvement: +Individualized Activity: +Reflection Question: +Teacher Suggestion: +Reasoning: `; +} + +export async function gradeWithOpenAI(submission: string): Promise { + if (!OPENAI_API_KEY || !OPENAI_ENDPOINT) { + // Fallback for local/demo mode + return { + grade: '7/10', + strengths: 'Clear argument and good evidence.', + areas_for_improvement: 'Needs deeper analysis and more examples.', + individualized_activity: 'Write a paragraph expanding on your main point and provide two additional examples to support your argument.', + reflection_question: 'What was the most challenging part of this assignment for you, and why?', + teacher_suggestion: 'In the next lesson, review how to develop arguments with supporting evidence and provide a model answer for comparison.', + reasoning: 'The submission demonstrates a good structure and evidence, but lacks depth and breadth of analysis. The activity and suggestions are designed to target this gap.' + }; + } + + const prompt = buildGradingPrompt(submission); + + const res = await fetch(`${OPENAI_ENDPOINT}/openai/deployments/${OPENAI_DEPLOYMENT}/chat/completions?api-version=2024-02-15-preview`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'api-key': OPENAI_API_KEY + }, + body: JSON.stringify({ + messages: [ + { role: 'system', content: 'You are a helpful, fair, and transparent grading assistant.' }, + { role: 'user', content: prompt } + ], + max_tokens: 500, + temperature: 0.2, + stream: false + }) + }); + + if (!res.ok) throw new Error('OpenAI API error'); + const data = await res.json(); + const content = data.choices?.[0]?.message?.content || ''; + + // Parse the response using regex + const gradeMatch = content.match(/Grade:\s*(.*)/i); + const strengthsMatch = content.match(/Strengths:\s*([\s\S]*?)Areas for Improvement:/i); + const improvementMatch = content.match(/Areas for Improvement:\s*([\s\S]*?)Individualized Activity:/i); + const activityMatch = content.match(/Individualized Activity:\s*([\s\S]*?)Reflection Question:/i); + const reflectionMatch = content.match(/Reflection Question:\s*([\s\S]*?)Teacher Suggestion:/i); + const teacherSuggestionMatch = content.match(/Teacher Suggestion:\s*([\s\S]*?)Reasoning:/i); + const reasoningMatch = content.match(/Reasoning:\s*([\s\S]*)/i); + + return { + grade: gradeMatch?.[1]?.trim() || '', + strengths: strengthsMatch?.[1]?.trim() || '', + areas_for_improvement: improvementMatch?.[1]?.trim() || '', + individualized_activity: activityMatch?.[1]?.trim() || '', + reflection_question: reflectionMatch?.[1]?.trim() || '', + teacher_suggestion: teacherSuggestionMatch?.[1]?.trim() || '', + reasoning: reasoningMatch?.[1]?.trim() || '' + }; +} diff --git a/src/lib/utils/types.ts b/src/lib/utils/types.ts new file mode 100644 index 0000000..e4d40bb --- /dev/null +++ b/src/lib/utils/types.ts @@ -0,0 +1,9 @@ +export interface OpenAIResponse { + grade: string; + strengths: string; + areas_for_improvement: string; + individualized_activity: string; + reflection_question: string; + teacher_suggestion: string; + reasoning: string; +} diff --git a/src/routes/api/grade/+server.ts b/src/routes/api/grade/+server.ts index 8071342..610d686 100644 --- a/src/routes/api/grade/+server.ts +++ b/src/routes/api/grade/+server.ts @@ -1,25 +1,40 @@ import type { RequestHandler } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit'; +import { gradeWithOpenAI } from '$lib/utils/ai'; -// TODO - don't just mock the grading logic export const POST: RequestHandler = async (event: RequestEvent) => { - const data = await event.request.formData(); - const file = data.get('file'); - const text = data.get('text'); + try { + const data = await event.request.formData(); + const file = data.get('file'); + const text = data.get('text'); + let submission = ''; - // Simulation of grading and feedback - let feedback = ''; - if (file) { - feedback = 'Received file: ' + (file as File).name + '. [AI feedback coming soon]'; - } else if (typeof text === 'string' && text.trim().length > 0) { - feedback = 'Received text submission. [AI feedback coming soon]'; - } else { - return new Response(JSON.stringify({ error: 'No valid input provided.' }), { status: 400 }); + if (file && typeof file === 'object' && 'arrayBuffer' in file && 'name' in file) { + // Only support .txt for now for hackathon speed + const fileObj = file as File; + if (!fileObj.name.endsWith('.txt')) { + return new Response(JSON.stringify({ error: 'Only .txt files are supported at this time.' }), { status: 400 }); + } + const buffer = await fileObj.arrayBuffer(); + submission = new TextDecoder('utf-8').decode(buffer); + } else if (typeof text === 'string' && text.trim().length > 0) { + submission = text.trim(); + } else { + return new Response(JSON.stringify({ error: 'No valid input provided.' }), { status: 400 }); + } + + // Call the AI grading agent + const aiResult = await gradeWithOpenAI(submission); + + return new Response(JSON.stringify({ + success: true, + feedback: `Grade: ${aiResult.grade}\nStrengths: ${aiResult.strengths}\nWeaknesses: ${aiResult.weaknesses}\nIndividualized Activity: ${aiResult.individualized_activity}\nReasoning: ${aiResult.reasoning}`, + ...aiResult + }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (err: any) { + return new Response(JSON.stringify({ error: err.message || 'Internal server error.' }), { status: 500 }); } - - // Return mock feedback for now - return new Response(JSON.stringify({ success: true, feedback }), { - status: 200, - headers: { 'Content-Type': 'application/json' } - }); };