j-js commited on
Commit
a064a65
·
verified ·
1 Parent(s): 193b73f

Update solver_algebra.py

Browse files
Files changed (1) hide show
  1. solver_algebra.py +326 -101
solver_algebra.py CHANGED
@@ -34,12 +34,13 @@ def solve_algebra(text: str) -> Optional[SolverResult]:
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)
@@ -56,9 +57,14 @@ def solve_algebra(text: str) -> Optional[SolverResult]:
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:
@@ -103,7 +109,7 @@ def _looks_like_algebra(raw: str, lower: str) -> bool:
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"
@@ -255,6 +261,125 @@ def _degree_of_expr(expr) -> Optional[int]:
255
  return None
256
 
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  # =========================================================
259
  # Handlers
260
  # =========================================================
@@ -271,14 +396,11 @@ def _handle_systems(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverRe
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:
@@ -298,9 +420,10 @@ def _handle_systems(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverRe
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()
@@ -308,19 +431,41 @@ def _handle_systems(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverRe
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.",
@@ -338,6 +483,8 @@ def _handle_systems(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverRe
338
  ),
339
  solved=True,
340
  help_mode=help_mode,
 
 
341
  )
342
  except Exception:
343
  return None
@@ -356,26 +503,29 @@ def _handle_inequality(parsed: Dict[str, Any], help_mode: str) -> Optional[Solve
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."
@@ -389,6 +539,8 @@ def _handle_inequality(parsed: Dict[str, Any], help_mode: str) -> Optional[Solve
389
  ),
390
  solved=True,
391
  help_mode=help_mode,
 
 
392
  )
393
  except Exception:
394
  return None
@@ -406,10 +558,21 @@ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverR
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:
@@ -427,27 +590,26 @@ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverR
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:
@@ -455,8 +617,10 @@ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverR
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
 
@@ -466,7 +630,6 @@ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverR
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
 
@@ -479,6 +642,8 @@ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverR
479
  ),
480
  solved=True,
481
  help_mode=help_mode,
 
 
482
  )
483
 
484
  if deg and deg > 2:
@@ -493,6 +658,7 @@ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverR
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.",
@@ -502,6 +668,8 @@ def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverR
502
  ),
503
  solved=True,
504
  help_mode=help_mode,
 
 
505
  )
506
 
507
  except Exception:
@@ -523,74 +691,85 @@ def _handle_expression(parsed: Dict[str, Any], help_mode: str, intent: str) -> O
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.",
@@ -609,6 +788,8 @@ def _handle_expression(parsed: Dict[str, Any], help_mode: str, intent: str) -> O
609
  ),
610
  solved=True,
611
  help_mode=help_mode,
 
 
612
  )
613
  except Exception:
614
  return None
@@ -616,7 +797,6 @@ def _handle_expression(parsed: Dict[str, Any], help_mode: str, intent: str) -> O
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",
@@ -626,18 +806,18 @@ def _handle_word_translation(parsed: Dict[str, Any], help_mode: str) -> Optional
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 = []
@@ -660,6 +840,8 @@ def _handle_word_translation(parsed: Dict[str, Any], help_mode: str) -> Optional
660
  ),
661
  solved=True,
662
  help_mode=help_mode,
 
 
663
  )
664
 
665
 
@@ -667,20 +849,23 @@ def _handle_degree_reasoning(parsed: Dict[str, Any], help_mode: str) -> Optional
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
 
@@ -690,20 +875,23 @@ def _handle_integer_restricted(parsed: Dict[str, Any], help_mode: str) -> Option
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
 
@@ -760,32 +948,33 @@ def _format_explanation_only(raw: str, lower: str, help_mode: str, intent: str)
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
 
@@ -794,8 +983,18 @@ def _join_sections(*parts: str) -> str:
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",
@@ -806,5 +1005,31 @@ def _mk_result(reply: str, solved: bool, help_mode: str) -> SolverResult:
806
  "topic": "algebra",
807
  "used_retrieval": False,
808
  "used_generator": False,
 
 
 
809
  },
810
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  reply=_format_explanation_only(raw, lower, help_mode, intent),
35
  solved=False,
36
  help_mode=help_mode,
37
+ steps=[],
38
+ display_steps=[],
39
  )
40
 
41
  parsed = _parse_request(cleaned, lower)
