BuddyMath / math_engine.py
dotandru's picture
Fix: Clean production deployment with sse-starlette
9d29c62
import sympy as sp
class MathContext:
"""
A sandbox for the LLM to execute mathematics.
Each operation is recorded with Hebrew context and exact LaTeX representation.
"""
def __init__(self):
self.steps = []
# Injected SymPy helpers
self.sp = sp
self.Eq = sp.Eq
self.solve = sp.solve
self.expand = sp.expand
self.simplify = sp.simplify
self.sqrt = sp.sqrt
self.diff = sp.diff
def _format_latex(self, expr) -> str:
"""Converts SymPy to precise LaTeX, without $$ so it renders correctly in Flutter."""
if isinstance(expr, str):
# If the LLM passed a string instead of SymPy object, try parsing it
try:
expr = sp.sympify(expr)
except:
pass # Return as-is if not parseable
latex_str = sp.latex(expr)
return latex_str
def explain(self, text: str):
"""Adds a pure Hebrew explanation step without math."""
self.steps.append({
"content_mixed": text,
"block_math": ""
})
def declare_equation(self, text: str, eq: sp.Eq):
"""Prints a known equation with its explanation."""
self.steps.append({
"content_mixed": text,
"block_math": self._format_latex(eq)
})
return eq
def expand_expr(self, text: str, expr):
"""Expands an expression completely (e.g. squaring a root)."""
expanded = sp.expand(expr)
self.steps.append({
"content_mixed": text,
"block_math": self._format_latex(expanded)
})
return expanded
def solve_equation(self, text: str, eq: sp.Eq, var):
"""Solves an equation for a specific variable."""
solutions = sp.solve(eq, var)
# Format solutions nicely
if isinstance(solutions, list):
if len(solutions) == 1:
math_result = sp.Eq(var, solutions[0])
else:
# E.g. x_1 = 2, x_2 = -2
parts = [f"{sp.latex(var)}_{{{i+1}}} = {sp.latex(sol)}" for i, sol in enumerate(solutions)]
math_result = " \\text{ 讜讗讜 } ".join(parts)
else:
math_result = sp.Eq(var, solutions)
self.steps.append({
"content_mixed": text,
"block_math": math_result if isinstance(math_result, str) else self._format_latex(math_result)
})
return solutions
def finish(self, final_answer: str, teacher_summary: str = ""):
"""Sets the final human-readable answer for the UI."""
self.final_answer = final_answer
self.teacher_summary = teacher_summary
def run_llm_code(python_code: str) -> dict:
"""
Executes the LLM-generated Python code in our secure MathContext.
Returns the step-by-step UI format required by BuddyMath.
"""
ctx = MathContext()
# Secure Globals the LLM is allowed to use
safe_globals = {
"ctx": ctx,
"x": sp.Symbol('x'),
"y": sp.Symbol('y'),
"a": sp.Symbol('a'),
"b": sp.Symbol('b'),
"c": sp.Symbol('c'),
"m": sp.Symbol('m'),
"R": sp.Symbol('R'),
"sp": sp,
}
try:
# Execute the LLM's dynamically generated mathematics
exec(python_code, safe_globals)
return {
"success": True,
"steps": ctx.steps,
"final_answer": getattr(ctx, 'final_answer', "讛讙注谞讜 诇驻转专讜谉."),
"teacher_summary": getattr(ctx, 'teacher_summary', "")
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
if __name__ == "__main__":
# Simulate LLM output to solve a Locus / Parabola problem:
# "The distance from (x, y) to (2, 0) is equal to its distance to x = -2."
llm_code = """
ctx.explain("谞住诪谉 讗转 讛谞拽讜讚讛 讛讻诇诇讬转 注诇 讛诪拽讜诐 讛讙讬讗讜诪讟专讬 讻- (x,y). 诇驻讬 讛谞转讜谉, 讛诪专讞拽 诪讛诪讜拽讚 砖讜讜讛 诇诪专讞拽 诪讛诪讚专讬讱.")
d1 = sp.sqrt((x - 2)**2 + (y - 0)**2) # 诪讜拽讚
d2 = sp.sqrt((x - (-2))**2) # 诪讚专讬讱
eq1 = ctx.declare_equation("讛诪砖讜讜讗讛 讛诪砖讜讜讛 讘讬谉 讛诪专讞拽讬诐 讛讬讗:", ctx.Eq(d1, d2))
ctx.explain("谞注诇讛 讗转 砖谞讬 讛讗讙驻讬诐 讘专讬讘讜注 讻讚讬 诇讛讬驻讟专 诪讛砖讜专砖:")
# SymPy understands squaring both sides! We square them and declare equality.
squared_eq = ctx.Eq(d1**2, d2**2)
ctx.steps[-1]["block_math"] = ctx._format_latex(squared_eq) # Override previous block math
# Now expand and simplify it beautifully
expanded_eq = ctx.declare_equation("谞专讞讬讘 讗转 讛讘讬讟讜讬讬诐 (驻转讬讞转 住讜讙专讬讬诐 诪诇讗讛):", ctx.Eq(ctx.expand(squared_eq.lhs), ctx.expand(squared_eq.rhs)))
# SymPy's powerful simplify equation solver (subtract RHS from LHS)
simplified_expr = ctx.simplify(expanded_eq.lhs - expanded_eq.rhs)
final_eq = ctx.declare_equation("诇讗讞专 讻讬谞讜住 讗讬讘专讬诐 讜讛注讘专转 讗讙驻讬诐, 谞拽讘诇 讗转 爪讜专转 讛驻专讘讜诇讛 讛驻砖讜讟讛:", ctx.Eq(simplified_expr, 0))
# Also isolate y^2 if needed
y_sq_isolated = ctx.solve(final_eq, y**2)
if y_sq_isolated:
ctx.declare_equation("谞讘讜讚讚 讗转 y^2 讘诪砖讜讜讗讛:", ctx.Eq(y**2, y_sq_isolated[0]))
ctx.finish("$$ y^2 = 8x $$")
"""
print("馃殌 Running LLM Mathematics Script:")
result = run_llm_code(llm_code)
import json
# Print the resulting UI object
print(json.dumps(result, indent=2, ensure_ascii=False))