rpmjp/projects/skillbridge/quiz_grader.py
CompletedFebruary – April 2026
SkillBridge AI — Test Prep Academy Platform
Multi-tenant AI learning platform for test prep academies. Socratic AI tutor that refuses to give answers, AI quiz generation from uploaded files, async pre-grading, weak-spot detection, and AI parent summaries. Full role-based platform across admin, instructor, student, and parent.
LIVE DEMOPython 3.12FastAPIPostgreSQL 16SQLAlchemy 2.0ReactTypeScriptTailwindGroq / Llama
Languages
Python54.2%
TypeScript45.1%
CSS0.3%
JavaScript0.2%
Dockerfile0.1%
Mako0.1%
quiz_grader.py
"""
AI-powered grading for short-answer quiz questions.
Returns a suggested score + feedback per answer. The instructor reviews
the AI's suggestions and either approves them or overrides with their own
score before final-grade publishing — the AI never publishes a grade on its own.
Runs off the request's critical path (async pre-grading): the student's
submission persists and confirms immediately, and this grading happens
separately so a slow LLM round-trip never makes a student wait.
"""
import json
from app.services.ai_provider import get_provider
_MAX_TOKENS_PER_GRADE = 400
def _build_grading_prompt(
question_prompt: str,
reference_answer: str,
max_points: int,
) -> str:
return f"""You are a fair, careful grader of short-answer quiz questions.
QUESTION: {question_prompt}
REFERENCE ANSWER (what the instructor expects, or key elements to look for):
{reference_answer or "(no reference answer provided — grade based on the question alone)"}
MAXIMUM POINTS: {max_points}
YOUR TASK:
Grade the student's response. Return ONLY valid JSON — no markdown, no commentary.
REQUIRED JSON SHAPE:
{{
"score": <integer between 0 and {max_points}>,
"is_correct": <true if score >= {max_points}, false otherwise>,
"feedback": "<1-2 sentences of constructive feedback for the student>"
}}
GRADING PRINCIPLES:
- Award full credit for fully correct answers, even if phrased differently from the reference.
- Award partial credit for partially correct answers — be specific about what's missing.
- For math: equivalent expressions are correct (e.g. "x = 7", "7", "the value is 7" all credit).
- For short-answer concepts: if the student captures the key idea, give credit even if wording differs.
- Be strict but fair. Don't reward answers that are off-topic or fundamentally wrong.
- Feedback should help the student learn — point to what's right AND what's missing.
- Do NOT mention the reference answer in feedback. Speak directly to the student about their work.
Return only the JSON object."""
def grade_short_answer(
question_prompt: str,
student_answer: str,
reference_answer: str | None,
max_points: int,
) -> dict:
"""
Grade a single short-answer response.
Returns {"score", "is_correct", "feedback", "input_tokens", "output_tokens"}.
Raises ValueError if the AI returns invalid JSON.
"""
if not student_answer or not student_answer.strip():
# Don't waste tokens on empty answers
return {
"score": 0,
"is_correct": False,
"feedback": "No answer was provided.",
"input_tokens": 0,
"output_tokens": 0,
}
system_prompt = _build_grading_prompt(
question_prompt=question_prompt,
reference_answer=reference_answer or "",
max_points=max_points,
)
provider = get_provider()
response = provider.send_message(
system_prompt=system_prompt,
messages=[
{
"role": "user",
"content": f"Student's answer:\n\n{student_answer.strip()}",
}
],
max_tokens=_MAX_TOKENS_PER_GRADE,
)
raw = response.content.strip()
# Strip code fences if the model returned them
if raw.startswith("```"):
raw = raw.lstrip("`")
if raw.lower().startswith("json"):
raw = raw[4:]
raw = raw.strip()
if raw.endswith("```"):
raw = raw[:-3].strip()
try:
parsed = json.loads(raw)
except json.JSONDecodeError as e:
raise ValueError(
f"AI returned invalid grading JSON: {e}. First 200 chars: {raw[:200]!r}"
)
score = int(parsed.get("score", 0))
score = max(0, min(score, max_points)) # clamp to valid range
return {
"score": score,
"is_correct": bool(parsed.get("is_correct", score == max_points)),
"feedback": (parsed.get("feedback") or "").strip(),
"input_tokens": response.input_tokens,
"output_tokens": response.output_tokens,
}