42
  explanation = _explain_what_is_being_asked(parsed, intent)
43
 
 
44
  result = (
45
  _handle_systems(parsed, help_mode)
46
  or _handle_inequality(parsed, help_mode)
 
57
  explanation,
58
  _generic_algebra_guidance(parsed, help_mode, intent),
59
  )
60
+ return _mk_result(
61
+ reply=reply,
62
+ solved=False,
63
+ help_mode=help_mode,
64
+ steps=[],
65
+ display_steps=[],
66
+ )
67
 
 
68
  if explanation:
69
  result.reply = _join_sections("Let’s work through it.", explanation, result.reply)
70
  else:
 
109
 
110
 
111
  def _detect_help_mode(lower: str) -> str:
112
+ if any(x in lower for x in ["hint", "nudge", "small hint", "next hint"]):
113
  return "hint"
114
  if any(x in lower for x in ["step by step", "steps", "walkthrough", "work through"]):
115
  return "walkthrough"
 
261
  return None
262
 
263
 
264
+ def _safe_str(obj: Any) -> str:
265
+ try:
266
+ return str(sp.simplify(obj))
267
+ except Exception:
268
+ return str(obj)
269
+
270
+
271
+ def _format_eq(lhs: Any, rhs: Any) -> str:
272
+ return f"{_safe_str(lhs)} = {_safe_str(rhs)}"
273
+
274
+
275
+ def _strip_bullet_prefix(step: str) -> str:
276
+ return re.sub(r"^\s*-\s*", "", (step or "").strip())
277
+
278
+
279
+ def _ensure_bullets(steps: List[str]) -> List[str]:
280
+ out = []
281
+ for s in steps:
282
+ s = _strip_bullet_prefix(s)
283
+ if s:
284
+ out.append(f"- {s}")
285
+ return out
286
+
287
+
288
+ # =========================================================
289
+ # Step builders
290
+ # =========================================================
291
+
292
+ def _build_linear_equation_steps(eq, var) -> Tuple[List[str], List[str], Optional[Any]]:
293
+ """
294
+ Returns:
295
+ all_steps: full internal steps including final solved step if available
296
+ display_steps: safe steps for walkthrough/method/answer without answer leakage
297
+ final_value: solved value if found
298
+ """
299
+ lhs = sp.simplify(eq.lhs)
300
+ rhs = sp.simplify(eq.rhs)
301
+
302
+ start_step = f"Start with {_format_eq(lhs, rhs)}."
303
+
304
+ try:
305
+ solutions = sp.solve(eq, var)
306
+ except Exception:
307
+ solutions = []
308
+
309
+ final_value = solutions[0] if len(solutions) == 1 else None
310
+
311
+ expr = sp.expand(lhs - rhs)
312
+ coeff = sp.expand(expr).coeff(var, 1)
313
+ const = sp.expand(expr).subs(var, 0)
314
+
315
+ all_steps: List[str] = [start_step]
316
+ display_steps: List[str] = [start_step]
317
+
318
+ # Pattern 1: x / c = k
319
+ if lhs == var / sp.denom(lhs) and rhs.is_number:
320
+ denom = sp.denom(lhs)
321
+ step2_lhs = sp.simplify(lhs * denom)
322
+ step2_rhs = sp.simplify(rhs * denom)
323
+ step2 = f"Multiply both sides by {_safe_str(denom)} to undo the division."
324
+ step3 = f"This gives {_format_eq(step2_lhs, step2_rhs)}."
325
+ all_steps.extend([step2, step3])
326
+ display_steps.extend([step2, step3])
327
+ if final_value is not None:
328
+ all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.")
329
+ return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value
330
+
331
+ # Pattern 2: a*x = b
332
+ try:
333
+ ratio = sp.simplify(lhs / var)
334
+ if lhs == sp.simplify(ratio * var) and ratio.is_number and ratio != 1 and rhs.is_number:
335
+ step2 = f"Divide both sides by {_safe_str(ratio)} to isolate {_safe_str(var)}."
336
+ step3 = f"This leaves {_safe_str(var)} by itself on the left."
337
+ all_steps.extend([step2, step3])
338
+ display_steps.extend([step2, step3])
339
+ if final_value is not None:
340
+ all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.")
341
+ return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value
342
+ except Exception:
343
+ pass
344
+
345
+ # Pattern 3: x + b = c
346
+ try:
347
+ if sp.expand(lhs).coeff(var, 1) == 1 and rhs.is_number:
348
+ extra = sp.simplify(lhs - var)
349
+ if extra.free_symbols == set():
350
+ if extra != 0:
351
+ direction = "subtract" if extra > 0 else "add"
352
+ amount = abs(extra)
353
+ step2 = f"{direction.capitalize()} {_safe_str(amount)} on both sides to undo the constant term."
354
+ step3 = f"That isolates {_safe_str(var)} on the left."
355
+ all_steps.extend([step2, step3])
356
+ display_steps.extend([step2, step3])
357
+ if final_value is not None:
358
+ all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.")
359
+ return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value
360
+ except Exception:
361
+ pass
362
+
363
+ # General linear fallback
364
+ if coeff != 0:
365
+ if const != 0:
366
+ step2 = "Rearrange so the variable term is isolated."
367
+ step3 = "Undo the constant term using the inverse operation on both sides."
368
+ display_steps.extend([step2, step3])
369
+ all_steps.extend([step2, step3])
370
+ if coeff != 1:
371
+ step4 = f"Then divide by the coefficient of {_safe_str(var)} to isolate the variable."
372
+ display_steps.append(step4)
373
+ all_steps.append(step4)
374
+ if final_value is not None:
375
+ all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.")
376
+ else:
377
+ all_steps.append("There is no variable term left after simplification, so check whether the equation is always true or impossible.")
378
+ display_steps.append("After simplification, check whether the statement is always true or impossible.")
379
+
380
+ return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value
381
+
382
+
383
  # =========================================================
