gradeops / scripts /pregrade.py
samsadar236
feat: deploy ControlPlane.ai — updated backend, frontend redesign, Responsible AI Gate
54f341c
Raw History Blame Contribute Delete
4.44 kB
"""Pre-grade the demo set into the database (T4.1), via the real API.
Why HTTP and not direct DB writes: this drives the same endpoints the app
already uses (POST /api/exams, /api/rubrics, /api/papers/upload), so the
resulting gradeops.db is exactly what the app produces, with no assumptions
about internal ORM constructors. Run it once against a locally-running
instance the night before the demo, then bake the produced gradeops.db into
the image (see the Dockerfile note at the bottom).
This needs the app running and the grading keys set (GOOGLE_API_KEY / GROQ_API_KEY),
so it is not run in CI.
Manifest (JSON):
{
"session_id": "public",
"exam_title": "Demo Midterm",
"rubrics": [
{"title": "Q1", "question_text": "...", "max_marks": 3.0,
"course_instructions": "...",
"criteria": [{"name": "...", "points": 1.0, "conditions": "..."}]}
],
"papers": [
{"file": "data/demo/paper1.pdf", "rubric_index": 0, "student_anon_id": "STU-001"}
]
}
Usage:
# 1. start the app locally with keys set, then:
python scripts/pregrade.py --manifest data/demo/manifest.json --base http://localhost:8000
# 2. bake the produced gradeops.db into the image (see note below)
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
try:
import requests
except ImportError:
print("pip install requests", file=sys.stderr)
raise
def _post_json(base: str, path: str, body: dict, headers: dict | None = None) -> dict:
r = requests.post(f"{base}{path}", json=body, headers=headers or {}, timeout=120)
r.raise_for_status()
return r.json()
def create_exam(base: str, title: str, session_id: str) -> int:
out = _post_json(base, "/api/exams", {"title": title}, {"X-Session-Id": session_id})
print(f" exam #{out['id']} '{out['title']}'")
return out["id"]
def create_rubric(base: str, exam_id: int, rubric: dict) -> int:
body = {
"exam_id": exam_id,
"title": rubric["title"],
"question_text": rubric["question_text"],
"max_marks": float(rubric["max_marks"]),
"course_instructions": rubric.get("course_instructions", ""),
"criteria": rubric["criteria"],
}
out = _post_json(base, "/api/rubrics", body)
print(f" rubric #{out['id']} '{out['title']}' ({out['version']})")
return out["id"]
def upload_paper(base: str, exam_id: int, rubric_id: int, file_path: str,
student_anon_id: str | None) -> dict:
p = Path(file_path)
if not p.exists():
raise FileNotFoundError(f"paper file not found: {file_path}")
data = {"exam_id": str(exam_id), "rubric_id": str(rubric_id)}
if student_anon_id:
data["student_anon_id"] = student_anon_id
with p.open("rb") as fh:
files = {"file": (p.name, fh)}
r = requests.post(f"{base}/api/papers/upload", data=data, files=files, timeout=1800)
r.raise_for_status()
out = r.json()
print(f" paper #{out['paper_id']} ({out['student_anon_id']}): "
f"{len(out['crop_ids'])} crop(s) graded")
return out
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--manifest", required=True)
ap.add_argument("--base", default="http://localhost:8000")
args = ap.parse_args()
manifest = json.loads(Path(args.manifest).read_text(encoding="utf-8"))
session_id = manifest.get("session_id", "public")
print(f"pre-grading against {args.base} (session '{session_id}')")
exam_id = create_exam(args.base, manifest["exam_title"], session_id)
rubric_ids = [create_rubric(args.base, exam_id, r) for r in manifest["rubrics"]]
total_crops = 0
for paper in manifest["papers"]:
rid = rubric_ids[int(paper.get("rubric_index", 0))]
out = upload_paper(args.base, exam_id, rid, paper["file"],
paper.get("student_anon_id"))
total_crops += len(out["crop_ids"])
print(f"\ndone: exam #{exam_id}, {len(rubric_ids)} rubric(s), {total_crops} crop(s) graded.")
print("The app's gradeops.db is now populated. Bake it into the image:")
print(" Dockerfile: COPY gradeops.db /app/gradeops.db")
print(" and make sure DATABASE_URL points at that path (sqlite:////app/gradeops.db).")
print("The live demo then reads from the DB with no LLM call, and survives restarts.")
if __name__ == "__main__":
main()