j-js commited on
Commit
5f98033
·
verified ·
1 Parent(s): 4d3cfba

Create solver_algebra.py

Browse files
Files changed (1) hide show
  1. solver_algebra.py +810 -0
solver_algebra.py ADDED
@@ -0,0 +1,810 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import List, Optional, Tuple, Dict, Any
5
+
6
+ from models import SolverResult
7
+
8
+ try:
9
+ import sympy as sp
10
+ except Exception:
11
+ sp = None
12
+
13
+
14
+ # =========================================================
15
+ # Main entry
16
+ # =========================================================
17
+
18
+ def solve_algebra(text: str) -> Optional[SolverResult]:
19
+ raw = (text or "").strip()
20
+ if not raw:
21
+ return None
22
+
23
+ lower = raw.lower()
24
+
25
+ if not _looks_like_algebra(raw, lower):
26
+ return None
27
+
28
+ help_mode = _detect_help_mode(lower)
29
+ intent = _detect_intent(raw, lower)
30
+ cleaned = _normalize_text(raw)
31
+
32
+ if sp is None:
33
+ return _mk_result(
34
+ reply=_format_explanation_only(raw, lower, help_mode, intent),
35
+ solved=False,
36
+ help_mode=help_mode,
37
+ )
38
+
39
+ parsed = _parse_request(cleaned, lower)
40
+ explanation = _explain_what_is_being_asked(parsed, intent)
41
+
42
+ # Route by structure
43
+ result = (
44
+ _handle_systems(parsed, help_mode)
45
+ or _handle_inequality(parsed, help_mode)
46
+ or _handle_equation(parsed, help_mode)
47
+ or _handle_expression(parsed, help_mode, intent)
48
+ or _handle_word_translation(parsed, help_mode)
49
+ or _handle_degree_reasoning(parsed, help_mode)
50
+ or _handle_integer_restricted(parsed, help_mode)
51
+ )
52
+
53
+ if result is None:
54
+ reply = _join_sections(
55
+ "Let’s work through it.",
56
+ explanation,
57
+ _generic_algebra_guidance(parsed, help_mode, intent),
58
+ )
59
+ return _mk_result(reply=reply, solved=False, help_mode=help_mode)
60
+
61
+ # Prefix with decoded question meaning when useful
62
+ if explanation:
63
+ result.reply = _join_sections("Let’s work through it.", explanation, result.reply)
64
+ else:
65
+ result.reply = _join_sections("Let’s work through it.", result.reply)
66
+
67
+ return result
68
+
69
+
70
+ # =========================================================
71
+ # Recognition
72
+ # =========================================================
73
+
74
+ _ALGEBRA_KEYWORDS = [
75
+ "solve", "simplify", "factor", "factorise", "factorize", "expand",
76
+ "rearrange", "rewrite", "substitute", "expression", "equation",
77
+ "inequality", "quadratic", "linear", "polynomial", "root", "roots",
78
+ "simultaneous", "system", "identity", "identities", "degree",
79
+ "in terms of", "what is the value of", "which expression",
80
+ "equivalent expression", "collect like terms", "brackets",
81
+ ]
82
+
83
+ _WORD_MATH_SIGNALS = [
84
+ "more than", "less than", "twice", "three times", "sum of", "difference",
85
+ "product of", "quotient", "at least", "at most", "no more than",
86
+ "no less than", "consecutive", "integer", "positive integer",
87
+ "real number", "rational", "variable",
88
+ ]
89
+
90
+
91
+ def _looks_like_algebra(raw: str, lower: str) -> bool:
92
+ if any(k in lower for k in _ALGEBRA_KEYWORDS):
93
+ return True
94
+ if any(k in lower for k in _WORD_MATH_SIGNALS):
95
+ return True
96
+ if "=" in raw or "<" in raw or ">" in raw or "≤" in raw or "≥" in raw:
97
+ return True
98
+ if re.search(r"[a-zA-Z]", raw) and re.search(r"[\+\-\*/\^\(\)]", raw):
99
+ return True
100
+ if re.search(r"\b[a-zA-Z]\b", raw):
101
+ return True
102
+ return False
103
+
104
+
105
+ def _detect_help_mode(lower: str) -> str:
106
+ if any(x in lower for x in ["hint", "nudge", "small hint"]):
107
+ return "hint"
108
+ if any(x in lower for x in ["step by step", "steps", "walkthrough", "work through"]):
109
+ return "walkthrough"
110
+ if any(x in lower for x in ["explain", "what is this asking", "what does this mean", "decode"]):
111
+ return "explain"
112
+ if any(x in lower for x in ["method", "approach", "how do i solve", "how to solve"]):
113
+ return "method"
114
+ return "answer"
115
+
116
+
117
+ def _detect_intent(raw: str, lower: str) -> str:
118
+ if any(x in lower for x in ["simplify", "collect like terms", "reduce"]):
119
+ return "simplify"
120
+ if any(x in lower for x in ["expand", "multiply out", "open brackets"]):
121
+ return "expand"
122
+ if any(x in lower for x in ["factor", "factorise", "factorize"]):
123
+ return "factor"
124
+ if any(x in lower for x in ["rearrange", "write in terms of", "make", "isolate"]):
125
+ return "rearrange"
126
+ if any(x in lower for x in ["solve", "find x", "find y", "roots", "root"]):
127
+ return "solve"
128
+ if any(x in lower for x in ["which expression", "equivalent expression"]):
129
+ return "equivalent"
130
+ if any(x in lower for x in ["inequality", "at least", "at most", "greater than", "less than"]):
131
+ return "inequality"
132
+ if "=" in raw:
133
+ return "solve"
134
+ return "general"
135
+
136
+
137
+ # =========================================================
138
+ # Parsing / normalization
139
+ # =========================================================
140
+
141
+ def _normalize_text(raw: str) -> str:
142
+ text = raw.replace("×", "*").replace("÷", "/").replace("−", "-")
143
+ text = text.replace("≤", "<=").replace("≥", ">=")
144
+ text = text.replace("^", "**")
145
+ text = re.sub(r"\s+", " ", text).strip()
146
+ return text
147
+
148
+
149
+ def _parse_request(text: str, lower: str) -> Dict[str, Any]:
150
+ variables = sorted(set(re.findall(r"\b[a-zA-Z]\b", text)))
151
+ equations = _extract_equations(text)
152
+ inequalities = _extract_inequalities(text)
153
+ expressions = _extract_expressions(text, equations, inequalities)
154
+
155
+ return {
156
+ "raw": text,
157
+ "lower": lower,
158
+ "variables": variables,
159
+ "equations": equations,
160
+ "inequalities": inequalities,
161
+ "expressions": expressions,
162
+ "has_integer_constraint": bool(re.search(r"\binteger|positive integer|whole number|natural number\b", lower)),
163
+ "has_real_constraint": bool(re.search(r"\breal\b", lower)),
164
+ "has_nonzero_constraint": bool(re.search(r"\bnonzero|not zero\b", lower)),
165
+ "has_distinct_constraint": bool(re.search(r"\bdistinct\b", lower)),
166
+ "mentions_degree": "degree" in lower or "quadratic" in lower or "linear" in lower or "cubic" in lower,
167
+ }
168
+
169
+
170
+ def _extract_equations(text: str) -> List[str]:
171
+ parts = re.split(r"[;,]| and ", text)
172
+ eqs = []
173
+ for p in parts:
174
+ p = p.strip()
175
+ if p.count("=") == 1 and "<" not in p and ">" not in p:
176
+ eqs.append(p)
177
+ return eqs
178
+
179
+
180
+ def _extract_inequalities(text: str) -> List[str]:
181
+ parts = re.split(r"[;,]| and ", text)
182
+ out = []
183
+ for p in parts:
184
+ p = p.strip()
185
+ if any(op in p for op in ["<=", ">=", "<", ">"]):
186
+ out.append(p)
187
+ return out
188
+
189
+
190
+ def _extract_expressions(text: str, equations: List[str], inequalities: List[str]) -> List[str]:
191
+ t = text
192
+ for x in equations + inequalities:
193
+ t = t.replace(x, " ")
194
+ t = re.sub(
195
+ r"\b(solve|simplify|expand|factor|factorise|factorize|rearrange|rewrite|find|explain|what is|what's|which)\b",
196
+ " ",
197
+ t,
198
+ flags=re.I
199
+ )
200
+ t = re.sub(r"\s+", " ", t).strip()
201
+ return [t] if t else []
202
+
203
+
204
+ # =========================================================
205
+ # Sympy helpers
206
+ # =========================================================
207
+
208
+ def _sympify_expr(expr: str):
209
+ expr = expr.strip()
210
+ expr = re.sub(r"(?<=\d)(?=[a-zA-Z\(])", "*", expr)
211
+ expr = re.sub(r"(?<=[a-zA-Z\)])(?=\d)", "*", expr)
212
+ expr = re.sub(r"(?<=[a-zA-Z])(?=\()", "*", expr)
213
+ expr = re.sub(r"(?<=\))(?=[a-zA-Z])", "*", expr)
214
+ return sp.sympify(expr, evaluate=True)
215
+
216
+
217
+ def _sympify_equation(eq: str):
218
+ left, right = eq.split("=")
219
+ return sp.Eq(_sympify_expr(left), _sympify_expr(right))
220
+
221
+
222
+ def _sympify_inequality(ineq: str):
223
+ for op in ["<=", ">=", "<", ">"]:
224
+ if op in ineq:
225
+ left, right = ineq.split(op, 1)
226
+ l = _sympify_expr(left)
227
+ r = _sympify_expr(right)
228
+ if op == "<=":
229
+ return l <= r, op
230
+ if op == ">=":
231
+ return l >= r, op
232
+ if op == "<":
233
+ return l < r, op
234
+ if op == ">":
235
+ return l > r, op
236
+ return None, None
237
+
238
+
239
+ def _free_symbols_from_strings(items: List[str]) -> List[sp.Symbol]:
240
+ syms = set()
241
+ for item in items:
242
+ try:
243
+ obj = _sympify_equation(item) if "=" in item else _sympify_expr(item)
244
+ syms |= obj.free_symbols
245
+ except Exception:
246
+ pass
247
+ return sorted(list(syms), key=lambda s: s.name)
248
+
249
+
250
+ def _degree_of_expr(expr) -> Optional[int]:
251
+ try:
252
+ poly = sp.Poly(sp.expand(expr))
253
+ return poly.total_degree()
254
+ except Exception:
255
+ return None
256
+
257
+
258
+ # =========================================================
259
+ # Handlers
260
+ # =========================================================
261
+
262
+ def _handle_systems(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]:
263
+ eqs = parsed["equations"]
264
+ if len(eqs) < 2:
265
+ return None
266
+
267
+ try:
268
+ sym_eqs = [_sympify_equation(e) for e in eqs]
269
+ symbols = _free_symbols_from_strings(eqs)
270
+
271
+ if not symbols:
272
+ return None
273
+
274
+ lins = []
275
+ nonlinear = []
276
+ for eq in sym_eqs:
277
+ expr = sp.expand(eq.lhs - eq.rhs)
278
+ deg = _degree_of_expr(expr)
279
+ if deg == 1:
280
+ lins.append(eq)
281
+ else:
282
+ nonlinear.append(eq)
283
+
284
+ if nonlinear:
285
+ steps = [
286
+ "- Identify each equation and look for a substitution or elimination route.",
287
+ "- Check whether any equation can isolate one variable cleanly.",
288
+ "- Substitute that expression into the others to reduce the system."
289
+ ]
290
+ if parsed["has_integer_constraint"]:
291
+ steps.append("- Because there is an integer restriction, candidate values can also be checked efficiently.")
292
+ return _mk_result(
293
+ reply=_modeled_steps(
294
+ title="This is a system of equations.",
295
+ method="Use substitution or elimination to reduce the number of variables.",
296
+ steps=steps,
297
+ help_mode=help_mode,
298
+ ),
299
+ solved=True,
300
+ help_mode=help_mode,
301
+ )
302
+
303
+ # Linear system classification
304
+ matrix, vec = sp.linear_eq_to_matrix([eq.lhs - eq.rhs for eq in sym_eqs], symbols)
305
+ rank_a = matrix.rank()
306
+ rank_aug = matrix.row_join(vec).rank()
307
+ nvars = len(symbols)
308
+
309
+ if rank_a != rank_aug:
310
+ msg = [
311
+ "This system is inconsistent.",
312
+ "That means the equations conflict with each other, so there is no common solution.",
313
+ "A good first move is to eliminate one variable and compare the resulting statements."
314
+ ]
315
+ return _mk_result(reply="\n\n".join(msg), solved=True, help_mode=help_mode)
316
+
317
+ if rank_a < nvars:
318
+ msg = [
319
+ "This system does not pin down a unique solution.",
320
+ "That means there are infinitely many solutions or at least one free variable.",
321
+ "On GMAT-style questions, this often means you should solve for a relationship instead of individual values."
322
+ ]
323
+ return _mk_result(reply="\n\n".join(msg), solved=True, help_mode=help_mode)
324
+
325
+ steps = [
326
+ "- Choose one variable to eliminate.",
327
+ "- Make the coefficients match, then subtract or add the equations.",
328
+ "- Solve the reduced one-variable equation.",
329
+ "- Substitute back into one original equation.",
330
+ "- Check the pair against the remaining equation(s)."
331
+ ]
332
+ return _mk_result(
333
+ reply=_modeled_steps(
334
+ title="This is a linear system with a unique solution structure.",
335
+ method="Elimination is usually the cleanest route unless one equation already isolates a variable.",
336
+ steps=steps,
337
+ help_mode=help_mode,
338
+ ),
339
+ solved=True,
340
+ help_mode=help_mode,
341
+ )
342
+ except Exception:
343
+ return None
344
+
345
+
346
+ def _handle_inequality(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]:
347
+ ineqs = parsed["inequalities"]
348
+ if not ineqs:
349
+ return None
350
+
351
+ try:
352
+ first = ineqs[0]
353
+ rel, op = _sympify_inequality(first)
354
+ if rel is None:
355
+ return None
356
+
357
+ syms = sorted(list(rel.free_symbols), key=lambda s: s.name)
358
+ if len(syms) != 1:
359
+ return _mk_result(
360
+ reply=_modeled_steps(
361
+ title="This is an inequality problem.",
362
+ method="Rearrange so one side becomes 0, then analyze sign changes or isolate the variable.",
363
+ steps=[
364
+ "- Collect all terms on one side.",
365
+ "- Factor if possible.",
366
+ "- Mark critical points where the expression is 0 or undefined.",
367
+ "- Test intervals to see where the inequality is true.",
368
+ "- Remember: multiplying or dividing by a negative flips the inequality sign."
369
+ ],
370
+ help_mode=help_mode,
371
+ ),
372
+ solved=True,
373
+ help_mode=help_mode,
374
+ )
375
+
376
+ var = syms[0]
377
+ steps = [
378
+ f"- Isolate {var} as much as possible.",
379
+ "- Be careful with brackets, fractions, and negative coefficients.",
380
+ "- If you multiply or divide by a negative quantity, reverse the inequality sign.",
381
+ "- If the expression factors, use sign analysis instead of treating it like a normal equation."
382
+ ]
383
+ return _mk_result(
384
+ reply=_modeled_steps(
385
+ title="This is a one-variable inequality.",
386
+ method="Solve it like an equation, but track sign changes carefully.",
387
+ steps=steps,
388
+ help_mode=help_mode,
389
+ ),
390
+ solved=True,
391
+ help_mode=help_mode,
392
+ )
393
+ except Exception:
394
+ return None
395
+
396
+
397
+ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]:
398
+ eqs = parsed["equations"]
399
+ if len(eqs) != 1:
400
+ return None
401
+
402
+ try:
403
+ eq = _sympify_equation(eqs[0])
404
+ expr = sp.expand(eq.lhs - eq.rhs)
405
+ syms = sorted(list(expr.free_symbols), key=lambda s: s.name)
406
+ deg = _degree_of_expr(expr)
407
+
408
+ if not syms:
409
+ return _mk_result(
410
+ reply="This expression has no variable left after simplification, so the key question is whether the statement is always true, never true, or just a numeric identity.",
411
+ solved=True,
412
+ help_mode=help_mode,
413
+ )
414
+
415
+ if len(syms) > 1 and deg == 1:
416
+ steps = [
417
+ "- One equation with multiple variables usually does not determine each variable uniquely.",
418
+ "- Rearrange to express one variable in terms of the others.",
419
+ "- If the question asks for a combination like x+y, look for a way to isolate that combination directly."
420
+ ]
421
+ return _mk_result(
422
+ reply=_modeled_steps(
423
+ title="This is a linear equation in more than one variable.",
424
+ method="Do not assume you can find unique values for every variable from a single equation.",
425
+ steps=steps,
426
+ help_mode=help_mode,
427
+ ),
428
+ solved=True,
429
+ help_mode=help_mode,
430
+ )
431
+
432
+ if deg == 1:
433
+ var = syms[0]
434
+ steps = [
435
+ "- Expand brackets if needed.",
436
+ "- Clear fractions or decimals if that makes the equation cleaner.",
437
+ f"- Collect all {var} terms on one side and constants on the other.",
438
+ f"- Factor out {var} if needed, then isolate it."
439
+ ]
440
+ if re.search(r"/[a-zA-Z]|\b[a-zA-Z]\s*/", parsed["raw"]):
441
+ steps.append("- Be careful: dividing by a variable assumes that variable is not 0.")
442
+ return _mk_result(
443
+ reply=_modeled_steps(
444
+ title="This is a linear equation.",
445
+ method="Use inverse operations and keep both sides balanced.",
446
+ steps=steps,
447
+ help_mode=help_mode,
448
+ ),
449
+ solved=True,
450
+ help_mode=help_mode,
451
+ )
452
+
453
+ if deg == 2:
454
+ var = syms[0]
455
+ disc = None
456
+ try:
457
+ poly = sp.Poly(expr, var)
458
+ a, b, c = poly.all_coeffs()
459
+ disc = sp.expand(b**2 - 4*a*c)
460
+ except Exception:
461
+ pass
462
+
463
+ steps = [
464
+ "- Rearrange into standard quadratic form.",
465
+ "- Check whether it factors neatly.",
466
+ "- If it does not factor cleanly, use a systematic method such as the quadratic formula or completing the square.",
467
+ "- After finding candidate roots internally, substitute back to verify."
468
+ ]
469
+
470
+ if disc is not None:
471
+ steps.append("- The discriminant tells you whether there are two, one, or no real roots.")
472
+
473
+ return _mk_result(
474
+ reply=_modeled_steps(
475
+ title="This is a quadratic equation.",
476
+ method="First look for factorization; otherwise move to a general solving method.",
477
+ steps=steps,
478
+ help_mode=help_mode,
479
+ ),
480
+ solved=True,
481
+ help_mode=help_mode,
482
+ )
483
+
484
+ if deg and deg > 2:
485
+ factored = sp.factor(expr)
486
+ steps = [
487
+ "- Look for a common factor first.",
488
+ "- Check for algebraic identities such as difference of squares or grouping patterns.",
489
+ "- See whether a substitution can reduce the degree, for example letting u = x^2 or u = x^3.",
490
+ "- Once reduced, solve the lower-degree equation and then translate back."
491
+ ]
492
+ if factored != expr:
493
+ steps.append("- This one appears factorable, so the zero-product idea is likely useful.")
494
+ if parsed["has_integer_constraint"]:
495
+ steps.append("- Since the variables may be restricted to integers, candidate checking can also be efficient.")
496
+ return _mk_result(
497
+ reply=_modeled_steps(
498
+ title="This is a higher-degree algebra equation.",
499
+ method="Reduce it by factorization or substitution before trying to solve.",
500
+ steps=steps,
501
+ help_mode=help_mode,
502
+ ),
503
+ solved=True,
504
+ help_mode=help_mode,
505
+ )
506
+
507
+ except Exception:
508
+ return None
509
+
510
+ return None
511
+
512
+
513
+ def _handle_expression(parsed: Dict[str, Any], help_mode: str, intent: str) -> Optional[SolverResult]:
514
+ exprs = parsed["expressions"]
515
+ if not exprs:
516
+ return None
517
+
518
+ expr_text = exprs[0].strip()
519
+ if not expr_text:
520
+ return None
521
+
522
+ try:
523
+ expr = _sympify_expr(expr_text)
524
+
525
+ if intent == "simplify":
526
+ return _mk_result(
527
+ reply=_modeled_steps(
528
+ title="This is a simplification task.",
529
+ method="Combine like terms, reduce fractions carefully, and use identities where helpful.",
530
+ steps=[
531
+ "- Expand only if that helps combine terms.",
532
+ "- Collect like powers and like variable terms.",
533
+ "- Factor common pieces if the expression becomes cleaner that way.",
534
+ "- Check for hidden identities such as a^2-b^2 or perfect squares."
535
+ ],
536
+ help_mode=help_mode,
537
+ ),
538
+ solved=True,
539
+ help_mode=help_mode,
540
+ )
541
+
542
+ if intent == "expand":
543
+ return _mk_result(
544
+ reply=_modeled_steps(
545
+ title="This is an expansion task.",
546
+ method="Distribute carefully across every term in the bracket(s).",
547
+ steps=[
548
+ "- Multiply each outside factor by each inside term.",
549
+ "- Watch negative signs.",
550
+ "- Combine like terms at the end."
551
+ ],
552
+ help_mode=help_mode,
553
+ ),
554
+ solved=True,
555
+ help_mode=help_mode,
556
+ )
557
+
558
+ if intent == "factor":
559
+ return _mk_result(
560
+ reply=_modeled_steps(
561
+ title="This is a factorization task.",
562
+ method="Start by pulling out any common factor, then check special identities and quadratic patterns.",
563
+ steps=[
564
+ "- Take out the greatest common factor first.",
565
+ "- Check for difference of squares.",
566
+ "- Check for perfect-square trinomials.",
567
+ "- If it is quadratic in form, use sum/product structure."
568
+ ],
569
+ help_mode=help_mode,
570
+ ),
571
+ solved=True,
572
+ help_mode=help_mode,
573
+ )
574
+
575
+ if intent == "rearrange":
576
+ return _mk_result(
577
+ reply=_modeled_steps(
578
+ title="This is a rearranging / isolating task.",
579
+ method="Move all terms involving the target variable together, then factor it out.",
580
+ steps=[
581
+ "- Identify which variable must be isolated.",
582
+ "- Move all target-variable terms to one side.",
583
+ "- Move all non-target terms to the other side.",
584
+ "- Factor the target variable if it appears in multiple terms.",
585
+ "- Divide only when you know the divisor is allowed to be nonzero."
586
+ ],
587
+ help_mode=help_mode,
588
+ ),
589
+ solved=True,
590
+ help_mode=help_mode,
591
+ )
592
+
593
+ # Generic expression handling
594
+ deg = _degree_of_expr(expr)
595
+ steps = [
596
+ "- Decide whether the best move is simplify, expand, factor, or substitute.",
597
+ "- Look for common factors and algebraic identities.",
598
+ "- Watch for domain restrictions if variables appear in denominators or radicals."
599
+ ]
600
+ if deg is not None:
601
+ steps.append(f"- The expression behaves like degree {deg}, which can guide which identities are likely useful.")
602
+
603
+ return _mk_result(
604
+ reply=_modeled_steps(
605
+ title="This is an algebraic expression task.",
606
+ method="Classify the structure first, then use the matching algebra tool.",
607
+ steps=steps,
608
+ help_mode=help_mode,
609
+ ),
610
+ solved=True,
611
+ help_mode=help_mode,
612
+ )
613
+ except Exception:
614
+ return None
615
+
616
+
617
+ def _handle_word_translation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]:
618
+ lower = parsed["lower"]
619
+ raw = parsed["raw"]
620
+
621
+ triggers = [
622
+ "more than", "less than", "twice", "times", "sum of", "difference",
623
+ "product of", "quotient", "consecutive", "integer", "age", "number"
624
+ ]
625
+ if not any(t in lower for t in triggers):
626
+ return None
627
+
628
+ mappings = [
629
+ ("more than", "be careful with order: '10 more than x' means x + 10"),
630
+ ("less than", "be careful with order: '3 less than x' means x - 3, but '3 less than a number' means number - 3"),
631
+ ("twice", "'twice x' means 2x"),
632
+ ("sum of", "'sum of a and b' means a + b"),
633
+ ("difference", "'difference of a and b' means a - b"),
634
+ ("product of", "'product of a and b' means ab"),
635
+ ("quotient", "'quotient of a and b' means a / b"),
636
+ ("at least", "this signals >= "),
637
+ ("at most", "this signals <= "),
638
+ ("no more than", "this signals <= "),
639
+ ("no less than", "this signals >= "),
640
+ ("consecutive", "use n, n+1, n+2 ..."),
641
+ ]
642
+
643
+ bullets = []
644
+ for k, v in mappings:
645
+ if k in lower:
646
+ bullets.append(f"- {v}")
647
+
648
+ if not bullets:
649
+ bullets = [
650
+ "- Translate the wording into variables first.",
651
+ "- Build the equation or inequality before trying to solve."
652
+ ]
653
+
654
+ return _mk_result(
655
+ reply=_modeled_steps(
656
+ title="This is an algebra-from-words problem.",
657
+ method="First translate the English into algebraic structure, then solve that structure.",
658
+ steps=bullets,
659
+ help_mode=help_mode,
660
+ ),
661
+ solved=True,
662
+ help_mode=help_mode,
663
+ )
664
+
665
+
666
+ def _handle_degree_reasoning(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]:
667
+ if not parsed["mentions_degree"]:
668
+ return None
669
+
670
+ return _mk_result(
671
+ reply=_modeled_steps(
672
+ title="This question is using degree / polynomial structure.",
673
+ method="The degree tells you the highest power present and helps narrow the right solving method.",
674
+ steps=[
675
+ "- Degree 1 suggests a linear structure.",
676
+ "- Degree 2 suggests a quadratic structure.",
677
+ "- Higher degree often calls for factorization, substitution, or identity spotting.",
678
+ "- A polynomial of degree n can have at most n roots over the reals/complexes combined."
679
+ ],
680
+ help_mode=help_mode,
681
+ ),
682
+ solved=True,
683
+ help_mode=help_mode,
684
+ )
685
+
686
+
687
+ def _handle_integer_restricted(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]:
688
+ if not parsed["has_integer_constraint"]:
689
+ return None
690
+ if not parsed["equations"] and not parsed["inequalities"]:
691
+ return None
692
+
693
+ return _mk_result(
694
+ reply=_modeled_steps(
695
+ title="This problem has an integer restriction.",
696
+ method="That often makes controlled testing or divisibility reasoning much faster than pure symbolic solving.",
697
+ steps=[
698
+ "- Use the algebra to narrow the possible forms first.",
699
+ "- Then test only values consistent with the restriction.",
700
+ "- Stop when the restriction makes further values impossible.",
701
+ "- Always check the tested value in the original condition."
702
+ ],
703
+ help_mode=help_mode,
704
+ ),
705
+ solved=True,
706
+ help_mode=help_mode,
707
+ )
708
+
709
+
710
+ # =========================================================
711
+ # Explanation logic
712
+ # =========================================================
713
+
714
+ def _explain_what_is_being_asked(parsed: Dict[str, Any], intent: str) -> str:
715
+ if intent == "solve" and parsed["equations"]:
716
+ return "What the question is asking: find the value(s) of the variable that make the equation true."
717
+ if intent == "simplify":
718
+ return "What the question is asking: rewrite the expression into a cleaner equivalent form."
719
+ if intent == "expand":
720
+ return "What the question is asking: multiply out the brackets so the expression is written term-by-term."
721
+ if intent == "factor":
722
+ return "What the question is asking: rewrite the expression as a product of simpler factors."
723
+ if intent == "rearrange":
724
+ return "What the question is asking: isolate one variable or rewrite the formula in a requested form."
725
+ if intent == "inequality" or parsed["inequalities"]:
726
+ return "What the question is asking: find which value(s) make the inequality true, not just where two sides are equal."
727
+ if len(parsed["equations"]) >= 2:
728
+ return "What the question is asking: find values that satisfy all equations at the same time."
729
+ return ""
730
+
731
+
732
+ def _generic_algebra_guidance(parsed: Dict[str, Any], help_mode: str, intent: str) -> str:
733
+ steps = [
734
+ "- First classify the task: simplify, expand, factor, solve, rearrange, or compare.",
735
+ "- Then choose the matching algebra move instead of manipulating blindly.",
736
+ "- Keep track of hidden restrictions such as denominators not being zero."
737
+ ]
738
+ return _modeled_steps(
739
+ title="This is an algebra problem.",
740
+ method="The key is to identify the structure before choosing a method.",
741
+ steps=steps,
742
+ help_mode=help_mode,
743
+ )
744
+
745
+
746
+ def _format_explanation_only(raw: str, lower: str, help_mode: str, intent: str) -> str:
747
+ return _join_sections(
748
+ "Let’s work through it.",
749
+ "I can identify this as an algebra problem, but the symbolic engine is unavailable in this environment.",
750
+ _generic_algebra_guidance(
751
+ {"equations": [], "inequalities": [], "raw": raw},
752
+ help_mode,
753
+ intent,
754
+ ),
755
+ )
756
+
757
+
758
+ # =========================================================
759
+ # Output shaping
760
+ # =========================================================
761
+
762
+ def _modeled_steps(title: str, method: str, steps: List[str], help_mode: str) -> str:
763
+ if help_mode == "hint":
764
+ return _join_sections(
765
+ title,
766
+ f"Hint: {method}",
767
+ steps[0] if steps else ""
768
+ )
769
+
770
+ if help_mode == "explain":
771
+ return _join_sections(
772
+ title,
773
+ f"Method idea: {method}",
774
+ "\n".join(steps[:3])
775
+ )
776
+
777
+ if help_mode == "method":
778
+ return _join_sections(
779
+ title,
780
+ f"Method: {method}",
781
+ "\n".join(steps)
782
+ )
783
+
784
+ # walkthrough / answer
785
+ return _join_sections(
786
+ title,
787
+ f"Walkthrough method: {method}",
788
+ "\n".join(steps)
789
+ )
790
+
791
+
792
+ def _join_sections(*parts: str) -> str:
793
+ clean = [p.strip() for p in parts if p and p.strip()]
794
+ return "\n\n".join(clean)
795
+
796
+
797
+ def _mk_result(reply: str, solved: bool, help_mode: str) -> SolverResult:
798
+ return SolverResult(
799
+ reply=reply,
800
+ meta={
801
+ "domain": "quant",
802
+ "solved": solved,
803
+ "help_mode": help_mode,
804
+ "answer_letter": None,
805
+ "answer_value": None,
806
+ "topic": "algebra",
807
+ "used_retrieval": False,
808
+ "used_generator": False,
809
+ },
810
+ )