| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| _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" |
|
|
|
|
| |
| |
| |
|
|
| 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 [] |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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] |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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, |
| ), |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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, |
| }, |
| ) |
|
|
| |
| 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 |