384
  # Handlers
385
  # =========================================================
 
396
  if not symbols:
397
  return None
398
 
 
399
  nonlinear = []
400
  for eq in sym_eqs:
401
  expr = sp.expand(eq.lhs - eq.rhs)
402
  deg = _degree_of_expr(expr)
403
+ if deg != 1:
 
 
404
  nonlinear.append(eq)
405
 
406
  if nonlinear:
 
420
  ),
421
  solved=True,
422
  help_mode=help_mode,
423
+ steps=steps,
424
+ display_steps=steps,
425
  )
426
 
 
427
  matrix, vec = sp.linear_eq_to_matrix([eq.lhs - eq.rhs for eq in sym_eqs], symbols)
428
  rank_a = matrix.rank()
429
  rank_aug = matrix.row_join(vec).rank()
 
431
 
432
  if rank_a != rank_aug:
433
  msg = [
434
+ "- This system is inconsistent.",
435
+ "- That means the equations conflict with each other, so there is no common solution.",
436
+ "- A good first move is to eliminate one variable and compare the resulting statements."
437
  ]
438
+ return _mk_result(
439
+ reply=_modeled_steps(
440
+ title="This system is inconsistent.",
441
+ method="Eliminate a variable and compare the resulting statements.",
442
+ steps=msg,
443
+ help_mode=help_mode,
444
+ ),
445
+ solved=True,
446
+ help_mode=help_mode,
447
+ steps=msg,
448
+ display_steps=msg,
449
+ )
450
 
451
  if rank_a < nvars:
452
  msg = [
453
+ "- This system does not pin down a unique solution.",
454
+ "- That means there are infinitely many solutions or at least one free variable.",
455
+ "- On GMAT-style questions, this often means you should solve for a relationship instead of individual values."
456
  ]
457
+ return _mk_result(
458
+ reply=_modeled_steps(
459
+ title="This system does not have a unique solution.",
460
+ method="Look for a relationship rather than separate fixed values.",
461
+ steps=msg,
462
+ help_mode=help_mode,
463
+ ),
464
+ solved=True,
465
+ help_mode=help_mode,
466
+ steps=msg,
467
+ display_steps=msg,
468
+ )
469
 
470
  steps = [
471
  "- Choose one variable to eliminate.",
 
483
  ),
484
  solved=True,
485
  help_mode=help_mode,
486
+ steps=steps,
487
+ display_steps=steps,
488
  )
489
  except Exception:
490
  return None
 
503
 
504
  syms = sorted(list(rel.free_symbols), key=lambda s: s.name)
505
  if len(syms) != 1:
