mirror of
https://github.com/jcreek/AutomatedAssessmentFeedbackAgent.git
synced 2026-07-12 18:43:49 +00:00
Add work in progress for refactoring to avoid 10+ second API calls that don't work on Netlify
This commit is contained in:
+6
-40
@@ -283,12 +283,14 @@ async function processRunStream(
|
|||||||
throw new Error('Stream ended without completion');
|
throw new Error('Stream ended without completion');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { client, agent };
|
||||||
|
|
||||||
export async function gradeSubmissionWithAgent(
|
export async function gradeSubmissionWithAgent(
|
||||||
submission: string,
|
submission: string,
|
||||||
task: string,
|
task: string,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
hitl: boolean = false
|
hitl: boolean = false
|
||||||
): Promise<OpenAIResponse> {
|
): Promise<{ threadId: string; runId: string }> {
|
||||||
if (!client || !agent) {
|
if (!client || !agent) {
|
||||||
throw new Error('Agent not available');
|
throw new Error('Agent not available');
|
||||||
}
|
}
|
||||||
@@ -301,52 +303,16 @@ export async function gradeSubmissionWithAgent(
|
|||||||
: buildGradingPrompt(submission, task)
|
: buildGradingPrompt(submission, task)
|
||||||
});
|
});
|
||||||
|
|
||||||
let initialStream: AsyncIterable<RunStreamEvent>;
|
|
||||||
try {
|
try {
|
||||||
const runInvoker = client.agents.createRun(thread.id, agent.id, {
|
const run = await client.agents.createRun(thread.id, agent.id, {
|
||||||
parallelToolCalls: false
|
parallelToolCalls: false
|
||||||
});
|
});
|
||||||
initialStream = await runInvoker.stream();
|
return { threadId: thread.id, runId: run.id };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to start run on thread ${thread.id}:`, err);
|
logger.error(`Failed to start run on thread ${thread.id}:`, err);
|
||||||
logger.info('Task:', task);
|
logger.info('Task:', task);
|
||||||
logger.info('Submission', submission);
|
logger.info('Submission', submission);
|
||||||
return fallbackGrade(submission, task);
|
throw new Error('Failed to start grading run.');
|
||||||
}
|
|
||||||
|
|
||||||
// process all tool calls
|
|
||||||
let finalRun: ThreadRunOutput;
|
|
||||||
try {
|
|
||||||
finalRun = await processRunStream(thread.id, initialStream, roomId);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Agent streaming failed on thread ${thread.id}:`, err);
|
|
||||||
return fallbackGrade(submission, task);
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgs = await client.agents.listMessages(thread.id);
|
|
||||||
const assistant = msgs.data.find((m) => m.role === 'assistant');
|
|
||||||
const raw = assistant ? extractTextFromMessage(assistant) : '';
|
|
||||||
|
|
||||||
// strip anything before the JSON object
|
|
||||||
const match = raw.match(/\{[\s\S]*\}$/);
|
|
||||||
const jsonText = match ? match[0] : raw;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(jsonText) as OpenAIResponse;
|
|
||||||
if (parsed.grade === 'HUMAN_REVIEW_REQUIRED') {
|
|
||||||
return {
|
|
||||||
...parsed,
|
|
||||||
success: true,
|
|
||||||
threadId: thread.id,
|
|
||||||
runId: finalRun?.id ?? undefined,
|
|
||||||
hitlContext: { threadId: thread.id, runId: finalRun?.id ?? undefined }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
} catch (parseErr) {
|
|
||||||
logger.error(`JSON parse failed, returning fallback on thread ${thread.id}:`, parseErr);
|
|
||||||
logger.info('JSON', jsonText);
|
|
||||||
return { ...fallbackGrade(submission, task), reasoning: raw };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,15 @@ export const POST: RequestHandler = async (event: RequestEvent) => {
|
|||||||
return new Response(JSON.stringify({ error: 'No valid input provided.' }), { status: 400 });
|
return new Response(JSON.stringify({ error: 'No valid input provided.' }), { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the AI grading agent with threading and fallback
|
// Call the AI grading agent asynchronously
|
||||||
const aiResult = await gradeSubmissionWithAgent(submission, task.trim(), roomId ?? '', hitl);
|
const { threadId, runId } = await gradeSubmissionWithAgent(submission, task.trim(), roomId ?? '', hitl);
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
task: task.trim(),
|
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}`,
|
threadId,
|
||||||
...aiResult
|
runId
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import type { RequestHandler } from '@sveltejs/kit';
|
||||||
|
import type { RequestEvent } from '@sveltejs/kit';
|
||||||
|
import { client, agent } from '$lib/server/ai';
|
||||||
|
|
||||||
|
// GET /api/grade/status?threadId=...&runId=...
|
||||||
|
export const GET: RequestHandler = async (event: RequestEvent) => {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
const threadId = url.searchParams.get('threadId');
|
||||||
|
const runId = url.searchParams.get('runId');
|
||||||
|
|
||||||
|
if (!client || !agent) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Agent not available' }), { status: 500 });
|
||||||
|
}
|
||||||
|
if (!threadId || !runId) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Missing threadId or runId' }), { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const run = await client.agents.getRun(threadId, runId);
|
||||||
|
// Enhanced: handle all important statuses for polling
|
||||||
|
let toolEvents = [];
|
||||||
|
if (run.tool_calls) {
|
||||||
|
toolEvents = run.tool_calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = null;
|
||||||
|
let parsed = null;
|
||||||
|
let humanReviewRequired = false;
|
||||||
|
let parseError = null;
|
||||||
|
|
||||||
|
// Handle agent run statuses for polling, mirroring processRunStream
|
||||||
|
if (run.status === 'requires_action' && run.requiredAction) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
status: 'requires_action',
|
||||||
|
requiredAction: run.requiredAction,
|
||||||
|
toolCalls: run.requiredAction.submitToolOutputs?.toolCalls || [],
|
||||||
|
toolEvents,
|
||||||
|
message: 'Agent requires tool outputs to proceed.'
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (run.status === 'failed' || run.status === 'cancelling' || run.status === 'cancelled') {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
status: run.status,
|
||||||
|
toolEvents,
|
||||||
|
error: run.lastError || 'Run failed or was cancelled.'
|
||||||
|
}),
|
||||||
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (run.status === 'in_progress' || run.status === 'queued' || run.status === 'starting') {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
status: run.status,
|
||||||
|
toolEvents,
|
||||||
|
message: 'Grading in progress.'
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completed and fallback handled below as before
|
||||||
|
if (run.status === 'completed') {
|
||||||
|
const msgs = await client.agents.listMessages(threadId);
|
||||||
|
const assistant = msgs.data.find((m: any) => m.role === 'assistant');
|
||||||
|
const raw = assistant ? (typeof assistant.content === 'string' ? assistant.content : (assistant.content?.[0]?.text || '')) : '';
|
||||||
|
// strip anything before the JSON object
|
||||||
|
const match = raw.match(/\{[\s\S]*\}$/);
|
||||||
|
const jsonText = match ? match[0] : raw;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(jsonText);
|
||||||
|
if (parsed.grade === 'HUMAN_REVIEW_REQUIRED') {
|
||||||
|
humanReviewRequired = true;
|
||||||
|
}
|
||||||
|
result = parsed;
|
||||||
|
} catch (err: any) {
|
||||||
|
parseError = err.message || 'Failed to parse AI response.';
|
||||||
|
result = { raw };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
status: run.status,
|
||||||
|
toolEvents,
|
||||||
|
result,
|
||||||
|
humanReviewRequired,
|
||||||
|
parseError
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
return new Response(JSON.stringify({ error: err.message || 'Failed to get run status' }), { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -40,111 +40,104 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(event: Event) {
|
async function handleSubmit(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submitting = true;
|
submitting = true;
|
||||||
successMsg = '';
|
successMsg = '';
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
const THINKING_EVENT = { tool: 'thinking', time: new Date().toISOString() };
|
const THINKING_EVENT = { tool: 'thinking', time: new Date().toISOString() };
|
||||||
function ensureThinkingStep(events) {
|
function ensureThinkingStep(events) {
|
||||||
// Remove any existing 'thinking' event
|
// Remove any existing 'thinking' event
|
||||||
const filtered = events.filter((e) => e.tool !== 'thinking');
|
const filtered = events.filter((e) => e.tool !== 'thinking');
|
||||||
return [THINKING_EVENT, ...filtered];
|
return [THINKING_EVENT, ...filtered];
|
||||||
}
|
}
|
||||||
toolEvents = [THINKING_EVENT];
|
toolEvents = [THINKING_EVENT];
|
||||||
|
|
||||||
// Generate a unique roomId for this submission
|
// Generate a unique roomId for this submission
|
||||||
roomId = crypto.randomUUID();
|
roomId = crypto.randomUUID();
|
||||||
// Connect to PartyKit for this session
|
// Connect to PartyKit for this session
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
ws = new WebSocket(`${PARTYKIT_BASE_URL}/party/tool-usage-server-${roomId}`);
|
ws = new WebSocket(`${PARTYKIT_BASE_URL}/party/tool-usage-server-${roomId}`);
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
toolEvents = ensureThinkingStep([...toolEvents, data]);
|
toolEvents = ensureThinkingStep([...toolEvents, data]);
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!taskDescription.trim()) {
|
if (!taskDescription.trim()) {
|
||||||
errorMsg = 'Please provide a description of the task or assignment.';
|
errorMsg = 'Please provide a description of the task or assignment.';
|
||||||
|
submitting = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('task', taskDescription.trim());
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
formData.append('file', file);
|
||||||
|
} else if (textInput.trim().length > 0) {
|
||||||
|
formData.append('text', textInput.trim());
|
||||||
|
}
|
||||||
|
formData.append('roomId', roomId);
|
||||||
|
formData.append('hitl', hitl ? 'true' : 'false');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/grade', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) {
|
||||||
|
errorMsg = data.error || 'Failed to start grading.';
|
||||||
submitting = false;
|
submitting = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const formData = new FormData();
|
const { threadId, runId } = data;
|
||||||
formData.append('task', taskDescription.trim());
|
await pollForStatus(threadId, runId);
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMsg = err.message || 'Failed to start grading.';
|
||||||
|
submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (file) {
|
async function pollForStatus(threadId: string, runId: string) {
|
||||||
formData.append('file', file);
|
humanReviewRequired = false;
|
||||||
} else if (textInput.trim().length > 0) {
|
aiReasoning = '';
|
||||||
formData.append('text', textInput.trim());
|
hitlContext = null;
|
||||||
}
|
let polling = true;
|
||||||
formData.append('roomId', roomId);
|
while (polling) {
|
||||||
formData.append('hitl', hitl ? 'true' : 'false');
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
errorMsg = '';
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/grade', {
|
const res = await fetch(`/api/grade/status?threadId=${encodeURIComponent(threadId)}&runId=${encodeURIComponent(runId)}`);
|
||||||
method: 'POST',
|
const statusData = await res.json();
|
||||||
body: formData
|
if (!statusData.success) {
|
||||||
});
|
errorMsg = statusData.error || 'Unknown error during grading.';
|
||||||
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = await response.json();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error parsing /api/grade response:', err);
|
|
||||||
errorMsg = 'Received an invalid response from the server.';
|
|
||||||
submitting = false;
|
submitting = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (response.ok && result.success) {
|
toolEvents = statusData.toolEvents || toolEvents;
|
||||||
if (result.grade === 'HUMAN_REVIEW_REQUIRED') {
|
if (statusData.status === 'completed') {
|
||||||
humanReviewRequired = true;
|
polling = false;
|
||||||
aiReasoning = result.reasoning;
|
submitting = false;
|
||||||
hitlContext = result.hitlContext || { threadId: result.threadId, runId: result.runId };
|
if (statusData.result) {
|
||||||
submitting = false;
|
const result = statusData.result;
|
||||||
return;
|
aiReasoning = result.reasoning || '';
|
||||||
}
|
if (statusData.humanReviewRequired) {
|
||||||
|
humanReviewRequired = true;
|
||||||
try {
|
hitlContext = { threadId, runId };
|
||||||
// Save to localStorage history
|
} else {
|
||||||
let prev = JSON.parse(localStorage.getItem('assessmentHistory') || '[]');
|
successMsg = `Suggested Grade: ${result.grade}\nStrengths: ${result.strengths}\nAreas for Improvement: ${result.areasForImprovement}\nIndividualized Activity: ${result.individualizedActivity}\nSpelling and Grammar: ${result.spellingAndGrammar}\nReasoning: ${result.reasoning}`;
|
||||||
const entry = {
|
}
|
||||||
timestamp: Date.now(),
|
} else if (statusData.parseError) {
|
||||||
...result,
|
errorMsg = statusData.parseError;
|
||||||
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.';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error('Error submitting grade:', err);
|
errorMsg = err.message || 'Failed to poll grading status.';
|
||||||
errorMsg = 'Network or server error. Please check your connection and try again.';
|
submitting = false;
|
||||||
} finally {
|
return;
|
||||||
// Disabled to make the transition to the results page smoother
|
|
||||||
// submitting = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitHumanReview() {
|
async function submitHumanReview() {
|
||||||
submitting = true;
|
submitting = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user