j-js commited on
Commit
ad65d71
·
verified ·
1 Parent(s): 280a1a8

Create question_fallback_router.py

Browse files
Files changed (1) hide show
  1. question_fallback_router.py +269 -0
question_fallback_router.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from question_support_loader import question_support_bank
7
+
8
+
9
+ class QuestionFallbackRouter:
10
+ def _clean(self, text: Optional[str]) -> str:
11
+ return (text or "").strip()
12
+
13
+ def _listify(self, value: Any) -> List[str]:
14
+ if value is None:
15
+ return []
16
+ if isinstance(value, list):
17
+ return [str(v).strip() for v in value if str(v).strip()]
18
+ if isinstance(value, tuple):
19
+ return [str(v).strip() for v in value if str(v).strip()]
20
+ if isinstance(value, str):
21
+ text = value.strip()
22
+ return [text] if text else []
23
+ return []
24
+
25
+ def _normalize_topic(self, topic: Optional[str], question_text: str) -> str:
26
+ q = (question_text or "").lower()
27
+ t = (topic or "").strip().lower()
28
+ if t and t not in {"general", "unknown", "general_quant"}:
29
+ return t
30
+ if "%" in q or "percent" in q:
31
+ return "percent"
32
+ if "ratio" in q or re.search(r"\b\d+\s*:\s*\d+\b", q):
33
+ return "ratio"
34
+ if any(k in q for k in ["probability", "odds", "chance", "random"]):
35
+ return "probability"
36
+ if any(k in q for k in ["remainder", "factor", "multiple", "prime", "divisible"]):
37
+ return "number_theory"
38
+ if any(k in q for k in ["triangle", "circle", "angle", "area", "perimeter"]):
39
+ return "geometry"
40
+ if any(k in q for k in ["mean", "median", "average", "standard deviation"]):
41
+ return "statistics"
42
+ if "=" in q or re.search(r"\b[xyz]\b", q):
43
+ return "algebra"
44
+ return "general"
45
+
46
+ def _preview_question(self, question_text: str) -> str:
47
+ cleaned = " ".join(question_text.split())
48
+ if len(cleaned) <= 120:
49
+ return cleaned
50
+ return cleaned[:117].rstrip() + "..."
51
+
52
+ def _topic_defaults(self, topic: str, question_text: str, options_text: Optional[List[str]]) -> Dict[str, Any]:
53
+ preview = self._preview_question(question_text)
54
+ has_options = bool(options_text)
55
+
56
+ generic = {
57
+ "first_step": f"Focus on what the question is asking in: {preview}",
58
+ "hint_ladder": [
59
+ "Identify the exact quantity the question wants.",
60
+ "Translate the relationships in the stem into a setup you can work with.",
61
+ "Check each step against the wording before choosing an option.",
62
+ ],
63
+ "walkthrough_steps": [
64
+ "Underline what is given and what must be found.",
65
+ "Set up the relationship before you calculate.",
66
+ "Work step by step and keep the units or labels consistent.",
67
+ ],
68
+ "method_steps": [
69
+ "Start from the structure of the problem rather than jumping into arithmetic.",
70
+ "Use the wording to decide which relationship matters most.",
71
+ ],
72
+ "answer_path": [
73
+ "Set up the correct structure first.",
74
+ "Only then simplify or evaluate the result.",
75
+ ],
76
+ "common_trap": "Rushing into calculation before setting up the relationship.",
77
+ }
78
+
79
+ if topic == "algebra":
80
+ generic.update(
81
+ {
82
+ "first_step": "Write the equation exactly as given and identify the operation attached to the variable.",
83
+ "hint_ladder": [
84
+ "Look at which operation is attached to the variable first.",
85
+ "Undo that operation on both sides, not just one side.",
86
+ "Keep simplifying until the variable is isolated.",
87
+ ],
88
+ "walkthrough_steps": [
89
+ "Copy the equation exactly as written.",
90
+ "Use inverse operations to undo what is attached to the variable.",
91
+ "Do the same thing to both sides each time.",
92
+ "Once the variable is isolated, compare with the answer choices if there are any.",
93
+ ],
94
+ "method_steps": [
95
+ "Algebra questions are about preserving balance while isolating the unknown.",
96
+ "Treat each operation in reverse order to peel the equation back.",
97
+ ],
98
+ "common_trap": "Changing only one side of the equation or combining terms too early.",
99
+ }
100
+ )
101
+ elif topic == "percent":
102
+ generic.update(
103
+ {
104
+ "first_step": "Identify the base quantity before doing any percent calculation.",
105
+ "hint_ladder": [
106
+ "Decide what quantity the percent is 'of'.",
107
+ "Rewrite the percent as a decimal or fraction if that helps.",
108
+ "Set up part = percent × base, or reverse it if the base is unknown.",
109
+ ],
110
+ "walkthrough_steps": [
111
+ "Find the base quantity the percent refers to.",
112
+ "Translate the wording into a percent relationship.",
113
+ "Solve for the missing quantity.",
114
+ "Check whether the question asks for the part, the whole, or a percent change.",
115
+ ],
116
+ "method_steps": [
117
+ "Percent questions become easier once you identify the correct base.",
118
+ "Do not apply the percent to the wrong quantity.",
119
+ ],
120
+ "common_trap": "Using the wrong base quantity.",
121
+ }
122
+ )
123
+ elif topic == "ratio":
124
+ generic.update(
125
+ {
126
+ "first_step": "Keep the ratio order consistent and assign one shared multiplier.",
127
+ "hint_ladder": [
128
+ "Write each part of the ratio with the same multiplier.",
129
+ "Use the total or known part to solve for that multiplier.",
130
+ "Substitute back into the specific part you need.",
131
+ ],
132
+ "walkthrough_steps": [
133
+ "Translate the ratio into algebraic parts with one common scale factor.",
134
+ "Use the total or given piece to solve for the factor.",
135
+ "Find the required part and check the order of the ratio.",
136
+ ],
137
+ "method_steps": [
138
+ "Ratio questions are controlled by one shared multiplier.",
139
+ "Preserve the ratio order all the way through.",
140
+ ],
141
+ "common_trap": "Reversing the ratio order or using different multipliers for different parts.",
142
+ }
143
+ )
144
+ elif topic == "probability":
145
+ generic.update(
146
+ {
147
+ "first_step": "Count the successful outcomes and the total possible outcomes separately.",
148
+ "hint_ladder": [
149
+ "Decide exactly what counts as a success.",
150
+ "Count all possible outcomes under the same rules.",
151
+ "Write probability as successful over total, then simplify if needed.",
152
+ ],
153
+ "walkthrough_steps": [
154
+ "Identify the event the question cares about.",
155
+ "Count the successful outcomes.",
156
+ "Count the full sample space.",
157
+ "Form the probability and simplify carefully.",
158
+ ],
159
+ "method_steps": [
160
+ "Probability is a comparison of favorable outcomes to all valid outcomes.",
161
+ "Make sure the numerator and denominator come from the same setup.",
162
+ ],
163
+ "common_trap": "Counting the right numerator with the wrong denominator.",
164
+ }
165
+ )
166
+ elif topic == "number_theory":
167
+ generic.update(
168
+ {
169
+ "first_step": "Identify which number property matters most: factors, multiples, divisibility, or remainders.",
170
+ "common_trap": "Using arithmetic intuition instead of the actual number-property rule being tested.",
171
+ }
172
+ )
173
+
174
+ if has_options:
175
+ generic["answer_path"].append("Use the choices to check which one matches your setup instead of guessing.")
176
+
177
+ return generic
178
+
179
+ def get_support_pack(
180
+ self,
181
+ *,
182
+ question_id: Optional[str],
183
+ question_text: str,
184
+ options_text: Optional[List[str]],
185
+ topic: Optional[str],
186
+ category: Optional[str],
187
+ ) -> Dict[str, Any]:
188
+ stored = question_support_bank.get(question_id=question_id, question_text=question_text)
189
+ resolved_topic = self._normalize_topic(topic, question_text)
190
+ if stored:
191
+ pack = dict(stored)
192
+ pack.setdefault("topic", resolved_topic)
193
+ pack.setdefault("category", category or "General")
194
+ pack.setdefault("support_source", "question_bank")
195
+ return pack
196
+
197
+ generated = self._topic_defaults(resolved_topic, question_text, options_text)
198
+ generated.update(
199
+ {
200
+ "question_id": question_id,
201
+ "question_text": question_text,
202
+ "options_text": list(options_text or []),
203
+ "topic": resolved_topic,
204
+ "category": category or "General",
205
+ "support_source": "generated_question_specific",
206
+ }
207
+ )
208
+ return generated
209
+
210
+ def build_response(
211
+ self,
212
+ *,
213
+ question_id: Optional[str],
214
+ question_text: str,
215
+ options_text: Optional[List[str]],
216
+ topic: Optional[str],
217
+ category: Optional[str],
218
+ help_mode: str,
219
+ hint_stage: int,
220
+ verbosity: float,
221
+ ) -> Dict[str, Any]:
222
+ pack = self.get_support_pack(
223
+ question_id=question_id,
224
+ question_text=question_text,
225
+ options_text=options_text,
226
+ topic=topic,
227
+ category=category,
228
+ )
229
+
230
+ mode = (help_mode or "answer").lower()
231
+ stage = max(1, min(int(hint_stage or 1), 3))
232
+
233
+ hint_ladder = self._listify(pack.get("hint_ladder") or pack.get("hints"))
234
+ walkthrough_steps = self._listify(pack.get("walkthrough_steps"))
235
+ method_steps = self._listify(pack.get("method_steps") or pack.get("method_explanation"))
236
+ answer_path = self._listify(pack.get("answer_path"))
237
+ common_trap = self._clean(pack.get("common_trap"))
238
+ first_step = self._clean(pack.get("first_step"))
239
+
240
+ if mode == "hint":
241
+ selected = []
242
+ if first_step and stage == 1:
243
+ selected.append(first_step)
244
+ if hint_ladder:
245
+ idx = min(stage - 1, len(hint_ladder) - 1)
246
+ selected.append(hint_ladder[idx])
247
+ if common_trap and verbosity >= 0.55:
248
+ selected.append(f"Watch out for this trap: {common_trap}")
249
+ lines = selected or [first_step or "Start by identifying the structure of the question."]
250
+ elif mode in {"walkthrough", "step_by_step", "instruction"}:
251
+ lines = walkthrough_steps or answer_path or hint_ladder or [first_step or "Start by setting up the problem."]
252
+ if first_step and (not lines or lines[0] != first_step):
253
+ lines = [first_step] + lines
254
+ elif mode in {"method", "explain", "concept", "definition"}:
255
+ lines = method_steps or walkthrough_steps or answer_path or [first_step or "Start from the problem structure."]
256
+ if common_trap and verbosity >= 0.65:
257
+ lines = list(lines) + [f"Common trap: {common_trap}"]
258
+ else:
259
+ lines = answer_path or walkthrough_steps or hint_ladder or [first_step or "Start by identifying the relationship in the question."]
260
+ if first_step and (not lines or lines[0] != first_step):
261
+ lines = [first_step] + lines
262
+
263
+ return {
264
+ "lines": lines,
265
+ "pack": pack,
266
+ }
267
+
268
+
269
+ question_fallback_router = QuestionFallbackRouter()