AlexFocus commited on
Commit
e468c5f
·
1 Parent(s): 65bd9ac

added gamification and evolution

Browse files
src/learnbee/gamification.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gamification system for tracking progress and rewarding achievements in learning sessions."""
2
+
3
+ from typing import List, Dict, Tuple
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass
8
+ class Badge:
9
+ """Represents an unlockable badge."""
10
+ name: str
11
+ emoji: str
12
+ description: str
13
+ requirement: int # Number of stars needed
14
+
15
+
16
+ # Define available badges
17
+ BADGES = [
18
+ Badge("First Steps", "🎯", "Started your learning adventure!", 1),
19
+ Badge("Explorer", "🔍", "Asked 3 great questions!", 3),
20
+ Badge("Problem Solver", "🧩", "Solved 5 challenges!", 5),
21
+ Badge("Star Student", "⭐", "Earned 10 stars!", 10),
22
+ Badge("Creative Thinker", "🎨", "Showed amazing creativity!", 15),
23
+ Badge("Adventure Master", "🏆", "Completed the adventure!", 20),
24
+ ]
25
+
26
+
27
+ class GamificationTracker:
28
+ """Tracks gamification progress for a learning session."""
29
+
30
+ def __init__(self):
31
+ """Initialize a new gamification tracker."""
32
+ self.stars: int = 0
33
+ self.badges_unlocked: List[str] = []
34
+ self.achievements: List[Dict[str, str]] = [] # List of {reason, emoji, timestamp}
35
+ self.milestone_messages: List[str] = []
36
+
37
+ def award_star(self, reason: str = "Great job!") -> Tuple[bool, str]:
38
+ """
39
+ Award a star to the child.
40
+
41
+ Args:
42
+ reason: Why the star was awarded
43
+
44
+ Returns:
45
+ Tuple of (badge_unlocked, message)
46
+ """
47
+ self.stars += 1
48
+ self.achievements.append({
49
+ "type": "star",
50
+ "reason": reason,
51
+ "emoji": "⭐"
52
+ })
53
+
54
+ # Check for badge unlocks
55
+ badge_unlocked, badge_message = self._check_badge_unlock()
56
+
57
+ return badge_unlocked, badge_message
58
+
59
+ def _check_badge_unlock(self) -> Tuple[bool, str]:
60
+ """
61
+ Check if any badges should be unlocked based on current stars.
62
+
63
+ Returns:
64
+ Tuple of (unlocked, message)
65
+ """
66
+ for badge in BADGES:
67
+ if badge.name not in self.badges_unlocked and self.stars >= badge.requirement:
68
+ self.badges_unlocked.append(badge.name)
69
+ message = f"🎉 Badge Unlocked: {badge.emoji} {badge.name} - {badge.description}"
70
+ self.milestone_messages.append(message)
71
+ return True, message
72
+
73
+ return False, ""
74
+
75
+ def get_progress_percentage(self, max_stars: int = 20) -> int:
76
+ """
77
+ Calculate progress percentage.
78
+
79
+ Args:
80
+ max_stars: Maximum stars for the adventure (default 20)
81
+
82
+ Returns:
83
+ Progress percentage (0-100)
84
+ """
85
+ return min(int((self.stars / max_stars) * 100), 100)
86
+
87
+ def get_next_milestone(self) -> str:
88
+ """
89
+ Get the next milestone the child is working towards.
90
+
91
+ Returns:
92
+ Description of next milestone
93
+ """
94
+ for badge in BADGES:
95
+ if badge.name not in self.badges_unlocked:
96
+ stars_needed = badge.requirement - self.stars
97
+ if stars_needed > 0:
98
+ return f"{stars_needed} more star{'s' if stars_needed != 1 else ''} to unlock {badge.emoji} {badge.name}!"
99
+
100
+ return "You've unlocked all badges! Amazing work! 🌟"
101
+
102
+ def get_progress_html(self) -> str:
103
+ """
104
+ Generate HTML for displaying gamification progress.
105
+
106
+ Returns:
107
+ HTML string with progress display
108
+ """
109
+ # Calculate progress
110
+ progress_pct = self.get_progress_percentage()
111
+ progress_bar_filled = int(progress_pct / 5) # 20 blocks total
112
+ progress_bar_empty = 20 - progress_bar_filled
113
+ progress_bar = "█" * progress_bar_filled + "░" * progress_bar_empty
114
+
115
+ # Generate star display (show up to 10 stars visually)
116
+ star_display = "⭐" * min(self.stars, 10)
117
+ if self.stars > 10:
118
+ star_display += f" +{self.stars - 10}"
119
+
120
+ # Generate badges display (compact)
121
+ badges_html = ""
122
+ if self.badges_unlocked:
123
+ badge_items = []
124
+ for badge in BADGES:
125
+ if badge.name in self.badges_unlocked:
126
+ badge_items.append(f"{badge.emoji} {badge.name}")
127
+ badges_html = " • ".join(badge_items[:3]) # Show max 3 badges
128
+ if len(self.badges_unlocked) > 3:
129
+ badges_html += f" +{len(self.badges_unlocked) - 3}"
130
+ else:
131
+ badges_html = "Keep learning to unlock badges!"
132
+
133
+ # Get next milestone
134
+ next_milestone = self.get_next_milestone()
135
+
136
+ # Build compact horizontal HTML
137
+ html = f"""
138
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 16px 24px; border-radius: 12px; color: white; margin: 20px 0; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);">
139
+ <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 20px;">
140
+
141
+ <!-- Stars Section -->
142
+ <div style="display: flex; align-items: center; gap: 12px;">
143
+ <div style="font-size: 1.2rem; font-weight: bold;">🌟 Progress</div>
144
+ <div style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 8px; font-size: 1rem;">
145
+ {star_display} <span style="opacity: 0.9;">({self.stars} stars)</span>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- Progress Bar Section -->
150
+ <div style="flex: 1; min-width: 200px; max-width: 300px;">
151
+ <div style="background: rgba(0,0,0,0.2); border-radius: 10px; padding: 6px; font-family: monospace; font-size: 0.85rem; letter-spacing: 1px; text-align: center;">
152
+ {progress_bar} {progress_pct}%
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Badges Section -->
157
+ <div style="display: flex; align-items: center; gap: 12px;">
158
+ <div style="font-weight: bold;">🏆</div>
159
+ <div style="background: rgba(255,255,255,0.2); padding: 8px 16px; border-radius: 8px; font-size: 0.9rem;">
160
+ {badges_html}
161
+ </div>
162
+ </div>
163
+
164
+ <!-- Next Milestone -->
165
+ <div style="background: rgba(255,255,255,0.25); padding: 8px 16px; border-radius: 8px; font-size: 0.9rem; font-weight: 500;">
166
+ 🎯 {next_milestone}
167
+ </div>
168
+
169
+ </div>
170
+ </div>
171
+ """
172
+
173
+ return html
174
+
175
+
176
+ def get_celebration_message(self) -> str:
177
+ """
178
+ Get a celebration message if there are new achievements.
179
+
180
+ Returns:
181
+ Celebration message or empty string
182
+ """
183
+ if self.milestone_messages:
184
+ message = self.milestone_messages[-1]
185
+ return message
186
+ return ""
187
+
188
+ def clear_milestone_messages(self):
189
+ """Clear milestone messages after they've been displayed."""
190
+ self.milestone_messages.clear()
src/learnbee/llm_call.py CHANGED
@@ -161,21 +161,22 @@ class LLMCall:
161
  concepts_text = ", ".join(concepts[:8]) # Show up to 8 concepts
