saad-sust commited on
Commit
b14e24b
ยท
verified ยท
1 Parent(s): ce35390

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -178
app.py CHANGED
@@ -1,9 +1,20 @@
 
1
  import streamlit as st
2
  import sympy as sp
3
  import anthropic
4
- import re
5
- from sympy import *
6
- from sympy.parsing.sympy_parser import parse_expr, standard_transformations, implicit_multiplication_application
 
 
 
 
 
 
 
 
 
 
7
 
8
  # โ”€โ”€โ”€ Page Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
9
  st.set_page_config(
@@ -37,11 +48,9 @@ html, body, [class*="css"] {
37
  font-family: 'Nunito', sans-serif;
38
  }
39
 
40
- /* โ”€โ”€ Hide Streamlit chrome โ”€โ”€ */
41
  #MainMenu, footer, header { visibility: hidden; }
42
  .block-container { padding: 1.5rem 2rem 2rem 2rem; max-width: 1100px; }
43
 
44
- /* โ”€โ”€ Hero Banner โ”€โ”€ */
45
  .hero {
46
  background: linear-gradient(135deg, #0d1526 0%, #0f1f40 50%, #091428 100%);
47
  border: 1px solid var(--border);
@@ -87,7 +96,6 @@ html, body, [class*="css"] {
87
  letter-spacing: 0.5px;
88
  }
89
 
90
- /* โ”€โ”€ Chat Container โ”€โ”€ */
91
  .chat-area {
92
  background: var(--surface);
93
  border: 1px solid var(--border);
@@ -99,7 +107,6 @@ html, body, [class*="css"] {
99
  margin-bottom: 1rem;
100
  }
101
 
102
- /* โ”€โ”€ Message Bubbles โ”€โ”€ */
103
  .msg-user {
104
  display: flex;
105
  justify-content: flex-end;
@@ -142,25 +149,6 @@ html, body, [class*="css"] {
142
  line-height: 1.7;
143
  }
144
 
145
- /* โ”€โ”€ Step Block โ”€โ”€ */
146
- .step-block {
147
- background: rgba(56,189,248,0.04);
148
- border-left: 3px solid var(--accent);
149
- border-radius: 0 8px 8px 0;
150
- padding: 0.6rem 1rem;
151
- margin: 0.6rem 0;
152
- font-family: 'JetBrains Mono', monospace;
153
- font-size: 0.85rem;
154
- }
155
- .step-label {
156
- color: var(--accent);
157
- font-size: 0.72rem;
158
- text-transform: uppercase;
159
- letter-spacing: 1px;
160
- margin-bottom: 0.2rem;
161
- }
162
-
163
- /* โ”€โ”€ Input Row โ”€โ”€ */
164
  .stTextInput > div > div > input {
165
  background: var(--card) !important;
166
  border: 1px solid var(--border) !important;
@@ -175,7 +163,6 @@ html, body, [class*="css"] {
175
  box-shadow: 0 0 0 2px rgba(56,189,248,0.15) !important;
176
  }
177
 
178
- /* โ”€โ”€ Buttons โ”€โ”€ */
179
  .stButton > button {
180
  background: linear-gradient(135deg, #1e4080, #1a3060) !important;
181
  border: 1px solid var(--accent) !important;
@@ -193,7 +180,6 @@ html, body, [class*="css"] {
193
  box-shadow: 0 4px 15px rgba(56,189,248,0.2) !important;
194
  }
195
 
196
- /* โ”€โ”€ Sidebar โ”€โ”€ */
197
  [data-testid="stSidebar"] {
198
  background: var(--surface) !important;
199
  border-right: 1px solid var(--border) !important;
@@ -204,7 +190,6 @@ html, body, [class*="css"] {
204
  letter-spacing: 1px;
205
  }
206
 
207
- /* โ”€โ”€ Topic Pills โ”€โ”€ */
208
  .topic-pill {
209
  background: rgba(129,140,248,0.08);
210
  border: 1px solid rgba(129,140,248,0.2);
@@ -214,26 +199,14 @@ html, body, [class*="css"] {
214
  font-size: 0.78rem;
215
  margin: 3px 2px;
216
  display: inline-block;
217
- cursor: pointer;
218
  font-family: 'JetBrains Mono', monospace;
219
  }
220
- .topic-pill:hover {
221
- background: rgba(129,140,248,0.18);
222
- }
223
 
224
- /* โ”€โ”€ Scrollbar โ”€โ”€ */
225
  ::-webkit-scrollbar { width: 6px; }
226
  ::-webkit-scrollbar-track { background: var(--surface); }
227
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
228
  ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
229
 
230
- /* โ”€โ”€ Latex rendering โ”€โ”€ */
231
- .katex { font-size: 1.05em !important; }
232
-
233
- /* โ”€โ”€ Spinner override โ”€โ”€ */
234
- .stSpinner > div { border-top-color: var(--accent) !important; }
235
-
236
- /* โ”€โ”€ Selectbox โ”€โ”€ */
237
  .stSelectbox > div > div {
238
  background: var(--card) !important;
239
  border-color: var(--border) !important;
@@ -246,148 +219,130 @@ html, body, [class*="css"] {
246
  # โ”€โ”€โ”€ Session State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
247
  if "messages" not in st.session_state:
248
  st.session_state.messages = []
249
- if "sympy_results" not in st.session_state:
250
- st.session_state.sympy_results = {}
251
 
252
  # โ”€โ”€โ”€ SymPy Engine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
253
  def run_sympy(problem: str) -> dict:
254
  """
255
- Attempt to extract a verifiable symbolic computation from the problem.
256
- Returns a dict with keys: type, result, latex, error
257
  """
258
  p = problem.lower().strip()
259
- x, y, z, t, n = symbols('x y z t n')
260
- results = {}
261
 
262
  try:
263
- transformations = standard_transformations + (implicit_multiplication_application,)
264
 
265
- # --- Derivative ---
 
 
 
 
 
 
266
  if any(k in p for k in ["derivative", "differentiate", "d/dx", "diff"]):
267
- # Extract expression after keywords
268
  for kw in ["derivative of", "differentiate", "diff of", "d/dx of", "d/dx"]:
269
  if kw in p:
270
- raw = p.split(kw)[-1].strip()
271
  break
272
- else:
273
- raw = re.sub(r"(derivative|differentiate)", "", p).strip()
274
- raw = re.sub(r"\s+", "", raw)
275
- expr = parse_expr(raw, transformations=transformations)
276
- result = diff(expr, x)
277
- results = {"type": "Derivative", "result": str(result), "latex": latex(result)}
278
-
279
- # --- Integral ---
280
- elif any(k in p for k in ["integral", "integrate", "antiderivative", "โˆซ"]):
281
- for kw in ["integral of", "integrate", "antiderivative of", "โˆซ"]:
282
  if kw in p:
283
- raw = p.split(kw)[-1].strip()
284
  break
285
- else:
286
- raw = re.sub(r"(integral|integrate|antiderivative)", "", p).strip()
287
- raw = re.sub(r"\s+", "", raw)
288
- expr = parse_expr(raw, transformations=transformations)
289
- result = integrate(expr, x)
290
- results = {"type": "Integral", "result": str(result), "latex": latex(result)}
291
 
292
- # --- Limit ---
293
  elif "limit" in p:
294
- # e.g. "limit of sin(x)/x as x->0"
295
- match = re.search(r"([\w\(\)\+\-\*/\^\s]+)\s+as\s+x\s*[->โ†’]+\s*([\w\.\-\+]+)", p)
 
 
296
  if match:
297
- raw_expr = re.sub(r"\^", "**", match.group(1).strip())
298
- raw_expr = re.sub(r"\s+", "", raw_expr)
299
  point_str = match.group(2).strip()
300
- point = sympify(point_str) if point_str != "inf" else oo
301
- expr = parse_expr(raw_expr, transformations=transformations)
302
- result = limit(expr, x, point)
303
- results = {"type": "Limit", "result": str(result), "latex": latex(result)}
304
-
305
- # --- Solve equation ---
306
- elif any(k in p for k in ["solve", "roots", "find x", "find n"]):
307
- raw = re.sub(r"(solve|find x|find n|roots of|roots)", "", p).strip()
308
- # handle "= 0" or "=" in expression
309
  if "=" in raw:
310
- lhs, rhs = raw.split("=", 1)
311
- lhs_e = parse_expr(re.sub(r"\s+", "", lhs), transformations=transformations)
312
- rhs_e = parse_expr(re.sub(r"\s+", "", rhs), transformations=transformations)
313
  expr = lhs_e - rhs_e
314
  else:
315
- expr = parse_expr(re.sub(r"\s+", "", raw), transformations=transformations)
316
- sol = solve(expr, x)
317
- sol_latex = ", ".join([latex(s) for s in sol])
318
- results = {"type": "Equation", "result": str(sol), "latex": f"x \\in \\{{{sol_latex}\\}}"}
 
319
 
320
- # --- Matrix determinant / eigenvalues ---
321
- elif any(k in p for k in ["matrix", "determinant", "eigenvalue", "eigenvector", "det"]):
322
- results = {"type": "Matrix", "result": "matrix_detected", "latex": ""}
323
 
324
- # --- Modular arithmetic ---
325
  elif "mod" in p or "congruence" in p:
326
- results = {"type": "NumberTheory", "result": "mod_detected", "latex": ""}
327
 
328
- else:
329
- results = {"type": "general", "result": None, "latex": ""}
330
 
331
- except Exception as e:
332
- results = {"type": "error", "result": None, "error": str(e), "latex": ""}
333
-
334
- return results
335
 
336
 
337
  # โ”€โ”€โ”€ Claude API Call โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
338
  def ask_claude(problem: str, sympy_info: dict, history: list) -> str:
339
- """Call Claude API with BSc math context + SymPy verified result."""
340
-
341
- # Build sympy context string
342
  sympy_context = ""
343
- if sympy_info.get("result") and sympy_info["result"] not in ["matrix_detected", "mod_detected"]:
 
344
  sympy_context = f"""
345
- SYMPY VERIFIED RESULT:
346
- - Operation type : {sympy_info.get('type', 'N/A')}
347
- - Exact result : {sympy_info.get('result', 'N/A')}
348
- - LaTeX : {sympy_info.get('latex', 'N/A')}
349
-
350
- You MUST use this exact verified result in your answer. Do NOT deviate from it.
351
  """
352
 
353
- system_prompt = f"""You are SAAD AI โ€” an elite BSc Mathematics tutor. You solve every problem with:
354
- 1. PERFECT step-by-step working (no skipped steps)
355
- 2. Clear labels: "Step 1:", "Step 2:", etc.
356
- 3. Proper mathematical notation (use LaTeX between $$ for display math)
357
- 4. Final boxed answer clearly stated
358
- 5. Brief explanation of the method used
359
-
360
  {sympy_context}
361
-
362
- Topics you master:
363
- - Calculus: derivatives, integrals, limits, series
364
- - Linear Algebra: matrices, determinants, eigenvalues, vector spaces
365
- - Number Theory: congruences, modular arithmetic, primes, Euler's theorem
366
- - Differential Equations: ODEs, PDEs, Laplace transforms
367
- - Numerical Methods: Newton-Raphson, Euler's method, numerical integration
368
- - Differential Geometry: curves, surfaces, curvature
369
- - Hydro Mechanics: fluid dynamics, Bernoulli, continuity equation
370
- - Theory of Numbers: divisibility, GCD, LCM, Fermat, Euler
371
- - Real Analysis II: sequences, series convergence, uniform continuity, Riemann integral
372
- - General Algebra & Arithmetic
373
-
374
- STRICT RULES:
375
- - NEVER make arithmetic mistakes
376
- - Show ALL intermediate steps
377
- - Use $...$ for inline math and $$...$$ for display equations
378
- - If a sympy verified result is provided, your calculation MUST match it exactly
379
- - Be thorough but clear โ€” a student must be able to follow every step
380
  """
381
 
382
- # Build message history
383
  messages = []
384
- for msg in history[-8:]: # last 8 messages for context
385
  messages.append({"role": msg["role"], "content": msg["content"]})
386
  messages.append({"role": "user", "content": problem})
387
 
388
  client = anthropic.Anthropic()
389
  response = client.messages.create(
390
- model="claude-opus-4-5",
391
  max_tokens=3000,
392
  system=system_prompt,
393
  messages=messages
@@ -414,26 +369,27 @@ with st.sidebar:
414
  st.markdown("**โšก Quick Examples**")
415
 
416
  examples = {
 
417
  "๐Ÿ”ข Derivative": "Find the derivative of x^3 + 5x^2 - 3x + 7",
418
  "โˆซ Integral": "Integrate sin(x) * e^x dx",
419
  "๐Ÿ“Š Eigenvalue": "Find eigenvalues of matrix [[3,1],[1,3]]",
420
  "๐Ÿ” Congruence": "Solve 14x โ‰ก 30 (mod 44)",
421
  "๐ŸŒŠ ODE": "Solve dy/dx + 2y = e^x",
422
- "๐Ÿ“ Limit": "Find limit of sin(x)/x as x approaches 0",
423
  "๐Ÿ” Series": "Test convergence of sum 1/n^2 from n=1 to infinity",
424
  "๐Ÿ“ Newton-Raphson": "Apply Newton-Raphson to find root of x^3 - 2x - 5 = 0",
425
  }
426
 
427
  selected_example = st.selectbox(
428
  "Load an example:",
429
- ["-- Choose --"] + list(examples.keys()),
430
  key="example_select"
431
  )
432
 
433
  st.markdown("---")
434
  st.markdown("**๐Ÿ”ง Engine**")
435
  st.markdown("""
436
- <div style="font-size:0.8rem; color: #64748b; line-height:1.8;">
437
  ๐Ÿค– <b style="color:#38bdf8">Claude AI</b> โ€” reasoning & steps<br>
438
  ๐Ÿงฎ <b style="color:#34d399">SymPy</b> โ€” verified computation<br>
439
  โœ… Zero arithmetic errors
@@ -443,7 +399,7 @@ with st.sidebar:
443
  st.markdown("---")
444
  if st.button("๐Ÿ—‘๏ธ Clear Chat"):
445
  st.session_state.messages = []
446
- st.session_state.sympy_results = {}
447
  st.rerun()
448
 
449
 
@@ -468,13 +424,13 @@ chat_html = '<div class="chat-area" id="chatArea">'
468
 
469
  if not st.session_state.messages:
470
  chat_html += """
471
- <div style="display:flex; flex-direction:column; align-items:center; justify-content:center;
472
- height:300px; color:#334155; text-align:center;">
473
- <div style="font-size:3rem; margin-bottom:1rem;">๐Ÿ“</div>
474
- <div style="font-family:'Rajdhani',sans-serif; font-size:1.2rem; color:#475569; letter-spacing:1px;">
475
  Ask any BSc Mathematics problem
476
  </div>
477
- <div style="font-size:0.82rem; color:#334155; margin-top:0.5rem;">
478
  Calculus ยท Linear Algebra ยท Number Theory ยท ODEs ยท Real Analysis ยท and more
479
  </div>
480
  </div>
@@ -482,31 +438,35 @@ if not st.session_state.messages:
482
  else:
483
  for msg in st.session_state.messages:
484
  if msg["role"] == "user":
485
- chat_html += f"""
486
- <div class="msg-user">
487
- <div class="bubble">๐Ÿง‘โ€๐ŸŽ“ {msg['content']}</div>
488
- </div>
489
- """
 
490
  else:
491
- # Convert **bold** and basic formatting
492
  content = msg['content']
 
 
 
 
 
493
  content = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', content)
494
  content = content.replace('\n', '<br>')
495
- chat_html += f"""
496
- <div class="msg-ai">
497
- <div class="ai-avatar">โˆ‘</div>
498
- <div class="bubble">{content}</div>
499
- </div>
500
- """
501
 
502
  chat_html += '</div>'
503
  st.markdown(chat_html, unsafe_allow_html=True)
504
 
505
- # Auto scroll script
506
  st.markdown("""
507
  <script>
508
- const chatArea = document.getElementById('chatArea');
509
- if (chatArea) chatArea.scrollTop = chatArea.scrollHeight;
510
  </script>
511
  """, unsafe_allow_html=True)
512
 
@@ -515,14 +475,10 @@ st.markdown("""
515
  col1, col2 = st.columns([5, 1])
516
 
517
  with col1:
518
- # Pre-fill from example selector
519
- default_val = ""
520
- if selected_example and selected_example != "-- Choose --":
521
- default_val = examples[selected_example]
522
-
523
  user_input = st.text_input(
524
  label="problem_input",
525
- value=default_val,
526
  placeholder="Type a BSc math problem... e.g. 'Solve dยฒy/dxยฒ + 4y = cos(2x)'",
527
  label_visibility="collapsed",
528
  key="user_input_field"
@@ -533,21 +489,24 @@ with col2:
533
 
534
 
535
  # โ”€โ”€โ”€ Process Input โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
536
- if (send_btn or user_input) and user_input.strip():
537
- problem = user_input.strip()
 
 
 
 
538
 
539
  with st.spinner("๐Ÿงฎ Running SymPy verification + Claude reasoning..."):
540
- # Step 1: SymPy verification
541
  sympy_result = run_sympy(problem)
542
-
543
- # Step 2: Claude with verified result
544
  try:
545
  answer = ask_claude(problem, sympy_result, st.session_state.messages)
546
  except Exception as e:
547
- answer = f"โš ๏ธ API Error: {str(e)}\n\nPlease ensure ANTHROPIC_API_KEY is set in your Hugging Face Space secrets."
 
 
 
 
548
 
549
- # Save to history
550
  st.session_state.messages.append({"role": "user", "content": problem})
551
  st.session_state.messages.append({"role": "assistant", "content": answer})
552
-
553
- st.rerun()
 
1
+ # โ”€โ”€ Imports โ”€โ”€ (ORDER MATTERS: re must come after sympy wildcard)
2
  import streamlit as st
3
  import sympy as sp
4
  import anthropic
5
+ from sympy import (
6
+ symbols, diff, integrate, limit, solve, simplify, expand, factor,
7
+ latex, sympify, oo, sin, cos, tan, exp, log, sqrt, pi, E,
8
+ Matrix, det, eigenvals, Symbol, Function, dsolve, Eq,
9
+ series, Sum, Product, Rational, Integer, Float, Abs,
10
+ standard_transformations
11
+ )
12
+ from sympy.parsing.sympy_parser import (
13
+ parse_expr,
14
+ standard_transformations,
15
+ implicit_multiplication_application
16
+ )
17
+ import re # MUST be last โ€” sympy wildcard would overwrite it otherwise
18
 
19
  # โ”€โ”€โ”€ Page Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
20
  st.set_page_config(
 
48
  font-family: 'Nunito', sans-serif;
49
  }
50
 
 
51
  #MainMenu, footer, header { visibility: hidden; }
52
  .block-container { padding: 1.5rem 2rem 2rem 2rem; max-width: 1100px; }
53
 
 
54
  .hero {
55
  background: linear-gradient(135deg, #0d1526 0%, #0f1f40 50%, #091428 100%);
56
  border: 1px solid var(--border);
 
96
  letter-spacing: 0.5px;
97
  }
98
 
 
99
  .chat-area {
100
  background: var(--surface);
101
  border: 1px solid var(--border);
 
107
  margin-bottom: 1rem;
108
  }
109
 
 
110
  .msg-user {
111
  display: flex;
112
  justify-content: flex-end;
 
149
  line-height: 1.7;
150
  }
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  .stTextInput > div > div > input {
153
  background: var(--card) !important;
154
  border: 1px solid var(--border) !important;
 
163
  box-shadow: 0 0 0 2px rgba(56,189,248,0.15) !important;
164
  }
165
 
 
166
  .stButton > button {
167
  background: linear-gradient(135deg, #1e4080, #1a3060) !important;
168
  border: 1px solid var(--accent) !important;
 
180
  box-shadow: 0 4px 15px rgba(56,189,248,0.2) !important;
181
  }
182
 
 
183
  [data-testid="stSidebar"] {
184
  background: var(--surface) !important;
185
  border-right: 1px solid var(--border) !important;
 
190
  letter-spacing: 1px;
191
  }
192
 
 
193
  .topic-pill {
194
  background: rgba(129,140,248,0.08);
195
  border: 1px solid rgba(129,140,248,0.2);
 
199
  font-size: 0.78rem;
200
  margin: 3px 2px;
201
  display: inline-block;
 
202
  font-family: 'JetBrains Mono', monospace;
203
  }
 
 
 
204
 
 
205
  ::-webkit-scrollbar { width: 6px; }
206
  ::-webkit-scrollbar-track { background: var(--surface); }
207
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
208
  ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
209
 
 
 
 
 
 
 
 
210
  .stSelectbox > div > div {
211
  background: var(--card) !important;
212
  border-color: var(--border) !important;
 
219
  # โ”€โ”€โ”€ Session State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
220
  if "messages" not in st.session_state:
221
  st.session_state.messages = []
222
+ if "last_submitted" not in st.session_state:
223
+ st.session_state.last_submitted = "" # prevents infinite rerun loop
224
 
225
  # โ”€โ”€โ”€ SymPy Engine โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
226
  def run_sympy(problem: str) -> dict:
227
  """
228
+ Safely attempt symbolic computation.
229
+ Uses sp.* explicitly โ€” no wildcard collisions possible here.
230
  """
231
  p = problem.lower().strip()
232
+ x = sp.Symbol('x')
233
+ default = {"type": "general", "result": None, "latex": ""}
234
 
235
  try:
236
+ tfms = standard_transformations + (implicit_multiplication_application,)
237
 
238
+ def clean(s):
239
+ """Normalise expression string for sympy parsing."""
240
+ s = re.sub(r"\s+", "", s)
241
+ s = re.sub(r"\^", "**", s)
242
+ return s
243
+
244
+ # โ”€โ”€ Derivative โ”€โ”€
245
  if any(k in p for k in ["derivative", "differentiate", "d/dx", "diff"]):
246
+ raw = p
247
  for kw in ["derivative of", "differentiate", "diff of", "d/dx of", "d/dx"]:
248
  if kw in p:
249
+ raw = p.split(kw, 1)[-1].strip()
250
  break
251
+ raw = re.sub(r"\s*(dx|with\s*respect\s*to\s*x).*$", "", raw).strip()
252
+ expr = parse_expr(clean(raw), transformations=tfms, local_dict={"x": x})
253
+ result = sp.diff(expr, x)
254
+ return {"type": "Derivative", "result": str(result), "latex": sp.latex(result)}
255
+
256
+ # โ”€โ”€ Integral โ”€โ”€
257
+ elif any(k in p for k in ["integral", "integrate", "antiderivative"]):
258
+ raw = p
259
+ for kw in ["integral of", "integrate", "antiderivative of"]:
 
260
  if kw in p:
261
+ raw = p.split(kw, 1)[-1].strip()
262
  break
263
+ raw = re.sub(r"\s*dx.*$", "", raw).strip()
264
+ expr = parse_expr(clean(raw), transformations=tfms, local_dict={"x": x})
265
+ result = sp.integrate(expr, x)
266
+ return {"type": "Integral", "result": str(result), "latex": sp.latex(result)}
 
 
267
 
268
+ # โ”€โ”€ Limit โ”€โ”€
269
  elif "limit" in p:
270
+ match = re.search(
271
+ r"limit\s+of\s+([\w\s\(\)\+\-\*/\^\.\,]+?)\s+as\s+x\s*(?:->|โ†’|approaches)\s*([\w\.\+\-]+)",
272
+ p
273
+ )
274
  if match:
275
+ raw_expr = clean(match.group(1))
 
276
  point_str = match.group(2).strip()
277
+ point = sp.oo if point_str in ("inf", "infinity", "oo") else sp.sympify(point_str)
278
+ expr = parse_expr(raw_expr, transformations=tfms, local_dict={"x": x})
279
+ result = sp.limit(expr, x, point)
280
+ return {"type": "Limit", "result": str(result), "latex": sp.latex(result)}
281
+
282
+ # โ”€โ”€ Solve equation โ”€โ”€
283
+ elif any(k in p for k in ["solve", "roots", "find x"]):
284
+ raw = re.sub(r"(solve|find x|roots of|roots|the equation)", "", p).strip().strip(": ")
 
285
  if "=" in raw:
286
+ lhs_s, rhs_s = raw.split("=", 1)
287
+ lhs_e = parse_expr(clean(lhs_s), transformations=tfms, local_dict={"x": x})
288
+ rhs_e = parse_expr(clean(rhs_s), transformations=tfms, local_dict={"x": x})
289
  expr = lhs_e - rhs_e
290
  else:
291
+ expr = parse_expr(clean(raw), transformations=tfms, local_dict={"x": x})
292
+ if x in expr.free_symbols:
293
+ sol = sp.solve(expr, x)
294
+ sol_latex = ", ".join([sp.latex(s) for s in sol])
295
+ return {"type": "Equation", "result": str(sol), "latex": f"x \\in \\{{{sol_latex}\\}}"}
296
 
297
+ # โ”€โ”€ Matrix / eigenvalues โ€” delegate to Claude โ”€โ”€
298
+ elif any(k in p for k in ["matrix", "determinant", "eigenvalue", "eigenvector", "det("]):
299
+ return {"type": "Matrix", "result": "matrix_detected", "latex": ""}
300
 
301
+ # โ”€โ”€ Modular arithmetic โ€” delegate to Claude โ”€โ”€
302
  elif "mod" in p or "congruence" in p:
303
+ return {"type": "NumberTheory", "result": "mod_detected", "latex": ""}
304
 
305
+ except Exception:
306
+ pass # fall back gracefully; Claude handles it without sympy hint
307
 
308
+ return default
 
 
 
309
 
310
 
311
  # โ”€โ”€โ”€ Claude API Call โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
312
  def ask_claude(problem: str, sympy_info: dict, history: list) -> str:
 
 
 
313
  sympy_context = ""
314
+ if (sympy_info.get("result")
315
+ and sympy_info["result"] not in (None, "matrix_detected", "mod_detected")):
316
  sympy_context = f"""
317
+ SYMPY VERIFIED RESULT (mathematically exact โ€” your final answer MUST match this):
318
+ - Operation : {sympy_info.get('type', 'N/A')}
319
+ - Result : {sympy_info.get('result', 'N/A')}
320
+ - LaTeX : {sympy_info.get('latex', 'N/A')}
 
 
321
  """
322
 
323
+ system_prompt = f"""You are SAAD AI โ€” an elite BSc Mathematics tutor.
 
 
 
 
 
 
324
  {sympy_context}
325
+ RULES:
326
+ 1. Label every step: "Step 1:", "Step 2:", etc. โ€” no steps skipped.
327
+ 2. Use $...$ for inline math and $$...$$ for display equations.
328
+ 3. State the method/theorem at the start.
329
+ 4. Show ALL intermediate algebra.
330
+ 5. End with: **Final Answer:** $$...$$
331
+ 6. If a SymPy verified result is shown above, your final answer MUST match it exactly.
332
+ 7. NEVER make arithmetic errors.
333
+
334
+ TOPICS: Calculus | Linear Algebra | Number Theory | ODEs | Numerical Methods |
335
+ Differential Geometry | Hydro Mechanics | Theory of Numbers | Real Analysis II | General Math
 
 
 
 
 
 
 
 
336
  """
337
 
 
338
  messages = []
339
+ for msg in history[-8:]:
340
  messages.append({"role": msg["role"], "content": msg["content"]})
341
  messages.append({"role": "user", "content": problem})
342
 
343
  client = anthropic.Anthropic()
344
  response = client.messages.create(
345
+ model="claude-sonnet-4-5",
346
  max_tokens=3000,
347
  system=system_prompt,
348
  messages=messages
 
369
  st.markdown("**โšก Quick Examples**")
370
 
371
  examples = {
372
+ "-- Choose --": "",
373
  "๐Ÿ”ข Derivative": "Find the derivative of x^3 + 5x^2 - 3x + 7",
374
  "โˆซ Integral": "Integrate sin(x) * e^x dx",
375
  "๐Ÿ“Š Eigenvalue": "Find eigenvalues of matrix [[3,1],[1,3]]",
376
  "๐Ÿ” Congruence": "Solve 14x โ‰ก 30 (mod 44)",
377
  "๐ŸŒŠ ODE": "Solve dy/dx + 2y = e^x",
378
+ "๐Ÿ“ Limit": "Find limit of sin(x)/x as x -> 0",
379
  "๐Ÿ” Series": "Test convergence of sum 1/n^2 from n=1 to infinity",
380
  "๐Ÿ“ Newton-Raphson": "Apply Newton-Raphson to find root of x^3 - 2x - 5 = 0",
381
  }
382
 
383
  selected_example = st.selectbox(
384
  "Load an example:",
385
+ list(examples.keys()),
386
  key="example_select"
387
  )
388
 
389
  st.markdown("---")
390
  st.markdown("**๐Ÿ”ง Engine**")
391
  st.markdown("""
392
+ <div style="font-size:0.8rem; color:#64748b; line-height:1.8;">
393
  ๐Ÿค– <b style="color:#38bdf8">Claude AI</b> โ€” reasoning & steps<br>
394
  ๐Ÿงฎ <b style="color:#34d399">SymPy</b> โ€” verified computation<br>
395
  โœ… Zero arithmetic errors
 
399
  st.markdown("---")
400
  if st.button("๐Ÿ—‘๏ธ Clear Chat"):
401
  st.session_state.messages = []
402
+ st.session_state.last_submitted = ""
403
  st.rerun()
404
 
405
 
 
424
 
425
  if not st.session_state.messages:
426
  chat_html += """
427
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
428
+ height:300px;color:#334155;text-align:center;">
429
+ <div style="font-size:3rem;margin-bottom:1rem;">๐Ÿ“</div>
430
+ <div style="font-family:'Rajdhani',sans-serif;font-size:1.2rem;color:#475569;letter-spacing:1px;">
431
  Ask any BSc Mathematics problem
432
  </div>
433
+ <div style="font-size:0.82rem;color:#334155;margin-top:0.5rem;">
434
  Calculus ยท Linear Algebra ยท Number Theory ยท ODEs ยท Real Analysis ยท and more
435
  </div>
436
  </div>
 
438
  else:
439
  for msg in st.session_state.messages:
440
  if msg["role"] == "user":
441
+ # Escape HTML to prevent rendering issues with math symbols
442
+ safe = (msg['content']
443
+ .replace("&", "&amp;")
444
+ .replace("<", "&lt;")
445
+ .replace(">", "&gt;"))
446
+ chat_html += f'<div class="msg-user"><div class="bubble">๐Ÿง‘โ€๐ŸŽ“ {safe}</div></div>'
447
  else:
 
448
  content = msg['content']
449
+ # Escape first, then apply safe bold conversion
450
+ content = (content
451
+ .replace("&", "&amp;")
452
+ .replace("<", "&lt;")
453
+ .replace(">", "&gt;"))
454
  content = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', content)
455
  content = content.replace('\n', '<br>')
456
+ chat_html += (
457
+ f'<div class="msg-ai">'
458
+ f'<div class="ai-avatar">โˆ‘</div>'
459
+ f'<div class="bubble">{content}</div>'
460
+ f'</div>'
461
+ )
462
 
463
  chat_html += '</div>'
464
  st.markdown(chat_html, unsafe_allow_html=True)
465
 
 
466
  st.markdown("""
467
  <script>
468
+ const c = document.getElementById('chatArea');
469
+ if (c) c.scrollTop = c.scrollHeight;
470
  </script>
471
  """, unsafe_allow_html=True)
472
 
 
475
  col1, col2 = st.columns([5, 1])
476
 
477
  with col1:
478
+ prefill = examples.get(selected_example, "") if selected_example != "-- Choose --" else ""
 
 
 
 
479
  user_input = st.text_input(
480
  label="problem_input",
481
+ value=prefill,
482
  placeholder="Type a BSc math problem... e.g. 'Solve dยฒy/dxยฒ + 4y = cos(2x)'",
483
  label_visibility="collapsed",
484
  key="user_input_field"
 
489
 
490
 
491
  # โ”€โ”€โ”€ Process Input โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
492
+ # Only fires when SOLVE is clicked, input is non-empty, AND it's a new problem
493
+ # This prevents the infinite rerun loop caused by value="" pre-fill on every cycle
494
+ problem = user_input.strip() if user_input else ""
495
+
496
+ if send_btn and problem and problem != st.session_state.last_submitted:
497
+ st.session_state.last_submitted = problem
498
 
499
  with st.spinner("๐Ÿงฎ Running SymPy verification + Claude reasoning..."):
 
500
  sympy_result = run_sympy(problem)
 
 
501
  try:
502
  answer = ask_claude(problem, sympy_result, st.session_state.messages)
503
  except Exception as e:
504
+ answer = (
505
+ f"โš ๏ธ **API Error:** {str(e)}\n\n"
506
+ "Please ensure **ANTHROPIC_API_KEY** is added in your Hugging Face Space โ†’ "
507
+ "Settings โ†’ Variables and Secrets."
508
+ )
509
 
 
510
  st.session_state.messages.append({"role": "user", "content": problem})
511
  st.session_state.messages.append({"role": "assistant", "content": answer})
512
+ st.rerun()