aciang commited on
Commit
ee56013
·
verified ·
1 Parent(s): 0531f23

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -61
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
- # ---- 僅做安全正規化:全形→半形、^→**、√→sqrt ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def normalize_ascii(s: str) -> str:
14
- table = str.maketrans({
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
- # 不再自動補 *,避免把 sin(x) 變成 sin*(x)
26
  return s
27
 
28
- # SymPy 解析:啟用隱式乘法與 ^ 號處理
 
 
 
 
 
 
 
 
 
 
 
29
  TRANS = standard_transformations + (implicit_multiplication_application, convert_xor)
30
 
31
- def to_sympy_expr(s: str):
32
- return parse_expr(preprocess(s), transformations=TRANS)
 
 
33
 
34
- def to_sympy_eq(s: str):
35
- s = preprocess(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 "請輸入算式或方程,例如:3x+7=1;sin(x)^2+cos(x)^2;factor(x^4-1)"
 
 
 
 
 
 
 
46
 
47
- try:
48
- # 多行 / 分號;分隔
49
- parts = [s.strip() for seg in q.split(";") for s in seg.split("\n")]
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 + "\\n"
92
- "貼上算式(可多行 / 分號 `;` 分隔)。支援隱式乘法(例:`3x`, `2(x+1)`, `(x+1)(x-1)`, `2sqrt(x)`)與 `^`。"
 
 
 
 
 
 
93
  )
94
- q = gr.Textbox(lines=6, label="題目 / 算式(可含聯立方程)")
95
- out = gr.Textbox(lines=12, label="輸出")
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
+