162
 
163
  system_prompt = (
164
- f"You are an educational expert creating an introduction for a lesson for children ages 3-12. "
165
  f"IMPORTANT: You must write the ENTIRE introduction in {language}. "
166
- f"Create a friendly, engaging introduction that includes:\n\n"
167
- f"1. A brief, exciting summary of what the child will learn (2-3 sentences, very simple language)\n"
168
- f"2. A list of the key concepts they'll explore\n"
169
- f"3. 2-3 example questions that the tutor could ask to start the conversation and guide the child\n\n"
170
- f"Format your response as follows:\n"
171
- f"SUMMARY:\n"
172
- f"[Brief summary here in {language}]\n\n"
173
- f"KEY CONCEPTS:\n"
174
- f"[List concepts here, one per line with a bullet point]\n\n"
175
- f"EXAMPLE QUESTIONS TO GET STARTED:\n"
176
- f"[2-3 engaging questions, one per line with a bullet point, in {language}]\n\n"
177
- f"Use very simple, age-appropriate language in {language}. Make it fun and exciting! "
178
- f"The questions should be open-ended and encourage exploration. "
 
179
  f"Everything must be written in {language}."
180
  )
181
 
 
161
  concepts_text = ", ".join(concepts[:8]) # Show up to 8 concepts
