Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
import re
|
| 3 |
import gradio as gr
|
| 4 |
import sympy as sp
|
|
@@ -6,94 +7,246 @@ from sympy.parsing.sympy_parser import (
|
|
| 6 |
parse_expr, standard_transformations,
|
| 7 |
implicit_multiplication_application, convert_xor
|
| 8 |
)
|
|
|
|
|
|
|
| 9 |
|
| 10 |
TITLE = "LanguageBridge — Math Fast Agent (SymPy)"
|
| 11 |
|
| 12 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def normalize_ascii(s: str) -> str:
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
"=":"=", "-":"-", "+":"+", "*":"*", "/":"/", "。":".",
|
| 17 |
-
"×":"*", "√":"sqrt"
|
| 18 |
-
})
|
| 19 |
-
return s.translate(table)
|
| 20 |
-
|
| 21 |
-
def preprocess(expr: str) -> str:
|
| 22 |
-
s = (expr or "").strip()
|
| 23 |
-
s = normalize_ascii(s)
|
| 24 |
s = s.replace("^", "**") # 2^3 -> 2**3
|
| 25 |
-
#
|
| 26 |
return s
|
| 27 |
|
| 28 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
TRANS = standard_transformations + (implicit_multiplication_application, convert_xor)
|
| 30 |
|
| 31 |
-
def to_sympy_expr(
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
def to_sympy_eq(
|
| 35 |
-
s =
|
|
|
|
| 36 |
if "=" not in s:
|
| 37 |
raise ValueError("等式缺少 '='")
|
| 38 |
L, R = s.split("=", 1)
|
| 39 |
return sp.Eq(parse_expr(L, transformations=TRANS),
|
| 40 |
parse_expr(R, transformations=TRANS))
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def solve_math(q: str):
|
| 43 |
q = (q or "").strip()
|
| 44 |
if not q:
|
| 45 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
parts = [p for p in parts if p]
|
| 51 |
-
|
| 52 |
-
# 任何一行有 '=' → 解(可聯立)
|
| 53 |
-
if any("=" in p for p in parts):
|
| 54 |
-
eqs, syms = [], set()
|
| 55 |
-
for s in parts:
|
| 56 |
-
if "=" in s:
|
| 57 |
-
e = to_sympy_eq(s)
|
| 58 |
-
eqs.append(e)
|
| 59 |
-
syms |= e.free_symbols | getattr(e, "rhs", sp.Integer(0)).free_symbols
|
| 60 |
-
if not syms:
|
| 61 |
-
syms = {sp.symbols("x")}
|
| 62 |
-
sol = sp.solve(eqs, list(syms), dict=True)
|
| 63 |
-
if not sol:
|
| 64 |
-
return "無解或需要更多條件。"
|
| 65 |
-
return "\n".join(
|
| 66 |
-
f"解 {i}: " + ", ".join([f"{k} = {sp.simplify(v)}" for k, v in d.items()])
|
| 67 |
-
for i, d in enumerate(sol, 1)
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
# 否則:一般表達式(簡化/因式/微分/積分)
|
| 71 |
-
expr = to_sympy_expr(q)
|
| 72 |
-
out = []
|
| 73 |
-
try: out.append(f"簡化:{sp.simplify(expr)}")
|
| 74 |
-
except Exception: out.append(f"簡化:{expr}")
|
| 75 |
-
try:
|
| 76 |
-
fact = sp.factor(expr)
|
| 77 |
-
if fact != expr: out.append(f"因式分解:{fact}")
|
| 78 |
-
except Exception: pass
|
| 79 |
-
try:
|
| 80 |
-
x = next(iter(expr.free_symbols)) if expr.free_symbols else sp.symbols("x")
|
| 81 |
-
out.append(f"對 {x} 微分:{sp.diff(expr, x)}")
|
| 82 |
-
out.append(f"對 {x} 積分:{sp.integrate(expr, x)}")
|
| 83 |
-
except Exception: pass
|
| 84 |
-
return "\n".join(out) if out else f"結果:{expr}"
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
except Exception as e:
|
| 87 |
return f"解析失敗:{e}"
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
with gr.Blocks(title=TITLE) as demo:
|
| 90 |
gr.Markdown(
|
| 91 |
-
"## " + TITLE + "
|
| 92 |
-
"貼上算式(可多行 / 分號 `;` 分隔)。支援隱式乘法(例:`3x`, `2(x+1)`, `(x+1)(x-1)`, `2sqrt(x)`)與
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
)
|
| 94 |
-
q = gr.Textbox(lines=6, label="題目 /
|
| 95 |
-
out = gr.Textbox(lines=
|
| 96 |
gr.Button("送出 🚀").click(fn=solve_math, inputs=q, outputs=out)
|
| 97 |
|
| 98 |
if __name__ == "__main__":
|
|
|
|
| 99 |
demo.launch()
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
# LanguageBridge — Math Fast Agent (SymPy) (2025-11-04 修正版)
|
| 3 |
import re
|
| 4 |
import gradio as gr
|
| 5 |
import sympy as sp
|
|
|
|
| 7 |
parse_expr, standard_transformations,
|
| 8 |
implicit_multiplication_application, convert_xor
|
| 9 |
)
|
| 10 |
+
from sympy import Interval, Union, S, And, Or
|
| 11 |
+
from sympy.solvers.inequalities import solve_univariate_inequality
|
| 12 |
|
| 13 |
TITLE = "LanguageBridge — Math Fast Agent (SymPy)"
|
| 14 |
|
| 15 |
+
# ----------------------------
|
| 16 |
+
# 1) 安全正規化:全形→半形、特殊符號、常見中文習慣
|
| 17 |
+
# ----------------------------
|
| 18 |
+
_ASCII_TABLE = str.maketrans({
|
| 19 |
+
# 括號與標點
|
| 20 |
+
"(": "(", ")": ")", ",": ",", "、": ",", ";": ";", ":": ":",
|
| 21 |
+
".": ".", "。": ".", ",": ",", "!": "!", "?": "?",
|
| 22 |
+
# 運算與關係
|
| 23 |
+
"+": "+", "-": "-", "~": "~", "=": "=", "*": "*", "/": "/",
|
| 24 |
+
"<": "<", ">": ">", "≤": "<=", "≥": ">=",
|
| 25 |
+
"∧": "&", "∨": "|",
|
| 26 |
+
# 乘號、根號
|
| 27 |
+
"×": "*", "√": "sqrt",
|
| 28 |
+
# 希臘字母/常數
|
| 29 |
+
"π": "pi", "Π": "pi",
|
| 30 |
+
"θ": "theta", "Θ": "theta",
|
| 31 |
+
})
|
| 32 |
+
|
| 33 |
+
# 允許保留的英文單字(其餘中文字會被視為非數學描述)
|
| 34 |
+
_ALLOWED_WORDS = {
|
| 35 |
+
"sin","cos","tan","cot","sec","csc",
|
| 36 |
+
"asin","acos","atan","sinh","cosh","tanh","log","ln","exp","sqrt","abs",
|
| 37 |
+
"diff","integrate","factor","simplify","expand"
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
def normalize_ascii(s: str) -> str:
|
| 41 |
+
s = (s or "").replace("\u00A0", " ").replace("\u3000", " ") # 不換行空白/全形空白
|
| 42 |
+
s = s.translate(_ASCII_TABLE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
s = s.replace("^", "**") # 2^3 -> 2**3
|
| 44 |
+
# 把中文逗號/頓號已轉成 ,;允許拿來分割「等式 + 條件」
|
| 45 |
return s
|
| 46 |
|
| 47 |
+
# d/dx(...) → diff(..., x)
|
| 48 |
+
_DDX_RE = re.compile(r"d\s*/\s*d([A-Za-z_]\w*)\s*\(\s*(.+)\s*\)\s*$")
|
| 49 |
+
|
| 50 |
+
def desugar_ddx(s: str) -> str:
|
| 51 |
+
m = _DDX_RE.match(s.strip())
|
| 52 |
+
if not m:
|
| 53 |
+
return s
|
| 54 |
+
var = m.group(1)
|
| 55 |
+
inside = m.group(2)
|
| 56 |
+
return f"diff(({inside}), {var})"
|
| 57 |
+
|
| 58 |
+
# 基本轉換:隱式乘法、^ 號
|
| 59 |
TRANS = standard_transformations + (implicit_multiplication_application, convert_xor)
|
| 60 |
|
| 61 |
+
def to_sympy_expr(raw: str):
|
| 62 |
+
s = normalize_ascii(raw)
|
| 63 |
+
s = desugar_ddx(s)
|
| 64 |
+
return parse_expr(s, transformations=TRANS)
|
| 65 |
|
| 66 |
+
def to_sympy_eq(raw: str):
|
| 67 |
+
s = normalize_ascii(raw)
|
| 68 |
+
# 只取第一個 "=" 左右,避免 a=b=c 之類字串誤用
|
| 69 |
if "=" not in s:
|
| 70 |
raise ValueError("等式缺少 '='")
|
| 71 |
L, R = s.split("=", 1)
|
| 72 |
return sp.Eq(parse_expr(L, transformations=TRANS),
|
| 73 |
parse_expr(R, transformations=TRANS))
|
| 74 |
|
| 75 |
+
# ----------------------------
|
| 76 |
+
# 2) 解析「逗號分隔的不等式條件」→ 定義域 (Interval/Set)
|
| 77 |
+
# 例: "0 <= theta < 2*pi" 或 "x>=0" 或 "x in [0,1]"
|
| 78 |
+
# ----------------------------
|
| 79 |
+
def parse_domain_chunks(chunks, var):
|
| 80 |
+
"""
|
| 81 |
+
將類似 "0 <= theta < 2*pi"、"theta<pi/3"、"x in [0,1]" 轉成 Set
|
| 82 |
+
以交集的方式逐一收斂 domain。
|
| 83 |
+
"""
|
| 84 |
+
domain = S.Reals
|
| 85 |
+
for c in chunks:
|
| 86 |
+
c = normalize_ascii(c.strip())
|
| 87 |
+
if not c:
|
| 88 |
+
continue
|
| 89 |
+
|
| 90 |
+
# 支援 x in [a,b] / (a,b) / [a,b) / (a,b]
|
| 91 |
+
m = re.match(rf"^{var}\s*in\s*([\[\(])\s*(.+)\s*,\s*(.+)\s*([\]\)])$", c)
|
| 92 |
+
if m:
|
| 93 |
+
left_open = m.group(1) == "("
|
| 94 |
+
right_open = m.group(4) == ")"
|
| 95 |
+
a = parse_expr(m.group(2), transformations=TRANS)
|
| 96 |
+
b = parse_expr(m.group(3), transformations=TRANS)
|
| 97 |
+
domain = domain.intersect(Interval(a, b, left_open, right_open))
|
| 98 |
+
continue
|
| 99 |
+
|
| 100 |
+
# 一般不等式/連鎖不等式:交給 solve_univariate_inequality
|
| 101 |
+
try:
|
| 102 |
+
ineq = parse_expr(c, transformations=TRANS, evaluate=False)
|
| 103 |
+
set_part = solve_univariate_inequality(ineq, var, relational=False)
|
| 104 |
+
domain = domain.intersect(set_part)
|
| 105 |
+
except Exception:
|
| 106 |
+
# 不可解析就保留原 domain,不報錯
|
| 107 |
+
pass
|
| 108 |
+
return domain
|
| 109 |
+
|
| 110 |
+
# ----------------------------
|
| 111 |
+
# 3) 輔助:檢查是否混雜太多非數學描述
|
| 112 |
+
# ----------------------------
|
| 113 |
+
_NON_MATH_RE = re.compile(r"[^\dA-Za-z_+\-*/^().,;=<>!&| \[\]{}:]+")
|
| 114 |
+
|
| 115 |
+
def looks_like_math(s: str) -> bool:
|
| 116 |
+
# 去除已允許的單字與函數名
|
| 117 |
+
tmp = s
|
| 118 |
+
for w in _ALLOWED_WORDS:
|
| 119 |
+
tmp = re.sub(rf"\b{w}\b", "", tmp)
|
| 120 |
+
# 若還剩大量非數學符號(尤其中文敘述),視為不適合直接解析
|
| 121 |
+
return not _NON_MATH_RE.search(tmp)
|
| 122 |
+
|
| 123 |
+
# ----------------------------
|
| 124 |
+
# 4) 主邏輯
|
| 125 |
+
# ----------------------------
|
| 126 |
def solve_math(q: str):
|
| 127 |
q = (q or "").strip()
|
| 128 |
if not q:
|
| 129 |
+
return (
|
| 130 |
+
"請輸入算式(可多行或用分號 ; 分隔)。\n"
|
| 131 |
+
"例:\n"
|
| 132 |
+
" 1) 2x + 5 = 11\n"
|
| 133 |
+
" 2) sin(theta) = sqrt(3)/2 , 0 <= theta < 2*pi\n"
|
| 134 |
+
" 3) factor(x^2 - 9x + 18)\n"
|
| 135 |
+
" 4) d/dx ((x^2+1)*(x-3))\n"
|
| 136 |
+
)
|
| 137 |
|
| 138 |
+
# 允許多行 or 分號
|
| 139 |
+
segs = [s for line in q.split("\n") for s in line.split(";")]
|
| 140 |
+
segs = [s.strip() for s in segs if s.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
+
# 只要有 '=' 視為求解方程(可聯立)
|
| 143 |
+
if any("=" in s for s in segs):
|
| 144 |
+
# 先把「每行的 equation 與 同行後面的不等式/條件」分開
|
| 145 |
+
equations = []
|
| 146 |
+
all_symbols = set()
|
| 147 |
+
per_eq_domains = []
|
| 148 |
+
|
| 149 |
+
for line in segs:
|
| 150 |
+
# 用逗號分出「主等式」與「條件」
|
| 151 |
+
parts = [p.strip() for p in line.split(",") if p.strip()]
|
| 152 |
+
if not parts:
|
| 153 |
+
continue
|
| 154 |
+
if "=" in parts[0]:
|
| 155 |
+
eq = to_sympy_eq(parts[0])
|
| 156 |
+
equations.append(eq)
|
| 157 |
+
all_symbols |= (eq.lhs.free_symbols | eq.rhs.free_symbols)
|
| 158 |
+
# 解析該行其餘不等式作為此等式變數的 domain
|
| 159 |
+
# 僅對「一元」等式給 domain(多元聯立先忽略 domain)
|
| 160 |
+
per_eq_domains.append(parts[1:] if len(parts) > 1 else [])
|
| 161 |
+
else:
|
| 162 |
+
# 這行不是等式,可能是額外條件,先忽略(或日後擴充)
|
| 163 |
+
pass
|
| 164 |
+
|
| 165 |
+
if not equations:
|
| 166 |
+
return "解析失敗:未偵測到等式。"
|
| 167 |
+
|
| 168 |
+
# 多元聯立 → 用 solve(無 domain)
|
| 169 |
+
# 一元等式(只有一個主要變數)→ 用 solveset + domain
|
| 170 |
+
# 嘗試偵測「主變數」
|
| 171 |
+
main_syms = set()
|
| 172 |
+
for eq in equations:
|
| 173 |
+
main_syms |= (eq.lhs.free_symbols | eq.rhs.free_symbols)
|
| 174 |
+
if len(main_syms) == 1:
|
| 175 |
+
var = next(iter(main_syms))
|
| 176 |
+
# 匯總所有行的 domain 條件(若有)
|
| 177 |
+
domain_chunks = []
|
| 178 |
+
for ch in per_eq_domains:
|
| 179 |
+
domain_chunks.extend(ch)
|
| 180 |
+
domain = parse_domain_chunks(domain_chunks, str(var))
|
| 181 |
+
solset = sp.solveset(sp.Eq(equations[0].lhs - equations[0].rhs, 0), var, domain=domain)
|
| 182 |
+
if solset is sp.EmptySet:
|
| 183 |
+
return "無解(考慮定義域後)。"
|
| 184 |
+
return f"解:{sp.simplify(solset)}"
|
| 185 |
+
else:
|
| 186 |
+
# 多元聯立:舊路徑(不支援 domain)
|
| 187 |
+
try:
|
| 188 |
+
sol = sp.solve(equations, list(main_syms), dict=True)
|
| 189 |
+
if not sol:
|
| 190 |
+
return "無解或需要更多條件。"
|
| 191 |
+
lines = []
|
| 192 |
+
for i, d in enumerate(sol, 1):
|
| 193 |
+
items = ", ".join([f"{k} = {sp.simplify(v)}" for k, v in d.items()])
|
| 194 |
+
lines.append(f"解 {i}: {items}")
|
| 195 |
+
return "\n".join(lines)
|
| 196 |
+
except Exception as e:
|
| 197 |
+
return f"解析失敗:{e}"
|
| 198 |
+
|
| 199 |
+
# 否則當作一般表達式:簡化 / 因式 / 微分 / 積分
|
| 200 |
+
expr_text = normalize_ascii(q)
|
| 201 |
+
if not looks_like_math(expr_text):
|
| 202 |
+
return "偵測到大量非數學描述,請改以純算式輸入(可用逗號加入條件)。\n例:sin(theta)=sqrt(3)/2 , 0<=theta<2*pi"
|
| 203 |
+
|
| 204 |
+
try:
|
| 205 |
+
expr = to_sympy_expr(expr_text)
|
| 206 |
except Exception as e:
|
| 207 |
return f"解析失敗:{e}"
|
| 208 |
|
| 209 |
+
out_lines = []
|
| 210 |
+
try:
|
| 211 |
+
out_lines.append(f"簡化:{sp.simplify(expr)}")
|
| 212 |
+
except Exception:
|
| 213 |
+
out_lines.append(f"簡化:{expr}")
|
| 214 |
+
|
| 215 |
+
try:
|
| 216 |
+
fact = sp.factor(expr)
|
| 217 |
+
if fact != expr:
|
| 218 |
+
out_lines.append(f"因式分解:{fact}")
|
| 219 |
+
except Exception:
|
| 220 |
+
pass
|
| 221 |
+
|
| 222 |
+
try:
|
| 223 |
+
x = next(iter(expr.free_symbols)) if expr.free_symbols else sp.symbols("x")
|
| 224 |
+
out_lines.append(f"對 {x} 微分:{sp.diff(expr, x)}")
|
| 225 |
+
out_lines.append(f"對 {x} 積分:{sp.integrate(expr, x)}")
|
| 226 |
+
except Exception:
|
| 227 |
+
pass
|
| 228 |
+
|
| 229 |
+
return "\n".join(out_lines) if out_lines else f"結果:{expr}"
|
| 230 |
+
|
| 231 |
+
# ----------------------------
|
| 232 |
+
# 5) UI
|
| 233 |
+
# ----------------------------
|
| 234 |
with gr.Blocks(title=TITLE) as demo:
|
| 235 |
gr.Markdown(
|
| 236 |
+
"## " + TITLE + "\n"
|
| 237 |
+
"貼上算式(可多行 / 分號 `;` 分隔)。支援隱式乘法(例:`3x`, `2(x+1)`, `(x+1)(x-1)`, `2sqrt(x)`)與 `^`。\n\n"
|
| 238 |
+
"**範例**:\n"
|
| 239 |
+
"- `2x + 5 = 11`\n"
|
| 240 |
+
"- `sin(theta) = sqrt(3)/2 , 0 <= theta < 2*pi`\n"
|
| 241 |
+
"- `factor(x^2 - 9x + 18)`\n"
|
| 242 |
+
"- `d/dx ((x^2+1)*(x-3))`\n"
|
| 243 |
+
"※ 若有定義域,請以逗號分隔放在等式後面;可用 `in [a,b]`、`0<=x<1` 等寫法。\n"
|
| 244 |
)
|
| 245 |
+
q = gr.Textbox(lines=6, label="題目 / 算式(可含聯立方程與條件)")
|
| 246 |
+
out = gr.Textbox(lines=14, label="輸出")
|
| 247 |
gr.Button("送出 🚀").click(fn=solve_math, inputs=q, outputs=out)
|
| 248 |
|
| 249 |
if __name__ == "__main__":
|
| 250 |
+
# Space 內會由 runner 啟動;本地測試可開啟
|
| 251 |
demo.launch()
|
| 252 |
+
|