j-js commited on
Commit
31fad45
·
verified ·
1 Parent(s): 08229e6

Create quant_solver.py

Browse files
Files changed (1) hide show
  1. quant_solver.py +470 -0
quant_solver.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+ from statistics import mean, median
6
+ from typing import Dict, List, Optional
7
+
8
+ from models import SolverResult
9
+ from utils import clean_math_text, normalize_spaces
10
+
11
+ try:
12
+ import sympy as sp
13
+ except Exception:
14
+ sp = None
15
+
16
+
17
+ CHOICE_LETTERS = ["A", "B", "C", "D", "E"]
18
+
19
+
20
+ def has_answer_choices(text: str) -> bool:
21
+ patterns = [
22
+ r"\bA[\)\.\:]",
23
+ r"\bB[\)\.\:]",
24
+ r"\bC[\)\.\:]",
25
+ r"\bD[\)\.\:]",
26
+ r"\bE[\)\.\:]",
27
+ ]
28
+ return sum(bool(re.search(p, text, flags=re.I)) for p in patterns) >= 3
29
+
30
+
31
+ def extract_choices(text: str) -> Dict[str, str]:
32
+ matches = list(
33
+ re.finditer(
34
+ r"(?im)(?:^|\n|\s)([A-E])[\)\.\:]\s*(.*?)(?=(?:\n?\s*[A-E][\)\.\:]\s)|$)",
35
+ text,
36
+ )
37
+ )
38
+ choices: Dict[str, str] = {}
39
+ for m in matches:
40
+ letter = m.group(1).upper()
41
+ content = normalize_spaces(m.group(2))
42
+ choices[letter] = content
43
+ return choices
44
+
45
+
46
+ def extract_numbers(text: str) -> List[float]:
47
+ nums = re.findall(r"-?\d+(?:\.\d+)?", text.replace(",", ""))
48
+ out: List[float] = []
49
+ for n in nums:
50
+ try:
51
+ out.append(float(n))
52
+ except Exception:
53
+ pass
54
+ return out
55
+
56
+
57
+ def is_quant_question(text: str) -> bool:
58
+ lower = text.lower()
59
+ quant_keywords = [
60
+ "solve", "equation", "integer", "percent", "percentage", "ratio", "probability",
61
+ "mean", "median", "average", "sum", "difference", "product", "quotient",
62
+ "triangle", "circle", "rectangle", "perimeter", "area", "volume",
63
+ "number line", "positive", "negative", "multiple", "factor", "prime",
64
+ "distance", "speed", "work", "mixture", "consecutive", "algebra",
65
+ "value of x", "value of y", "what is x", "what is y"
66
+ ]
67
+ if any(k in lower for k in quant_keywords):
68
+ return True
69
+ if re.search(r"[0-9]", text) and ("?" in text or has_answer_choices(text)):
70
+ return True
71
+ return False
72
+
73
+
74
+ def try_eval_expression(expr: str) -> Optional[float]:
75
+ expr = clean_math_text(expr)
76
+ expr = expr.strip()
77
+ expr = expr.replace("%", "/100")
78
+ expr = re.sub(r"[^0-9\.\+\-\*\/\(\)\s]", "", expr)
79
+
80
+ if not expr:
81
+ return None
82
+
83
+ try:
84
+ return float(eval(expr, {"__builtins__": {}}, {}))
85
+ except Exception:
86
+ return None
87
+
88
+
89
+ def parse_direct_expression_question(text: str) -> Optional[str]:
90
+ lower = text.lower()
91
+ m = re.search(
92
+ r"(?:what is|calculate|compute|evaluate|find)\s+([0-9\.\+\-\*\/\(\)\s]+)\??",
93
+ lower,
94
+ )
95
+ if not m:
96
+ return None
97
+ expr = m.group(1).strip()
98
+ if len(expr) < 1:
99
+ return None
100
+ return expr
101
+
102
+
103
+ def solve_basic_expression(text: str, help_mode: str) -> Optional[SolverResult]:
104
+ expr = parse_direct_expression_question(text)
105
+ if not expr:
106
+ return None
107
+
108
+ value = try_eval_expression(expr)
109
+ if value is None:
110
+ return None
111
+
112
+ if help_mode == "hint":
113
+ reply = "Focus on order of operations: parentheses, multiplication/division, then addition/subtraction."
114
+ elif help_mode == "walkthrough":
115
+ reply = f"Evaluate the expression {expr} using order of operations.\nThat gives {value:g}."
116
+ else:
117
+ reply = f"The value is {value:g}."
118
+
119
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{value:g}")
120
+
121
+
122
+ def solve_percent_question(text: str, help_mode: str) -> Optional[SolverResult]:
123
+ lower = text.lower()
124
+
125
+ m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+of\s+(\d+(?:\.\d+)?)", lower)
126
+ if m:
127
+ p = float(m.group(1))
128
+ n = float(m.group(2))
129
+ ans = p / 100 * n
130
+
131
+ if help_mode == "hint":
132
+ reply = "Convert the percent to a decimal, then multiply."
133
+ elif help_mode == "walkthrough":
134
+ reply = f"{p}% of {n} = {p/100:g} × {n} = {ans:g}."
135
+ else:
136
+ reply = f"The answer is {ans:g}."
137
+
138
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
139
+
140
+ m = re.search(r"(\d+(?:\.\d+)?)\s+is\s+what percent of\s+(\d+(?:\.\d+)?)", lower)
141
+ if m:
142
+ x = float(m.group(1))
143
+ y = float(m.group(2))
144
+ if y == 0:
145
+ return None
146
+
147
+ ans = x / y * 100
148
+
149
+ if help_mode == "hint":
150
+ reply = "Use part ÷ whole × 100."
151
+ elif help_mode == "walkthrough":
152
+ reply = f"Percent = ({x} / {y}) × 100 = {ans:g}%."
153
+ else:
154
+ reply = f"The answer is {ans:g}%."
155
+
156
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}%")
157
+
158
+ m = re.search(
159
+ r"(\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased)\s+by\s+(\d+(?:\.\d+)?)\s*%",
160
+ lower,
161
+ )
162
+ if m:
163
+ base = float(m.group(1))
164
+ direction = m.group(2)
165
+ p = float(m.group(3)) / 100
166
+ ans = base * (1 + p) if direction == "increased" else base * (1 - p)
167
+
168
+ if help_mode == "hint":
169
+ reply = "Use multiplier form: increase → 1 + p, decrease → 1 - p."
170
+ elif help_mode == "walkthrough":
171
+ mult = 1 + p if direction == "increased" else 1 - p
172
+ reply = f"Multiplier = {mult:g}, so {base:g} × {mult:g} = {ans:g}."
173
+ else:
174
+ reply = f"The result is {ans:g}."
175
+
176
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
177
+
178
+ return None
179
+
180
+
181
+ def solve_ratio_question(text: str, help_mode: str) -> Optional[SolverResult]:
182
+ lower = text.lower()
183
+
184
+ m = re.search(
185
+ r"ratio of .*? is\s+(\d+)\s*:\s*(\d+).*?total (?:is|of)\s+(\d+(?:\.\d+)?)",
186
+ lower
187
+ )
188
+ if not m:
189
+ m = re.search(
190
+ r"(\d+)\s*:\s*(\d+).*?total (?:is|of)\s+(\d+(?:\.\d+)?)",
191
+ lower
192
+ )
193
+
194
+ if m:
195
+ a = float(m.group(1))
196
+ b = float(m.group(2))
197
+ total = float(m.group(3))
198
+ parts = a + b
199
+ if parts == 0:
200
+ return None
201
+
202
+ first = total * a / parts
203
+ second = total * b / parts
204
+
205
+ if help_mode == "hint":
206
+ reply = "Add the ratio parts, then convert each part into a fraction of the total."
207
+ elif help_mode == "walkthrough":
208
+ reply = (
209
+ f"Total parts = {a:g} + {b:g} = {parts:g}.\n"
210
+ f"First quantity = {a:g}/{parts:g} × {total:g} = {first:g}.\n"
211
+ f"Second quantity = {b:g}/{parts:g} × {total:g} = {second:g}."
212
+ )
213
+ else:
214
+ reply = f"The two quantities are {first:g} and {second:g}."
215
+
216
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{first:g}, {second:g}")
217
+
218
+ return None
219
+
220
+
221
+ def solve_average_question(text: str, help_mode: str) -> Optional[SolverResult]:
222
+ lower = text.lower()
223
+
224
+ if "mean" in lower or "average" in lower:
225
+ nums = extract_numbers(text)
226
+ if len(nums) >= 2:
227
+ ans = mean(nums)
228
+
229
+ if help_mode == "hint":
230
+ reply = "Add the numbers, then divide by how many there are."
231
+ elif help_mode == "walkthrough":
232
+ reply = f"The numbers are {', '.join(f'{x:g}' for x in nums)}.\nTheir average is {ans:g}."
233
+ else:
234
+ reply = f"The average is {ans:g}."
235
+
236
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
237
+
238
+ if "median" in lower:
239
+ nums = extract_numbers(text)
240
+ if len(nums) >= 2:
241
+ ans = median(nums)
242
+
243
+ if help_mode == "hint":
244
+ reply = "Sort the numbers first, then take the middle value."
245
+ elif help_mode == "walkthrough":
246
+ s = sorted(nums)
247
+ reply = f"Sorted numbers: {', '.join(f'{x:g}' for x in s)}.\nThe median is {ans:g}."
248
+ else:
249
+ reply = f"The median is {ans:g}."
250
+
251
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
252
+
253
+ return None
254
+
255
+
256
+ def solve_probability_question(text: str, help_mode: str) -> Optional[SolverResult]:
257
+ lower = text.lower()
258
+
259
+ m = re.search(r"(\d+)\s+red.*?(\d+)\s+blue.*?probability", lower)
260
+ if m:
261
+ r = float(m.group(1))
262
+ b = float(m.group(2))
263
+ total = r + b
264
+ if total == 0:
265
+ return None
266
+
267
+ ans = r / total
268
+
269
+ if help_mode == "hint":
270
+ reply = "Probability = favorable outcomes ÷ total outcomes."
271
+ elif help_mode == "walkthrough":
272
+ reply = f"Favorable outcomes = {r:g}, total outcomes = {total:g}, so probability = {r:g}/{total:g} = {ans:g}."
273
+ else:
274
+ reply = f"The probability is {ans:g}."
275
+
276
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
277
+
278
+ return None
279
+
280
+
281
+ def solve_geometry_question(text: str, help_mode: str) -> Optional[SolverResult]:
282
+ lower = text.lower()
283
+
284
+ m = re.search(r"rectangle.*?length\s*(?:is|=)?\s*(\d+(?:\.\d+)?)\D+width\s*(?:is|=)?\s*(\d+(?:\.\d+)?)", lower)
285
+ if m and "area" in lower:
286
+ l = float(m.group(1))
287
+ w = float(m.group(2))
288
+ ans = l * w
289
+
290
+ if help_mode == "hint":
291
+ reply = "For a rectangle, area = length × width."
292
+ elif help_mode == "walkthrough":
293
+ reply = f"Area = {l:g} × {w:g} = {ans:g}."
294
+ else:
295
+ reply = f"The area is {ans:g}."
296
+
297
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
298
+
299
+ m = re.search(r"circle.*?radius\s*(?:is|=)?\s*(\d+(?:\.\d+)?)", lower)
300
+ if m and "area" in lower:
301
+ r = float(m.group(1))
302
+ ans = math.pi * r * r
303
+
304
+ if help_mode == "hint":
305
+ reply = "For a circle, area = πr²."
306
+ elif help_mode == "walkthrough":
307
+ reply = f"Area = π({r:g})² = {ans:.4f}."
308
+ else:
309
+ reply = f"The area is {ans:.4f}."
310
+
311
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:.4f}")
312
+
313
+ m = re.search(r"triangle.*?base\s*(?:is|=)?\s*(\d+(?:\.\d+)?)\D+height\s*(?:is|=)?\s*(\d+(?:\.\d+)?)", lower)
314
+ if m and "area" in lower:
315
+ b = float(m.group(1))
316
+ h = float(m.group(2))
317
+ ans = 0.5 * b * h
318
+
319
+ if help_mode == "hint":
320
+ reply = "Triangle area = 1/2 × base × height."
321
+ elif help_mode == "walkthrough":
322
+ reply = f"Area = 1/2 × {b:g} × {h:g} = {ans:g}."
323
+ else:
324
+ reply = f"The area is {ans:g}."
325
+
326
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
327
+
328
+ return None
329
+
330
+
331
+ def solve_linear_equation(text: str, help_mode: str) -> Optional[SolverResult]:
332
+ if sp is None:
333
+ return None
334
+
335
+ lower = text.lower().replace("^", "**")
336
+
337
+ m = re.search(r"(?:solve for x|find x|value of x).*?([\-0-9a-z\+\*/\s\(\)]+=[\-0-9a-z\+\*/\s\(\)]+)", lower)
338
+ if not m:
339
+ m = re.search(r"([\-0-9a-z\+\*/\s\(\)]+=[\-0-9a-z\+\*/\s\(\)]+)", lower)
340
+ if not m:
341
+ return None
342
+
343
+ eq_text = m.group(1).strip()
344
+ eq_text = re.sub(r"(\d)([a-z])", r"\1*\2", eq_text)
345
+ eq_text = re.sub(r"([a-z])(\d)", r"\1*\2", eq_text)
346
+
347
+ parts = eq_text.split("=")
348
+ if len(parts) != 2:
349
+ return None
350
+
351
+ try:
352
+ x = sp.symbols("x")
353
+ left = sp.sympify(parts[0])
354
+ right = sp.sympify(parts[1])
355
+ sols = sp.solve(sp.Eq(left, right), x)
356
+ if not sols:
357
+ return None
358
+
359
+ sol = sols[0]
360
+
361
+ if help_mode == "hint":
362
+ reply = "Collect the x-terms on one side and constants on the other."
363
+ elif help_mode == "walkthrough":
364
+ reply = f"Solve {eq_text} for x.\nThis gives x = {sp.simplify(sol)}."
365
+ else:
366
+ reply = f"x = {sp.simplify(sol)}."
367
+
368
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=str(sp.simplify(sol)))
369
+ except Exception:
370
+ return None
371
+
372
+
373
+ def compare_to_choices_numeric(answer_value: float, choices: Dict[str, str]) -> Optional[str]:
374
+ best_letter = None
375
+ best_diff = float("inf")
376
+
377
+ for letter, raw in choices.items():
378
+ expr = raw.strip()
379
+ expr_val = try_eval_expression(expr)
380
+
381
+ if expr_val is None:
382
+ nums = extract_numbers(expr)
383
+ if len(nums) == 1:
384
+ expr_val = nums[0]
385
+
386
+ if expr_val is None:
387
+ continue
388
+
389
+ diff = abs(expr_val - answer_value)
390
+ if diff < best_diff:
391
+ best_diff = diff
392
+ best_letter = letter
393
+
394
+ return best_letter
395
+
396
+
397
+ def solve_by_option_checking(text: str, help_mode: str) -> Optional[SolverResult]:
398
+ choices = extract_choices(text)
399
+ if len(choices) < 3:
400
+ return None
401
+
402
+ expr = parse_direct_expression_question(text)
403
+ if expr:
404
+ val = try_eval_expression(expr)
405
+ if val is not None:
406
+ letter = compare_to_choices_numeric(val, choices)
407
+ if letter:
408
+ if help_mode == "hint":
409
+ reply = "Evaluate the expression first, then match it to the answer choices."
410
+ elif help_mode == "walkthrough":
411
+ reply = f"The expression evaluates to {val:g}, which matches choice {letter}."
412
+ else:
413
+ reply = f"The correct choice is {letter}."
414
+
415
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_letter=letter, answer_value=f"{val:g}")
416
+
417
+ lower = text.lower()
418
+ m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+of\s+(\d+(?:\.\d+)?)", lower)
419
+ if m:
420
+ val = float(m.group(1)) / 100 * float(m.group(2))
421
+ letter = compare_to_choices_numeric(val, choices)
422
+ if letter:
423
+ if help_mode == "hint":
424
+ reply = "Compute the percentage, then match it to the options."
425
+ elif help_mode == "walkthrough":
426
+ reply = f"{m.group(1)}% of {m.group(2)} = {val:g}, so the correct choice is {letter}."
427
+ else:
428
+ reply = f"The correct choice is {letter}."
429
+
430
+ return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_letter=letter, answer_value=f"{val:g}")
431
+
432
+ return None
433
+
434
+
435
+ def solve_quant(text: str, help_mode: str) -> SolverResult:
436
+ solvers = [
437
+ solve_by_option_checking,
438
+ solve_basic_expression,
439
+ solve_percent_question,
440
+ solve_ratio_question,
441
+ solve_average_question,
442
+ solve_probability_question,
443
+ solve_geometry_question,
444
+ solve_linear_equation,
445
+ ]
446
+
447
+ for solver in solvers:
448
+ try:
449
+ out = solver(text, help_mode)
450
+ if out:
451
+ return out
452
+ except Exception:
453
+ pass
454
+
455
+ if help_mode == "hint":
456
+ reply = (
457
+ "Identify the question type first: arithmetic, percent, ratio, algebra, probability, "
458
+ "statistics, or geometry. Then extract the given numbers and set up the core relation."
459
+ )
460
+ elif help_mode == "walkthrough":
461
+ reply = (
462
+ "I can help with this, but I cannot confidently solve it from the current parse alone. "
463
+ "I can still talk through the first step or eliminate options if needed."
464
+ )
465
+ else:
466
+ reply = (
467
+ "I can help with this, but I can’t confidently solve it from the current parse alone yet."
468
+ )
469
+
470
+ return SolverResult(reply=reply, domain="quant", solved=False, help_mode=help_mode)