162
 
163
  system_prompt = (
164
+ f"You are an educational expert creating a warm, natural introduction for an interactive learning adventure for children ages 3-12. "
165
  f"IMPORTANT: You must write the ENTIRE introduction in {language}. "
166
+ f"Create a friendly, conversational introduction that:\n\n"
167
+ f"1. Explains what adventure/activity the child is about to experience (in natural, story-like language)\n"
168
+ f"2. Describes the child's role or task in this adventure (what they'll be doing)\n"
169
+ f"3. Ends with an engaging, open-ended question to start the conversation\n\n"
170
+ f"STYLE GUIDELINES:\n"
171
+ f"- Write in a warm, conversational tone (like a friend talking to the child)\n"
172
+ f"- Use simple, age-appropriate language in {language}\n"
173
+ f"- Make it exciting and inviting, not formal or structured\n"
174
+ f"- Don't use bullet points or structured lists - write in natural paragraphs\n"
175
+ f"- The question at the end should be specific to the adventure and encourage the child to share their thoughts\n"
176
+ f"- Keep it to 3-4 sentences total, then the question\n\n"
177
+ f"Example structure (but make it unique to the lesson):\n"
178
+ f"'Today we're going on [adventure description]! Your mission is to [child's task]. "
179
+ f"Along the way, we'll discover [what they'll learn]. So, [engaging question]?'\n\n"
180
  f"Everything must be written in {language}."
181
  )
182
 
src/learnbee/prompts.py CHANGED
@@ -5,7 +5,8 @@ def generate_tutor_system_prompt(
5
  tutor_name: str,
6
  tutor_description: str,
7
  difficulty_level: str,
8
- lesson_content: str
 
9
  ) -> str:
10
  """
11
  Generate the system prompt for an educational tutor.
@@ -97,7 +98,18 @@ def generate_tutor_system_prompt(
97
  "====================\n"
98
  f"{lesson_content}\n"
99
  "====================\n\n"
100
-
 
 
 
 
 
 
 
 
 
 
 
101
  "YOUR ROLE:\n"
102
  "You are teaching this lesson content through PROBLEMS AND QUESTIONS. Your job is to:\n"
103
  "1. Present engaging challenges based on the lesson\n"
 
5
  tutor_name: str,
6
  tutor_description: str,
7
  difficulty_level: str,
8
+ lesson_content: str,
9
+ adaptive_context: str = ""
10
  ) -> str:
11
  """
12
  Generate the system prompt for an educational tutor.
 
98
  "====================\n"
99
  f"{lesson_content}\n"
100
  "====================\n\n"
