Spaces:
Running
Running
Arin Sujal commited on
Add files via upload
Browse files- backend/models.py +1 -0
- backend/quiz_engine.py +102 -25
backend/models.py
CHANGED
|
@@ -525,6 +525,7 @@ class StudentDescriptiveQuestionResult(BaseModel):
|
|
| 525 |
key_points_covered: List[str] = Field(default_factory=list)
|
| 526 |
key_points_missing: List[str] = Field(default_factory=list)
|
| 527 |
evidence_quote: Optional[str] = None
|
|
|
|
| 528 |
|
| 529 |
|
| 530 |
class StudentDescriptiveQuizResult(BaseModel):
|
|
|
|
| 525 |
key_points_covered: List[str] = Field(default_factory=list)
|
| 526 |
key_points_missing: List[str] = Field(default_factory=list)
|
| 527 |
evidence_quote: Optional[str] = None
|
| 528 |
+
explanation: Optional[str] = None
|
| 529 |
|
| 530 |
|
| 531 |
class StudentDescriptiveQuizResult(BaseModel):
|
backend/quiz_engine.py
CHANGED
|
@@ -98,16 +98,68 @@ def _is_similar_question(q_text: str, existing_q_texts: List[str], threshold: fl
|
|
| 98 |
return False
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
def _is_valid_mcq(q: Any) -> bool:
|
| 102 |
-
"""Validates that a generated MCQ is complete, non-dummy,
|
| 103 |
if not isinstance(q, QuizQuestion) or not q.question or len(q.question.strip()) < 10:
|
| 104 |
return False
|
| 105 |
-
if
|
|
|
|
|
|
|
| 106 |
return False
|
| 107 |
seen_texts = set()
|
|
|
|
| 108 |
for opt in q.options:
|
| 109 |
text = opt.text.strip()
|
| 110 |
opt_id = opt.id.strip().upper()
|
|
|
|
|
|
|
|
|
|
| 111 |
if len(text) <= 1:
|
| 112 |
return False
|
| 113 |
if text.upper() in (opt_id, f"OPTION {opt_id}", "A", "B", "C", "D"):
|
|
@@ -116,6 +168,17 @@ def _is_valid_mcq(q: Any) -> bool:
|
|
| 116 |
if lower_t in seen_texts:
|
| 117 |
return False
|
| 118 |
seen_texts.add(lower_t)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
return True
|
| 120 |
|
| 121 |
|
|
@@ -143,11 +206,13 @@ def _is_valid_truefalse(q: Any) -> bool:
|
|
| 143 |
|
| 144 |
|
| 145 |
def _is_valid_descriptive(q: Any) -> bool:
|
| 146 |
-
"""Validates that a generated Descriptive question is complete, non-dummy, and has a substantive answer."""
|
| 147 |
if not isinstance(q, DescriptiveQuestion) or not q.question or len(q.question.strip()) < 10:
|
| 148 |
return False
|
| 149 |
if not q.model_answer or len(q.model_answer.strip()) < 15:
|
| 150 |
return False
|
|
|
|
|
|
|
| 151 |
return True
|
| 152 |
|
| 153 |
|
|
@@ -599,7 +664,7 @@ class RAGQuizEngine:
|
|
| 599 |
self.gemini_api_key = new_key.strip()
|
| 600 |
self._init_client()
|
| 601 |
|
| 602 |
-
def _create_fresh_llm(self):
|
| 603 |
"""Creates a fresh LangChain LLM instance bound to the active event loop to prevent 'Event loop is closed' errors."""
|
| 604 |
if not self.token:
|
| 605 |
return None
|
|
@@ -609,7 +674,7 @@ class RAGQuizEngine:
|
|
| 609 |
repo_id=self.model,
|
| 610 |
huggingfacehub_api_token=self.token,
|
| 611 |
task="text-generation",
|
| 612 |
-
max_new_tokens=
|
| 613 |
temperature=0.7
|
| 614 |
)
|
| 615 |
return ChatHuggingFace(llm=endpoint)
|
|
@@ -643,16 +708,20 @@ class RAGQuizEngine:
|
|
| 643 |
anchor = BLOOM_ANCHORS[idx % len(BLOOM_ANCHORS)]
|
| 644 |
seed = random.randint(1000, 9999999)
|
| 645 |
|
| 646 |
-
|
| 647 |
if context_chunks and len(context_chunks) > 0:
|
| 648 |
focal = context_chunks[idx % len(context_chunks)]
|
| 649 |
-
focal_clause = f"\nFOCAL PASSAGE EXCERPT (Base this question on the specific facts or events in this excerpt):\n{focal}\n"
|
| 650 |
|
| 651 |
exclude_clause = ""
|
| 652 |
if exclude_stems:
|
| 653 |
clean_stems = [s[:60] for s in exclude_stems[:3]]
|
| 654 |
exclude_clause = f"\nDO NOT repeat or ask about these already covered topics: {'; '.join(clean_stems)}\n"
|
| 655 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
if is_hindi:
|
| 657 |
system_prompt = (
|
| 658 |
f"You are an expert Hindi language and literature (हिन्दी भाषा एवं साहित्य) examiner for "
|
|
@@ -672,9 +741,8 @@ class RAGQuizEngine:
|
|
| 672 |
+ (f"Topic: {topic}\n" if topic else "")
|
| 673 |
+ f"Question #{idx+1} Cognitive Level: {anchor['level']}\n"
|
| 674 |
f"Entropy Seed: {seed}\n"
|
| 675 |
-
f"{focal_clause}"
|
| 676 |
f"{exclude_clause}\n"
|
| 677 |
-
f"
|
| 678 |
"Generate exactly 1 high-quality multiple choice question with 4 options, the correct option, and an explanation."
|
| 679 |
)
|
| 680 |
else:
|
|
@@ -689,7 +757,10 @@ class RAGQuizEngine:
|
|
| 689 |
"3. CRITICAL RULE FOR OPTIONS:\n"
|
| 690 |
" - The 'text' field of each option MUST contain the full descriptive answer statement (e.g. '100 pesos', 'To preserve Lencho faith in God', 'It increases quadratically').\n"
|
| 691 |
" - NEVER set 'text' to just 'A', 'B', 'C', 'D' or single letters. All 4 option texts must be meaningful and distinct.\n"
|
| 692 |
-
"4.
|
|
|
|
|
|
|
|
|
|
| 693 |
"5. Shuffle the correct option position randomly.\n"
|
| 694 |
"6. SCIENTIFIC NOTATION: Use clean symbols (e.g. ε₀, μ₀, π, ℏ, °C) and superscripts/subscripts (e.g. 10⁻¹², C² N⁻¹ m⁻², m/s²).\n"
|
| 695 |
"7. NEVER mention page numbers or figure numbers."
|
|
@@ -699,10 +770,9 @@ class RAGQuizEngine:
|
|
| 699 |
+ (f"Topic: {topic}\n" if topic else "")
|
| 700 |
+ f"Question #{idx+1} Cognitive Level: {anchor['level']}\n"
|
| 701 |
f"Entropy Seed: {seed}\n"
|
| 702 |
-
f"{focal_clause}"
|
| 703 |
f"{exclude_clause}\n"
|
| 704 |
-
f"
|
| 705 |
-
"Generate exactly 1 high-quality multiple choice question with 4 options, the correct option, and an explanation."
|
| 706 |
)
|
| 707 |
|
| 708 |
messages = [
|
|
@@ -723,11 +793,11 @@ class RAGQuizEngine:
|
|
| 723 |
))
|
| 724 |
|
| 725 |
q_val = getattr(raw, "question", None) or (raw.get("question") if isinstance(raw, dict) else "")
|
| 726 |
-
corr_val = getattr(raw, "correct_option", None) or (raw.get("correct_option") if isinstance(raw, dict) else "
|
| 727 |
expl_val = getattr(raw, "explanation", None) or (raw.get("explanation") if isinstance(raw, dict) else "")
|
| 728 |
|
| 729 |
q_text = format_scientific_symbols(_sanitize_text(str(q_val)))
|
| 730 |
-
corr_opt =
|
| 731 |
expl = format_scientific_symbols(str(expl_val))
|
| 732 |
|
| 733 |
candidate = QuizQuestion(
|
|
@@ -754,7 +824,7 @@ class RAGQuizEngine:
|
|
| 754 |
|
| 755 |
for r in results:
|
| 756 |
if isinstance(r, QuizQuestion) and _is_valid_mcq(r):
|
| 757 |
-
if not _is_similar_question(r.question, accepted_stems):
|
| 758 |
valid_questions.append(r)
|
| 759 |
accepted_stems.append(r.question)
|
| 760 |
if len(valid_questions) == num_questions:
|
|
@@ -772,7 +842,7 @@ class RAGQuizEngine:
|
|
| 772 |
topup_results = await asyncio.gather(*topup_tasks, return_exceptions=True)
|
| 773 |
for r in topup_results:
|
| 774 |
if isinstance(r, QuizQuestion) and _is_valid_mcq(r):
|
| 775 |
-
if not _is_similar_question(r.question, accepted_stems):
|
| 776 |
valid_questions.append(r)
|
| 777 |
accepted_stems.append(r.question)
|
| 778 |
if len(valid_questions) == num_questions:
|
|
@@ -788,7 +858,7 @@ class RAGQuizEngine:
|
|
| 788 |
final_topup_results = await asyncio.gather(*final_topup_tasks, return_exceptions=True)
|
| 789 |
for r in final_topup_results:
|
| 790 |
if isinstance(r, QuizQuestion) and _is_valid_mcq(r):
|
| 791 |
-
if not _is_similar_question(r.question, accepted_stems):
|
| 792 |
valid_questions.append(r)
|
| 793 |
accepted_stems.append(r.question)
|
| 794 |
if len(valid_questions) == num_questions:
|
|
@@ -1332,8 +1402,9 @@ class RAGQuizEngine:
|
|
| 1332 |
"1. Formulate 1 clear, academic descriptive question grounded strictly in the provided textbook context.\n"
|
| 1333 |
"2. MODEL ANSWER: Provide a thorough, authoritative reference answer that would achieve full marks.\n"
|
| 1334 |
"3. KEY FACTS: List 3 to 6 distinct, concrete facts, terms, or steps that are mandatory in a correct answer.\n"
|
| 1335 |
-
"4.
|
| 1336 |
-
"5.
|
|
|
|
| 1337 |
)
|
| 1338 |
user_prompt = (
|
| 1339 |
f"Board: {board_id.upper()} | Class: {class_id} | Subject: {subject} | Chapter: {chapter}\n"
|
|
@@ -1342,7 +1413,7 @@ class RAGQuizEngine:
|
|
| 1342 |
f"Entropy Seed: {seed}\n"
|
| 1343 |
f"{exclude_clause}\n"
|
| 1344 |
f"{knowledge_block}\n\n"
|
| 1345 |
-
f"Generate exactly 1 descriptive question with model answer
|
| 1346 |
)
|
| 1347 |
|
| 1348 |
messages = [
|
|
@@ -1363,7 +1434,10 @@ class RAGQuizEngine:
|
|
| 1363 |
q_text = format_scientific_symbols(_sanitize_text(str(q_val)))
|
| 1364 |
model_ans = format_scientific_symbols(str(ans_val))
|
| 1365 |
cleaned_facts = [format_scientific_symbols(str(f).strip()) for f in facts_val if str(f).strip()]
|
| 1366 |
-
|
|
|
|
|
|
|
|
|
|
| 1367 |
|
| 1368 |
candidate = DescriptiveQuestion(
|
| 1369 |
id=idx + 1,
|
|
@@ -3068,7 +3142,8 @@ class RAGQuizEngine:
|
|
| 3068 |
feedback="No answer provided.",
|
| 3069 |
key_points_covered=[],
|
| 3070 |
key_points_missing=q.key_facts,
|
| 3071 |
-
evidence_quote=q.source_context
|
|
|
|
| 3072 |
)
|
| 3073 |
|
| 3074 |
system_prompt = (
|
|
@@ -3136,7 +3211,8 @@ class RAGQuizEngine:
|
|
| 3136 |
feedback=str(fb) or ("Good answer." if is_correct else "Needs improvement on key details."),
|
| 3137 |
key_points_covered=[str(c) for c in covered if str(c).strip()],
|
| 3138 |
key_points_missing=[str(m) for m in missing if str(m).strip()],
|
| 3139 |
-
evidence_quote=str(ev) if ev else q.source_context
|
|
|
|
| 3140 |
)
|
| 3141 |
except Exception as e:
|
| 3142 |
print(f"[AI Engine] Error grading descriptive question #{q.id}: {e}")
|
|
@@ -3154,7 +3230,8 @@ class RAGQuizEngine:
|
|
| 3154 |
feedback="Automated grading fallback applied.",
|
| 3155 |
key_points_covered=[],
|
| 3156 |
key_points_missing=q.key_facts,
|
| 3157 |
-
evidence_quote=q.source_context
|
|
|
|
| 3158 |
)
|
| 3159 |
|
| 3160 |
async def grade_all_answers() -> List[StudentDescriptiveQuestionResult]:
|
|
|
|
| 98 |
return False
|
| 99 |
|
| 100 |
|
| 101 |
+
def _resolve_mcq_correct_option(corr_val: Any, options: List[MCQOption]) -> Optional[str]:
|
| 102 |
+
"""
|
| 103 |
+
Robustly resolves correct option letter ('A', 'B', 'C', 'D') even if LLM returns
|
| 104 |
+
'Option B', '(C)', 'B.', or the full text of the option statement.
|
| 105 |
+
"""
|
| 106 |
+
if not corr_val:
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
raw_str = str(corr_val).strip()
|
| 110 |
+
upper_str = raw_str.upper()
|
| 111 |
+
|
| 112 |
+
# Direct match: 'A', 'B', 'C', 'D'
|
| 113 |
+
if upper_str in ("A", "B", "C", "D"):
|
| 114 |
+
return upper_str
|
| 115 |
+
|
| 116 |
+
# Prefix/regex match: 'Option B', '(B)', 'B:', 'B)'
|
| 117 |
+
match = re.search(r'(?:OPTION|ANSWER)?\s*[\(\[]?\s*([A-D])\s*[\)\]\:\.\-]?', upper_str)
|
| 118 |
+
if match and len(raw_str) <= 12:
|
| 119 |
+
return match.group(1)
|
| 120 |
+
|
| 121 |
+
# If LLM returned text (e.g. "Newton's first law..."), find the best matching option
|
| 122 |
+
corr_tokens = _normalize_tokens(raw_str)
|
| 123 |
+
if corr_tokens and options:
|
| 124 |
+
best_opt_id = None
|
| 125 |
+
best_score = 0.0
|
| 126 |
+
for opt in options:
|
| 127 |
+
opt_tokens = _normalize_tokens(opt.text)
|
| 128 |
+
if not opt_tokens:
|
| 129 |
+
continue
|
| 130 |
+
inter = len(corr_tokens & opt_tokens)
|
| 131 |
+
union = len(corr_tokens | opt_tokens)
|
| 132 |
+
score = (inter / union) if union > 0 else 0.0
|
| 133 |
+
if score > best_score and score >= 0.40:
|
| 134 |
+
best_score = score
|
| 135 |
+
best_opt_id = opt.id.upper()
|
| 136 |
+
if best_opt_id in ("A", "B", "C", "D"):
|
| 137 |
+
return best_opt_id
|
| 138 |
+
|
| 139 |
+
# Fallback to single character if it is strictly A-D
|
| 140 |
+
first_char = upper_str[:1]
|
| 141 |
+
if first_char in ("A", "B", "C", "D"):
|
| 142 |
+
return first_char
|
| 143 |
+
|
| 144 |
+
return None
|
| 145 |
+
|
| 146 |
+
|
| 147 |
def _is_valid_mcq(q: Any) -> bool:
|
| 148 |
+
"""Validates that a generated MCQ is complete, non-dummy, has valid correct_option, and 4 distinct options."""
|
| 149 |
if not isinstance(q, QuizQuestion) or not q.question or len(q.question.strip()) < 10:
|
| 150 |
return False
|
| 151 |
+
if not q.correct_option or q.correct_option.strip().upper() not in ("A", "B", "C", "D"):
|
| 152 |
+
return False
|
| 153 |
+
if not q.options or len(q.options) != 4:
|
| 154 |
return False
|
| 155 |
seen_texts = set()
|
| 156 |
+
option_ids = set()
|
| 157 |
for opt in q.options:
|
| 158 |
text = opt.text.strip()
|
| 159 |
opt_id = opt.id.strip().upper()
|
| 160 |
+
if opt_id not in ("A", "B", "C", "D") or opt_id in option_ids:
|
| 161 |
+
return False
|
| 162 |
+
option_ids.add(opt_id)
|
| 163 |
if len(text) <= 1:
|
| 164 |
return False
|
| 165 |
if text.upper() in (opt_id, f"OPTION {opt_id}", "A", "B", "C", "D"):
|
|
|
|
| 168 |
if lower_t in seen_texts:
|
| 169 |
return False
|
| 170 |
seen_texts.add(lower_t)
|
| 171 |
+
|
| 172 |
+
# Check pairwise word overlap (Jaccard similarity) between options to catch near-duplicates / rephrasings
|
| 173 |
+
tokens_list = [_normalize_tokens(opt.text) for opt in q.options]
|
| 174 |
+
for i in range(len(tokens_list)):
|
| 175 |
+
for j in range(i + 1, len(tokens_list)):
|
| 176 |
+
t1, t2 = tokens_list[i], tokens_list[j]
|
| 177 |
+
if len(t1) >= 5 and len(t2) >= 5:
|
| 178 |
+
inter = len(t1 & t2)
|
| 179 |
+
union = len(t1 | t2)
|
| 180 |
+
if union > 0 and (inter / union) >= 0.82:
|
| 181 |
+
return False
|
| 182 |
return True
|
| 183 |
|
| 184 |
|
|
|
|
| 206 |
|
| 207 |
|
| 208 |
def _is_valid_descriptive(q: Any) -> bool:
|
| 209 |
+
"""Validates that a generated Descriptive question is complete, non-dummy, and has a substantive answer and explanation."""
|
| 210 |
if not isinstance(q, DescriptiveQuestion) or not q.question or len(q.question.strip()) < 10:
|
| 211 |
return False
|
| 212 |
if not q.model_answer or len(q.model_answer.strip()) < 15:
|
| 213 |
return False
|
| 214 |
+
if not q.explanation or len(q.explanation.strip()) < 5 or q.explanation.strip().lower() in ("none", "null"):
|
| 215 |
+
return False
|
| 216 |
return True
|
| 217 |
|
| 218 |
|
|
|
|
| 664 |
self.gemini_api_key = new_key.strip()
|
| 665 |
self._init_client()
|
| 666 |
|
| 667 |
+
def _create_fresh_llm(self, max_new_tokens: int = 700):
|
| 668 |
"""Creates a fresh LangChain LLM instance bound to the active event loop to prevent 'Event loop is closed' errors."""
|
| 669 |
if not self.token:
|
| 670 |
return None
|
|
|
|
| 674 |
repo_id=self.model,
|
| 675 |
huggingfacehub_api_token=self.token,
|
| 676 |
task="text-generation",
|
| 677 |
+
max_new_tokens=max_new_tokens,
|
| 678 |
temperature=0.7
|
| 679 |
)
|
| 680 |
return ChatHuggingFace(llm=endpoint)
|
|
|
|
| 708 |
anchor = BLOOM_ANCHORS[idx % len(BLOOM_ANCHORS)]
|
| 709 |
seed = random.randint(1000, 9999999)
|
| 710 |
|
| 711 |
+
focal = None
|
| 712 |
if context_chunks and len(context_chunks) > 0:
|
| 713 |
focal = context_chunks[idx % len(context_chunks)]
|
|
|
|
| 714 |
|
| 715 |
exclude_clause = ""
|
| 716 |
if exclude_stems:
|
| 717 |
clean_stems = [s[:60] for s in exclude_stems[:3]]
|
| 718 |
exclude_clause = f"\nDO NOT repeat or ask about these already covered topics: {'; '.join(clean_stems)}\n"
|
| 719 |
|
| 720 |
+
knowledge_block = (
|
| 721 |
+
f"FOCAL TEXTBOOK EXCERPT (Base your question and options strictly on this specific passage):\n{focal}\n"
|
| 722 |
+
if focal else f"Textbook Knowledge:\n{context_text}\n"
|
| 723 |
+
)
|
| 724 |
+
|
| 725 |
if is_hindi:
|
| 726 |
system_prompt = (
|
| 727 |
f"You are an expert Hindi language and literature (हिन्दी भाषा एवं साहित्य) examiner for "
|
|
|
|
| 741 |
+ (f"Topic: {topic}\n" if topic else "")
|
| 742 |
+ f"Question #{idx+1} Cognitive Level: {anchor['level']}\n"
|
| 743 |
f"Entropy Seed: {seed}\n"
|
|
|
|
| 744 |
f"{exclude_clause}\n"
|
| 745 |
+
f"{knowledge_block}\n\n"
|
| 746 |
"Generate exactly 1 high-quality multiple choice question with 4 options, the correct option, and an explanation."
|
| 747 |
)
|
| 748 |
else:
|
|
|
|
| 757 |
"3. CRITICAL RULE FOR OPTIONS:\n"
|
| 758 |
" - The 'text' field of each option MUST contain the full descriptive answer statement (e.g. '100 pesos', 'To preserve Lencho faith in God', 'It increases quadratically').\n"
|
| 759 |
" - NEVER set 'text' to just 'A', 'B', 'C', 'D' or single letters. All 4 option texts must be meaningful and distinct.\n"
|
| 760 |
+
"4. ALL 4 OPTIONS MUST BE DISTINCT AND UNIQUE:\n"
|
| 761 |
+
" - Exactly ONE option must be correct. The other 3 must be plausible distractors.\n"
|
| 762 |
+
" - Distractors must NOT be slight rephrasings or synonyms of the correct answer or each other.\n"
|
| 763 |
+
" - Each distractor must represent a completely distinct concept, alternative scientific law/effect, or different outcome.\n"
|
| 764 |
"5. Shuffle the correct option position randomly.\n"
|
| 765 |
"6. SCIENTIFIC NOTATION: Use clean symbols (e.g. ε₀, μ₀, π, ℏ, °C) and superscripts/subscripts (e.g. 10⁻¹², C² N⁻¹ m⁻², m/s²).\n"
|
| 766 |
"7. NEVER mention page numbers or figure numbers."
|
|
|
|
| 770 |
+ (f"Topic: {topic}\n" if topic else "")
|
| 771 |
+ f"Question #{idx+1} Cognitive Level: {anchor['level']}\n"
|
| 772 |
f"Entropy Seed: {seed}\n"
|
|
|
|
| 773 |
f"{exclude_clause}\n"
|
| 774 |
+
f"{knowledge_block}\n\n"
|
| 775 |
+
"Generate exactly 1 high-quality multiple choice question with 4 distinct options, the correct option identifier (A, B, C, or D), and an explanation."
|
| 776 |
)
|
| 777 |
|
| 778 |
messages = [
|
|
|
|
| 793 |
))
|
| 794 |
|
| 795 |
q_val = getattr(raw, "question", None) or (raw.get("question") if isinstance(raw, dict) else "")
|
| 796 |
+
corr_val = getattr(raw, "correct_option", None) or (raw.get("correct_option") if isinstance(raw, dict) else "")
|
| 797 |
expl_val = getattr(raw, "explanation", None) or (raw.get("explanation") if isinstance(raw, dict) else "")
|
| 798 |
|
| 799 |
q_text = format_scientific_symbols(_sanitize_text(str(q_val)))
|
| 800 |
+
corr_opt = _resolve_mcq_correct_option(corr_val, options) or ""
|
| 801 |
expl = format_scientific_symbols(str(expl_val))
|
| 802 |
|
| 803 |
candidate = QuizQuestion(
|
|
|
|
| 824 |
|
| 825 |
for r in results:
|
| 826 |
if isinstance(r, QuizQuestion) and _is_valid_mcq(r):
|
| 827 |
+
if not _is_similar_question(r.question, accepted_stems, threshold=0.70):
|
| 828 |
valid_questions.append(r)
|
| 829 |
accepted_stems.append(r.question)
|
| 830 |
if len(valid_questions) == num_questions:
|
|
|
|
| 842 |
topup_results = await asyncio.gather(*topup_tasks, return_exceptions=True)
|
| 843 |
for r in topup_results:
|
| 844 |
if isinstance(r, QuizQuestion) and _is_valid_mcq(r):
|
| 845 |
+
if not _is_similar_question(r.question, accepted_stems, threshold=0.70):
|
| 846 |
valid_questions.append(r)
|
| 847 |
accepted_stems.append(r.question)
|
| 848 |
if len(valid_questions) == num_questions:
|
|
|
|
| 858 |
final_topup_results = await asyncio.gather(*final_topup_tasks, return_exceptions=True)
|
| 859 |
for r in final_topup_results:
|
| 860 |
if isinstance(r, QuizQuestion) and _is_valid_mcq(r):
|
| 861 |
+
if not _is_similar_question(r.question, accepted_stems, threshold=0.70):
|
| 862 |
valid_questions.append(r)
|
| 863 |
accepted_stems.append(r.question)
|
| 864 |
if len(valid_questions) == num_questions:
|
|
|
|
| 1402 |
"1. Formulate 1 clear, academic descriptive question grounded strictly in the provided textbook context.\n"
|
| 1403 |
"2. MODEL ANSWER: Provide a thorough, authoritative reference answer that would achieve full marks.\n"
|
| 1404 |
"3. KEY FACTS: List 3 to 6 distinct, concrete facts, terms, or steps that are mandatory in a correct answer.\n"
|
| 1405 |
+
"4. EXPLANATION: Provide a 1-2 sentence educational explanation summarizing the core principle being tested.\n"
|
| 1406 |
+
"5. SCIENTIFIC NOTATION: Use clean scientific symbols (e.g. ε₀, μ₀, π, 10⁻¹², m/s²) where applicable.\n"
|
| 1407 |
+
"6. ZERO FLUFF: Keep the question direct and unambiguous. Do not reference 'in the passage' or 'according to the text'."
|
| 1408 |
)
|
| 1409 |
user_prompt = (
|
| 1410 |
f"Board: {board_id.upper()} | Class: {class_id} | Subject: {subject} | Chapter: {chapter}\n"
|
|
|
|
| 1413 |
f"Entropy Seed: {seed}\n"
|
| 1414 |
f"{exclude_clause}\n"
|
| 1415 |
f"{knowledge_block}\n\n"
|
| 1416 |
+
f"Generate exactly 1 descriptive question with model answer, key facts, and educational explanation."
|
| 1417 |
)
|
| 1418 |
|
| 1419 |
messages = [
|
|
|
|
| 1434 |
q_text = format_scientific_symbols(_sanitize_text(str(q_val)))
|
| 1435 |
model_ans = format_scientific_symbols(str(ans_val))
|
| 1436 |
cleaned_facts = [format_scientific_symbols(str(f).strip()) for f in facts_val if str(f).strip()]
|
| 1437 |
+
clean_expl = str(expl_val).strip() if expl_val is not None else ""
|
| 1438 |
+
if not clean_expl or clean_expl.lower() in ("none", "null", ""):
|
| 1439 |
+
clean_expl = f"Tests comprehensive conceptual understanding of {topic or chapter} principles."
|
| 1440 |
+
expl = format_scientific_symbols(clean_expl)
|
| 1441 |
|
| 1442 |
candidate = DescriptiveQuestion(
|
| 1443 |
id=idx + 1,
|
|
|
|
| 3142 |
feedback="No answer provided.",
|
| 3143 |
key_points_covered=[],
|
| 3144 |
key_points_missing=q.key_facts,
|
| 3145 |
+
evidence_quote=q.source_context,
|
| 3146 |
+
explanation=q.explanation
|
| 3147 |
)
|
| 3148 |
|
| 3149 |
system_prompt = (
|
|
|
|
| 3211 |
feedback=str(fb) or ("Good answer." if is_correct else "Needs improvement on key details."),
|
| 3212 |
key_points_covered=[str(c) for c in covered if str(c).strip()],
|
| 3213 |
key_points_missing=[str(m) for m in missing if str(m).strip()],
|
| 3214 |
+
evidence_quote=str(ev) if ev else q.source_context,
|
| 3215 |
+
explanation=q.explanation
|
| 3216 |
)
|
| 3217 |
except Exception as e:
|
| 3218 |
print(f"[AI Engine] Error grading descriptive question #{q.id}: {e}")
|
|
|
|
| 3230 |
feedback="Automated grading fallback applied.",
|
| 3231 |
key_points_covered=[],
|
| 3232 |
key_points_missing=q.key_facts,
|
| 3233 |
+
evidence_quote=q.source_context,
|
| 3234 |
+
explanation=q.explanation
|
| 3235 |
)
|
| 3236 |
|
| 3237 |
async def grade_all_answers() -> List[StudentDescriptiveQuestionResult]:
|