omarash2016 commited on
Commit
cc15b1a
Β·
verified Β·
1 Parent(s): 6cdd504

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +140 -48
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import sys
3
  import re
 
4
  import gradio as gr
5
  from langchain_openai import ChatOpenAI
6
  from langgraph.prebuilt import create_react_agent
@@ -40,7 +41,7 @@ BEHAVIOR GUIDELINES:
40
  (End of your main lesson text)
41
 
42
  ---SECTION: SUMMARY---
43
- (Provide 3-4 concise bullet points summarizing the key syntax, functions, or concepts learned in this lesson. This acts as a 'Cheat Sheet'.)
44
 
45
  ---SECTION: VIDEOS---
46
  (List 2-3 YouTube search queries or URLs relevant to the topic)
@@ -49,25 +50,25 @@ BEHAVIOR GUIDELINES:
49
  (List 2-3 documentation links or course names, e.g., RealPython, FreeCodeCamp)
50
 
51
  ---SECTION: QUIZ---
52
- (Create 2 short multiple-choice question. Use HTML <details> and <summary> tags to hide the answer. Example:
53
- **Question**: ...
54
- - A) ...
55
- - B) ...
56
- <details><summary>πŸ‘€ Reveal Answers</summary>Correct is A because...</details>)
57
  """
58
 
59
  def parse_agent_response(full_text):
60
  """
61
  Robust parsing using Regex to handle LLM formatting inconsistencies.
62
- Splits the single LLM response into 5 UI components: Chat, Summary, Videos, Articles, Quiz.
 
