Prerit018 commited on
Commit
6bc3db2
·
verified ·
1 Parent(s): f8ad0d1

Upload 26 files

Browse files
.env ADDED
@@ -0,0 +1 @@
 
 
1
+ OPENAI_API_KEY=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
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
README.md CHANGED
@@ -1,12 +1,36 @@
1
- ---
2
- title: Version1
3
- emoji: 🔥
4
- colorFrom: gray
5
- colorTo: indigo
6
- sdk: gradio
7
- sdk_version: 6.3.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Teacher Bot
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: "3.44"
8
+ app_file: app.py
9
+ pinned: true
10
+ ---
11
+
12
+ # AI Teacher Bot
13
+
14
+ **Interactive Personalized Learning Platform**
15
+
16
+ AI Teacher Bot is an interactive learning platform that:
17
+ - Teaches modules with detailed explanations adapted to the user's learning level (Novice, Intermediate, Advanced)
18
+ - Evaluates student examples and provides constructive feedback
19
+ - Performs Bloom’s Taxonomy assessments per chapter to measure understanding
20
+ - Generates structured, level-appropriate curriculum automatically
21
+
22
+ **Usage**
23
+ 1. Enter a topic and select your claimed level.
24
+ 2. Click “Start Session” to begin learning.
25
+ 3. Follow module explanations and submit examples.
26
+ 4. Complete Bloom assessments to evaluate mastery.
27
+
28
+ **Tech Stack**
29
+ - Python 3.12
30
+ - Gradio (for UI)
31
+ - OpenAI API (GPT-4o-mini)
32
+
33
+ **Notes**
34
+ - Ensure you have a valid OpenAI API key set in `.env`.
35
+ - For testing and temporary deployment, Gradio provides shareable links.
36
+ - For permanent hosting, this app runs on Hugging Face Spaces.
Untitled-2025-07-21-1300.svg ADDED
__pycache__/curriculum.cpython-312.pyc ADDED
Binary file (3.45 kB). View file
 
__pycache__/main.cpython-312.pyc ADDED
Binary file (7.17 kB). View file
 
__pycache__/oai.cpython-312.pyc ADDED
Binary file (803 Bytes). View file
 
__pycache__/streamlit_app.cpython-312.pyc ADDED
Binary file (56 kB). View file
 
__pycache__/user_state.cpython-312.pyc ADDED
Binary file (6.85 kB). View file
 
agents/__pycache__/base_agent.cpython-312.pyc ADDED
Binary file (699 Bytes). View file
 
agents/__pycache__/bloom_assess.cpython-312.pyc ADDED
Binary file (9.94 kB). View file
 
agents/__pycache__/coordinator.cpython-312.pyc ADDED
Binary file (6.16 kB). View file
 
agents/__pycache__/curriculum_planner.cpython-312.pyc ADDED
Binary file (4.65 kB). View file
 
agents/__pycache__/level_assess.cpython-312.pyc ADDED
Binary file (13.8 kB). View file
 
agents/__pycache__/teacher.cpython-312.pyc ADDED
Binary file (11.7 kB). View file
 