101
+ )
102
+
103
+ # Add adaptive context if provided
104
+ if adaptive_context:
105
+ system_prompt += (
106
+ "ADAPTIVE PERSONALIZATION:\n"
107
+ "====================\n"
108
+ f"{adaptive_context}\n"
109
+ "====================\n\n"
110
+ )
111
+
112
+ system_prompt += (
113
  "YOUR ROLE:\n"
114
  "You are teaching this lesson content through PROBLEMS AND QUESTIONS. Your job is to:\n"
115
  "1. Present engaging challenges based on the lesson\n"
src/learnbee/session_state.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Session state management for tracking child interactions and adaptive personalization."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Dict
5
+
6
+
7
+ @dataclass
8
+ class SessionState:
9
+ """Tracks the state of a learning session for adaptive personalization."""
10
+
11
+ # Interaction tracking
12
+ total_messages: int = 0
13
+ correct_answers: int = 0
14
+ incorrect_answers: int = 0
15
+ questions_asked_by_child: int = 0
16
+
17
+ # Engagement metrics
18
+ short_responses: int = 0 # 1-5 words
19
+ detailed_responses: int = 0 # 6+ words
20
+ emoji_count: int = 0
21
+ exclamation_count: int = 0
22
+
23
+ # Adaptive difficulty
24
+ current_difficulty_adjustment: str = "neutral" # "easier", "neutral", "harder"
25
+
26
+ # Topics explored
27
+ topics_discussed: List[str] = field(default_factory=list)
28
+
29
+ # Gamification (will be used by GamificationTracker)
30
+ stars_earned: int = 0
31
+ badges_unlocked: List[str] = field(default_factory=list)
32
+
33
+ def analyze_message(self, message: str) -> Dict[str, any]:
34
+ """
35
+ Analyze a child's message to extract engagement and comprehension signals.
36
+
37
+ Args:
38
+ message: The child's message
39
+
40
+ Returns:
41
+ Dictionary with analysis results
42
+ """
43
+ self.total_messages += 1
44
+
45
+ # Count words
46
+ words = message.split()
47
+ word_count = len(words)
48
+
49
+ # Track response length
50
+ if word_count <= 5:
51
+ self.short_responses += 1
52
+ else:
53
+ self.detailed_responses += 1
54
+
55
+ # Count engagement markers
56
+ emoji_count = sum(1 for char in message if ord(char) > 127000) # Simple emoji detection
57
+ exclamation_count = message.count('!')
58
+ question_count = message.count('?')
59
+
60
+ self.emoji_count += emoji_count
61
+ self.exclamation_count += exclamation_count
62
+
63
+ if question_count > 0:
64
+ self.questions_asked_by_child += 1
65
+
66
+ # Calculate engagement level
67
+ engagement_score = 0
68
+ if word_count > 5:
69
+ engagement_score += 2
70
+ if emoji_count > 0:
71
+ engagement_score += 1
72
+ if exclamation_count > 0:
73
+ engagement_score += 1
74
+ if question_count > 0:
75
+ engagement_score += 2
76
+
77
+ engagement_level = "low" if engagement_score < 2 else "medium" if engagement_score < 4 else "high"
78
+
79
+ return {
80
+ "word_count": word_count,
81
+ "engagement_level": engagement_level,
82
+ "has_question": question_count > 0,
83
+ "has_emoji": emoji_count > 0,
84
+ "has_exclamation": exclamation_count > 0
85
+ }
86
+
87
+ def record_answer_quality(self, is_correct: bool):
88
+ """
89
+ Record whether the child's answer was correct.
90
+
91
+ Args:
92
+ is_correct: True if answer was correct, False otherwise
93
+ """
94
+ if is_correct:
95
+ self.correct_answers += 1
96
+ else:
97
+ self.incorrect_answers += 1
98
+
99
+ # Update difficulty adjustment based on success rate
100
+ total_answers = self.correct_answers + self.incorrect_answers
101
+ if total_answers >= 3: # Need at least 3 answers to adjust
102
+ success_rate = self.correct_answers / total_answers
103
+
104
+ if success_rate < 0.4:
105
+ self.current_difficulty_adjustment = "easier"
106
+ elif success_rate > 0.8:
107
+ self.current_difficulty_adjustment = "harder"
108
+ else:
109
+ self.current_difficulty_adjustment = "neutral"
110
+
111
+ def get_adaptive_context(self) -> str:
112
+ """
113
+ Generate adaptive context string for the system prompt.
114
+
115
+ Returns:
116
+ String with adaptive instructions based on session state
117
+ """
118
+ context_parts = []
119
+
120
+ # Difficulty adjustment
121
+ if self.current_difficulty_adjustment == "easier":
122
+ context_parts.append(
123
+ "ADAPTIVE INSTRUCTION: The child is struggling. Simplify your language, "
124
+ "break problems into smaller steps, and provide more hints and encouragement."
125
+ )
126
+ elif self.current_difficulty_adjustment == "harder":
127
+ context_parts.append(
128
+ "ADAPTIVE INSTRUCTION: The child is excelling! Increase the challenge, "
129
+ "ask deeper questions, and introduce more complex concepts."
130
+ )
131
+
132
+ # Engagement adjustment
133
+ if self.total_messages >= 3:
134
+ engagement_ratio = self.detailed_responses / max(self.total_messages, 1)
135
+
136
+ if engagement_ratio < 0.3:
137
+ context_parts.append(
138
+ "ENGAGEMENT NOTE: The child is giving short responses. Make your questions "
139
+ "more exciting, use more of your character's personality, and relate to their interests."
140
+ )
141
+ elif engagement_ratio > 0.7:
142
+ context_parts.append(
143
+ "ENGAGEMENT NOTE: The child is highly engaged with detailed responses! "
144
+ "Match their energy and enthusiasm. Present bonus challenges and celebrate their curiosity."
145
+ )
146
+
147
+ # Question-asking behavior
148
+ if self.questions_asked_by_child >= 2:
149
+ context_parts.append(
150
+ "CURIOSITY NOTE: The child is asking great questions! Encourage this curiosity "
151
+ "and turn their questions into exciting learning opportunities."
152
+ )
153
+
154
+ return "\n".join(context_parts) if context_parts else ""
155
+
156
+ def get_session_summary(self) -> Dict[str, any]:
157
+ """
158
+ Get a summary of the current session state.
159
+
160
+ Returns:
161
+ Dictionary with session statistics
162
+ """
163
+ total_answers = self.correct_answers + self.incorrect_answers
164
+ success_rate = (self.correct_answers / total_answers * 100) if total_answers > 0 else 0
165
+
166
+ return {
167
+ "total_messages": self.total_messages,
168
+ "correct_answers": self.correct_answers,
169
+ "incorrect_answers": self.incorrect_answers,
170
+ "success_rate": success_rate,
171
+ "questions_asked": self.questions_asked_by_child,
172
+ "stars_earned": self.stars_earned,
173
+ "badges_unlocked": len(self.badges_unlocked),
174
+ "difficulty_adjustment": self.current_difficulty_adjustment
175
+ }
src/learnbee/tutor_handlers.py CHANGED
@@ -9,6 +9,10 @@ from learnbee.mcp_server import create_lesson, get_lesson_content, get_lesson_li
9
  from learnbee.prompts import generate_tutor_system_prompt
10
 
11
 
 
 
 
 
12
  def load_lesson_content(lesson_name, selected_tutor, selected_language, progress=gr.Progress()):
13
  """
14
  Load lesson content and extract key concepts.
@@ -73,8 +77,7 @@ def load_lesson_content(lesson_name, selected_tutor, selected_language, progress
73
  # Format the introduction as a friendly greeting from the tutor
74
  tutor_greeting = (
75
  f"Hello! 👋 I'm {selected_tutor}, and I'm so excited to learn with you today!\n\n"
76
- f"{introduction}\n\n"
77
- f"Let's start our learning adventure! What would you like to explore first? 🌟"
78
  )
79
  chatbot_messages = [{"role": "assistant", "content": tutor_greeting}]
80
  else:
@@ -93,6 +96,9 @@ def load_lesson_content(lesson_name, selected_tutor, selected_language, progress
93
  chatbot_messages,
94
  gr.update(visible=False), # Hide welcome card
95
  gr.update(visible=True, value=status_message), # Show status
 
 
 
96
  )
97
  else:
98
  status_message = (
@@ -111,6 +117,9 @@ def load_lesson_content(lesson_name, selected_tutor, selected_language, progress
111
  chatbot_messages,
112
  gr.update(visible=False), # Hide welcome card
113
  gr.update(visible=True, value=status_message), # Show status
 
 
 
114
  )
115
  except Exception as e:
116
  status_message = (
@@ -129,6 +138,9 @@ def load_lesson_content(lesson_name, selected_tutor, selected_language, progress
129
  chatbot_messages,
130
  gr.update(visible=False), # Hide welcome card
131
  gr.update(visible=True, value=status_message), # Show status
 
 
 
132
  )
133
 
134
 
@@ -157,6 +169,9 @@ def reset_chat_interface():
157
  gr.update(visible=False), # Hide status
158
  [],
159
  gr.update(visible=True), # Show welcome card
 
 
 
160
  )
161
 
162
 
@@ -218,11 +233,13 @@ def create_new_lesson(topic, lesson_name, age_range, progress=gr.Progress()):
218
  return result + lesson_content_preview, "", lesson_dropdown_update
219
 
220
 
 
221
  def custom_respond(
222
- message, history, lesson_name, lesson_content, selected_tutor, difficulty_level
 
223
  ):
224
  """
225
- Custom respond function with educational system prompt.
226
 
227
  Args:
228
  message: User's message
@@ -231,32 +248,52 @@ def custom_respond(
231
  lesson_content: Content of the lesson
232
  selected_tutor: Name of the selected tutor
233
  difficulty_level: Difficulty level (beginner, intermediate, advanced)
 
 
234
 
235
  Yields:
236
- Response chunks from the LLM
237
  """
238
  if not lesson_name or not selected_tutor:
239
- yield "Please select a lesson and tutor first."
240
  return
241
 
242
  if not lesson_content:
243
  lesson_content = get_lesson_content(lesson_name, LESSON_CONTENT_MAX_LENGTH)
244
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  # Get tutor description
246
  tutor_description = get_tutor_description(selected_tutor)
247
  if not tutor_description:
248
  tutor_description = "a friendly and patient educational tutor"
249
 
250
- # Generate educational system prompt with enhanced pedagogy focused on problem-solving
 
 
 
251
  system_prompt = generate_tutor_system_prompt(
252
  tutor_name=selected_tutor,
253
  tutor_description=tutor_description,
254
  difficulty_level=difficulty_level,
255
- lesson_content=lesson_content
 
256
  )
257
 
258
  # Call the respond method with educational system prompt
259
  call_llm = LLMCall()
 
260
  for response in call_llm.respond(
261
  message,
262
  history,
@@ -264,5 +301,28 @@ def custom_respond(
264
  tutor_name=selected_tutor,
265
  difficulty_level=difficulty_level
266
  ):
267
- yield response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
 
9
  from learnbee.prompts import generate_tutor_system_prompt
10
 
11
 
12
+ from learnbee.session_state import SessionState
13
+ from learnbee.gamification import GamificationTracker
14
+
15
+
16
  def load_lesson_content(lesson_name, selected_tutor, selected_language, progress=gr.Progress()):
17
  """
18
  Load lesson content and extract key concepts.
 
77
  # Format the introduction as a friendly greeting from the tutor
78
  tutor_greeting = (
79
  f"Hello! 👋 I'm {selected_tutor}, and I'm so excited to learn with you today!\n\n"
80
+ f"{introduction}\n"
 
81
  )
82
  chatbot_messages = [{"role": "assistant", "content": tutor_greeting}]
83
  else:
 
96
  chatbot_messages,
97
  gr.update(visible=False), # Hide welcome card
98
  gr.update(visible=True, value=status_message), # Show status
99
+ SessionState(), # Reset session state
100
+ GamificationTracker(), # Reset gamification
101
+ gr.update(visible=True), # Show gamification display
102
  )
103
  else:
104
  status_message = (
 
117
  chatbot_messages,
118
  gr.update(visible=False), # Hide welcome card
119
  gr.update(visible=True, value=status_message), # Show status
120
+ SessionState(), # Reset session state
121
+ GamificationTracker(), # Reset gamification
122
+ gr.update(visible=True), # Show gamification display
123
  )
124
  except Exception as e:
125
  status_message = (
 
138
  chatbot_messages,
139
  gr.update(visible=False), # Hide welcome card
140
  gr.update(visible=True, value=status_message), # Show status
141
+ SessionState(), # Reset session state
142
+ GamificationTracker(), # Reset gamification
143
+ gr.update(visible=True), # Show gamification display
144
  )
145
 
146
 
 
169
  gr.update(visible=False), # Hide status
170
  [],
171
  gr.update(visible=True), # Show welcome card
172
+ SessionState(), # Reset session state
173
+ GamificationTracker(), # Reset gamification
174
+ gr.update(visible=False), # Hide gamification display
175
  )