506
+ steps = [
507
+ "- Collect all terms on one side.",
508
+ "- Factor if possible.",
509
+ "- Mark critical points where the expression is 0 or undefined.",
510
+ "- Test intervals to see where the inequality is true.",
511
+ "- Remember: multiplying or dividing by a negative flips the inequality sign."
512
+ ]
513
  return _mk_result(
514
  reply=_modeled_steps(
515
  title="This is an inequality problem.",
516
  method="Rearrange so one side becomes 0, then analyze sign changes or isolate the variable.",
517
+ steps=steps,
 
 
 
 
 
 
518
  help_mode=help_mode,
519
  ),
520
  solved=True,
521
  help_mode=help_mode,
522
+ steps=steps,
523
+ display_steps=steps,
524
  )
525
 
526
  var = syms[0]
527
  steps = [
528
+ f"- Isolate {_safe_str(var)} as much as possible.",
529
  "- Be careful with brackets, fractions, and negative coefficients.",
530
  "- If you multiply or divide by a negative quantity, reverse the inequality sign.",
531
  "- If the expression factors, use sign analysis instead of treating it like a normal equation."
 
539
  ),
540
  solved=True,
541
  help_mode=help_mode,
542
+ steps=steps,
543
+ display_steps=steps,
544
  )
545
  except Exception:
546
  return None
 
558
  deg = _degree_of_expr(expr)
559
 
560
  if not syms:
561
+ steps = [
562
+ "- Simplify both sides fully.",
563
+ "- Then decide whether the statement is always true, never true, or just a numeric identity."
564
+ ]
565
  return _mk_result(
566
+ reply=_modeled_steps(
567
+ title="No variable remains after simplification.",
568
+ method="Check the resulting statement itself.",
569
+ steps=steps,
570
+ help_mode=help_mode,
571
+ ),
572
  solved=True,
573
  help_mode=help_mode,
574
+ steps=steps,
575
+ display_steps=steps,
576
  )
577
 
578
  if len(syms) > 1 and deg == 1:
 
590
  ),
591
  solved=True,
592
  help_mode=help_mode,
593
+ steps=steps,
594
+ display_steps=steps,
595
  )
596
 
597
  if deg == 1:
598
  var = syms[0]
599
+ all_steps, display_steps, final_value = _build_linear_equation_steps(eq, var)
600
+ reply = _modeled_steps(
601
+ title="This is a linear equation.",
602
+ method="Use inverse operations and keep both sides balanced.",
603
+ steps=display_steps,
604
+ help_mode=help_mode,
605
+ )
 
606
  return _mk_result(
607
+ reply=reply,
 
 
 
 
 
608
  solved=True,
609
  help_mode=help_mode,
610
+ steps=all_steps,
611
+ display_steps=display_steps,
612
+ final_value=final_value,
613
  )
614
 
615
  if deg == 2:
 
617
  disc = None
618
  try:
619
  poly = sp.Poly(expr, var)
620
+ coeffs = poly.all_coeffs()
621
+ if len(coeffs) == 3:
622
+ a, b, c = coeffs
623
+ disc = sp.expand(b**2 - 4 * a * c)
624
  except Exception:
625
  pass
626
 
 
630
  "- If it does not factor cleanly, use a systematic method such as the quadratic formula or completing the square.",
631
  "- After finding candidate roots internally, substitute back to verify."
632
  ]
 
633
  if disc is not None:
634
  steps.append("- The discriminant tells you whether there are two, one, or no real roots.")
635
 
 
642
  ),
643
  solved=True,
644
  help_mode=help_mode,
645
+ steps=steps,
646
+ display_steps=steps,
647
  )
648
 
649
  if deg and deg > 2:
 
658
  steps.append("- This one appears factorable, so the zero-product idea is likely useful.")
659
  if parsed["has_integer_constraint"]:
660
  steps.append("- Since the variables may be restricted to integers, candidate checking can also be efficient.")
661
+
662
  return _mk_result(
663
  reply=_modeled_steps(
664
  title="This is a higher-degree algebra equation.",
 
668
  ),
669
  solved=True,
670
  help_mode=help_mode,
671
+ steps=steps,
672
+ display_steps=steps,
673
  )
674
 
675
  except Exception:
 
691
  expr = _sympify_expr(expr_text)
692
 
693
  if intent == "simplify":
