cryogenic22 commited on
Commit
365677f
·
verified ·
1 Parent(s): 0de457f

Update src/components/ai_tutor.py

Browse files
Files changed (1) hide show
  1. src/components/ai_tutor.py +229 -73
src/components/ai_tutor.py CHANGED
@@ -1,80 +1,236 @@
1
  import streamlit as st
2
- from src.services.ai_service import AITutorService
3
- from src.utils.session import get_tutor_context
 
 
4
 
5
- class AITutor:
6
  def __init__(self):
7
- self.service = AITutorService()
 
 
8
 
9
- def display_chat_interface(self):
10
- """Display the enhanced chat interface with avatar"""
11
- st.header("AI Tutor")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- # Voice controls with TTS mode indicator
14
- col1, col2 = st.columns([3, 2])
15
- with col1:
16
- if self.service.voice_enabled:
17
- voice_active = st.toggle("Enable Voice", value=False, key="voice_active")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  else:
19
- voice_active = False
20
- with col2:
21
- if self.service.tts_mode:
22
- st.info(f"Using {self.service.tts_mode.upper()} TTS")
23
-
24
- # Display avatar
25
- self.service.display_avatar(state='neutral')
26
-
27
- # Topic selection
28
- topics = [None, 'Physics', 'Mathematics', 'Computer Science', 'Artificial Intelligence']
29
- selected_topic = st.selectbox(
30
- "Select Topic",
31
- topics,
32
- format_func=lambda x: 'All Topics' if x is None else x,
33
- key="topic_selector"
34
- )
35
-
36
- context = get_tutor_context()
37
- if selected_topic != context['current_topic']:
38
- context['current_topic'] = selected_topic
39
-
40
- # Display chat container
41
- chat_container = st.container()
42
- with chat_container:
43
- # Display chat history with avatar states
44
- for message in context['chat_history']:
45
- with st.chat_message(message["role"]):
46
- st.write(message["content"])
47
- if message["role"] == "assistant":
48
- self.service.display_avatar(state='happy')
49
- if voice_active and message.get('speak', True):
50
- self.service.speak(message["content"])
51
- message['speak'] = False
52
-
53
- # Chat input
54
- if prompt := st.text_input("Ask your question...", key="chat_input"):
55
- self.handle_user_input(prompt)
56
-
57
- def handle_user_input(self, user_input: str):
58
- """Process user input and generate response"""
59
- # Show thinking avatar
60
- self.service.display_avatar(state='thinking')
61
-
62
- # Add user message
63
- context = get_tutor_context()
64
- context['chat_history'].append({
65
- "role": "user",
66
- "content": user_input
67
- })
68
-
69
- # Generate and display AI response
70
- response = self.service.generate_response(user_input)
71
-
72
- # Add AI response
73
- context['chat_history'].append({
74
- "role": "assistant",
75
- "content": response,
76
- "speak": True
77
  })
78
-
79
- # Show happy avatar
80
- self.service.display_avatar(state='happy')
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ from datetime import datetime
3
+ import anthropic
4
+ import os
5
+ from typing import Dict
6
 
7
+ class AITutorService:
8
  def __init__(self):
9
+ self._client = None
10
+ self.initialize_client()
11
+ self._load_avatar_assets()
12
 
13
+ def initialize_client(self):
14
+ """Initialize Anthropic client with better error handling"""
15
+ try:
16
+ # First try getting API key from Streamlit secrets
17
+ api_key = None
18
+ if hasattr(st, 'secrets') and 'ANTHROPIC_API_KEY' in st.secrets:
19
+ api_key = st.secrets['ANTHROPIC_API_KEY']
20
+
21
+ # If not in secrets, try environment variable
22
+ if not api_key:
23
+ api_key = os.getenv('ANTHROPIC_API_KEY')
24
+
25
+ if not api_key:
26
+ st.error("Anthropic API key not found. Please set ANTHROPIC_API_KEY in .streamlit/secrets.toml or environment variables.")
27
+ st.info("For testing, you can set this up locally by creating a .streamlit/secrets.toml file with:\nANTHROPIC_API_KEY='your-key-here'")
28
+ self.voice_enabled = False
29
+ self.tts_mode = None
30
+ return
31
 
