| 61 | |
| 62 | |
| 63 | def load_questions_from_json(json_path: str) -> Dict[str, List[Question]]: |
| 64 | with open(json_path, "r", encoding="utf-8") as f: |
| 65 | raw = json.load(f) |
| 66 | |
| 67 | concept_questions: Dict[str, List[Question]] = {} |
| 68 | for concept, qlist in raw.items(): |
| 69 | qs: List[Question] = [] |
| 70 | for q in qlist: |
| 71 | # Normalize option order to A-D |
| 72 | options_dict = q.get("options", {}) |
| 73 | ordered_keys = ["A", "B", "C", "D"] |
| 74 | options = [options_dict[k] for k in ordered_keys if k in options_dict] |
| 75 | # Convert correct answer from letter to text to match grading logic |
| 76 | ans_letter = q.get("answer", "").strip().upper() |
| 77 | if ans_letter not in ["A", "B", "C", "D"]: |
| 78 | # Skip and log if error occurs instead of raising |
| 79 | print( |
| 80 | f"[WARN] Invalid answer letter '{ans_letter}' for concept '{concept}' question '{q.get('question','')[:40]}...'" |
| 81 | ) |
| 82 | continue |
| 83 | ans_idx = ord(ans_letter) - ord("A") |
| 84 | if ans_idx >= len(options): |
| 85 | print(f"[WARN] Answer index out of range for concept '{concept}'") |
| 86 | continue |
| 87 | |
| 88 | qs.append( |
| 89 | Question( |
| 90 | question=q.get("question", ""), |
| 91 | options=options, |
| 92 | correct_answer=options[ans_idx], |
| 93 | difficulty=q.get("difficulty", "medium"), |
| 94 | ) |
| 95 | ) |
| 96 | if qs: |
| 97 | concept_questions[concept] = qs |
| 98 | return concept_questions |
| 99 | |
| 100 | |
| 101 | @retry(max_retries=3, base_delay=0.6, jitter=0.3) |