jackal79 commited on
Commit
ee57662
·
1 Parent(s): 6c2a8db

Update router + NL fallbacks

Browse files
Files changed (2) hide show
  1. app.py +371 -41
  2. requirements.txt +1 -1
app.py CHANGED
@@ -20,9 +20,19 @@ HF_TOKEN = os.environ.get("HF_TOKEN", "")
20
  _EXPR_RE = re.compile(r"^[0-9()+\-*/^= ]{1,200}$")
21
 
22
  PROMPT_SYSTEM = (
23
- "You are a math expression translator. Output ONLY JSON of the form {\"expr\":\"...\"}. "
24
- "Use integers, parentheses, and + - * / ^. No words, no units, no variables. "
25
- "Keep exact integers (no decimals) unless explicitly in the prompt."
 
 
 
 
 
 
 
 
 
 
26
  )
27
 
28
  def _sign_headers(body: bytes) -> dict:
@@ -48,8 +58,8 @@ def _call_qwen(prompt: str) -> str:
48
  timeout=20,
49
  )
50
  r.raise_for_status()
51
- data = r.json()
52
- return data.get("expr", "")
53
  except Exception:
54
  return ""
55
  # Fallback: HF Inference text generation if model+token given
@@ -71,12 +81,7 @@ def _call_qwen(prompt: str) -> str:
71
  text = out[0].get("generated_text", "") or out[0].get("summary_text", "")
72
  if not text:
73
  text = json.dumps(out)
74
- try:
75
- j = json.loads(text)
76
- return str(j.get("expr", ""))
77
- except Exception:
78
- m = re.search(r"\{\s*\"expr\"\s*:\s*\"([^\"]+)\"\s*\}", text)
79
- return m.group(1) if m else ""
80
  except Exception:
81
  return ""
82
  return ""
@@ -85,53 +90,378 @@ def _call_qwen(prompt: str) -> str:
85
  def solve_chat(message: str, history: list[list[str]]):
86
  if not PAT_API_URL or not PAT_API_KEY:
87
  return "API not configured"
88
- # Helper: normalize simple command forms like: simplify "expr" or simplify expr
 
 
 
 
 
 
 
 
 
89
  def _norm_expr(msg: str) -> str:
90
  s = msg.strip()
91
  try:
92
- import re as _re
93
- m = _re.search(r'"([^"]+)"', s)
94
  if m:
95
- return m.group(1)
96
- m = _re.match(r'^(?:simplify|expand)\s+(.+)$', s, _re.I)
97
  if m:
98
- return m.group(1)
99
  except Exception:
100
  pass
101
- return s
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  # If the user input contains variables (letters), route to symbolic simplify
104
  if any(c.isalpha() for c in message):
 
 
 
 
 
105
  try:
106
  expr = _norm_expr(message)
107
- body = json.dumps({"mode": "simplify", "expr": expr}).encode("utf-8")
108
- r = requests.post(f"{PAT_API_URL}/v1/symbolic", headers=_sign_headers(body), data=body, timeout=30)
109
- r.raise_for_status()
110
- data = r.json()
111
- expr_out = data.get("expr") or data.get("value") or data
112
  return expr_out if isinstance(expr_out, str) else json.dumps(expr_out)
113
  except Exception:
114
  # fall through to numeric path attempt
115
  pass
116
- # 1) Parse to numeric expression via Qwen (or pass-through if already numeric)
117
- expr = message.strip()
118
- if not _EXPR_RE.fullmatch(expr.rstrip("=")):
119
- expr = _call_qwen(message).strip()
120
- if not expr:
121
- return "Could not parse the question into a numeric expression."
122
- expr = expr.rstrip("=")
123
- if not _EXPR_RE.fullmatch(expr):
124
- return f"Parsed expression rejected: {expr}"
125
- # 2) Forward to Modulus numeric solver
126
- body = json.dumps({"question": expr, "options": {"certificates": True}}).encode("utf-8")
127
  try:
128
- r = requests.post(f"{PAT_API_URL}/v1/solve", headers=_sign_headers(body), data=body, timeout=30)
129
- r.raise_for_status()
130
- data = r.json()
131
- ans = data.get("answer", "<no answer>")
132
- return ans
133
- except Exception as e:
134
- return f"error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  chat = gr.ChatInterface(
137
  fn=solve_chat,
 
