File size: 10,553 Bytes
30b060a
9d3a830
 
bbd8ccb
 
6a748ed
76dba01
 
efbfdc1
76dba01
 
 
efbfdc1
85ea4ec
a07f719
76dba01
 
a07f719
 
 
 
2565f04
76dba01
a07f719
76dba01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a07f719
 
 
 
 
 
 
 
 
 
76dba01
bbd8ccb
76dba01
 
bbd8ccb
76dba01
 
 
bbd8ccb
76dba01
 
 
 
 
bbd8ccb
76dba01
 
 
 
 
 
415b778
76dba01
efbfdc1
 
b8dcaff
8c6ac41
efbfdc1
415b778
76dba01
 
 
 
 
 
 
 
 
 
 
 
415b778
b8dcaff
 
 
6a748ed
 
8b5cec5
76dba01
 
8b5cec5
76dba01
8b5cec5
76dba01
8b5cec5
76dba01
 
3aa12d6
8b5cec5
 
76dba01
 
 
 
 
3aa12d6
8b5cec5
76dba01
 
 
 
 
 
3aa12d6
8b5cec5
76dba01
 
 
 
 
 
3aa12d6
76dba01
8b5cec5
76dba01
 
 
 
 
 
8b5cec5
 
 
efbfdc1
76dba01
3aa12d6
 
 
 
 
 
 
ee504a8
3aa12d6
 
efbfdc1
2565f04
3aa12d6
 
2565f04
3aa12d6
 
 
 
 
 
 
 
 
 
 
9d3a830
efbfdc1
bbd8ccb
76dba01
 
bbd8ccb
 
 
 
76dba01
 
 
 
 
8b5cec5
76dba01
8b5cec5
 
76dba01
 
 
bbd8ccb
76dba01
 
bbd8ccb
 
76dba01
 
 
 
 
2565f04
 
76dba01
0e5e0a4
b8dcaff
 
 
0e5e0a4
415b778
b8dcaff
 
 
 
 
 
 
 
 
 
76dba01
415b778
bbd8ccb
76dba01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbd8ccb
819a580
 
76dba01
 
 
 
 
819a580
2565f04
819a580
bbd8ccb
76dba01
 
 
 
 
3aa12d6
 
 
6296ee3
76dba01
 
 
 
 
 
 
 
 
6296ee3
819a580
 
 
 
76dba01
 
 
 
bbd8ccb
76dba01
 
 
 
 
 
 
9d3a830
6296ee3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import os
import gradio as gr
from huggingface_hub import InferenceClient
from datasets import load_dataset
import random
import re
import sympy as sp
import threading

# Global datasets
math_samples = []
loading_status = {"loaded": False, "error": None}

def load_sample_problems():
    """Load sample problems - using fallback only to avoid storage limits"""
    global math_samples, loading_status
    
    # Skip dataset loading to avoid 50GB storage limit
    math_samples = get_fallback_samples()
    loading_status["loaded"] = True
    print(f"✅ Loaded {len(math_samples)} fallback samples")

def get_fallback_samples():
    """Extended fallback problems covering diverse topics"""
    return [
        "Find the derivative of f(x) = 3x² + 2x - 1",
        "A triangle has sides 5, 12, and 13. What is its area?",
        "If log₂(x) + log₂(x+6) = 4, find x",
        "Calculate lim(x→0) sin(x)/x",
        "Solve: x + 2y = 7, 3x - y = 4",
        "Integrate sin(x) from 0 to π",
        "What is P(rolling three 6s in a row)?",
        "Simplify: (x² - 4)/(x - 2)",
        "Find the sum of 1 + 2 + 3 + ... + 100",
        "What is the 10th Fibonacci number?",
        "Calculate the area of a circle with radius 5",
        "Factor x² + 5x + 6",
        "Solve x² - 4x + 4 = 0",
        "Find tan(π/4)",
        "What is 15% of 240?",
        "Evaluate ∫x³ dx from 1 to 3",
        "Find the slope of the line through (2,3) and (5,9)",
        "Convert 45° to radians",
        "Solve |2x - 5| = 7",
        "Find the vertex of y = x² - 6x + 8",
        "Calculate the discriminant of 2x² + 3x - 5 = 0",
        "What is the standard deviation of [2, 4, 6, 8, 10]?",
        "Simplify √(72)",
        "Find cos(60°)",
        "Solve the inequality 3x - 7 > 11",
    ]

# Start background loading
threading.Thread(target=load_sample_problems, daemon=True).start()

