cryogenic22 commited on
Commit
f9b0731
·
verified ·
1 Parent(s): 5bc5901

Update src/components/ai_tutor.py

Browse files
Files changed (1) hide show
  1. src/components/ai_tutor.py +204 -101
src/components/ai_tutor.py CHANGED
@@ -1,46 +1,153 @@
1
  import streamlit as st
2
- from src.services.ai_service import AITutorService
3
- from src.utils.session import get_tutor_context
4
- from datetime import datetime
5
  import platform
6
  import base64
7
- from openai import OpenAI
8
- import tempfile
9
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  class AITutor:
12
  def __init__(self):
13
- self.service = AITutorService()
14
- self.voice_enabled = False
15
- self.tts_mode = None # 'local' or 'openai'
16
- self.openai_enabled = False # Initialize the flag
17
- self.initialize_openai() # Initialize OpenAI first
18
- self.initialize_speech_components() # Then initialize speech components
 
 
 
 
 
 
 
 
 
 
19
 
20
  def initialize_openai(self):
21
- """Initialize OpenAI client"""
 
 
 
 
22
  try:
23
- # Get API key from environment variable
24
  api_key = os.getenv('OPENAI_API_KEY')
25
  if not api_key:
26
- st.warning("OpenAI API key not found in environment variables. Voice features will be limited.")
27
- self.openai_enabled = False
28
  return
29
 
30
  self.openai_client = OpenAI(api_key=api_key)
31
  self.openai_enabled = True
32
-
33
  except Exception as e:
34
- st.warning("Error initializing OpenAI client. Voice features will be limited.")
35
- self.openai_enabled = False
36
 
37
  def initialize_speech_components(self):
38
- """Initialize text-to-speech with fallback options"""
39
- if 'tts_engine' not in st.session_state:
40
- try:
41
- import pyttsx3
42
-
43
- # Try local TTS first
 
 
44
  system = platform.system()
45
  if system == "Windows":
46
  st.session_state.tts_engine = pyttsx3.init(driverName='sapi5')
@@ -58,88 +165,88 @@ class AITutor:
58
  female_voice = next((voice for voice in voices if 'female' in voice.name.lower()), voices[0])
59
  st.session_state.tts_engine.setProperty('voice', female_voice.id)
60
  except Exception:
61
- pass # Use default voice if female voice setting fails
62
 
63
  self.voice_enabled = True
64
  self.tts_mode = 'local'
65
- except Exception as e:
66
- st.info("Local text-to-speech unavailable. Falling back to OpenAI TTS.")
67
- st.session_state.tts_engine = None
68
- if self.openai_enabled:
69
- self.voice_enabled = True
70
- self.tts_mode = 'openai'
71
- else:
72
- self.voice_enabled = False
73
- self.tts_mode = None
 
74
 
75
  def speak_with_openai(self, text: str):
76
- """Generate speech using OpenAI's TTS"""
 
 
 
 
77
  try:
78
- # Generate speech using OpenAI
79
  response = self.openai_client.audio.speech.create(
80
  model="tts-1",
81
- voice="alloy", # or "nova", "shimmer", "echo", "fable"
82
  input=text
83
  )
84
 
85
- # Create a temporary file to store the audio
86
  with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as tmp_file:
87
  response.stream_to_file(tmp_file.name)
88
 
89
- # Create audio player in Streamlit
90
  audio_file = open(tmp_file.name, 'rb')
91
  audio_bytes = audio_file.read()
92
  st.audio(audio_bytes, format='audio/mp3')
93
 
94
- # Clean up
95
  audio_file.close()
96
  os.unlink(tmp_file.name)
97
 
98
  except Exception as e:
99
- st.error(f"Error during OpenAI speech synthesis: {e}")
100
 
101
  def speak(self, text: str):
102
- """Make the AI tutor speak using available TTS method"""
103
  if not self.voice_enabled:
 
104
  return
105
 
106
- if self.tts_mode == 'local':
107
- try:
108
- import threading
109
  def speak_text():
110
  st.session_state.tts_engine.say(text)
111
  st.session_state.tts_engine.runAndWait()
112
 
113
  thread = threading.Thread(target=speak_text)
114
  thread.start()
115
- except Exception as e:
116
- st.error(f"Error during local speech synthesis: {e}")
117
- if self.openai_enabled:
118
- st.info("Falling back to OpenAI TTS...")
119
- self.speak_with_openai(text)
120
 
121
- elif self.tts_mode == 'openai':
122
- self.speak_with_openai(text)
123
 
124
  def display_chat_interface(self):
125
- """Display the enhanced chat interface with avatar"""
 
 
 
 
 
126
  st.header("AI Tutor")
127
 
128
- # Voice controls with TTS mode indicator
129
  col1, col2 = st.columns([3, 2])
130
  with col1:
131
- if self.voice_enabled:
132
- voice_active = st.toggle("Enable Voice", value=False, key="voice_active")
133
- else:
134
- voice_active = False
135
  with col2:
136
  if self.tts_mode:
137
  st.info(f"Using {self.tts_mode.upper()} TTS")
138
 
139
- # Display avatar
140
  self.service.display_avatar(state='neutral')
141
 
142
- # Topic selection
143
  topics = [None, 'Physics', 'Mathematics', 'Computer Science', 'Artificial Intelligence']
144
  selected_topic = st.selectbox(
145
  "Select Topic",
@@ -148,17 +255,17 @@ class AITutor:
148
  key="topic_selector"
149
  )
150
 
151
- context = get_tutor_context()
152
- if selected_topic != context['current_topic']:
153
- context['current_topic'] = selected_topic
154
-
155
- # Display chat container
156
  chat_container = st.container()
157
  with chat_container:
158
- # Display chat history with avatar states
159
- for message in context['chat_history']:
160
  with st.chat_message(message["role"]):
161
- st.write(message["content"])
 
162
  if message["role"] == "assistant":
163
  self.service.display_avatar(state='happy')
164
  if voice_active and message.get('speak', True):
@@ -167,25 +274,38 @@ class AITutor:
167
 
168
  # Chat input
169
  if prompt := st.chat_input("Ask your question..."):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  self.handle_user_input(prompt)
171
 
172
- def handle_user_input(self, user_input: str):
173
  """Process user input and generate response"""
174
  # Show thinking avatar
175
  self.service.display_avatar(state='thinking')
176
 
177
- # Add user message
178
- context = get_tutor_context()
179
- context['chat_history'].append({
180
  "role": "user",
181
  "content": user_input
182
  })
183
 
184
  # Generate and display AI response
185
- response = self.service.generate_response(user_input)
186
 
187
  # Add AI response
