Samanfatima563474 commited on
Commit
a89fcf7
·
verified ·
1 Parent(s): 6bd2f6c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +398 -0
app.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import openai
3
+ import os
4
+ import sys
5
+ from io import StringIO
6
+ import traceback
7
+ import datetime
8
+ import pytz
9
+ import time
10
+ import random
11
+ import json
12
+
13
+ ### --- CONSTANTS & CONFIGURATION --- ###
14
+ MODEL_NAME = "openai/gpt-5-chat-latest"
15
+ MAX_HINTS = 15
16
+ # Set timezone to Pakistan Standard Time (PKT)
17
+ PAKISTAN_TZ = pytz.timezone('Asia/Karachi')
18
+
19
+ ### --- ROBUST API CLIENT SETUP --- ###
20
+ api_key = None
21
+ try:
22
+ # Preferred method for cloud environments like Google Colab
23
+ from google.colab import userdata
24
+ api_key = userdata.get('AIML_API_KEY')
25
+ print("Successfully found API key in Google Colab Secrets.")
26
+ except (ImportError, KeyError):
27
+ # Fallback for local development
28
+ api_key = os.getenv("AIML_API_KEY")
29
+ if api_key:
30
+ print("Successfully found API key in environment variables.")
31
+
32
+ if not api_key:
33
+ print("FATAL ERROR: API Key not found. Please set the 'AIML_API_KEY' in your environment or secrets.")
34
+
35
+ # Initialize the OpenAI client with the custom base URL and your API key
36
+ client = openai.OpenAI(
37
+ base_url="https://api.aimlapi.com/v1",
38
+ api_key=api_key,
39
+ )
40
+
41
+
42
+ ### --- HELPER & AI FUNCTIONS --- ###
43
+
44
+ def make_api_call(system_prompt, user_prompt, timeout=120.0):
45
+ """
46
+ A single, robust function for all API calls.
47
+ """
48
+ if not api_key:
49
+ return {"success": False, "error": "API Key is not configured."}
50
+ try:
51
+ response = client.chat.completions.create(
52
+ model=MODEL_NAME,
53
+ messages=[
54
+ {"role": "system", "content": system_prompt},
55
+ {"role": "user", "content": user_prompt}
56
+ ],
57
+ timeout=timeout
58
+ )
59
+ if response.choices and response.choices[0].message and response.choices[0].message.content:
60
+ return {"success": True, "content": response.choices[0].message.content}
61
+ else:
62
+ return {"success": False, "error": "The AI returned an empty response."}
63
+ except Exception as e:
64
+ return {"success": False, "error": f"An API error occurred: {e}"}
65
+
66
+ def generate_dynamic_curriculum(topic):
67
+ """
68
+ Asks the AI to generate a full curriculum and practice problems as a JSON object.
69
+ """
70
+ system_prompt = """
71
+ You are an expert Python programming instructor and curriculum designer. Your task is to generate a complete, interactive learning module based on a user-provided topic. You MUST respond with ONLY a valid JSON object and nothing else. Do not include any introductory text, markdown formatting like ```json, or any explanations outside of the JSON structure.
72
+
73
+ The JSON object must have two top-level keys: "curriculum" and "practice_problems".
74
+
75
+ 1. **curriculum**: This must be a JSON list of 2 to 3 lesson objects. Each lesson object must have:
76
+ - "module": A string with the name of the overall topic (e.g., "Mastering Dictionaries").
77
+ - "title": A string for the specific lesson title (e.g., "Lesson 1: What is a Dictionary?").
78
+ - "explanation": A concise, simple explanation of the concept (1-3 sentences).
79
+ - "check": A JSON object containing:
80
+ - "type": The string "mcq".
81
+ - "question": A string with a clear question.
82
+ - "options": A JSON list of 3 string options.
83
+ - "answer": A string that exactly matches one of the options.
84
+
85
+ 2. **practice_problems**: This must be a JSON list containing exactly ONE practice problem object. This object must have:
86
+ - "title": A string (e.g., "Practice: Word Frequency Counter").
87
+ - "problem_statement": A clear problem description.
88
+ - "starter_code": A string of Python code for the user to start with.
89
+ - "check_type": The string "variable".
90
+ - "check_variable": The name of the variable to check for the final answer.
91
+ - "expected_result": The correct final value of the check_variable (can be a number, string, list, or dictionary).
92
+ - "background_knowledge": A brief hint about the overall approach.
93
+ """
94
+ user_prompt = f"Generate a learning module for the Python topic: '{topic}'"
95
+
96
+ result = make_api_call(system_prompt, user_prompt)
97
+
98
+ if result["success"]:
99
+ if not result["content"] or not result["content"].strip():
100
+ return {"success": False, "error": "The AI returned an empty response. This might be a temporary issue. Please try again."}
101
+ try:
102
+ json_string = result["content"].strip()
103
+ if json_string.startswith("```json"):
104
+ json_string = json_string[7:-3].strip()
105
+
106
+ data = json.loads(json_string)
107
+ if "curriculum" in data and "practice_problems" in data:
108
+ return {"success": True, "data": data}
109
+ else:
110
+ return {"success": False, "error": "AI response was missing the required 'curriculum' or 'practice_problems' keys."}
111
+ except json.JSONDecodeError as e:
112
+ return {"success": False, "error": f"AI returned invalid JSON. Error: {e}\n\nRaw response received:\n{result['content']}"}
113
+ else:
114
+ return {"success": False, "error": result["error"]}
115
+
116
+
117
+ def get_socratic_hint(problem, user_code, actual_result, error_message=None):
118
+ system_prompt = "You are a Socratic Python tutor. Your goal is to help a student solve a problem by asking guiding questions, never by giving the direct answer. Analyze the user's code and their incorrect result or error. Identify the logical flaw. Then, formulate a short, encouraging hint that makes them think about that specific flaw. DO NOT write any code. DO NOT give away the solution. Ask a question that leads them to the right way of thinking."
119
+
120
+ if error_message:
121
+ user_prompt = f"The problem is:\n---\n{problem['problem_statement']}\n---\nThe student's code is:\n---\n{user_code}\n---\nThis code failed to run and produced this error: `{error_message}`.\nPlease provide a Socratic hint to help them fix the error."
122
+ else:
123
+ user_prompt = f"The problem is:\n---\n{problem['problem_statement']}\n---\nThe student's code is:\n---\n{user_code}\n---\nThis code produced the result `{actual_result}` instead of the expected `{problem['expected_result']}`.\nPlease provide a Socratic hint."
124
+
125
+ result = make_api_call(system_prompt, user_prompt)
126
+ return result["content"] if result["success"] else f"Error getting hint: {result['error']}"
127
+
128
+ def execute_practice_code(user_code):
129
+ try:
130
+ local_scope = {}
131
+ exec(user_code, {}, local_scope)
132
+ return {"success": True, "scope": local_scope, "error": None}
133
+ except Exception:
134
+ error_string = traceback.format_exc()
135
+ return {"success": False, "scope": None, "error": error_string}
136
+
137
+ def get_code_coaching(problem, user_code):
138
+ system_prompt = "You are an expert Python developer acting as a code coach. A student has written the following correct code. Your job is to refactor it to be more efficient, readable, or 'Pythonic' (the way a professional would write it). Then, explain your changes clearly using markdown. First show the improved code in a block, then explain the key improvements in a bulleted list."
139
+ user_prompt = f"The original problem was:\n---\n{problem['problem_statement']}\n---\nHere is the student's correct solution:\n---\n```python\n{user_code}\n```\n\nPlease refactor this code and explain the improvements."
140
+
141
+ result = make_api_call(system_prompt, user_prompt)
142
+ return result["content"] if result["success"] else f"Error getting coaching feedback: {result['error']}"
143
+
144
+
145
+ ### --- GRADIO APP WITH DYNAMIC TUTOR --- ###
146
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
147
+ time_str = "Thursday, August 21, 2025 at 09:30 PM PKT"
148
+
149
+ gr.Markdown(f"# Welcome to the AI Python Master\n*Current Location: Jhang, Punjab, Pakistan. Current Time: {time_str}*")
150
+
151
+ # --- STATE MANAGEMENT ---
152
+ dynamic_curriculum = gr.State([])
153
+ dynamic_practice_problems = gr.State([])
154
+ lesson_index = gr.State(0)
155
+ practice_attempts = gr.State(0)
156
+
157
+ # --- UI for Topic Selection ---
158
+ with gr.Column(visible=True) as topic_selection_ui:
159
+ gr.Markdown("## What Python concept do you want to master today?")
160
+ topic_input = gr.Textbox(label="Enter a Topic", placeholder="e.g., Python Lists, Dictionaries, For Loops, etc.")
161
+ generate_lesson_btn = gr.Button("Generate Lesson Plan")
162
+ generation_status = gr.Markdown("")
163
+
164
+ # --- UI for Dynamic Learning ---
165
+ with gr.Column(visible=False) as learning_ui:
166
+ # --- Learn Section ---
167
+ progress_bar = gr.Markdown("")
168
+ lesson_progress = gr.Markdown("")
169
+ explanation_box = gr.Markdown("")
170
+ with gr.Column() as check_ui:
171
+ check_question = gr.Markdown("")
172
+ mcq_input = gr.Radio(label="Select your answer")
173
+ submit_button = gr.Button("Submit Answer")
174
+ lesson_feedback_box = gr.Markdown("")
175
+ next_button = gr.Button("➡️ Continue", visible=False)
176
+
177
+ # --- Practice Section ---
178
+ with gr.Column(visible=False) as practice_ui:
179
+ gr.Markdown("--- \n ## Practice Arena")
180
+ with gr.Row():
181
+ with gr.Column(scale=2):
182
+ practice_problem_statement = gr.Markdown("")
183
+ practice_code_input = gr.Code(language="python", label="Your Code")
184
+ with gr.Column(scale=1):
185
+ coach_feedback_box = gr.Markdown(visible=False)
186
+
187
+ submit_practice_button = gr.Button("Run Code")
188
+ hint_button = gr.Button("🤔 Get a Hint", visible=False)
189
+ coach_button = gr.Button("🤖 Coach Me: Improve My Code", visible=False)
190
+ practice_feedback_box = gr.Markdown("")
191
+
192
+ success_message = gr.Markdown("🎉 **Congratulations! You have finished this module!** 🎉", visible=False)
193
+ start_new_topic_btn = gr.Button("Learn Another Topic", visible=False)
194
+
195
+ # --- LOGIC FOR DYNAMIC TUTOR ---
196
+ def generate_and_start_lesson(topic, progress=gr.Progress()):
197
+ progress(0, desc="Asking the AI to design your lesson... This may take a moment.")
198
+
199
+ result = generate_dynamic_curriculum(topic)
200
+
201
+ if not result["success"]:
202
+ return {
203
+ topic_selection_ui: gr.update(visible=True),
204
+ generation_status: gr.update(visible=True, value=f"❌ **Error:** {result['error']}"),
205
+ learning_ui: gr.update(visible=False)
206
+ }
207
+
208
+ new_curriculum = result["data"]["curriculum"]
209
+ new_practice = result["data"]["practice_problems"]
210
+
211
+ first_lesson_ui = update_ui_for_lesson(0, new_curriculum)
212
+
213
+ return {
214
+ dynamic_curriculum: new_curriculum,
215
+ dynamic_practice_problems: new_practice,
216
+ lesson_index: 0,
217
+ practice_attempts: 0,
218
+ learning_ui: gr.update(visible=True),
219
+ topic_selection_ui: gr.update(visible=False),
220
+ generation_status: gr.update(visible=False),
221
+ **first_lesson_ui
222
+ }
223
+
224
+ def update_ui_for_lesson(index, current_curriculum):
225
+ lesson = current_curriculum[index]
226
+ check_data = lesson.get("check", {}) # Safer access
227
+
228
+ total_lessons = len(current_curriculum)
229
+ completed_lessons = index
230
+ percentage = int((completed_lessons / total_lessons) * 100)
231
+ bar_length = 30
232
+ filled_length = int(bar_length * percentage / 100)
233
+ bar = '█' * filled_length + '░' * (bar_length - filled_length)
234
+ progress_text = f"**Progress:** {bar} {percentage}% ({completed_lessons}/{total_lessons} Lessons)"
235
+
236
+ return {
237
+ lesson_index: index,
238
+ lesson_progress: f"### Module: {lesson.get('module', 'Module')} - Lesson {index + 1} / {total_lessons}",
239
+ explanation_box: lesson.get('explanation', ''),
240
+ check_ui: gr.update(visible=True),
241
+ check_question: f"**Question:** {check_data.get('question', '')}",
242
+ mcq_input: gr.update(choices=check_data.get('options', []), value=None),
243
+ lesson_feedback_box: "",
244
+ next_button: gr.update(visible=False),
245
+ progress_bar: progress_text
246
+ }
247
+
248
+ def process_answer(index, mcq_answer, current_curriculum):
249
+ lesson = current_curriculum[index]
250
+ check_data = lesson.get("check", {})
251
+ is_correct = str(mcq_answer).strip().lower() == str(check_data.get('answer', '')).strip().lower()
252
+ if is_correct:
253
+ return { lesson_feedback_box: "✅ **Correct!**", check_ui: gr.update(visible=False), next_button: gr.update(visible=True) }
254
+ else:
255
+ return { lesson_feedback_box: "❌ Not quite. Try again.", check_ui: gr.update(visible=True), next_button: gr.update(visible=False) }
256
+
257
+ def on_next_lesson(index, current_curriculum, current_practice):
258
+ new_index = index + 1
259
+ if new_index < len(current_curriculum):
260
+ return update_ui_for_lesson(new_index, current_curriculum)
261
+ else:
262
+ # End of lessons, show practice problem
263
+ practice_problem = current_practice[0]
264
+ total_lessons = len(current_curriculum)
265
+ bar = '█' * 30
266
+ progress_text = f"**Progress:** {bar} 100% ({total_lessons}/{total_lessons} Lessons)"
267
+
268
+ return {
269
+ lesson_progress: "### All Lessons Complete! Now for the Practice Arena.",
270
+ explanation_box: "",
271
+ check_ui: gr.update(visible=False),
272
+ next_button: gr.update(visible=False),
273
+ practice_ui: gr.update(visible=True),
274
+ practice_problem_statement: practice_problem["problem_statement"],
275
+ practice_code_input: practice_problem["starter_code"],
276
+ progress_bar: progress_text,
277
+ lesson_index: new_index
278
+ }
279
+
280
+ def process_practice_answer(user_code, current_practice):
281
+ problem = current_practice[0]
282
+ result = execute_practice_code(user_code)
283
+
284
+ attempts = 0
285
+ if not result['success']:
286
+ return {
287
+ practice_feedback_box: f"❌ **Code Error:**\n\n```\n{result['error']}\n```",
288
+ hint_button: gr.update(visible=True), coach_button: gr.update(visible=False),
289
+ practice_attempts: attempts
290
+ }
291
+
292
+ var_name = problem['check_variable']
293
+ expected_value = problem['expected_result']
294
+ actual_value = result['scope'].get(var_name, f"Variable '{var_name}' not found!")
295
+
296
+ if str(actual_value) == str(expected_value):
297
+ return {
298
+ practice_feedback_box: f"✅ **Success!** The variable `{var_name}` has the correct value. Great job! Now, let the AI Code Coach show you how to improve it.",
299
+ hint_button: gr.update(visible=False), coach_button: gr.update(visible=True),
300
+ practice_attempts: 0
301
+ }
302
+ else:
303
+ return {
304
+ practice_feedback_box: f"�� **Incorrect Result.** Your code ran, but `{var_name}` was `{actual_value}` instead of `{expected_value}`. Try again, or ask for a hint.",
305
+ hint_button: gr.update(visible=True), coach_button: gr.update(visible=False),
306
+ practice_attempts: attempts
307
+ }
308
+
309
+ def provide_hint(user_code, current_practice, attempts):
310
+ new_attempts = attempts + 1
311
+ problem = current_practice[0]
312
+
313
+ if new_attempts > MAX_HINTS:
314
+ return {
315
+ practice_feedback_box: f"**Hint Limit Reached ({MAX_HINTS}/{MAX_HINTS}).**",
316
+ hint_button: gr.update(visible=False),
317
+ practice_attempts: new_attempts
318
+ }
319
+
320
+ result = execute_practice_code(user_code)
321
+
322
+ if new_attempts == 1 and result['success']:
323
+ hint = problem.get("background_knowledge", "No background knowledge available.")
324
+ else:
325
+ actual_value = "an error"
326
+ if result['success']:
327
+ actual_value = result['scope'].get(problem['check_variable'], "Not Found")
328
+ hint = get_socratic_hint(problem, user_code, actual_value, error_message=result['error'])
329
+
330
+ return {
331
+ practice_feedback_box: f"**(Hint {new_attempts}/{MAX_HINTS})** {hint}",
332
+ hint_button: gr.update(visible=True),
333
+ practice_attempts: new_attempts
334
+ }
335
+
336
+ def coach_code(user_code, current_practice):
337
+ problem = current_practice[0]
338
+ coaching_text = get_code_coaching(problem, user_code)
339
+ return {
340
+ coach_feedback_box: gr.update(visible=True, value=coaching_text),
341
+ coach_button: gr.update(interactive=False),
342
+ success_message: gr.update(visible=True),
343
+ start_new_topic_btn: gr.update(visible=True)
344
+ }
345
+
346
+ def reset_to_start():
347
+ return {
348
+ topic_selection_ui: gr.update(visible=True),
349
+ learning_ui: gr.update(visible=False),
350
+ practice_ui: gr.update(visible=False),
351
+ success_message: gr.update(visible=False),
352
+ start_new_topic_btn: gr.update(visible=False),
353
+ coach_feedback_box: gr.update(visible=False, value=""),
354
+ coach_button: gr.update(visible=False, interactive=True),
355
+ hint_button: gr.update(visible=False),
356
+ practice_feedback_box: "",
357
+ generation_status: ""
358
+ }
359
+
360
+ # --- EVENT LISTENERS ---
361
+ generate_lesson_btn.click(
362
+ fn=generate_and_start_lesson,
363
+ inputs=[topic_input],
364
+ outputs=[topic_selection_ui, generation_status, dynamic_curriculum, dynamic_practice_problems, lesson_index, practice_attempts, learning_ui, lesson_progress, explanation_box, check_ui, check_question, mcq_input, lesson_feedback_box, next_button, progress_bar]
365
+ )
366
+ submit_button.click(
367
+ fn=process_answer,
368
+ inputs=[lesson_index, mcq_input, dynamic_curriculum],
369
+ outputs=[lesson_feedback_box, check_ui, next_button]
370
+ )
371
+ next_button.click(
372
+ fn=on_next_lesson,
373
+ inputs=[lesson_index, dynamic_curriculum, dynamic_practice_problems],
374
+ outputs=[lesson_progress, explanation_box, check_ui, next_button, practice_ui, practice_problem_statement, practice_code_input, progress_bar, lesson_index]
375
+ )
376
+ submit_practice_button.click(
377
+ fn=process_practice_answer,
378
+ inputs=[practice_code_input, dynamic_practice_problems],
379
+ outputs=[practice_feedback_box, hint_button, coach_button, practice_attempts]
380
+ )
381
+ hint_button.click(
382
+ fn=provide_hint,
383
+ inputs=[practice_code_input, dynamic_practice_problems, practice_attempts],
384
+ outputs=[practice_feedback_box, hint_button, practice_attempts]
385
+ )
386
+ coach_button.click(
387
+ fn=coach_code,
388
+ inputs=[practice_code_input, dynamic_practice_problems],
389
+ outputs=[coach_feedback_box, coach_button, success_message, start_new_topic_btn]
390
+ )
391
+ start_new_topic_btn.click(
392
+ fn=reset_to_start,
393
+ inputs=None,
394
+ outputs=[topic_selection_ui, learning_ui, practice_ui, success_message, start_new_topic_btn, coach_feedback_box, coach_button, hint_button, practice_feedback_box, generation_status]
395
+ )
396
+
397
+ if __name__ == "__main__":
398
+ demo.launch(debug=True)