176
 
177
 
 
233
  return result + lesson_content_preview, "", lesson_dropdown_update
234
 
235
 
236
+
237
  def custom_respond(
238
+ message, history, lesson_name, lesson_content, selected_tutor, difficulty_level,
239
+ session_state, gamification_tracker
240
  ):
241
  """
242
+ Custom respond function with educational system prompt, adaptive personalization, and gamification.
243
 
244
  Args:
245
  message: User's message
 
248
  lesson_content: Content of the lesson
249
  selected_tutor: Name of the selected tutor
250
  difficulty_level: Difficulty level (beginner, intermediate, advanced)
251
+ session_state: SessionState object for tracking interactions
252
+ gamification_tracker: GamificationTracker object for progress tracking
253
 
254
  Yields:
255
+ Tuple of (response, gamification_html)
256
  """
257
  if not lesson_name or not selected_tutor:
258
+ yield "Please select a lesson and tutor first.", ""
259
  return
260
 
261
  if not lesson_content:
262
  lesson_content = get_lesson_content(lesson_name, LESSON_CONTENT_MAX_LENGTH)
263
 
264
+ # Analyze the child's message for adaptive personalization
265
+ message_analysis = session_state.analyze_message(message)
266
+
267
+ # Award star for engagement (not on first message)
268
+ star_awarded = False
269
+ badge_message = ""
270
+ if session_state.total_messages > 1: # Skip first message
271
+ # Award star based on engagement
272
+ if message_analysis["engagement_level"] in ["medium", "high"]:
273
+ star_awarded, badge_message = gamification_tracker.award_star("Great engagement!")
274
+ elif message_analysis["has_question"]:
275
+ star_awarded, badge_message = gamification_tracker.award_star("Excellent question!")
276
+
277
  # Get tutor description
