diff --git a/src/routes/api/grade/+server.ts b/src/routes/api/grade/+server.ts index 79b0bdd..c712594 100644 --- a/src/routes/api/grade/+server.ts +++ b/src/routes/api/grade/+server.ts @@ -1,58 +1,70 @@ import type { RequestHandler } from '@sveltejs/kit'; import type { RequestEvent } from '@sveltejs/kit'; -import { gradeSubmissionWithAgent } from '$lib/server/ai'; +import { client } from '$lib/agent/services/agentService'; +import { buildGradingPrompt, buildGradingPromptWithSupportForHumanInTheLoop } from '$lib/server/ai'; +import { error, json } from '@sveltejs/kit'; + +interface GradingRequest { + submission: string; + task: string; + roomId?: string; + hitl?: boolean; +} export const POST: RequestHandler = async (event: RequestEvent) => { - try { - const data = await event.request.formData(); - const file = data.get('file'); - const text = data.get('text'); - const task = data.get('task'); - const roomId = data.get('roomId') as string | undefined; - const hitl = data.get('hitl') === 'true'; - let submission = ''; + try { + const data = await event.request.formData(); + const file = data.get('file'); + const text = data.get('text'); + const task = data.get('task'); + const roomId = data.get('roomId') as string | undefined; + const hitl = data.get('hitl') === 'true'; + let submission = ''; - if (!task || typeof task !== 'string' || !task.trim()) { - return new Response(JSON.stringify({ error: 'Task/assignment description is required.' }), { - status: 400 - }); - } + if (!task || typeof task !== 'string' || !task.trim()) { + throw error(400, 'Task/assignment description is required.'); + } - 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 }); - } + 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')) { + throw error(400, 'Only .txt files are supported at this time.'); + } + 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 { + throw error(400, 'No valid input provided.'); + } - // Call the AI grading agent with threading and fallback - const aiResult = await gradeSubmissionWithAgent(submission, task.trim(), roomId ?? '', hitl); + // Create a new thread + const thread = await client.agents.createThread(); + + // Add the initial message with the grading prompt + await client.agents.createMessage(thread.id, { + role: 'user', + content: hitl + ? buildGradingPromptWithSupportForHumanInTheLoop(submission, task.trim()) + : buildGradingPrompt(submission, task.trim()) + }); - return new Response( - JSON.stringify({ - success: true, - task: task.trim(), - feedback: `Suggested Grade: ${aiResult.grade}\nStrengths: ${aiResult.strengths}\nAreas for Improvement: ${aiResult.areasForImprovement}\nIndividualized Activity: ${aiResult.individualizedActivity}\nSpelling and Grammar: ${aiResult.spellingAndGrammar}\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 - }); - } + // Start the run asynchronously + const run = await client.agents.createRun(thread.id, (globalThis as any).agent.id, { + parallelToolCalls: false + }); + + // Return immediately with the thread ID for polling + return json({ + success: true, + threadId: thread.id, + runId: run.id, + status: 'started', + statusUrl: `/api/grade/status/${thread.id}` + }); + } catch (err: any) { + console.error('Error starting grading operation:', err); + throw error(500, `Error starting grading operation: ${err.message}`); + } }; diff --git a/src/routes/api/grade/status/[threadId]/+server.ts b/src/routes/api/grade/status/[threadId]/+server.ts new file mode 100644 index 0000000..f8b27dc --- /dev/null +++ b/src/routes/api/grade/status/[threadId]/+server.ts @@ -0,0 +1,67 @@ +import { json, type RequestHandler } from '@sveltejs/kit'; +import { client } from '$lib/agent/services/agentService'; +import { error } from '@sveltejs/kit'; +import type { ThreadRunOutput } from '@azure/ai-projects'; + +export const GET: RequestHandler = async ({ params }) => { + const { threadId } = params; + + if (!threadId) { + throw error(400, 'Thread ID is required'); + } + + try { + // Get the thread to check if it exists + const thread = await client.agents.getThread(threadId); + if (!thread) { + throw error(404, 'Thread not found'); + } + + // Get the latest run for this thread + const runs = await client.agents.listRuns(threadId); + const latestRun = runs.data[0]; + + if (!latestRun) { + return json({ + status: 'completed', + message: 'No runs found for this thread' + }); + } + + // Get the run status + const run = await client.agents.getRun(threadId, latestRun.id); + + // Get the latest messages to see if we have a result + const messages = await client.agents.listMessages(threadId); + const assistantMessage = messages.data.find((m: any) => m.role === 'assistant'); + let result = null; + + if (assistantMessage) { + const raw = assistantMessage.content + .filter((c: any) => c.type === 'text') + .map((c: any) => c.text.value) + .join('\n'); + + // Try to parse the JSON response + try { + const match = raw.match(/\{[\s\S]*\}$/); + const jsonText = match ? match[0] : raw; + result = JSON.parse(jsonText); + } catch (e) { + // If we can't parse JSON, return the raw text + result = { raw }; + } + } + + return json({ + status: run.status, + runId: run.id, + threadId, + result, + completed: ['completed', 'failed', 'cancelled', 'expired'].includes(run.status) + }); + } catch (err: any) { + console.error('Error checking status:', err); + throw error(500, `Error checking status: ${err.message}`); + } +}; diff --git a/src/routes/upload/+page.svelte b/src/routes/upload/+page.svelte index 29b050f..38981cf 100644 --- a/src/routes/upload/+page.svelte +++ b/src/routes/upload/+page.svelte @@ -39,17 +39,70 @@ errorMsg = ''; } + // Poll for completion of the grading operation + interface PollResult { + completed: boolean; + result?: any; + } + + interface ToolEvent { + tool: string; + time: string; + // Add other properties as needed + } + + async function pollForCompletion(threadId: string, runId: string, maxAttempts = 60, interval = 2000): Promise { + let attempts = 0; + + const checkStatus = async (): Promise => { + try { + const response = await fetch(`/api/grade/status/${threadId}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json() as PollResult; + + if (result.completed) { + // If we have a result, return it + if (result.result) { + return result; + } + // If no result but completed, throw an error + throw new Error('Operation completed but no result was returned'); + } + + // If not completed and we have attempts left, poll again + if (attempts < maxAttempts) { + attempts++; + await new Promise(resolve => setTimeout(resolve, interval)); + return checkStatus(); + } + + // Max attempts reached + throw new Error('Operation timed out'); + } catch (error) { + console.error('Error polling status:', error); + throw error; + } + }; + + return checkStatus(); + } + async function handleSubmit(event: Event) { event.preventDefault(); submitting = true; successMsg = ''; errorMsg = ''; const THINKING_EVENT = { tool: 'thinking', time: new Date().toISOString() }; - function ensureThinkingStep(events) { + + function ensureThinkingStep(events: ToolEvent[]): ToolEvent[] { // Remove any existing 'thinking' event - const filtered = events.filter((e) => e.tool !== 'thinking'); + const filtered = events.filter((e: ToolEvent) => e.tool !== 'thinking'); return [THINKING_EVENT, ...filtered]; } + toolEvents = [THINKING_EVENT]; // Generate a unique roomId for this submission @@ -69,6 +122,7 @@ submitting = false; return; } + const formData = new FormData(); formData.append('task', taskDescription.trim()); @@ -82,6 +136,7 @@ errorMsg = ''; try { + // Start the grading operation const response = await fetch('/api/grade', { method: 'POST', body: formData @@ -96,53 +151,64 @@ submitting = false; return; } - if (response.ok && result.success) { - if (result.grade === 'HUMAN_REVIEW_REQUIRED') { - humanReviewRequired = true; - aiReasoning = result.reasoning; - hitlContext = result.hitlContext || { threadId: result.threadId, runId: result.runId }; - submitting = false; - return; - } - try { - // Save to localStorage history - let prev = JSON.parse(localStorage.getItem('assessmentHistory') || '[]'); - const entry = { - timestamp: Date.now(), - ...result, - submission: result.submission || textInput || '', - task: taskDescription - }; - prev.unshift(entry); - localStorage.setItem('assessmentHistory', JSON.stringify(prev.slice(0, 50))); - } catch {} - - // Redirect to /results if HITL is not needed - window.location.assign('/results'); - - file = null; - textInput = ''; - taskDescription = ''; - return; - } else { - console.error('Grade API error or non-success:', response, result); - if (result && result.error) { - errorMsg = `Sorry, something went wrong: ${result.error}`; - } else if (response.status === 413) { - errorMsg = 'The uploaded file is too large. Please upload a smaller file.'; - } else if (response.status >= 500) { - errorMsg = 'The server encountered an error. Please try again later.'; - } else { - errorMsg = 'An unknown error occurred. Please check your input and try again.'; - } + if (!response.ok || !result.success) { + throw new Error(result.error || 'Failed to start grading operation'); } - } catch (err) { - console.error('Error submitting grade:', err); - errorMsg = 'Network or server error. Please check your connection and try again.'; - } finally { - // Disabled to make the transition to the results page smoother - // submitting = false; + + // Start polling for completion + try { + const pollResult = await pollForCompletion(result.threadId, result.runId); + + if (pollResult.result) { + // Handle the result + const finalResult = pollResult.result; + + if (finalResult.grade === 'HUMAN_REVIEW_REQUIRED') { + humanReviewRequired = true; + aiReasoning = finalResult.reasoning; + hitlContext = finalResult.hitlContext || { + threadId: result.threadId, + runId: result.runId + }; + submitting = false; + return; + } + + // Save to localStorage history + try { + let prev = JSON.parse(localStorage.getItem('assessmentHistory') || '[]'); + const entry = { + timestamp: Date.now(), + ...finalResult, + submission: finalResult.submission || textInput || '', + task: taskDescription + }; + prev.unshift(entry); + localStorage.setItem('assessmentHistory', JSON.stringify(prev.slice(0, 50))); + } catch (e) { + console.error('Error saving to history:', e); + } + + // Redirect to results page + window.location.assign('/results'); + + // Reset form + file = null; + textInput = ''; + taskDescription = ''; + } + } catch (pollError: unknown) { + console.error('Error during polling:', pollError); + const errorMessage = pollError instanceof Error ? pollError.message : 'An unknown error occurred during polling'; + errorMsg = `Error during grading: ${errorMessage}`; + submitting = false; + } + } catch (err: unknown) { + console.error('Error starting grading operation:', err); + const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred'; + errorMsg = `Failed to start grading: ${errorMessage}`; + submitting = false; } }