# 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!&| \[\]{}:]+") 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()