Skip to main content
Glama
bulkGrade.ts6.91 kB
import { listSubmissions, Submission } from "../assignments/listSubmissions.js"; import { gradeWithRubric } from "./gradeWithRubric.js"; export interface GradeResult { points?: number; rubricAssessment?: Record<string, { points: number; ratingId?: string; comments?: string; }>; grade?: string | number; comment?: string; } export interface BulkGradeInput { courseIdentifier: string | number; assignmentId: string | number; gradingFunction: (submission: Submission) => GradeResult | null | Promise<GradeResult | null>; dryRun?: boolean; maxConcurrent?: number; rateLimitDelay?: number; } export interface BulkGradeResult { total: number; graded: number; skipped: number; failed: number; failedResults: Array<{ userId: number; error: string; }>; } /** * Process submissions in concurrent batches */ async function processBatch( submissions: Submission[], input: BulkGradeInput, stats: { graded: number; skipped: number; failed: number }, failedResults: Array<{ userId: number; error: string }> ): Promise<void> { const results = await Promise.allSettled( submissions.map(async (submission) => { try { // Run grading function (may be async) const gradeResult = await Promise.resolve(input.gradingFunction(submission)); if (!gradeResult) { // Skip this submission stats.skipped++; console.log(`Skipped submission for user ${submission.user_id}`); return { status: 'skipped' as const, userId: submission.user_id }; } if (!input.dryRun) { // Actually grade the submission await gradeWithRubric({ courseIdentifier: input.courseIdentifier, assignmentId: input.assignmentId, userId: submission.user_id, rubricAssessment: gradeResult.rubricAssessment, grade: gradeResult.grade, comment: gradeResult.comment }); } stats.graded++; console.log(`✓ Graded submission for user ${submission.user_id}`); return { status: 'success' as const, userId: submission.user_id }; } catch (error: any) { stats.failed++; const errorMsg = error.message || String(error); failedResults.push({ userId: submission.user_id, error: errorMsg }); console.error(`✗ Failed to grade user ${submission.user_id}: ${errorMsg}`); return { status: 'failed' as const, userId: submission.user_id, error: errorMsg }; } }) ); return; } /** * Grade multiple submissions efficiently with concurrent processing. * * THIS IS THE MOST TOKEN-EFFICIENT WAY TO GRADE BULK SUBMISSIONS. * * The grading function runs locally in the execution environment, * processing submissions in parallel batches without loading all data into Claude's context. * Only the summary results flow back to Claude. * * Token savings example: * - Traditional approach: 90 submissions × 15K tokens each = 1.35M tokens * - Bulk grade approach: ~3.5K tokens total (99.7% reduction!) * * The grading function receives each submission and should return: * - GradeResult object if the submission should be graded * - null if the submission should be skipped * - Promise resolving to either (async functions supported) * * @param input - Configuration for bulk grading * @param input.gradingFunction - Function that analyzes each submission locally (can be async) * @param input.dryRun - If true, analyze but don't actually grade (for testing) * @param input.maxConcurrent - Max concurrent grading operations (default: 5) * @param input.rateLimitDelay - Delay between batches in ms (default: 1000) * * @example * ```typescript * // Grade Jupyter notebooks that run without errors * await bulkGrade({ * courseIdentifier: "60366", * assignmentId: "123", * gradingFunction: async (submission) => { * // Find notebook file * const notebook = submission.attachments?.find( * f => f.filename.endsWith('.ipynb') * ); * * if (!notebook) { * return null; // Skip - no notebook * } * * // Analyze notebook (runs locally, can be async!) * const hasErrors = await checkNotebook(notebook.url); * * if (hasErrors) { * return { * points: 50, * rubricAssessment: { "_8027": { points: 50 } }, * comment: "Notebook has errors. Please fix and resubmit." * }; * } * * return { * points: 100, * rubricAssessment: { "_8027": { points: 100 } }, * comment: "Excellent! Notebook runs without errors." * }; * } * }); * ``` */ export async function bulkGrade( input: BulkGradeInput ): Promise<BulkGradeResult> { const maxConcurrent = input.maxConcurrent || 5; const rateLimitDelay = input.rateLimitDelay || 1000; console.log(`Starting bulk grading for assignment ${input.assignmentId}...`); console.log(`Concurrent processing: ${maxConcurrent} submissions per batch`); // Fetch all submissions (stays in execution environment) const submissions = await listSubmissions({ courseIdentifier: input.courseIdentifier, assignmentId: input.assignmentId }); console.log(`Found ${submissions.length} submissions to process`); const stats = { graded: 0, skipped: 0, failed: 0 }; const failedResults: Array<{ userId: number; error: string }> = []; // Process in batches to respect rate limits for (let i = 0; i < submissions.length; i += maxConcurrent) { const batch = submissions.slice(i, i + maxConcurrent); const batchNum = Math.floor(i / maxConcurrent) + 1; const totalBatches = Math.ceil(submissions.length / maxConcurrent); console.log(`\nProcessing batch ${batchNum}/${totalBatches} (${batch.length} submissions)...`); await processBatch(batch, input, stats, failedResults); // Rate limit between batches (except after the last batch) if (i + maxConcurrent < submissions.length) { console.log(`Waiting ${rateLimitDelay}ms before next batch...`); await new Promise(resolve => setTimeout(resolve, rateLimitDelay)); } } const summary: BulkGradeResult = { total: submissions.length, graded: stats.graded, skipped: stats.skipped, failed: stats.failed, failedResults: failedResults // Return ALL failures, not just first 5 }; console.log(`\n${'='.repeat(50)}`); console.log(`Bulk grading complete!`); console.log(`${'='.repeat(50)}`); console.log(` Total: ${summary.total}`); console.log(` Graded: ${summary.graded}`); console.log(` Skipped: ${summary.skipped}`); console.log(` Failed: ${summary.failed}`); if (summary.failed > 0) { console.log(`\nFailed submissions:`); failedResults.forEach(({ userId, error }) => { console.log(` User ${userId}: ${error}`); }); } return summary; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vishalsachdev/canvas-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server