Spaces:
Sleeping
Sleeping
| # 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() | |