elkay: lesson.py, quiz.py, api.py
Browse files- phase/Student_view/lesson.py +1 -26
- phase/Student_view/quiz.py +3 -5
- utils/api.py +83 -240
phase/Student_view/lesson.py
CHANGED
|
@@ -14,7 +14,6 @@ USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
|
|
| 14 |
|
| 15 |
FALLBACK_TAG = "<!--fallback-->"
|
| 16 |
|
| 17 |
-
|
| 18 |
# ---------------------------------------------
|
| 19 |
# Page state helpers
|
| 20 |
# ---------------------------------------------
|
|
@@ -30,7 +29,6 @@ _SS_DEFAULTS = {
|
|
| 30 |
"chatbot_feedback": None, # str
|
| 31 |
}
|
| 32 |
|
| 33 |
-
|
| 34 |
def _ensure_state():
|
| 35 |
for k, v in _SS_DEFAULTS.items():
|
| 36 |
if k not in st.session_state:
|
|
@@ -137,9 +135,6 @@ MODULES_META: Dict[str, List[Dict[str, Any]]] = {
|
|
| 137 |
"advanced": [],
|
| 138 |
}
|
| 139 |
|
| 140 |
-
|
| 141 |
-
# Helper to read topic titles regardless of whether metadata uses `topics` or `topic_labels`
|
| 142 |
-
|
| 143 |
def _topic_plan(level: str, module_id: int):
|
| 144 |
"""
|
| 145 |
Returns a list of (title, backend_ordinal) after filtering:
|
|
@@ -159,7 +154,6 @@ def _topic_plan(level: str, module_id: int):
|
|
| 159 |
|
| 160 |
# Ensure at most 6 topics: first five + Summary if present
|
| 161 |
if len(plan) > 6:
|
| 162 |
-
# Prefer keeping a 'Summary' entry last if it exists
|
| 163 |
summary_pos = next((idx for idx, (title, _) in enumerate(plan)
|
| 164 |
if title.strip().lower().startswith("summary")), None)
|
| 165 |
if summary_pos is not None:
|
|
@@ -171,7 +165,6 @@ def _topic_plan(level: str, module_id: int):
|
|
| 171 |
def _topic_titles(level: str, module_id: int):
|
| 172 |
return [t for (t, _) in _topic_plan(level, module_id)]
|
| 173 |
|
| 174 |
-
|
| 175 |
# ---------------------------------------------
|
| 176 |
# Backend integrations
|
| 177 |
# ---------------------------------------------
|
|
@@ -220,7 +213,6 @@ def _fetch_topic_from_backend(level: str, module_id: int, topic_idx: int) -> Tup
|
|
| 220 |
|
| 221 |
return ui_title, content
|
| 222 |
|
| 223 |
-
|
| 224 |
def _extract_takeaways(text: str, max_items: int = 5) -> List[str]:
|
| 225 |
"""Heuristic key-takeaway extractor from raw lesson text."""
|
| 226 |
if not text:
|
|
@@ -248,7 +240,6 @@ def _extract_takeaways(text: str, max_items: int = 5) -> List[str]:
|
|
| 248 |
sents = re.split(r"(?<=[.!?])\s+", text.strip())
|
| 249 |
return [s for s in sents if len(s) > 20][:min(max_items, 3)]
|
| 250 |
|
| 251 |
-
|
| 252 |
def _start_quiz(level: str, module_id: int) -> Optional[List[Dict[str, Any]]]:
|
| 253 |
"""Ask backend to generate a 5-question mini quiz for this module."""
|
| 254 |
module_conf = next(m for m in MODULES_META[level] if m["id"] == module_id)
|
|
@@ -265,7 +256,6 @@ def _start_quiz(level: str, module_id: int) -> Optional[List[Dict[str, Any]]]:
|
|
| 265 |
st.error(f"Could not generate quiz: {e}")
|
| 266 |
return None
|
| 267 |
|
| 268 |
-
|
| 269 |
def _submit_quiz(level: str, module_id: int, original_quiz: List[Dict[str, Any]], answers_map: Dict[int, str]) -> Optional[Dict[str, Any]]:
|
| 270 |
"""Submit answers and get score + tutor feedback."""
|
| 271 |
user_answers = []
|
|
@@ -363,7 +353,6 @@ def _fallback_text(title: str, module_id: int, topic_ordinal: int) -> str:
|
|
| 363 |
return (f"This unit will be populated from lesson_{module_id}/topic_{topic_ordinal}.txt. "
|
| 364 |
"For now, review the key idea and write one example from daily life.") + "\n" + FALLBACK_TAG
|
| 365 |
|
| 366 |
-
|
| 367 |
# ---------------------------------------------
|
| 368 |
# UI building blocks
|
| 369 |
# ---------------------------------------------
|
|
@@ -396,8 +385,6 @@ def _render_catalog():
|
|
| 396 |
st.session_state.mode = "lesson"
|
| 397 |
st.rerun()
|
| 398 |
|
| 399 |
-
|
| 400 |
-
|
| 401 |
def _render_lesson():
|
| 402 |
level = st.session_state.level
|
| 403 |
module_id = st.session_state.module_id
|
|
@@ -497,7 +484,6 @@ def _render_lesson():
|
|
| 497 |
st.session_state.topic_idx += 1
|
| 498 |
st.rerun()
|
| 499 |
|
| 500 |
-
|
| 501 |
with st.expander("Module Units", expanded=False):
|
| 502 |
for i, (tt, _) in enumerate(topics):
|
| 503 |
label = f"{i+1}. {tt}"
|
|
@@ -527,7 +513,6 @@ def _get_topics(level: str, module_id: int) -> List[Tuple[str, str]]:
|
|
| 527 |
def _letter_for(i: int) -> str:
|
| 528 |
return chr(ord("A") + i)
|
| 529 |
|
| 530 |
-
|
| 531 |
def _render_quiz():
|
| 532 |
quiz: List[Dict[str, Any]] = st.session_state.quiz_data or []
|
| 533 |
if not quiz:
|
|
@@ -561,7 +546,6 @@ def _render_quiz():
|
|
| 561 |
)
|
| 562 |
st.divider()
|
| 563 |
|
| 564 |
-
# Submit
|
| 565 |
all_answered = len(st.session_state.quiz_answers) == len(quiz)
|
| 566 |
if st.button("Submit Quiz", disabled=not all_answered):
|
| 567 |
with st.spinner("Grading…"):
|
|
@@ -578,8 +562,6 @@ def _render_quiz():
|
|
| 578 |
_send_quiz_summary_to_chatbot(result)
|
| 579 |
st.rerun()
|
| 580 |
|
| 581 |
-
|
| 582 |
-
|
| 583 |
def _render_results():
|
| 584 |
result = st.session_state.quiz_result or {}
|
| 585 |
score = result.get("score", {})
|
|
@@ -628,7 +610,6 @@ def _render_results():
|
|
| 628 |
st.session_state.topic_idx = quiz_index + 1
|
| 629 |
st.rerun()
|
| 630 |
|
| 631 |
-
|
| 632 |
# ---------------------------------------------
|
| 633 |
# Public entry point(s)
|
| 634 |
# ---------------------------------------------
|
|
@@ -703,7 +684,6 @@ def _render_assigned_lesson(lesson_id: int, assignment_id: Optional[int] = None)
|
|
| 703 |
st.session_state[key] = idx + 1
|
| 704 |
st.rerun()
|
| 705 |
|
| 706 |
-
|
| 707 |
def render():
|
| 708 |
_ensure_state()
|
| 709 |
# If we were routed here from the Teacher Link, skip the catalog flow.
|
|
@@ -712,7 +692,6 @@ def render():
|
|
| 712 |
_render_assigned_lesson(int(route.get("lesson_id", 0)), route.get("assignment_id"))
|
| 713 |
return
|
| 714 |
|
| 715 |
-
|
| 716 |
# Breadcrumb
|
| 717 |
st.caption("Learning Path · " + st.session_state.level.capitalize())
|
| 718 |
|
|
@@ -729,14 +708,10 @@ def render():
|
|
| 729 |
st.session_state.mode = "catalog"
|
| 730 |
_render_catalog()
|
| 731 |
|
| 732 |
-
|
| 733 |
# Some parts of the app import pages and call a conventional `show()`
|
| 734 |
show_page = render
|
| 735 |
|
| 736 |
-
|
| 737 |
if __name__ == "__main__":
|
| 738 |
# Allow standalone run for local testing
|
| 739 |
st.set_page_config(page_title="Lesson", page_icon="📘", layout="centered")
|
| 740 |
-
render()
|
| 741 |
-
|
| 742 |
-
#comment
|
|
|
|
| 14 |
|
| 15 |
FALLBACK_TAG = "<!--fallback-->"
|
| 16 |
|
|
|
|
| 17 |
# ---------------------------------------------
|
| 18 |
# Page state helpers
|
| 19 |
# ---------------------------------------------
|
|
|
|
| 29 |
"chatbot_feedback": None, # str
|
| 30 |
}
|
| 31 |
|
|
|
|
| 32 |
def _ensure_state():
|
| 33 |
for k, v in _SS_DEFAULTS.items():
|
| 34 |
if k not in st.session_state:
|
|
|
|
| 135 |
"advanced": [],
|
| 136 |
}
|
| 137 |
|
|
|
|
|
|
|
|
|
|
| 138 |
def _topic_plan(level: str, module_id: int):
|
| 139 |
"""
|
| 140 |
Returns a list of (title, backend_ordinal) after filtering:
|
|
|
|
| 154 |
|
| 155 |
# Ensure at most 6 topics: first five + Summary if present
|
| 156 |
if len(plan) > 6:
|
|
|
|
| 157 |
summary_pos = next((idx for idx, (title, _) in enumerate(plan)
|
| 158 |
if title.strip().lower().startswith("summary")), None)
|
| 159 |
if summary_pos is not None:
|
|
|
|
| 165 |
def _topic_titles(level: str, module_id: int):
|
| 166 |
return [t for (t, _) in _topic_plan(level, module_id)]
|
| 167 |
|
|
|
|
| 168 |
# ---------------------------------------------
|
| 169 |
# Backend integrations
|
| 170 |
# ---------------------------------------------
|
|
|
|
| 213 |
|
| 214 |
return ui_title, content
|
| 215 |
|
|
|
|
| 216 |
def _extract_takeaways(text: str, max_items: int = 5) -> List[str]:
|
| 217 |
"""Heuristic key-takeaway extractor from raw lesson text."""
|
| 218 |
if not text:
|
|
|
|
| 240 |
sents = re.split(r"(?<=[.!?])\s+", text.strip())
|
| 241 |
return [s for s in sents if len(s) > 20][:min(max_items, 3)]
|
| 242 |
|
|
|
|
| 243 |
def _start_quiz(level: str, module_id: int) -> Optional[List[Dict[str, Any]]]:
|
| 244 |
"""Ask backend to generate a 5-question mini quiz for this module."""
|
| 245 |
module_conf = next(m for m in MODULES_META[level] if m["id"] == module_id)
|
|
|
|
| 256 |
st.error(f"Could not generate quiz: {e}")
|
| 257 |
return None
|
| 258 |
|
|
|
|
| 259 |
def _submit_quiz(level: str, module_id: int, original_quiz: List[Dict[str, Any]], answers_map: Dict[int, str]) -> Optional[Dict[str, Any]]:
|
| 260 |
"""Submit answers and get score + tutor feedback."""
|
| 261 |
user_answers = []
|
|
|
|
| 353 |
return (f"This unit will be populated from lesson_{module_id}/topic_{topic_ordinal}.txt. "
|
| 354 |
"For now, review the key idea and write one example from daily life.") + "\n" + FALLBACK_TAG
|
| 355 |
|
|
|
|
| 356 |
# ---------------------------------------------
|
| 357 |
# UI building blocks
|
| 358 |
# ---------------------------------------------
|
|
|
|
| 385 |
st.session_state.mode = "lesson"
|
| 386 |
st.rerun()
|
| 387 |
|
|
|
|
|
|
|
| 388 |
def _render_lesson():
|
| 389 |
level = st.session_state.level
|
| 390 |
module_id = st.session_state.module_id
|
|
|
|
| 484 |
st.session_state.topic_idx += 1
|
| 485 |
st.rerun()
|
| 486 |
|
|
|
|
| 487 |
with st.expander("Module Units", expanded=False):
|
| 488 |
for i, (tt, _) in enumerate(topics):
|
| 489 |
label = f"{i+1}. {tt}"
|
|
|
|
| 513 |
def _letter_for(i: int) -> str:
|
| 514 |
return chr(ord("A") + i)
|
| 515 |
|
|
|
|
| 516 |
def _render_quiz():
|
| 517 |
quiz: List[Dict[str, Any]] = st.session_state.quiz_data or []
|
| 518 |
if not quiz:
|
|
|
|
| 546 |
)
|
| 547 |
st.divider()
|
| 548 |
|
|
|
|
| 549 |
all_answered = len(st.session_state.quiz_answers) == len(quiz)
|
| 550 |
if st.button("Submit Quiz", disabled=not all_answered):
|
| 551 |
with st.spinner("Grading…"):
|
|
|
|
| 562 |
_send_quiz_summary_to_chatbot(result)
|
| 563 |
st.rerun()
|
| 564 |
|
|
|
|
|
|
|
| 565 |
def _render_results():
|
| 566 |
result = st.session_state.quiz_result or {}
|
| 567 |
score = result.get("score", {})
|
|
|
|
| 610 |
st.session_state.topic_idx = quiz_index + 1
|
| 611 |
st.rerun()
|
| 612 |
|
|
|
|
| 613 |
# ---------------------------------------------
|
| 614 |
# Public entry point(s)
|
| 615 |
# ---------------------------------------------
|
|
|
|
| 684 |
st.session_state[key] = idx + 1
|
| 685 |
st.rerun()
|
| 686 |
|
|
|
|
| 687 |
def render():
|
| 688 |
_ensure_state()
|
| 689 |
# If we were routed here from the Teacher Link, skip the catalog flow.
|
|
|
|
| 692 |
_render_assigned_lesson(int(route.get("lesson_id", 0)), route.get("assignment_id"))
|
| 693 |
return
|
| 694 |
|
|
|
|
| 695 |
# Breadcrumb
|
| 696 |
st.caption("Learning Path · " + st.session_state.level.capitalize())
|
| 697 |
|
|
|
|
| 708 |
st.session_state.mode = "catalog"
|
| 709 |
_render_catalog()
|
| 710 |
|
|
|
|
| 711 |
# Some parts of the app import pages and call a conventional `show()`
|
| 712 |
show_page = render
|
| 713 |
|
|
|
|
| 714 |
if __name__ == "__main__":
|
| 715 |
# Allow standalone run for local testing
|
| 716 |
st.set_page_config(page_title="Lesson", page_icon="📘", layout="centered")
|
| 717 |
+
render()
|
|
|
|
|
|
phase/Student_view/quiz.py
CHANGED
|
@@ -29,7 +29,6 @@ def _submit_quiz_result(student_id: int, assignment_id: int, quiz_id: int,
|
|
| 29 |
quiz_id=quiz_id,
|
| 30 |
score=score, total=total, details=details)
|
| 31 |
# backend: POST /quizzes/submit (or your route of choice)
|
| 32 |
-
# utils.api should wrap that route; below assumes api.submit_quiz exists.
|
| 33 |
return api.submit_quiz(student_id=student_id,
|
| 34 |
assignment_id=assignment_id,
|
| 35 |
quiz_id=quiz_id,
|
|
@@ -144,7 +143,6 @@ def get_level_style(level):
|
|
| 144 |
else:
|
| 145 |
return ("#6c757d", level)
|
| 146 |
|
| 147 |
-
|
| 148 |
# --- Sidebar Progress ---
|
| 149 |
def show_quiz_progress_sidebar(quiz_id):
|
| 150 |
qobj = _load_quiz_obj(quiz_id)
|
|
@@ -267,8 +265,6 @@ def show_quiz(quiz_id):
|
|
| 267 |
st.session_state.current_q += 1
|
| 268 |
st.rerun()
|
| 269 |
|
| 270 |
-
|
| 271 |
-
|
| 272 |
# --- Quiz Results ---
|
| 273 |
def show_results(quiz_id):
|
| 274 |
qobj = _load_quiz_obj(quiz_id)
|
|
@@ -391,7 +387,6 @@ def show_quiz_list():
|
|
| 391 |
st.session_state.answers = {}
|
| 392 |
st.rerun()
|
| 393 |
|
| 394 |
-
|
| 395 |
# --- Main Router for Quiz Page ---
|
| 396 |
def show_page():
|
| 397 |
if "selected_quiz" not in st.session_state:
|
|
@@ -411,3 +406,6 @@ def show_page():
|
|
| 411 |
show_quiz(quiz_id)
|
| 412 |
else:
|
| 413 |
show_results(quiz_id)
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
quiz_id=quiz_id,
|
| 30 |
score=score, total=total, details=details)
|
| 31 |
# backend: POST /quizzes/submit (or your route of choice)
|
|
|
|
| 32 |
return api.submit_quiz(student_id=student_id,
|
| 33 |
assignment_id=assignment_id,
|
| 34 |
quiz_id=quiz_id,
|
|
|
|
| 143 |
else:
|
| 144 |
return ("#6c757d", level)
|
| 145 |
|
|
|
|
| 146 |
# --- Sidebar Progress ---
|
| 147 |
def show_quiz_progress_sidebar(quiz_id):
|
| 148 |
qobj = _load_quiz_obj(quiz_id)
|
|
|
|
| 265 |
st.session_state.current_q += 1
|
| 266 |
st.rerun()
|
| 267 |
|
|
|
|
|
|
|
| 268 |
# --- Quiz Results ---
|
| 269 |
def show_results(quiz_id):
|
| 270 |
qobj = _load_quiz_obj(quiz_id)
|
|
|
|
| 387 |
st.session_state.answers = {}
|
| 388 |
st.rerun()
|
| 389 |
|
|
|
|
| 390 |
# --- Main Router for Quiz Page ---
|
| 391 |
def show_page():
|
| 392 |
if "selected_quiz" not in st.session_state:
|
|
|
|
| 406 |
show_quiz(quiz_id)
|
| 407 |
else:
|
| 408 |
show_results(quiz_id)
|
| 409 |
+
|
| 410 |
+
# Note: No changes needed here as this file handles pre-loaded quizzes and teacher-assigned quizzes,
|
| 411 |
+
# which use /quizzes/{quiz_id} and /quizzes/submit, not /generate_quiz.
|
utils/api.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
| 1 |
-
# utils/api.py
|
| 2 |
import os, json, requests
|
| 3 |
from urllib3.util.retry import Retry
|
| 4 |
from requests.adapters import HTTPAdapter
|
| 5 |
|
| 6 |
-
|
| 7 |
# ---- Setup ----
|
| 8 |
BACKEND = (os.getenv("BACKEND_URL") or "").strip().rstrip("/")
|
| 9 |
if not BACKEND:
|
|
@@ -87,8 +85,6 @@ def health():
|
|
| 87 |
except Exception:
|
| 88 |
return {"ok": False}
|
| 89 |
|
| 90 |
-
#---helpers
|
| 91 |
-
|
| 92 |
# --- Optional API prefix (e.g., "/api" or "/v1")
|
| 93 |
API_PREFIX_ENV = (os.getenv("BACKEND_API_PREFIX") or "").strip().rstrip("/")
|
| 94 |
|
|
@@ -123,8 +119,7 @@ def _try_candidates(method: str, candidates: list[tuple[str, dict]]):
|
|
| 123 |
# 404/405/etc.: try next candidate
|
| 124 |
raise RuntimeError("No matching endpoint for this operation. Tried:\n- " + "\n- ".join(tried))
|
| 125 |
|
| 126 |
-
|
| 127 |
-
#--helpers for student_db.py
|
| 128 |
def user_stats(student_id: int):
|
| 129 |
return _req("GET", f"/students/{student_id}/stats").json()
|
| 130 |
def list_assignments_for_student(student_id: int):
|
|
@@ -150,38 +145,56 @@ def student_quiz_average(student_id: int):
|
|
| 150 |
def recent_lessons_for_student(student_id: int, limit: int = 5):
|
| 151 |
return _req("GET", f"/students/{student_id}/recent", params={"limit": limit}).json()
|
| 152 |
|
| 153 |
-
#
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
|
| 180 |
# Optional if you want to compute levels server-side
|
| 181 |
def level_from_xp(xp: int):
|
| 182 |
return _json_or_raise(_req("GET", "/levels/from_xp", params={"xp": xp}))["level"]
|
| 183 |
|
| 184 |
-
#--teacherlink.py helpers
|
| 185 |
def join_class_by_code(student_id: int, code: str):
|
| 186 |
d = _json_or_raise(_req("POST", f"/students/{student_id}/classes/join", json={"code": code}))
|
| 187 |
# backend may return {"class_id": ...} or full class object; both are fine
|
|
@@ -191,7 +204,10 @@ def list_classes_for_student(student_id: int):
|
|
| 191 |
return _json_or_raise(_req("GET", f"/students/{student_id}/classes"))
|
| 192 |
|
| 193 |
def class_content_counts(class_id: int):
|
| 194 |
-
return
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
def student_class_progress(student_id: int, class_id: int):
|
| 197 |
return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/progress"))
|
|
@@ -204,14 +220,10 @@ def leave_class(student_id: int, class_id: int):
|
|
| 204 |
def student_assignments_for_class(student_id: int, class_id: int):
|
| 205 |
return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/assignments"))
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
# ---------- TEACHERS / CLASSES / CONTENT (BACKEND ROUTES THAT EXIST) ----------
|
| 211 |
|
| 212 |
# Classes
|
| 213 |
def create_class(teacher_id: int, name: str):
|
| 214 |
-
# Backend has POST /teachers/{teacher_id}/classes with body {name}
|
| 215 |
return _try_candidates("POST", [
|
| 216 |
(f"/teachers/{teacher_id}/classes", {"json": {"name": name}}),
|
| 217 |
# fallbacks if you ever rename:
|
|
@@ -243,7 +255,6 @@ def teacher_tiles(teacher_id: int):
|
|
| 243 |
return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/tiles"))
|
| 244 |
|
| 245 |
def class_student_metrics(class_id: int):
|
| 246 |
-
# backend: /classes/{id}/students/metrics
|
| 247 |
return _try_candidates("GET", [
|
| 248 |
(f"/classes/{class_id}/students/metrics", {}),
|
| 249 |
# tolerant fallbacks:
|
|
@@ -252,21 +263,18 @@ def class_student_metrics(class_id: int):
|
|
| 252 |
])
|
| 253 |
|
| 254 |
def class_weekly_activity(class_id: int):
|
| 255 |
-
# backend: /classes/{id}/activity/weekly
|
| 256 |
return _try_candidates("GET", [
|
| 257 |
(f"/classes/{class_id}/activity/weekly", {}),
|
| 258 |
(f"/classes/{class_id}/weekly_activity", {}),
|
| 259 |
])
|
| 260 |
|
| 261 |
def class_progress_overview(class_id: int):
|
| 262 |
-
# backend: /classes/{id}/progress
|
| 263 |
return _try_candidates("GET", [
|
| 264 |
(f"/classes/{class_id}/progress", {}),
|
| 265 |
(f"/classes/{class_id}/progress_overview", {}),
|
| 266 |
])
|
| 267 |
|
| 268 |
def class_recent_activity(class_id: int, limit=6, days=30):
|
| 269 |
-
# backend: /classes/{id}/activity/recent
|
| 270 |
return _try_candidates("GET", [
|
| 271 |
(f"/classes/{class_id}/activity/recent", {"params": {"limit": limit, "days": days}}),
|
| 272 |
(f"/classes/{class_id}/recent_activity", {"params": {"limit": limit, "days": days}}),
|
|
@@ -285,13 +293,11 @@ def create_lesson(teacher_id: int, title: str, description: str,
|
|
| 285 |
"level": level,
|
| 286 |
"sections": sections,
|
| 287 |
}
|
| 288 |
-
# backend route:
|
| 289 |
d = _try_candidates("POST", [
|
| 290 |
(f"/teachers/{teacher_id}/lessons", {"json": payload}),
|
| 291 |
# fallback if you later add a flat /lessons route:
|
| 292 |
("/lessons", {"json": {"teacher_id": teacher_id, **payload}}),
|
| 293 |
])
|
| 294 |
-
# tolerate both {"lesson_id": N} or full object with id
|
| 295 |
return d.get("lesson_id", d.get("id", d))
|
| 296 |
|
| 297 |
def get_lesson(lesson_id: int):
|
|
@@ -333,26 +339,28 @@ def set_assignment_progress(student_id: int, assignment_id: int, current_pos: in
|
|
| 333 |
f"/assignments/{assignment_id}/progress",
|
| 334 |
json={"student_id": student_id, "current_pos": current_pos, "progress": progress}).json()
|
| 335 |
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
# def get_quiz(quiz_id: int):
|
| 339 |
-
# return _json_or_raise(_req("GET", f"/quizzes/{quiz_id}"))
|
| 340 |
-
|
| 341 |
-
# def get_quiz(quiz_id: int):
|
| 342 |
-
# # NEW wrapper that hits GET /quizzes/{quiz_id}
|
| 343 |
-
# return _req("GET", f"/quizzes/{quiz_id}")
|
| 344 |
-
|
| 345 |
def get_quiz(quiz_id: int):
|
| 346 |
"""Fetch a teacher-created quiz (GET /quizzes/{quiz_id}) and return JSON."""
|
| 347 |
return _json_or_raise(_req("GET", f"/quizzes/{quiz_id}"))
|
| 348 |
|
| 349 |
-
def submit_quiz(
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
def update_quiz(quiz_id: int,
|
| 355 |
-
|
| 356 |
d = _req("PUT", f"/quizzes/{quiz_id}", json={
|
| 357 |
"teacher_id": teacher_id, "title": title, "items": items, "settings": settings
|
| 358 |
}).json()
|
|
@@ -370,7 +378,6 @@ def list_assigned_students_for_quiz(quiz_id: int):
|
|
| 370 |
|
| 371 |
# Assignments
|
| 372 |
def assign_to_class(lesson_id: int | None, quiz_id: int | None, class_id: int, teacher_id: int, due_at: str | None = None):
|
| 373 |
-
# backend route name is /assign (not /assignments)
|
| 374 |
d = _try_candidates("POST", [
|
| 375 |
("/assign", {"json": {
|
| 376 |
"lesson_id": lesson_id, "quiz_id": quiz_id,
|
|
@@ -383,132 +390,6 @@ def assign_to_class(lesson_id: int | None, quiz_id: int | None, class_id: int, t
|
|
| 383 |
])
|
| 384 |
return bool(d.get("ok", True))
|
| 385 |
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
# # ---- Classes / Teacher endpoints (tolerant) ----
|
| 390 |
-
# def create_class(teacher_id: int, name: str):
|
| 391 |
-
# return _try_candidates("POST", [
|
| 392 |
-
# (f"/teachers/{teacher_id}/classes", {"json": {"name": name}}),
|
| 393 |
-
# (f"/teachers/{teacher_id}/classrooms",{"json": {"name": name}}),
|
| 394 |
-
# ("/classes", {"json": {"teacher_id": teacher_id, "name": name}}),
|
| 395 |
-
# ("/classrooms", {"json": {"teacher_id": teacher_id, "name": name}}),
|
| 396 |
-
# ])
|
| 397 |
-
|
| 398 |
-
# def list_classes_by_teacher(teacher_id: int):
|
| 399 |
-
# return _try_candidates("GET", [
|
| 400 |
-
# (f"/teachers/{teacher_id}/classes", {}),
|
| 401 |
-
# (f"/teachers/{teacher_id}/classrooms", {}),
|
| 402 |
-
# (f"/classes/by-teacher/{teacher_id}", {}),
|
| 403 |
-
# (f"/classrooms/by-teacher/{teacher_id}", {}),
|
| 404 |
-
# ("/classes", {"params": {"teacher_id": teacher_id}}),
|
| 405 |
-
# ("/classrooms", {"params": {"teacher_id": teacher_id}}),
|
| 406 |
-
# ])
|
| 407 |
-
|
| 408 |
-
# def list_students_in_class(class_id: int):
|
| 409 |
-
# return _try_candidates("GET", [
|
| 410 |
-
# (f"/classes/{class_id}/students", {}),
|
| 411 |
-
# (f"/classrooms/{class_id}/students", {}),
|
| 412 |
-
# ("/students", {"params": {"class_id": class_id}}),
|
| 413 |
-
# ])
|
| 414 |
-
|
| 415 |
-
# def class_content_counts(class_id: int):
|
| 416 |
-
# return _try_candidates("GET", [
|
| 417 |
-
# (f"/classes/{class_id}/content_counts", {}),
|
| 418 |
-
# (f"/classrooms/{class_id}/content_counts", {}),
|
| 419 |
-
# (f"/classes/{class_id}/counts", {}),
|
| 420 |
-
# (f"/classrooms/{class_id}/counts", {}),
|
| 421 |
-
# ])
|
| 422 |
-
|
| 423 |
-
# def list_class_assignments(class_id: int):
|
| 424 |
-
# return _try_candidates("GET", [
|
| 425 |
-
# (f"/classes/{class_id}/assignments", {}),
|
| 426 |
-
# (f"/classrooms/{class_id}/assignments", {}),
|
| 427 |
-
# ("/assignments", {"params": {"class_id": class_id}}),
|
| 428 |
-
# ])
|
| 429 |
-
|
| 430 |
-
# def class_analytics(class_id: int):
|
| 431 |
-
# return _try_candidates("GET", [
|
| 432 |
-
# (f"/classes/{class_id}/analytics", {}),
|
| 433 |
-
# (f"/classrooms/{class_id}/analytics", {}),
|
| 434 |
-
# ])
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
# #--contentmanage.py helpers
|
| 438 |
-
|
| 439 |
-
# # ---------- Teacher/content management endpoints (backend Space) ----------
|
| 440 |
-
# def list_classes_by_teacher(teacher_id: int):
|
| 441 |
-
# return _req("GET", f"/teachers/{teacher_id}/classes").json()
|
| 442 |
-
|
| 443 |
-
# def list_all_students_for_teacher(teacher_id: int):
|
| 444 |
-
# return _req("GET", f"/teachers/{teacher_id}/students").json()
|
| 445 |
-
|
| 446 |
-
# def list_lessons_by_teacher(teacher_id: int):
|
| 447 |
-
# return _req("GET", f"/teachers/{teacher_id}/lessons").json()
|
| 448 |
-
|
| 449 |
-
# def list_quizzes_by_teacher(teacher_id: int):
|
| 450 |
-
# return _req("GET", f"/teachers/{teacher_id}/quizzes").json()
|
| 451 |
-
|
| 452 |
-
# def create_lesson(teacher_id: int, title: str, description: str, subject: str, level: str, sections: list[dict]):
|
| 453 |
-
# d = _req("POST", "/lessons", json={
|
| 454 |
-
# "teacher_id": teacher_id, "title": title, "description": description,
|
| 455 |
-
# "subject": subject, "level": level, "sections": sections
|
| 456 |
-
# }).json()
|
| 457 |
-
# return d["lesson_id"]
|
| 458 |
-
|
| 459 |
-
# def update_lesson(lesson_id: int, teacher_id: int, title: str, description: str, subject: str, level: str, sections: list[dict]):
|
| 460 |
-
# d = _req("PUT", f"/lessons/{lesson_id}", json={
|
| 461 |
-
# "teacher_id": teacher_id, "title": title, "description": description,
|
| 462 |
-
# "subject": subject, "level": level, "sections": sections
|
| 463 |
-
# }).json()
|
| 464 |
-
# return bool(d.get("ok", True))
|
| 465 |
-
|
| 466 |
-
# def delete_lesson(lesson_id: int, teacher_id: int):
|
| 467 |
-
# d = _req("DELETE", f"/lessons/{lesson_id}", json={"teacher_id": teacher_id}).json()
|
| 468 |
-
# return bool(d.get("ok", True)), d.get("message", "")
|
| 469 |
-
|
| 470 |
-
# def get_lesson(lesson_id: int):
|
| 471 |
-
# return _req("GET", f"/lessons/{lesson_id}").json() # {"lesson":{...}, "sections":[...]}
|
| 472 |
-
|
| 473 |
-
# def create_quiz(lesson_id: int, title: str, items: list[dict], settings: dict):
|
| 474 |
-
# d = _req("POST", "/quizzes", json={"lesson_id": lesson_id, "title": title, "items": items, "settings": settings}).json()
|
| 475 |
-
# return d["quiz_id"]
|
| 476 |
-
|
| 477 |
-
# def update_quiz(quiz_id: int, teacher_id: int, title: str, items: list[dict], settings: dict):
|
| 478 |
-
# d = _req("PUT", f"/quizzes/{quiz_id}", json={"teacher_id": teacher_id, "title": title, "items": items, "settings": settings}).json()
|
| 479 |
-
# return bool(d.get("ok", True))
|
| 480 |
-
|
| 481 |
-
# def delete_quiz(quiz_id: int, teacher_id: int):
|
| 482 |
-
# d = _req("DELETE", f"/quizzes/{quiz_id}", json={"teacher_id": teacher_id}).json()
|
| 483 |
-
# return bool(d.get("ok", True)), d.get("message", "")
|
| 484 |
-
|
| 485 |
-
# def list_assigned_students_for_lesson(lesson_id: int):
|
| 486 |
-
# return _req("GET", f"/lessons/{lesson_id}/assignees").json()
|
| 487 |
-
|
| 488 |
-
# def list_assigned_students_for_quiz(quiz_id: int):
|
| 489 |
-
# return _req("GET", f"/quizzes/{quiz_id}/assignees").json()
|
| 490 |
-
|
| 491 |
-
# def assign_to_class(lesson_id: int | None, quiz_id: int | None, class_id: int, teacher_id: int):
|
| 492 |
-
# d = _req("POST", "/assignments", json={
|
| 493 |
-
# "lesson_id": lesson_id, "quiz_id": quiz_id, "class_id": class_id, "teacher_id": teacher_id
|
| 494 |
-
# }).json()
|
| 495 |
-
# return bool(d.get("ok", True))
|
| 496 |
-
|
| 497 |
-
# #-- studentlist helpers
|
| 498 |
-
|
| 499 |
-
# def list_classes_by_teacher(teacher_id: int):
|
| 500 |
-
# return _req("GET", f"/teachers/{teacher_id}/classes").json()
|
| 501 |
-
|
| 502 |
-
# def get_class(class_id: int):
|
| 503 |
-
# return _req("GET", f"/classes/{class_id}").json()
|
| 504 |
-
|
| 505 |
-
# def class_student_metrics(class_id: int):
|
| 506 |
-
# # expected to return list of rows with fields used in the UI
|
| 507 |
-
# return _req("GET", f"/classes/{class_id}/students").json()
|
| 508 |
-
|
| 509 |
-
# def list_assignments_for_student(student_id: int):
|
| 510 |
-
# return _req("GET", f"/students/{student_id}/assignments").json()
|
| 511 |
-
|
| 512 |
# ---------- LLM-based quiz generation (backend uses GEN_MODEL) ----------
|
| 513 |
def generate_quiz_from_text(content: str, n_questions: int = 5, subject: str = "finance", level: str = "beginner"):
|
| 514 |
"""
|
|
@@ -519,6 +400,23 @@ def generate_quiz_from_text(content: str, n_questions: int = 5, subject: str = "
|
|
| 519 |
"content": content, "n_questions": n_questions, "subject": subject, "level": level
|
| 520 |
}).json()
|
| 521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
|
| 523 |
# ---- Legacy agent endpoints (keep) ----
|
| 524 |
def start_agent(student_id: int, lesson_id: int, level_slug: str):
|
|
@@ -526,9 +424,9 @@ def start_agent(student_id: int, lesson_id: int, level_slug: str):
|
|
| 526 |
json={"student_id": student_id, "lesson_id": lesson_id, "level_slug": level_slug}))
|
| 527 |
|
| 528 |
def get_agent_quiz(student_id: int, lesson_id: int, level_slug: str):
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
|
| 533 |
def grade_quiz(student_id: int, lesson_id: int, level_slug: str,
|
| 534 |
answers: list[str], assignment_id: int | None = None):
|
|
@@ -552,7 +450,6 @@ def signup_student(name: str, email: str, password: str, level_label: str, count
|
|
| 552 |
"name": name, "email": email, "password": password,
|
| 553 |
"level_label": level_label, "country_label": country_label
|
| 554 |
}
|
| 555 |
-
# Prefer dedicated route; fall back to /auth/register with role
|
| 556 |
return _try_candidates("POST", [
|
| 557 |
("/auth/signup/student", {"json": payload_student}),
|
| 558 |
("/auth/register", {"json": {
|
|
@@ -587,9 +484,7 @@ def submit_practice_quiz(lesson: str, responses: dict):
|
|
| 587 |
def send_to_chatbot(messages: list[dict]):
|
| 588 |
return _json_or_raise(_req("POST", "/chatbot", json={"messages": messages}))
|
| 589 |
|
| 590 |
-
|
| 591 |
# --- Game API helpers ---
|
| 592 |
-
|
| 593 |
def record_money_match_play(user_id: int, target: int, total: int,
|
| 594 |
elapsed_ms: int, matched: bool, gained_xp: int):
|
| 595 |
payload = {
|
|
@@ -614,7 +509,6 @@ def record_budget_builder_play(user_id: int, weekly_allowance: int, budget_score
|
|
| 614 |
("/games/budget_builder/record", {"json": payload}),
|
| 615 |
])
|
| 616 |
|
| 617 |
-
|
| 618 |
def record_debt_dilemma_play(user_id: int, loans_cleared: int,
|
| 619 |
mistakes: int, elapsed_ms: int, gained_xp: int):
|
| 620 |
payload = {
|
|
@@ -628,68 +522,17 @@ def record_debt_dilemma_play(user_id: int, loans_cleared: int,
|
|
| 628 |
("/games/debt_dilemma/record", {"json": payload}),
|
| 629 |
])
|
| 630 |
|
| 631 |
-
|
| 632 |
def record_profit_puzzler_play(user_id: int, puzzles_solved: int, mistakes: int, elapsed_ms: int, gained_xp: int | None = None):
|
| 633 |
payload = {"user_id": user_id, "puzzles_solved": puzzles_solved, "mistakes": mistakes, "elapsed_ms": elapsed_ms}
|
| 634 |
if gained_xp is not None:
|
| 635 |
payload["gained_xp"] = gained_xp
|
| 636 |
return _try_candidates("POST", [("/games/profit_puzzler/record", {"json": payload})])
|
| 637 |
|
| 638 |
-
|
| 639 |
-
def generate_quiz(*, lesson_id: int | None, level_slug: str | None, lesson_title: str | None):
|
| 640 |
-
payload = {
|
| 641 |
-
"lesson_id": int(lesson_id) if lesson_id is not None else None,
|
| 642 |
-
"level_slug": level_slug,
|
| 643 |
-
"lesson_title": lesson_title,
|
| 644 |
-
"k": 12,
|
| 645 |
-
}
|
| 646 |
-
|
| 647 |
-
try:
|
| 648 |
-
resp = _try_candidates("POST", [
|
| 649 |
-
("/quiz/auto", {"json": payload}),
|
| 650 |
-
])
|
| 651 |
-
except RuntimeError:
|
| 652 |
-
# Fallback for older backend that only has /generate_quiz
|
| 653 |
-
resp = _try_candidates("POST", [
|
| 654 |
-
("/generate_quiz", {"json": {
|
| 655 |
-
"lesson_id": payload["lesson_id"],
|
| 656 |
-
"level_slug": payload["level_slug"],
|
| 657 |
-
"lesson_title": payload["lesson_title"], # REQUIRED
|
| 658 |
-
"k": payload["k"], # REQUIRED
|
| 659 |
-
}}),
|
| 660 |
-
])
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
if isinstance(resp, dict):
|
| 664 |
-
return resp.get("items") or resp.get("quiz") or []
|
| 665 |
-
return resp if isinstance(resp, list) else []
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
def submit_quiz(*, lesson_id: int | None, level_slug: str | None,
|
| 670 |
-
user_answers: list[dict], original_quiz: list[dict]):
|
| 671 |
-
"""
|
| 672 |
-
Grade the quiz on the backend. Returns:
|
| 673 |
-
{"score":{"correct":int,"total":int}, "wrong":[...], "feedback":str}
|
| 674 |
-
"""
|
| 675 |
-
payload = {
|
| 676 |
-
"lesson_id": int(lesson_id) if lesson_id is not None else None,
|
| 677 |
-
"level_slug": level_slug,
|
| 678 |
-
"user_answers": user_answers, # [{"question":"...", "answer":"A"}...]
|
| 679 |
-
"original_quiz": original_quiz, # [{"question","options","answer_key"}...]
|
| 680 |
-
}
|
| 681 |
-
|
| 682 |
-
return _try_candidates("POST", [
|
| 683 |
-
("/quiz/grade", {"json": payload}), # only this
|
| 684 |
-
])
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
def tutor_explain(lesson_id: int, level_slug: str, wrong: list[dict]):
|
| 689 |
-
|
| 690 |
"lesson_id": lesson_id,
|
| 691 |
"level_slug": level_slug,
|
| 692 |
"wrong": wrong
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
return r.json()["feedback"]
|
|
|
|
|
|
|
| 1 |
import os, json, requests
|
| 2 |
from urllib3.util.retry import Retry
|
| 3 |
from requests.adapters import HTTPAdapter
|
| 4 |
|
|
|
|
| 5 |
# ---- Setup ----
|
| 6 |
BACKEND = (os.getenv("BACKEND_URL") or "").strip().rstrip("/")
|
| 7 |
if not BACKEND:
|
|
|
|
| 85 |
except Exception:
|
| 86 |
return {"ok": False}
|
| 87 |
|
|
|
|
|
|
|
| 88 |
# --- Optional API prefix (e.g., "/api" or "/v1")
|
| 89 |
API_PREFIX_ENV = (os.getenv("BACKEND_API_PREFIX") or "").strip().rstrip("/")
|
| 90 |
|
|
|
|
| 119 |
# 404/405/etc.: try next candidate
|
| 120 |
raise RuntimeError("No matching endpoint for this operation. Tried:\n- " + "\n- ".join(tried))
|
| 121 |
|
| 122 |
+
# -- Helpers for student_db.py
|
|
|
|
| 123 |
def user_stats(student_id: int):
|
| 124 |
return _req("GET", f"/students/{student_id}/stats").json()
|
| 125 |
def list_assignments_for_student(student_id: int):
|
|
|
|
| 145 |
def recent_lessons_for_student(student_id: int, limit: int = 5):
|
| 146 |
return _req("GET", f"/students/{student_id}/recent", params={"limit": limit}).json()
|
| 147 |
|
| 148 |
+
# -- Teacher endpoints (backend Space)
|
| 149 |
+
def create_class(teacher_id: int, name: str):
|
| 150 |
+
return _try_candidates("POST", [
|
| 151 |
+
(f"/teachers/{teacher_id}/classes", {"json": {"name": name}}),
|
| 152 |
+
# fallbacks if you ever rename:
|
| 153 |
+
("/classes", {"json": {"teacher_id": teacher_id, "name": name}}),
|
| 154 |
+
])
|
| 155 |
|
| 156 |
+
def teacher_tiles(teacher_id: int):
|
| 157 |
+
return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/tiles"))
|
| 158 |
|
| 159 |
+
def list_classes_by_teacher(teacher_id: int):
|
| 160 |
+
return _try_candidates("GET", [
|
| 161 |
+
(f"/teachers/{teacher_id}/classes", {}),
|
| 162 |
+
])
|
| 163 |
|
| 164 |
+
def class_student_metrics(class_id: int):
|
| 165 |
+
return _try_candidates("GET", [
|
| 166 |
+
(f"/classes/{class_id}/students/metrics", {}),
|
| 167 |
+
# tolerant fallbacks:
|
| 168 |
+
(f"/classes/{class_id}/student_metrics", {}),
|
| 169 |
+
(f"/classes/{class_id}/students", {}), # older shape (list of students)
|
| 170 |
+
])
|
| 171 |
|
| 172 |
+
def class_weekly_activity(class_id: int):
|
| 173 |
+
return _try_candidates("GET", [
|
| 174 |
+
(f"/classes/{class_id}/activity/weekly", {}),
|
| 175 |
+
(f"/classes/{class_id}/weekly_activity", {}),
|
| 176 |
+
])
|
| 177 |
|
| 178 |
+
def class_progress_overview(class_id: int):
|
| 179 |
+
return _try_candidates("GET", [
|
| 180 |
+
(f"/classes/{class_id}/progress", {}),
|
| 181 |
+
(f"/classes/{class_id}/progress_overview", {}),
|
| 182 |
+
])
|
| 183 |
|
| 184 |
+
def class_recent_activity(class_id: int, limit=6, days=30):
|
| 185 |
+
return _try_candidates("GET", [
|
| 186 |
+
(f"/classes/{class_id}/activity/recent", {"params": {"limit": limit, "days": days}}),
|
| 187 |
+
(f"/classes/{class_id}/recent_activity", {"params": {"limit": limit, "days": days}}),
|
| 188 |
+
])
|
| 189 |
|
| 190 |
+
def list_students_in_class(class_id: int):
|
| 191 |
+
return _json_or_raise(_req("GET", f"/classes/{class_id}/students"))
|
| 192 |
|
| 193 |
# Optional if you want to compute levels server-side
|
| 194 |
def level_from_xp(xp: int):
|
| 195 |
return _json_or_raise(_req("GET", "/levels/from_xp", params={"xp": xp}))["level"]
|
| 196 |
|
| 197 |
+
# -- teacherlink.py helpers
|
| 198 |
def join_class_by_code(student_id: int, code: str):
|
| 199 |
d = _json_or_raise(_req("POST", f"/students/{student_id}/classes/join", json={"code": code}))
|
| 200 |
# backend may return {"class_id": ...} or full class object; both are fine
|
|
|
|
| 204 |
return _json_or_raise(_req("GET", f"/students/{student_id}/classes"))
|
| 205 |
|
| 206 |
def class_content_counts(class_id: int):
|
| 207 |
+
return _try_candidates("GET", [
|
| 208 |
+
(f"/classes/{class_id}/content_counts", {}),
|
| 209 |
+
(f"/classes/{class_id}/counts", {}),
|
| 210 |
+
])
|
| 211 |
|
| 212 |
def student_class_progress(student_id: int, class_id: int):
|
| 213 |
return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/progress"))
|
|
|
|
| 220 |
def student_assignments_for_class(student_id: int, class_id: int):
|
| 221 |
return _json_or_raise(_req("GET", f"/classes/{class_id}/students/{student_id}/assignments"))
|
| 222 |
|
|
|
|
|
|
|
|
|
|
| 223 |
# ---------- TEACHERS / CLASSES / CONTENT (BACKEND ROUTES THAT EXIST) ----------
|
| 224 |
|
| 225 |
# Classes
|
| 226 |
def create_class(teacher_id: int, name: str):
|
|
|
|
| 227 |
return _try_candidates("POST", [
|
| 228 |
(f"/teachers/{teacher_id}/classes", {"json": {"name": name}}),
|
| 229 |
# fallbacks if you ever rename:
|
|
|
|
| 255 |
return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/tiles"))
|
| 256 |
|
| 257 |
def class_student_metrics(class_id: int):
|
|
|
|
| 258 |
return _try_candidates("GET", [
|
| 259 |
(f"/classes/{class_id}/students/metrics", {}),
|
| 260 |
# tolerant fallbacks:
|
|
|
|
| 263 |
])
|
| 264 |
|
| 265 |
def class_weekly_activity(class_id: int):
|
|
|
|
| 266 |
return _try_candidates("GET", [
|
| 267 |
(f"/classes/{class_id}/activity/weekly", {}),
|
| 268 |
(f"/classes/{class_id}/weekly_activity", {}),
|
| 269 |
])
|
| 270 |
|
| 271 |
def class_progress_overview(class_id: int):
|
|
|
|
| 272 |
return _try_candidates("GET", [
|
| 273 |
(f"/classes/{class_id}/progress", {}),
|
| 274 |
(f"/classes/{class_id}/progress_overview", {}),
|
| 275 |
])
|
| 276 |
|
| 277 |
def class_recent_activity(class_id: int, limit=6, days=30):
|
|
|
|
| 278 |
return _try_candidates("GET", [
|
| 279 |
(f"/classes/{class_id}/activity/recent", {"params": {"limit": limit, "days": days}}),
|
| 280 |
(f"/classes/{class_id}/recent_activity", {"params": {"limit": limit, "days": days}}),
|
|
|
|
| 293 |
"level": level,
|
| 294 |
"sections": sections,
|
| 295 |
}
|
|
|
|
| 296 |
d = _try_candidates("POST", [
|
| 297 |
(f"/teachers/{teacher_id}/lessons", {"json": payload}),
|
| 298 |
# fallback if you later add a flat /lessons route:
|
| 299 |
("/lessons", {"json": {"teacher_id": teacher_id, **payload}}),
|
| 300 |
])
|
|
|
|
| 301 |
return d.get("lesson_id", d.get("id", d))
|
| 302 |
|
| 303 |
def get_lesson(lesson_id: int):
|
|
|
|
| 339 |
f"/assignments/{assignment_id}/progress",
|
| 340 |
json={"student_id": student_id, "current_pos": current_pos, "progress": progress}).json()
|
| 341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
def get_quiz(quiz_id: int):
|
| 343 |
"""Fetch a teacher-created quiz (GET /quizzes/{quiz_id}) and return JSON."""
|
| 344 |
return _json_or_raise(_req("GET", f"/quizzes/{quiz_id}"))
|
| 345 |
|
| 346 |
+
def submit_quiz(*, lesson_id: int | None, level_slug: str | None,
|
| 347 |
+
user_answers: list[dict], original_quiz: list[dict]):
|
| 348 |
+
"""
|
| 349 |
+
Grade the quiz on the backend. Returns:
|
| 350 |
+
{"score":{"correct":int,"total":int}, "wrong":[...], "feedback":str}
|
| 351 |
+
"""
|
| 352 |
+
payload = {
|
| 353 |
+
"lesson_id": int(lesson_id) if lesson_id is not None else None,
|
| 354 |
+
"level_slug": level_slug,
|
| 355 |
+
"user_answers": user_answers, # [{"question":"...", "answer":"A"}...]
|
| 356 |
+
"original_quiz": original_quiz, # [{"question","options","answer_key"}...]
|
| 357 |
+
}
|
| 358 |
+
return _try_candidates("POST", [
|
| 359 |
+
("/quiz/submit", {"json": payload}), # Updated to match backend
|
| 360 |
+
])
|
| 361 |
|
| 362 |
def update_quiz(quiz_id: int,
|
| 363 |
+
teacher_id: int, title: str, items: list[dict], settings: dict):
|
| 364 |
d = _req("PUT", f"/quizzes/{quiz_id}", json={
|
| 365 |
"teacher_id": teacher_id, "title": title, "items": items, "settings": settings
|
| 366 |
}).json()
|
|
|
|
| 378 |
|
| 379 |
# Assignments
|
| 380 |
def assign_to_class(lesson_id: int | None, quiz_id: int | None, class_id: int, teacher_id: int, due_at: str | None = None):
|
|
|
|
| 381 |
d = _try_candidates("POST", [
|
| 382 |
("/assign", {"json": {
|
| 383 |
"lesson_id": lesson_id, "quiz_id": quiz_id,
|
|
|
|
| 390 |
])
|
| 391 |
return bool(d.get("ok", True))
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
# ---------- LLM-based quiz generation (backend uses GEN_MODEL) ----------
|
| 394 |
def generate_quiz_from_text(content: str, n_questions: int = 5, subject: str = "finance", level: str = "beginner"):
|
| 395 |
"""
|
|
|
|
| 400 |
"content": content, "n_questions": n_questions, "subject": subject, "level": level
|
| 401 |
}).json()
|
| 402 |
|
| 403 |
+
def generate_quiz(*, lesson_id: int | None, level_slug: str | None, lesson_title: str | None):
|
| 404 |
+
payload = {
|
| 405 |
+
"lesson_id": int(lesson_id) if lesson_id is not None else None,
|
| 406 |
+
"level_slug": level_slug,
|
| 407 |
+
"lesson_title": lesson_title,
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
try:
|
| 411 |
+
resp = _try_candidates("POST", [
|
| 412 |
+
("/quiz/auto", {"json": payload}),
|
| 413 |
+
])
|
| 414 |
+
except RuntimeError as e:
|
| 415 |
+
raise RuntimeError(f"Failed to generate quiz: {str(e)}")
|
| 416 |
+
|
| 417 |
+
if isinstance(resp, dict):
|
| 418 |
+
return resp.get("items") or resp.get("quiz") or []
|
| 419 |
+
return resp if isinstance(resp, list) else []
|
| 420 |
|
| 421 |
# ---- Legacy agent endpoints (keep) ----
|
| 422 |
def start_agent(student_id: int, lesson_id: int, level_slug: str):
|
|
|
|
| 424 |
json={"student_id": student_id, "lesson_id": lesson_id, "level_slug": level_slug}))
|
| 425 |
|
| 426 |
def get_agent_quiz(student_id: int, lesson_id: int, level_slug: str):
|
| 427 |
+
d = _json_or_raise(_req("POST", "/agent/quiz",
|
| 428 |
+
json={"student_id": student_id, "lesson_id": lesson_id, "level_slug": level_slug}))
|
| 429 |
+
return d["items"]
|
| 430 |
|
| 431 |
def grade_quiz(student_id: int, lesson_id: int, level_slug: str,
|
| 432 |
answers: list[str], assignment_id: int | None = None):
|
|
|
|
| 450 |
"name": name, "email": email, "password": password,
|
| 451 |
"level_label": level_label, "country_label": country_label
|
| 452 |
}
|
|
|
|
| 453 |
return _try_candidates("POST", [
|
| 454 |
("/auth/signup/student", {"json": payload_student}),
|
| 455 |
("/auth/register", {"json": {
|
|
|
|
| 484 |
def send_to_chatbot(messages: list[dict]):
|
| 485 |
return _json_or_raise(_req("POST", "/chatbot", json={"messages": messages}))
|
| 486 |
|
|
|
|
| 487 |
# --- Game API helpers ---
|
|
|
|
| 488 |
def record_money_match_play(user_id: int, target: int, total: int,
|
| 489 |
elapsed_ms: int, matched: bool, gained_xp: int):
|
| 490 |
payload = {
|
|
|
|
| 509 |
("/games/budget_builder/record", {"json": payload}),
|
| 510 |
])
|
| 511 |
|
|
|
|
| 512 |
def record_debt_dilemma_play(user_id: int, loans_cleared: int,
|
| 513 |
mistakes: int, elapsed_ms: int, gained_xp: int):
|
| 514 |
payload = {
|
|
|
|
| 522 |
("/games/debt_dilemma/record", {"json": payload}),
|
| 523 |
])
|
| 524 |
|
|
|
|
| 525 |
def record_profit_puzzler_play(user_id: int, puzzles_solved: int, mistakes: int, elapsed_ms: int, gained_xp: int | None = None):
|
| 526 |
payload = {"user_id": user_id, "puzzles_solved": puzzles_solved, "mistakes": mistakes, "elapsed_ms": elapsed_ms}
|
| 527 |
if gained_xp is not None:
|
| 528 |
payload["gained_xp"] = gained_xp
|
| 529 |
return _try_candidates("POST", [("/games/profit_puzzler/record", {"json": payload})])
|
| 530 |
|
| 531 |
+
# --- Tutor Explanation ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
def tutor_explain(lesson_id: int, level_slug: str, wrong: list[dict]):
|
| 533 |
+
payload = {
|
| 534 |
"lesson_id": lesson_id,
|
| 535 |
"level_slug": level_slug,
|
| 536 |
"wrong": wrong
|
| 537 |
+
}
|
| 538 |
+
return _json_or_raise(_req("POST", "/tutor/explain", json=payload, timeout=60))
|
|
|