20
  _EXPR_RE = re.compile(r"^[0-9()+\-*/^= ]{1,200}$")
21
 
22
  PROMPT_SYSTEM = (
23
+ "You are Modulus' NL parser. Return ONLY compact JSON with an 'intent' and fields.\n"
24
+ "Supported intents: simplify, expand, eval, factor, check_equal, recover, solve, logic, code, ode, pde.\n"
25
+ "Schemas:\n"
26
+ "- simplify|expand|eval: {intent, expr, subs?} where expr uses + - * / ^, integers, single-letter vars; subs is {var:int}.\n"
27
+ "- factor: {intent, poly} where poly is textual polynomial in one var.\n"
28
+ "- check_equal: {intent, left, right}.\n"
29
+ "- recover: {intent, samples:[{x: string, y: string}], kind:'rational'|'poly', hint?}.\n"
30
+ "- solve: {intent, expr} for numeric arithmetic.\n"
31
+ "- logic: {intent, expr, compare?}.\n"
32
+ "- code: {intent, expr_a, expr_b, vars?, bound?}.\n"
33
+ "- ode: {intent, A, x0, dt, steps}.\n"
34
+ "- pde: {intent, kind, N}.\n"
35
+ "Do not add commentary. If unsure, return {intent:'simplify', expr:'...'} with your best guess."
36
  )
37
 
38
  def _sign_headers(body: bytes) -> dict:
 
58
  timeout=20,
59
  )
60
  r.raise_for_status()
61
+ data = r.json()
62
+ return json.dumps(data)
63
  except Exception:
64
  return ""
65
  # Fallback: HF Inference text generation if model+token given
 
81
  text = out[0].get("generated_text", "") or out[0].get("summary_text", "")
82
  if not text:
83
  text = json.dumps(out)
84
+ return text
 
 
 
 
 
85
  except Exception:
86
  return ""
87
  return ""
 
90
  def solve_chat(message: str, history: list[list[str]]):
91
  if not PAT_API_URL or not PAT_API_KEY:
92
  return "API not configured"
93
+ # Helpers
94
+ def _insert_implicit_mul(expr: str) -> str:
95
+ # Insert * between adjacent tokens like 2x, )(, x(, )x, xx, x2
96
+ e = expr
97
+ e = re.sub(r'(\d|\))\(', r'\1*(', e)
98
+ e = re.sub(r'(\d|\))([a-zA-Z])', r'\1*\2', e)
99
+ e = re.sub(r'([a-zA-Z]|\))(\d)', r'\1*\2', e)
100
+ e = re.sub(r'([a-zA-Z]|\))([a-zA-Z])', r'\1*\2', e)
101
+ return e
102
+
103
  def _norm_expr(msg: str) -> str:
104
  s = msg.strip()
105
  try:
106
+ m = re.search(r'"([^"]+)"', s)
 
107
  if m:
108
+ return _insert_implicit_mul(m.group(1))
109
+ m = re.match(r'^(?:simplify|expand)\s*:??\s+(.+)$', s, re.I)
110
  if m:
111
+ return _insert_implicit_mul(m.group(1))
112
  except Exception:
113
  pass