def create_math_system_message():
    """System prompt for math tutoring"""
    return r"""You are Mathetics AI, an expert mathematics tutor.

**Teaching Approach:**
1. Understand the problem clearly
2. Show step-by-step solutions with LaTeX
3. Verify answers when possible
4. Suggest alternative methods

**LaTeX Formatting:**
- Inline: $x^2 + y^2 = r^2$
- Display: $$\int_0^\pi \sin(x)\,dx = 2$$
- Box answers: $\boxed{42}$
- Fractions: $\frac{a}{b}$
- Limits: $\lim_{x \to 0}$

Be clear, precise, and educational."""

def render_latex(text):
    """Clean and normalize LaTeX for proper rendering"""
    if not text:
        return text
    
    # Convert LaTeX brackets to dollars
    text = re.sub(r'\\\[(.*?)\\\]', r'$$\1$$', text, flags=re.DOTALL)
    text = re.sub(r'\\\((.*?)\\\)', r'$\1$', text, flags=re.DOTALL)
    
    # Fix boxed answers outside math mode
    text = re.sub(r'(?<!\$)\\boxed\{([^}]+)\}(?!\$)', r'$\\boxed{\1}$', text)
    
    # Convert equation environments
    text = re.sub(r'\\begin\{equation\*?\}(.*?)\\end\{equation\*?\}', r'$$\1$$', text, flags=re.DOTALL)
    
    # Remove duplicate $$
    text = re.sub(r'\$\$+', '$$', text)
    
    # Escape backslashes for proper rendering in Gradio
    text = text.replace('\\\\', '\\')
    
    return text

def try_sympy_compute(message):
    """Enhanced SymPy computation with better parsing"""
    from sympy.parsing.sympy_parser import parse_expr, standard_transformations, implicit_multiplication_application
    
    msg_lower = message.lower()
    x = sp.Symbol('x')
    transforms = standard_transformations + (implicit_multiplication_application,)
    
    try:
        # Definite integrals
        match = re.search(r'\bint(?:egral)?(?:\s+of)?\s+(.+?)\s+from\s+(.+?)\s+to\s+(.+?)(?:\s|$)', msg_lower)
        if match:
            expr_str, lower, upper = match.groups()
            expr = parse_expr(expr_str.replace('^', '**'), transformations=transforms)
            result = sp.integrate(expr, (x, sp.sympify(lower.strip()), sp.sympify(upper.strip())))
            return sp.latex(result, mode='plain')
        
        # Derivatives
        match = re.search(r'\bderiv(?:ative)?(?:\s+of)?\s+(.+?)(?:\s|$)', msg_lower)
        if match:
            expr_str = match.group(1).strip()
            expr = parse_expr(expr_str.replace('^', '**'), transformations=transforms)
            result = sp.diff(expr, x)
            return sp.latex(result, inv_trig_style='power')
        
        # Limits
        match = re.search(r'\blim(?:it)?.*?(?:\()?(.+?)\s+(?:as\s+)?(?:x\s*)?(?:→|->|to)\s*(.+?)(?:\)|$)', msg_lower)
        if match:
            expr_str, to_val = match.groups()
            expr = parse_expr(expr_str.replace('^', '**'), transformations=transforms)
            result = sp.limit(expr, x, sp.sympify(to_val.strip()))
            return sp.latex(result)
        
        # Triangle area (Heron's formula)
        match = re.search(r'\b(?:triangle|area)\b.*?(\d+)[,\s-]+(\d+)[,\s-]+(\d+)', message)
        if match and 'triangle' in msg_lower:
            a, b, c = map(float, match.groups())
            s = (a + b + c) / 2
            area = sp.sqrt(s * (s-a) * (s-b) * (s-c))
            return sp.latex(area.evalf())
            
    except Exception as e:
        print(f"⚠️ SymPy error: {e}")
    
    return None

