jdesiree commited on
Commit
383dc6d
·
verified ·
1 Parent(s): 9b40468

Added Graph Generating Functionality

Browse files
Files changed (1) hide show
  1. app.py +145 -262
app.py CHANGED
@@ -29,7 +29,7 @@ tools = [
29
  "type": "function",
30
  "function": {
31
  "name": "create_graph",
32
- "description": "Generates a plot (bar, line, or pie) and returns it as an HTML-formatted Base64-encoded image string. The data and labels arguments must be JSON-encoded strings. Use simple, descriptive labels for plots. Use this tool to produce plots for practice questions, data visualization use cases, or any other case where the student may benefit from a graph.",
33
  "parameters": {
34
  "type": "object",
35
  "properties": {
@@ -75,21 +75,34 @@ You recognize that students may seek direct answers to homework, assignments, or
75
  - **Suggest study strategies**: Recommend effective learning approaches for the subject matter
76
 
77
  ## Tool Usage
78
- You have access to a create_graph tool that can generate bar charts, line graphs, and pie charts. Use this tool when:
79
- - A visual representation would help explain a concept
80
- - The student asks for data visualization
81
- - Creating practice problems that involve interpreting charts
82
- - Demonstrating mathematical relationships visually
 
 
 
83
 
84
- When using the create_graph tool, provide JSON-formatted data and labels. For example:
85
- - data_json: '{"Math": 85, "Science": 92, "English": 78}'
86
- - labels_json: '["Math", "Science", "English"]'
 
 
 
 
 
 
 
 
 
87
 
88
  ## Response Guidelines
89
  - **For math problems**: Explain concepts, provide formula derivations, and guide through problem-solving steps without computing final numerical answers
90
  - **For multiple-choice questions**: Discuss the concepts being tested and help students understand how to analyze options rather than identifying the correct choice
91
  - **For essays or written work**: Discuss research strategies, organizational techniques, and critical thinking approaches rather than providing content or thesis statements
92
  - **For factual questions**: Provide educational context and encourage students to synthesize information rather than stating direct answers
 
93
 
94
  ## Communication Guidelines
95
  - Maintain a supportive, non-judgmental tone in all interactions
@@ -103,6 +116,41 @@ Your goal is to be an educational partner who empowers students to succeed throu
103
 
104
  # --- Core Logic Functions ---
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  def smart_truncate(text, max_length=3000):
107
  """Truncates text intelligently to the last full sentence or word."""
108
  if len(text) <= max_length:
@@ -113,7 +161,31 @@ def smart_truncate(text, max_length=3000):
113
  if len(sentences) > 1:
114
  return ' '.join(sentences[:-1]) + "... [Response truncated - ask for continuation]"
115
  # Otherwise, split by word
116
- + "top_p": 0.9,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  "return_full_text": False
118
  }
119
  }
@@ -136,276 +208,87 @@ def smart_truncate(text, max_length=3000):
136
  result = response.json()
137
 
138
  if isinstance(result, list) and len(result) > 0:
139
- return result[0].get('generated_text', '').strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  else:
141
  return "I apologize, but I received an unexpected response format. Please try again."
142
 
143
- except requests.exceptions.Timeout:
 
144
  if attempt < max_retries - 1:
145
- logger.warning(f"Request timeout, retrying... (attempt {attempt + 1})")
146
  time.sleep(2)
147
  continue
148
  else:
149
- return "I'm sorry, the request timed out. Please try again."
150
- except requests.exceptions.RequestException as e:
 
151
  if attempt < max_retries - 1:
152
- logger.warning(f"Request failed: {e}, retrying... (attempt {attempt + 1})")
153
  time.sleep(2)
154
  continue
155
  else:
156
- return f"I'm sorry, there was an error connecting to the service: {str(e)}"
157
-
158
- return "I'm sorry, I encountered an error and couldn't generate a response."
159
-
160
- def format_messages_for_hf(messages):
161
- """Format messages for HF API."""
162
- formatted = ""
163
- for msg in messages:
164
- role = msg["role"]
165
- content = msg["content"]
166
- if role == "system":
167
- formatted += f"System: {content}\n\n"
168
- elif role == "user":
169
- formatted += f"Human: {content}\n\n"
170
- elif role == "assistant":
171
- formatted += f"Assistant: {content}\n\n"
172
-
173
- formatted += "Assistant: "
174
- return formatted
175
-
176
- def process_response_for_tools(response_text, original_query):
177
- """Check if we should generate a graph based on the response and query."""
178
- # Simple heuristic - if the response mentions creating a chart/graph or the query requested one
179
- should_create_graph = (
180
- detect_tool_request(original_query) or
181
- any(phrase in response_text.lower() for phrase in [
182
- "let me create a", "i'll make a", "here's a chart", "here's a graph"
183
- ])
184
- )
185
-
186
- if should_create_graph:
187
- # Try to extract data from context or create a simple example
188
- if "grade" in original_query.lower() or "score" in original_query.lower():
189
- data_json = '{"Math": 85, "Science": 92, "English": 78, "History": 88}'
190
- labels_json = '["Math", "Science", "English", "History"]'
191
- title = "Sample Grade Distribution"
192
- plot_type = "bar"
193
- elif "population" in original_query.lower():
194
- data_json = '{"City A": 1200000, "City B": 950000, "City C": 800000}'
195
- labels_json = '["City A", "City B", "City C"]'
196
- title = "Population Comparison"
197
- plot_type = "bar"
198
- elif "time" in original_query.lower() or "trend" in original_query.lower():
199
- data_json = '{"Jan": 20, "Feb": 25, "Mar": 30, "Apr": 28, "May": 35}'
200
- labels_json = '["Jan", "Feb", "Mar", "Apr", "May"]'
201
- title = "Monthly Trends"
202
- plot_type = "line"
203
- else:
204
- # Default example
205
- data_json = '{"Category A": 30, "Category B": 25, "Category C": 20, "Category D": 25}'
206
- labels_json = '["Category A", "Category B", "Category C", "Category D"]'
207
- title = "Sample Data Distribution"
208
- plot_type = "pie"
209
-
210
- try:
211
- graph_html = generate_plot(data_json, labels_json, plot_type, title)
212
- response_text += f"\n\n{graph_html}"
213
- except Exception as e:
214
- logger.error(f"Error generating graph: {e}")
215
- response_text += f"\n\n<p style='color:orange;'>I tried to create a visualization but encountered an error. The concept explanation above should still be helpful!</p>"
216
-
217
- return response_text
218
-
219
- def respond_with_enhanced_streaming(message, history):
220
- """Streams the bot's response, with support for graph generation."""
221
- timing_context = metrics_tracker.start_timing()
222
- error_occurred = False
223
- response = ""
224
- mode = ""
225
 
 
 
226
  try:
227
- # ---- Call HuggingFace Inference ----
228
- payload = {
229
- "inputs": message,
230
- "parameters": {"return_full_text": False},
231
- }
232
- headers = {"Authorization": f"Bearer {HF_API_TOKEN}"}
233
- api_response = requests.post(HF_API_URL, headers=headers, json=payload)
234
- api_response.raise_for_status()
235
- outputs = api_response.json()
236
-
237
- # HuggingFace might return [{"generated_text": "..."}]
238
- model_output = outputs[0].get("generated_text", "").strip()
239
-
240
- # ---- Check if it's a tool call ----
241
- try:
242
- parsed = json.loads(model_output)
243
- if isinstance(parsed, dict) and parsed.get("name") == "create_graph":
244
- args = parsed.get("arguments", {})
245
- graph_html = generate_plot(
246
- args.get("data_json", "{}"),
247
- args.get("labels_json", "[]"),
248
- args.get("plot_type", "bar"),
249
- args.get("title", "Untitled"),
250
- args.get("x_label", ""),
251
- args.get("y_label", "")
252
- )
253
- response = graph_html
254
- mode = "graph"
255
- else:
256
- response = model_output
257
- mode = "text"
258
- except json.JSONDecodeError:
259
- # Not JSON → just plain text
260
- response = model_output
261
- mode = "text"
262
-
263
  except Exception as e:
264
- error_occurred = True
265
- response = f"⚠️ Error: {str(e)}"
266
- mode = "error"
267
-
268
- finally:
269
- metrics_tracker.stop_timing(timing_context, error_occurred)
270
-
271
- return response, mode
272
-
273
-
274
- # ===============================================================================
275
- # UI CONFIGURATION SECTION - ALL UI RELATED CODE CENTRALIZED HERE
276
- # ===============================================================================
277
 
278
- # --- UI: HTML Head Content ---
279
- html_head_content = '''
280
- <link rel="preconnect" href="https://fonts.googleapis.com">
281
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
282
- <link href="https://fonts.googleapis.com/css2?family=Oswald:wght@200..700&display=swap" rel="stylesheet">
283
- <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
284
- <script>
285
- // Force light theme in Gradio
286
- window.addEventListener('DOMContentLoaded', function () {
287
- const gradioURL = window.location.href;
288
- const url = new URL(gradioURL);
289
- const currentTheme = url.searchParams.get('__theme');
290
-
291
- if (currentTheme !== 'light') {
292
- url.searchParams.set('__theme', 'light');
293
- window.location.replace(url.toString());
294
- }
295
- });
296
- </script>
297
- '''
298
-
299
- # --- UI: MathJax Configuration ---
300
- mathjax_config = '''
301
- <script>
302
- window.MathJax = {
303
- tex: {
304
- inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
305
- displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
306
- packages: {'[+]': ['ams']}
307
- },
308
- svg: {fontCache: 'global'},
309
- startup: {
310
- ready: () => {
311
- MathJax.startup.defaultReady();
312
- // Re-render math when new content is added
313
- const observer = new MutationObserver(function(mutations) {
314
- MathJax.typesetPromise();
315
- });
316
- observer.observe(document.body, {childList: true, subtree: true});
317
- }
318
- }
319
- };
320
- </script>
321
- '''
322
-
323
- # --- UI: Event Handlers ---
324
- def respond_and_update(message, history):
325
- """Main function to handle user submission."""
326
- if not message.strip():
327
- return history, ""
328
-
329
- # Add user message to history
330
- history.append({"role": "user", "content": message})
331
- # Yield history to show the user message immediately, and clear the textbox
332
- yield history, ""
333
-
334
- # Stream the bot's response
335
- full_response = ""
336
- for response_chunk in respond_with_enhanced_streaming(message, history):
337
- full_response = response_chunk
338
- # Update the last message (bot's response)
339
- if len(history) > 0 and history[-1]["role"] == "user":
340
- history.append({"role": "assistant", "content": full_response})
341
- else:
342
- history[-1] = {"role": "assistant", "content": full_response}
343
- yield history, ""
344
-
345
- def clear_chat():
346
- """Clear the chat history."""
347
- return [], ""
348
-
349
- # --- UI: Interface Creation ---
350
  def create_interface():
351
- """Creates and configures the complete Gradio interface."""
352
 
353
- with gr.Blocks(
354
- title="EduBot",
355
- fill_width=True,
356
- fill_height=True,
357
- theme=gr.themes.Origin(),
358
- css="./styles.css"
359
- ) as demo:
360
- # Add head content and MathJax
361
- gr.HTML(html_head_content)
362
- gr.HTML('<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>')
363
- gr.HTML(mathjax_config)
364
 
365
- with gr.Column(elem_classes=["main-container"]):
366
- # Title Section
367
- gr.HTML('<div class="title-header"><h1>🎓 EduBot</h1></div>')
368
-
369
- # Chat Section
370
- with gr.Row():
371
- chatbot = gr.Chatbot(
372
- type="messages",
373
- show_copy_button=True,
374
- show_share_button=False,
375
- avatar_images=None,
376
- elem_id="main-chatbot",
377
- container=False,
378
- scale=1,
379
- height="70vh"
380
- )
381
-
382
- # Input Section - fixed height
383
- with gr.Row(elem_classes=["input-controls"]):
384
- msg = gr.Textbox(
385
- placeholder="Ask me about math, research, study strategies, or any educational topic...",
386
- show_label=False,
387
- lines=4,
388
- max_lines=6,
389
- elem_classes=["input-textbox"],
390
- container=False,
391
- scale=4
392
- )
393
- with gr.Column(elem_classes=["button-column"], scale=1):
394
- send = gr.Button("Send", elem_classes=["send-button"], size="sm")
395
- clear = gr.Button("Clear", elem_classes=["clear-button"], size="sm")
396
-
397
- # Set up event handlers
398
- msg.submit(respond_and_update, [msg, chatbot], [chatbot, msg])
399
- send.click(respond_and_update, [msg, chatbot], [chatbot, msg])
400
- clear.click(clear_chat, outputs=[chatbot, msg])
401
 
402
- return demo
403
-
404
- # ===============================================================================
405
- # END UI CONFIGURATION SECTION
406
- # ===============================================================================
 
 
407
 
 
408
  if __name__ == "__main__":
409
- logger.info("Starting EduBot...")
410
- demo = create_interface()
411
- demo.launch(debug=True, share=True)
 
 
 
 
 
 
29
  "type": "function",
30
  "function": {
31
  "name": "create_graph",
32
+ "description": "Generates a plot (bar, line, or pie) and returns it as an HTML-formatted Base64-encoded image string. Use this tool when teaching concepts that benefit from visual representation, such as: statistical distributions, mathematical functions, data comparisons, survey results, grade analyses, scientific relationships, economic models, or any quantitative information that would be clearer with a graph. The data and labels arguments must be JSON-encoded strings.",
33
  "parameters": {
34
  "type": "object",
35
  "properties": {
 
75
  - **Suggest study strategies**: Recommend effective learning approaches for the subject matter
76
 
77
  ## Tool Usage
78
+ You have access to a create_graph tool. Use this tool naturally when a visual representation would enhance understanding or when discussing concepts that involve data, relationships, patterns, or quantitative information. Consider creating graphs for:
79
+ - Mathematical concepts (functions, distributions, relationships)
80
+ - Statistical examples and explanations
81
+ - Scientific data and relationships
82
+ - Practice problems involving graph interpretation
83
+ - Comparative analyses
84
+ - Economic models or business concepts
85
+ - Any situation where visualization aids comprehension
86
 
87
+ When using the create_graph tool, format data as JSON strings:
88
+ - data_json: '{"Category1": 25, "Category2": 40, "Category3": 35}'
89
+ - labels_json: '["Category1", "Category2", "Category3"]'
90
+
91
+ ## Function Calling Format
92
+ When you need to create a graph, use this exact format:
93
+
94
+ <function_call>
95
+ {"name": "create_graph", "arguments": {"data_json": "{\"key1\": value1, \"key2\": value2}", "labels_json": "[\"label1\", \"label2\"]", "plot_type": "bar|line|pie", "title": "Graph Title", "x_label": "X Axis Label", "y_label": "Y Axis Label"}}
96
+ </function_call>
97
+
98
+ The graph will be automatically generated and displayed in your response.
99
 
100
  ## Response Guidelines
101
  - **For math problems**: Explain concepts, provide formula derivations, and guide through problem-solving steps without computing final numerical answers
102
  - **For multiple-choice questions**: Discuss the concepts being tested and help students understand how to analyze options rather than identifying the correct choice
103
  - **For essays or written work**: Discuss research strategies, organizational techniques, and critical thinking approaches rather than providing content or thesis statements
104
  - **For factual questions**: Provide educational context and encourage students to synthesize information rather than stating direct answers
105
+ - Use graphs naturally when they would clarify or enhance your explanations
106
 
107
  ## Communication Guidelines
108
  - Maintain a supportive, non-judgmental tone in all interactions
 
116
 
117
  # --- Core Logic Functions ---
118
 
119
+ def execute_function_call(function_name, function_args):
120
+ """Execute the called function and return the result."""
121
+ if function_name == "create_graph":
122
+ try:
123
+ return generate_plot(
124
+ data_json=function_args.get("data_json", "{}"),
125
+ labels_json=function_args.get("labels_json", "[]"),
126
+ plot_type=function_args.get("plot_type", "bar"),
127
+ title=function_args.get("title", "Graph"),
128
+ x_label=function_args.get("x_label", ""),
129
+ y_label=function_args.get("y_label", "")
130
+ )
131
+ except Exception as e:
132
+ return f"<p style='color:red;'>Error creating graph: {str(e)}</p>"
133
+ else:
134
+ return f"<p style='color:red;'>Unknown function: {function_name}</p>"
135
+
136
+ def parse_function_calls(text):
137
+ """Parse function calls from model response."""
138
+ function_calls = []
139
+
140
+ # Look for function call patterns in the text
141
+ function_pattern = r'<function_call>(.*?)</function_call>'
142
+ matches = re.findall(function_pattern, text, re.DOTALL)
143
+
144
+ for match in matches:
145
+ try:
146
+ # Parse the function call JSON
147
+ call_data = json.loads(match.strip())
148
+ function_calls.append(call_data)
149
+ except json.JSONDecodeError:
150
+ continue
151
+
152
+ return function_calls
153
+
154
  def smart_truncate(text, max_length=3000):
155
  """Truncates text intelligently to the last full sentence or word."""
156
  if len(text) <= max_length:
 
161
  if len(sentences) > 1:
162
  return ' '.join(sentences[:-1]) + "... [Response truncated - ask for continuation]"
163
  # Otherwise, split by word
164
+ words = text[:max_length].split()
165
+ return ' '.join(words[:-1]) + "... [Response truncated]"
166
+
167
+ def generate_response_with_tools(messages, max_retries=3):
168
+ """Generate response with function calling capability."""
169
+
170
+ # Format messages for the API
171
+ formatted_messages = []
172
+ formatted_messages.append(f"System: {SYSTEM_MESSAGE}")
173
+
174
+ for msg in messages:
175
+ if msg["role"] == "user":
176
+ formatted_messages.append(f"User: {msg['content']}")
177
+ elif msg["role"] == "assistant":
178
+ formatted_messages.append(f"Assistant: {msg['content']}")
179
+
180
+ conversation = "\n\n".join(formatted_messages)
181
+ conversation += "\n\nAssistant: "
182
+
183
+ payload = {
184
+ "inputs": conversation,
185
+ "parameters": {
186
+ "max_new_tokens": 1000,
187
+ "temperature": 0.7,
188
+ "top_p": 0.9,
189
  "return_full_text": False
190
  }
191
  }
 
208
  result = response.json()
209
 
210
  if isinstance(result, list) and len(result) > 0:
211
+ raw_response = result[0].get('generated_text', '').strip()
212
+
213
+ # Process function calls
214
+ function_calls = parse_function_calls(raw_response)
215
+
216
+ for call in function_calls:
217
+ if call.get("name") == "create_graph":
218
+ # Execute the function call
219
+ html_result = execute_function_call("create_graph", call.get("arguments", {}))
220
+ # Replace the function call with the generated HTML
221
+ pattern = r'<function_call>.*?</function_call>'
222
+ raw_response = re.sub(pattern, html_result, raw_response, count=1, flags=re.DOTALL)
223
+
224
+ return smart_truncate(raw_response)
225
  else:
226
  return "I apologize, but I received an unexpected response format. Please try again."
227
 
228
+ except requests.exceptions.RequestException as e:
229
+ logger.error(f"Request failed (attempt {attempt + 1}): {e}")
230
  if attempt < max_retries - 1:
 
231
  time.sleep(2)
232
  continue
233
  else:
234
+ return f"I'm having trouble connecting right now. Please try again later. (Error: {e})"
235
+ except Exception as e:
236
+ logger.error(f"Unexpected error (attempt {attempt + 1}): {e}")
237
  if attempt < max_retries - 1:
 
238
  time.sleep(2)
239
  continue
240
  else:
241
+ return f"An unexpected error occurred: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ def chat_response(message, history):
244
+ """Process chat message and return response."""
245
  try:
246
+ # Track metrics
247
+ metrics_tracker.log_interaction(message, "user_query")
248
+
249
+ # Format conversation history
250
+ messages = []
251
+ for user_msg, bot_msg in history:
252
+ messages.append({"role": "user", "content": user_msg})
253
+ if bot_msg:
254
+ messages.append({"role": "assistant", "content": bot_msg})
255
+ messages.append({"role": "user", "content": message})
256
+
257
+ # Generate response with tool support
258
+ response = generate_response_with_tools(messages)
259
+
260
+ # Log metrics
261
+ metrics_tracker.log_interaction(response, "bot_response")
262
+
263
+ return response
264
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  except Exception as e:
266
+ logger.error(f"Error in chat_response: {e}")
267
+ return f"I apologize, but I encountered an error while processing your message: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ # --- Gradio Interface (UNCHANGED) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  def create_interface():
271
+ """Create and return the Gradio interface."""
272
 
273
+ with gr.Blocks(title="EduBot - AI Tutor", theme=gr.themes.Soft()) as interface:
 
 
 
 
 
 
 
 
 
 
274
 
275
+ gr.Markdown("# EduBot - Your AI Learning Companion")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
+ chatbot = gr.Chatbot(height=400, show_label=False)
278
+ msg = gr.Textbox(label="Ask me anything!", placeholder="Type your question here...")
279
+
280
+ msg.submit(chat_response, [msg, chatbot], [chatbot])
281
+ msg.submit(lambda: "", None, [msg])
282
+
283
+ return interface
284
 
285
+ # --- Main Execution ---
286
  if __name__ == "__main__":
287
+ try:
288
+ logger.info("Starting EduBot...")
289
+ interface = create_interface()
290
+ interface.queue()
291
+ interface.launch()
292
+ except Exception as e:
293
+ logger.error(f"Failed to launch EduBot: {e}")
294
+ raise