File size: 26,443 Bytes
ae81756
 
ad65d71
 
 
 
 
 
ae81756
 
 
 
 
 
 
 
 
 
 
 
 
 
ad65d71
 
 
 
 
 
 
 
 
 
 
 
ae81756
ad65d71
 
3a11913
 
 
 
 
 
 
 
 
 
 
ad65d71
 
 
40b145a
ad65d71
 
 
ae81756
ad65d71
ae81756
 
 
 
 
ad65d71
ae81756
ad65d71
ae81756
ad65d71
 
 
3a11913
ae81756
 
3a11913
 
ae81756
 
 
 
 
 
40b145a
010a947
 
 
ae81756
 
 
 
 
 
 
 
010a947
ae81756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
010a947
ae81756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a11913
ae81756
 
 
 
 
 
ad65d71
ae81756
 
 
 
ad65d71
 
ae81756
 
 
 
ad65d71
 
ae81756
ad65d71
 
ae81756
 
 
ad65d71
ae81756
ad65d71
 
ae81756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a11913
ae81756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad65d71
ae81756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad65d71
40b145a
 
ae81756
 
 
 
 
 
 
 
 
 
010a947
40b145a
ae81756
 
 
 
010a947
40b145a
ae81756
ad65d71
 
ae81756
40b145a
 
 
 
 
 
 
 
 
ad65d71
3a11913
ae81756
 
 
010a947
 
 
ae81756
 
 
 
3a11913
 
ae81756
 
 
010a947
3a11913
 
 
010a947
3a11913
40b145a
010a947
 
 
ae81756
010a947
3a11913
 
ae81756
 
 
010a947
3a11913
ae81756
 
3a11913
ae81756
3a11913
 
 
ae81756
 
ad65d71
ae81756
3a11913
 
 
 
010a947
3a11913
ad65d71
ae81756
40b145a
ae81756
 
 
 
 
3a11913
ae81756
 
40b145a
ae81756
 
3a11913
ad65d71
ae81756
 
 
ad65d71
 
3a11913
ae81756
3a11913
ae81756
ad65d71
 
ae81756
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
from __future__ import annotations

import re
from typing import Any, Dict, List, Optional

from question_support_loader import question_support_bank


GENERIC_MARKERS = {
    "write the equation clearly and identify the variable.",
    "undo operations in reverse order.",
    "keep both sides balanced while isolating the variable.",
    "understand the problem.",
    "identify variables.",
    "set up relationships.",
    "solve step by step.",
    "what is being asked?",
    "what information is given?",
    "how can you link them mathematically?",
}


