The FastAPI app is defined in app/main.py. Routes are organized in app/routes.
Start the server:
uvicorn app.main:app --reload --port 8000Grading endpoints are a mix of LLM-backed and rule-based paths:
| Endpoint | Behavior |
|---|---|
POST /api/grade |
Rule-based placeholder (score_answer() in app/services/grader_service.py). Kept for legacy single-answer demos. |
POST /api/grade-batch |
Uses the Gemini RubricGrader when each submission supplies both rubric and question_text; otherwise falls back to score_answer(). |
POST /api/evaluation/calibrate |
Runs the full multi-round calibration loop end-to-end through RubricGrader. |
POST /api/survey-submissions, /api/mistake-stats, /api/revise-rubric |
Rule-based heuristics today, except /api/revise-rubric which delegates to the core revise_rubric logic in src/grading_tool/grading/rubric_reviser.py. |
Endpoints that invoke RubricGrader (/api/grade-batch, /api/evaluation/calibrate) read the key from request headers so the server does not need a process-level GEMINI_API_KEY:
X-API-Key: <key>, orAuthorization: Bearer <key>
If neither header is present, RubricGrader falls back to os.environ["GEMINI_API_KEY"].
Defined in app/routes/health.py.
Response:
{ "status": "ok" }Defined in app/routes/grading.py.
Request schema: GradeRequest in app/schemas/api_models.py
{ "student_id": "S1", "answer": "...", "question_id": "Q1" }Response schema: GradeResult
{
"student_id": "S1",
"question_id": "Q1",
"score": 7.0,
"max_score": 10.0,
"confidence": 0.78,
"review_required": true,
"review_reason": "Borderline score",
"reasoning": "..."
}Each submission in submissions may include rubric, question_text, max_score, benchmark_type, and reference_solution. Providing both rubric and question_text routes that submission through the Gemini RubricGrader; submissions missing either field fall through to the rule-based scorer.
Request: BatchGradeRequest
{
"submissions": [
{
"student_id": "S1",
"question_id": "q3",
"answer": "...",
"question_text": "Enumerate all algorithm design techniques...",
"rubric": { "criteria": [ { "criterion_id": "c1", "points": 4, "description": "..." } ] },
"max_score": 12,
"benchmark_type": "short_answer",
"reference_solution": "..."
}
]
}Headers: X-API-Key: <gemini_key> (or Authorization: Bearer <gemini_key>) is required for Gemini-backed submissions.
Response: BatchGradeResponse
results: list ofGradeResultreview_queue: items requiring review
Request: SurveyBatchRequest
- Accepts
question_text, optionalrubricandsolution, andsubmissions.
Response: SurveyBatchResponse containing SurveyCommentResult[].
Request: MistakeStatsRequest with survey results.
Response: MistakeStatsResponse with mistake clusters.
Request: RubricRevisionRequest
original_rubric- optional
mistake_stats - optional
instructor_note
Response: RubricRevisionResponse
revision_neededrevised_rubricchange_log
Defined in app/routes/evaluation.py.
Request: EvaluationRequest
ai_results:GradeResult[]professor_grades:ProfessorGradeInput[]difference_thresholdinclude_semantic_metrics
Response: EvaluationResponse
metrics: MAE/MSE/variance/within-thresholdflagged_cases
Note: semantic metrics fields exist in schemas, but the current implementation returns None for semantic similarity.
Request: CalibrationRequest
question_id,question_text,original_rubric,solutionsubmissions(GradeRequest[]),professor_grades(ProfessorGradeInput[])max_rounds(default 5),difference_threshold(default 0.5),target_mse,min_improvement(default 0.01),include_semantic_metrics
Headers: X-API-Key: <gemini_key> (or Authorization: Bearer <gemini_key>). Required — every round invokes RubricGrader.
Response: CalibrationResponse
completed_rounds,best_round_index,best_mse,best_rubric,stopping_reasonrounds[]contains the rubric used,grade_results, the fullEvaluationResponse, and arevision_noteper round
For a non-API entry point, see scripts/run_calibration.py.
CORS is configured in app/main.py. Allowed origins include:
- Localhost dev ports
5173,5174,4173(bothlocalhostand127.0.0.1). - Production Vercel deployments (
grading-tool-beige.vercel.app,grading-tool-ruby.vercel.app). - An
allow_origin_regexthat additionally permits any*.vercel.apphost and LAN-private IPs (10.x,172.16–31.x,192.168.x,100.x) on ports5173/5174, so Vite served on a LAN IP works without CORS errors.