agents/base_agent.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ class BaseAgent:
2
+ def __init__(self, name):
3
+ self.name = name
4
+
5
+ def process(self, *args, **kwargs):
6
+ raise NotImplementedError("Each agent must implement the process method.")
agents/bloom_assess.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ import openai
4
+ import json
5
+ from .base_agent import BaseAgent
6
+
7
+ load_dotenv()
8
+
9
+ class BloomsAssessmentAgent(BaseAgent):
10
+ def __init__(self):
11
+ super().__init__("BloomsAssessmentAgent")
12
+ api_key = os.getenv("OPENAI_API_KEY")
13
+ self.client = openai.OpenAI(api_key=api_key)
14
+ self.bloom_levels = [
15
+ "Remembering", "Understanding", "Applying",
16
+ "Analyzing", "Evaluating", "Creating"
17
+ ]
18
+
19
+ def generate_bloom_question(self, chapter, bloom_level):
20
+ system_prompt = (
21
+ f"You are an expert educator creating a {bloom_level} level question according to Bloom's Taxonomy. "
22
+ f"Generate ONE question that tests the student's ability at the {bloom_level} level for the given chapter. "
23
+ "The question should be clear, concise, and appropriate for the chapter content. "
24
+ "Return ONLY the question text, no additional formatting or explanation."
25
+ )
26
+
27
+ user_prompt = (
28
+ f"Chapter: {chapter.name}\n"
29
+ f"Modules in this chapter: {[m.name for m in chapter.modules]}\n"
30
+ f"Bloom's Level: {bloom_level}\n"
31
+ f"Generate a {bloom_level} level question."
32
+ )
33
+
34
+ response = self.client.chat.completions.create(
35
+ model="gpt-4o-mini",
36
+ messages=[
37
+ {"role": "system", "content": system_prompt},
38
+ {"role": "user", "content": user_prompt}
39
+ ]
40
+ )
41
+ return response.choices[0].message.content.strip()
42
+
43
+ def evaluate_bloom_answer(self, question, user_answer, bloom_level, chapter):
44
+ system_prompt = (
45
+ f"You are an expert educator evaluating a student's answer for a {bloom_level} level question. "
46
+ "Evaluate the answer based on the specific cognitive skills required for this Bloom's level. "
47
+ "Be STRICT about answer quality - vague, incomplete, or overly brief answers should receive low scores.\n\n"
48
+ "**Evaluation Criteria:**\n"
49
+ "- **Precision:** Is the answer specific and detailed enough for the Bloom's level?\n"
50
+ "- **Relevance:** Does it directly address the question asked?\n"
51
+ "- **Depth:** Does it demonstrate the expected cognitive complexity?\n"
52
+ "- **Completeness:** Are all parts of the question addressed?\n\n"
53
+ "**Automatic Penalties:**\n"
54
+ "- Answers under 15 words: Maximum score of 3\n"
55
+ "- Vague responses (yes/no, maybe, I think): Maximum score of 2\n"
56
+ "- Off-topic or irrelevant answers: Score of 0-1\n\n"
57
+ "You must respond with a JSON object containing:\n"
58
+ "1. 'score': A number between 0 and 10\n"
59
+ "2. 'feedback': Detailed explanation of the score and what was missing\n"
60
+ "3. 'level_achieved': The Bloom's level the answer demonstrates\n"
61
+ "4. 'hint': A subtle hint to guide toward better understanding (if score < 7)"
62
+ )
63
+
64
+ user_prompt = (
65
+ f"Question (Bloom's Level: {bloom_level}): {question}\n"
66
+ f"Student's Answer: {user_answer}\n"
67
+ f"Chapter Context: {chapter.name}\n"
68
+ "Evaluate this answer and provide the JSON response."
69
+ )
70
+
71
+ response = self.client.chat.completions.create(
72
+ model="gpt-4o-mini",
73
+ messages=[
74
+ {"role": "system", "content": system_prompt},
75
+ {"role": "user", "content": user_prompt}
76
+ ]
77
+ )
78
+ return response.choices[0].message.content.strip()
79
+
80
+ def process(self, chapter):
81
+ print(f"\n{'='*60}")
82
+ print(f"BLOOM'S TAXONOMY ASSESSMENT")
83
+ print(f"Chapter: {chapter.name}")
84
+ print(f"{'='*60}")
85
+ print("You will be asked 2 questions for each of the 6 Bloom's levels.")
86
+ print("This comprehensive assessment will evaluate your mastery of the chapter.")
87
+
88
+ results = {}
89
+ total_score = 0
90
+ questions_asked = 0
91
+
92
+ for bloom_level in self.bloom_levels:
93
+ level_scores = []
94
+ print(f"\n--- {bloom_level.upper()} LEVEL (2 Questions) ---")
95
+
96
+ for question_num in range(1, 3): # 2 questions per level
97
+ print(f"\n[Question {question_num}/2 for {bloom_level}]")
98
+
99
+ question = self.generate_bloom_question(chapter, bloom_level)
100
+ print(f"Question: {question}")
101
+
102
+ user_answer = input("Your answer: ")
103
+
104
+ evaluation_json = self.evaluate_bloom_answer(question, user_answer, bloom_level, chapter)
105
+
106
+ try:
107
+ evaluation = json.loads(evaluation_json)
108
+ score = evaluation.get('score', 0)
109
+ feedback = evaluation.get('feedback', 'No feedback provided')
110
+ level_achieved = evaluation.get('level_achieved', bloom_level)
111
+ hint = evaluation.get('hint', '')
112
+
113
+ print(f"Score: {score}/10")
114
+ print(f"Feedback: {feedback}")
115
+ print(f"Level Demonstrated: {level_achieved}")
116
+
117
+ # If score is low, provide hint and offer retry
118
+ if score < 7 and hint:
119
+ print(f"💡 Hint: {hint}")
120
+
121
+ retry = input("\nYour answer needs improvement. Would you like to try again? (y/n): ").lower().strip()
122
+ if retry == 'y':
123
+ print("\n🔄 Please provide a more detailed and specific answer.")
124
+ retry_answer = input("Your revised answer: ")
125
+
126
+ # Re-evaluate the retry answer
127
+ retry_evaluation_json = self.evaluate_bloom_answer(question, retry_answer, bloom_level, chapter)
128
+ try:
129
+ retry_evaluation = json.loads(retry_evaluation_json)
130
+ retry_score = retry_evaluation.get('score', 0)
131
+ retry_feedback = retry_evaluation.get('feedback', 'No feedback provided')
132
+ retry_level = retry_evaluation.get('level_achieved', bloom_level)
133
+
134
+ print(f"\nRetry Score: {retry_score}/10")
135
+ print(f"Retry Feedback: {retry_feedback}")
136
+ print(f"Retry Level Demonstrated: {retry_level}")
137
+
138
+ # Use the better of the two scores
139
+ final_score = max(score, retry_score)
140
+ level_scores.append(final_score)
141
+ total_score += final_score
142
+ questions_asked += 1
143
+
144
+ except json.JSONDecodeError:
145
+ print("Error evaluating retry answer. Using original score.")
146
+ level_scores.append(score)
147
+ total_score += score
148
+ questions_asked += 1
149
+ else:
150
+ level_scores.append(score)
151
+ total_score += score
152
+ questions_asked += 1
153
+ else:
154
+ level_scores.append(score)
155
+ total_score += score
156
+ questions_asked += 1
157
+
158
+ except json.JSONDecodeError:
159
+ print("Error evaluating answer. Defaulting to score 5.")
160
+ level_scores.append(5)
161
+ total_score += 5
162
+ questions_asked += 1
163
+
164
+ # Store results for this Bloom's level
165
+ results[bloom_level] = {
166
+ 'scores': level_scores,
167
+ 'average_score': sum(level_scores) / len(level_scores),
168
+ 'total_score': sum(level_scores)
169
+ }
170
+
171
+ # Calculate overall results
172
+ max_possible = questions_asked * 10
173
+ average_score = total_score / questions_asked
174
+ percentage = (total_score / max_possible) * 100
175
+
176
+ print(f"\n{'='*60}")
177
+ print("COMPREHENSIVE ASSESSMENT RESULTS")
178
+ print(f"{'='*60}")
179
+ print(f"Total Score: {total_score}/{max_possible}")
180
+ print(f"Average Score: {average_score:.1f}/10")
181
+ print(f"Percentage: {percentage:.1f}%")
182
+
183
+ # Enhanced mastery determination
184
+ if percentage >= 85:
185
+ mastery = "Excellent Mastery"
186
+ recommendation = "Ready to advance to next level"
187
+ elif percentage >= 70:
188
+ mastery = "Good Understanding"
189
+ recommendation = "Ready to advance with some review"
190
+ elif percentage >= 55:
191
+ mastery = "Basic Understanding"
192
+ recommendation = "Review weak areas before advancing"
193
+ else:
194
+ mastery = "Needs Significant Review"
195
+ recommendation = "Revisit chapter content and retake assessment"
196
+
197
+ print(f"Mastery Level: {mastery}")
198
+ print(f"Recommendation: {recommendation}")
199
+
200
+ print(f"\nDetailed Breakdown by Bloom's Level:")
201
+ for level, result in results.items():
202
+ avg = result['average_score']
203
+ print(f" {level}: {avg:.1f}/10 (Scores: {result['scores']})")
204
+
205
+ # Determine if student should advance or review
206
+ should_advance = percentage >= 70
207
+
208
+ return {
209
+ 'chapter': chapter.name,
210
+ 'total_score': total_score,
211
+ 'max_possible': max_possible,
212
+ 'average_score': average_score,
213
+ 'percentage': percentage,
214
+ 'mastery_level': mastery,
215
+ 'recommendation': recommendation,
216
+ 'should_advance': should_advance,
217
+ 'detailed_results': results
218
+ }
agents/coordinator.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base_agent import BaseAgent
2
+ from .teacher import NoviceTeacherAgent, IntermediateTeacherAgent, AdvancedTeacherAgent
3
+ from .bloom_assess import BloomsAssessmentAgent
4
+ from curriculum import Chapter, Module
5
+
6
+ class CoordinatorAgent(BaseAgent):
7
+ def __init__(self, user_level, user_state):
8
+ super().__init__("CoordinatorAgent")
9
+ if user_level == "novice":
10
+ self.teacher = NoviceTeacherAgent()
11
+ elif user_level == "intermediate":
12
+ self.teacher = IntermediateTeacherAgent()
13
+ else:
14
+ self.teacher = AdvancedTeacherAgent()
15
+
16
+ self.bloom_assessor = BloomsAssessmentAgent()
17
+ self.user_state = user_state
18
+
19
+ def teach_curriculum(self, curriculum):
20
+ print("\n[System] Starting the teaching session!")
21
+ print(self.user_state.get_progress_summary(curriculum))
22
+
23
+ for chapter_idx, chapter in enumerate(curriculum.chapters, 1):
24
+ print(f"\n{'='*60}")
25
+ print(f"CHAPTER {chapter_idx}: {chapter.name}")
26
+ print(f"{'='*60}")
27
+
28
+ # Teach all modules in the chapter
29
+ for module_idx, module in enumerate(chapter.modules, 1):
30
+ while True:
31
+ print(f"\n--- Module {chapter_idx}.{module_idx}: {module.name} ---")
32
+ # Display the teaching content
33
+ explanation = self.teacher.teach_module(module)
34
+ print(f"\n📚 {explanation}")
35
+ if self.teacher.check_example(module):
36
+ print("[System] Good job! Moving to next module.")
37
+ # Update progress
38
+ self.user_state.update_module_progress(chapter.name, module.name, "completed")
39
+ break
40
+ else:
41
+ print("[System] Let's review this module again.")
42
+ self.user_state.update_module_progress(chapter.name, module.name, "reviewed")
43
+
44
+ # Chapter completed - run Bloom's assessment
45
+ print(f"\n[System] Chapter '{chapter.name}' completed! Time for a comprehensive assessment.")
46
+ assessment_results = self.bloom_assessor.process(chapter)
47
+
48
+ # Check if student should advance or review based on Bloom's assessment
49
+ should_advance = assessment_results.get('should_advance', False)
50
+ percentage = assessment_results.get('percentage', 0)
51
+
52
+ if should_advance:
53
+ print(f"\n🎉 Excellent work! You scored {percentage:.1f}% and demonstrated mastery.")
54
+ print("✅ You're ready to advance to the next chapter!")
55
+
56
+ # Update progress with assessment results
57
+ self.user_state.update_bloom_assessment(chapter.name, assessment_results)
58
+ self.user_state.update_chapter_progress(chapter.name, "completed")
59
+
60
+ # Show updated progress
61
+ print("\n" + self.user_state.get_progress_summary(curriculum))
62
+
63
+ # Ask if user wants to continue
64
+ if chapter_idx < len(curriculum.chapters):
65
+ continue_choice = input("\nContinue to next chapter? (y/n): ").lower().strip()
66
+ if continue_choice != 'y':
67
+ print("[System] Progress updated. You can resume later.")
68
+ return
69
+ else:
70
+ print(f"\n{'='*60}")
71
+ print("CONGRATULATIONS!")
72
+ print(f"{'='*60}")
73
+ print("You have completed the entire curriculum!")
74
+ print(self.user_state.get_progress_summary(curriculum))
75
+ else:
76
+ print(f"\n📚 You scored {percentage:.1f}% on the assessment.")
77
+ print("🔄 Based on your performance, let's review this chapter to strengthen your understanding.")
78
+
79
+ # Update progress but mark chapter as needing review
80
+ self.user_state.update_bloom_assessment(chapter.name, assessment_results)
81
+ self.user_state.update_chapter_progress(chapter.name, "needs_review")
82
+
83
+ # Ask if user wants to retake the chapter or continue anyway
84
+ review_choice = input("\nWould you like to:\n1. Review this chapter again (r)\n2. Continue to next chapter anyway (c)\n3. Save progress and exit (s)\nChoice: ").lower().strip()
85
+
86
+ if review_choice == 'r':
87
+ print(f"\n🔄 Let's review Chapter {chapter_idx}: {chapter.name}")
88
+ # Restart the chapter
89
+ chapter_idx -= 1 # Will be incremented at the end of the loop
90
+ elif review_choice == 'c':
91
+ print("⚠️ Continuing to next chapter. Consider reviewing weak areas later.")
92
+ if chapter_idx < len(curriculum.chapters):
93
+ continue_choice = input("\nContinue to next chapter? (y/n): ").lower().strip()
94
+ if continue_choice != 'y':
95
+ print("[System] Progress updated. You can resume later.")
96
+ return
97
+ else:
98
+ print(f"\n{'='*60}")
99
+ print("CURRICULUM COMPLETED!")
100
+ print(f"{'='*60}")
101
+ print("You have completed the entire curriculum!")
102
+ print("Note: Some chapters may need review for better mastery.")
103
+ print(self.user_state.get_progress_summary(curriculum))
104
+ else: # exit
105
+ print("[System] Progress updated. You can resume later.")
106
+ return
agents/curriculum_planner.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ import openai
4
+ import json
5
+ import re
6
+ from .base_agent import BaseAgent
7
+ from curriculum import Curriculum, Chapter, Module
8
+
9
+ class CurriculumPlannerAgent(BaseAgent):
10
+ def __init__(self):
11
+ super().__init__("CurriculumPlannerAgent")
12
+ load_dotenv()
13
+ api_key = os.getenv("OPENAI_API_KEY")
14
+ self.client = openai.OpenAI(api_key=api_key)
15
+
16
+ def process(self, topic, level):
17
+ system_prompt = (
18
+ "You are an Expert Instructional Architect. Design a detailed, academically rigorous curriculum.\n\n"
19
+
20
+ "### Rules:\n"
21
+ "1. Ensure academic depth and avoid trivial content.\n"
22
+ "2. Respect the level:\n"
23
+ "- Novice: fundamentals, 4–6 modules per chapter.\n"
24
+ "- Intermediate: problem-solving & applications, 5–7 modules per chapter.\n"
25
+ "- Advanced: theory, research, edge cases, 6–8 modules per chapter.\n"
26
+ "3. Each module must have:\n"
27
+ "- `module_name`\n"
28
+ "- `learning_objective` (with knowledge, skills, applications, examples)\n"
29
+ "4. Use progressive complexity.\n\n"
30
+
31
+ "### Example Curriculum (Novice, Topic: Python Programming)\n"
32
+ "{\n"
33
+ " \"chapters\": [\n"
34
+ " {\n"
35
+ " \"chapter_name\": \"Introduction to Python\",\n"
36
+ " \"modules\": [\n"
37
+ " {\"module_name\": \"What is Python?\", \"learning_objective\": \"Understand Python’s role as a programming language, its history, and its everyday applications.\"},\n"
38
+ " {\"module_name\": \"Setting Up Python\", \"learning_objective\": \"Learn how to install Python and write your first basic script.\"},\n"
39
+ " {\"module_name\": \"Variables and Data Types\", \"learning_objective\": \"Understand how to store data in variables and use types such as strings, numbers, and booleans.\"}\n"
40
+ " ]\n"
41
+ " },\n"
42
+ " {\n"
43
+ " \"chapter_name\": \"Control Structures\",\n"
44
+ " \"modules\": [\n"
45
+ " {\"module_name\": \"If Statements\", \"learning_objective\": \"Learn decision-making in programs with if/else statements and simple examples.\"},\n"
46
+ " {\"module_name\": \"Loops\", \"learning_objective\": \"Understand repetition using for and while loops with practical use cases.\"}\n"
47
+ " ]\n"
48
+ " }\n"
49
+ " ]\n"
50
+ "}\n\n"
51
+
52
+ "### Instructions:\n"
53
+ "- Follow the same style for the requested topic.\n"
54
+ "- Do NOT output explanations outside JSON.\n"
55
+ )
56
+
57
+ user_prompt = f"Generate a curriculum for:\nTopic: {topic}\nLevel: {level}"
58
+
59
+ response = self.client.chat.completions.create(
60
+ model="gpt-4o-mini",
61
+ response_format={"type": "json_object"},
62
+ messages=[
63
+ {"role": "system", "content": system_prompt},
64
+ {"role": "user", "content": user_prompt}
65
+ ]
66
+ )
67
+
68
+ try:
69
+ curriculum_json = response.choices[0].message.content.strip()
70
+ match = re.search(r'\{.*\}', curriculum_json, re.DOTALL)
71
+ if match:
72
+ json_str = match.group(0)
73
+ else:
74
+ raise ValueError("No JSON found in the response")
75
+
76
+ chapters_obj = json.loads(json_str)
77
+ chapters_data = chapters_obj["chapters"]
78
+
79
+ chapters = []
80
+ for ch in chapters_data:
81
+ modules = [Module(m["module_name"], m["learning_objective"]) for m in ch["modules"]]
82
+ chapters.append(Chapter(ch["chapter_name"], modules))
83
+
84
+ return Curriculum(topic, chapters)
85
+
86
+ except Exception as e:
87
+ print("[CurriculumPlannerAgent] Error parsing curriculum:", e)
88
+ return Curriculum(topic, [Chapter("General Introduction", [Module("Overview")])])
agents/level_assess.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base_agent import BaseAgent
2
+ import os
3
+ import openai
4
+ from dotenv import load_dotenv
5
+ import json
6
+
7
+ class LevelAssessmentAgent(BaseAgent):
8
+ def __init__(self):
9
+ super().__init__("LevelAssessmentAgent")
10
+ load_dotenv()
11
+ api_key = os.getenv("OPENAI_API_KEY")
12
+ self.client = openai.OpenAI(api_key=api_key)
13
+
14
+ def generate_questions(self, topic, level):
15
+ system_prompt = (
16
+ "You are a Master Educator and Curriculum Designer with deep expertise in pedagogical theory and assessment creation. "
17
+ "Your primary goal is to design a short, insightful assessment to precisely gauge a student's mastery of a topic at a specific proficiency level.\n\n"
18
+ "**Your Guiding Principles:**\n\n"
19
+ "1. **Cognitive Depth:** The questions must go beyond rote memorization. They should probe the student's reasoning, problem-solving abilities, and their ability to connect concepts.\n"
20
+ "2. **Level-Specific Targeting:** You must strictly adhere to the specified 'level':\n"
21
+ " - **Beginner:** Focus on core concepts, definitions, and simple \"how\" or \"why\" explanations. (Testing Comprehension & Application)\n"
22
+ " - **Intermediate:** Focus on comparing/contrasting concepts, applying knowledge to simple scenarios, and analyzing processes. (Testing Application & Analysis)\n"
23
+ " - **Advanced:** Demand synthesis of multiple concepts, evaluation of complex scenarios, and creation of novel solutions or arguments. (Testing Synthesis & Evaluation)\n"
24
+ "3. **Clarity and Precision:** Each question must be unambiguous, concise, and clearly worded.\n\n"
25
+ "**Output Format:**\n"
26
+ "You must generate exactly 5 to 6 questions. Your response should contain ONLY the numbered list of questions. Do not include a title, introduction, conclusion, or any other text."
27
+ )
28
+
29
+ user_prompt = (
30
+ f"Generate a assessment for 5 questions to test if a student is truly at the '{level}' level in the topic '{topic}'. "
31
+ "The question should be open-ended and require a thoughtful answer."
32
+ )
33
+ response = self.client.chat.completions.create(model = "gpt-4o-mini",
34
+ messages=[
35
+ {"role": "system", "content": system_prompt},
36
+ {"role": "user", "content": user_prompt}
37
+ ]
38
+ )
39
+ return response.choices[0].message.content.strip()
40
+
41
+ def evaluate_answer(self, topic, level, question, user_answer):
42
+ system_prompt = (
43
+ "You are a meticulous and impartial Grader AI. Your task is to evaluate a student's answer based on a provided question and the student's claimed proficiency level. You must adhere to the following STRICT rubric:\n\n"
44
+ "1. **Precision Requirement:** The answer must be specific, detailed, and demonstrate clear understanding. Vague, generic, or one-word answers are automatically INCORRECT.\n"
45
+ "2. **Correctness:** Is the answer factually accurate and relevant to the question?\n"
46
+ "3. **Depth:** Does the answer's depth match the expected level?\n"
47
+ " - **Novice/Beginner:** The answer should demonstrate basic comprehension and recall of key concepts with specific examples.\n"
48
+ " - **Intermediate:** The answer should show an ability to apply concepts, compare, and analyze with clear reasoning.\n"
49
+ " - **Advanced:** The answer must demonstrate synthesis, evaluation, and nuanced understanding with sophisticated insights.\n"
50
+ "4. **Completeness:** The answer must address all parts of the question comprehensively.\n\n"
51
+ "**AUTOMATIC FAILURES:**\n"
52
+ "- Answers under 20 words are automatically incorrect\n"
53
+ "- Vague responses like 'yes', 'no', 'it depends', 'I think so', 'maybe', 'probably'\n"
54
+ "- Responses that don't directly address the specific question asked\n"
55
+ "- Copy-paste definitions without personal understanding or application\n\n"
56
+ "You must respond ONLY with a JSON object. Do not include any other text or markdown formatting. The JSON object must have three keys:\n"
57
+ "1. `\"evaluation\"`: a string with a value of either `\"correct\"` or `\"incorrect\"`.\n"
58
+ "2. `\"reasoning\"`: a detailed explanation for your decision, including what was missing or incorrect.\n"
59
+ "3. `\"hint\"`: a subtle hint to guide the student toward the correct answer (only if incorrect)."
60
+ )
61
+ user_prompt = (
62
+ f"Evaluate the following data based on the rubric. Provide your response in the required JSON format.\n\n"
63
+ f"**Topic:** \"{topic}\"\n"
64
+ f"**Proficiency Level to Evaluate:** \"{level}\"\n"
65
+ f"**Question:** \"{question}\"\n"
66
+ f"**Student's Answer:** \"{user_answer}\""
67
+ )
68
+ response = self.client.chat.completions.create(
69
+ model="gpt-4o-mini",
70
+ messages=[
71
+ {"role": "system", "content": system_prompt},
72
+ {"role": "user", "content": user_prompt}
73
+ ]
74
+ )
75
+ return response.choices[0].message.content.strip()
76
+
77
+ def challenge_feedback(self, topic, level, question, user_answer, original_evaluation, original_reasoning):
78
+ """
79
+ Allows a student to challenge the feedback given. Re-evaluates the answer
80
+ with a focus on being fair and reconsidering the evaluation.
81
+ Returns a JSON string with the challenge result.
82
+ """
83
+ system_prompt = (
84
+ "You are a fair and reconsidering Grader AI. A student has challenged your previous evaluation. "
85
+ "Your task is to carefully re-examine the student's answer with fresh eyes, considering that "
86
+ "your initial evaluation may have been too strict or may have missed valid points.\n\n"
87
+ "**Re-evaluation Guidelines:**\n"
88
+ "1. Be open to reconsidering your initial assessment\n"
89
+ "2. Look for valid points in the answer that may have been overlooked\n"
90
+ "3. Consider alternative interpretations of the question\n"
91
+ "4. If the answer demonstrates understanding (even if not perfect), acknowledge it\n"
92
+ "5. Be fair and balanced in your re-evaluation\n\n"
93
+ "You must respond ONLY with a JSON object containing:\n"
94
+ "1. `\"evaluation\"`: either `\"correct\"` or `\"incorrect\"`\n"
95
+ "2. `\"reasoning\"`: detailed explanation of your re-evaluation, including whether you changed your mind and why\n"
96
+ "3. `\"original_was_fair\"`: true/false indicating if the original evaluation was fair\n"
97
+ "4. `\"hint\"`: a hint if still incorrect"
98
+ )
99
+
100
+ user_prompt = (
101
+ f"**Topic:** {topic}\n"
102
+ f"**Proficiency Level:** {level}\n"
103
+ f"**Question:** {question}\n"
104
+ f"**Student's Answer:** {user_answer}\n\n"
105
+ f"**Original Evaluation:** {original_evaluation}\n"
106
+ f"**Original Reasoning:** {original_reasoning}\n\n"
107
+ "Please re-evaluate this answer fairly. The student believes the original evaluation may have been incorrect."
108
+ )
109
+
110
+ response = self.client.chat.completions.create(
111
+ model="gpt-4o-mini",
112
+ messages=[
113
+ {"role": "system", "content": system_prompt},
114
+ {"role": "user", "content": user_prompt}
115
+ ]
116
+ )
117
+ return response.choices[0].message.content.strip()
118
+
119
+ def challenge_discussion(self, topic, level, question, user_answer, original_evaluation, original_reasoning, conversation_history, student_message):
120
+ """
121
+ Handles a conversational challenge discussion between student and grader.
122
+ conversation_history: list of [user_message, assistant_message] pairs
123
+ student_message: the current student's argument/question
124
+ Returns: assistant response and final evaluation (if discussion is ending)
125
+ """
126
+ system_prompt = (
127
+ "You are a fair and patient Grader AI engaged in a discussion with a student who is challenging your evaluation. "
128
+ "You should be open to reconsidering your assessment, but also maintain academic standards.\n\n"
129
+ "**Your Role:**\n"
130
+ "1. Listen carefully to the student's arguments\n"
131
+ "2. Be willing to reconsider if the student makes valid points\n"
132
+ "3. Explain your reasoning clearly and respectfully\n"
133
+ "4. If the student is right, acknowledge it and update your evaluation\n"
134
+ "5. If the student's argument doesn't change your assessment, explain why clearly\n\n"
135
+ "**Context:**\n"
136
+ f"- Topic: {topic}\n"
137
+ f"- Level: {level}\n"
138
+ f"- Question: {question}\n"
139
+ f"- Student's Original Answer: {user_answer}\n"
140
+ f"- Original Evaluation: {original_evaluation}\n"
141
+ f"- Original Reasoning: {original_reasoning}\n\n"
142
+ "Respond naturally in a conversational manner. Be helpful and educational, not defensive."
143
+ )
144
+
145
+ messages = [{"role": "system", "content": system_prompt}]
146
+
147
+ # Add conversation history
148
+ for user_msg, assistant_msg in conversation_history:
149
+ messages.append({"role": "user", "content": user_msg})
150
+ messages.append({"role": "assistant", "content": assistant_msg})
151
+
152
+ # Add current student message
153
+ messages.append({"role": "user", "content": student_message})
154
+
155
+ response = self.client.chat.completions.create(
156
+ model="gpt-4o-mini",
157
+ messages=messages
158
+ )
159
+ return response.choices[0].message.content.strip()
160
+
161
+ def process(self, topic, level):
162
+ print(f"\n[Assessment] Let's test your knowledge for the '{level}' level in '{topic}'.")
163
+ print("I will ask you 5 questions.")
164
+
165
+ NUM_QUESTIONS = 5
166
+ # New scoring thresholds based on your requirements
167
+ ADVANCED_PASS = 0.70 # 70% for advanced level
168
+ INTERMEDIATE_PASS = 0.65 # 65% for intermediate level
169
+ MINIMUM_THRESHOLD = 0.30 # 30% minimum to avoid novice
170
+
171
+ all_questions_str = self.generate_questions(topic, level)
172
+ questions = [q.strip() for q in all_questions_str.split('\n') if q.strip()]
173
+ questions = questions[:NUM_QUESTIONS]
174
+
175
+ correct_answers = 0
176
+ per_question_feedback = [] # collect feedback for summary
177
+
178
+ for i, question_text in enumerate(questions):
179
+ current_question = ". ".join(question_text.split('. ')[1:])
180
+
181
+ print(f"\n----------\n[Question {i+1}/{len(questions)}] {current_question}")
182
+ user_answer = input("Your answer: ")
183
+ try:
184
+ evaluation_json_str = self.evaluate_answer(topic, level, current_question, user_answer)
185
+ evaluation_data = json.loads(evaluation_json_str)
186
+
187
+ evaluation = str(evaluation_data.get("evaluation", "incorrect")).lower()
188
+ reasoning = evaluation_data.get("reasoning", "No explanation provided.")
189
+ hint = evaluation_data.get("hint", "")
190
+
191
+ is_correct = evaluation == "correct"
192
+ if is_correct:
193
+ correct_answers += 1
194
+ print(f"[Feedback] {reasoning}")
195
+ print("✅ Correct!")
196
+ else:
197
+ print(f"[Feedback] {reasoning}")
198
+ print("❌ Incorrect.")
199
+ if hint:
200
+ print(f"💡 Hint: {hint}")
201
+
202
+ per_question_feedback.append({
203
+ "question": current_question,
204
+ "correct": is_correct,
205
+ "reason": reasoning,
206
+ "hint": hint,
207
+ })
208
+
209
+ except (json.JSONDecodeError, AttributeError) as e:
210
+ print(f"[System Error] Could not parse the evaluation. Let's skip this one. Error: {e}")
211
+ continue
212
+
213
+ score_percentage = (correct_answers / len(questions)) * 100
214
+ # Print detailed feedback summary
215
+ print("\n----------")
216
+ print("ASSESSMENT FEEDBACK SUMMARY")
217
+ for i, fb in enumerate(per_question_feedback, 1):
218
+ tag = "✅" if fb["correct"] else "❌"
219
+ print(f"{tag} Q{i}: {fb['question']}")
220
+ if fb.get("reason"):
221
+ print(f" Reason: {fb['reason']}")
222
+ if (not fb["correct"]) and fb.get("hint"):
223
+ print(f" Hint: {fb['hint']}")
224
+
225
+ print(f"\n[Assessment Complete] You scored {score_percentage:.1f}% ({correct_answers}/{len(questions)} questions correct).")
226
+
227
+ # Level determination based on claimed level thresholds
228
+ if level == "advanced":
229
+ if score_percentage >= 70:
230
+ print("🎯 Assigned Level: advanced")
231
+ return "advanced"
232
+ elif score_percentage >= 65:
233
+ print("🎯 Assigned Level: intermediate")
234
+ return "intermediate"
235
+ else:
236
+ print("🎯 Assigned Level: novice")
237
+ return "novice"
238
+ elif level == "intermediate":
239
+ if score_percentage >= 65:
240
+ print("🎯 Assigned Level: intermediate")
241
+ return "intermediate"
242
+ else:
243
+ print("🎯 Assigned Level: novice")
244
+ return "novice"
245
+ else:
246
+ print("🎯 Assigned Level: novice")
247
+ return "novice"
agents/teacher.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents/teacher.py
2
+ import os
3
+ import json
4
+ import time
5
+ from dotenv import load_dotenv
6
+ import openai
7
+ from .base_agent import BaseAgent
8
+
9
+ load_dotenv()
10
+
11
+
12
+ class TeacherAgent(BaseAgent):
13
+ def __init__(self, level):
14
+ super().__init__(f"{level.capitalize()}TeacherAgent")
15
+ api_key = os.getenv("OPENAI_API_KEY")
16
+ self.client = openai.OpenAI(api_key=api_key)
17
+ self.level = level.lower()
18
+
19
+ def teach_module(self, module):
20
+ """
21
+ Returns a rich, k-shot based explanation string for the given module.
22
+ Will attempt a small re-prompt if the output is too short.
23
+ """
24
+ system_prompt = (
25
+ "You are an engaging and adaptive AI Tutor. Produce a thorough, structured explanation "
26
+ "of the requested module tuned to the user's learning level. Use the structure below."
27
+ "\n\nTeaching style differences by level:\n"
28
+ "- Novice: Simple language, relatable analogies, many concrete examples.\n"
29
+ "- Intermediate: Explain 'how' and 'why', connect to related concepts, include practical examples.\n"
30
+ "- Advanced: Discuss nuances, edge-cases, performance, comparative methods and deeper insights.\n\n"
31
+ "Explanation structure (MANDATORY):\n"
32
+ "1) Core Concept — clear, precise description (several sentences)\n"
33
+ "2) Worked Example or Analogy — at least one detailed example that illustrates application\n"
34
+ "4) Key Takeaway — one concise sentence\n\n"
35
+ "### Few-shot examples (follow style):\n\n"
36
+ "Novice Example:\n"
37
+ "Module: Variables\n"
38
+ "Core Concept: Variables are containers that store values like numbers or text. Example: x = 5.\n"
39
+ "Analogy/Example: Think of a labeled jar.\n"
40
+ "Intermediate Example:\n"
41
+ "Module: For loops\n"
42
+ "Core Concept: For loops iterate over a collection. Example: for item in collection: process(item)\n"
43
+ "Application: Useful for batch-processing and iteration in algorithms.\n\n"
44
+ "Advanced Example:\n"
45
+ "Module: Tail recursion\n"
46
+ "Core Concept: Tail recursion preserves state for compiler optimizations; consider stack usage.\n\n"
47
+ "Produce detailed output (minimum ~130 words). Do NOT output JSON — just plain text explanation."
48
+ )
49
+
50
+ user_prompt = (
51
+ f"Module: {module.name}\n"
52
+ f"Learning objective: {getattr(module, 'learning_objective', '')}\n\n"
53
+ "Please produce the explanation now."
54
+ )
55
+
56
+ resp = self.client.chat.completions.create(
57
+ model="gpt-4o-mini",
58
+ messages=[
59
+ {"role": "system", "content": system_prompt},
60
+ {"role": "user", "content": user_prompt}
61
+ ],
62
+ # you can tune temperature if needed
63
+ )
64
+ explanation = resp.choices[0].message.content.strip()
65
+
66
+ # If too short, ask for expansion once
67
+ if len(explanation.split()) < 120:
68
+ followup = (
69
+ "The previous explanation was too short. Please expand the explanation, add another worked example "
70
+ "and a short code or pseudo-code snippet where appropriate. Keep same style & level."
71
+ )
72
+ resp2 = self.client.chat.completions.create(
73
+ model="gpt-4o-mini",
74
+ messages=[
75
+ {"role": "system", "content": system_prompt},
76
+ {"role": "user", "content": user_prompt},
77
+ {"role": "user", "content": followup}
78
+ ],
79
+ )
80
+ extra = resp2.choices[0].message.content.strip()
81
+ # concatenate but keep readable
82
+ explanation = explanation + "\n\n" + extra
83
+
84
+ # return explanation string (do not print in agent; app will show it)
85
+ return explanation
86
+
87
+ def evaluate_example(self, module, student_example):
88
+ """
89
+ Evaluate the student's example for the given module.
90
+ Returns a dict: { "is_correct": bool, "feedback": str, "confidence": float (0-1) }
91
+ The model MUST output JSON only; we robustly parse it and fallback if needed.
92
+ """
93
+ system_prompt = (
94
+ "You are an expert educational assessor. Evaluate the student's example strictly with respect "
95
+ "to the module's learning objective. Respond with JSON ONLY (no extra text). The JSON object MUST contain:\n"
96
+ " - is_correct: true/false\n"
97
+ " - feedback: short one-sentence constructive feedback\n"
98
+ " - confidence: numeric between 0 and 1\n\n"
99
+ f"Evaluation sensitivity is based on learner level: {self.level}."
100
+ )
101
+
102
+ user_prompt = (
103
+ f"Module: {module.name}\n"
104
+ f"Learning objective: {getattr(module, 'learning_objective', '')}\n\n"
105
+ f"Student example: {student_example}\n\n"
106
+ "Evaluate and return JSON only."
107
+ )
108
+
109
+ resp = self.client.chat.completions.create(
110
+ model="gpt-4o-mini",
111
+ messages=[
112
+ {"role": "system", "content": system_prompt},
113
+ {"role": "user", "content": user_prompt}
114
+ ],
115
+ # lower temperature can reduce hallucinations
116
+ )
117
+ raw = resp.choices[0].message.content.strip()
118
+
119
+ # Try to parse JSON; be forgiving (extract first {...})
120
+ try:
121
+ parsed = json.loads(raw)
122
+ except Exception:
123
+ import re
124
+ m = re.search(r'(\{.*\})', raw, re.DOTALL)
125
+ if m:
126
+ try:
127
+ parsed = json.loads(m.group(1))
128
+ except Exception:
129
+ parsed = None
130
+ else:
131
+ parsed = None
132
+
133
+ if not parsed:
134
+ # fallback: simple heuristics (very conservative)
135
+ is_correct = len(student_example.strip()) > 20
136
+ feedback = "Could not parse evaluator output; using conservative heuristic. Provide a more concrete example." \
137
+ if not is_correct else "Example seems plausible but automatic evaluation failed to parse."
138
+ confidence = 0.45 if not is_correct else 0.6
139
+ return {"is_correct": bool(is_correct), "feedback": feedback, "confidence": confidence, "raw": raw}
140
+
141
+ # normalize fields
142
+ is_correct = bool(parsed.get("is_correct", parsed.get("correct", False)))
143
+ feedback = str(parsed.get("feedback", parsed.get("explanation", "")))
144
+ confidence = float(parsed.get("confidence", parsed.get("score", 0))) if parsed.get("confidence") is not None else 0.9
145
+
146
+ return {"is_correct": is_correct, "feedback": feedback, "confidence": confidence, "raw": raw}
147
+
148
+ def check_example(self, module):
149
+ """
150
+ Interactive method to check if student understands the module by asking for an example.
151
+ Returns True if the student provides a satisfactory example, False otherwise.
152
+ """
153
+ print(f"\n🎯 Let's test your understanding of '{module.name}'!")
154
+ print("Please provide a specific example or application that demonstrates this concept.")
155
+
156
+ learning_objective = getattr(module, 'learning_objective', '')
157
+ if learning_objective:
158
+ print(f"Learning Objective: {learning_objective}")
159
+
160
+ print("\nYour example should be:")
161
+ print("• Specific and concrete (not just a definition)")
162
+ print("• Relevant to the module content")
163
+ print("• Show your understanding of how to apply the concept")
164
+ print("• At least 2-3 sentences with clear reasoning")
165
+
166
+ while True: # Allow multiple attempts
167
+ student_example = input("\nYour example: ").strip()
168
+
169
+ if not student_example:
170
+ print("❌ Please provide an example to demonstrate your understanding.")
171
+ continue
172
+
173
+ # Check for minimum length and specificity
174
+ if len(student_example.split()) < 10:
175
+ print("❌ Your example is too brief. Please provide a more detailed example with specific details.")
176
+ continue
177
+
178
+ # Evaluate the example
179
+ evaluation = self.evaluate_example(module, student_example)
180
+
181
+ if evaluation['is_correct']:
182
+ print("✅ Correct! Your example demonstrates good understanding.")
183
+ return True
184
+ else:
185
+ print("❌ Incorrect. Your example doesn't demonstrate sufficient understanding.")
186
+
187
+ # Provide a simple hint
188
+ hint = self.get_simple_hint(module)
189
+ if hint:
190
+ print(f"💡 Hint: {hint}")
191
+
192
+ retry = input("\nWould you like to try again with a different example? (y/n): ").lower().strip()
193
+ if retry == 'y':
194
+ continue
195
+ else:
196
+ print("📚 Let's review the concept again and then try the example.")
197
+ return False
198
+
199
+ def get_simple_hint(self, module):
200
+ """Generate a simple hint to guide the student toward a better example."""
201
+ system_prompt = (
202
+ "You are an expert tutor providing a simple hint. Give a brief, helpful suggestion (1-2 sentences) "
203
+ "to guide the student toward providing a better example. Don't give away the answer, just nudge them in the right direction."
204
+ )
205
+
206
+ user_prompt = (
207
+ f"Module: {module.name}\n"
208
+ f"Learning Objective: {getattr(module, 'learning_objective', '')}\n\n"
209
+ "Provide a simple hint to help the student think of a better example."
210
+ )
211
+
212
+ try:
213
+ response = self.client.chat.completions.create(
214
+ model="gpt-4o-mini",
215
+ messages=[
216
+ {"role": "system", "content": system_prompt},
217
+ {"role": "user", "content": user_prompt}
218
+ ]
219
+ )
220
+ return response.choices[0].message.content.strip()
221
+ except Exception:
222
+ return "Think about how this concept applies in real-world situations or practical scenarios."
223
+
224
+
225
+ # convenience subclasses (optional)
226
+ class NoviceTeacherAgent(TeacherAgent):
227
+ def __init__(self):
228
+ super().__init__("novice")
229
+
230
+
231
+ class IntermediateTeacherAgent(TeacherAgent):
232
+ def __init__(self):
233
+ super().__init__("intermediate")
234
+
235
+
236
+ class AdvancedTeacherAgent(TeacherAgent):
237
+ def __init__(self):
238
+ super().__init__("advanced")
app.py ADDED
@@ -0,0 +1,951 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ AI Teacher Bot - Single Panel UI (fixed example-evaluation & progression)
4
+ """
5
+
6
+ import gradio as gr
7
+ import io
8
+ from contextlib import redirect_stdout
9
+
10
+ from main import LEVELS, check_api_key, generate_curriculum
11
+ from user_state import UserState
12
+ from agents.level_assess import LevelAssessmentAgent
13
+ from agents.teacher import TeacherAgent
14
+ from agents.bloom_assess import BloomsAssessmentAgent
15
+ import json
16
+
17
+ # Global session
18
+ current_session = {
19
+ "user": None,
20
+ "curriculum": None,
21
+ "chapter_idx": 0,
22
+ "module_idx": 0,
23
+ "mode": None, # modes: None/idle, "assessment", "teaching", "awaiting_example", "module_passed", "bloom", "done"
24
+ "questions": [],
25
+ "answers": [],
26
+ "q_idx": 0,
27
+ "bloom_level": None,
28
+ # assessment flow controls
29
+ "correct_count": 0,
30
+ "assessment_feedback": [], # list of dicts per question with correctness and feedback
31
+ "last_feedback": None, # stores last question, answer, evaluation, reasoning for challenging
32
+ "challenge_chat_history": [], # list of [user_msg, assistant_msg] pairs for challenge discussion
33
+ "challenge_exchanges": 0, # count of challenge exchanges (max 3)
34
+ "challenge_mode": False, # whether challenge chat is active
35
+ "last_output": ""
36
+ }
37
+
38
+ BLOOM_ORDER = ["remember", "understand", "apply", "analyze", "evaluate", "create"]
39
+
40
+
41
+ def update_main_output(text):
42
+ current_session["last_output"] = text
43
+ return text
44
+
45
+ # ------------------------------
46
+ # Chatbot helpers (Gradio 6 safe)
47
+ # ------------------------------
48
+ def empty_chat():
49
+ """
50
+ Returns empty chat in messages format (dictionaries with 'role' and 'content' keys).
51
+ """
52
+ return [{"role": "assistant", "content": " "}]
53
+
54
+
55
+ def safe_chat(chat):
56
+ """
57
+ Ensures chat history is always valid. Uses messages format (list of dicts with 'role' and 'content').
58
+ """
59
+ if not isinstance(chat, list):
60
+ return empty_chat()
61
+ if len(chat) == 0:
62
+ return empty_chat()
63
+ # Ensure all items are dicts with role and content
64
+ result = []
65
+ for item in chat:
66
+ if isinstance(item, dict) and "role" in item and "content" in item:
67
+ result.append(item)
68
+ elif isinstance(item, tuple) and len(item) == 2:
69
+ # Convert tuple (user_msg, assistant_msg) to dict format
70
+ result.append({"role": "user", "content": item[0]})
71
+ result.append({"role": "assistant", "content": item[1]})
72
+ return result if result else empty_chat()
73
+
74
+
75
+ def get_output_update():
76
+ return gr.update(value=current_session.get("last_output", ""))
77
+
78
+ # ------------------------------
79
+ # Session & Flow helpers
80
+ # ------------------------------
81
+ def reset_session_state():
82
+ current_session.update({
83
+ "user": None,
84
+ "curriculum": None,
85
+ "chapter_idx": 0,
86
+ "module_idx": 0,
87
+ "mode": None,
88
+ "questions": [],
89
+ "answers": [],
90
+ "q_idx": 0,
91
+ "bloom_level": None,
92
+ "correct_count": 0,
93
+ "assessment_feedback": [],
94
+ "last_feedback": None,
95
+ "challenge_chat_history": [],
96
+ "challenge_exchanges": 0,
97
+ "challenge_mode": False,
98
+ "last_output": ""
99
+ })
100
+
101
+ def start_learning_session(topic, claimed_level):
102
+ if not topic or not topic.strip():
103
+ return "❌ Please enter a topic"
104
+ if not check_api_key():
105
+ return "❌ OpenAI API key not configured! Please set OPENAI_API_KEY in your .env file."
106
+
107
+ try:
108
+ reset_session_state()
109
+ user = UserState(topic=topic.strip(), claimed_level=claimed_level)
110
+ current_session["user"] = user
111
+
112
+ # Generate curriculum (level may be updated later after assessment)
113
+ curriculum = generate_curriculum(user.topic, claimed_level)
114
+ if curriculum is None:
115
+ return "❌ Failed to generate curriculum. Please try again or check your API key."
116
+ current_session["curriculum"] = curriculum
117
+ except Exception as e:
118
+ return f"❌ Error starting session: {str(e)}. Please try again."
119
+
120
+ if claimed_level == "novice":
121
+ user.set_actual_level("novice")
122
+ current_session["mode"] = None # ready to start teaching
123
+ return update_main_output(show_curriculum(curriculum) + "\n\nType 'next' to start Module 1.")
124
+ else:
125
+ # start level assessment
126
+ assessor = LevelAssessmentAgent()
127
+ q_text = assessor.generate_questions(user.topic, claimed_level)
128
+ questions = [q.strip() for q in q_text.split("\n") if q.strip() and q[0].isdigit()]
129
+ if not questions:
130
+ # fallback: skip assessment
131
+ user.set_actual_level(claimed_level)
132
+ current_session["mode"] = None
133
+ return update_main_output(show_curriculum(curriculum) + "\n\nType 'next' to start Module 1.")
134
+ current_session.update({
135
+ "mode": "assessment",
136
+ "questions": questions,
137
+ "q_idx": 0,
138
+ "answers": [],
139
+ "correct_count": 0,
140
+ "assessment_feedback": [],
141
+ "last_feedback": None, # Will be set after first answer submission
142
+ "challenge_chat_history": [],
143
+ "challenge_exchanges": 0,
144
+ "challenge_mode": False
145
+ })
146
+ # Button should be hidden initially (no feedback yet), will show after first answer
147
+ return update_main_output(f"📝 LEVEL ASSESSMENT\n\nQuestion 1 of {len(questions)}:\n{questions[0]}\n\nPlease submit your answer.")
148
+
149
+ # ------------------------------
150
+ # Level assessment handlers
151
+ # ------------------------------
152
+ def _strip_number_prefix(q_line: str) -> str:
153
+ # Converts "1. Question" -> "Question" safely
154
+ q = q_line.strip()
155
+ if ". " in q:
156
+ parts = q.split(". ", 1)
157
+ if parts[0].isdigit():
158
+ return parts[1]
159
+ return q
160
+
161
+
162
+ def handle_assessment(answer):
163
+ qs = current_session["questions"]
164
+ idx = current_session["q_idx"]
165
+
166
+ if not qs:
167
+ return "⚠️ No assessment in progress."
168
+
169
+ question_full = qs[idx]
170
+ question = _strip_number_prefix(question_full)
171
+
172
+ # guard on empty/very short answers
173
+ user_answer = (answer or "").strip()
174
+ if len(user_answer.split()) < 5:
175
+ # Don't set last_feedback for invalid answers, so button won't show
176
+ return "❌ Your answer is too brief. Please provide a more detailed and specific response (at least 5 words or 1-2 sentences)."
177
+
178
+ assessor = LevelAssessmentAgent()
179
+ try:
180
+ raw = assessor.evaluate_answer(
181
+ current_session["user"].topic,
182
+ current_session["user"].claimed_level,
183
+ question,
184
+ user_answer,
185
+ )
186
+ import json as _json
187
+ parsed = _json.loads(raw)
188
+ evaluation = str(parsed.get("evaluation", "incorrect")).lower()
189
+ reasoning = parsed.get("reasoning", "")
190
+ hint = parsed.get("hint", "") # Extract hint for completeness
191
+ except Exception as e:
192
+ # Better error handling
193
+ evaluation = "correct" if len(user_answer.split()) >= 20 else "incorrect"
194
+ reasoning = f"Heuristic grading fallback used. (Error: {str(e)})"
195
+ hint = ""
196
+
197
+ # Record attempt
198
+ current_session["answers"].append(user_answer)
199
+ is_correct = evaluation == "correct"
200
+ if is_correct:
201
+ current_session["correct_count"] += 1
202
+
203
+ # Save per-question feedback
204
+ current_session["assessment_feedback"].append({
205
+ "question": question,
206
+ "correct": is_correct,
207
+ "reason": reasoning,
208
+ "hint": hint,
209
+ })
210
+
211
+ # Store last feedback for challenging
212
+ current_session["last_feedback"] = {
213
+ "question": question,
214
+ "answer": user_answer,
215
+ "evaluation": evaluation,
216
+ "reasoning": reasoning,
217
+ "is_correct": is_correct,
218
+ "hint": hint
219
+ }
220
+
221
+ # Advance to next question or finish
222
+ if idx + 1 < len(qs):
223
+ current_session["q_idx"] += 1
224
+ status = "✅ Correct!" if is_correct else "❌ Incorrect."
225
+ extra = f"\nReason: {reasoning}" if reasoning else ""
226
+ hint_text = f"\n💡 Hint: {hint}" if hint and not is_correct else ""
227
+ return (
228
+ f"{status}{extra}{hint_text}\n\n"
229
+ f"Question {current_session['q_idx']+1} of {len(qs)}:\n{qs[current_session['q_idx']]}"
230
+ )
231
+ else:
232
+ # Finish: compute score and assign level based on claimed level thresholds
233
+ total = len(qs)
234
+ score_pct = (current_session["correct_count"] / total) * 100
235
+ claimed = current_session["user"].claimed_level
236
+
237
+ if claimed == "advanced":
238
+ if score_pct >= 70:
239
+ assigned = "advanced"
240
+ elif score_pct >= 65:
241
+ assigned = "intermediate"
242
+ else:
243
+ assigned = "novice"
244
+ elif claimed == "intermediate":
245
+ assigned = "intermediate" if score_pct >= 65 else "novice"
246
+ else:
247
+ assigned = "novice"
248
+
249
+ # Build full feedback summary
250
+ lines = [
251
+ "🧪 Assessment Feedback:",
252
+ ]
253
+ for i, fb in enumerate(current_session["assessment_feedback"], 1):
254
+ tag = "✅" if fb["correct"] else "❌"
255
+ line = f"{tag} Q{i}: {fb['question']}"
256
+ if fb.get("reason"):
257
+ line += f"\n Reason: {fb['reason']}"
258
+ lines.append(line)
259
+ lines.append("")
260
+ lines.append(f"Score: {score_pct:.1f}% | Assigned Level: {assigned}")
261
+
262
+ current_session["user"].set_actual_level(assigned)
263
+ # Prepare curriculum but show it on next screen
264
+ curriculum = generate_curriculum(current_session["user"].topic, assigned)
265
+ if curriculum:
266
+ current_session["curriculum"] = curriculum
267
+
268
+ feedback_text = "\n".join(lines)
269
+ current_session["mode"] = "assessment_summary"
270
+ current_session["last_feedback"] = None # Clear last feedback when assessment completes
271
+ return feedback_text + "\n\n➡️ Press 'Next' to view your personalized curriculum."
272
+
273
+ # ------------------------------
274
+ # Teaching helpers
275
+ # ------------------------------
276
+ def show_curriculum(curriculum):
277
+ txt = f"📖 CURRICULUM FOR {current_session['user'].topic.upper()}\n" + "="*40 + "\n"
278
+ for i, ch in enumerate(curriculum.chapters, 1):
279
+ txt += f"\nChapter {i}: {ch.name}\n"
280
+ for j, mod in enumerate(ch.modules, 1):
281
+ txt += f" {i}.{j} {mod.name}\n"
282
+ if getattr(mod, "learning_objective", None):
283
+ txt += f" → {mod.learning_objective}\n"
284
+ return txt
285
+
286
+ def next_step(_):
287
+ mode = current_session["mode"]
288
+ # If module just passed, Next moves to next module
289
+ if mode == "module_passed":
290
+ # advance module index now
291
+ current_session["module_idx"] += 1
292
+ current_session["mode"] = None
293
+ return update_main_output(start_teaching_module())
294
+ # If idle/none → start teaching module
295
+ if mode is None:
296
+ return update_main_output(start_teaching_module())
297
+ if mode == "teaching":
298
+ return update_main_output(get_explanation())
299
+ if mode == "awaiting_example":
300
+ return update_main_output("✋ Please submit your example using 'Submit Answer' before moving on.")
301
+ if mode == "bloom":
302
+ return update_main_output("🌸 Bloom assessment in progress — answer the Bloom question or submit to retry.")
303
+ if mode == "assessment_summary":
304
+ # show curriculum now and transition to normal teaching flow
305
+ current_session["mode"] = None
306
+ return update_main_output(show_curriculum(current_session["curriculum"]) + "\n\nType 'next' to start Module 1.")
307
+ return update_main_output("⚠️ Invalid state.")
308
+
309
+ def start_teaching_module():
310
+ if not current_session.get("curriculum"):
311
+ return "⚠️ No curriculum loaded. Please start a session first."
312
+
313
+ try:
314
+ cur = current_session["curriculum"]
315
+ ch_i = current_session["chapter_idx"]
316
+ m_i = current_session["module_idx"]
317
+
318
+ if ch_i >= len(cur.chapters):
319
+ current_session["mode"] = "done"
320
+ return "🎉 You have completed the entire curriculum!"
321
+
322
+ chapter = cur.chapters[ch_i]
323
+ # if all modules finished -> start Bloom for the chapter
324
+ if m_i >= len(chapter.modules):
325
+ return start_bloom_assessment()
326
+
327
+ module = chapter.modules[m_i]
328
+ current_session["mode"] = "teaching"
329
+ return f"📚 Chapter {ch_i+1}: {chapter.name}\nModule {ch_i+1}.{m_i+1}: {module.name}\n\nObjective: {getattr(module,'learning_objective','')}\n\nClick 'Next' to get the explanation."
330
+ except Exception as e:
331
+ return f"❌ Error starting teaching module: {str(e)}"
332
+
333
+ def get_explanation():
334
+ if not current_session.get("curriculum") or not current_session.get("user"):
335
+ return "⚠️ Session not properly initialized. Please start a new session."
336
+
337
+ try:
338
+ cur = current_session["curriculum"]
339
+ ch_i = current_session["chapter_idx"]
340
+ m_i = current_session["module_idx"]
341
+ chapter = cur.chapters[ch_i]
342
+ # Guard
343
+ if m_i >= len(chapter.modules):
344
+ return start_bloom_assessment()
345
+
346
+ module = chapter.modules[m_i]
347
+ teacher = TeacherAgent(current_session["user"].actual_level or current_session["user"].claimed_level)
348
+ explanation = teacher.teach_module(module) # returns string
349
+ current_session["mode"] = "awaiting_example"
350
+ return f"📖 Explanation for {module.name}\n\n{explanation}\n\n✍️ Now submit your example in the box and click 'Submit Answer'."
351
+ except Exception as e:
352
+ return f"❌ Error getting explanation: {str(e)}"
353
+
354
+ # ------------------------------
355
+ # Submit handler (single entry point wired to Submit button)
356
+ # ------------------------------
357
+ def submit_answer(answer):
358
+ mode = current_session.get("mode")
359
+ if mode == "assessment":
360
+ response = handle_assessment(answer)
361
+ elif mode == "awaiting_example":
362
+ response = handle_example_submission(answer)
363
+ elif mode == "bloom":
364
+ response = handle_bloom(answer)
365
+ else:
366
+ response = "⚠️ Nothing to submit right now. Click 'Next' to proceed."
367
+ current_session["last_output"] = response
368
+ # Return output + clear input
369
+ return response, ""
370
+
371
+ def start_challenge_discussion():
372
+ """
373
+ Opens the challenge discussion chat interface as a modal pop-up.
374
+ Shows the original feedback and prompts user to start discussion.
375
+ """
376
+ mode = current_session.get("mode")
377
+ if mode != "assessment":
378
+ return gr.update(visible=False), [], "⚠️ Challenge is only available during assessment.", get_output_update()
379
+
380
+ last_fb = current_session.get("last_feedback")
381
+ if not last_fb:
382
+ return gr.update(visible=False), [], "⚠️ No feedback available to challenge. Please submit an answer first.", get_output_update()
383
+
384
+ # Initialize challenge discussion
385
+ current_session["challenge_mode"] = True
386
+ current_session["challenge_exchanges"] = 0
387
+ current_session["challenge_chat_history"] = []
388
+
389
+ # Show initial context
390
+ initial_greeting = (
391
+ f"**Grader's Original Feedback:**\n"
392
+ f"Evaluation: {'✅ Correct' if last_fb['is_correct'] else '❌ Incorrect'}\n"
393
+ f"Reasoning: {last_fb['reasoning']}\n\n"
394
+ f"**Question:** {last_fb['question']}\n"
395
+ f"**Your Answer:** {last_fb['answer']}\n\n"
396
+ f"💬 You can now present your arguments. You have up to 3 exchanges with the grader."
397
+ )
398
+
399
+ # Use messages format: list of dicts with 'role' and 'content'
400
+ chat_history = [{"role": "assistant", "content": initial_greeting}]
401
+ status_msg = "💬 Challenge discussion opened. Present your first argument below (3 exchanges remaining)."
402
+
403
+ return (
404
+ gr.update(visible=True),
405
+ chat_history,
406
+ status_msg,
407
+ get_output_update()
408
+ )
409
+
410
+ def handle_challenge_message(message, chat_history):
411
+ """
412
+ Handles a message in the challenge discussion.
413
+ Limits to 3 total exchanges (student messages).
414
+ Returns: chat_history, status_msg, msg_enabled, btn_enabled, output_update
415
+ """
416
+ if not current_session.get("challenge_mode"):
417
+ return chat_history, "⚠️ Challenge discussion is not active.", False, False, get_output_update()
418
+
419
+ if not message or not message.strip():
420
+ return chat_history, "", True, True, gr.update()
421
+
422
+ if current_session["challenge_exchanges"] >= 3:
423
+ return chat_history, "⚠️ Maximum exchanges (3) reached. Discussion closed. Click 'Close Discussion' to continue.", False, False, get_output_update()
424
+
425
+ last_fb = current_session.get("last_feedback")
426
+ if not last_fb:
427
+ return chat_history, "⚠️ No feedback available.", False, False, get_output_update()
428
+
429
+ assessor = LevelAssessmentAgent()
430
+ try:
431
+ grader_response = assessor.challenge_discussion(
432
+ current_session["user"].topic,
433
+ current_session["user"].claimed_level,
434
+ last_fb["question"],
435
+ last_fb["answer"],
436
+ last_fb["evaluation"],
437
+ last_fb["reasoning"],
438
+ current_session["challenge_chat_history"],
439
+ message
440
+ )
441
+
442
+ current_session["challenge_chat_history"].append([message, grader_response])
443
+ current_session["challenge_exchanges"] += 1
444
+
445
+ # Ensure chat_history is in messages format (list of dicts)
446
+ chat_history = safe_chat(chat_history)
447
+
448
+ # Add the new messages in messages format
449
+ chat_history.append({"role": "user", "content": message})
450
+ chat_history.append({"role": "assistant", "content": grader_response})
451
+
452
+
453
+ remaining = 3 - current_session["challenge_exchanges"]
454
+ if remaining > 0:
455
+ status_msg = f"💬 {remaining} exchange(s) remaining. You can continue the discussion."
456
+ output_update = gr.update()
457
+ msg_enabled = True
458
+ btn_enabled = True
459
+ else:
460
+ # Finalize evaluation after 3 exchanges
461
+ status_msg, output_update = finalize_challenge_discussion(grader_response)
462
+ msg_enabled = False
463
+ btn_enabled = False
464
+
465
+ return chat_history, status_msg, msg_enabled, btn_enabled, output_update
466
+ except Exception as e:
467
+ return chat_history, f"❌ Error: {str(e)}", True, True, gr.update()
468
+
469
+ def finalize_challenge_discussion(final_response):
470
+ """
471
+ Finalizes the challenge discussion and updates evaluation if needed.
472
+ Extracts final evaluation from the last grader response.
473
+ Returns: (status_msg, main_output_update)
474
+ """
475
+ last_fb = current_session.get("last_feedback")
476
+ if not last_fb:
477
+ return "⚠️ Could not finalize challenge.", get_output_update()
478
+
479
+ # Try to extract evaluation from the final response
480
+ # Use the challenge_feedback method to get a structured final evaluation
481
+ assessor = LevelAssessmentAgent()
482
+ try:
483
+ # Get the full conversation context
484
+ conversation_text = "\n".join([
485
+ f"Student: {msg[0]}\nGrader: {msg[1]}"
486
+ for msg in current_session["challenge_chat_history"]
487
+ ])
488
+
489
+ # Final re-evaluation request
490
+ final_prompt = (
491
+ f"Based on our discussion:\n{conversation_text}\n\n"
492
+ "Please provide your FINAL evaluation as JSON with: "
493
+ '{"evaluation": "correct" or "incorrect", "reasoning": "explanation", "original_was_fair": true/false}'
494
+ )
495
+
496
+ raw = assessor.challenge_feedback(
497
+ current_session["user"].topic,
498
+ current_session["user"].claimed_level,
499
+ last_fb["question"],
500
+ last_fb["answer"],
501
+ last_fb["evaluation"],
502
+ last_fb["reasoning"]
503
+ )
504
+
505
+ import json as _json
506
+ parsed = _json.loads(raw)
507
+ new_evaluation = str(parsed.get("evaluation", "incorrect")).lower()
508
+ new_reasoning = parsed.get("reasoning", "")
509
+ original_was_fair = parsed.get("original_was_fair", True)
510
+
511
+ new_is_correct = new_evaluation == "correct"
512
+ old_is_correct = last_fb["is_correct"]
513
+
514
+ # Build the main output update
515
+ qs = current_session["questions"]
516
+ idx = current_session["q_idx"]
517
+ current_question_text = ""
518
+ if idx < len(qs):
519
+ current_question_text = f"\n\n📝 Current Question {idx+1} of {len(qs)}:\n{qs[idx]}"
520
+
521
+ # Update if evaluation changed
522
+ if new_is_correct != old_is_correct:
523
+ if new_is_correct and not old_is_correct:
524
+ current_session["correct_count"] += 1
525
+ elif not new_is_correct and old_is_correct:
526
+ current_session["correct_count"] = max(0, current_session["correct_count"] - 1)
527
+
528
+ if current_session["assessment_feedback"]:
529
+ current_session["assessment_feedback"][-1]["correct"] = new_is_correct
530
+ current_session["assessment_feedback"][-1]["reason"] = new_reasoning
531
+
532
+ current_session["last_feedback"]["evaluation"] = new_evaluation
533
+ current_session["last_feedback"]["reasoning"] = new_reasoning
534
+ current_session["last_feedback"]["is_correct"] = new_is_correct
535
+
536
+ # Build updated main output
537
+ main_output = (
538
+ f"🔄 **EVALUATION UPDATED AFTER CHALLENGE**\n\n"
539
+ f"**Question:** {last_fb['question']}\n"
540
+ f"**Your Answer:** {last_fb['answer']}\n\n"
541
+ f"**Original Evaluation:** {'✅ Correct' if old_is_correct else '❌ Incorrect'}\n"
542
+ f"**Updated Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'}\n\n"
543
+ f"**Updated Reasoning:** {new_reasoning}\n"
544
+ f"{current_question_text}"
545
+ )
546
+
547
+ status_msg = "🔄 **FINAL EVALUATION UPDATED**\n\n" + \
548
+ f"**New Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'}\n" + \
549
+ f"**Final Reasoning:** {new_reasoning}\n\n" + \
550
+ "✅ Challenge discussion completed. Evaluation has been updated on the main screen."
551
+ else:
552
+ # Build main output showing final evaluation
553
+ main_output = (
554
+ f"📋 **FINAL EVALUATION AFTER CHALLENGE**\n\n"
555
+ f"**Question:** {last_fb['question']}\n"
556
+ f"**Your Answer:** {last_fb['answer']}\n\n"
557
+ f"**Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'} (unchanged)\n\n"
558
+ f"**Final Reasoning:** {new_reasoning}\n"
559
+ f"{current_question_text}"
560
+ )
561
+
562
+ status_msg = "📋 **FINAL EVALUATION**\n\n" + \
563
+ f"**Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'} (unchanged)\n" + \
564
+ f"**Final Reasoning:** {new_reasoning}\n\n" + \
565
+ "✅ Challenge discussion completed. You may continue with the assessment."
566
+
567
+ # Close challenge mode
568
+ current_session["challenge_mode"] = False
569
+ current_session["last_output"] = main_output
570
+
571
+ return status_msg, gr.update(value=main_output)
572
+ except Exception as e:
573
+ current_session["challenge_mode"] = False
574
+ error_msg = f"⚠️ Error finalizing challenge: {str(e)}"
575
+ return error_msg, get_output_update()
576
+
577
+ def handle_example_submission(example_text):
578
+ """
579
+ Uses TeacherAgent.evaluate_example(module, example) to decide correctness.
580
+ If correct -> set mode to 'module_passed' and require user to press Next to move on.
581
+ If incorrect -> remain in 'awaiting_example' and show feedback.
582
+ """
583
+ if not current_session.get("curriculum"):
584
+ return "⚠️ No curriculum loaded. Please start a session first."
585
+
586
+ cur = current_session["curriculum"]
587
+ ch_i = current_session["chapter_idx"]
588
+ m_i = current_session["module_idx"]
589
+
590
+ if ch_i >= len(cur.chapters):
591
+ return "⚠️ No active chapter available."
592
+
593
+ chapter = cur.chapters[ch_i]
594
+ if m_i >= len(chapter.modules):
595
+ # Shouldn't happen, but guard
596
+ return "⚠️ No active module to evaluate."
597
+
598
+ module = chapter.modules[m_i]
599
+ teacher = TeacherAgent(current_session["user"].actual_level or current_session["user"].claimed_level)
600
+
601
+ # quick client-side guardrails for empty/brief examples
602
+ if not example_text or not str(example_text).strip():
603
+ return "❌ Please provide an example to demonstrate your understanding."
604
+ if len(str(example_text).strip().split()) < 10:
605
+ return "❌ Your example is too brief. Provide 2-3 sentences with specific details."
606
+
607
+ try:
608
+ eval_result = teacher.evaluate_example(module, example_text)
609
+ is_correct = bool(eval_result.get("is_correct"))
610
+ feedback = eval_result.get("feedback", "No feedback provided.")
611
+ confidence = eval_result.get("confidence", None)
612
+ except Exception as e:
613
+ return f"❌ Error evaluating example: {str(e)}. Please try again."
614
+
615
+ if is_correct:
616
+ # mark module as passed (do not auto-increment module_idx — require Next)
617
+ current_session["mode"] = "module_passed"
618
+ return f"✅ Example accepted. Feedback: {feedback}\n\n➡️ Click 'Next' to continue to the next module."
619
+ else:
620
+ # remain in awaiting_example — must retry
621
+ current_session["mode"] = "awaiting_example"
622
+ return f"❌ Example not sufficient. Feedback: {feedback}\n\nPlease try another example for the same module."
623
+
624
+ # ------------------------------
625
+ # Bloom assessment (per chapter)
626
+ # ------------------------------
627
+ def start_bloom_assessment():
628
+ if not current_session.get("curriculum"):
629
+ return "⚠️ No curriculum loaded. Please start a session first."
630
+
631
+ try:
632
+ current_session["mode"] = "bloom"
633
+ current_session["bloom_level"] = BLOOM_ORDER[0]
634
+ chapter = current_session["curriculum"].chapters[current_session["chapter_idx"]]
635
+ return ask_bloom_question(current_session["bloom_level"], chapter)
636
+ except Exception as e:
637
+ return f"❌ Error starting Bloom assessment: {str(e)}"
638
+
639
+ def ask_bloom_question(level, chapter):
640
+ try:
641
+ agent = BloomsAssessmentAgent()
642
+ # agent.generate_bloom_question expects (chapter, bloom_level)
643
+ q = agent.generate_bloom_question(chapter, level)
644
+ current_session["questions"] = [q]
645
+ return f"🌸 Bloom's Assessment ({level.title()}) for Chapter: {chapter.name}\n\n{q}\n\n✍️ Answer below and click Submit."
646
+ except Exception as e:
647
+ return f"❌ Error generating Bloom question: {str(e)}"
648
+
649
+ def handle_bloom(answer):
650
+ if not current_session.get("curriculum"):
651
+ return "⚠️ No curriculum loaded. Please start a session first."
652
+
653
+ if not answer or not str(answer).strip():
654
+ return "❌ Please provide an answer for the Bloom assessment question."
655
+
656
+ try:
657
+ level = current_session["bloom_level"]
658
+ if not current_session["questions"]:
659
+ return "⚠️ No question available. Please start a new session."
660
+ question = current_session["questions"][0]
661
+ chapter = current_session["curriculum"].chapters[current_session["chapter_idx"]]
662
+ agent = BloomsAssessmentAgent()
663
+
664
+ # agent.evaluate_bloom_answer returns a JSON string per your agent implementation
665
+ raw_eval = agent.evaluate_bloom_answer(question, answer, level, chapter)
666
+
667
+ # parse evaluation JSON
668
+ try:
669
+ eval_obj = json.loads(raw_eval)
670
+ score = float(eval_obj.get("score", 0))
671
+ feedback = str(eval_obj.get("feedback", "No feedback"))
672
+ except (json.JSONDecodeError, ValueError) as e:
673
+ # fallback: if text contains 'correct' treat as pass
674
+ feedback = f"Could not parse evaluation response. Raw: {raw_eval[:100]}..."
675
+ score = 10 if "correct" in raw_eval.lower() else 0
676
+ except KeyError as e:
677
+ return f"❌ Session error: Missing required data ({str(e)}). Please restart the session."
678
+ except Exception as e:
679
+ return f"❌ Error evaluating Bloom answer: {str(e)}. Please try again."
680
+
681
+ # use threshold (e.g., >=6/10)
682
+ if score >= 6:
683
+ # advance bloom level
684
+ next_idx = BLOOM_ORDER.index(level) + 1
685
+ if next_idx < len(BLOOM_ORDER):
686
+ current_session["bloom_level"] = BLOOM_ORDER[next_idx]
687
+ # generate new question for next level
688
+ return f"✅ {feedback}\n\n➡️ Moving to {BLOOM_ORDER[next_idx].title()}.\n\n" + ask_bloom_question(current_session["bloom_level"], chapter)
689
+ else:
690
+ # finished Bloom for chapter -> next chapter
691
+ current_session["chapter_idx"] += 1
692
+ current_session["module_idx"] = 0
693
+ current_session["mode"] = None
694
+ return f"🎉 {feedback}\n\n✅ Bloom’s assessment completed for chapter '{chapter.name}'.\nType 'next' to continue."
695
+ else:
696
+ # ask a new question for same level
697
+ return f"❌ {feedback}\n\n🔁 Try another question at the same level.\n\n" + ask_bloom_question(level, chapter)
698
+
699
+ # ------------------------------
700
+ # Gradio UI wiring (single unified output)
701
+ # ------------------------------
702
+ # Custom CSS for modal pop-up
703
+ modal_css = """
704
+ .modal-overlay:not([style*="display: none"]) {
705
+ position: fixed !important;
706
+ top: 0 !important;
707
+ left: 0 !important;
708
+ width: 100% !important;
709
+ height: 100% !important;
710
+ background-color: rgba(0, 0, 0, 0.5) !important;
711
+ z-index: 1000 !important;
712
+ display: flex !important;
713
+ align-items: center !important;
714
+ justify-content: center !important;
715
+ padding: 20px !important;
716
+ }
717
+
718
+ /* When Gradio hides the element, ensure it doesn't block interactions */
719
+ .modal-overlay[style*="display: none"],
720
+ .modal-overlay[style*="display:none"] {
721
+ display: none !important;
722
+ visibility: hidden !important;
723
+ pointer-events: none !important;
724
+ z-index: -1 !important;
725
+ opacity: 0 !important;
726
+ }
727
+
728
+ .modal-content {
729
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
730
+ border-radius: 10px !important;
731
+ padding: 20px !important;
732
+ max-width: 900px !important;
733
+ width: 100% !important;
734
+ max-height: 90vh !important;
735
+ overflow-y: auto !important;
736
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3) !important;
737
+ }
738
+
739
+ /* Chat window styling */
740
+ .modal-content .gradio-chatbot {
741
+ background-color: #ffffff !important;
742
+ border-radius: 8px !important;
743
+ padding: 15px !important;
744
+ border: 2px solid #e0e0e0 !important;
745
+ }
746
+
747
+ .modal-content .gradio-chatbot .message {
748
+ background-color: #f8f9fa !important;
749
+ border-radius: 8px !important;
750
+ padding: 10px !important;
751
+ margin: 5px 0 !important;
752
+ }
753
+
754
+ .modal-content .gradio-chatbot .user-message {
755
+ background-color: #e3f2fd !important;
756
+ border-left: 4px solid #2196f3 !important;
757
+ }
758
+
759
+ .modal-content .gradio-chatbot .bot-message {
760
+ background-color: #f1f8e9 !important;
761
+ border-left: 4px solid #8bc34a !important;
762
+ }
763
+
764
+ .modal-header {
765
+ margin: 0 !important;
766
+ padding: 0 !important;
767
+ flex-grow: 1 !important;
768
+ }
769
+
770
+ .modal-close-btn {
771
+ min-width: 40px !important;
772
+ height: 40px !important;
773
+ border-radius: 50% !important;
774
+ font-size: 20px !important;
775
+ font-weight: bold !important;
776
+ }
777
+
778
+ .modal-note {
779
+ font-size: 12px !important;
780
+ color: #666 !important;
781
+ margin-top: 10px !important;
782
+ }
783
+ """
784
+
785
+ with gr.Blocks(title="AI Teacher Bot") as demo:
786
+ gr.Markdown("""
787
+ # 🧠 AI Teacher Bot
788
+
789
+ In this interactive learning experience, you will be prompted to select your learning level—**Beginner**, **Intermediate**, or **Advanced**.
790
+
791
+ - If you choose **Intermediate** or **Advanced**, you will be presented with an assessment designed to test your understanding of the material. The questions in these assessments are tailored to the selected level, ensuring they are **challenging and reflective of the knowledge expected at that stage**. For instance, **Intermediate-level questions** will require more in-depth explanations, not just basic one-liner answers. Similarly, **Advanced-level assessments** will be comprehensive and demand a higher level of critical thinking and subject mastery.
792
+ """)
793
+
794
+
795
+ with gr.Row():
796
+ topic = gr.Textbox(label="📝 Topic", placeholder="e.g., Python Programming", scale=2)
797
+ level = gr.Dropdown(choices=LEVELS, value="novice", label="🎓 Your Level", scale=1)
798
+ start_btn = gr.Button("🚀 Start Session")
799
+
800
+ output = gr.Textbox(label="📚 Session Output", lines=25, interactive=False, autoscroll=True)
801
+ answer_box = gr.Textbox(label="✍️ Your Answer / Example", placeholder="Type your answer...", lines=5, max_lines=10)
802
+ with gr.Row():
803
+ submit_btn = gr.Button("Submit Answer")
804
+ next_btn = gr.Button("Next")
805
+ challenge_btn = gr.Button("Challenge Assessment", visible=False)
806
+
807
+ # Challenge discussion chat interface - Modal Pop-up
808
+ with gr.Column(visible=False, elem_classes="modal-overlay") as challenge_modal_overlay:
809
+ with gr.Column(elem_classes="modal-content"):
810
+ with gr.Row():
811
+ gr.Markdown("### 💬 Challenge Discussion with Grader", elem_classes="modal-header")
812
+ challenge_close_btn = gr.Button("✕", elem_classes="modal-close-btn", scale=0)
813
+
814
+ with gr.Row():
815
+ with gr.Column(scale=3):
816
+ challenge_chat = gr.Chatbot(
817
+ label="",
818
+ height=400,
819
+ show_label=False,
820
+ container=True
821
+ )
822
+ challenge_status = gr.Textbox(
823
+ label="Status",
824
+ interactive=False,
825
+ lines=2,
826
+ container=True
827
+ )
828
+ with gr.Column(scale=1):
829
+ challenge_msg_box = gr.Textbox(
830
+ label="Your Argument/Question",
831
+ placeholder="Explain why you think your answer is correct...",
832
+ lines=5,
833
+ container=True
834
+ )
835
+ challenge_send_btn = gr.Button("Send", variant="primary")
836
+ gr.Markdown("**Note:** You have up to 3 exchanges with the grader.", elem_classes="modal-note")
837
+
838
+ # Function to update challenge button visibility
839
+ def update_challenge_visibility():
840
+ mode = current_session.get("mode")
841
+ has_feedback = current_session.get("last_feedback") is not None
842
+ # Show button during assessment mode after first answer is submitted
843
+ should_show = (mode == "assessment" and has_feedback)
844
+ return gr.update(visible=should_show)
845
+
846
+ # handlers
847
+ start_btn.click(
848
+ fn=start_learning_session,
849
+ inputs=[topic, level],
850
+ outputs=[output]
851
+ ).then(
852
+ fn=update_challenge_visibility,
853
+ inputs=None,
854
+ outputs=[challenge_btn]
855
+ )
856
+
857
+ submit_btn.click(
858
+ fn=submit_answer,
859
+ inputs=[answer_box],
860
+ outputs=[output, answer_box]
861
+ ).then(
862
+ fn=update_challenge_visibility,
863
+ inputs=None,
864
+ outputs=[challenge_btn]
865
+ )
866
+
867
+ next_btn.click(
868
+ fn=next_step,
869
+ inputs=[answer_box],
870
+ outputs=[output]
871
+ ).then(
872
+ fn=update_challenge_visibility,
873
+ inputs=None,
874
+ outputs=[challenge_btn]
875
+ )
876
+ challenge_btn.click(
877
+ fn=start_challenge_discussion,
878
+ inputs=None,
879
+ outputs=[challenge_modal_overlay, challenge_chat, challenge_status, output]
880
+ ).then(
881
+ fn=lambda: (gr.update(value="", interactive=True), gr.update(interactive=True)),
882
+ inputs=None,
883
+ outputs=[challenge_msg_box, challenge_send_btn]
884
+ )
885
+
886
+ def send_and_clear(message, chat_history):
887
+ """Send message and clear input box (if needed)"""
888
+ history, status, msg_enabled, btn_enabled, output_update = handle_challenge_message(message, chat_history)
889
+ if message and message.strip():
890
+ msg_update = gr.update(value="", interactive=msg_enabled)
891
+ else:
892
+ msg_update = gr.update(value=message, interactive=msg_enabled)
893
+ btn_update = gr.update(interactive=btn_enabled)
894
+ return history, msg_update, status, btn_update, output_update
895
+
896
+ challenge_send_btn.click(
897
+ fn=send_and_clear,
898
+ inputs=[challenge_msg_box, challenge_chat],
899
+ outputs=[challenge_chat, challenge_msg_box, challenge_status, challenge_send_btn, output]
900
+ )
901
+
902
+ challenge_msg_box.submit(
903
+ fn=send_and_clear,
904
+ inputs=[challenge_msg_box, challenge_chat],
905
+ outputs=[challenge_chat, challenge_msg_box, challenge_status, challenge_send_btn, output]
906
+ )
907
+
908
+ def close_challenge_discussion():
909
+ """
910
+ Properly closes the challenge modal and restores UI control
911
+ """
912
+ # Reset backend state
913
+ current_session["challenge_mode"] = False
914
+ current_session["challenge_chat_history"] = []
915
+ current_session["challenge_exchanges"] = 0
916
+
917
+ # IMPORTANT: Use visible=False to hide the overlay completely
918
+ # This should remove it from the DOM or set display:none which CSS will respect
919
+ return (
920
+ gr.update(visible=False), # Hide overlay - this should remove blocking
921
+ empty_chat(), # Reset chat
922
+ "✅ Challenge closed.", # Status message
923
+ gr.update(value=current_session.get("last_output", "")) # Keep main output
924
+ )
925
+
926
+
927
+ challenge_close_btn.click(
928
+ fn=close_challenge_discussion,
929
+ inputs=None,
930
+ outputs=[
931
+ challenge_modal_overlay, # visible=False
932
+ challenge_chat, # reset chat
933
+ challenge_status, # status text
934
+ output # main output (reevaluated decision)
935
+ ]
936
+ ).then(
937
+ fn=update_challenge_visibility,
938
+ inputs=None,
939
+ outputs=[challenge_btn]
940
+ )
941
+
942
+
943
+ if __name__ == "__main__":
944
+ demo.launch(
945
+ share=True,
946
+ server_name="0.0.0.0",
947
+ server_port=7860,
948
+ theme=gr.themes.Soft(),
949
+ css=modal_css
950
+ )
951
+
curriculum.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Curriculum:
2
+ def __init__(self, topic, chapters=None):
3
+ self.topic = topic
4
+ self.chapters = chapters if chapters else [] # List of Chapter objects
5
+
6
+ def add_chapter(self, chapter):
7
+ self.chapters.append(chapter)
8
+
9
+ def print_curriculum(self):
10
+ print(f"\nCurriculum for: {self.topic}\n" + "="*40)
11
+ for i, chapter in enumerate(self.chapters, 1):
12
+ print(f"\nChapter {i}: {chapter.name}")
13
+ print("-" * (10 + len(chapter.name)))
14
+ for j, module in enumerate(chapter.modules, 1):
15
+ print(f" {i}.{j} {module.name}")
16
+ if hasattr(module, 'learning_objective') and module.learning_objective:
17
+ print(f" Learning Objective: {module.learning_objective}")
18
+
19
+ def __repr__(self):
20
+ return f"Curriculum(topic={self.topic}, chapters={self.chapters})"
21
+
22
+ class Chapter:
23
+ def __init__(self, name, modules=None):
24
+ self.name = name
25
+ self.modules = modules if modules else [] # List of Module objects
26
+
27
+ def add_module(self, module):
28
+ self.modules.append(module)
29
+
30
+ def __repr__(self):
31
+ return f"Chapter(name={self.name}, modules={self.modules})"
32
+
33
+ class Module:
34
+ def __init__(self, name, learning_objective=None, content=None):
35
+ self.name = name
36
+ self.learning_objective = learning_objective
37
+ self.content = content
38
+
39
+ def __repr__(self):
40
+ return f"Module(name={self.name}, learning_objective={self.learning_objective})"
41
+
main.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ AI Teacher Bot - Complete CLI Interface
4
+ This is the main command-line interface for the AI Teacher Bot system.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ from datetime import datetime
11
+ from dotenv import load_dotenv
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ from user_state import UserState
17
+ from curriculum import Curriculum, Chapter, Module
18
+ from agents.level_assess import LevelAssessmentAgent
19
+ from agents.curriculum_planner import CurriculumPlannerAgent
20
+ from agents.coordinator import CoordinatorAgent
21
+ from agents.bloom_assess import BloomsAssessmentAgent
22
+
23
+ LEVELS = ['novice', 'intermediate', 'advanced']
24
+
25
+ def print_banner():
26
+ """Print the application banner"""
27
+ print("=" * 60)
28
+ print("🧠 AI TEACHER BOT - COMPLETE LEARNING SYSTEM")
29
+ print("=" * 60)
30
+ print("Multi-Agent Adaptive Learning Platform")
31
+ print("Features: Level Assessment, Dynamic Curriculum, Bloom's Taxonomy")
32
+ print("=" * 60)
33
+
34
+ def check_api_key():
35
+ """Check if OpenAI API key is configured"""
36
+ api_key = os.getenv("OPENAI_API_KEY")
37
+ if not api_key or api_key == "your_openai_api_key_here":
38
+ print("❌ OpenAI API key not configured!")
39
+ print("Please set your OPENAI_API_KEY in the .env file")
40
+ print("Get your API key from: https://platform.openai.com/api-keys")
41
+ return False
42
+ return True
43
+
44
+ # Removed progress loading for now - focusing on core flow
45
+
46
+ def get_user_input():
47
+ """Get topic and level from user"""
48
+ print("\n🎯 Let's start your learning journey!")
49
+
50
+ # Get topic
51
+ while True:
52
+ topic = input("\nWhat topic would you like to study? ").strip()
53
+ if topic:
54
+ break
55
+ print("Please enter a valid topic")
56
+
57
+ # Get level
58
+ print("\n📚 What's your current experience level?")
59
+ for i, level in enumerate(LEVELS, 1):
60
+ print(f" {i}. {level.capitalize()}")
61
+
62
+ while True:
63
+ try:
64
+ level_choice = int(input("Enter the number for your level: "))
65
+ if 1 <= level_choice <= len(LEVELS):
66
+ claimed_level = LEVELS[level_choice - 1]
67
+ break
68
+ else:
69
+ print("Please enter a valid number")
70
+ except ValueError:
71
+ print("Please enter a valid number")
72
+
73
+ return topic, claimed_level
74
+
75
+ def run_level_assessment(topic, claimed_level):
76
+ """Run level assessment for non-novice users"""
77
+ print(f"\n📝 Assessing your knowledge level for {topic}...")
78
+ print("This will help us create the perfect learning path for you.")
79
+
80
+ try:
81
+ assessor = LevelAssessmentAgent()
82
+ actual_level = assessor.process(topic, claimed_level)
83
+ return actual_level
84
+ except Exception as e:
85
+ print(f"❌ Error during assessment: {e}")
86
+ print("⚠️ Defaulting to novice level")
87
+ return "novice"
88
+
89
+ def generate_curriculum(topic, level):
90
+ """Generate curriculum for the topic and level"""
91
+ print(f"\n📚 Generating personalized curriculum for {topic} at {level} level...")
92
+
93
+ try:
94
+ planner = CurriculumPlannerAgent()
95
+ curriculum = planner.process(topic, level)
96
+ print("✅ Curriculum generated successfully!")
97
+ return curriculum
98
+ except Exception as e:
99
+ print(f"❌ Error generating curriculum: {e}")
100
+ return None
101
+
102
+ def main():
103
+ """Main application function - Core Learning Flow"""
104
+ print_banner()
105
+
106
+ # Check API key
107
+ if not check_api_key():
108
+ return
109
+
110
+ # Get user input for new session
111
+ topic, claimed_level = get_user_input()
112
+ user = UserState(topic=topic, claimed_level=claimed_level)
113
+ print(f"\n👤 User Profile: {user.topic} at {user.claimed_level} level")
114
+
115
+ # Level assessment
116
+ if claimed_level == 'novice':
117
+ user.set_actual_level("novice")
118
+ print("✅ Skipping level assessment for novice users")
119
+ else:
120
+ actual_level = run_level_assessment(topic, claimed_level)
121
+ user.set_actual_level(actual_level)
122
+ print(f"✅ Assessment complete! Your level: {actual_level}")
123
+
124
+ # Generate curriculum
125
+ curriculum = generate_curriculum(user.topic, user.actual_level)
126
+ if curriculum is None:
127
+ print("❌ Cannot proceed without curriculum")
128
+ return
129
+
130
+ # Display curriculum
131
+ print("\n" + "="*60)
132
+ print("📖 YOUR PERSONALIZED CURRICULUM")
133
+ print("="*60)
134
+ curriculum.print_curriculum()
135
+
136
+ # Start teaching
137
+ print(f"\n🚀 Starting your learning journey at {user.actual_level} level!")
138
+ print("The AI teacher will guide you through each module step by step.")
139
+
140
+ try:
141
+ coordinator = CoordinatorAgent(user.actual_level, user)
142
+ coordinator.teach_curriculum(curriculum)
143
+
144
+ # Final summary
145
+ print("\n" + "="*60)
146
+ print("🎉 LEARNING SESSION COMPLETE!")
147
+ print("="*60)
148
+ print("You've completed your learning journey!")
149
+ print("Great job on mastering the concepts! 🌟")
150
+
151
+ except KeyboardInterrupt:
152
+ print("\n\n⏸️ Learning session stopped by user")
153
+ print("Thanks for learning with us!")
154
+ except Exception as e:
155
+ print(f"\n❌ Error during learning session: {e}")
156
+ print("Please try again or check your configuration.")
157
+
158
+ print("\n👋 Thank you for using AI Teacher Bot!")
159
+ print("Keep learning and growing! 🌱")
160
+
161
+ if __name__ == "__main__":
162
+ main()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio
2
+ openai
3
+ python-dotenv
4
+ requests
user_state.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from datetime import datetime
3
+
4
+ class UserState:
5
+ def __init__(self, topic=None, claimed_level=None):
6
+ self.topic = topic
7
+ self.claimed_level = claimed_level # e.g., 'novice', 'intermediate', 'advanced'
8
+ self.actual_level = None # Set after assessment
9
+ self.progress = {
10
+ 'modules_completed': {}, # {chapter_name: {module_name: status}}
11
+ 'chapters_completed': {}, # {chapter_name: completion_status}
12
+ 'bloom_assessments': {}, # {chapter_name: assessment_results}
13
+ 'overall_progress': {
14
+ 'total_modules': 0,
15
+ 'completed_modules': 0,
16
+ 'total_chapters': 0,
17
+ 'completed_chapters': 0,
18
+ 'average_bloom_score': 0
19
+ },
20
+ 'session_data': {
21
+ 'start_time': datetime.now().isoformat(),
22
+ 'last_activity': datetime.now().isoformat()
23
+ }
24
+ }
25
+
26
+ def set_actual_level(self, level):
27
+ self.actual_level = level
28
+
29
+ @property
30
+ def display_level(self):
31
+ """Returns the actual level if set, otherwise the claimed level"""
32
+ return self.actual_level or self.claimed_level
33
+
34
+ def update_module_progress(self, chapter_name, module_name, status="completed"):
35
+ """Update the completion status of a module"""
36
+ if chapter_name not in self.progress['modules_completed']:
37
+ self.progress['modules_completed'][chapter_name] = {}
38
+
39
+ self.progress['modules_completed'][chapter_name][module_name] = {
40
+ 'status': status,
41
+ 'completed_at': datetime.now().isoformat()
42
+ }
43
+ self.progress['session_data']['last_activity'] = datetime.now().isoformat()
44
+
45
+ def update_chapter_progress(self, chapter_name, status="completed"):
46
+ """Update the completion status of a chapter"""
47
+ self.progress['chapters_completed'][chapter_name] = {
48
+ 'status': status,
49
+ 'completed_at': datetime.now().isoformat()
50
+ }
51
+ self.progress['session_data']['last_activity'] = datetime.now().isoformat()
52
+
53
+ def update_bloom_assessment(self, chapter_name, assessment_results):
54
+ """Store Bloom's assessment results for a chapter"""
55
+ # Store the assessment results directly without wrapping in 'results'
56
+ self.progress['bloom_assessments'][chapter_name] = assessment_results
57
+ self.progress['bloom_assessments'][chapter_name]['completed_at'] = datetime.now().isoformat()
58
+ self.progress['session_data']['last_activity'] = datetime.now().isoformat()
59
+
60
+ def calculate_overall_progress(self, curriculum):
61
+ """Calculate overall progress based on curriculum"""
62
+ total_modules = sum(len(chapter.modules) for chapter in curriculum.chapters)
63
+ completed_modules = sum(
64
+ len([m for m in modules.values() if m.get('status') == 'completed'])
65
+ for modules in self.progress['modules_completed'].values()
66
+ )
67
+
68
+ total_chapters = len(curriculum.chapters)
69
+ completed_chapters = len([
70
+ c for c in self.progress['chapters_completed'].values()
71
+ if c.get('status') == 'completed'
72
+ ])
73
+
74
+ # Calculate average Bloom's score
75
+ bloom_scores = [
76
+ assessment.get('average_score', 0)
77
+ for assessment in self.progress['bloom_assessments'].values()
78
+ ]
79
+ average_bloom_score = sum(bloom_scores) / len(bloom_scores) if bloom_scores else 0
80
+
81
+ self.progress['overall_progress'] = {
82
+ 'total_modules': total_modules,
83
+ 'completed_modules': completed_modules,
84
+ 'total_chapters': total_chapters,
85
+ 'completed_chapters': completed_chapters,
86
+ 'average_bloom_score': average_bloom_score,
87
+ 'completion_percentage': (completed_modules / total_modules * 100) if total_modules > 0 else 0
88
+ }
89
+
90
+ def get_progress_summary(self, curriculum=None):
91
+ """Get a formatted progress summary"""
92
+ if curriculum:
93
+ self.calculate_overall_progress(curriculum)
94
+
95
+ overall = self.progress['overall_progress']
96
+
97
+ summary = f"""
98
+ PROGRESS SUMMARY
99
+ ===============
100
+ Topic: {self.topic}
101
+ Level: {self.actual_level or self.claimed_level}
102
+
103
+ Overall Progress:
104
+ - Modules: {overall['completed_modules']}/{overall['total_modules']} ({overall['completion_percentage']:.1f}%)
105
+ - Chapters: {overall['completed_chapters']}/{overall['total_chapters']}
106
+ - Average Bloom's Score: {overall['average_bloom_score']:.1f}/10
107
+
108
+ Chapter Progress:
109
+ """
110
+
111
+ for chapter_name, chapter_data in self.progress['chapters_completed'].items():
112
+ status = chapter_data.get('status', 'not_started')
113
+ summary += f"- {chapter_name}: {status}\n"
114
+
115
+ return summary
116
+
117
+
118
+ def __repr__(self):
119
+ return f"UserState(topic={self.topic}, claimed_level={self.claimed_level}, actual_level={self.actual_level})"