aciang's picture
Update app.py
ee56013 verified
# app.py
# LanguageBridge — Math Fast Agent (SymPy) (2025-11-04 修正版)
import re
import gradio as gr
import sympy as sp
from sympy.parsing.sympy_parser import (
parse_expr, standard_transformations,
implicit_multiplication_application, convert_xor
)
from sympy import Interval, Union, S, And, Or
from sympy.solvers.inequalities import solve_univariate_inequality
TITLE = "LanguageBridge — Math Fast Agent (SymPy)"
# ----------------------------
# 1) 安全正規化:全形→半形、特殊符號、常見中文習慣
# ----------------------------
_ASCII_TABLE = str.maketrans({
# 括號與標點
"(": "(", ")": ")", ",": ",", "、": ",", ";": ";", ":": ":",
".": ".", "。": ".", ",": ",", "!": "!", "?": "?",
# 運算與關係
"+": "+", "-": "-", "~": "~", "=": "=", "*": "*", "/": "/",
"<": "<", ">": ">", "≤": "<=", "≥": ">=",
"∧": "&", "∨": "|",
# 乘號、根號
"×": "*", "√": "sqrt",
# 希臘字母/常數
"π": "pi", "Π": "pi",
"θ": "theta", "Θ": "theta",
})
# 允許保留的英文單字(其餘中文字會被視為非數學描述)
_ALLOWED_WORDS = {
"sin","cos","tan","cot","sec","csc",
"asin","acos","atan","sinh","cosh","tanh","log","ln","exp","sqrt","abs",
"diff","integrate","factor","simplify","expand"
}
def normalize_ascii(s: str) -> str:
s = (s or "").replace("\u00A0", " ").replace("\u3000", " ") # 不換行空白/全形空白
s = s.translate(_ASCII_TABLE)
s = s.replace("^", "**") # 2^3 -> 2**3
# 把中文逗號/頓號已轉成 ,;允許拿來分割「等式 + 條件」
return s
# d/dx(...) → diff(..., x)
_DDX_RE = re.compile(r"d\s*/\s*d([A-Za-z_]\w*)\s*\(\s*(.+)\s*\)\s*$")
def desugar_ddx(s: str) -> str:
m = _DDX_RE.match(s.strip())
if not m:
return s
var = m.group(1)
inside = m.group(2)
return f"diff(({inside}), {var})"
# 基本轉換:隱式乘法、^ 號
TRANS = standard_transformations + (implicit_multiplication_application, convert_xor)
def to_sympy_expr(raw: str):
s = normalize_ascii(raw)
s = desugar_ddx(s)
return parse_expr(s, transformations=TRANS)
def to_sympy_eq(raw: str):
s = normalize_ascii(raw)
# 只取第一個 "=" 左右,避免 a=b=c 之類字串誤用
if "=" not in s:
raise ValueError("等式缺少 '='")
L, R = s.split("=", 1)
return sp.Eq(parse_expr(L, transformations=TRANS),
parse_expr(R, transformations=TRANS))
# ----------------------------
# 2) 解析「逗號分隔的不等式條件」→ 定義域 (Interval/Set)
# 例: "0 <= theta < 2*pi" 或 "x>=0" 或 "x in [0,1]"
# ----------------------------
def parse_domain_chunks(chunks, var):
"""
將類似 "0 <= theta < 2*pi"、"theta<pi/3"、"x in [0,1]" 轉成 Set
以交集的方式逐一收斂 domain。
"""
domain = S.Reals
for c in chunks:
c = normalize_ascii(c.strip())
if not c:
continue
# 支援 x in [a,b] / (a,b) / [a,b) / (a,b]
m = re.match(rf"^{var}\s*in\s*([\[\(])\s*(.+)\s*,\s*(.+)\s*([\]\)])$", c)
if m:
left_open = m.group(1) == "("
right_open = m.group(4) == ")"
a = parse_expr(m.group(2), transformations=TRANS)
b = parse_expr(m.group(3), transformations=TRANS)
domain = domain.intersect(Interval(a, b, left_open, right_open))
continue
# 一般不等式/連鎖不等式:交給 solve_univariate_inequality
try:
ineq = parse_expr(c, transformations=TRANS, evaluate=False)
set_part = solve_univariate_inequality(ineq, var, relational=False)
domain = domain.intersect(set_part)
except Exception:
# 不可解析就保留原 domain,不報錯
pass
return domain
# ----------------------------
# 3) 輔助:檢查是否混雜太多非數學描述
# ----------------------------
_NON_MATH_RE = re.compile(r"[^\dA-Za-z_+\-*/^().,;=<>!&| \[\]{}:]+")
def looks_like_math(s: str) -> bool:
# 去除已允許的單字與函數名
tmp = s
for w in _ALLOWED_WORDS:
tmp = re.sub(rf"\b{w}\b", "", tmp)
# 若還剩大量非數學符號(尤其中文敘述),視為不適合直接解析
return not _NON_MATH_RE.search(tmp)
# ----------------------------
# 4) 主邏輯
# ----------------------------
def solve_math(q: str):
q = (q or "").strip()
if not q:
return (
"請輸入算式(可多行或用分號 ; 分隔)。\n"
"例:\n"
" 1) 2x + 5 = 11\n"
" 2) sin(theta) = sqrt(3)/2 , 0 <= theta < 2*pi\n"
" 3) factor(x^2 - 9x + 18)\n"
" 4) d/dx ((x^2+1)*(x-3))\n"
)
# 允許多行 or 分號
segs = [s for line in q.split("\n") for s in line.split(";")]
segs = [s.strip() for s in segs if s.strip()]
# 只要有 '=' 視為求解方程(可聯立)
if any("=" in s for s in segs):
# 先把「每行的 equation 與 同行後面的不等式/條件」分開
equations = []
all_symbols = set()
per_eq_domains = []
for line in segs:
# 用逗號分出「主等式」與「條件」
parts = [p.strip() for p in line.split(",") if p.strip()]
if not parts:
continue
if "=" in parts[0]:
eq = to_sympy_eq(parts[0])
equations.append(eq)
all_symbols |= (eq.lhs.free_symbols | eq.rhs.free_symbols)
# 解析該行其餘不等式作為此等式變數的 domain
# 僅對「一元」等式給 domain(多元聯立先忽略 domain)
per_eq_domains.append(parts[1:] if len(parts) > 1 else [])
else:
# 這行不是等式,可能是額外條件,先忽略(或日後擴充)
pass
if not equations:
return "解析失敗:未偵測到等式。"
# 多元聯立 → 用 solve(無 domain)
# 一元等式(只有一個主要變數)→ 用 solveset + domain
# 嘗試偵測「主變數」
main_syms = set()
for eq in equations:
main_syms |= (eq.lhs.free_symbols | eq.rhs.free_symbols)
if len(main_syms) == 1:
var = next(iter(main_syms))
# 匯總所有行的 domain 條件(若有)
domain_chunks = []
for ch in per_eq_domains:
domain_chunks.extend(ch)
domain = parse_domain_chunks(domain_chunks, str(var))
solset = sp.solveset(sp.Eq(equations[0].lhs - equations[0].rhs, 0), var, domain=domain)
if solset is sp.EmptySet:
return "無解(考慮定義域後)。"
return f"解:{sp.simplify(solset)}"
else:
# 多元聯立:舊路徑(不支援 domain)
try:
sol = sp.solve(equations, list(main_syms), dict=True)
if not sol:
return "無解或需要更多條件。"
lines = []
for i, d in enumerate(sol, 1):
items = ", ".join([f"{k} = {sp.simplify(v)}" for k, v in d.items()])
lines.append(f"解 {i}: {items}")
return "\n".join(lines)
except Exception as e:
return f"解析失敗:{e}"
# 否則當作一般表達式:簡化 / 因式 / 微分 / 積分
expr_text = normalize_ascii(q)
if not looks_like_math(expr_text):
return "偵測到大量非數學描述,請改以純算式輸入(可用逗號加入條件)。\n例:sin(theta)=sqrt(3)/2 , 0<=theta<2*pi"
try:
expr = to_sympy_expr(expr_text)
except Exception as e:
return f"解析失敗:{e}"
out_lines = []
try:
out_lines.append(f"簡化:{sp.simplify(expr)}")
except Exception:
out_lines.append(f"簡化:{expr}")
try:
fact = sp.factor(expr)
if fact != expr:
out_lines.append(f"因式分解:{fact}")
except Exception:
pass
try:
x = next(iter(expr.free_symbols)) if expr.free_symbols else sp.symbols("x")
out_lines.append(f"對 {x} 微分:{sp.diff(expr, x)}")
out_lines.append(f"對 {x} 積分:{sp.integrate(expr, x)}")
except Exception:
pass
return "\n".join(out_lines) if out_lines else f"結果:{expr}"
# ----------------------------
# 5) UI
# ----------------------------
with gr.Blocks(title=TITLE) as demo:
gr.Markdown(
"## " + TITLE + "\n"
"貼上算式(可多行 / 分號 `;` 分隔)。支援隱式乘法(例:`3x`, `2(x+1)`, `(x+1)(x-1)`, `2sqrt(x)`)與 `^`。\n\n"
"**範例**:\n"
"- `2x + 5 = 11`\n"
"- `sin(theta) = sqrt(3)/2 , 0 <= theta < 2*pi`\n"
"- `factor(x^2 - 9x + 18)`\n"
"- `d/dx ((x^2+1)*(x-3))`\n"
"※ 若有定義域,請以逗號分隔放在等式後面;可用 `in [a,b]`、`0<=x<1` 等寫法。\n"
)
q = gr.Textbox(lines=6, label="題目 / 算式(可含聯立方程與條件)")
out = gr.Textbox(lines=14, label="輸出")
gr.Button("送出 🚀").click(fn=solve_math, inputs=q, outputs=out)
if __name__ == "__main__":
# Space 內會由 runner 啟動;本地測試可開啟
demo.launch()