32
+ self._client = anthropic.Anthropic(api_key=api_key)
33
+ self.voice_enabled = True
34
+ self.tts_mode = 'openai'
35
+
36
+ except Exception as e:
37
+ st.error(f"Failed to initialize Anthropic client: {str(e)}")
38
+ self._client = None
39
+ self.voice_enabled = False
40
+ self.tts_mode = None
41
+
42
+ def _load_avatar_assets(self):
43
+ """Initialize avatar states and assets"""
44
+ self.avatar_states = {
45
+ 'neutral': """
46
+ <svg width="100" height="100" viewBox="0 0 100 100">
47
+ <circle cx="50" cy="50" r="45" fill="#4A90E2"/>
48
+ <circle cx="35" cy="40" r="5" fill="white"/>
49
+ <circle cx="65" cy="40" r="5" fill="white"/>
50
+ <path d="M 30 65 Q 50 65 70 65" stroke="white" fill="none" stroke-width="3"/>
51
+ </svg>
52
+ """,
53
+ 'thinking': """
54
+ <svg width="100" height="100" viewBox="0 0 100 100">
55
+ <circle cx="50" cy="50" r="45" fill="#4A90E2"/>
56
+ <circle cx="35" cy="40" r="5" fill="white"/>
57
+ <circle cx="65" cy="40" r="5" fill="white"/>
58
+ <path d="M 30 60 Q 50 70 70 60" stroke="white" fill="none" stroke-width="3"/>
59
+ </svg>
60
+ """,
61
+ 'happy': """
62
+ <svg width="100" height="100" viewBox="0 0 100 100">
63
+ <circle cx="50" cy="50" r="45" fill="#4A90E2"/>
64
+ <circle cx="35" cy="40" r="5" fill="white"/>
65
+ <circle cx="65" cy="40" r="5" fill="white"/>
66
+ <path d="M 30 60 Q 50 80 70 60" stroke="white" fill="none" stroke-width="3"/>
67
+ </svg>
68
+ """
69
+ }
70
+
71
+ @property
72
+ def client(self):
73
+ """Property to safely access the Anthropic client"""
74
+ if self._client is None:
75
+ raise ValueError("Anthropic client not properly initialized")
76
+ return self._client
77
+
78
+ import base64
79
+ from io import BytesIO
80
+ from typing import Dict
81
+
82
+ class AITutorService:
83
+ def __init__(self):
84
+ self._client = None
85
+ self.initialize_client()
86
+ self._load_avatar_assets()
87
+
88
+ def _load_avatar_assets(self):
89
+ """Initialize avatar states and assets"""
90
+ self.avatar_states: Dict[str, bytes] = {
91
+ 'neutral': self._svg_to_bytes("""
92
+ <svg width="100" height="100" viewBox="0 0 100 100">
93
+ <circle cx="50" cy="50" r="45" fill="#4A90E2"/>
94
+ <circle cx="35" cy="40" r="5" fill="white"/>
95
+ <circle cx="65" cy="40" r="5" fill="white"/>
96
+ <path d="M 30 65 Q 50 65 70 65" stroke="white" fill="none" stroke-width="3"/>
97
+ </svg>
98
+ """),
99
+ 'thinking': self._svg_to_bytes("""
100
+ <svg width="100" height="100" viewBox="0 0 100 100">
101
+ <circle cx="50" cy="50" r="45" fill="#4A90E2"/>
102
+ <circle cx="35" cy="40" r="5" fill="white"/>
103
+ <circle cx="65" cy="40" r="5" fill="white"/>
104
+ <path d="M 30 60 Q 50 70 70 60" stroke="white" fill="none" stroke-width="3"/>
105
+ </svg>
106
+ """),
107
+ 'happy': self._svg_to_bytes("""
108
+ <svg width="100" height="100" viewBox="0 0 100 100">
109
+ <circle cx="50" cy="50" r="45" fill="#4A90E2"/>
110
+ <circle cx="35" cy="40" r="5" fill="white"/>
111
+ <circle cx="65" cy="40" r="5" fill="white"/>
112
+ <path d="M 30 60 Q 50 80 70 60" stroke="white" fill="none" stroke-width="3"/>
113
+ </svg>
114
+ """)
115
+ }
116
+
117
+ def _svg_to_bytes(self, svg_str: str) -> bytes:
118
+ """Convert SVG string to bytes"""
119
+ return base64.b64encode(svg_str.encode()).decode().encode()
120
+
121
+ def display_avatar(self, state: str = 'neutral'):
122
+ """Display the AI tutor avatar"""
123
+ avatar_bytes = self.avatar_states.get(state, self.avatar_states['neutral'])
124
+ st.image(avatar_bytes, width=100)
125
+
126
+ def generate_response(self, user_input: str) -> str:
127
+ """Generate response using Claude with better error handling"""
128
+ if not self._client:
129
+ return "I apologize, but I'm not properly connected right now. Please check the API configuration."
130
+
131
+ try:
132
+ # Get session context
133
+ context = get_tutor_context()
134
+ current_topic = context.get('current_topic')
135
+
136
+ # Build system prompt
137
+ system_prompt = """You are an expert tutor helping students learn.
138
+ Give clear, step-by-step explanations with examples.
139
+ Focus on the current topic if specified."""
140
+
141
+ if current_topic and current_topic != "All Topics":
142
+ system_prompt += f"\nYou are currently tutoring about {current_topic}."
143
+
144
+ # Generate response
145
+ response = self.client.messages.create(
146
+ model="claude-3-opus-20240229",
147
+ temperature=0.7,
148
+ max_tokens=1024,
149
+ system=system_prompt,
150
+ messages=[{
151
+ "role": "user",
152
+ "content": user_input
153
+ }]
154
+ )
155
+
156
+ # Get response text
157
+ response_text = response.content[0].text
158
+
159
+ # Update metrics
160
+ self.update_engagement_metrics(user_input, response_text)
161
+
162
+ return response_text
163
+
164
+ except anthropic.APIError as e:
165
+ st.error(f"API Error: {str(e)}")
166
+ return "I encountered an API error. Please try again in a moment."
167
+ except anthropic.APIConnectionError as e:
168
+ st.error(f"Connection Error: {str(e)}")
169
+ return "I'm having trouble connecting to my API. Please check your internet connection."
170
+ except Exception as e:
171
+ st.error(f"Unexpected error: {str(e)}")
172
+ return "I encountered an unexpected error. Please try again."
173
+
174
+ def speak(self, text: str):
175
+ """Make the AI tutor speak using available TTS method"""
176
+ if not self.voice_enabled:
177
+ return
178
+
179
+ if self.tts_mode == 'openai':
180
+ if self.openai_enabled:
181
+ self.speak_with_openai(text)
182
  else:
183
+ st.warning("OpenAI TTS is not available. Voice output is disabled.")
184
+ self.tts_mode = None
185
+ self.voice_enabled = False
186
+ else:
187
+ st.warning("Local TTS is not implemented yet.")
188
+
189
+ def speak_with_openai(self, text: str):
190
+ """Generate speech using OpenAI's TTS"""
191
+ try:
192
+ # Generate speech using OpenAI
193
+ response = self.client.audio.speech.create(
194
+ model="tts-1",
195
+ voice="alloy", # or "nova", "shimmer", "echo", "fable"
196
+ input=text
197
+ )
198
+
199
+ # Create a temporary file to store the audio
200
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as tmp_file:
201
+ response.stream_to_file(tmp_file.name)
202
+
203
+ # Create audio player in Streamlit
204
+ audio_file = open(tmp_file.name, 'rb')
205
+ audio_bytes = audio_file.read()
206
+ st.audio(audio_bytes, format='audio/mp3')
207
+
208
+ # Clean up
209
+ audio_file.close()
210
+ os.unlink(tmp_file.name)
211
+
212
+ except Exception as e:
213
+ st.error(f"Error during OpenAI speech synthesis: {e}")
214
+
215
+ def update_engagement_metrics(self, user_input: str, response: str):
216
+ """Update engagement metrics"""
217
+ if 'tutor_context' not in st.session_state:
218
+ st.session_state.tutor_context = {
219
+ 'engagement_metrics': []
220
+ }
221
+
222
+ metrics = st.session_state.tutor_context['engagement_metrics']
223
+ metrics.append({
224
+ 'timestamp': datetime.now().isoformat(),
225
+ 'sentiment_score': min(1.0, len(user_input) / 100.0),
226
+ 'interaction_length': len(user_input)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  })
228
+
229
+ def get_tutor_context() -> Dict:
230
+ """Get or initialize tutor context"""
231
+ if 'tutor_context' not in st.session_state:
232
+ st.session_state.tutor_context = {
233
+ 'current_topic': None,
234
+ 'chat_history': []
235
+ }
236
+ return st.session_state.tutor_context