63
  """
64
  chat_content = full_text
65
 
66
- # Default values for "waiting" state
67
  summary = "### πŸ“ Key Takeaways\n*Ask a coding question to get a cheat sheet!*"
68
  videos = "### πŸ“Ί Recommended Videos\n*Ask a coding question to get recommendations!*"
69
  articles = "### πŸ“š Articles & Courses\n*Ask a coding question to get resources!*"
70
- quiz = "### 🧠 Quick Quiz\n*Ask a coding question to take a quiz!*"
71
 
72
  # Regex patterns
73
  summary_pattern = r"---SECTION:\s*SUMMARY\s*---"
@@ -76,10 +77,9 @@ def parse_agent_response(full_text):
76
  quiz_pattern = r"---SECTION:\s*QUIZ\s*---"
77
 
78
  try:
79
- # 1. Extract Chat vs Summary (first split)
80
  split_summary = re.split(summary_pattern, full_text, flags=re.IGNORECASE, maxsplit=1)
81
 
82
- # If SUMMARY is found
83
  if len(split_summary) > 1:
84
  chat_content = split_summary[0].strip()
85
  remaining_after_chat = split_summary[1]
@@ -115,27 +115,28 @@ def parse_agent_response(full_text):
115
  articles = f"### πŸ“š Articles & Courses\n{article_content}"
116
 
117
  if len(split_quiz) > 1:
118
- quiz_content = split_quiz[1].strip()
119
- if quiz_content:
120
- quiz = f"### 🧠 Quick Quiz\n{quiz_content}"
121
-
 
 
 
 
 
122
 
123
  elif "---SECTION: VIDEOS---" in full_text:
 
124
  split_video_fallback = re.split(video_pattern, full_text, flags=re.IGNORECASE, maxsplit=1)
125
  chat_content = split_video_fallback[0].strip()
126
 
127
-
128
  except Exception as e:
129
  print(f"Parsing error: {e}")
130
  chat_content = full_text
131
 
132
- return chat_content, summary, videos, articles, quiz
133
 
134
  async def run_tutor_dashboard(user_message, model_id):
135
- """
136
- Main function to run the agent loop.
137
- Accepts 'model_id' to select the LLM dynamically.
138
- """
139
  server_params = StdioServerParameters(
140
  command=sys.executable,
141
  args=["server.py"],
@@ -147,7 +148,6 @@ async def run_tutor_dashboard(user_message, model_id):
147
  await session.initialize()
148
  tools = await load_mcp_tools(session)
149
 
150
- # Use the selected model_id
151
  llm = ChatOpenAI(
152
  api_key=NEBIUS_API_KEY,
153
  base_url=NEBIUS_BASE_URL,
@@ -168,6 +168,27 @@ async def run_tutor_dashboard(user_message, model_id):
168
 
169
  return parse_agent_response(final_text)
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  # --- Gradio Dashboard UI ---
172
  theme = gr.themes.Soft(
173
  primary_hue="slate",
@@ -194,6 +215,9 @@ custom_css = """
194
  """
195
 
196
  with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custom_css) as demo:
 
 
 
197
  # --- Header with Model Selector ---
198
  with gr.Row(variant="compact", elem_classes="header-row"):
199
  with gr.Column(scale=1):
@@ -202,7 +226,7 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custo
202
  with gr.Column(scale=0, min_width=250):
203
  model_selector = gr.Dropdown(
204
  choices=AVAILABLE_MODELS,
205
- value=AVAILABLE_MODELS[0], # Default to 20b
206
  label="Select Model",
207
  show_label=False,
208
  container=True,
@@ -236,7 +260,6 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custo
236
  )
237
  submit_btn = gr.Button("πŸš€ Start", variant="primary", scale=1)
238
 
239
- # Quick Examples
240
  gr.Examples(
241
  examples=[
242
  "Hello! I'm new to Python.",
@@ -247,9 +270,9 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custo
247
  inputs=msg
248
  )
249
 
250
- # Right Column: Resources Dashboard (Side View - Visible by default)
251
  with gr.Column(scale=2) as right_col:
252
- gr.Markdown("### πŸŽ’ Learning Dashboard:", elem_classes="tight-header")
253
 
254
  summary_box_side = gr.Markdown(value="### πŸ“ Key Takeaways\n*Ask a topic to get a cheat sheet!*", elem_classes="tight-content")
255
 
@@ -261,12 +284,23 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custo
261
  article_box_side = gr.Markdown(value="### Articles & Docs\n*Ask a topic to see reading materials!*")
262
 
263
  with gr.TabItem("🧠 Quiz"):
264
- quiz_box_side = gr.Markdown(value="### Knowledge Check\n*Ask a topic to unlock the quiz!*")
 
 
 
 
 
 
 
 
 
 
 
265
 
266
- # Bottom Row: Resources Dashboard (Bottom View - Hidden by default)
267
  with gr.Row(visible=False) as bottom_dashboard:
268
  with gr.Column():
269
- gr.Markdown("### πŸŽ’ Learning Dashboard:")
270
 
271
  summary_box_bottom = gr.Markdown(value="### πŸ“ Key Takeaways\n*Ask a topic to get a cheat sheet!*")
272
 
@@ -278,44 +312,76 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custo
278
  article_box_bottom = gr.Markdown(value="### Articles & Docs\n*Ask a topic to see reading materials!*")
279
 
280
  with gr.TabItem("🧠 Quiz"):
281
- quiz_box_bottom = gr.Markdown(value="### Knowledge Check\n*Ask a topic to unlock the quiz!*")
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  # --- Interaction Logic ---
284
  async def respond(user_message, history, model_id):
285
  if history is None: history = []
286
 
287
- # Immediate user update
288
  history.append({"role": "user", "content": user_message})
289
- # Placeholder for AI
290
  history.append({"role": "assistant", "content": f"Thinking (using {model_id})..."})
291
 
292
- # Yield placeholders to ALL output boxes (Side AND Bottom)
293
- # We now have 5 outputs per view (Chat + 4 tabs)
294
- # Output order: Chat, Summary, Videos, Articles, Quiz (x2 for layouts)
295
- yield history, "", "", "", "", "", "", "", "", ""
 
 
 
 
 
 
296
 
297
- # Run Agent with SELECTED MODEL
298
- chat_text, summary_text, video_text, article_text, quiz_text = await run_tutor_dashboard(user_message, model_id)
299
 
300
- # Update AI response
301
  history[-1]["content"] = chat_text
302
 
303
- # Yield final content to ALL output boxes
304
- yield history, "", summary_text, video_text, article_text, quiz_text, summary_text, video_text, article_text, quiz_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
  # --- Focus Mode Logic ---
307
  is_fullscreen = gr.State(False)
308
 
309
  def toggle_fullscreen(current_state):
310
  new_state = not current_state
311
- # If new_state is True (Fullscreen): Hide side col, Show bottom row
312
- # If False (Normal): Show side col, Hide bottom row
313
-
314
  side_visible = not new_state
315
  bottom_visible = new_state
316
-
317
  btn_text = "↩ Exit Focus" if new_state else "β›Ά Focus Mode"
318
-
319
  return new_state, gr.Column(visible=side_visible), gr.Row(visible=bottom_visible), btn_text
320
 
321
  fullscreen_btn.click(
@@ -324,10 +390,36 @@ with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custo
324
  outputs=[is_fullscreen, right_col, bottom_dashboard, fullscreen_btn]
325
  )
326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  outputs_list = [
328
  chatbot, msg,
329
- summary_box_side, video_box_side, article_box_side, quiz_box_side,
330
- summary_box_bottom, video_box_bottom, article_box_bottom, quiz_box_bottom
 
 
 
331
  ]
332
 
333
  submit_btn.click(
 
1
  import os
2
  import sys
3
  import re
4
+ import json
5
  import gradio as gr
6
  from langchain_openai import ChatOpenAI
7
  from langgraph.prebuilt import create_react_agent
 
41
  (End of your main lesson text)
42
 
43
  ---SECTION: SUMMARY---
44
+ (Provide 3-4 concise bullet points summarizing the key syntax, functions, or concepts learned in this lesson.)
45
 
46
  ---SECTION: VIDEOS---
47
  (List 2-3 YouTube search queries or URLs relevant to the topic)
 
50
  (List 2-3 documentation links or course names, e.g., RealPython, FreeCodeCamp)
51
 
52
  ---SECTION: QUIZ---
53
+ Provide a valid JSON list of exactly 2 objects. Do not use Markdown code blocks.
54
+ [
55
+ {"question": "Question text here?", "options": ["Option A", "Option B", "Option C"], "correct_answer": "Option A", "explanation": "Brief explanation why."}
56
+ ]
 
57
  """