278
  tutor_description = get_tutor_description(selected_tutor)
279
  if not tutor_description:
280
  tutor_description = "a friendly and patient educational tutor"
281
 
282
+ # Get adaptive context from session state
283
+ adaptive_context = session_state.get_adaptive_context()
284
+
285
+ # Generate educational system prompt with adaptive personalization
286
  system_prompt = generate_tutor_system_prompt(
287
  tutor_name=selected_tutor,
288
  tutor_description=tutor_description,
289
  difficulty_level=difficulty_level,
290
+ lesson_content=lesson_content,
291
+ adaptive_context=adaptive_context
292
  )
293
 
294
  # Call the respond method with educational system prompt
295
  call_llm = LLMCall()
296
+ response_text = ""
297
  for response in call_llm.respond(
298
  message,
299
  history,
 
301
  tutor_name=selected_tutor,
302
  difficulty_level=difficulty_level
303
  ):
304
+ response_text = response
305
+ # Generate updated gamification HTML
306
+ gamification_html = gamification_tracker.get_progress_html()
307
+ yield response, gamification_html
308
+
309
+ # After response is complete, check if we should award additional stars
310
+ # (This is a simple heuristic - in production you might use LLM to evaluate)
311
+ if len(response_text) > 0:
312
+ # Award star if tutor gave positive feedback (heuristic: contains praise words)
313
+ praise_words = ["great", "excellent", "wonderful", "amazing", "perfect", "correct", "right", "good job"]
314
+ if any(word in response_text.lower() for word in praise_words):
315
+ # Record as correct answer
316
+ session_state.record_answer_quality(is_correct=True)
317
+ # Award star for correct answer
318
+ star_awarded, new_badge_message = gamification_tracker.award_star("Correct answer!")
319
+ if new_badge_message:
320
+ badge_message = new_badge_message
321
+ elif any(word in response_text.lower() for word in ["try again", "not quite", "almost", "let's think"]):
322
+ # Record as incorrect/partial answer
323
+ session_state.record_answer_quality(is_correct=False)
324
+
325
+ # Final yield with updated gamification display
326
+ final_html = gamification_tracker.get_progress_html()
327
+ yield response_text, final_html
328
 