114
+ return _insert_implicit_mul(s)
115
+
116
+ def _http_json_post(url: str, body: dict) -> dict:
117
+ data = json.dumps(body).encode("utf-8")
118
+ r = requests.post(url, headers=_sign_headers(data), data=data, timeout=30)
119
+ try:
120
+ r.raise_for_status()
121
+ except requests.RequestException as e:
122
+ try:
123
+ j = r.json()
124
+ return {"error": j}
125
+ except Exception:
126
+ return {"error": str(e)}
127
+ return r.json()
128
+
129
+ def _poly_from_str(poly: str) -> list[str] | None:
130
+ # Parse a polynomial in one variable (assumed 'x') into integer coeffs (low→high)
131
+ # Supports + - * ^ and parentheses; no division here.
132
+ s = _insert_implicit_mul(poly.replace(' ', ''))
133
+ # tokenizer
134
+ pos = 0
135
+ n = len(s)
136
+ def peek():
137
+ return s[pos] if pos < n else ''
138
+ def consume(ch=None):
139
+ nonlocal pos
140
+ if ch and peek() != ch:
141
+ return False
142
+ pos += 1
143
+ return True
144
+ # poly ops
145
+ def poly_const(c: int):
146
+ return {0: c}
147
+ def poly_var():
148
+ return {1: 1}
149
+ def poly_add(a: dict, b: dict):
150
+ r = dict(a)
151
+ for k,v in b.items():
152
+ r[k] = r.get(k,0) + v
153
+ return {k:v for k,v in r.items() if v != 0}
154
+ def poly_mul(a: dict, b: dict):
155
+ r: dict[int,int] = {}
156
+ for da,va in a.items():
157
+ for db,vb in b.items():
158
+ r[da+db] = r.get(da+db,0) + va*vb
159
+ return {k:v for k,v in r.items() if v != 0}
160
+ def poly_pow(a: dict, m: int):
161
+ res = {0:1}
162
+ p = dict(a)
163
+ mm = int(m)
164
+ if mm < 0:
165
+ return None
166
+ while mm > 0:
167
+ if mm & 1:
168
+ res = poly_mul(res, p)
169
+ p = poly_mul(p, p)
170
+ mm >>= 1
171
+ return res
172
+ # grammar: expr := term (('+'|'-') term)* ; term := factor (('*') factor)* ; factor := number | 'x' ('^' number)? | '(' expr ')' ('^' number)?
173
+ def parse_number():
174
+ nonlocal pos
175
+ start = pos
176
+ if peek() in '+-':
177
+ pos += 1
178
+ while pos < n and s[pos].isdigit():
179
+ pos += 1
180
+ if start == pos or (s[start] in '+-' and pos == start+1):
181
+ return None
182
+ return int(s[start:pos])
183
+ def parse_factor():
184
+ nonlocal pos
185
+ if peek().isdigit() or peek() in '+-':
186
+ c = parse_number()
187
+ return poly_const(int(c)) if c is not None else None
188
+ if peek() == 'x':
189
+ consume('x')
190
+ base = poly_var()
191
+ if peek() == '^':
192
+ consume('^')
193
+ exp = parse_number()
194
+ if exp is None:
195
+ return None
196
+ return poly_pow(base, int(exp))
197
+ return base
198
+ if peek() == '(':
199
+ consume('(')
200
+ a = parse_expr()
201
+ if not consume(')'):
202
+ return None
203
+ if peek() == '^':
204
+ consume('^')
205
+ exp = parse_number()
206
+ if exp is None:
207
+ return None
208
+ return poly_pow(a, int(exp))
209
+ return a
210
+ return None
211
+ def parse_term():
212
+ v = parse_factor()
213
+ while True:
214
+ if peek() == '*':
215
+ consume('*')
216
+ w = parse_factor()
217
+ if w is None:
218
+ return None
219
+ v = poly_mul(v, w)
220
+ else:
221
+ break
222
+ return v
223
+ def parse_expr():
224
+ v = parse_term()
225
+ while True:
226
+ if peek() == '+':
227
+ consume('+')
228
+ w = parse_term()
229
+ if w is None:
230
+ return None
231
+ v = poly_add(v, w)
232
+ elif peek() == '-':
233
+ consume('-')
234
+ w = parse_term()
235
+ if w is None:
236
+ return None
237
+ v = poly_add(v, poly_mul(w, {0:-1}))
238
+ else:
239
+ break
240
+ return v
241
+ poly_map = parse_expr()
242
+ if poly_map is None:
243
+ return None
244
+ deg_max = max(poly_map.keys()) if poly_map else 0
245
+ coeffs = [str(poly_map.get(d, 0)) for d in range(0, deg_max+1)]
246
+ return coeffs
247
+
248
+ def _dispatch(plan: dict) -> str:
249
+ intent = (plan.get('intent') or '').lower()
250
+ if intent in ('simplify','expand','eval'):
251
+ expr = _norm_expr(plan.get('expr',''))
252
+ mode = 'simplify' if intent in ('simplify','expand') else 'eval'
253
+ body = {"mode": mode, "expr": expr}
254
+ if mode == 'eval' and isinstance(plan.get('subs'), dict):
255
+ body['subs'] = {k: int(str(v)) for k,v in plan['subs'].items() if re.fullmatch(r"[a-z]", k)}
256
+ resp = _http_json_post(f"{PAT_API_URL}/v1/symbolic", body)
257
+ return json.dumps(resp) if isinstance(resp, dict) else str(resp)
258
+ if intent == 'factor':
259
+ poly = _insert_implicit_mul(plan.get('poly',''))
260
+ coeffs = _try_poly_coeffs(poly)
261
+ if coeffs:
262
+ resp = _http_json_post(f"{PAT_API_URL}/v1/factor", {"coeffs": coeffs})
263
+ return json.dumps(resp)
264
+ resp = _http_json_post(f"{PAT_API_URL}/v1/symbolic", {"mode":"simplify","expr": poly})
265
+ return json.dumps(resp)
266
+ if intent == 'check_equal':
267
+ left = _insert_implicit_mul(plan.get('left',''))
268
+ right = _insert_implicit_mul(plan.get('right',''))
269
+ resp = _http_json_post(f"{PAT_API_URL}/v1/symbolic", {"mode":"check_equal","expr":"","left":left,"right":right,"certify_points":["-2","-1","0","1","2","3"]})
270
+ return json.dumps(resp)
271
+ if intent == 'recover':
272
+ kind = (plan.get('kind') or 'rational').lower()
273
+ samples = plan.get('samples') or []
274
+ if not samples:
275
+ return "No samples provided"
276
+ body = {"mode": 'poly' if kind=='poly' else 'rational', "var": "x", "samples": samples, "certify": True}
277
+ resp = _http_json_post(f"{PAT_API_URL}/v1/recover", body)
278
+ return json.dumps(resp)
279
+ if intent == 'logic':
280
+ resp = _http_json_post(f"{PAT_API_URL}/v1/logic_simplify", {"expr": plan.get('expr',''), "compare": plan.get('compare')})
281
+ return json.dumps(resp)
282
+ if intent == 'code':
283
+ resp = _http_json_post(f"{PAT_API_URL}/v1/code_equiv", {
284
+ "expr_a": plan.get('expr_a',''),
285
+ "expr_b": plan.get('expr_b',''),
286
+ "vars": plan.get('vars') or ['x'],
287
+ "bound": int(plan.get('bound') or 5),
288
+ })
289
+ return json.dumps(resp)
290
+ if intent == 'ode':
291
+ resp = _http_json_post(f"{PAT_API_URL}/v1/ode_solve", {
292
+ "A": plan.get('A'),
293
+ "x0": plan.get('x0'),
294
+ "dt": plan.get('dt'),
295
+ "steps": int(plan.get('steps') or 1),
296
+ })
297
+ return json.dumps(resp)
298
+ if intent == 'pde':
299
+ resp = _http_json_post(f"{PAT_API_URL}/v1/pde_solve", {
300
+ "kind": plan.get('kind') or 'poisson_1d',
301
+ "N": int(plan.get('N') or 8),
302
+ })
303
+ return json.dumps(resp)
304
+ if intent == 'solve':
305
+ expr = (plan.get('expr') or '').strip()
306
+ if not _EXPR_RE.fullmatch(expr.rstrip('=')):
307
+ return "Could not parse the question into a numeric expression."
308
+ body = {"question": expr.rstrip('='), "options": {"certificates": True}}
309
+ resp = _http_json_post(f"{PAT_API_URL}/v1/solve", body)
310
+ return str(resp.get('answer')) if isinstance(resp, dict) else str(resp)
311
+ return "Unknown intent"
312
+
313
+ def _try_poly_coeffs(expr: str) -> list[str] | None:
314
+ # Very simple monomial parser for one variable polynomials (e.g., x^4+5x^3+6x^2-7)
315
+ s = expr.replace(' ', '')
316
+ if any(ch in s for ch in '()/'): # too complex for this quick path
317
+ return None
318
+ var = None
319
+ pos = 0
320
+ n = len(s)
321
+ deg_to_coeff: dict[int, int] = {}
322
+ while pos < n:
323
+ # sign
324
+ sign = 1
325
+ if s[pos] in '+-':
326
+ sign = -1 if s[pos] == '-' else 1
327
+ pos += 1
328
+ # coefficient
329
+ start = pos
330
+ while pos < n and s[pos].isdigit():
331
+ pos += 1
332
+ coeff = int(s[start:pos] or '1')
333
+ # optional *
334
+ if pos < n and s[pos] == '*':
335
+ pos += 1
336
+ # variable
337
+ exp = 0
338
+ if pos < n and s[pos].isalpha():
339
+ v = s[pos]
340
+ if var is None:
341
+ var = v
342
+ elif var != v:
343
+ return None
344
+ pos += 1
345
+ exp = 1
346
+ # power
347
+ if pos < n and s[pos] == '^':
348
+ pos += 1
349
+ start = pos
350
+ while pos < n and s[pos].isdigit():
351
+ pos += 1
352
+ if start == pos:
353
+ return None
354
+ exp = int(s[start:pos])
355
+ # record term
356
+ deg_to_coeff[exp] = deg_to_coeff.get(exp, 0) + sign * coeff
357
+ # next must be + or - or end
358
+ if pos < n and s[pos] not in '+-':
359
+ return None
360
+ # build ascending coeff list
361
+ if not deg_to_coeff:
362
+ return None
363
+ max_deg = max(deg_to_coeff.keys())
364
+ coeffs = [str(deg_to_coeff.get(d, 0)) for d in range(0, max_deg + 1)]
365
+ return coeffs
366
+
367
+ # Command routing: expand/simplify, factor, equality, recover
368
+ msg = message.strip()
369
+ # Equality style: "check_equal: A = B" or "A = B"
370
+ if re.match(r'^(check_equal|equal|equiv|prove)\s*:?', msg, re.I) or re.search(r'(==|=|≟|\?=|≡)', msg):
371
+ # Extract payload after optional command prefix
372
+ payload = msg
373
+ mcmd = re.match(r'^(check_equal|equal|equiv|prove)\s*:??\s*(.+)$', msg, re.I)
374
+ if mcmd:
375
+ payload = mcmd.group(2)
376
+ parts = re.split(r'\s*(?:==|=|≟|\?=|≡)\s*', payload, maxsplit=1)
377
+ if len(parts) == 2:
378
+ left = _insert_implicit_mul(parts[0])
379
+ right = _insert_implicit_mul(parts[1])
380
+ resp = _http_json_post(f"{PAT_API_URL}/v1/symbolic", {"mode": "check_equal", "expr": "", "left": left, "right": right})
381
+ if "error" in resp:
382
+ return json.dumps(resp["error"]) if isinstance(resp["error"], dict) else str(resp["error"])
383
+ ok = resp.get("equal")
384
+ return f"equal: {ok}"
385
+ # fall through to normal handling if split failed
386
+
387
+ # Factor: "factor: expr"
388
+ m = re.match(r'^(factor)\s*:??\s+(.+)$', msg, re.I)
389
+ if m:
390
+ expr = _insert_implicit_mul(m.group(2))
391
+ coeffs = _try_poly_coeffs(expr)
392
+ if coeffs:
393
+ resp = _http_json_post(f"{PAT_API_URL}/v1/factor", {"coeffs": coeffs})
394
+ return json.dumps(resp)
395
+ # fallback to symbolic simplify
396
+ body = {"mode": "simplify", "expr": expr}
397
+ resp = _http_json_post(f"{PAT_API_URL}/v1/symbolic", body)
398
+ if "error" in resp:
399
+ return json.dumps(resp["error"]) if isinstance(resp["error"], dict) else str(resp["error"])
400
+ return resp.get("expr") or resp.get("value") or json.dumps(resp)
401
+
402
+ # Recover: "recover: (1->3/2), (2->4/3) ..."
403
+ m = re.match(r'^recover\s*:??\s+(.+)$', msg, re.I)
404
+ if m:
405
+ payload = m.group(1)
406
+ samples = []
407
+ for mm in re.finditer(r'[-\d]+\s*(?:->|→)\s*[-\d]+(?:/\d+)?', payload):
408
+ pair = mm.group(0)
409
+ lhs, rhs = re.split(r'\s*(?:->|→)\s*', pair)
410
+ samples.append({"x": lhs.strip(), "y": rhs.strip()})
411
+ if samples:
412
+ resp = _http_json_post(f"{PAT_API_URL}/v1/recover", {"mode": "rational", "var": "x", "samples": samples, "certify": True})
413
+ return json.dumps(resp)
414
+ return "Could not parse samples for recover. Use like: recover: (1->3/2), (2->4/3)"
415
 