58
 
59
  def parse_agent_response(full_text):
60
  """
61
  Robust parsing using Regex to handle LLM formatting inconsistencies.
62
+ Splits the single LLM response into UI components.
63
+ Returns: chat_content, summary, videos, articles, quiz_data (list of dicts)
64
  """
65
  chat_content = full_text
66
 
67
+ # Default values
68
  summary = "### πŸ“ Key Takeaways\n*Ask a coding question to get a cheat sheet!*"
69
  videos = "### πŸ“Ί Recommended Videos\n*Ask a coding question to get recommendations!*"
70
  articles = "### πŸ“š Articles & Courses\n*Ask a coding question to get resources!*"
71
+ quiz_data = [] # Empty list for interactive quiz
72
 
73
  # Regex patterns
74
  summary_pattern = r"---SECTION:\s*SUMMARY\s*---"
 
77
  quiz_pattern = r"---SECTION:\s*QUIZ\s*---"
78
 
79
  try:
80
+ # 1. Extract Chat vs Summary
81
  split_summary = re.split(summary_pattern, full_text, flags=re.IGNORECASE, maxsplit=1)
82
 
 
83
  if len(split_summary) > 1:
84
  chat_content = split_summary[0].strip()
85
  remaining_after_chat = split_summary[1]
 
115
  articles = f"### πŸ“š Articles & Courses\n{article_content}"
116
 
117
  if len(split_quiz) > 1:
118
+ quiz_raw_json = split_quiz[1].strip()
119
+ # Attempt to parse JSON
120
+ try:
121
+ # Clean up potential markdown code blocks if the LLM ignored instructions
122
+ clean_json = quiz_raw_json.replace("```json", "").replace("```", "").strip()
123
+ quiz_data = json.loads(clean_json)
124
+ except json.JSONDecodeError as e:
125
+ print(f"Quiz JSON Error: {e}")
126
+ quiz_data = [] # Fallback
127
 
128
  elif "---SECTION: VIDEOS---" in full_text:
129
+ # Fallback parsing
130
  split_video_fallback = re.split(video_pattern, full_text, flags=re.IGNORECASE, maxsplit=1)
131
  chat_content = split_video_fallback[0].strip()
132
 
 
133
  except Exception as e:
134
  print(f"Parsing error: {e}")
135
  chat_content = full_text
136
 
137
+ return chat_content, summary, videos, articles, quiz_data
138
 
139
  async def run_tutor_dashboard(user_message, model_id):
 
 
 
 
