Kerikim commited on
Commit
fbe1d49
·
1 Parent(s): 1d8456d

elkay: lesson.py, quiz.py, api.py

Browse files
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
- # # --- Teacher endpoints (backend Space) ---
154
- # def create_class(teacher_id: int, name: str):
155
- # return _json_or_raise(_req("POST", f"/teachers/{teacher_id}/classes",
156
- # json={"name": name}))
 
 
 
157
 
158
- # def teacher_tiles(teacher_id: int):
159
- # return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/tiles"))
160
 
161
- # def list_classes_by_teacher(teacher_id: int):
162
- # return _json_or_raise(_req("GET", f"/teachers/{teacher_id}/classes"))
 
 
163
 
164
- # def class_student_metrics(class_id: int):
165
- # return _json_or_raise(_req("GET", f"/classes/{class_id}/student_metrics"))
 
 
 
 
 
166
 
167
- # def class_weekly_activity(class_id: int):
168
- # return _json_or_raise(_req("GET", f"/classes/{class_id}/weekly_activity"))
 
 
 
169
 
170
- # def class_progress_overview(class_id: int):
171
- # return _json_or_raise(_req("GET", f"/classes/{class_id}/progress_overview"))
 
 
 
172
 
173
- # def class_recent_activity(class_id: int, limit=6, days=30):
174
- # return _json_or_raise(_req("GET", f"/classes/{class_id}/recent_activity",
175
- # params={"limit": limit, "days": days}))
 
 
176
 
177
- # def list_students_in_class(class_id: int):
178
- # return _json_or_raise(_req("GET", f"/classes/{class_id}/students"))
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 _json_or_raise(_req("GET", f"/classes/{class_id}/counts"))
 
 
 
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(student_id, assignment_id, quiz_id, score, total, details):
350
- payload = {"student_id": student_id, "assignment_id": assignment_id,
351
- "quiz_id": quiz_id, "score": score, "total": total, "details": details}
352
- return _req("POST", "/quizzes/submit", json=payload)
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  def update_quiz(quiz_id: int,
355
- teacher_id: int, title: str, items: list[dict], settings: dict):
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
- d = _json_or_raise(_req("POST", "/agent/quiz",
530
- json={"student_id": student_id, "lesson_id": lesson_id, "level_slug": level_slug}))
531
- return d["items"]
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
- r = requests.post(f"{BACKEND}/tutor/explain", json={
690
  "lesson_id": lesson_id,
691
  "level_slug": level_slug,
692
  "wrong": wrong
693
- }, timeout=60)
694
- r.raise_for_status()
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))