j-js commited on
Commit
010a947
·
verified ·
1 Parent(s): 811374a

Update question_fallback_router.py

Browse files
Files changed (1) hide show
  1. question_fallback_router.py +153 -224
question_fallback_router.py CHANGED
@@ -7,16 +7,6 @@ from question_support_loader import question_support_bank
7
 
8
 
9
  class QuestionFallbackRouter:
10
- """
11
- Authoritative support-bank router.
12
-
13
- Design goals:
14
- - authored question support should beat generic topic help whenever a match exists
15
- - generated topic defaults should only fill real gaps
16
- - hint/walkthrough/method/answer ladders should prefer authored sequences as-is
17
- - generic "first step" text should not pollute an authored hint ladder
18
- """
19
-
20
  def _clean(self, text: Optional[str]) -> str:
21
  return (text or "").strip()
22
 
@@ -49,6 +39,8 @@ class QuestionFallbackRouter:
49
 
50
  if t and t not in {"general", "unknown", "general_quant", "quant"}:
51
  return t
 
 
52
  if "%" in q or "percent" in q:
53
  return "percent"
54
  if "ratio" in q or re.search(r"\b\d+\s*:\s*\d+\b", q):
@@ -82,7 +74,9 @@ class QuestionFallbackRouter:
82
  def _extract_ratio(self, question_text: str) -> Optional[str]:
83
  text = self._clean(question_text)
84
  m = re.search(r"\b(\d+\s*:\s*\d+)\b", text)
85
- return self._clean(m.group(1)) if m else None
 
 
86
 
87
  def _extract_percent_values(self, question_text: str) -> List[str]:
88
  return re.findall(r"\d+\.?\d*\s*%", question_text or "")
@@ -96,6 +90,34 @@ class QuestionFallbackRouter:
96
  and re.search(r"\d+[a-z]\b|\b[a-z]\b", q)
97
  )
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  def _topic_defaults(self, topic: str, question_text: str, options_text: Optional[List[str]]) -> Dict[str, Any]:
100
  preview = self._preview_question(question_text)
101
  equation = self._extract_equation(question_text)
@@ -103,20 +125,23 @@ class QuestionFallbackRouter:
103
  percent_values = self._extract_percent_values(question_text)
104
  has_options = bool(options_text)
105
 
