Add work in progress for refactoring to avoid 10+ second API calls that don't work on Netlify

This commit is contained in:
Josh Creek
2025-04-26 20:43:07 +01:00
parent 96d43d511c
commit 2e416ca00e
4 changed files with 196 additions and 135 deletions
+6 -40
View File
@@ -283,12 +283,14 @@ async function processRunStream(
throw new Error('Stream ended without completion');
}
export { client, agent };
export async function gradeSubmissionWithAgent(
submission: string,
task: string,
roomId: string,
hitl: boolean = false
): Promise<OpenAIResponse> {
): Promise<{ threadId: string; runId: string }> {
if (!client || !agent) {
throw new Error('Agent not available');
}
@@ -301,52 +303,16 @@ export async function gradeSubmissionWithAgent(
: buildGradingPrompt(submission, task)
});
let initialStream: AsyncIterable<RunStreamEvent>;
try {
const runInvoker = client.agents.createRun(thread.id, agent.id, {
const run = await client.agents.createRun(thread.id, agent.id, {
parallelToolCalls: false
});
initialStream = await runInvoker.stream();
return { threadId: thread.id, runId: run.id };
} catch (err) {
logger.error(`Failed to start run on thread ${thread.id}:`, err);
logger.info('Task:', task);
logger.info('Submission', submission);
return fallbackGrade(submission, task);
}
// 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 };
throw new Error('Failed to start grading run.');
}
}
+4 -4
View File
@@ -35,15 +35,15 @@ export const POST: RequestHandler = async (event: RequestEvent) => {
return new Response(JSON.stringify({ error: 'No valid input provided.' }), { status: 400 });
}
// Call the AI grading agent with threading and fallback
const aiResult = await gradeSubmissionWithAgent(submission, task.trim(), roomId ?? '', hitl);
// Call the AI grading agent asynchronously
const { threadId, runId } = await gradeSubmissionWithAgent(submission, task.trim(), roomId ?? '', hitl);
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
threadId,
runId
}),
{
status: 200,
+102
View File
@@ -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 });
}
};
+84 -91
View File
@@ -40,111 +40,104 @@
}
async function handleSubmit(event: Event) {
event.preventDefault();
submitting = true;
successMsg = '';
errorMsg = '';
const THINKING_EVENT = { tool: 'thinking', time: new Date().toISOString() };
function ensureThinkingStep(events) {
// Remove any existing 'thinking' event
const filtered = events.filter((e) => e.tool !== 'thinking');
return [THINKING_EVENT, ...filtered];
}
toolEvents = [THINKING_EVENT];
event.preventDefault();
submitting = true;
successMsg = '';
errorMsg = '';
const THINKING_EVENT = { tool: 'thinking', time: new Date().toISOString() };
function ensureThinkingStep(events) {
// Remove any existing 'thinking' event
const filtered = events.filter((e) => e.tool !== 'thinking');
return [THINKING_EVENT, ...filtered];
}
toolEvents = [THINKING_EVENT];
// Generate a unique roomId for this submission
roomId = crypto.randomUUID();
// Connect to PartyKit for this session
if (ws) ws.close();
ws = new WebSocket(`${PARTYKIT_BASE_URL}/party/tool-usage-server-${roomId}`);
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
toolEvents = ensureThinkingStep([...toolEvents, data]);
} catch {}
};
// Generate a unique roomId for this submission
roomId = crypto.randomUUID();
// Connect to PartyKit for this session
if (ws) ws.close();
ws = new WebSocket(`${PARTYKIT_BASE_URL}/party/tool-usage-server-${roomId}`);
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
toolEvents = ensureThinkingStep([...toolEvents, data]);
} catch {}
};
if (!taskDescription.trim()) {
errorMsg = 'Please provide a description of the task or assignment.';
if (!taskDescription.trim()) {
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;
return;
}
const formData = new FormData();
formData.append('task', taskDescription.trim());
const { threadId, runId } = data;
await pollForStatus(threadId, runId);
} catch (err: any) {
errorMsg = err.message || 'Failed to start grading.';
submitting = false;
}
}
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');
errorMsg = '';
async function pollForStatus(threadId: string, runId: string) {
humanReviewRequired = false;
aiReasoning = '';
hitlContext = null;
let polling = true;
while (polling) {
await new Promise((resolve) => setTimeout(resolve, 1500));
try {
const response = await fetch('/api/grade', {
method: 'POST',
body: formData
});
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.';
const res = await fetch(`/api/grade/status?threadId=${encodeURIComponent(threadId)}&runId=${encodeURIComponent(runId)}`);
const statusData = await res.json();
if (!statusData.success) {
errorMsg = statusData.error || 'Unknown error during grading.';
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.';
toolEvents = statusData.toolEvents || toolEvents;
if (statusData.status === 'completed') {
polling = false;
submitting = false;
if (statusData.result) {
const result = statusData.result;
aiReasoning = result.reasoning || '';
if (statusData.humanReviewRequired) {
humanReviewRequired = true;
hitlContext = { threadId, runId };
} else {
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}`;
}
} else if (statusData.parseError) {
errorMsg = statusData.parseError;
}
}
} 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;
} catch (err: any) {
errorMsg = err.message || 'Failed to poll grading status.';
submitting = false;
return;
}
}
}
async function submitHumanReview() {
submitting = true;