from __future__ import annotations import re from typing import List, Optional, Tuple, Dict, Any from models import SolverResult try: import sympy as sp except Exception: sp = None # ========================================================= # Main entry # ========================================================= def solve_algebra(text: str) -> Optional[SolverResult]: raw = (text or "").strip() if not raw: return None lower = raw.lower() if not _looks_like_algebra(raw, lower): return None help_mode = _detect_help_mode(lower) intent = _detect_intent(raw, lower) cleaned = _normalize_text(raw) if sp is None: return _mk_result( reply=_format_explanation_only(raw, lower, help_mode, intent), solved=False, help_mode=help_mode, steps=[], display_steps=[], ) parsed = _parse_request(cleaned, lower) explanation = _explain_what_is_being_asked(parsed, intent) result = ( _handle_systems(parsed, help_mode) or _handle_inequality(parsed, help_mode) or _handle_equation(parsed, help_mode) or _handle_expression(parsed, help_mode, intent) or _handle_word_translation(parsed, help_mode) or _handle_degree_reasoning(parsed, help_mode) or _handle_integer_restricted(parsed, help_mode) ) if result is None: reply = _join_sections( "Let’s work through it.", explanation, _generic_algebra_guidance(parsed, help_mode, intent), ) return _mk_result( reply=reply, solved=False, help_mode=help_mode, steps=[], display_steps=[], ) if explanation: result.reply = _join_sections("Let’s work through it.", explanation, result.reply) else: result.reply = _join_sections("Let’s work through it.", result.reply) return result # ========================================================= # Recognition # ========================================================= _ALGEBRA_KEYWORDS = [ "solve", "simplify", "factor", "factorise", "factorize", "expand", "rearrange", "rewrite", "substitute", "expression", "equation", "inequality", "quadratic", "linear", "polynomial", "root", "roots", "simultaneous", "system", "identity", "identities", "degree", "in terms of", "what is the value of", "which expression", "equivalent expression", "collect like terms", "brackets", ] _WORD_MATH_SIGNALS = [ "more than", "less than", "twice", "three times", "sum of", "difference", "product of", "quotient", "at least", "at most", "no more than", "no less than", "consecutive", "integer", "positive integer", "real number", "rational", "variable", ] def _looks_like_algebra(raw: str, lower: str) -> bool: if any(k in lower for k in _ALGEBRA_KEYWORDS): return True if any(k in lower for k in _WORD_MATH_SIGNALS): return True if "=" in raw or "<" in raw or ">" in raw or "≤" in raw or "≥" in raw: return True if re.search(r"[a-zA-Z]", raw) and re.search(r"[\+\-\*/\^\(\)]", raw): return True if re.search(r"\b[a-zA-Z]\b", raw): return True return False def _detect_help_mode(lower: str) -> str: if any(x in lower for x in ["hint", "nudge", "small hint", "next hint"]): return "hint" if any(x in lower for x in ["step by step", "steps", "walkthrough", "work through"]): return "walkthrough" if any(x in lower for x in ["explain", "what is this asking", "what does this mean", "decode"]): return "explain" if any(x in lower for x in ["method", "approach", "how do i solve", "how to solve"]): return "method" return "answer" def _detect_intent(raw: str, lower: str) -> str: if any(x in lower for x in ["simplify", "collect like terms", "reduce"]): return "simplify" if any(x in lower for x in ["expand", "multiply out", "open brackets"]): return "expand" if any(x in lower for x in ["factor", "factorise", "factorize"]): return "factor" if any(x in lower for x in ["rearrange", "write in terms of", "make", "isolate"]): return "rearrange" if any(x in lower for x in ["solve", "find x", "find y", "roots", "root"]): return "solve" if any(x in lower for x in ["which expression", "equivalent expression"]): return "equivalent" if any(x in lower for x in ["inequality", "at least", "at most", "greater than", "less than"]): return "inequality" if "=" in raw: return "solve" return "general" # ========================================================= # Parsing / normalization # ========================================================= def _normalize_text(raw: str) -> str: text = raw.replace("×", "*").replace("÷", "/").replace("−", "-") text = text.replace("≤", "<=").replace("≥", ">=") text = text.replace("^", "**") text = re.sub(r"\s+", " ", text).strip() return text def _parse_request(text: str, lower: str) -> Dict[str, Any]: variables = sorted(set(re.findall(r"\b[a-zA-Z]\b", text))) equations = _extract_equations(text) inequalities = _extract_inequalities(text) expressions = _extract_expressions(text, equations, inequalities) return { "raw": text, "lower": lower, "variables": variables, "equations": equations, "inequalities": inequalities, "expressions": expressions, "has_integer_constraint": bool(re.search(r"\binteger|positive integer|whole number|natural number\b", lower)), "has_real_constraint": bool(re.search(r"\breal\b", lower)), "has_nonzero_constraint": bool(re.search(r"\bnonzero|not zero\b", lower)), "has_distinct_constraint": bool(re.search(r"\bdistinct\b", lower)), "mentions_degree": "degree" in lower or "quadratic" in lower or "linear" in lower or "cubic" in lower, } def _extract_equations(text: str) -> List[str]: parts = re.split(r"[;,]| and ", text) eqs = [] for p in parts: p = p.strip() if p.count("=") == 1 and "<" not in p and ">" not in p: eqs.append(p) return eqs def _extract_inequalities(text: str) -> List[str]: parts = re.split(r"[;,]| and ", text) out = [] for p in parts: p = p.strip() if any(op in p for op in ["<=", ">=", "<", ">"]): out.append(p) return out def _extract_expressions(text: str, equations: List[str], inequalities: List[str]) -> List[str]: t = text for x in equations + inequalities: t = t.replace(x, " ") t = re.sub( r"\b(solve|simplify|expand|factor|factorise|factorize|rearrange|rewrite|find|explain|what is|what's|which)\b", " ", t, flags=re.I ) t = re.sub(r"\s+", " ", t).strip() return [t] if t else [] # ========================================================= # Sympy helpers # ========================================================= def _sympify_expr(expr: str): expr = expr.strip() expr = re.sub(r"(?<=\d)(?=[a-zA-Z\(])", "*", expr) expr = re.sub(r"(?<=[a-zA-Z\)])(?=\d)", "*", expr) expr = re.sub(r"(?<=[a-zA-Z])(?=\()", "*", expr) expr = re.sub(r"(?<=\))(?=[a-zA-Z])", "*", expr) return sp.sympify(expr, evaluate=True) def _sympify_equation(eq: str): left, right = eq.split("=") return sp.Eq(_sympify_expr(left), _sympify_expr(right)) def _sympify_inequality(ineq: str): for op in ["<=", ">=", "<", ">"]: if op in ineq: left, right = ineq.split(op, 1) l = _sympify_expr(left) r = _sympify_expr(right) if op == "<=": return l <= r, op if op == ">=": return l >= r, op if op == "<": return l < r, op if op == ">": return l > r, op return None, None def _free_symbols_from_strings(items: List[str]) -> List[sp.Symbol]: syms = set() for item in items: try: obj = _sympify_equation(item) if "=" in item else _sympify_expr(item) syms |= obj.free_symbols except Exception: pass return sorted(list(syms), key=lambda s: s.name) def _degree_of_expr(expr) -> Optional[int]: try: poly = sp.Poly(sp.expand(expr)) return poly.total_degree() except Exception: return None def _safe_str(obj: Any) -> str: try: return str(sp.simplify(obj)) except Exception: return str(obj) def _format_eq(lhs: Any, rhs: Any) -> str: return f"{_safe_str(lhs)} = {_safe_str(rhs)}" def _strip_bullet_prefix(step: str) -> str: return re.sub(r"^\s*-\s*", "", (step or "").strip()) def _ensure_bullets(steps: List[str]) -> List[str]: out = [] for s in steps: s = _strip_bullet_prefix(s) if s: out.append(f"- {s}") return out # ========================================================= # Step builders # ========================================================= def _build_linear_equation_steps(eq, var) -> Tuple[List[str], List[str], Optional[Any]]: """ Returns: all_steps: full internal steps including final solved step if available display_steps: safe steps for walkthrough/method/answer without answer leakage final_value: solved value if found """ lhs = sp.simplify(eq.lhs) rhs = sp.simplify(eq.rhs) start_step = f"Start with {_format_eq(lhs, rhs)}." try: solutions = sp.solve(eq, var) except Exception: solutions = [] final_value = solutions[0] if len(solutions) == 1 else None expr = sp.expand(lhs - rhs) coeff = sp.expand(expr).coeff(var, 1) const = sp.expand(expr).subs(var, 0) all_steps: List[str] = [start_step] display_steps: List[str] = [start_step] # Pattern 1: x / c = k if lhs == var / sp.denom(lhs) and rhs.is_number: denom = sp.denom(lhs) step2_lhs = sp.simplify(lhs * denom) step2_rhs = sp.simplify(rhs * denom) step2 = f"Multiply both sides by {_safe_str(denom)} to undo the division." step3 = f"This gives {_format_eq(step2_lhs, step2_rhs)}." all_steps.extend([step2, step3]) display_steps.extend([step2, step3]) if final_value is not None: all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.") return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value # Pattern 2: a*x = b try: ratio = sp.simplify(lhs / var) if lhs == sp.simplify(ratio * var) and ratio.is_number and ratio != 1 and rhs.is_number: step2 = f"Divide both sides by {_safe_str(ratio)} to isolate {_safe_str(var)}." step3 = f"This leaves {_safe_str(var)} by itself on the left." all_steps.extend([step2, step3]) display_steps.extend([step2, step3]) if final_value is not None: all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.") return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value except Exception: pass # Pattern 3: x + b = c try: if sp.expand(lhs).coeff(var, 1) == 1 and rhs.is_number: extra = sp.simplify(lhs - var) if extra.free_symbols == set(): if extra != 0: direction = "subtract" if extra > 0 else "add" amount = abs(extra) step2 = f"{direction.capitalize()} {_safe_str(amount)} on both sides to undo the constant term." step3 = f"That isolates {_safe_str(var)} on the left." all_steps.extend([step2, step3]) display_steps.extend([step2, step3]) if final_value is not None: all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.") return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value except Exception: pass # General linear fallback if coeff != 0: if const != 0: step2 = "Rearrange so the variable term is isolated." step3 = "Undo the constant term using the inverse operation on both sides." display_steps.extend([step2, step3]) all_steps.extend([step2, step3]) if coeff != 1: step4 = f"Then divide by the coefficient of {_safe_str(var)} to isolate the variable." display_steps.append(step4) all_steps.append(step4) if final_value is not None: all_steps.append(f"So {_safe_str(var)} = {_safe_str(final_value)}.") else: all_steps.append("There is no variable term left after simplification, so check whether the equation is always true or impossible.") display_steps.append("After simplification, check whether the statement is always true or impossible.") return _ensure_bullets(all_steps), _ensure_bullets(display_steps), final_value # ========================================================= # Handlers # ========================================================= def _handle_systems(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]: eqs = parsed["equations"] if len(eqs) < 2: return None try: sym_eqs = [_sympify_equation(e) for e in eqs] symbols = _free_symbols_from_strings(eqs) if not symbols: return None nonlinear = [] for eq in sym_eqs: expr = sp.expand(eq.lhs - eq.rhs) deg = _degree_of_expr(expr) if deg != 1: nonlinear.append(eq) if nonlinear: steps = [ "- Identify each equation and look for a substitution or elimination route.", "- Check whether any equation can isolate one variable cleanly.", "- Substitute that expression into the others to reduce the system." ] if parsed["has_integer_constraint"]: steps.append("- Because there is an integer restriction, candidate values can also be checked efficiently.") return _mk_result( reply=_modeled_steps( title="This is a system of equations.", method="Use substitution or elimination to reduce the number of variables.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) matrix, vec = sp.linear_eq_to_matrix([eq.lhs - eq.rhs for eq in sym_eqs], symbols) rank_a = matrix.rank() rank_aug = matrix.row_join(vec).rank() nvars = len(symbols) if rank_a != rank_aug: msg = [ "- This system is inconsistent.", "- That means the equations conflict with each other, so there is no common solution.", "- A good first move is to eliminate one variable and compare the resulting statements." ] return _mk_result( reply=_modeled_steps( title="This system is inconsistent.", method="Eliminate a variable and compare the resulting statements.", steps=msg, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=msg, display_steps=msg, ) if rank_a < nvars: msg = [ "- This system does not pin down a unique solution.", "- That means there are infinitely many solutions or at least one free variable.", "- On GMAT-style questions, this often means you should solve for a relationship instead of individual values." ] return _mk_result( reply=_modeled_steps( title="This system does not have a unique solution.", method="Look for a relationship rather than separate fixed values.", steps=msg, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=msg, display_steps=msg, ) steps = [ "- Choose one variable to eliminate.", "- Make the coefficients match, then subtract or add the equations.", "- Solve the reduced one-variable equation.", "- Substitute back into one original equation.", "- Check the pair against the remaining equation(s)." ] return _mk_result( reply=_modeled_steps( title="This is a linear system with a unique solution structure.", method="Elimination is usually the cleanest route unless one equation already isolates a variable.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) except Exception: return None def _handle_inequality(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]: ineqs = parsed["inequalities"] if not ineqs: return None try: first = ineqs[0] rel, op = _sympify_inequality(first) if rel is None: return None syms = sorted(list(rel.free_symbols), key=lambda s: s.name) if len(syms) != 1: steps = [ "- Collect all terms on one side.", "- Factor if possible.", "- Mark critical points where the expression is 0 or undefined.", "- Test intervals to see where the inequality is true.", "- Remember: multiplying or dividing by a negative flips the inequality sign." ] return _mk_result( reply=_modeled_steps( title="This is an inequality problem.", method="Rearrange so one side becomes 0, then analyze sign changes or isolate the variable.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) var = syms[0] steps = [ f"- Isolate {_safe_str(var)} as much as possible.", "- Be careful with brackets, fractions, and negative coefficients.", "- If you multiply or divide by a negative quantity, reverse the inequality sign.", "- If the expression factors, use sign analysis instead of treating it like a normal equation." ] return _mk_result( reply=_modeled_steps( title="This is a one-variable inequality.", method="Solve it like an equation, but track sign changes carefully.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) except Exception: return None def _handle_equation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]: eqs = parsed["equations"] if len(eqs) != 1: return None try: eq = _sympify_equation(eqs[0]) expr = sp.expand(eq.lhs - eq.rhs) syms = sorted(list(expr.free_symbols), key=lambda s: s.name) deg = _degree_of_expr(expr) if not syms: steps = [ "- Simplify both sides fully.", "- Then decide whether the statement is always true, never true, or just a numeric identity." ] return _mk_result( reply=_modeled_steps( title="No variable remains after simplification.", method="Check the resulting statement itself.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) if len(syms) > 1 and deg == 1: steps = [ "- One equation with multiple variables usually does not determine each variable uniquely.", "- Rearrange to express one variable in terms of the others.", "- If the question asks for a combination like x+y, look for a way to isolate that combination directly." ] return _mk_result( reply=_modeled_steps( title="This is a linear equation in more than one variable.", method="Do not assume you can find unique values for every variable from a single equation.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) if deg == 1: var = syms[0] all_steps, display_steps, final_value = _build_linear_equation_steps(eq, var) reply = _modeled_steps( title="This is a linear equation.", method="Use inverse operations and keep both sides balanced.", steps=display_steps, help_mode=help_mode, ) return _mk_result( reply=reply, solved=True, help_mode=help_mode, steps=all_steps, display_steps=display_steps, final_value=final_value, ) if deg == 2: var = syms[0] disc = None try: poly = sp.Poly(expr, var) coeffs = poly.all_coeffs() if len(coeffs) == 3: a, b, c = coeffs disc = sp.expand(b**2 - 4 * a * c) except Exception: pass steps = [ "- Rearrange into standard quadratic form.", "- Check whether it factors neatly.", "- If it does not factor cleanly, use a systematic method such as the quadratic formula or completing the square.", "- After finding candidate roots internally, substitute back to verify." ] if disc is not None: steps.append("- The discriminant tells you whether there are two, one, or no real roots.") return _mk_result( reply=_modeled_steps( title="This is a quadratic equation.", method="First look for factorization; otherwise move to a general solving method.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) if deg and deg > 2: factored = sp.factor(expr) steps = [ "- Look for a common factor first.", "- Check for algebraic identities such as difference of squares or grouping patterns.", "- See whether a substitution can reduce the degree, for example letting u = x^2 or u = x^3.", "- Once reduced, solve the lower-degree equation and then translate back." ] if factored != expr: steps.append("- This one appears factorable, so the zero-product idea is likely useful.") if parsed["has_integer_constraint"]: steps.append("- Since the variables may be restricted to integers, candidate checking can also be efficient.") return _mk_result( reply=_modeled_steps( title="This is a higher-degree algebra equation.", method="Reduce it by factorization or substitution before trying to solve.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) except Exception: return None return None def _handle_expression(parsed: Dict[str, Any], help_mode: str, intent: str) -> Optional[SolverResult]: exprs = parsed["expressions"] if not exprs: return None expr_text = exprs[0].strip() if not expr_text: return None try: expr = _sympify_expr(expr_text) if intent == "simplify": steps = [ "- Expand only if that helps combine terms.", "- Collect like powers and like variable terms.", "- Factor common pieces if the expression becomes cleaner that way.", "- Check for hidden identities such as a^2-b^2 or perfect squares." ] return _mk_result( reply=_modeled_steps( title="This is a simplification task.", method="Combine like terms, reduce fractions carefully, and use identities where helpful.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) if intent == "expand": steps = [ "- Multiply each outside factor by each inside term.", "- Watch negative signs.", "- Combine like terms at the end." ] return _mk_result( reply=_modeled_steps( title="This is an expansion task.", method="Distribute carefully across every term in the bracket(s).", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) if intent == "factor": steps = [ "- Take out the greatest common factor first.", "- Check for difference of squares.", "- Check for perfect-square trinomials.", "- If it is quadratic in form, use sum/product structure." ] return _mk_result( reply=_modeled_steps( title="This is a factorization task.", method="Start by pulling out any common factor, then check special identities and quadratic patterns.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) if intent == "rearrange": steps = [ "- Identify which variable must be isolated.", "- Move all target-variable terms to one side.", "- Move all non-target terms to the other side.", "- Factor the target variable if it appears in multiple terms.", "- Divide only when you know the divisor is allowed to be nonzero." ] return _mk_result( reply=_modeled_steps( title="This is a rearranging / isolating task.", method="Move all terms involving the target variable together, then factor it out.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) deg = _degree_of_expr(expr) steps = [ "- Decide whether the best move is simplify, expand, factor, or substitute.", "- Look for common factors and algebraic identities.", "- Watch for domain restrictions if variables appear in denominators or radicals." ] if deg is not None: steps.append(f"- The expression behaves like degree {deg}, which can guide which identities are likely useful.") return _mk_result( reply=_modeled_steps( title="This is an algebraic expression task.", method="Classify the structure first, then use the matching algebra tool.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) except Exception: return None def _handle_word_translation(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]: lower = parsed["lower"] triggers = [ "more than", "less than", "twice", "times", "sum of", "difference", "product of", "quotient", "consecutive", "integer", "age", "number" ] if not any(t in lower for t in triggers): return None mappings = [ ("more than", "Be careful with order: '10 more than x' means x + 10."), ("less than", "Be careful with order: '3 less than x' means x - 3, but '3 less than a number' means number - 3."), ("twice", "'Twice x' means 2x."), ("sum of", "'Sum of a and b' means a + b."), ("difference", "'Difference of a and b' means a - b."), ("product of", "'Product of a and b' means ab."), ("quotient", "'Quotient of a and b' means a / b."), ("at least", "This signals >=."), ("at most", "This signals <=."), ("no more than", "This signals <=."), ("no less than", "This signals >=."), ("consecutive", "Use n, n+1, n+2, ..."), ] bullets = [] for k, v in mappings: if k in lower: bullets.append(f"- {v}") if not bullets: bullets = [ "- Translate the wording into variables first.", "- Build the equation or inequality before trying to solve." ] return _mk_result( reply=_modeled_steps( title="This is an algebra-from-words problem.", method="First translate the English into algebraic structure, then solve that structure.", steps=bullets, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=bullets, display_steps=bullets, ) def _handle_degree_reasoning(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]: if not parsed["mentions_degree"]: return None steps = [ "- Degree 1 suggests a linear structure.", "- Degree 2 suggests a quadratic structure.", "- Higher degree often calls for factorization, substitution, or identity spotting.", "- A polynomial of degree n can have at most n roots over the reals/complexes combined." ] return _mk_result( reply=_modeled_steps( title="This question is using degree / polynomial structure.", method="The degree tells you the highest power present and helps narrow the right solving method.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) def _handle_integer_restricted(parsed: Dict[str, Any], help_mode: str) -> Optional[SolverResult]: if not parsed["has_integer_constraint"]: return None if not parsed["equations"] and not parsed["inequalities"]: return None steps = [ "- Use the algebra to narrow the possible forms first.", "- Then test only values consistent with the restriction.", "- Stop when the restriction makes further values impossible.", "- Always check the tested value in the original condition." ] return _mk_result( reply=_modeled_steps( title="This problem has an integer restriction.", method="That often makes controlled testing or divisibility reasoning much faster than pure symbolic solving.", steps=steps, help_mode=help_mode, ), solved=True, help_mode=help_mode, steps=steps, display_steps=steps, ) # ========================================================= # Explanation logic # ========================================================= def _explain_what_is_being_asked(parsed: Dict[str, Any], intent: str) -> str: if intent == "solve" and parsed["equations"]: return "What the question is asking: find the value(s) of the variable that make the equation true." if intent == "simplify": return "What the question is asking: rewrite the expression into a cleaner equivalent form." if intent == "expand": return "What the question is asking: multiply out the brackets so the expression is written term-by-term." if intent == "factor": return "What the question is asking: rewrite the expression as a product of simpler factors." if intent == "rearrange": return "What the question is asking: isolate one variable or rewrite the formula in a requested form." if intent == "inequality" or parsed["inequalities"]: return "What the question is asking: find which value(s) make the inequality true, not just where two sides are equal." if len(parsed["equations"]) >= 2: return "What the question is asking: find values that satisfy all equations at the same time." return "" def _generic_algebra_guidance(parsed: Dict[str, Any], help_mode: str, intent: str) -> str: steps = [ "- First classify the task: simplify, expand, factor, solve, rearrange, or compare.", "- Then choose the matching algebra move instead of manipulating blindly.", "- Keep track of hidden restrictions such as denominators not being zero." ] return _modeled_steps( title="This is an algebra problem.", method="The key is to identify the structure before choosing a method.", steps=steps, help_mode=help_mode, ) def _format_explanation_only(raw: str, lower: str, help_mode: str, intent: str) -> str: return _join_sections( "Let’s work through it.", "I can identify this as an algebra problem, but the symbolic engine is unavailable in this environment.", _generic_algebra_guidance( {"equations": [], "inequalities": [], "raw": raw}, help_mode, intent, ), ) # ========================================================= # Output shaping # ========================================================= def _modeled_steps(title: str, method: str, steps: List[str], help_mode: str) -> str: clean_steps = _ensure_bullets(steps) if help_mode == "hint": return _join_sections( title, f"Hint: {method}", clean_steps[0] if clean_steps else "" ) if help_mode == "explain": return _join_sections( title, f"Method idea: {method}", "\n".join(clean_steps[:3]) ) if help_mode == "method": return _join_sections( title, f"Method: {method}", "\n".join(clean_steps) ) return _join_sections( title, f"Walkthrough method: {method}", "\n".join(clean_steps) ) def _join_sections(*parts: str) -> str: clean = [p.strip() for p in parts if p and p.strip()] return "\n\n".join(clean) def _mk_result( reply: str, solved: bool, help_mode: str, steps: Optional[List[str]] = None, display_steps: Optional[List[str]] = None, final_value: Any = None, ) -> SolverResult: steps = steps or [] display_steps = display_steps or steps result = SolverResult( reply=reply, meta={ "domain": "quant", "solved": solved, "help_mode": help_mode, "answer_letter": None, "answer_value": None, "topic": "algebra", "used_retrieval": False, "used_generator": False, "steps": steps, "display_steps": display_steps, "internal_answer": _safe_str(final_value) if final_value is not None else None, }, ) # Add attributes directly in case your formatter / conversation layer reads them. try: setattr(result, "steps", display_steps) except Exception: pass try: setattr(result, "display_steps", display_steps) except Exception: pass try: setattr(result, "all_steps", steps) except Exception: pass try: setattr(result, "internal_answer", final_value) except Exception: pass return result