class QuestionFallbackRouter:
    def _clean(self, text: Optional[str]) -> str:
        return (text or "").strip()

    def _listify(self, value: Any) -> List[str]:
        if value is None:
            return []
        if isinstance(value, list):
            return [str(v).strip() for v in value if str(v).strip()]
        if isinstance(value, tuple):
            return [str(v).strip() for v in value if str(v).strip()]
        if isinstance(value, str):
            return [value.strip()] if value.strip() else []
        return []

    def _dedupe(self, items: List[str]) -> List[str]:
        seen = set()
        out: List[str] = []
        for item in items:
            text = self._clean(item)
            key = text.lower()
            if text and key not in seen:
                seen.add(key)
                out.append(text)
        return out

    def _normalize_topic(self, topic: Optional[str], question_text: str) -> str:
        q = (question_text or "").lower()
        t = (topic or "").strip().lower()
        if t and t not in {"general", "unknown", "general_quant", "quant"}:
            return t
        if "%" in q or "percent" in q:
            return "percent"
        if "probability" in q or "chance" in q or "at random" in q or "odds" in q:
            return "probability"
        if "ratio" in q or re.search(r"\d+\s*:\s*\d+", q):
            return "ratio"
        if any(k in q for k in ["mean", "median", "standard deviation", "variability", "spread"]):
            return "statistics"
        if any(k in q for k in ["remainder", "prime", "divisible", "factor", "multiple"]):
            return "number_theory"
        if any(k in q for k in ["rectangle", "triangle", "circle", "perimeter", "area"]):
            return "geometry"
        if "=" in q or re.search(r"[xyzabn]", q):
            return "algebra"
        return "general"

    def _extract_equation(self, question_text: str) -> Optional[str]:
        m = re.search(r"([^?]*=[^?]*)", self._clean(question_text))
        return self._clean(m.group(1)) if m else None

    def _extract_ratio(self, question_text: str) -> Optional[str]:
        m = re.search(r"(\d+\s*:\s*\d+)", question_text or "")
        return self._clean(m.group(1)) if m else None

    def _looks_like_simple_linear(self, question_text: str) -> bool:
        q = (question_text or "").lower()
        return bool("=" in q and re.search(r"what is\s+[a-z]", q))

    def _pack_looks_generic(self, pack: Dict[str, Any], topic: str) -> bool:
        if not pack:
            return True
        lines: List[str] = []
        for key in ["first_step", "hint_1", "hint_2", "hint_3", "common_trap", "concept"]:
            value = self._clean(pack.get(key))
            if value:
                lines.append(value.lower())
        for key in ["hint_ladder", "walkthrough_steps", "method_steps", "method_explanation", "answer_path"]:
            lines.extend(x.lower() for x in self._listify(pack.get(key)))
        if not lines:
            return True
        meaningful = 0
        for line in lines:
            if line not in GENERIC_MARKERS and len(line.split()) >= 5:
                meaningful += 1
        if pack.get("support_match", {}).get("mode") in {"question_id", "signature_exact", "text_exact"} and meaningful >= 2:
            return False
        return meaningful < 3

    def _algebra_pack(self, question_text: str) -> Dict[str, Any]:
        eq = self._extract_equation(question_text) or "the equation"
        if self._looks_like_simple_linear(question_text):
            return {
                "first_step": f"Start with {eq} and undo the outer operation around the variable first.",
                "hint_1": "Move the constant term on the variable side by doing the opposite operation to both sides.",
                "hint_2": "Once the variable term is isolated, undo the coefficient on the variable.",
                "hint_3": "Check your value by substituting it back into the original equation.",
                "hint_ladder": [
                    f"Look at {eq} and ask which operation is happening to the variable last.",
                    "Undo the constant attached to the variable side by using the opposite operation on both sides.",
                    "After that, undo the multiplication or division on the variable itself.",
                    "Substitute your candidate value back in to verify it reproduces the original right-hand side.",
                ],
                "walkthrough_steps": [
                    f"Rewrite the equation cleanly: {eq}.",
                    "Undo the addition or subtraction around the variable by applying the opposite operation to both sides.",
                    "Then undo the multiplication or division on the variable.",
                    "Check the result in the original equation, not just the simplified one.",
                ],
                "method_steps": [
                    "Linear equations are usually solved by reversing operations in the opposite order from how they affect the variable.",
                    "Keeping both sides balanced is what lets every step stay equivalent to the original equation.",
                ],
                "answer_path": [
                    "Reverse the outer operation first.",
                    "Then remove the coefficient from the variable.",
                    "Verify by substitution.",
                ],
                "common_trap": "Undoing the coefficient before removing the constant on the variable side.",
            }
        return {
            "first_step": f"Rewrite {eq} in a clean algebraic form before manipulating it.",
            "hint_1": "Decide which quantity is unknown and which relationships are given.",
            "hint_2": "Set up one equation at a time from those relationships.",
            "hint_3": "Only simplify after the structure is correct.",
            "walkthrough_steps": [
                "Name the unknown quantity clearly.",
                "Translate each condition into an equation or constraint.",
                "Simplify the algebra only after the setup is correct.",
            ],
            "method_steps": [
                "Algebra questions are easiest when you translate the wording into relationships before calculating.",
            ],
            "answer_path": [
                "Identify the unknown.",
                "Build the equation.",
                "Isolate the target quantity.",
            ],
            "common_trap": "Starting calculations before defining the unknown or building the equation correctly.",
        }

    def _percent_pack(self, question_text: str) -> Dict[str, Any]:
        q = question_text.lower()
        if "increase" in q or "decrease" in q:
            return {
                "first_step": "Turn each percent change into its own multiplier before combining anything.",
                "hint_1": "Use the original amount as a clean base, often 100, unless the question already gives a convenient number.",
                "hint_2": "Apply the first percentage change to the current amount, not the final amount.",
                "hint_3": "Apply the second change to the updated amount, then compare with the original only at the end.",
                "hint_ladder": [
                    "Treat a percent increase or decrease as multiplication, not simple adding or subtracting percentages.",
                    "Apply the first multiplier to the starting amount.",
                    "Apply the second multiplier to that new amount.",
                    "Compare the final result with the original base only after both changes are done.",
                ],
                "walkthrough_steps": [
                    "Choose an easy original value such as 100 if no starting number is given.",
                    "Convert each percentage change into a multiplier.",
                    "Apply the multipliers in sequence.",
                    "Express the final amount relative to the original amount.",
                ],
                "method_steps": [
                    "Successive percent changes are multiplicative because each new percent acts on the current amount.",
                    "That is why equal increases and decreases do not cancel each other out.",
                ],
                "answer_path": [
                    "Pick a clean base value.",
                    "Apply each change in order.",
                    "Compare final with original.",
                ],
                "common_trap": "Adding and subtracting the percentages directly instead of applying them sequentially.",
            }
        return {
            "first_step": "Ask 'percent of what?' before writing any equation.",
            "hint_1": "Separate the part, the percent, and the base quantity.",
            "hint_2": "Write the relationship as part = percent × base, or reverse it if the base is unknown.",
            "hint_3": "Only convert to a final percent form after the relationship is set up correctly.",
            "walkthrough_steps": [
                "Identify the base amount the percent is taken from.",
                "Write the percent as a decimal or fraction.",
                "Set up the percent relationship.",
                "Solve for the requested quantity.",
            ],
            "method_steps": [
                "Most percent errors come from choosing the wrong base quantity, not from arithmetic.",
            ],
            "answer_path": [
                "Identify the base quantity.",
                "Set up the percent relationship.",
                "Solve for the target.",
            ],
            "common_trap": "Using the part as the base or applying the percent to the wrong quantity.",
        }

    def _ratio_pack(self, question_text: str) -> Dict[str, Any]:
        ratio_text = self._extract_ratio(question_text)
        first = f"Treat {ratio_text} as matching parts of one whole." if ratio_text else "Treat the ratio numbers as parts, not final values."
        return {
            "first_step": first,
            "hint_1": "Represent each ratio part using one shared multiplier such as k.",
            "hint_2": "Use the given total or condition to find that shared multiplier.",
            "hint_3": "Substitute back into the exact quantity the question asks for.",
            "hint_ladder": [
                first,
                "Write each quantity as a ratio part times the same multiplier.",
                "Use the total or condition to solve for the multiplier.",
                "Build the requested expression from the actual quantities, not the raw ratio numbers.",
            ],
            "walkthrough_steps": [
                first,
                "Assign variables to each ratio part using one multiplier.",
                "Solve for the multiplier from the given condition.",
                "Evaluate the requested quantity.",
            ],
            "method_steps": [
                "Ratio questions simplify when you convert the ratio into actual quantities with one shared multiplier.",
            ],
            "answer_path": [
                "Write each part with a common multiplier.",
                "Solve for the multiplier.",
                "Substitute into the target expression.",
            ],
            "common_trap": "Using the raw ratio numbers as actual values instead of scaled parts.",
        }

    def _probability_pack(self, question_text: str) -> Dict[str, Any]:
        q = question_text.lower()
        pack = {
            "first_step": "Define exactly what counts as a successful outcome before you count anything.",
            "hint_1": "Count the favorable outcomes that satisfy the condition.",
            "hint_2": "Count the total possible outcomes in the sample space.",
            "hint_3": "Write probability as favorable over total, then simplify only at the end.",
            "hint_ladder": [
                "State the event in plain language: what outcome are you trying to get?",
                "Count the favorable cases for that event.",
                "Count the total possible cases in the sample space.",
                "Build the probability as favorable over total.",
            ],
            "walkthrough_steps": [
                "Define the event the question cares about.",
                "Count the favorable cases.",
                "Count the total possible cases.",
                "Write the probability as favorable divided by total.",
            ],
            "method_steps": [
                "Probability becomes much easier once the event and sample space are both explicit.",
                "Many mistakes come from counting the wrong denominator, not the numerator.",
            ],
            "answer_path": [
                "Define the event.",
                "Count favorable outcomes.",
                "Count total outcomes.",
            ],
            "common_trap": "Using the wrong denominator or forgetting outcomes that belong in the sample space.",
        }
        if "at least" in q or "at most" in q:
            pack["hint_ladder"] = [
                "Check whether the complement is easier to count than the event you want.",
                "Count the easier side first if that reduces the work.",
                "Convert back to the requested event at the end.",
                "Then write the probability with the correct denominator.",
            ]
        return pack

    def _statistics_pack(self, question_text: str) -> Dict[str, Any]:
        q = question_text.lower()
        if any(k in q for k in ["variability", "spread", "standard deviation"]):
            return {
                "first_step": "Notice that the question is about spread, not average.",
                "hint_1": "Compare how far the values sit from the centre of each set.",
                "hint_2": "A set with values clustered tightly has lower variability than a set spread farther apart.",
                "hint_3": "Pick the set with the widest spread, not the highest mean.",
                "hint_ladder": [
                    "Ignore the mean at first and focus on how spread out the values are.",
                    "Compare the distance of the outer values from the middle of each set.",
                    "The set with the wider spread has greater variability.",
                ],
                "walkthrough_steps": [
                    "Identify the centre of each set mentally or numerically.",
                    "Compare how tightly the values cluster around that centre.",
                    "Choose the set with the larger spread.",
                ],
                "method_steps": [
                    "Variability measures spread, so a dataset can have the same mean as another and still be more variable.",
                ],
                "answer_path": [
                    "Look at spread around the centre.",
                    "Compare clustering versus spread.",
                ],
                "common_trap": "Choosing the set with the highest mean instead of the greatest spread.",
            }
        return {
            "first_step": "Identify which statistical measure the question cares about before calculating.",
            "hint_1": "Decide whether the task is about mean, median, range, spread, or another measure.",
            "hint_2": "Organise the data cleanly if that makes the measure easier to see.",
            "hint_3": "Use the exact definition of the requested measure.",
            "walkthrough_steps": [
                "Identify the requested statistic.",
                "Organise the data.",
                "Apply the definition of that statistic.",
            ],
            "method_steps": [
                "Statistics questions are easiest once you know which measure matters.",
            ],
            "answer_path": [
                "Identify the target statistic.",
                "Apply its definition.",
            ],
            "common_trap": "Using a nearby but different statistical measure.",
        }

    def _geometry_pack(self, question_text: str) -> Dict[str, Any]:
        q = question_text.lower()
        if "perimeter" in q and "rectangle" in q:
            return {
                "first_step": "Start with the perimeter formula for a rectangle: 2L + 2W.",
                "hint_1": "Substitute the known perimeter and known side length into the formula.",
                "hint_2": "Isolate the remaining side length after substitution.",
                "hint_3": "Check the width in the perimeter formula once more.",
                "walkthrough_steps": [
                    "Write the perimeter formula.",
                    "Plug in the given perimeter and length.",
                    "Solve for the width.",
                ],
                "method_steps": [
                    "Geometry questions are often formula-matching questions first and algebra questions second.",
                ],
                "answer_path": [
                    "Write the formula.",
                    "Substitute given values.",
                    "Solve for the missing side.",
                ],
                "common_trap": "Forgetting that perimeter includes both lengths and both widths.",
            }
        return {
            "first_step": "Identify the shape and the formula that matches it.",
            "hint_1": "Write the relevant geometry formula before substituting numbers.",
            "hint_2": "Substitute carefully and keep track of what the question actually asks for.",
            "hint_3": "Use algebra only after the correct formula is in place.",
            "walkthrough_steps": [
                "Identify the shape.",
                "Choose the correct formula.",
                "Substitute values and solve.",
            ],
            "method_steps": [
                "Most geometry errors come from choosing the wrong formula or solving for the wrong quantity.",
            ],
            "answer_path": [
                "Match the shape to its formula.",
                "Substitute the known values.",
                "Solve for the target quantity.",
            ],
            "common_trap": "Using the wrong formula or solving for the wrong dimension.",
        }

    def _topic_defaults(self, topic: str, question_text: str, options_text: Optional[List[str]]) -> Dict[str, Any]:
        topic = self._normalize_topic(topic, question_text)
        if topic == "algebra":
            return self._algebra_pack(question_text)
        if topic == "percent":
            return self._percent_pack(question_text)
        if topic == "ratio":
            return self._ratio_pack(question_text)
        if topic == "probability":
            return self._probability_pack(question_text)
        if topic == "statistics":
            return self._statistics_pack(question_text)
        if topic == "geometry":
            return self._geometry_pack(question_text)
        return {
            "first_step": "Identify the exact relationship the question is testing before doing any arithmetic.",
            "hint_1": "Separate what is given from what you need to find.",
            "hint_2": "Build the relationship or formula that links those pieces.",
            "hint_3": "Only calculate after the structure is correct.",
            "walkthrough_steps": [
                "State what is given and what is unknown.",
                "Build the relationship between them.",
                "Solve for the requested quantity.",
            ],
            "method_steps": [
                "General quant questions become clearer when you translate the wording into a structure first.",
            ],
            "answer_path": [
                "Identify the structure.",
                "Set up the relationship.",
                "Solve the target quantity.",
            ],
            "common_trap": "Starting arithmetic before the structure of the problem is clear.",
        }

    def _merge_support_pack(self, generated: Dict[str, Any], stored: Optional[Dict[str, Any]], topic: str) -> Dict[str, Any]:
        if not stored:
            out = dict(generated)
            out["support_source"] = "generated_question_specific"
            return out

        merged = dict(stored)
        looks_generic = self._pack_looks_generic(stored, topic)
        if looks_generic:
            for key, value in generated.items():
                if value:
                    merged[key] = value
            merged["support_source"] = "question_bank_refined"
        else:
            for key, value in generated.items():
                if key not in merged or not merged.get(key):
                    merged[key] = value
            merged["support_source"] = "question_bank"
        return merged

    def get_support_pack(self, *, question_id: Optional[str], question_text: str, options_text: Optional[List[str]], topic: Optional[str], category: Optional[str]) -> Dict[str, Any]:
        resolved_topic = self._normalize_topic(topic, question_text)
        generated = self._topic_defaults(resolved_topic, question_text, options_text)
        stored = question_support_bank.get(question_id=question_id, question_text=question_text, options_text=options_text)
        pack = self._merge_support_pack(generated, stored, resolved_topic)
        pack.setdefault("question_id", question_id)
        pack.setdefault("question_text", question_text)
        pack.setdefault("stem", question_text)
        pack.setdefault("options_text", list(options_text or []))
        pack.setdefault("choices", list(options_text or []))
        pack.setdefault("topic", resolved_topic)
        pack.setdefault("category", category or "General")
        return pack

    def _hint_ladder_from_pack(self, pack: Dict[str, Any]) -> List[str]:
        lines: List[str] = []
        if self._clean(pack.get("first_step")):
            lines.append(self._clean(pack.get("first_step")))
        for key in ("hint_1", "hint_2", "hint_3"):
            value = self._clean(pack.get(key))
            if value:
                lines.append(value)
        lines.extend(self._listify(pack.get("hint_ladder")))
        lines.extend(self._listify(pack.get("hints")))
        return self._dedupe(lines)

    def _walkthrough_from_pack(self, pack: Dict[str, Any]) -> List[str]:
        lines = self._listify(pack.get("walkthrough_steps"))
        if not lines and self._clean(pack.get("first_step")):
            lines.append(self._clean(pack.get("first_step")))
        return self._dedupe(lines)

    def _method_from_pack(self, pack: Dict[str, Any]) -> List[str]:
        lines: List[str] = []
        concept = self._clean(pack.get("concept"))
        if concept:
            lines.append(concept)
        lines.extend(self._listify(pack.get("method_steps")))
        lines.extend(self._listify(pack.get("method_explanation")))
        if not lines:
            lines.extend(self._walkthrough_from_pack(pack)[:2])
        return self._dedupe(lines)

    def _answer_path_from_pack(self, pack: Dict[str, Any]) -> List[str]:
        lines = self._listify(pack.get("answer_path"))
        if not lines:
            lines = self._walkthrough_from_pack(pack)
        return self._dedupe(lines)

    def _verbosity_limit(self, verbosity: float, *, low: int, mid: int, high: int) -> int:
        if verbosity < 0.28:
            return low
        if verbosity < 0.68:
            return mid
        return high

    def build_response(self, *, question_id: Optional[str], question_text: str, options_text: Optional[List[str]], topic: Optional[str], category: Optional[str], help_mode: str, hint_stage: int, verbosity: float) -> Dict[str, Any]:
        pack = self.get_support_pack(question_id=question_id, question_text=question_text, options_text=options_text, topic=topic, category=category)
        mode = (help_mode or "answer").lower()
        stage = max(1, int(hint_stage or 1))
        hint_ladder = self._hint_ladder_from_pack(pack)
        walkthrough_steps = self._walkthrough_from_pack(pack)
        method_steps = self._method_from_pack(pack)
        answer_path = self._answer_path_from_pack(pack)
        common_trap = self._clean(pack.get("common_trap"))

        if mode == "hint":
            idx = min(stage - 1, max(len(hint_ladder) - 1, 0))
            if hint_ladder:
                lines = [hint_ladder[idx]]
                if verbosity >= 0.62 and idx + 1 < len(hint_ladder):
                    lines.append(hint_ladder[idx + 1])
                if verbosity >= 0.82 and stage >= 3 and common_trap:
                    lines.append(f"Watch out for this trap: {common_trap}")
            else:
                lines = [self._clean(pack.get("first_step")) or "Start with the structure of the problem."]
        elif mode in {"walkthrough", "instruction", "step_by_step"}:
            source = walkthrough_steps or answer_path or hint_ladder
            lines = source[: self._verbosity_limit(verbosity, low=2, mid=4, high=6)]
            if verbosity >= 0.8 and common_trap:
                lines = list(lines) + [f"Watch out for this trap: {common_trap}"]
        elif mode in {"method", "explain", "concept", "definition"}:
            source = method_steps or walkthrough_steps or answer_path
            lines = source[: self._verbosity_limit(verbosity, low=1, mid=2, high=4)]
            if verbosity >= 0.72 and common_trap:
                lines = list(lines) + [f"Common trap: {common_trap}"]
        else:
            source = answer_path or walkthrough_steps or hint_ladder
            lines = source[: self._verbosity_limit(verbosity, low=2, mid=3, high=5)]

        return {"lines": self._dedupe(lines), "pack": pack}


question_fallback_router = QuestionFallbackRouter()