def respond(message, history, system_message, max_tokens, temperature, top_p):
    """Streaming response with error handling"""
    # Get HF token from environment
    hf_token = os.getenv("HF_TOKEN")
    if not hf_token:
        yield "❌ **Error:** HF_TOKEN not found. Set it in your environment or Hugging Face Spaces secrets."
        return
    
    client = InferenceClient(
        model="Qwen/Qwen2.5-Math-7B-Instruct",
        token=hf_token
    )
    
    messages = [{"role": "system", "content": system_message}]
    
    # Safely handle history format
    for msg in history:
        if isinstance(msg, dict):
            role = msg.get("role", "user")
            content = msg.get("content", "")
        else:
            # Fallback for unexpected formats
            role = "user"
            content = str(msg)
        
        if content:  # Only add non-empty messages
            messages.append({"role": role, "content": content})
    
    messages.append({"role": "user", "content": message})
    
    try:
        response_text = ""
        for chunk in client.chat_completion(
            messages,
            max_tokens=max_tokens,
            temperature=temperature,
            top_p=top_p,
            stream=True
        ):
            if chunk.choices[0].delta.content:
                response_text += chunk.choices[0].delta.content
                yield render_latex(response_text)
        
        # Add SymPy verification
        sympy_result = try_sympy_compute(message)
        if sympy_result:
            response_text += f"\n\n**✓ Verified with SymPy:** $${sympy_result}$$"
            yield render_latex(response_text)
            
    except Exception as e:
        error_msg = f"❌ **Error:** {str(e)}\n\nTry:\n- Simpler wording\n- Breaking into steps\n- Checking notation"
        yield error_msg

def get_random_sample():
    """Get random sample with loading status"""
    if not loading_status["loaded"]:
        return "⏳ Loading samples..."
    if not math_samples:
        return get_fallback_samples()[0]
    return random.choice(math_samples)

# Gradio Interface
with gr.Blocks(title="🧮 Mathematics AI", theme=gr.themes.Soft(), css="""
    .katex { font-size: 1.1em; }
    .katex-display { margin: 1em 0; }
""") as demo:
    gr.Markdown("# 🧮 **Mathematics AI**\n*Advanced Math Tutor powered by Qwen2.5-Math*")
    
    chatbot = gr.Chatbot(
        height=500, 
        type='messages', 
        label="💬 Conversation",
        latex_delimiters=[
            {"left": "$$", "right": "$$", "display": True},
            {"left": "$", "right": "$", "display": False}
        ],
        show_copy_button=True
    )
    msg = gr.Textbox(placeholder="Ask any math problem...", show_label=False, scale=4)
    
    with gr.Row():
        submit = gr.Button("🚀 Solve", variant="primary", scale=1)
        clear = gr.Button("🗑️ Clear", variant="secondary", scale=1)
        sample = gr.Button("🎲 Random", variant="secondary", scale=1)
    
    with gr.Accordion("⚙️ Advanced Settings", open=False):
        temp_slider = gr.Slider(0.1, 1.0, value=0.3, step=0.1, label="Temperature (creativity)")
        tokens_slider = gr.Slider(256, 2048, value=1024, step=128, label="Max Tokens")
        top_p_slider = gr.Slider(0.1, 1.0, value=0.85, step=0.05, label="Top-p (nucleus sampling)")
    
    with gr.Accordion("💡 Help & Examples", open=False):
        gr.Markdown("""
**Tips:**
- Be specific: "derivative of sin(2x)" not "help with calculus"
- Request steps: "show step-by-step"
- Use clear notation: `x^2` for powers, `lim x->0` for limits

**Examples:**
- Calculus: "Find ∫x² dx from 0 to 5"
- Algebra: "Solve x² + 5x + 6 = 0"
- Geometry: "Area of triangle with sides 3, 4, 5"
- Limits: "Calculate lim(x→0) sin(x)/x"
        """)
    
    gr.Examples(
        examples=[
            ["Find the derivative of x² sin(x)"],
            ["Calculate the area of a triangle with sides 5, 12, 13"],
            ["Integrate x² from 0 to 2"],
            ["What is lim(x→0) sin(x)/x?"],
            ["Solve the equation x² - 5x + 6 = 0"],
        ],
        inputs=msg
    )
    
    # Hidden system message (passed to respond)
    system_msg = gr.State(create_math_system_message())
    
    def chat_response(message, history, sys_msg, max_tok, temp, top_p):
        """Handle chat with streaming"""
        if not message.strip():
            return history, ""
        
        history.append({"role": "user", "content": message})
        
        # Create assistant message slot
        history.append({"role": "assistant", "content": ""})
        
        # Stream responses
        for response in respond(message, history[:-1], sys_msg, max_tok, temp, top_p):
            history[-1]["content"] = response
            yield history, ""
        
        return history, ""
    
    def clear_chat():
        return [], ""
    
    msg.submit(
        chat_response, 
        [msg, chatbot, system_msg, tokens_slider, temp_slider, top_p_slider], 
        [chatbot, msg]
    )
    submit.click(
        chat_response, 
        [msg, chatbot, system_msg, tokens_slider, temp_slider, top_p_slider], 
        [chatbot, msg]
    )
    clear.click(clear_chat, outputs=[chatbot, msg])
    sample.click(get_random_sample, outputs=msg)

demo.launch()