src/learnbee/ui.py CHANGED
@@ -12,6 +12,8 @@ from learnbee.tutor_handlers import (
12
  create_new_lesson,
13
  custom_respond
14
  )
 
 
15
 
16
 
17
  def create_gradio_ui():
@@ -23,6 +25,10 @@ def create_gradio_ui():
23
  """
24
  lesson_name = gr.BrowserState("")
25
  selected_tutor = gr.BrowserState(get_tutor_names()[0] if TUTOR_NAMES else "")
 
 
 
 
26
 
27
  lesson_choices = json.loads(get_lesson_list())
28
 
@@ -169,7 +175,11 @@ def create_gradio_ui():
169
  # Spacer where the multilingual card used to be (moved to footer)
170
  with gr.Row():
171
  gr.HTML('<div style="height:0.5rem;"></div>')
 
 
 
172
 
 
173
  with gr.Column(scale=2):
174
  # Chat interface - defined before use
175
  chat_interface = gr.ChatInterface(
@@ -179,7 +189,10 @@ def create_gradio_ui():
179
  lesson_content,
180
  tutor_dropdown,
181
  difficulty_dropdown,
 
 
182
  ],
 
183
  type="messages",
184
  autofocus=False
185
  )
@@ -195,6 +208,9 @@ def create_gradio_ui():
195
  chat_interface.chatbot_value,
196
  welcome_card,
197
  status_markdown,
 
 
 
198
  ],
199
  )
200
 
@@ -210,6 +226,9 @@ def create_gradio_ui():
210
  status_markdown,
211
  chat_interface.chatbot_value,
212
  welcome_card,
 
 
 
213
  ],
214
  )
215
 
 
12
  create_new_lesson,
13
  custom_respond
14
  )
15
+ from learnbee.session_state import SessionState
16
+ from learnbee.gamification import GamificationTracker
17
 
18
 
19
  def create_gradio_ui():
 
25
  """