188
- context['chat_history'].append({
189
  "role": "assistant",
190
  "content": response,
191
  "speak": True
@@ -193,31 +313,14 @@ class AITutor:
193
 
194
  # Show happy avatar and rerun
195
  self.service.display_avatar(state='happy')
196
- st.rerun()
197
 
198
- def display_learning_metrics(self):
199
- """Display learning progress and engagement metrics"""
200
- with st.sidebar:
201
- st.subheader("Learning Metrics")
202
-
203
- context = get_tutor_context()
204
- # Engagement score
205
- metrics = context['engagement_metrics']
206
- if metrics:
207
- avg_sentiment = sum(m['sentiment_score'] for m in metrics) / len(metrics)
208
- st.metric(
209
- "Engagement Score",
210
- f"{avg_sentiment:.2f}",
211
- delta="0.1" if avg_sentiment > 0.5 else "-0.1"
212
- )
213
-
214
- # Interaction stats
215
- if context['chat_history']:
216
- st.metric(
217
- "Questions Asked",
218
- len([m for m in context['chat_history'] if m['role'] == 'user'])
219
- )
220
-
221
- # Topic focus
222
- if context['current_topic']:
223
- st.info(f"Current focus: {context['current_topic']}")
 
1
  import streamlit as st
 
 
 
2
  import platform
3
  import base64
 
 
4
  import os
5
+ import tempfile
6
+ import threading
7
+ from datetime import datetime
8
+
9
+ # Optional third-party imports with error handling
10
+ try:
11
+ import pyttsx3
12
+ PYTTSX3_AVAILABLE = True
13
+ except ImportError:
14
+ PYTTSX3_AVAILABLE = False
15
+
16
+ try:
17
+ from openai import OpenAI
18
+ OPENAI_AVAILABLE = True
19
+ except ImportError:
20
+ OPENAI_AVAILABLE = False
21
+
22
+ class AITutorService:
23
+ def __init__(self):
24
+ """Initialize the AI Tutor Service"""
25
+ self.topics = {
26
+ 'Physics': self._generate_physics_response,
27
+ 'Mathematics': self._generate_math_response,
28
+ 'Computer Science': self._generate_cs_response,
29
+ 'Artificial Intelligence': self._generate_ai_response
30
+ }
31
+
32
+ def display_avatar(self, state='neutral'):
33
+ """
34
+ Display avatar with fallback text descriptions
35
+
36
+ Args:
37
+ state (str): Avatar state (neutral, happy, thinking)
38
+ """
39
+ try:
40
+ # Placeholder for actual avatar image rendering
41
+ avatar_paths = {
42
+ 'neutral': 'path/to/neutral_avatar.png',
43
+ 'happy': 'path/to/happy_avatar.png',
44
+ 'thinking': 'path/to/thinking_avatar.png'
45
+ }
46
+
47
+ # If image path exists and is valid, render image
48
+ if state in avatar_paths:
49
+ st.image(avatar_paths[state], width=200)
50
+ else:
51
+ # Fallback text descriptions
52
+ avatar_descriptions = {
53
+ 'neutral': 'AI Tutor is listening',
54
+ 'happy': 'AI Tutor looks excited',
55
+ 'thinking': 'AI Tutor is pondering'
56
+ }
57
+ st.write(avatar_descriptions.get(state, 'AI Tutor'))
58
+
59
+ except Exception as e:
60
+ st.warning(f"Could not render avatar: {e}")
61
+ st.write("AI Tutor is here to help!")
62
+
63
+ def generate_response(self, user_input, topic=None):
64
+ """
65
+ Generate a response based on user input and selected topic
66
+
67
+ Args:
68
+ user_input (str): User's question or message
69
+ topic (str, optional): Selected learning topic
70
+
71
+ Returns:
72
+ str: Generated response
73
+ """
74
+ try:
75
+ # If no specific topic, use a general response generator
76
+ if not topic or topic not in self.topics:
77
+ return self._generate_general_response(user_input)
78
+
79
+ # Use topic-specific response generator
80
+ return self.topics[topic](user_input)
81
+
82
+ except Exception as e:
83
+ return f"I apologize, but I'm having trouble generating a response. Error: {e}"
84
+
85
+ def _generate_general_response(self, user_input):
86
+ """General response generation method"""
87
+ return f"Thank you for your message: '{user_input}'. How can I help you learn today?"
88
+
89
+ def _generate_physics_response(self, user_input):
90
+ """Physics-specific response generator"""
91
+ return "Let's explore physics together! What specific physics concept interests you?"
92
+
93
+ def _generate_math_response(self, user_input):
94
+ """Mathematics-specific response generator"""
95
+ return "Mathematics is fascinating! What mathematical topic would you like to discuss?"
96
+
97
+ def _generate_cs_response(self, user_input):
98
+ """Computer Science-specific response generator"""
99
+ return "Computer Science is an exciting field! What programming or tech area would you like to learn about?"
100
+
101
+ def _generate_ai_response(self, user_input):
102
+ """AI-specific response generator"""
103
+ return "Artificial Intelligence is a rapidly evolving field. What aspect of AI intrigues you?"
104
 
105
  class AITutor:
106
  def __init__(self):
107
+ """Initialize AI Tutor with fallback mechanisms"""
108
+ self.fallback_mode = False
109
+
110
+ try:
111
+ self.service = AITutorService()
112
+ self.voice_enabled = False
113
+ self.tts_mode = None
114
+ self.openai_enabled = False
115
+
116
+ # Initialize components
117
+ self.initialize_openai()
118
+ self.initialize_speech_components()
119
+
120
+ except Exception as e:
121
+ st.error(f"Could not initialize AI Tutor: {e}")
122
+ self.fallback_mode = True
123
 
124
  def initialize_openai(self):
125
+ """Initialize OpenAI client with error handling"""
126
+ if not OPENAI_AVAILABLE:
127
+ st.warning("OpenAI library not installed. Voice features will be limited.")
128
+ return
129
+
130
  try:
 
131
  api_key = os.getenv('OPENAI_API_KEY')
132
  if not api_key:
133
+ st.warning("OpenAI API key not found. Voice features will be limited.")
 
134
  return
135
 
136
  self.openai_client = OpenAI(api_key=api_key)
137
  self.openai_enabled = True
138
+
139
  except Exception as e:
140
+ st.warning(f"Error initializing OpenAI client: {e}")
 
141
 
142
  def initialize_speech_components(self):
143
+ """Initialize text-to-speech with comprehensive fallback options"""
144
+ if not PYTTSX3_AVAILABLE and not self.openai_enabled:
145
+ st.info("No TTS engines available. Voice output disabled.")
146
+ return
147
+
148
+ try:
149
+ if PYTTSX3_AVAILABLE:
150
+ # Local TTS initialization
151
  system = platform.system()
152
  if system == "Windows":
153
  st.session_state.tts_engine = pyttsx3.init(driverName='sapi5')
 
165
  female_voice = next((voice for voice in voices if 'female' in voice.name.lower()), voices[0])
166
  st.session_state.tts_engine.setProperty('voice', female_voice.id)
167
  except Exception:
168
+ pass # Use default voice
169
 
170
  self.voice_enabled = True
171
  self.tts_mode = 'local'
172
+
173
+ elif self.openai_enabled:
174
+ # Fallback to OpenAI TTS
175
+ self.voice_enabled = True
176
+ self.tts_mode = 'openai'
177
+
178
+ except Exception as e:
179
+ st.info(f"Speech initialization error: {e}")
180
+ self.voice_enabled = False
181
+ self.tts_mode = None
182
 
183
  def speak_with_openai(self, text: str):
184
+ """Generate speech using OpenAI's TTS with error handling"""
185
+ if not self.openai_enabled:
186
+ st.warning("OpenAI TTS not available.")
187
+ return
188
+
189
  try:
 
190
  response = self.openai_client.audio.speech.create(
191
  model="tts-1",
192
+ voice="alloy",
193
  input=text
194
  )
195
 
 
196
  with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as tmp_file:
197
  response.stream_to_file(tmp_file.name)
198
 
 
199
  audio_file = open(tmp_file.name, 'rb')
200
  audio_bytes = audio_file.read()
201
  st.audio(audio_bytes, format='audio/mp3')
202
 
 
203
  audio_file.close()
204
  os.unlink(tmp_file.name)
205
 
206
  except Exception as e:
207
+ st.error(f"OpenAI speech synthesis error: {e}")
208
 
209
  def speak(self, text: str):
210
+ """Speak text using available TTS method"""
211
  if not self.voice_enabled:
212
+ st.info("Voice output is currently disabled.")
213
  return
214
 
215
+ try:
216
+ if self.tts_mode == 'local':
 
217
  def speak_text():
218
  st.session_state.tts_engine.say(text)
219
  st.session_state.tts_engine.runAndWait()
220
 
221
  thread = threading.Thread(target=speak_text)
222
  thread.start()
223
+
224
+ elif self.tts_mode == 'openai':
225
+ self.speak_with_openai(text)
 
 
226
 
227
+ except Exception as e:
228
+ st.error(f"Speech error: {e}")
229
 
230
  def display_chat_interface(self):
231
+ """Display enhanced chat interface with robust error handling"""
232
+ if self.fallback_mode:
233
+ st.warning("Running in text-only mode due to initialization errors.")
234
+ self._text_only_chat_interface()
235
+ return
236
+
237
  st.header("AI Tutor")
238
 
239
+ # Voice and TTS configuration
240
  col1, col2 = st.columns([3, 2])
241
  with col1:
242
+ voice_active = st.toggle("Enable Voice", value=False, key="voice_active") if self.voice_enabled else False
 
 
 
243
  with col2:
244
  if self.tts_mode:
245
  st.info(f"Using {self.tts_mode.upper()} TTS")
246
 
247
+ # Avatar and topic selection
248
  self.service.display_avatar(state='neutral')
249
 
 
250
  topics = [None, 'Physics', 'Mathematics', 'Computer Science', 'Artificial Intelligence']
251
  selected_topic = st.selectbox(
252
  "Select Topic",
 
255
  key="topic_selector"
256
  )
257
 
258
+ # Retrieve or initialize conversation context
259
+ if 'chat_history' not in st.session_state:
260
+ st.session_state.chat_history = []
261
+
 
262
  chat_container = st.container()
263
  with chat_container:
264
+ # Display chat history
265
+ for message in st.session_state.chat_history:
266
  with st.chat_message(message["role"]):
267
+ st.markdown(f"**{message['role'].capitalize()}:** {message['content']}")
268
+
269
  if message["role"] == "assistant":
270
  self.service.display_avatar(state='happy')
271
  if voice_active and message.get('speak', True):
 
274
 
275
  # Chat input
276
  if prompt := st.chat_input("Ask your question..."):
277
+ self.handle_user_input(prompt, selected_topic)
278
+
279
+ def _text_only_chat_interface(self):
280
+ """Fallback text-only chat interface"""
281
+ st.header("AI Tutor (Text Mode)")
282
+ st.write("Some features are currently unavailable.")
283
+
284
+ if 'chat_history' not in st.session_state:
285
+ st.session_state.chat_history = []
286
+
287
+ for message in st.session_state.chat_history:
288
+ st.write(f"{message['role'].capitalize()}: {message['content']}")
289
+
290
+ if prompt := st.text_input("Enter your message:"):
291
  self.handle_user_input(prompt)
292
 
293
+ def handle_user_input(self, user_input: str, topic=None):
294
  """Process user input and generate response"""
295
  # Show thinking avatar
296
  self.service.display_avatar(state='thinking')
297
 
298
+ # Manage chat history
299
+ st.session_state.chat_history.append({
 
300
  "role": "user",
301
  "content": user_input
302
  })
303
 
304
  # Generate and display AI response
305
+ response = self.service.generate_response(user_input, topic)
306
 
307
  # Add AI response
308
+ st.session_state.chat_history.append({
309
  "role": "assistant",
310
  "content": response,
311
  "speak": True
 
313
 
314
  # Show happy avatar and rerun
315
  self.service.display_avatar(state='happy')
316
+ st.experimental_rerun()
317
 
318
+ def main():
319
+ """Main Streamlit application entry point"""
320
+ st.set_page_config(page_title="AI Tutor", page_icon="🤖")
321
+
322
+ tutor = AITutor()
323
+ tutor.display_chat_interface()
324
+
325
+ if __name__ == "__main__":
326
+ main()