Upload app.py
Browse files
app.py
CHANGED
|
@@ -20,6 +20,10 @@ from sympy.parsing.sympy_parser import (
|
|
| 20 |
implicit_multiplication_application
|
| 21 |
)
|
| 22 |
import re # MUST be last β 'from sympy import *' would overwrite re otherwise
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# ββ Page config ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
st.set_page_config(
|
|
@@ -1475,6 +1479,158 @@ def run_sympy(problem: str) -> dict:
|
|
| 1475 |
|
| 1476 |
return {"type": "general", "result": None, "latex": ""}
|
| 1477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1478 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1479 |
# GROQ API β free, fast, Llama 3.3 70B
|
| 1480 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1667,7 +1823,7 @@ def ask_ai(problem: str, sympy_info: dict, history: list) -> str:
|
|
| 1667 |
"C. State exact trig values directly: $\\sin(\\pi)=0$, $\\cos(\\pi)=-1$ β never recompute.\n"
|
| 1668 |
"D. For Lagrange/Newton interpolation: DO NOT re-derive the polynomial.\n"
|
| 1669 |
" Show basis polynomials then state final polynomial from the verified result.\n"
|
| 1670 |
-
"E. For Euler/RK4: show k-values at each step then give y_{n+1}.\n\n"
|
| 1671 |
|
| 1672 |
"=== THEORY OF NUMBERS RULES ===\n"
|
| 1673 |
"A. For congruences $ax \\equiv b \\pmod{n}$: always show full Euclidean algorithm steps.\n"
|
|
@@ -2015,6 +2171,9 @@ if problem and problem != st.session_state.last_submitted:
|
|
| 2015 |
# Step 2: Stream AI response word by word
|
| 2016 |
answer = ask_ai_streaming(problem, sympy_result, st.session_state.messages)
|
| 2017 |
|
|
|
|
|
|
|
|
|
|
| 2018 |
# Save to history
|
| 2019 |
st.session_state.messages.append({"role": "user", "content": problem})
|
| 2020 |
st.session_state.messages.append({"role": "assistant", "content": answer})
|
|
|
|
| 20 |
implicit_multiplication_application
|
| 21 |
)
|
| 22 |
import re # MUST be last β 'from sympy import *' would overwrite re otherwise
|
| 23 |
+
import matplotlib
|
| 24 |
+
matplotlib.use('Agg') # non-interactive backend β required on HuggingFace Spaces
|
| 25 |
+
import matplotlib.pyplot as plt
|
| 26 |
+
import numpy as np
|
| 27 |
|
| 28 |
# ββ Page config ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
st.set_page_config(
|
|
|
|
| 1479 |
|
| 1480 |
return {"type": "general", "result": None, "latex": ""}
|
| 1481 |
|
| 1482 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1483 |
+
# GRAPH PLOTTING β only when user asks to plot/graph/draw/visualize
|
| 1484 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1485 |
+
def plot_graph(problem: str, sympy_info: dict):
|
| 1486 |
+
"""
|
| 1487 |
+
Plot graph only when user explicitly requests it.
|
| 1488 |
+
Keywords: plot, graph, draw, sketch, visualize, show graph.
|
| 1489 |
+
Supports: single function, derivative overlay, ODE solution, equation roots.
|
| 1490 |
+
Safe fallback β never crashes the app.
|
| 1491 |
+
"""
|
| 1492 |
+
p = problem.lower().strip()
|
| 1493 |
+
|
| 1494 |
+
# ββ Only trigger on explicit plot keywords ββββββββββββββββββββββ
|
| 1495 |
+
if not any(k in p for k in ["plot", "graph", "draw",
|
| 1496 |
+
"sketch", "visualize", "show graph"]):
|
| 1497 |
+
return # user didn't ask for graph β do nothing
|
| 1498 |
+
|
| 1499 |
+
try:
|
| 1500 |
+
x_sym = sp.Symbol('x')
|
| 1501 |
+
tfms = standard_transformations + (implicit_multiplication_application,)
|
| 1502 |
+
ld = {
|
| 1503 |
+
"x": x_sym, "e": sp.E, "E": sp.E,
|
| 1504 |
+
"pi": sp.pi, "PI": sp.pi,
|
| 1505 |
+
"sin": sp.sin, "cos": sp.cos, "tan": sp.tan,
|
| 1506 |
+
"exp": sp.exp, "log": sp.log, "ln": sp.log,
|
| 1507 |
+
"sqrt": sp.sqrt
|
| 1508 |
+
}
|
| 1509 |
+
|
| 1510 |
+
def clean_expr(s):
|
| 1511 |
+
s = re.sub(r"\s+", "", s)
|
| 1512 |
+
s = re.sub(r"\^", "**", s)
|
| 1513 |
+
return s
|
| 1514 |
+
|
| 1515 |
+
# ββ Extract expression from problem βββββββββββββββββββββββββ
|
| 1516 |
+
expr_sym = None
|
| 1517 |
+
label = ""
|
| 1518 |
+
|
| 1519 |
+
# Try to extract from sympy_info first (already parsed)
|
| 1520 |
+
if sympy_info.get("type") in ["Derivative", "Integral", "Equation",
|
| 1521 |
+
"ODE", "Limit", "Curvature"]:
|
| 1522 |
+
# Extract f(x) from problem text
|
| 1523 |
+
for kw in ["plot", "graph", "draw", "sketch", "visualize",
|
| 1524 |
+
"of", "for", "function"]:
|
| 1525 |
+
parts = p.split(kw, 1)
|
| 1526 |
+
if len(parts) > 1:
|
| 1527 |
+
raw = parts[1].strip()
|
| 1528 |
+
raw = re.sub(r"\s*(dx|with\s*respect.*|at\s+x.*)$", "", raw).strip()
|
| 1529 |
+
raw = clean_expr(raw)
|
| 1530 |
+
if raw:
|
| 1531 |
+
try:
|
| 1532 |
+
expr_sym = parse_expr(raw, transformations=tfms,
|
| 1533 |
+
local_dict=ld)
|
| 1534 |
+
label = raw
|
| 1535 |
+
break
|
| 1536 |
+
except Exception:
|
| 1537 |
+
continue
|
| 1538 |
+
|
| 1539 |
+
# Fallback β extract any f(x) = ... or just expression
|
| 1540 |
+
if expr_sym is None:
|
| 1541 |
+
m = re.search(r"(?:f\s*\(x\)\s*=\s*|y\s*=\s*|of\s+|plot\s+|graph\s+|draw\s+)"
|
| 1542 |
+
r"([^\s,]+(?:\s*[\+\-\*/\^]\s*[^\s,]+)*)", p)
|
| 1543 |
+
if m:
|
| 1544 |
+
raw = clean_expr(m.group(1).strip())
|
| 1545 |
+
try:
|
| 1546 |
+
expr_sym = parse_expr(raw, transformations=tfms, local_dict=ld)
|
| 1547 |
+
label = raw
|
| 1548 |
+
except Exception:
|
| 1549 |
+
pass
|
| 1550 |
+
|
| 1551 |
+
if expr_sym is None:
|
| 1552 |
+
st.info("π Could not extract a plottable expression. "
|
| 1553 |
+
"Try: *plot f(x) = x^2 + 3x - 4*")
|
| 1554 |
+
return
|
| 1555 |
+
|
| 1556 |
+
# ββ Determine x range ββββββββββββββββββββββββββββββββββββββββ
|
| 1557 |
+
x_range_m = re.search(
|
| 1558 |
+
r"(?:from|for|on|between)\s*([-\d\.]+)\s*(?:to|and)\s*([-\d\.]+)", p)
|
| 1559 |
+
if x_range_m:
|
| 1560 |
+
x_min = float(x_range_m.group(1))
|
| 1561 |
+
x_max = float(x_range_m.group(2))
|
| 1562 |
+
else:
|
| 1563 |
+
x_min, x_max = -10, 10 # default range
|
| 1564 |
+
|
| 1565 |
+
# ββ Lambdify for fast numpy evaluation βββββββββββββββββββββββ
|
| 1566 |
+
f_num = sp.lambdify(x_sym, expr_sym, modules=["numpy"])
|
| 1567 |
+
df_sym = sp.diff(expr_sym, x_sym)
|
| 1568 |
+
df_num = sp.lambdify(x_sym, df_sym, modules=["numpy"])
|
| 1569 |
+
|
| 1570 |
+
x_vals = np.linspace(x_min, x_max, 800)
|
| 1571 |
+
|
| 1572 |
+
# Safe evaluation β replace infinities/errors with NaN
|
| 1573 |
+
with np.errstate(all='ignore'):
|
| 1574 |
+
y_vals = np.array(f_num(x_vals), dtype=float)
|
| 1575 |
+
dy_vals = np.array(df_num(x_vals), dtype=float)
|
| 1576 |
+
|
| 1577 |
+
y_vals[~np.isfinite(y_vals)] = np.nan
|
| 1578 |
+
dy_vals[~np.isfinite(dy_vals)] = np.nan
|
| 1579 |
+
|
| 1580 |
+
# ββ Build plot βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1581 |
+
fig, ax = plt.subplots(figsize=(8, 4))
|
| 1582 |
+
fig.patch.set_facecolor('#0f0f0f')
|
| 1583 |
+
ax.set_facecolor('#1a1a1a')
|
| 1584 |
+
|
| 1585 |
+
# Plot f(x)
|
| 1586 |
+
ax.plot(x_vals, y_vals, color='#3b82f6', linewidth=2,
|
| 1587 |
+
label=f'$f(x) = {sp.latex(expr_sym)}$')
|
| 1588 |
+
|
| 1589 |
+
# If derivative type β also plot f'(x)
|
| 1590 |
+
if sympy_info.get("type") == "Derivative":
|
| 1591 |
+
ax.plot(x_vals, dy_vals, color='#f59e0b', linewidth=1.8,
|
| 1592 |
+
linestyle='--', label=f"$f'(x) = {sp.latex(df_sym)}$")
|
| 1593 |
+
|
| 1594 |
+
# x and y axes
|
| 1595 |
+
ax.axhline(0, color='#444', linewidth=0.8)
|
| 1596 |
+
ax.axvline(0, color='#444', linewidth=0.8)
|
| 1597 |
+
|
| 1598 |
+
# Grid
|
| 1599 |
+
ax.grid(True, color='#2a2a2a', linewidth=0.6, linestyle='--')
|
| 1600 |
+
|
| 1601 |
+
# Labels and title
|
| 1602 |
+
ax.set_xlabel('x', color='#ececec', fontsize=11)
|
| 1603 |
+
ax.set_ylabel('y', color='#ececec', fontsize=11)
|
| 1604 |
+
ax.set_title(f'Graph of $f(x) = {sp.latex(expr_sym)}$',
|
| 1605 |
+
color='#ffffff', fontsize=12, pad=10)
|
| 1606 |
+
|
| 1607 |
+
# Tick colors
|
| 1608 |
+
ax.tick_params(colors='#888', labelsize=9)
|
| 1609 |
+
for spine in ax.spines.values():
|
| 1610 |
+
spine.set_edgecolor('#2a2a2a')
|
| 1611 |
+
|
| 1612 |
+
# Legend
|
| 1613 |
+
ax.legend(facecolor='#1a1a1a', edgecolor='#2a2a2a',
|
| 1614 |
+
labelcolor='#ececec', fontsize=9)
|
| 1615 |
+
|
| 1616 |
+
# Smart y-axis limits β ignore outliers
|
| 1617 |
+
valid_y = y_vals[np.isfinite(y_vals)]
|
| 1618 |
+
if len(valid_y) > 0:
|
| 1619 |
+
y_med = np.median(valid_y)
|
| 1620 |
+
y_std = np.std(valid_y)
|
| 1621 |
+
y_lo = max(valid_y.min(), y_med - 5*y_std)
|
| 1622 |
+
y_hi = min(valid_y.max(), y_med + 5*y_std)
|
| 1623 |
+
padding = (y_hi - y_lo) * 0.1 if y_hi != y_lo else 1
|
| 1624 |
+
ax.set_ylim(y_lo - padding, y_hi + padding)
|
| 1625 |
+
|
| 1626 |
+
plt.tight_layout()
|
| 1627 |
+
st.pyplot(fig)
|
| 1628 |
+
plt.close(fig) # free memory
|
| 1629 |
+
|
| 1630 |
+
except Exception:
|
| 1631 |
+
pass # silent fallback β never crash the app
|
| 1632 |
+
|
| 1633 |
+
|
| 1634 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1635 |
# GROQ API β free, fast, Llama 3.3 70B
|
| 1636 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1823 |
"C. State exact trig values directly: $\\sin(\\pi)=0$, $\\cos(\\pi)=-1$ β never recompute.\n"
|
| 1824 |
"D. For Lagrange/Newton interpolation: DO NOT re-derive the polynomial.\n"
|
| 1825 |
" Show basis polynomials then state final polynomial from the verified result.\n"
|
| 1826 |
+
"E. For Euler/RK4: show k-values at each step then give $y_{n+1}$.\n\n"
|
| 1827 |
|
| 1828 |
"=== THEORY OF NUMBERS RULES ===\n"
|
| 1829 |
"A. For congruences $ax \\equiv b \\pmod{n}$: always show full Euclidean algorithm steps.\n"
|
|
|
|
| 2171 |
# Step 2: Stream AI response word by word
|
| 2172 |
answer = ask_ai_streaming(problem, sympy_result, st.session_state.messages)
|
| 2173 |
|
| 2174 |
+
# Step 3: Plot graph if user asked for it
|
| 2175 |
+
plot_graph(problem, sympy_result)
|
| 2176 |
+
|
| 2177 |
# Save to history
|
| 2178 |
st.session_state.messages.append({"role": "user", "content": problem})
|
| 2179 |
st.session_state.messages.append({"role": "assistant", "content": answer})
|