694
+ steps = [
695
+ "- Expand only if that helps combine terms.",
696
+ "- Collect like powers and like variable terms.",
697
+ "- Factor common pieces if the expression becomes cleaner that way.",
698
+ "- Check for hidden identities such as a^2-b^2 or perfect squares."
699
+ ]
700
  return _mk_result(
701
  reply=_modeled_steps(
702
  title="This is a simplification task.",
703
  method="Combine like terms, reduce fractions carefully, and use identities where helpful.",
704
+ steps=steps,
 
 
 
 
 
705
  help_mode=help_mode,
706
  ),
707
  solved=True,
708
  help_mode=help_mode,
709
+ steps=steps,
710
+ display_steps=steps,
711
  )
712
 
713
  if intent == "expand":
714
+ steps = [
715
+ "- Multiply each outside factor by each inside term.",
716
+ "- Watch negative signs.",
717
+ "- Combine like terms at the end."
718
+ ]
719
  return _mk_result(
720
  reply=_modeled_steps(
721
  title="This is an expansion task.",
722
  method="Distribute carefully across every term in the bracket(s).",
723
+ steps=steps,
 
 
 
 
724
  help_mode=help_mode,
725
  ),
726
  solved=True,
727
  help_mode=help_mode,
728
+ steps=steps,
729
+ display_steps=steps,
730
  )
731
 
732
  if intent == "factor":
733
+ steps = [
734
+ "- Take out the greatest common factor first.",
735
+ "- Check for difference of squares.",
736
+ "- Check for perfect-square trinomials.",
737
+ "- If it is quadratic in form, use sum/product structure."
738
+ ]
739
  return _mk_result(
740
  reply=_modeled_steps(
741
  title="This is a factorization task.",
742
  method="Start by pulling out any common factor, then check special identities and quadratic patterns.",
743
+ steps=steps,
 
 
 
 
 
744
  help_mode=help_mode,
745
  ),
746
  solved=True,
747
  help_mode=help_mode,
748
+ steps=steps,
749
+ display_steps=steps,
750
  )
751
 
752
  if intent == "rearrange":
753
+ steps = [
754
+ "- Identify which variable must be isolated.",
755
+ "- Move all target-variable terms to one side.",
756
+ "- Move all non-target terms to the other side.",
757
+ "- Factor the target variable if it appears in multiple terms.",
758
+ "- Divide only when you know the divisor is allowed to be nonzero."
759
+ ]
760
  return _mk_result(
761
  reply=_modeled_steps(
762
  title="This is a rearranging / isolating task.",
763
  method="Move all terms involving the target variable together, then factor it out.",
764
+ steps=steps,
 
 
 
 
 
 
765
  help_mode=help_mode,
766
  ),
767
  solved=True,
768
  help_mode=help_mode,
769
+ steps=steps,
770
+ display_steps=steps,
771
  )
772
 
 
773
  deg = _degree_of_expr(expr)
774
  steps = [
775
  "- Decide whether the best move is simplify, expand, factor, or substitute.",
 
788
  ),
789
  solved=True,
790
  help_mode=help_mode,
791
+ steps=steps,
792
+ display_steps=steps,
793
  )
794
  except Exception:
795
  return None
 
797
 
798
  def _handle_word_translation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]:
799
  lower = parsed["lower"]
 
800
 
801
  triggers = [
802
  "more than", "less than", "twice", "times", "sum of", "difference",
 
806
  return None
807
 
808
  mappings = [
809
+ ("more than", "Be careful with order: '10 more than x' means x + 10."),
810
+ ("less than", "Be careful with order: '3 less than x' means x - 3, but '3 less than a number' means number - 3."),
811
+ ("twice", "'Twice x' means 2x."),
812
+ ("sum of", "'Sum of a and b' means a + b."),
813
+ ("difference", "'Difference of a and b' means a - b."),
814
+ ("product of", "'Product of a and b' means ab."),
815
+ ("quotient", "'Quotient of a and b' means a / b."),
816
+ ("at least", "This signals >=."),
817
+ ("at most", "This signals <=."),
818
+ ("no more than", "This signals <=."),
819
+ ("no less than", "This signals >=."),
820
+ ("consecutive", "Use n, n+1, n+2, ..."),
821
  ]
822
 
823
  bullets = []
 
840
  ),