140
  server_params = StdioServerParameters(
141
  command=sys.executable,
142
  args=["server.py"],
 
148
  await session.initialize()
149
  tools = await load_mcp_tools(session)
150
 
 
151
  llm = ChatOpenAI(
152
  api_key=NEBIUS_API_KEY,
153
  base_url=NEBIUS_BASE_URL,
 
168
 
169
  return parse_agent_response(final_text)
170
 
171
+ def check_quiz(ans1, ans2, quiz_data):
172
+ """Checks user answers against the quiz data."""
173
+ if not quiz_data or len(quiz_data) < 2:
174
+ return "��️ Quiz not loaded.", "⚠️ Quiz not loaded."
175
+
176
+ # Check Q1
177
+ q1 = quiz_data[0]
178
+ if ans1 == q1["correct_answer"]:
179
+ res1 = f"βœ… Correct! {q1['explanation']}"
180
+ else:
181
+ res1 = f"❌ Incorrect. The correct answer was **{q1['correct_answer']}**.\n\n{q1['explanation']}"
182
+
183
+ # Check Q2
184
+ q2 = quiz_data[1]
185
+ if ans2 == q2["correct_answer"]:
186
+ res2 = f"βœ… Correct! {q2['explanation']}"
187
+ else:
188
+ res2 = f"❌ Incorrect. The correct answer was **{q2['correct_answer']}**.\n\n{q2['explanation']}"
189
+
190
+ return gr.update(value=res1, visible=True), gr.update(value=res2, visible=True)
191
+
192
  # --- Gradio Dashboard UI ---
193
  theme = gr.themes.Soft(
194
  primary_hue="slate",
 
215
  """
216
 
217
  with gr.Blocks(title="AI Python Tutor", theme=theme, fill_height=True, css=custom_css) as demo:
218
+ # State to hold quiz data
219
+ quiz_state = gr.State([])
220
+
221
  # --- Header with Model Selector ---
222
  with gr.Row(variant="compact", elem_classes="header-row"):
223
  with gr.Column(scale=1):
 
226
  with gr.Column(scale=0, min_width=250):
227
  model_selector = gr.Dropdown(
228
  choices=AVAILABLE_MODELS,
229
+ value=AVAILABLE_MODELS[0],
230
  label="Select Model",
231
  show_label=False,
232
  container=True,
 
260
  )
261
  submit_btn = gr.Button("πŸš€ Start", variant="primary", scale=1)
262
 
 
263
  gr.Examples(
264
  examples=[
265
  "Hello! I'm new to Python.",
 
270
  inputs=msg
271
  )
272
 
273
+ # Right Column: Resources Dashboard (Side View)
274
  with gr.Column(scale=2) as right_col:
275
+ gr.Markdown("### πŸŽ’ Learning Dashboard", elem_classes="tight-header")
276
 
277
  summary_box_side = gr.Markdown(value="### πŸ“ Key Takeaways\n*Ask a topic to get a cheat sheet!*", elem_classes="tight-content")
278
 
 
284
  article_box_side = gr.Markdown(value="### Articles & Docs\n*Ask a topic to see reading materials!*")
285
 
286
  with gr.TabItem("🧠 Quiz"):
287
+ # Interactive Quiz UI (Side)
288
+ q1_text_side = gr.Markdown("*Quiz will appear here...*")
289
+ q1_radio_side = gr.Radio(choices=[], label="Select Answer")
290
+ q1_result_side = gr.Markdown(visible=False)
291
+
292
+ gr.Markdown("---")
293
+
294
+ q2_text_side = gr.Markdown("")
295
+ q2_radio_side = gr.Radio(choices=[], label="Select Answer")
296
+ q2_result_side = gr.Markdown(visible=False)
297
+
298
+ check_btn_side = gr.Button("βœ… Check Answers", variant="secondary")
299
 
300
+ # Bottom Row: Resources Dashboard (Focus Mode View)
301
  with gr.Row(visible=False) as bottom_dashboard:
302
  with gr.Column():
303
+ gr.Markdown("### πŸŽ’ Learning Dashboard")
304
 
305
  summary_box_bottom = gr.Markdown(value="### πŸ“ Key Takeaways\n*Ask a topic to get a cheat sheet!*")
306
 
 
312
  article_box_bottom = gr.Markdown(value="### Articles & Docs\n*Ask a topic to see reading materials!*")
313
 
314
  with gr.TabItem("🧠 Quiz"):
315
+ # Interactive Quiz UI (Bottom) - Mirroring functionality
316
+ q1_text_bottom = gr.Markdown("*Quiz will appear here...*")
317
+ q1_radio_bottom = gr.Radio(choices=[], label="Select Answer")
318
+ q1_result_bottom = gr.Markdown(visible=False)
319
+
320
+ gr.Markdown("---")
321
+
322
+ q2_text_bottom = gr.Markdown("")
323
+ q2_radio_bottom = gr.Radio(choices=[], label="Select Answer")
324
+ q2_result_bottom = gr.Markdown(visible=False)
325
+
326
+ check_btn_bottom = gr.Button("βœ… Check Answers", variant="secondary")
327
 
328
  # --- Interaction Logic ---
329
  async def respond(user_message, history, model_id):
330
  if history is None: history = []
331
 
 
332
  history.append({"role": "user", "content": user_message})
 
333
  history.append({"role": "assistant", "content": f"Thinking (using {model_id})..."})
334
 
335
+ # Yield placeholders
336
+ # Outputs: History, Chatbox, Side boxes, Bottom boxes, QUIZ STATE, Quiz Inputs (Side x2, Bottom x2), Quiz Texts (Side x2, Bottom x2)
337
+ # We need to yield updates for EVERYTHING.
338
+ # Simplified: We yield empty strings/defaults for now.
339
+ yield history, "", "", "", "", [], \
340
+ gr.update(), gr.update(), gr.update(), gr.update(), \
341
+ gr.update(), gr.update(), gr.update(), gr.update(), \
342
+ "", "", "", "", \
343
+ gr.update(), gr.update(), gr.update(), gr.update(), \
344
+ gr.update(), gr.update(), gr.update(), gr.update()
345
 
346
+ chat_text, summary_text, video_text, article_text, quiz_data = await run_tutor_dashboard(user_message, model_id)
 
347
 
 
348
  history[-1]["content"] = chat_text
349
 
350
+ # Prepare Quiz UI updates
351
+ if quiz_data and len(quiz_data) >= 2:
352
+ q1 = quiz_data[0]
353
+ q2 = quiz_data[1]
354
+
355
+ # Common updates for both views
356
+ q1_t = f"### 1. {q1['question']}"
357
+ q1_c = q1['options']
358
+ q2_t = f"### 2. {q2['question']}"
359
+ q2_c = q2['options']
360
+
361
+ # Reset results and set values
362
+ q_upd = [
363
+ gr.update(value=q1_t), gr.update(choices=q1_c, value=None, interactive=True), gr.update(value="", visible=False),
364
+ gr.update(value=q2_t), gr.update(choices=q2_c, value=None, interactive=True), gr.update(value="", visible=False)
365
+ ]
366
+ else:
367
+ # Empty updates if no quiz
368
+ q_upd = [gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()]
369
+
370
+ # Yield final content
371
+ # Note: We duplicate the quiz updates for Side and Bottom views
372
+ yield history, "", summary_text, video_text, article_text, quiz_data, \
373
+ q_upd[0], q_upd[1], q_upd[2], q_upd[3], q_upd[4], q_upd[5], \
374
+ summary_text, video_text, article_text, \
375
+ q_upd[0], q_upd[1], q_upd[2], q_upd[3], q_upd[4], q_upd[5]
376
 
377
  # --- Focus Mode Logic ---
378
  is_fullscreen = gr.State(False)
379
 
380
  def toggle_fullscreen(current_state):
381
  new_state = not current_state
 
 
 
382
  side_visible = not new_state
383
  bottom_visible = new_state
 
384
  btn_text = "↩ Exit Focus" if new_state else "β›Ά Focus Mode"
 
385
  return new_state, gr.Column(visible=side_visible), gr.Row(visible=bottom_visible), btn_text
386
 
387
  fullscreen_btn.click(
 
390
  outputs=[is_fullscreen, right_col, bottom_dashboard, fullscreen_btn]
391
  )
392
 
393
+ # --- Quiz Button Logic ---
394
+ # Side View Check
395
+ check_btn_side.click(
396
+ check_quiz,
397
+ inputs=[q1_radio_side, q2_radio_side, quiz_state],
398
+ outputs=[q1_result_side, q2_result_side]
399
+ )
400
+
401
+ # Bottom View Check
402
+ check_btn_bottom.click(
403
+ check_quiz,
404
+ inputs=[q1_radio_bottom, q2_radio_bottom, quiz_state],
405
+ outputs=[q1_result_bottom, q2_result_bottom]
406
+ )
407
+
408
+ # Output List Mapping (Order is critical for the 'respond' function)
409
+ # 1. Chatbot, 2. Msg
410
+ # 3. Side Summary, 4. Side Video, 5. Side Article
411
+ # 6. Quiz State
412
+ # 7-12. Side Quiz Components: Q1 Text, Q1 Radio, Q1 Result, Q2 Text, Q2 Radio, Q2 Result
413
+ # 13. Bottom Summary, 14. Bottom Video, 15. Bottom Article
414
+ # 16-21. Bottom Quiz Components: (Same order as Side)
415
+
416
  outputs_list = [
417
  chatbot, msg,
418
+ summary_box_side, video_box_side, article_box_side,
419
+ quiz_state,
420
+ q1_text_side, q1_radio_side, q1_result_side, q2_text_side, q2_radio_side, q2_result_side,
421
+ summary_box_bottom, video_box_bottom, article_box_bottom,
422
+ q1_text_bottom, q1_radio_bottom, q1_result_bottom, q2_text_bottom, q2_radio_bottom, q2_result_bottom
423
  ]
424
 
425
  submit_btn.click(