Kamyar-zeinalipour commited on
Commit
20783d2
Β·
verified Β·
1 Parent(s): 3b50f53

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +169 -13
app.py CHANGED
@@ -6,8 +6,9 @@ Fixes:
6
  - Dataframes use type="array" to ensure list-of-lists I/O
7
  - Robust _apply_edits() to handle empty/short rows and parse errors
8
  - Safer student answer table parsing
9
- Enhancement:
10
  - Personalized Study Summary per student on Analysis & Homework tab
 
11
  Run:
12
  pip install gradio openai
13
  python gradio_edu_app_fixed.py
@@ -15,6 +16,8 @@ Run:
15
 
16
  import json
17
  import uuid
 
 
18
  from typing import List, Dict, Any, Tuple
19
  import gradio as gr
20
 
@@ -65,7 +68,7 @@ def _call_openai_chat(
65
  return resp["choices"][0]["message"]["content"]
66
 
67
 
68
- # --- Prompt templates ------------------------------------------------------------
69
 
70
  SUBTOPIC_PROMPT = """You are a curriculum designer.
71
  Extract at least {min_subtopics} clear, non-overlapping subtopics from the EDUCATIONAL TEXT below.
@@ -108,12 +111,19 @@ SUBTOPICS (the generator must cover these and label each item with the matching
108
  {selected_subtopics}
109
  """
110
 
 
111
  SIMULATE_STUDENT_PROMPT = """You will roleplay as a student with this profile:
112
  ---
113
  {student_profile}
114
  ---
115
- Answer the following questions realistically based on your profile.
116
- For MCQ, answer ONLY the option key (A/B/C/D). For Short Answer, provide a 1–3 sentence response.
 
 
 
 
 
 
117
 
118
  Return ONLY valid JSON:
119
  {{
@@ -123,7 +133,7 @@ Return ONLY valid JSON:
123
  ]
124
  }}
125
 
126
- QUESTIONS (with IDs):
127
  {questions_json}
128
  """
129
 
@@ -190,7 +200,7 @@ PERFORMANCE SUMMARY (Student 2):
190
  {perf_2_json}
191
  """
192
 
193
- # NEW: Personalized study summary prompt
194
  STUDY_SUMMARY_PROMPT = """You are a learning coach. Using the performance summary and the proposed homework for ONE student, write a short **personalized home-study summary** they can follow on their own.
195
 
196
  Include, in order:
@@ -298,26 +308,163 @@ def generate_questions(
298
  return questions
299
 
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  def simulate_student_answers(
302
  api_key: str,
303
  model: str,
304
  student_profile: str,
305
  questions: List[Dict[str, Any]],
306
  ) -> List[Dict[str, Any]]:
 
307
  qpack = [
308
  {
309
  "id": q["id"],
 
310
  "question_type": q["question_type"],
311
  "question": q["question"],
312
  "options": q["options"],
313
  } for q in questions
314
  ]
 
 
 
 
315
  prompt = SIMULATE_STUDENT_PROMPT.format(
316
  student_profile=student_profile.strip(),
 
317
  questions_json=json.dumps(qpack, ensure_ascii=False, indent=2),
318
  )
319
  msg = [
320
- {"role": "system", "content": "Return strictly valid JSON and keep answers realistic given the profile."},
321
  {"role": "user", "content": prompt},
322
  ]
323
  raw = _call_openai_chat(api_key, model, msg, temperature=0.8, max_tokens=3000)
@@ -326,14 +473,22 @@ def simulate_student_answers(
326
  answers = data.get("answers", [])
327
  except Exception:
328
  raise gr.Error("Failed to parse student answers JSON.")
 
 
329
  normalized = []
330
  for a in answers:
331
  qid = a.get("id")
332
  ans = (a.get("answer") or "").strip()
333
  if qid and ans:
334
  normalized.append({"id": qid, "answer": ans})
 
 
335
  q_ids = {q["id"] for q in questions}
336
  filtered = [a for a in normalized if a["id"] in q_ids]
 
 
 
 
337
  return filtered
338
 
339
 
@@ -418,7 +573,7 @@ def prescribe_homework(
418
  "student_2": {"recap": "N/A", "weak_subtopics": [], "homework": []},
419
  }
420
 
421
- # NEW: personalized study summary helper
422
  def summarize_student(
423
  api_key: str,
424
  model: str,
@@ -440,7 +595,7 @@ def summarize_student(
440
  # --- Gradio UI ------------------------------------------------------------------
441
 
442
  with gr.Blocks(css="footer {visibility: hidden}") as demo:
443
- gr.Markdown("# πŸŽ“ Educational Text Tutor (Patched)\nDesign subtopics β†’ generate questions β†’ simulate students β†’ analyze β†’ prescribe homework")
444
 
445
  # App-wide state
446
  st_api_key = gr.State("")
@@ -639,7 +794,7 @@ with gr.Blocks(css="footer {visibility: hidden}") as demo:
639
  hw1 = gr.JSON(label="Student 1 – Homework Plan")
640
  hw2 = gr.JSON(label="Student 2 – Homework Plan")
641
 
642
- # NEW: Personalized study summaries
643
  gr.Markdown("### Student 1 – Personalized Study Summary")
644
  sum1_md = gr.Markdown()
645
  gr.Markdown("### Student 2 – Personalized Study Summary")
@@ -666,7 +821,7 @@ with gr.Blocks(css="footer {visibility: hidden}") as demo:
666
  s1_rx = rx_json.get("student_1", {})
667
  s2_rx = rx_json.get("student_2", {})
668
 
669
- # NEW: generate summaries using performance + homework
670
  s1_sum = summarize_student(api_key, model, by1, s1_rx)
671
  s2_sum = summarize_student(api_key, model, by2, s2_rx)
672
 
@@ -688,7 +843,8 @@ with gr.Blocks(css="footer {visibility: hidden}") as demo:
688
  ],
689
  )
690
 
691
- gr.Markdown("β€” Built with ❀️ using Gradio + OpenAI β€”")
692
 
693
  if __name__ == "__main__":
694
- demo.launch()
 
 
6
  - Dataframes use type="array" to ensure list-of-lists I/O
7
  - Robust _apply_edits() to handle empty/short rows and parse errors
8
  - Safer student answer table parsing
9
+ Enhancements:
10
  - Personalized Study Summary per student on Analysis & Homework tab
11
+ - Profile-aware student simulation with targeted accuracy by subtopic category
12
  Run:
13
  pip install gradio openai
14
  python gradio_edu_app_fixed.py
 
16
 
17
  import json
18
  import uuid
19
+ import re
20
+ import random
21
  from typing import List, Dict, Any, Tuple
22
  import gradio as gr
23
 
 
68
  return resp["choices"][0]["message"]["content"]
69
 
70
 
71
+ # --- Prompt templates (ALL literal braces escaped) ------------------------------
72
 
73
  SUBTOPIC_PROMPT = """You are a curriculum designer.
74
  Extract at least {min_subtopics} clear, non-overlapping subtopics from the EDUCATIONAL TEXT below.
 
111
  {selected_subtopics}
112
  """
113
 
114
+ # policy-aware simulation prompt (subtopic-aware)
115
  SIMULATE_STUDENT_PROMPT = """You will roleplay as a student with this profile:
116
  ---
117
  {student_profile}
118
  ---
119
+
120
+ **Policy (you MUST follow):**
121
+ {policy_json}
122
+
123
+ Guidelines:
124
+ - Use the **subtopic** of each question to decide where to excel vs. struggle.
125
+ - Hit the target accuracy ranges by category (strong/weak/neutral). If needed, deliberately pick a plausible but wrong choice. Never admit you’re doing this.
126
+ - MCQ: answer ONLY the option key (A/B/C/D). Short Answer: 1–3 sentences; on weak areas, it’s ok to be vague, omit a key detail, or make a misconception.
127
 
128
  Return ONLY valid JSON:
129
  {{
 
133
  ]
134
  }}
135
 
136
+ QUESTIONS (with IDs & subtopics):
137
  {questions_json}
138
  """
139
 
 
200
  {perf_2_json}
201
  """
202
 
203
+ # Personalized study summary prompt
204
  STUDY_SUMMARY_PROMPT = """You are a learning coach. Using the performance summary and the proposed homework for ONE student, write a short **personalized home-study summary** they can follow on their own.
205
 
206
  Include, in order:
 
308
  return questions
309
 
310
 
311
+ # --- Policy helpers to force visible divergence between students ----------------
312
+
313
+ def _derive_policy(student_profile: str) -> Dict[str, Any]:
314
+ """Infer strong/weak areas and target accuracies from a free-form profile."""
315
+ p = student_profile.lower()
316
+ strong_terms, weak_terms = set(), set()
317
+
318
+ # Heuristics from profile
319
+ if re.search(r"strong in (definitions?|theor(?:y|ies)|concepts?)", p):
320
+ strong_terms |= {"definition", "definitions", "theory", "theories", "concept", "concepts", "term", "terms"}
321
+ if re.search(r"weak(?:er)? in (definitions?|theor(?:y|ies)|concepts?)", p):
322
+ weak_terms |= {"definition", "definitions", "theory", "theories", "concept", "concepts", "term", "terms"}
323
+
324
+ if re.search(r"strong in (applications?|problem ?solving|calculations?)", p):
325
+ strong_terms |= {"application", "applications", "problem", "problems", "problem solving", "case", "cases", "calculation", "calculations", "practice"}
326
+ if re.search(r"weak(?:er)? in (applications?|problem ?solving|calculations?)", p):
327
+ weak_terms |= {"application", "applications", "problem", "problems", "problem solving", "case", "cases", "calculation", "calculations", "practice"}
328
+
329
+ # Generic defaults if not mentioned
330
+ if not strong_terms and "theor" in p:
331
+ strong_terms |= {"definition","concept","theory","term"}
332
+ if not weak_terms and "careless" in p:
333
+ weak_terms |= {"definition","term"} # careless β†’ slips on definitional precision
334
+
335
+ # Accuracy targets
336
+ overall = 0.65 # baseline realism
337
+ if "anxious" in p: overall -= 0.05
338
+ if "confident" in p: overall += 0.05
339
+
340
+ weak_acc = 0.45
341
+ strong_acc = 0.85
342
+ neutral_acc = overall
343
+
344
+ careless_rate = 0.15 if "careless" in p else 0.05
345
+ variance = 0.05 # small randomness
346
+
347
+ return {
348
+ "strong_terms": sorted(strong_terms),
349
+ "weak_terms": sorted(weak_terms),
350
+ "target_acc": {
351
+ "strong": strong_acc,
352
+ "weak": weak_acc,
353
+ "neutral": neutral_acc
354
+ },
355
+ "overall_target": overall,
356
+ "careless_rate": careless_rate,
357
+ "variance": variance
358
+ }
359
+
360
+ def _classify_subtopic(name: str, policy: Dict[str, Any]) -> str:
361
+ s = (name or "").lower()
362
+ strong_hits = any(t in s for t in policy["strong_terms"])
363
+ weak_hits = any(t in s for t in policy["weak_terms"])
364
+ if weak_hits and not strong_hits:
365
+ return "weak"
366
+ if strong_hits and not weak_hits:
367
+ return "strong"
368
+ return "neutral"
369
+
370
+ def _wrong_option_letter(correct_key: str) -> str:
371
+ pool = ["A","B","C","D"]
372
+ pool = [x for x in pool if x != (correct_key or "").upper()]
373
+ return random.choice(pool) if pool else "A"
374
+
375
+ def _enforce_profile_variation(
376
+ questions: List[Dict[str, Any]],
377
+ answers: List[Dict[str, Any]],
378
+ policy: Dict[str, Any]
379
+ ) -> List[Dict[str, Any]]:
380
+ """Post-process MCQ answers to meet target wrong-rate per category. Short answers untouched."""
381
+ # Indexing
382
+ q_by_id = {q["id"]: q for q in questions}
383
+ ans_by_id = {a["id"]: a["answer"] for a in answers}
384
+
385
+ # Collect MCQs per category
386
+ buckets = {"strong": [], "weak": [], "neutral": []}
387
+ for q in questions:
388
+ if q.get("question_type") != "MCQ":
389
+ continue
390
+ cat = _classify_subtopic(q.get("subtopic",""), policy)
391
+ buckets[cat].append(q["id"])
392
+
393
+ # For each category, compute current and target wrong counts
394
+ for cat, qids in buckets.items():
395
+ if not qids:
396
+ continue
397
+ target_acc = policy["target_acc"][cat]
398
+ # add small variance so runs don't look identical
399
+ target_acc += random.uniform(-policy["variance"], policy["variance"])
400
+ target_acc = max(0.2, min(0.95, target_acc))
401
+
402
+ total = len(qids)
403
+ desired_wrong = round(total * (1 - target_acc))
404
+
405
+ # Compute current wrongs
406
+ current_wrong = 0
407
+ correct_candidates = [] # qids currently correct β†’ can flip to wrong if needed
408
+ for qid in qids:
409
+ q = q_by_id[qid]
410
+ stu = (ans_by_id.get(qid) or "").strip().upper()
411
+ correct = (q.get("correct_key") or "").strip().upper()
412
+ if stu and correct and stu == correct:
413
+ correct_candidates.append(qid)
414
+ else:
415
+ current_wrong += 1
416
+
417
+ need_more_wrong = max(0, desired_wrong - current_wrong)
418
+
419
+ # Flip some correct ones to wrong
420
+ if need_more_wrong > 0 and correct_candidates:
421
+ random.shuffle(correct_candidates)
422
+ for qid in correct_candidates[:need_more_wrong]:
423
+ correct = (q_by_id[qid].get("correct_key") or "").strip().upper()
424
+ ans_by_id[qid] = _wrong_option_letter(correct)
425
+
426
+ # Optional: sprinkle a few careless slips across all categories
427
+ if random.random() < policy["careless_rate"]:
428
+ for qid in random.sample(qids, k=max(0, min(1, len(qids)))):
429
+ correct = (q_by_id[qid].get("correct_key") or "").strip().upper()
430
+ if ans_by_id.get(qid, "").upper() == correct:
431
+ ans_by_id[qid] = _wrong_option_letter(correct)
432
+
433
+ # Rebuild answers list
434
+ out = []
435
+ for a in answers:
436
+ qid = a["id"]
437
+ out.append({"id": qid, "answer": ans_by_id.get(qid, a["answer"])})
438
+ return out
439
+
440
+
441
  def simulate_student_answers(
442
  api_key: str,
443
  model: str,
444
  student_profile: str,
445
  questions: List[Dict[str, Any]],
446
  ) -> List[Dict[str, Any]]:
447
+ # Pack questions with subtopics so the model can bias performance
448
  qpack = [
449
  {
450
  "id": q["id"],
451
+ "subtopic": q["subtopic"],
452
  "question_type": q["question_type"],
453
  "question": q["question"],
454
  "options": q["options"],
455
  } for q in questions
456
  ]
457
+
458
+ # Derive an explicit policy from the free-text profile
459
+ policy = _derive_policy(student_profile)
460
+
461
  prompt = SIMULATE_STUDENT_PROMPT.format(
462
  student_profile=student_profile.strip(),
463
+ policy_json=json.dumps(policy, ensure_ascii=False, indent=2),
464
  questions_json=json.dumps(qpack, ensure_ascii=False, indent=2),
465
  )
466
  msg = [
467
+ {"role": "system", "content": "Return strictly valid JSON and keep answers realistic given the policy."},
468
  {"role": "user", "content": prompt},
469
  ]
470
  raw = _call_openai_chat(api_key, model, msg, temperature=0.8, max_tokens=3000)
 
473
  answers = data.get("answers", [])
474
  except Exception:
475
  raise gr.Error("Failed to parse student answers JSON.")
476
+
477
+ # Normalize
478
  normalized = []
479
  for a in answers:
480
  qid = a.get("id")
481
  ans = (a.get("answer") or "").strip()
482
  if qid and ans:
483
  normalized.append({"id": qid, "answer": ans})
484
+
485
+ # Keep only answers for our questions
486
  q_ids = {q["id"] for q in questions}
487
  filtered = [a for a in normalized if a["id"] in q_ids]
488
+
489
+ # Enforce target variation to visibly differentiate students (MCQ-safe)
490
+ filtered = _enforce_profile_variation(questions, filtered, policy)
491
+
492
  return filtered
493
 
494
 
 
573
  "student_2": {"recap": "N/A", "weak_subtopics": [], "homework": []},
574
  }
575
 
576
+ # Personalized study summary helper
577
  def summarize_student(
578
  api_key: str,
579
  model: str,
 
595
  # --- Gradio UI ------------------------------------------------------------------
596
 
597
  with gr.Blocks(css="footer {visibility: hidden}") as demo:
598
+ gr.Markdown("# πŸŽ“ Educational Tutor\nDesign subtopics β†’ generate questions β†’ simulate students β†’ analyze β†’ prescribe homework")
599
 
600
  # App-wide state
601
  st_api_key = gr.State("")
 
794
  hw1 = gr.JSON(label="Student 1 – Homework Plan")
795
  hw2 = gr.JSON(label="Student 2 – Homework Plan")
796
 
797
+ # Personalized study summaries
798
  gr.Markdown("### Student 1 – Personalized Study Summary")
799
  sum1_md = gr.Markdown()
800
  gr.Markdown("### Student 2 – Personalized Study Summary")
 
821
  s1_rx = rx_json.get("student_1", {})
822
  s2_rx = rx_json.get("student_2", {})
823
 
824
+ # generate summaries using performance + homework
825
  s1_sum = summarize_student(api_key, model, by1, s1_rx)
826
  s2_sum = summarize_student(api_key, model, by2, s2_rx)
827
 
 
843
  ],
844
  )
845
 
846
+ gr.Markdown("β€” Built using Gradio + OpenAI β€”")
847
 
848
  if __name__ == "__main__":
849
+ # Set share=True to get a public link
850
+ demo.launch(share=True)