841
  solved=True,
842
  help_mode=help_mode,
843
+ steps=bullets,
844
+ display_steps=bullets,
845
  )
846
 
847
 
 
849
  if not parsed["mentions_degree"]:
850
  return None
851
 
852
+ steps = [
853
+ "- Degree 1 suggests a linear structure.",
854
+ "- Degree 2 suggests a quadratic structure.",
855
+ "- Higher degree often calls for factorization, substitution, or identity spotting.",
856
+ "- A polynomial of degree n can have at most n roots over the reals/complexes combined."
857
+ ]
858
  return _mk_result(
859
  reply=_modeled_steps(
860
  title="This question is using degree / polynomial structure.",
861
  method="The degree tells you the highest power present and helps narrow the right solving method.",
862
+ steps=steps,
 
 
 
 
 
863
  help_mode=help_mode,
864
  ),
865
  solved=True,
866
  help_mode=help_mode,
867
+ steps=steps,
868
+ display_steps=steps,
869
  )
870
 
871
 
 
875
  if not parsed["equations"] and not parsed["inequalities"]:
876
  return None
877
 
878
+ steps = [
879
+ "- Use the algebra to narrow the possible forms first.",
880
+ "- Then test only values consistent with the restriction.",
881
+ "- Stop when the restriction makes further values impossible.",
882
+ "- Always check the tested value in the original condition."
883
+ ]
884
  return _mk_result(
885
  reply=_modeled_steps(
886
  title="This problem has an integer restriction.",
887
  method="That often makes controlled testing or divisibility reasoning much faster than pure symbolic solving.",
888
+ steps=steps,
 
 
 
 
 
889
  help_mode=help_mode,
890
  ),
891
  solved=True,
892
  help_mode=help_mode,
893
+ steps=steps,
894
+ display_steps=steps,
895
  )
896
 
897
 
 
948
  # =========================================================
949
 
950
  def _modeled_steps(title: str, method: str, steps: List[str], help_mode: str) -> str:
951
+ clean_steps = _ensure_bullets(steps)
952
+
953
  if help_mode == "hint":
954
  return _join_sections(
955
  title,
956
  f"Hint: {method}",
957
+ clean_steps[0] if clean_steps else ""
958
  )
959
 
960
  if help_mode == "explain":
961
  return _join_sections(
962
  title,
963
  f"Method idea: {method}",
964
+ "\n".join(clean_steps[:3])
965
  )
966
 
967
  if help_mode == "method":
968
  return _join_sections(
969
  title,
970
  f"Method: {method}",
971
+ "\n".join(clean_steps)
972
  )
973
 
 
974
  return _join_sections(
975
  title,
976
  f"Walkthrough method: {method}",
977
+ "\n".join(clean_steps)
978
  )
979
 
980
 
 
983
  return "\n\n".join(clean)
984
 
985
 
986
+ def _mk_result(
987
+ reply: str,
988
+ solved: bool,
989
+ help_mode: str,
990
+ steps: Optional[List[str]] = None,
991
+ display_steps: Optional[List[str]] = None,
992
+ final_value: Any = None,
993
+ ) -> SolverResult:
994
+ steps = steps or []
995
+ display_steps = display_steps or steps
996
+
997
+ result = SolverResult(
998
  reply=reply,
999
  meta={
1000
  "domain": "quant",
 
1005
  "topic": "algebra",
1006
  "used_retrieval": False,
1007
  "used_generator": False,
1008
+ "steps": steps,
1009
+ "display_steps": display_steps,
1010
+ "internal_answer": _safe_str(final_value) if final_value is not None else None,
1011
  },
1012
+ )
1013
+
1014
+ # Add attributes directly in case your formatter / conversation layer reads them.
1015
+ try:
1016
+ setattr(result, "steps", display_steps)
1017
+ except Exception:
1018
+ pass
1019
+
1020
+ try:
1021
+ setattr(result, "display_steps", display_steps)
1022
+ except Exception:
1023
+ pass
1024
+
1025
+ try:
1026
+ setattr(result, "all_steps", steps)
1027
+ except Exception:
1028
+ pass
1029
+
1030
+ try:
1031
+ setattr(result, "internal_answer", final_value)
1032
+ except Exception:
1033
+ pass
1034
+
1035
+ return result