106
- pack: Dict[str, Any] = {
107
  "first_step": f"Focus on what the question is really asking in: {preview}",
 
 
 
108
  "hint_ladder": [
109
  "Identify the exact quantity you need to find.",
110
- "Translate the key relationship into a usable setup.",
111
- "Check that your setup matches the wording before choosing an option.",
112
  ],
113
  "walkthrough_steps": [
114
  "Underline what is given and what must be found.",
115
  "Set up the relationship before you calculate.",
116
- "Work step by step and keep units or labels consistent.",
117
  ],
118
  "method_steps": [
119
- "Start from the structure of the problem rather than jumping straight into arithmetic.",
120
  "Use the wording to decide which relationship matters most.",
121
  ],
122
  "answer_path": [
@@ -128,9 +153,12 @@ class QuestionFallbackRouter:
128
 
129
  if topic == "algebra":
130
  if self._looks_like_linear_equation(question_text):
131
- pack.update(
132
  {
133
  "first_step": "Look at the variable side and identify the outermost operation attached to the variable.",
 
 
 
134
  "hint_ladder": [
135
  "Look at the variable side and identify the outermost operation attached to the variable.",
136
  "Undo the outside addition or subtraction on both sides before touching the coefficient.",
@@ -155,35 +183,40 @@ class QuestionFallbackRouter:
155
  }
156
  )
157
  elif equation:
158
- pack.update(
159
  {
160
  "first_step": f"Start from the equation {equation} and decide which operation should be reversed first.",
161
- "hint_ladder": [
162
- "Preserve balance by doing the same operation to both sides.",
163
- "Reverse the operations in a sensible order instead of trying to simplify everything at once.",
164
- "Only evaluate the target expression after the variables are in a usable form.",
165
- ],
166
  }
167
  )
 
168
  elif topic == "percent":
169
  first_step = "Identify the base quantity before doing any percent calculation."
170
  if percent_values:
171
  first_step = f"Track the percentage relationship carefully here: {' then '.join(percent_values[:2]) if len(percent_values) > 1 else percent_values[0]}"
172
  if "increased by" in question_text.lower() and "decreased by" in question_text.lower():
173
- pack.update(
174
  {
175
  "first_step": "Turn each percentage change into its own multiplier before combining anything.",
 
 
 
176
  "hint_ladder": [
177
- "An increase and a decrease of the same percent do not cancel because they apply to different bases.",
178
  "Apply the first multiplier, then apply the second multiplier to the updated amount.",
179
  "Compare the final amount with the original amount only at the end.",
180
  ],
181
  }
182
  )
183
  else:
184
- pack.update(
185
  {
186
  "first_step": first_step,
 
 
 
187
  "hint_ladder": [
188
  "Ask 'percent of what?' so you choose the correct base quantity.",
189
  "Rewrite the percent as a decimal or fraction if that makes the relationship clearer.",
@@ -191,13 +224,17 @@ class QuestionFallbackRouter:
191
  ],
192
  }
193
  )
 
194
  elif topic == "ratio":
195
  first_step = "Keep the ratio order consistent and assign one shared multiplier."
196
  if ratio_text:
197
  first_step = f"Use the ratio {ratio_text} as parts of one whole."
198
- pack.update(
199
  {
200
  "first_step": first_step,
 
 
 
201
  "hint_ladder": [
202
  "Write each part of the ratio using the same multiplier.",
203
  "Use the total or known part to solve for that shared multiplier.",
@@ -216,22 +253,19 @@ class QuestionFallbackRouter:
216
  "common_trap": "Using the raw ratio numbers as real values before solving for the common multiplier.",
217
  }
218
  )
 
219
  elif topic == "probability":
220
- hint_ladder = [
221
- "Decide what counts as a successful outcome before you count anything.",
222
- "Count the favorable outcomes that satisfy the condition.",
223
- "Count the total possible outcomes in the sample space.",
224
- ]
225
- if "at least" in question_text.lower():
226
- hint_ladder = [
227
- "Decide what counts as a successful outcome before you count anything.",
228
- "Check whether the complement is easier to count than the requested event.",
229
- "Build the probability as favorable over total, then simplify if needed.",
230
- ]
231
- pack.update(
232
  {
233
  "first_step": "Decide what counts as a successful outcome before you count anything.",
234
- "hint_ladder": hint_ladder,
 
 
 
 
 
 
 
235
  "walkthrough_steps": [
236
  "Define the event the question cares about.",
237
  "Count or construct the favorable cases.",
@@ -245,12 +279,23 @@ class QuestionFallbackRouter:
245
  "common_trap": "Changing the denominator incorrectly or forgetting which cases are actually favorable.",
246
  }
247
  )
 
 
 
 
 
 
 
 
248
  elif topic == "statistics":
249
  qlow = question_text.lower()
250
  if any(k in qlow for k in ["variability", "spread", "standard deviation"]):
251
- pack.update(
252
  {
253
  "first_step": "Notice that this is about spread, not average.",
 
 
 
254
  "hint_ladder": [
255
  "Notice that this is about spread, not average.",
256
  "Use the middle value as a centre and compare how far the outer values sit from it.",
@@ -259,153 +304,52 @@ class QuestionFallbackRouter:
259
  }
260
  )
261
  else:
262
- pack.update(
263
  {
264
  "first_step": "Identify which statistical measure the question wants before calculating anything.",
265
- "hint_ladder": [
266
- "Check whether the task is asking for mean, median, range, or another measure.",
267
- "Organise the data in a clean order if that helps reveal the measure.",
268
- "Use the exact definition of the requested measure rather than a nearby one.",
269
- ],
270
  }
271
  )
272
 
273
  if has_options:
274
- pack["answer_path"] = list(pack.get("answer_path", [])) + [
275
- "Use the answer choices to test which setup actually matches the question."
276
  ]
277
 
278
- return pack
279
-
280
- def _pack_looks_generic(self, pack: Dict[str, Any], topic: str) -> bool:
281
- if not pack:
282
- return True
283
- joined = " ".join(
284
- [
285
- self._clean(pack.get("first_step")),
286
- " ".join(self._listify(pack.get("hint_ladder"))),
287
- " ".join(self._listify(pack.get("walkthrough_steps"))),
288
- " ".join(self._listify(pack.get("method_steps"))),
289
- " ".join(self._listify(pack.get("method_explanation"))),
290
- " ".join(self._listify(pack.get("answer_path"))),
291
- ]
292
- ).lower()
293
- generic_signals = [
294
- "identify the exact quantity you need to find",
295
- "translate the key relationship",
296
- "set up the relationship before you calculate",
297
- "work step by step",
298
- "use the wording to decide",
299
- "start from the structure",
300
- "focus on what the question is really asking",
301
- "underline what is given and what must be found",
302
- ]
303
- if any(signal in joined for signal in generic_signals):
304
- return True
305
- if topic == "algebra" and "look at the variable side" not in joined and "equation" in joined and "both sides" in joined:
306
- return True
307
- return False
308
-
309
- def _extract_authored_pack(self, stored: Dict[str, Any]) -> Dict[str, Any]:
310
- if not stored:
311
- return {}
312
- pack: Dict[str, Any] = {}
313
- text_fields = [
314
- "first_step",
315
- "concept",
316
- "common_trap",
317
- "stem",
318
- "question_text",
319
- "topic",
320
- "category",
321
- ]
322
- list_fields = [
323
- "hint_ladder",
324
- "hints",
325
- "walkthrough_steps",
326
- "method_steps",
327
- "method_explanation",
328
- "answer_path",
329
- "choices",
330
- "options_text",
331
- ]
332
- for key in text_fields:
333
- value = self._clean(stored.get(key))
334
- if value:
335
- pack[key] = value
336
- for key in list_fields:
337
- values = self._dedupe(self._listify(stored.get(key)))
338
- if values:
339
- pack[key] = values
340
- for key in ("hint_1", "hint_2", "hint_3"):
341
- value = self._clean(stored.get(key))
342
- if value:
343
- pack[key] = value
344
- if "support_match" in stored:
345
- pack["support_match"] = dict(stored["support_match"])
346
- return pack
347
-
348
- def _confidence_rank(self, confidence: str) -> int:
349
- mapping = {"exact": 4, "high": 3, "medium": 2, "low": 1}
350
- return mapping.get((confidence or "").lower(), 0)
351
 
352
  def _merge_support_pack(self, generated: Dict[str, Any], stored: Optional[Dict[str, Any]], topic: str) -> Dict[str, Any]:
353
  if not stored:
354
- pack = dict(generated)
355
- pack["support_source"] = "generated_question_specific"
356
- pack["support_priority"] = 10
357
- pack["has_authored_support"] = False
358
- pack["_generated"] = dict(generated)
359
- return pack
360
-
361
- authored = self._extract_authored_pack(stored)
362
- support_match = dict(authored.get("support_match") or {})
363
- confidence = str(support_match.get("confidence") or "").lower()
364
- authored_generic = self._pack_looks_generic(authored, topic)
365
-
366
- pack = dict(authored)
367
- pack["_generated"] = dict(generated)
368
- pack["has_authored_support"] = True
369
- pack["support_match"] = support_match
370
-
371
- for key in ["first_step", "common_trap", "concept", "topic", "category"]:
372
- if not self._clean(pack.get(key)):
373
- val = self._clean(generated.get(key))
374
- if val:
375
- pack[key] = val
376
-
377
- for key in ["hint_ladder", "walkthrough_steps", "method_steps", "method_explanation", "answer_path"]:
378
- if not self._listify(pack.get(key)):
379
- vals = self._dedupe(self._listify(generated.get(key)))
380
- if vals:
381
- pack[key] = vals
382
-
383
- for key in ["hint_1", "hint_2", "hint_3"]:
384
- if not self._clean(pack.get(key)):
385
- val = self._clean(generated.get(key))
386
- if val:
387
- pack[key] = val
388
-
389
- if authored_generic and self._confidence_rank(confidence) <= 1:
390
- blended = dict(generated)
391
- for key, value in authored.items():
392
- if isinstance(value, str) and self._clean(value):
393
- blended[key] = value
394
- elif isinstance(value, (list, tuple)) and self._listify(value):
395
- blended[key] = list(value)
396
- elif key == "support_match":
397
- blended[key] = value
398
- pack = blended
399
- pack["_generated"] = dict(generated)
400
- pack["has_authored_support"] = True
401
- pack["support_match"] = support_match
402
- pack["support_source"] = "question_bank_refined"
403
- pack["support_priority"] = 60
404
  else:
405
- pack["support_source"] = "question_bank"
406
- pack["support_priority"] = 100 if self._confidence_rank(confidence) >= 2 else 80
407
 
408
- return pack
 
 
409
 
410
  def get_support_pack(
411
  self,
@@ -434,60 +378,45 @@ class QuestionFallbackRouter:
434
  pack.setdefault("category", category or "General")
435
  return pack
436
 
437
- def _authored_or_generated(self, pack: Dict[str, Any], key: str) -> Any:
438
- value = pack.get(key)
439
- if isinstance(value, str):
440
- if self._clean(value):
441
- return value
442
- elif isinstance(value, (list, tuple)):
443
- if self._listify(value):
444
- return value
445
- generated = dict(pack.get("_generated") or {})
446
- return generated.get(key)
447
-
448
  def _hint_ladder_from_pack(self, pack: Dict[str, Any]) -> List[str]:
449
  hints: List[str] = []
450
- ladder = self._listify(self._authored_or_generated(pack, "hint_ladder"))
451
- authored_hints = self._listify(self._authored_or_generated(pack, "hints"))
452
- if ladder or authored_hints:
453
- hints.extend(ladder)
454
- hints.extend(authored_hints)
455
- else:
456
- first_step = self._clean(self._authored_or_generated(pack, "first_step"))
457
- if first_step:
458
- hints.append(first_step)
459
- for key in ("hint_1", "hint_2", "hint_3"):
460
- value = self._clean(self._authored_or_generated(pack, key))
461
- if value:
462
- hints.append(value)
463
  return self._dedupe(hints)
464
 
465
  def _walkthrough_from_pack(self, pack: Dict[str, Any]) -> List[str]:
466
- lines = self._dedupe(self._listify(self._authored_or_generated(pack, "walkthrough_steps")))
467
- if lines:
468
- return lines
469
- first_step = self._clean(self._authored_or_generated(pack, "first_step"))
470
- hint_ladder = self._hint_ladder_from_pack(pack)
471
- return self._dedupe(([first_step] if first_step else []) + hint_ladder[:4])
472
 
473
  def _method_from_pack(self, pack: Dict[str, Any]) -> List[str]:
474
  lines: List[str] = []
475
- concept = self._clean(self._authored_or_generated(pack, "concept"))
476
  if concept:
477
  lines.append(concept)
478
- lines.extend(self._listify(self._authored_or_generated(pack, "method_steps")))
479
- lines.extend(self._listify(self._authored_or_generated(pack, "method_explanation")))
480
- if lines:
481
- return self._dedupe(lines)
482
- return self._walkthrough_from_pack(pack)[:3]
483
 
484
  def _answer_path_from_pack(self, pack: Dict[str, Any]) -> List[str]:
485
- lines = self._dedupe(self._listify(self._authored_or_generated(pack, "answer_path")))
486
- if lines:
487
- return lines
488
- first_step = self._clean(self._authored_or_generated(pack, "first_step"))
489
- hint_ladder = self._hint_ladder_from_pack(pack)
490
- return self._dedupe(([first_step] if first_step else []) + hint_ladder[:3])
491
 
492
  def _verbosity_limit(self, verbosity: float, low: int, mid: int, high: int) -> int:
493
  if verbosity < 0.25:
@@ -517,14 +446,14 @@ class QuestionFallbackRouter:
517
  )
518
 
519
  mode = (help_mode or "answer").lower()
520
- stage = max(1, min(int(hint_stage or 1), 6))
521
 
522
- first_step = self._clean(self._authored_or_generated(pack, "first_step"))
523
  hint_ladder = self._hint_ladder_from_pack(pack)
524
  walkthrough_steps = self._walkthrough_from_pack(pack)
525
  method_steps = self._method_from_pack(pack)
526
  answer_path = self._answer_path_from_pack(pack)
527
- common_trap = self._clean(self._authored_or_generated(pack, "common_trap"))
528
 
529
  lines: List[str] = []
530
 
@@ -536,7 +465,7 @@ class QuestionFallbackRouter:
536
  selected.append(hint_ladder[idx + 1])
537
  else:
538
  selected = [first_step or "Start by identifying the structure of the question."]
539
- if verbosity >= 0.8 and stage >= 3 and common_trap:
540
  selected.append(f"Watch out for this trap: {common_trap}")
541
  lines = self._dedupe(selected)
542
 
@@ -544,20 +473,20 @@ class QuestionFallbackRouter:
544
  source = walkthrough_steps or answer_path or hint_ladder
545
  limit = self._verbosity_limit(verbosity, low=2, mid=4, high=6)
546
  lines = source[:limit] if source else [first_step or "Start by setting up the problem."]
547
- if verbosity >= 0.75 and common_trap:
548
  lines = list(lines) + [f"Watch out for this trap: {common_trap}"]
549
 
550
  elif mode in {"method", "explain", "concept", "definition"}:
551
  source = method_steps or walkthrough_steps or answer_path or hint_ladder
552
  limit = self._verbosity_limit(verbosity, low=1, mid=2, high=4)
553
  lines = source[:limit] if source else [first_step or "Start from the problem structure."]
554
- if verbosity >= 0.7 and common_trap:
555
  lines = list(lines) + [f"Common trap: {common_trap}"]
556
 
557
  else:
558
  source = answer_path or walkthrough_steps or hint_ladder
559
  limit = self._verbosity_limit(verbosity, low=2, mid=3, high=5)
560
- lines = source[:limit] if source else [first_step or "Start by identifying the main relationship in the question."]
561
 
562
  lines = self._dedupe(lines)
563
  return {"lines": lines, "pack": pack}
 
7
 
8
 
9
  class QuestionFallbackRouter:
 
 
 
 
 
 
 
 
 
 
10
  def _clean(self, text: Optional[str]) -> str:
11
  return (text or "").strip()
12
 
 
39
 
40
  if t and t not in {"general", "unknown", "general_quant", "quant"}:
41
  return t
42
+ if t == "quant":
43
+ t = ""
44
  if "%" in q or "percent" in q:
45
  return "percent"
46
  if "ratio" in q or re.search(r"\b\d+\s*:\s*\d+\b", q):
 
74
  def _extract_ratio(self, question_text: str) -> Optional[str]:
75
  text = self._clean(question_text)
76
  m = re.search(r"\b(\d+\s*:\s*\d+)\b", text)
77
+ if m:
78
+ return self._clean(m.group(1))
79
+ return None
80
 
81
  def _extract_percent_values(self, question_text: str) -> List[str]:
82
  return re.findall(r"\d+\.?\d*\s*%", question_text or "")
 
90
  and re.search(r"\d+[a-z]\b|\b[a-z]\b", q)
91
  )
92
 
93
+ def _pack_looks_generic(self, pack: Dict[str, Any], topic: str) -> bool:
94
+ if not pack:
95
+ return True
96
+ joined = " ".join(
97
+ [
98
+ self._clean(pack.get("first_step")),
99
+ self._clean(pack.get("hint_1")),
100
+ self._clean(pack.get("hint_2")),
101
+ self._clean(pack.get("hint_3")),
102
+ " ".join(self._listify(pack.get("walkthrough_steps"))),
103
+ " ".join(self._listify(pack.get("method_explanation"))),
104
+ ]
105
+ ).lower()
106
+ generic_signals = [
107
+ "write the equation clearly and identify the variable",
108
+ "undo operations in reverse order",
109
+ "keep both sides balanced",
110
+ "break the question into known and unknown parts",
111
+ "what is being asked?",
112
+ "what information is given?",
113
+ "translate words into math",
114
+ ]
115
+ if any(signal in joined for signal in generic_signals):
116
+ return True
117
+ if topic == "algebra" and "look at the structure" in joined:
118
+ return True
119
+ return False
120
+
121
  def _topic_defaults(self, topic: str, question_text: str, options_text: Optional[List[str]]) -> Dict[str, Any]:
122
  preview = self._preview_question(question_text)
123
  equation = self._extract_equation(question_text)
 
125
  percent_values = self._extract_percent_values(question_text)
126
  has_options = bool(options_text)
127
 
128
+ generic = {
129
  "first_step": f"Focus on what the question is really asking in: {preview}",
130
+ "hint_1": "Identify the exact quantity you need to find.",
131
+ "hint_2": "Translate the key relationship in the question into a usable setup.",
132
+ "hint_3": "Check each step against the wording before choosing an option.",
133
  "hint_ladder": [
134
  "Identify the exact quantity you need to find.",
135
+ "Translate the key relationship in the question into a usable setup.",
136
+ "Check each step against the wording before choosing an option.",
137
  ],
138
  "walkthrough_steps": [
139
  "Underline what is given and what must be found.",
140
  "Set up the relationship before you calculate.",
141
+ "Work step by step and keep labels or units consistent.",
142
  ],
143
  "method_steps": [
144
+ "Start from the structure of the problem rather than jumping into arithmetic.",
145
  "Use the wording to decide which relationship matters most.",
146
  ],
147
  "answer_path": [
 
153
 
154
  if topic == "algebra":
155
  if self._looks_like_linear_equation(question_text):
156
+ generic.update(
157
  {
158
  "first_step": "Look at the variable side and identify the outermost operation attached to the variable.",
159
+ "hint_1": "Undo the outside addition or subtraction on both sides before touching the coefficient.",
160
+ "hint_2": "Once only the variable term remains, undo the multiplication or division.",
161
+ "hint_3": "After isolating the variable, compare carefully with what the question actually asks for.",
162
  "hint_ladder": [
163
  "Look at the variable side and identify the outermost operation attached to the variable.",
164
  "Undo the outside addition or subtraction on both sides before touching the coefficient.",
 
183
  }
184
  )
185
  elif equation:
186
+ generic.update(
187
  {
188
  "first_step": f"Start from the equation {equation} and decide which operation should be reversed first.",
189
+ "hint_1": "Preserve balance by doing the same operation to both sides.",
190
+ "hint_2": "Reverse the operations in a sensible order instead of trying to simplify everything at once.",
191
+ "hint_3": "Only evaluate the target expression after the variables are in a usable form.",
 
 
192
  }
193
  )
194
+
195
  elif topic == "percent":
196
  first_step = "Identify the base quantity before doing any percent calculation."
197
  if percent_values:
198
  first_step = f"Track the percentage relationship carefully here: {' then '.join(percent_values[:2]) if len(percent_values) > 1 else percent_values[0]}"
199
  if "increased by" in question_text.lower() and "decreased by" in question_text.lower():
200
+ generic.update(
201
  {
202
  "first_step": "Turn each percentage change into its own multiplier before combining anything.",
203
+ "hint_1": "An increase and a decrease of the same percent do not cancel because they apply to different bases.",
204
+ "hint_2": "Apply the first multiplier, then apply the second multiplier to the updated amount.",
205
+ "hint_3": "Compare the final amount with the original amount only at the end.",
206
  "hint_ladder": [
207
+ "Turn each percentage change into its own multiplier before combining anything.",
208
  "Apply the first multiplier, then apply the second multiplier to the updated amount.",
209
  "Compare the final amount with the original amount only at the end.",
210
  ],
211
  }
212
  )
213
  else:
214
+ generic.update(
215
  {
216
  "first_step": first_step,
217
+ "hint_1": "Ask 'percent of what?' so you choose the correct base quantity.",
218
+ "hint_2": "Rewrite the percent as a decimal or fraction if that makes the relationship clearer.",
219
+ "hint_3": "Set up part = percent × base, or reverse that relationship if the base is unknown.",
220
  "hint_ladder": [
221
  "Ask 'percent of what?' so you choose the correct base quantity.",
222
  "Rewrite the percent as a decimal or fraction if that makes the relationship clearer.",
 
224
  ],
225
  }
226
  )
227
+
228
  elif topic == "ratio":
229
  first_step = "Keep the ratio order consistent and assign one shared multiplier."
230
  if ratio_text:
231
  first_step = f"Use the ratio {ratio_text} as parts of one whole."
232
+ generic.update(
233
  {
234
  "first_step": first_step,
235
+ "hint_1": "Write each part of the ratio using the same multiplier.",
236
+ "hint_2": "Use the total or known part to solve for that shared multiplier.",
237
+ "hint_3": "Substitute back into the exact quantity the question asks for.",
238
  "hint_ladder": [
239
  "Write each part of the ratio using the same multiplier.",
240
  "Use the total or known part to solve for that shared multiplier.",
 
253
  "common_trap": "Using the raw ratio numbers as real values before solving for the common multiplier.",
254
  }
255
  )
256
+
257
  elif topic == "probability":
258
+ generic.update(
 
 
 
 
 
 
 
 
 
 
 
259
  {
260
  "first_step": "Decide what counts as a successful outcome before you count anything.",
261
+ "hint_1": "Count the favorable outcomes that satisfy the condition.",
262
+ "hint_2": "Count the total possible outcomes in the sample space.",
263
+ "hint_3": "Build the probability as favorable over total, then simplify if needed.",
264
+ "hint_ladder": [
265
+ "Decide what counts as a successful outcome before you count anything.",
266
+ "Count the favorable outcomes that satisfy the condition.",
267
+ "Count the total possible outcomes in the sample space.",
268
+ ],
269
  "walkthrough_steps": [
270
  "Define the event the question cares about.",
271
  "Count or construct the favorable cases.",
 
279
  "common_trap": "Changing the denominator incorrectly or forgetting which cases are actually favorable.",
280
  }
281
  )
282
+ if "at least" in question_text.lower():
283
+ generic["hint_2"] = "Check whether the complement is easier to count than the requested event."
284
+ generic["hint_ladder"] = [
285
+ generic["hint_1"],
286
+ "Check whether the complement is easier to count than the requested event.",
287
+ generic["hint_3"],
288
+ ]
289
+
290
  elif topic == "statistics":
291
  qlow = question_text.lower()
292
  if any(k in qlow for k in ["variability", "spread", "standard deviation"]):
293
+ generic.update(
294
  {
295
  "first_step": "Notice that this is about spread, not average.",
296
+ "hint_1": "Use the middle value as a centre and compare how far the outer values sit from it.",
297
+ "hint_2": "A set with values clustered tightly has lower variability than a set spread farther apart.",
298
+ "hint_3": "Choose the set with the widest spread, not the largest mean.",
299
  "hint_ladder": [
300
  "Notice that this is about spread, not average.",
301
  "Use the middle value as a centre and compare how far the outer values sit from it.",
 
304
  }
305
  )
306
  else:
307
+ generic.update(
308
  {
309
  "first_step": "Identify which statistical measure the question wants before calculating anything.",
310
+ "hint_1": "Check whether the task is asking for mean, median, range, or another measure.",
311
+ "hint_2": "Organise the data in a clean order if that helps reveal the measure.",
312
+ "hint_3": "Use the exact definition of the requested measure rather than a nearby one.",
 
 
313
  }
314
  )
315
 
316
  if has_options:
317
+ generic["answer_path"] = list(generic.get("answer_path", [])) + [
318
+ "Use the answer choices to check which setup fits the question instead of guessing."
319
  ]
320
 
321
+ return generic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
  def _merge_support_pack(self, generated: Dict[str, Any], stored: Optional[Dict[str, Any]], topic: str) -> Dict[str, Any]:
324
  if not stored:
325
+ merged = dict(generated)
326
+ merged["support_source"] = "generated_question_specific"
327
+ return merged
328
+
329
+ merged = dict(generated)
330
+ merged.update(dict(stored))
331
+
332
+ if self._pack_looks_generic(stored, topic):
333
+ for key in [
334
+ "first_step",
335
+ "hint_1",
336
+ "hint_2",
337
+ "hint_3",
338
+ "hint_ladder",
339
+ "walkthrough_steps",
340
+ "method_steps",
341
+ "answer_path",
342
+ "common_trap",
343
+ ]:
344
+ if key in generated:
345
+ merged[key] = generated[key]
346
+ merged["support_source"] = "question_bank_refined"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  else:
348
+ merged.setdefault("support_source", "question_bank")
 
349
 
350
+ merged.setdefault("method_steps", generated.get("method_steps", []))
351
+ merged.setdefault("answer_path", generated.get("answer_path", []))
352
+ return merged
353
 
354
  def get_support_pack(
355
  self,
 
378
  pack.setdefault("category", category or "General")
379
  return pack
380
 
 
 
 
 
 
 
 
 
 
 
 
381
  def _hint_ladder_from_pack(self, pack: Dict[str, Any]) -> List[str]:
382
  hints: List[str] = []
383
+ first_step = self._clean(pack.get("first_step"))
384
+ if first_step:
385
+ hints.append(first_step)
386
+ for key in ("hint_1", "hint_2", "hint_3"):
387
+ value = self._clean(pack.get(key))
388
+ if value:
389
+ hints.append(value)
390
+ hints.extend(self._listify(pack.get("hint_ladder")))
391
+ hints.extend(self._listify(pack.get("hints")))
 
 
 
 
392
  return self._dedupe(hints)
393
 
394
  def _walkthrough_from_pack(self, pack: Dict[str, Any]) -> List[str]:
395
+ lines: List[str] = []
396
+ first_step = self._clean(pack.get("first_step"))
397
+ if first_step:
398
+ lines.append(first_step)
399
+ lines.extend(self._listify(pack.get("walkthrough_steps")))
400
+ return self._dedupe(lines)
401
 
402
  def _method_from_pack(self, pack: Dict[str, Any]) -> List[str]:
403
  lines: List[str] = []
404
+ concept = self._clean(pack.get("concept"))
405
  if concept:
406
  lines.append(concept)
407
+ lines.extend(self._listify(pack.get("method_steps")))
408
+ lines.extend(self._listify(pack.get("method_explanation")))
409
+ if not lines:
410
+ lines.extend(self._walkthrough_from_pack(pack)[:3])
411
+ return self._dedupe(lines)
412
 
413
  def _answer_path_from_pack(self, pack: Dict[str, Any]) -> List[str]:
414
+ lines: List[str] = []
415
+ first_step = self._clean(pack.get("first_step"))
416
+ if first_step:
417
+ lines.append(first_step)
418
+ lines.extend(self._listify(pack.get("answer_path")))
419
+ return self._dedupe(lines)
420
 
421
  def _verbosity_limit(self, verbosity: float, low: int, mid: int, high: int) -> int:
422
  if verbosity < 0.25:
 
446
  )
447
 
448
  mode = (help_mode or "answer").lower()
449
+ stage = max(1, min(int(hint_stage or 1), 4))
450
 
451
+ first_step = self._clean(pack.get("first_step"))
452
  hint_ladder = self._hint_ladder_from_pack(pack)
453
  walkthrough_steps = self._walkthrough_from_pack(pack)
454
  method_steps = self._method_from_pack(pack)
455
  answer_path = self._answer_path_from_pack(pack)
456
+ common_trap = self._clean(pack.get("common_trap"))
457
 
458
  lines: List[str] = []
459
 
 
465
  selected.append(hint_ladder[idx + 1])
466
  else:
467
  selected = [first_step or "Start by identifying the structure of the question."]
468
+ if verbosity >= 0.75 and stage >= 3 and common_trap:
469
  selected.append(f"Watch out for this trap: {common_trap}")
470
  lines = self._dedupe(selected)
471
 
 
473
  source = walkthrough_steps or answer_path or hint_ladder
474
  limit = self._verbosity_limit(verbosity, low=2, mid=4, high=6)
475
  lines = source[:limit] if source else [first_step or "Start by setting up the problem."]
476
+ if verbosity >= 0.7 and common_trap:
477
  lines = list(lines) + [f"Watch out for this trap: {common_trap}"]
478
 
479
  elif mode in {"method", "explain", "concept", "definition"}:
480
  source = method_steps or walkthrough_steps or answer_path or hint_ladder
481
  limit = self._verbosity_limit(verbosity, low=1, mid=2, high=4)
482
  lines = source[:limit] if source else [first_step or "Start from the problem structure."]
483
+ if verbosity >= 0.65 and common_trap:
484
  lines = list(lines) + [f"Common trap: {common_trap}"]
485
 
486
  else:
487
  source = answer_path or walkthrough_steps or hint_ladder
488
  limit = self._verbosity_limit(verbosity, low=2, mid=3, high=5)
489
+ lines = source[:limit] if source else [first_step or "Start by identifying the relationship in the question."]
490
 
491
  lines = self._dedupe(lines)
492
  return {"lines": lines, "pack": pack}