416
  # If the user input contains variables (letters), route to symbolic simplify
417
  if any(c.isalpha() for c in message):
418
+ # If the message contains alphabetic words (>=2 letters in a row), prefer NL parser instead of raw symbolic
419
+ if re.search(r"[A-Za-z]{2,}", message):
420
+ # skip direct symbolic path; fall through to NL plan parsing below
421
+ pass
422
+ else:
423
  try:
424
  expr = _norm_expr(message)
425
+ resp = _http_json_post(f"{PAT_API_URL}/v1/symbolic", {"mode": "simplify", "expr": expr})
426
+ if "error" in resp:
427
+ return json.dumps(resp["error"]) if isinstance(resp["error"], dict) else str(resp["error"])
428
+ expr_out = resp.get("expr") or resp.get("value") or resp
 
429
  return expr_out if isinstance(expr_out, str) else json.dumps(expr_out)
430
  except Exception:
431
  # fall through to numeric path attempt
432
  pass
433
+ # 1) Ask Qwen for a structured plan
434
+ plan_text = _call_qwen(message)
 
 
 
 
 
 
 
 
 
435
  try:
436
+ plan = json.loads(plan_text) if plan_text else {}
437
+ except Exception:
438
+ plan = {}
439
+ # 1a) Heuristic fallback for common NL phrases when no Qwen plan is available
440
+ if not plan:
441
+ m_pf = re.search(r"partial\s+fractions?:\s*(.+)$", message, re.I)
442
+ if not m_pf:
443
+ m_pf = re.search(r"break\s+this\s+rational.*?:\s*(.+)$", message, re.I)
444
+ if m_pf:
445
+ frac = m_pf.group(1).strip()
446
+ m = re.match(r"^\((.+)\)\s*/\s*\((.+)\)\s*$", frac)
447
+ if not m:
448
+ m = re.match(r"^([^/]+)\s*/\s*([^/]+)$", frac)
449
+ if m:
450
+ P_str, Q_str = m.group(1), m.group(2)
451
+ P_coeffs = _poly_from_str(P_str)
452
+ Q_coeffs = _poly_from_str(Q_str)
453
+ if P_coeffs and Q_coeffs:
454
+ resp = _http_json_post(f"{PAT_API_URL}/v1/partial_fractions", {"P": P_coeffs, "Q": Q_coeffs})
455
+ return json.dumps(resp)
456
+ # Fallbacks: numeric-only quick path
457
+ if not plan:
458
+ expr = message.strip()
459
+ if _EXPR_RE.fullmatch(expr.rstrip('=')):
460
+ plan = {"intent": "solve", "expr": expr}
461
+ # Final guard
462
+ if not plan:
463
+ return "Could not parse. Try a shorter prompt with explicit math."
464
+ return _dispatch(plan)
465
 
466
  chat = gr.ChatInterface(
467
  fn=solve_chat,
requirements.txt CHANGED
@@ -1,7 +1,7 @@
1
  gradio==5.46.1
2
  gradio-client==1.13.1
3
  requests==2.32.3
4
- websockets>=15.0.1
5
  huggingface-hub>=0.33.5,<1.0
6
  pydantic==2.10.6
7
 
 
1
  gradio==5.46.1
2
  gradio-client==1.13.1
3
  requests==2.32.3
4
+ websockets==12.0
5
  huggingface-hub>=0.33.5,<1.0
6
  pydantic==2.10.6
7