26
  lesson_name = gr.BrowserState("")
27
  selected_tutor = gr.BrowserState(get_tutor_names()[0] if TUTOR_NAMES else "")
28
+
29
+ # Session state for adaptive personalization
30
+ session_state_obj = gr.State(SessionState())
31
+ gamification_tracker_obj = gr.State(GamificationTracker())
32
 
33
  lesson_choices = json.loads(get_lesson_list())
34
 
 
175
  # Spacer where the multilingual card used to be (moved to footer)
176
  with gr.Row():
177
  gr.HTML('<div style="height:0.5rem;"></div>')
178
+
179
+ # Define gamification display here (will be positioned in footer visually)
180
+ gamification_display = gr.HTML(visible=False, value="")
181
 
182
+ with gr.Row():
183
  with gr.Column(scale=2):
184
  # Chat interface - defined before use
185
  chat_interface = gr.ChatInterface(
 
189
  lesson_content,
190
  tutor_dropdown,
191
  difficulty_dropdown,
192
+ session_state_obj,
193
+ gamification_tracker_obj,
194
  ],
195
+ additional_outputs=[gamification_display],
196
  type="messages",
197
  autofocus=False
198
  )
 
208
  chat_interface.chatbot_value,
209
  welcome_card,
210
  status_markdown,
211
+ session_state_obj,
212
+ gamification_tracker_obj,
213
+ gamification_display,
214
  ],
215
  )
216
 
 
226
  status_markdown,
227
  chat_interface.chatbot_value,
228
  welcome_card,
229
+ session_state_obj,
230
+ gamification_tracker_obj,
231
+ gamification_display,
232
  ],
233
  )
234
 
user_profile.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Explorer1",
3
+ "age": 6,
4
+ "interests": "Space, Animals, Magic",
5
+ "stars": 0,
6
+ "level": 1,
7
+ "badges": [
8
+ {
9
+ "id": "first_lesson",
10
+ "name": "First Step",
11
+ "description": "Complete your first lesson",
12
+ "icon": "\ud83c\udf31",
13
+ "unlocked": false
14
+ },
15
+ {
16
+ "id": "star_collector",
17
+ "name": "Star Collector",
18
+ "description": "Earn 10 stars",
19
+ "icon": "\u2b50",
20
+ "unlocked": false
21
+ },
22
+ {
23
+ "id": "curious_mind",
24
+ "name": "Curious Mind",
25
+ "description": "Ask 5 questions",
26
+ "icon": "\ud83d\udd0d",
27
+ "unlocked": false
28
+ },
29
+ {
30
+ "id": "super_learner",
31
+ "name": "Super Learner",
32
+ "description": "Complete 5 lessons",
33
+ "icon": "\ud83d\ude80",
34
+ "unlocked": false
35
+ },
36
+ {
37
+ "id": "creative_artist",
38
+ "name": "Creative Artist",
39
+ "description": "Generate an image",
40
+ "icon": "\ud83c\udfa8",
41
+ "unlocked": false
42
+ }
43
+ ],
44
+ "completed_lessons": []
45
+ }