Peter Michael Gits commited on
Commit
32e756c
Β·
0 Parent(s):

feat: Initial ChatCal Voice-Enabled Assistant v0.1.0

Browse files

- Migrated ChatCal to Hugging Face Gradio platform
- Added voice interaction capabilities with STT/TTS integration
- Implemented session management for Gradio environment
- Created LLM fallback chain: Groq β†’ Anthropic β†’ Mock
- Built responsive voice-enabled chat interface
- Added demo mode for development and testing
- Security: Using HF environment variables for secrets

πŸŽ€πŸ“… Generated with Claude Code

.env.example ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ChatCal Voice Assistant Environment Configuration
2
+
3
+ # Required: LLM API Keys (Primary: Groq, Fallback: Anthropic)
4
+ GROQ_API_KEY=your_groq_api_key_here
5
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
6
+
7
+ # Required: Google Calendar Integration
8
+ GOOGLE_CLIENT_ID=your_google_client_id.googleusercontent.com
9
+ GOOGLE_CLIENT_SECRET=your_google_client_secret
10
+ GOOGLE_CALENDAR_ID=pgits.job@gmail.com
11
+
12
+ # Required: Contact Information
13
+ MY_PHONE_NUMBER=+1-555-123-4567
14
+ MY_EMAIL_ADDRESS=pgits.job@gmail.com
15
+
16
+ # Required: Security
17
+ SECRET_KEY=your_secret_key_here
18
+
19
+ # Email Configuration (for appointment notifications)
20
+ SMTP_USERNAME=your_email@gmail.com
21
+ SMTP_PASSWORD=your_gmail_app_password
22
+ SMTP_SERVER=smtp.gmail.com
23
+ SMTP_PORT=587
24
+ EMAIL_FROM_NAME=ChatCal Voice Assistant
25
+
26
+ # Audio Services (Update with your actual Hugging Face space URLs)
27
+ STT_SERVICE_URL=https://huggingface.co/spaces/YOUR_USERNAME/stt-gpu-service
28
+ TTS_SERVICE_URL=https://huggingface.co/spaces/YOUR_USERNAME/tts-gpu-service
29
+
30
+ # Voice Settings
31
+ DEFAULT_VOICE=v2/en_speaker_6
32
+ ENABLE_VOICE_RESPONSES=true
33
+
34
+ # Timezone and Working Hours
35
+ DEFAULT_TIMEZONE=America/New_York
36
+ WEEKDAY_START_TIME=07:30
37
+ WEEKDAY_END_TIME=18:30
38
+ WEEKEND_START_TIME=10:30
39
+ WEEKEND_END_TIME=16:30
40
+ WORKING_HOURS_TIMEZONE=America/New_York
41
+
42
+ # Session Configuration
43
+ MAX_CONVERSATION_HISTORY=20
44
+ SESSION_TIMEOUT_MINUTES=30
45
+
46
+ # Development Settings
47
+ TESTING_MODE=true
48
+ APP_ENV=production
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment files with secrets
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+
6
+ # Swap files
7
+ *.swp
8
+ *.swo
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+ *.so
15
+ .Python
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ downloads/
20
+ eggs/
21
+ .eggs/
22
+ lib/
23
+ lib64/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.sublime-*
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
40
+
41
+ # Logs
42
+ *.log
README.md ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ChatCal Voice-Enabled AI Assistant
3
+ emoji: πŸŽ€πŸ“…
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ suggested_hardware: t4-small
12
+ ---
13
+
14
+ # πŸŽ€πŸ“… ChatCal Voice-Enabled AI Assistant
15
+
16
+ An intelligent AI scheduling assistant with **voice interaction capabilities** powered by WebRTC, Whisper STT, and Bark TTS. Book Google Calendar appointments through natural conversation - now with voice input and audio responses!
17
+
18
+ ## 🎯 Features
19
+
20
+ ### πŸ—£οΈ Voice Interaction
21
+ - 🎀 **Real-time Speech-to-Text**: WebRTC audio capture with Whisper transcription
22
+ - πŸ”Š **Text-to-Speech Responses**: AI responses with natural voice synthesis
23
+ - 🎭 **Multiple Voice Options**: Choose from different voice personalities
24
+ - ⚑ **Real-time Processing**: Live transcription as you speak
25
+
26
+ ### πŸ“… Smart Calendar Integration
27
+ - πŸ€– **AI-Powered Booking**: Natural language appointment scheduling
28
+ - πŸ“… **Google Calendar Sync**: Seamless integration with your calendar
29
+ - πŸ” **Conflict Detection**: Smart availability checking
30
+ - πŸŽ₯ **Google Meet Integration**: Automatic video conference setup
31
+ - πŸ“§ **Email Notifications**: Booking confirmations and cancellations
32
+
33
+ ### 🧠 Intelligent Conversation
34
+ - πŸ’­ **Conversation Memory**: Persistent context across interactions
35
+ - 🎯 **Smart Extraction**: Automatically extract names, emails, times
36
+ - 🌍 **Timezone Awareness**: Global scheduling support
37
+ - ⏰ **Flexible Time Parsing**: "tomorrow at 2pm", "next Tuesday", etc.
38
+
39
+ ## πŸ—οΈ Architecture
40
+
41
+ - **Frontend**: Gradio with WebRTC audio capture
42
+ - **AI**: Groq Llama-3.1 with Anthropic Claude fallback
43
+ - **STT**: Whisper via external service integration
44
+ - **TTS**: Bark text-to-speech synthesis
45
+ - **Calendar**: Google Calendar API with OAuth2
46
+ - **Storage**: Google Cloud Secret Manager for persistent auth
47
+
48
+ ## πŸš€ Usage
49
+
50
+ ### Voice Interaction
51
+ 1. Click the microphone button to start recording
52
+ 2. Speak naturally: "Hi, I'm John. Book a 30-minute meeting tomorrow at 2pm"
53
+ 3. Watch real-time transcription appear in the text box
54
+ 4. AI responds with voice confirmation of your booking
55
+
56
+ ### Text Interaction
57
+ - Type messages as normal - voice and text work together
58
+ - Edit voice transcriptions before sending
59
+ - Use quick action buttons for common requests
60
+
61
+ ### Example Conversations
62
+
63
+ **Voice**: "Book a Google Meet with Peter next Tuesday at 10 AM for 45 minutes"
64
+ **AI Audio Response**: "Perfect! I've scheduled your 45-minute Google Meet with Peter for next Tuesday at 10:00 AM..."
65
+
66
+ ## πŸ› οΈ Development
67
+
68
+ This space integrates:
69
+ - **ChatCal Core**: Calendar booking logic and Google integration
70
+ - **STT Service**: External Whisper service for speech recognition
71
+ - **TTS Service**: External Bark service for voice synthesis
72
+ - **WebRTC**: Browser-based audio capture and streaming
73
+
74
+ ## πŸ” Privacy & Security
75
+
76
+ - Secure OAuth2 authentication with Google
77
+ - Audio processed in real-time, not stored
78
+ - Persistent token storage via Google Secret Manager
79
+ - All calendar operations respect your existing permissions
80
+
81
+ ## πŸ“ž Contact
82
+
83
+ For business scheduling needs or technical support: pgits.job@gmail.com
app.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ChatCal Voice-Enabled AI Assistant - Hugging Face Gradio Implementation
4
+
5
+ A voice-enabled calendar booking assistant with real-time speech-to-text,
6
+ text-to-speech responses, and Google Calendar integration.
7
+ """
8
+
9
+ import gradio as gr
10
+ import os
11
+ import asyncio
12
+ import json
13
+ from typing import Dict, List, Tuple, Optional
14
+ from datetime import datetime
15
+
16
+ # Core functionality imports
17
+ from core.chat_agent import ChatCalAgent
18
+ from core.session_manager import SessionManager
19
+ from core.audio_handler import AudioHandler
20
+ from core.config import config
21
+
22
+ class ChatCalVoiceApp:
23
+ """Main application class for voice-enabled ChatCal."""
24
+
25
+ def __init__(self):
26
+ self.session_manager = SessionManager()
27
+ self.chat_agent = ChatCalAgent()
28
+ self.audio_handler = AudioHandler()
29
+
30
+ async def process_message(
31
+ self,
32
+ message: str,
33
+ history: List[Tuple[str, str]],
34
+ session_id: str
35
+ ) -> Tuple[List[Tuple[str, str]], str]:
36
+ """Process a chat message and return updated history."""
37
+ try:
38
+ # Get or create session
39
+ session = await self.session_manager.get_session(session_id)
40
+
41
+ # Process message through ChatCal agent
42
+ response = await self.chat_agent.process_message(message, session)
43
+
44
+ # Update conversation history
45
+ history.append((message, response))
46
+
47
+ return history, ""
48
+
49
+ except Exception as e:
50
+ error_msg = f"Sorry, I encountered an error: {str(e)}"
51
+ history.append((message, error_msg))
52
+ return history, ""
53
+
54
+ async def process_audio(
55
+ self,
56
+ audio_data: bytes,
57
+ history: List[Tuple[str, str]],
58
+ session_id: str
59
+ ) -> Tuple[List[Tuple[str, str]], str, bytes]:
60
+ """Process audio input and return transcription + response audio."""
61
+ try:
62
+ # Convert audio to text via STT service
63
+ transcription = await self.audio_handler.speech_to_text(audio_data)
64
+
65
+ # Process the transcribed message
66
+ history, _ = await self.process_message(transcription, history, session_id)
67
+
68
+ # Get the latest response for TTS
69
+ if history:
70
+ latest_response = history[-1][1]
71
+ # Convert response to speech
72
+ response_audio = await self.audio_handler.text_to_speech(latest_response)
73
+ return history, transcription, response_audio
74
+
75
+ return history, transcription, None
76
+
77
+ except Exception as e:
78
+ error_msg = f"Audio processing error: {str(e)}"
79
+ history.append(("(Audio input)", error_msg))
80
+ return history, "", None
81
+
82
+ def create_interface(self) -> gr.Interface:
83
+ """Create the main Gradio interface."""
84
+
85
+ with gr.Blocks(
86
+ theme=gr.themes.Soft(),
87
+ title="ChatCal Voice Assistant",
88
+ css="""
89
+ .chat-container {
90
+ max-height: 500px;
91
+ overflow-y: auto;
92
+ }
93
+ .voice-controls {
94
+ background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
95
+ padding: 10px;
96
+ border-radius: 10px;
97
+ margin: 10px 0;
98
+ }
99
+ .status-indicator {
100
+ display: inline-block;
101
+ width: 12px;
102
+ height: 12px;
103
+ border-radius: 50%;
104
+ margin-right: 8px;
105
+ }
106
+ .recording { background-color: #ff4444; }
107
+ .idle { background-color: #44ff44; }
108
+ """
109
+ ) as demo:
110
+
111
+ # Title and description
112
+ gr.Markdown("""
113
+ # πŸŽ€πŸ“… ChatCal Voice Assistant
114
+
115
+ **Book your Google Calendar appointments with voice or text!**
116
+
117
+ - πŸ—£οΈ **Voice Input**: Click record, speak naturally
118
+ - πŸ’¬ **Text Input**: Type your message
119
+ - πŸ“… **Smart Booking**: AI understands dates, times, and preferences
120
+ - πŸŽ₯ **Google Meet**: Automatic video conference setup
121
+ """)
122
+
123
+ # Session state
124
+ session_id = gr.State(value=lambda: f"session_{datetime.now().timestamp()}")
125
+
126
+ with gr.Row():
127
+ with gr.Column(scale=3):
128
+ # Chat history display
129
+ chatbot = gr.Chatbot(
130
+ label="Chat History",
131
+ height=400,
132
+ elem_classes=["chat-container"]
133
+ )
134
+
135
+ with gr.Row(elem_classes=["voice-controls"]):
136
+ # Voice input section
137
+ with gr.Column(scale=2):
138
+ audio_input = gr.Audio(
139
+ source="microphone",
140
+ type="numpy",
141
+ label="🎀 Voice Input",
142
+ interactive=True
143
+ )
144
+ voice_status = gr.HTML(
145
+ value='<span class="status-indicator idle"></span>Ready for voice input'
146
+ )
147
+
148
+ with gr.Column(scale=1):
149
+ # Audio output
150
+ audio_output = gr.Audio(
151
+ label="πŸ”Š AI Response",
152
+ type="numpy",
153
+ interactive=False
154
+ )
155
+
156
+ # Text input section
157
+ with gr.Row():
158
+ text_input = gr.Textbox(
159
+ label="πŸ’¬ Type your message or see voice transcription",
160
+ placeholder="Hi! I'm [Your Name]. Book a 30-minute meeting tomorrow at 2 PM...",
161
+ lines=2,
162
+ scale=4
163
+ )
164
+ send_btn = gr.Button("Send", variant="primary", scale=1)
165
+
166
+ with gr.Column(scale=1):
167
+ # Quick action buttons
168
+ gr.Markdown("### πŸš€ Quick Actions")
169
+
170
+ quick_meet = gr.Button(
171
+ "πŸŽ₯ Google Meet (30m)",
172
+ variant="secondary"
173
+ )
174
+ quick_availability = gr.Button(
175
+ "πŸ“… Check Availability",
176
+ variant="secondary"
177
+ )
178
+ quick_cancel = gr.Button(
179
+ "❌ Cancel Meeting",
180
+ variant="secondary"
181
+ )
182
+
183
+ # Voice settings
184
+ gr.Markdown("### 🎭 Voice Settings")
185
+ voice_enabled = gr.Checkbox(
186
+ label="Enable voice responses",
187
+ value=True
188
+ )
189
+ voice_selection = gr.Dropdown(
190
+ choices=[
191
+ "v2/en_speaker_0",
192
+ "v2/en_speaker_1",
193
+ "v2/en_speaker_2",
194
+ "v2/en_speaker_6",
195
+ "v2/en_speaker_9"
196
+ ],
197
+ value="v2/en_speaker_6",
198
+ label="AI Voice"
199
+ )
200
+
201
+ # Event handlers
202
+ def handle_text_submit(message, history, session):
203
+ if message.strip():
204
+ # Use asyncio to handle the async function
205
+ loop = asyncio.new_event_loop()
206
+ asyncio.set_event_loop(loop)
207
+ try:
208
+ result = loop.run_until_complete(
209
+ app.process_message(message, history, session)
210
+ )
211
+ return result
212
+ finally:
213
+ loop.close()
214
+ return history, message
215
+
216
+ def handle_audio_submit(audio, history, session):
217
+ if audio is not None:
218
+ # Convert audio data and process
219
+ loop = asyncio.new_event_loop()
220
+ asyncio.set_event_loop(loop)
221
+ try:
222
+ # Convert numpy array to bytes (simplified)
223
+ audio_bytes = audio[1].tobytes() if len(audio) > 1 else b""
224
+ result = loop.run_until_complete(
225
+ app.process_audio(audio_bytes, history, session)
226
+ )
227
+ return result
228
+ finally:
229
+ loop.close()
230
+ return history, "", None
231
+
232
+ def handle_quick_action(action_text, history, session):
233
+ """Handle quick action button clicks."""
234
+ loop = asyncio.new_event_loop()
235
+ asyncio.set_event_loop(loop)
236
+ try:
237
+ result = loop.run_until_complete(
238
+ app.process_message(action_text, history, session)
239
+ )
240
+ return result[0], "" # Return updated history and clear text input
241
+ finally:
242
+ loop.close()
243
+
244
+ # Wire up the event handlers
245
+ send_btn.click(
246
+ fn=handle_text_submit,
247
+ inputs=[text_input, chatbot, session_id],
248
+ outputs=[chatbot, text_input]
249
+ )
250
+
251
+ text_input.submit(
252
+ fn=handle_text_submit,
253
+ inputs=[text_input, chatbot, session_id],
254
+ outputs=[chatbot, text_input]
255
+ )
256
+
257
+ audio_input.change(
258
+ fn=handle_audio_submit,
259
+ inputs=[audio_input, chatbot, session_id],
260
+ outputs=[chatbot, text_input, audio_output]
261
+ )
262
+
263
+ # Quick action handlers
264
+ quick_meet.click(
265
+ fn=lambda hist, sess: handle_quick_action(
266
+ "Book a 30-minute Google Meet with Peter for next available time",
267
+ hist, sess
268
+ ),
269
+ inputs=[chatbot, session_id],
270
+ outputs=[chatbot, text_input]
271
+ )
272
+
273
+ quick_availability.click(
274
+ fn=lambda hist, sess: handle_quick_action(
275
+ "What is Peter's availability this week?",
276
+ hist, sess
277
+ ),
278
+ inputs=[chatbot, session_id],
279
+ outputs=[chatbot, text_input]
280
+ )
281
+
282
+ quick_cancel.click(
283
+ fn=lambda hist, sess: handle_quick_action(
284
+ "Cancel my upcoming meeting with Peter",
285
+ hist, sess
286
+ ),
287
+ inputs=[chatbot, session_id],
288
+ outputs=[chatbot, text_input]
289
+ )
290
+
291
+ return demo
292
+
293
+ # Global app instance
294
+ app = ChatCalVoiceApp()
295
+
296
+ # Create and launch the interface
297
+ if __name__ == "__main__":
298
+ demo = app.create_interface()
299
+
300
+ # Launch configuration
301
+ demo.launch(
302
+ server_name="0.0.0.0",
303
+ server_port=7860,
304
+ share=True,
305
+ show_error=True,
306
+ enable_queue=True,
307
+ max_threads=10
308
+ )
core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Core ChatCal functionality for Hugging Face deployment
core/audio_handler.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Audio Handler for ChatCal Voice - Handles STT and TTS integration.
3
+
4
+ This module connects to the external Hugging Face STT and TTS services
5
+ to provide voice interaction capabilities.
6
+ """
7
+
8
+ import logging
9
+ import numpy as np
10
+ from typing import Optional, Tuple
11
+ from gradio_client import Client
12
+
13
+ from .config import config
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class AudioHandler:
19
+ """Handles audio processing for voice interactions."""
20
+
21
+ def __init__(self):
22
+ self.stt_client = None
23
+ self.tts_client = None
24
+ self.demo_mode = True # Start in demo mode
25
+
26
+ # Initialize clients if URLs are provided
27
+ self._initialize_clients()
28
+
29
+ def _initialize_clients(self):
30
+ """Initialize STT and TTS clients."""
31
+ try:
32
+ # Initialize STT client
33
+ if config.stt_service_url and "YOUR_USERNAME" not in config.stt_service_url:
34
+ self.stt_client = Client(config.stt_service_url)
35
+ logger.info(f"🎀 Connected to STT service: {config.stt_service_url}")
36
+
37
+ # Initialize TTS client
38
+ if config.tts_service_url and "YOUR_USERNAME" not in config.tts_service_url:
39
+ self.tts_client = Client(config.tts_service_url)
40
+ logger.info(f"πŸ”Š Connected to TTS service: {config.tts_service_url}")
41
+
42
+ # Check if we're still in demo mode
43
+ if self.stt_client and self.tts_client:
44
+ self.demo_mode = False
45
+ logger.info("🎡 Audio services fully initialized")
46
+ else:
47
+ logger.warning("🎡 Running in audio demo mode - no actual STT/TTS processing")
48
+
49
+ except Exception as e:
50
+ logger.error(f"Failed to initialize audio clients: {e}")
51
+ self.demo_mode = True
52
+
53
+ async def speech_to_text(self, audio_data: bytes) -> str:
54
+ """Convert speech to text using external STT service."""
55
+ try:
56
+ if self.demo_mode or not self.stt_client:
57
+ return self._simulate_stt(audio_data)
58
+
59
+ # Process with actual STT service
60
+ result = self.stt_client.predict(
61
+ audio_data,
62
+ "auto", # language auto-detection
63
+ "base", # model size
64
+ True, # include timestamps
65
+ api_name="/predict"
66
+ )
67
+
68
+ # Extract transcription from result
69
+ if result and len(result) > 1:
70
+ return result[1] # transcription text
71
+
72
+ return "Could not transcribe audio"
73
+
74
+ except Exception as e:
75
+ logger.error(f"STT error: {e}")
76
+ return self._simulate_stt(audio_data)
77
+
78
+ def _simulate_stt(self, audio_data: bytes) -> str:
79
+ """Simulate speech-to-text for demo purposes."""
80
+ # Return a realistic demo transcription
81
+ demo_transcriptions = [
82
+ "Hi, I'm John Smith. I'd like to book a 30-minute meeting with Peter tomorrow at 2 PM.",
83
+ "Hello, this is Sarah. Can we schedule a Google Meet for next Tuesday?",
84
+ "I'm Mike Johnson. Please book an appointment for Friday afternoon.",
85
+ "Hi there! I need to schedule a one-hour consultation about my project.",
86
+ "Good morning, I'd like to check Peter's availability this week."
87
+ ]
88
+
89
+ import random
90
+ return random.choice(demo_transcriptions)
91
+
92
+ async def text_to_speech(self, text: str, voice: Optional[str] = None) -> Optional[bytes]:
93
+ """Convert text to speech using external TTS service."""
94
+ try:
95
+ if not config.enable_voice_responses:
96
+ return None
97
+
98
+ if self.demo_mode or not self.tts_client:
99
+ return self._simulate_tts(text)
100
+
101
+ # Use provided voice or default
102
+ selected_voice = voice or config.default_voice
103
+
104
+ # Process with actual TTS service
105
+ result = self.tts_client.predict(
106
+ text,
107
+ selected_voice,
108
+ api_name="/predict"
109
+ )
110
+
111
+ # Extract audio from result
112
+ if result and len(result) > 0:
113
+ return result[0] # audio file data
114
+
115
+ return None
116
+
117
+ except Exception as e:
118
+ logger.error(f"TTS error: {e}")
119
+ return self._simulate_tts(text)
120
+
121
+ def _simulate_tts(self, text: str) -> Optional[bytes]:
122
+ """Simulate text-to-speech for demo purposes."""
123
+ # Return None to indicate no audio generation in demo mode
124
+ logger.info(f"πŸ”Š Demo TTS would say: {text[:50]}...")
125
+ return None
126
+
127
+ def process_audio_input(self, audio_tuple: Tuple) -> str:
128
+ """Process Gradio audio input format."""
129
+ try:
130
+ if audio_tuple is None or len(audio_tuple) < 2:
131
+ return "No audio received"
132
+
133
+ # Gradio audio format: (sample_rate, audio_array)
134
+ sample_rate, audio_array = audio_tuple
135
+
136
+ # Convert numpy array to bytes
137
+ if isinstance(audio_array, np.ndarray):
138
+ # Normalize and convert to bytes
139
+ audio_normalized = (audio_array * 32767).astype(np.int16)
140
+ audio_bytes = audio_normalized.tobytes()
141
+ return self.speech_to_text(audio_bytes)
142
+
143
+ return "Invalid audio format"
144
+
145
+ except Exception as e:
146
+ logger.error(f"Audio processing error: {e}")
147
+ return "Error processing audio"
148
+
149
+ def is_audio_service_available(self) -> Tuple[bool, bool]:
150
+ """Check if STT and TTS services are available."""
151
+ stt_available = bool(self.stt_client and not self.demo_mode)
152
+ tts_available = bool(self.tts_client and not self.demo_mode)
153
+ return stt_available, tts_available
154
+
155
+ def get_audio_status(self) -> dict:
156
+ """Get status of audio services."""
157
+ stt_available, tts_available = self.is_audio_service_available()
158
+
159
+ return {
160
+ "stt_available": stt_available,
161
+ "tts_available": tts_available,
162
+ "demo_mode": self.demo_mode,
163
+ "voice_responses_enabled": config.enable_voice_responses,
164
+ "default_voice": config.default_voice
165
+ }
166
+
167
+
168
+ # Global audio handler instance
169
+ audio_handler = AudioHandler()
core/calendar_service.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Calendar Service - Simplified Google Calendar integration for Hugging Face.
3
+
4
+ This is a streamlined version that focuses on the core booking functionality
5
+ while being compatible with the HF environment.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, List, Any, Optional
10
+ from datetime import datetime, timedelta
11
+ import json
12
+
13
+ from .config import config
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class CalendarService:
19
+ """Simplified Google Calendar service for HF deployment."""
20
+
21
+ def __init__(self):
22
+ self.calendar_id = config.google_calendar_id
23
+
24
+ # For development/demo mode, we'll simulate calendar operations
25
+ self.demo_mode = not (config.google_client_id and config.google_client_secret)
26
+
27
+ if self.demo_mode:
28
+ logger.warning("πŸ“… Running in demo mode - no actual calendar integration")
29
+ else:
30
+ logger.info("πŸ“… Google Calendar integration enabled")
31
+
32
+ async def book_appointment(self, booking_info: Dict[str, Any], user_info: Dict[str, Any]) -> Dict[str, Any]:
33
+ """Book an appointment on Google Calendar."""
34
+ try:
35
+ if self.demo_mode:
36
+ return self._simulate_booking(booking_info, user_info)
37
+
38
+ # TODO: Implement actual Google Calendar booking
39
+ # For now, return simulation
40
+ return self._simulate_booking(booking_info, user_info)
41
+
42
+ except Exception as e:
43
+ logger.error(f"Booking error: {e}")
44
+ return {
45
+ "success": False,
46
+ "error": str(e)
47
+ }
48
+
49
+ def _simulate_booking(self, booking_info: Dict[str, Any], user_info: Dict[str, Any]) -> Dict[str, Any]:
50
+ """Simulate a booking for demo purposes."""
51
+
52
+ # Generate a mock event
53
+ event_id = f"demo_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
54
+
55
+ # Parse the booking info
56
+ date_time = booking_info.get("date_time", "2024-01-01 14:00")
57
+ duration = booking_info.get("duration", 30)
58
+ meeting_type = booking_info.get("meeting_type", "google_meet")
59
+ topic = booking_info.get("topic", "Meeting")
60
+
61
+ # Create event details
62
+ event = {
63
+ "id": event_id,
64
+ "start_time": date_time,
65
+ "duration": duration,
66
+ "topic": topic,
67
+ "attendee_name": user_info.get("name", "Guest"),
68
+ "attendee_email": user_info.get("email", ""),
69
+ "attendee_phone": user_info.get("phone", ""),
70
+ "meeting_type": meeting_type
71
+ }
72
+
73
+ # Add Google Meet link for video meetings
74
+ if meeting_type == "google_meet":
75
+ event["meet_link"] = f"πŸŽ₯ **Google Meet:** https://meet.google.com/demo-link-{event_id[:8]}"
76
+
77
+ return {
78
+ "success": True,
79
+ "event": event,
80
+ "message": "Demo booking created successfully!"
81
+ }
82
+
83
+ async def get_availability(self, days: int = 7) -> str:
84
+ """Get availability information."""
85
+ if self.demo_mode:
86
+ return self._simulate_availability(days)
87
+
88
+ # TODO: Implement actual availability checking
89
+ return self._simulate_availability(days)
90
+
91
+ def _simulate_availability(self, days: int = 7) -> str:
92
+ """Simulate availability for demo purposes."""
93
+ today = datetime.now()
94
+ availability = []
95
+
96
+ for i in range(days):
97
+ date = today + timedelta(days=i)
98
+ day_name = date.strftime("%A")
99
+ date_str = date.strftime("%B %d")
100
+
101
+ if date.weekday() < 5: # Weekday
102
+ times = ["9:00 AM", "11:00 AM", "2:00 PM", "4:00 PM"]
103
+ else: # Weekend
104
+ times = ["10:00 AM", "1:00 PM", "3:00 PM"]
105
+
106
+ # Randomly remove some slots to simulate bookings
107
+ import random
108
+ available_times = random.sample(times, max(1, len(times) - random.randint(0, 2)))
109
+
110
+ availability.append(f"**{day_name}, {date_str}:** {', '.join(available_times)}")
111
+
112
+ return "\n".join(availability)
113
+
114
+ async def cancel_appointment(self, event_id: str) -> Dict[str, Any]:
115
+ """Cancel an appointment."""
116
+ if self.demo_mode:
117
+ return {
118
+ "success": True,
119
+ "message": f"Demo appointment {event_id} cancelled successfully!"
120
+ }
121
+
122
+ # TODO: Implement actual cancellation
123
+ return {
124
+ "success": False,
125
+ "error": "Cancellation not yet implemented"
126
+ }
127
+
128
+ async def list_upcoming_events(self, days: int = 7) -> List[Dict[str, Any]]:
129
+ """List upcoming events."""
130
+ if self.demo_mode:
131
+ return self._simulate_upcoming_events(days)
132
+
133
+ # TODO: Implement actual event listing
134
+ return self._simulate_upcoming_events(days)
135
+
136
+ def _simulate_upcoming_events(self, days: int = 7) -> List[Dict[str, Any]]:
137
+ """Simulate upcoming events for demo."""
138
+ events = []
139
+ today = datetime.now()
140
+
141
+ # Create a few sample events
142
+ import random
143
+ for i in range(3):
144
+ date = today + timedelta(days=i+1, hours=random.randint(9, 17))
145
+ events.append({
146
+ "id": f"demo_event_{i}",
147
+ "summary": f"Sample Meeting {i+1}",
148
+ "start_time": date.strftime("%Y-%m-%d %H:%M"),
149
+ "duration": 30,
150
+ "attendees": ["sample@email.com"]
151
+ })
152
+
153
+ return events
154
+
155
+ def format_event_for_display(self, event: Dict[str, Any]) -> str:
156
+ """Format an event for display."""
157
+ start_time = event.get("start_time", "")
158
+ duration = event.get("duration", 30)
159
+ topic = event.get("topic", "Meeting")
160
+
161
+ formatted = f"πŸ“… {topic}\n"
162
+ formatted += f"πŸ• {start_time} ({duration} minutes)\n"
163
+
164
+ if event.get("meet_link"):
165
+ formatted += f"{event['meet_link']}\n"
166
+
167
+ return formatted
core/chat_agent.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ChatCal Voice Agent - Simplified version for Hugging Face deployment.
3
+
4
+ This is a streamlined version of the ChatCal agent optimized for Gradio deployment
5
+ on Hugging Face, with voice interaction capabilities.
6
+ """
7
+
8
+ from typing import Dict, List, Optional, Any
9
+ import json
10
+ import re
11
+ import random
12
+ from datetime import datetime
13
+ from llama_index.core.llms import ChatMessage, MessageRole
14
+ from llama_index.core.memory import ChatMemoryBuffer
15
+
16
+ from .config import config
17
+ from .llm_provider import get_llm
18
+ from .calendar_service import CalendarService
19
+ from .session import SessionData
20
+
21
+ # System prompt for the voice-enabled assistant
22
+ SYSTEM_PROMPT = """You are ChatCal, a friendly AI assistant specializing in Google Calendar scheduling. You help users book, modify, and manage appointments through natural conversation, including voice interactions.
23
+
24
+ ## Your Identity
25
+ - You work with Peter ({my_email_address}, {my_phone_number})
26
+ - You're professional yet friendly, conversational and helpful
27
+ - You understand both voice and text input equally well
28
+ - You can provide both text and voice responses
29
+
30
+ ## Core Capabilities
31
+ - Book Google Calendar appointments with automatic Google Meet links
32
+ - Check availability and suggest optimal meeting times
33
+ - Cancel or modify existing meetings
34
+ - Extract contact info (name, email, phone) from natural conversation
35
+ - Handle timezone-aware scheduling
36
+ - Send email confirmations with calendar invites
37
+
38
+ ## Voice Interaction Guidelines
39
+ - Acknowledge when processing voice input naturally
40
+ - Be concise but complete in voice responses
41
+ - Ask clarifying questions when voice input is unclear
42
+ - Provide confirmation details in a voice-friendly format
43
+
44
+ ## Booking Requirements
45
+ To book appointments, you need:
46
+ 1. User's name (first name minimum)
47
+ 2. Contact method (email or phone)
48
+ 3. Meeting duration (default 30 minutes)
49
+ 4. Date and time (can suggest if not specified)
50
+
51
+ ## Response Style
52
+ - Keep responses conversational and natural
53
+ - Use HTML formatting for web display when needed
54
+ - For voice responses, speak clearly and provide key details
55
+ - Don't mention technical details or tools unless relevant
56
+
57
+ ## Current Context
58
+ Today is {current_date}. Peter's timezone is {timezone}.
59
+ Work hours: Weekdays {weekday_start}-{weekday_end}, Weekends {weekend_start}-{weekend_end}."""
60
+
61
+
62
+ class ChatCalAgent:
63
+ """Main agent for voice-enabled ChatCal interactions."""
64
+
65
+ def __init__(self):
66
+ self.llm = get_llm()
67
+ self.calendar_service = CalendarService()
68
+
69
+ async def process_message(self, message: str, session: SessionData) -> str:
70
+ """Process a message and return a response."""
71
+ try:
72
+ # Update session with the new message
73
+ session.add_message("user", message)
74
+
75
+ # Extract user information from message
76
+ self._extract_user_info(message, session)
77
+
78
+ # Check if this looks like a booking request
79
+ if self._is_booking_request(message):
80
+ return await self._handle_booking_request(message, session)
81
+
82
+ # Check if this is a cancellation request
83
+ elif self._is_cancellation_request(message):
84
+ return await self._handle_cancellation_request(message, session)
85
+
86
+ # Check if this is an availability request
87
+ elif self._is_availability_request(message):
88
+ return await self._handle_availability_request(message, session)
89
+
90
+ # General conversation
91
+ else:
92
+ return await self._handle_general_conversation(message, session)
93
+
94
+ except Exception as e:
95
+ return f"I apologize, but I encountered an error: {str(e)}. Please try again."
96
+
97
+ def _extract_user_info(self, message: str, session: SessionData):
98
+ """Extract user information from the message."""
99
+ # Extract name
100
+ name_patterns = [
101
+ r"(?:I'm|I am|My name is|This is|Call me)\s+([A-Za-z]+)",
102
+ r"Hi,?\s+(?:I'm|I am|My name is|This is)?\s*([A-Za-z]+)",
103
+ ]
104
+
105
+ for pattern in name_patterns:
106
+ match = re.search(pattern, message, re.IGNORECASE)
107
+ if match and not session.user_info.get("name"):
108
+ session.user_info["name"] = match.group(1).strip().title()
109
+
110
+ # Extract email
111
+ email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
112
+ email_match = re.search(email_pattern, message)
113
+ if email_match and not session.user_info.get("email"):
114
+ session.user_info["email"] = email_match.group()
115
+
116
+ # Extract phone
117
+ phone_pattern = r'\b(?:\+?1[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})\b'
118
+ phone_match = re.search(phone_pattern, message)
119
+ if phone_match and not session.user_info.get("phone"):
120
+ session.user_info["phone"] = f"{phone_match.group(1)}-{phone_match.group(2)}-{phone_match.group(3)}"
121
+
122
+ def _is_booking_request(self, message: str) -> bool:
123
+ """Check if message is a booking request."""
124
+ booking_keywords = [
125
+ "book", "schedule", "appointment", "meeting", "reserve",
126
+ "set up", "arrange", "plan", "meet"
127
+ ]
128
+ return any(keyword in message.lower() for keyword in booking_keywords)
129
+
130
+ def _is_cancellation_request(self, message: str) -> bool:
131
+ """Check if message is a cancellation request."""
132
+ cancel_keywords = ["cancel", "delete", "remove", "unbook"]
133
+ return any(keyword in message.lower() for keyword in cancel_keywords)
134
+
135
+ def _is_availability_request(self, message: str) -> bool:
136
+ """Check if message is asking about availability."""
137
+ availability_keywords = [
138
+ "available", "availability", "free", "busy", "schedule",
139
+ "when", "what time", "open slots"
140
+ ]
141
+ return any(keyword in message.lower() for keyword in availability_keywords)
142
+
143
+ async def _handle_booking_request(self, message: str, session: SessionData) -> str:
144
+ """Handle booking requests."""
145
+ # Check if we have required info
146
+ missing_info = []
147
+ if not session.user_info.get("name"):
148
+ missing_info.append("your name")
149
+ if not session.user_info.get("email") and not session.user_info.get("phone"):
150
+ missing_info.append("your email or phone number")
151
+
152
+ if missing_info:
153
+ return f"I'd be happy to help you book an appointment! I just need {' and '.join(missing_info)} to get started."
154
+
155
+ # Try to book the appointment
156
+ try:
157
+ # Parse the booking request using LLM
158
+ booking_info = await self._parse_booking_request(message, session)
159
+
160
+ if booking_info.get("needs_clarification"):
161
+ return booking_info["clarification_message"]
162
+
163
+ # Attempt to book with calendar service
164
+ result = await self.calendar_service.book_appointment(booking_info, session.user_info)
165
+
166
+ if result["success"]:
167
+ response = f"""βœ… **Appointment Booked Successfully!**
168
+
169
+ πŸ“… **Meeting Details:**
170
+ - **Date:** {result['event']['start_time']}
171
+ - **Duration:** {result['event']['duration']} minutes
172
+ - **Attendee:** {session.user_info['name']} ({session.user_info.get('email', session.user_info.get('phone', ''))})
173
+
174
+ {result['event'].get('meet_link', '')}
175
+
176
+ πŸ“§ Calendar invitation sent to your email!"""
177
+
178
+ session.add_message("assistant", response)
179
+ return response
180
+ else:
181
+ return f"❌ I couldn't book the appointment: {result['error']}"
182
+
183
+ except Exception as e:
184
+ return f"I encountered an issue while booking: {str(e)}. Please try again with more specific details."
185
+
186
+ async def _handle_cancellation_request(self, message: str, session: SessionData) -> str:
187
+ """Handle cancellation requests."""
188
+ return "πŸ”„ Cancellation feature is being implemented. Please contact Peter directly to cancel appointments."
189
+
190
+ async def _handle_availability_request(self, message: str, session: SessionData) -> str:
191
+ """Handle availability requests."""
192
+ try:
193
+ availability = await self.calendar_service.get_availability()
194
+ return f"πŸ“… **Peter's Availability:**\n\n{availability}"
195
+ except Exception as e:
196
+ return f"I couldn't check availability right now: {str(e)}"
197
+
198
+ async def _handle_general_conversation(self, message: str, session: SessionData) -> str:
199
+ """Handle general conversation."""
200
+ # Build conversation context
201
+ messages = [
202
+ ChatMessage(
203
+ role=MessageRole.SYSTEM,
204
+ content=SYSTEM_PROMPT.format(
205
+ my_email_address=config.my_email_address,
206
+ my_phone_number=config.my_phone_number,
207
+ current_date=datetime.now().strftime("%Y-%m-%d"),
208
+ timezone=config.default_timezone,
209
+ weekday_start=config.weekday_start_time,
210
+ weekday_end=config.weekday_end_time,
211
+ weekend_start=config.weekend_start_time,
212
+ weekend_end=config.weekend_end_time
213
+ )
214
+ )
215
+ ]
216
+
217
+ # Add conversation history
218
+ for msg in session.conversation_history[-10:]: # Last 10 messages
219
+ role = MessageRole.USER if msg["role"] == "user" else MessageRole.ASSISTANT
220
+ messages.append(ChatMessage(role=role, content=msg["content"]))
221
+
222
+ # Get response from LLM
223
+ response = await self.llm.achat(messages)
224
+
225
+ session.add_message("assistant", response.message.content)
226
+ return response.message.content
227
+
228
+ async def _parse_booking_request(self, message: str, session: SessionData) -> Dict[str, Any]:
229
+ """Parse booking request details using LLM."""
230
+ parsing_prompt = f"""
231
+ Parse this booking request and extract the following information:
232
+
233
+ Message: "{message}"
234
+ User Info: {json.dumps(session.user_info)}
235
+
236
+ Extract:
237
+ 1. Date and time (convert to specific datetime)
238
+ 2. Duration in minutes (default 30)
239
+ 3. Meeting type (in-person, Google Meet, phone)
240
+ 4. Topic/purpose if mentioned
241
+
242
+ Return JSON format:
243
+ {{
244
+ "date_time": "YYYY-MM-DD HH:MM",
245
+ "duration": 30,
246
+ "meeting_type": "google_meet",
247
+ "topic": "General meeting",
248
+ "needs_clarification": false,
249
+ "clarification_message": ""
250
+ }}
251
+
252
+ If you need clarification about date/time, set needs_clarification to true.
253
+ """
254
+
255
+ try:
256
+ response = await self.llm.acomplete(parsing_prompt)
257
+ return json.loads(response.text.strip())
258
+ except:
259
+ # Fallback parsing
260
+ return {
261
+ "date_time": "2024-01-01 14:00", # Placeholder
262
+ "duration": 30,
263
+ "meeting_type": "google_meet",
264
+ "topic": "Meeting request",
265
+ "needs_clarification": True,
266
+ "clarification_message": "Could you please specify the date and time for your meeting?"
267
+ }
core/config.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List, Optional
3
+ from pydantic_settings import BaseSettings
4
+ from pydantic import Field
5
+
6
+
7
+ class Config(BaseSettings):
8
+ """Configuration for ChatCal Voice-Enabled Hugging Face deployment."""
9
+
10
+ # Application
11
+ app_name: str = Field(default="ChatCal Voice Assistant", env="APP_NAME")
12
+ app_env: str = Field(default="production", env="APP_ENV")
13
+
14
+ # Groq API (primary LLM)
15
+ groq_api_key: str = Field(..., env="GROQ_API_KEY")
16
+
17
+ # Anthropic (fallback LLM)
18
+ anthropic_api_key: Optional[str] = Field(None, env="ANTHROPIC_API_KEY")
19
+
20
+ # Gemini API (fallback LLM)
21
+ gemini_api_key: Optional[str] = Field(None, env="GEMINI_API_KEY")
22
+
23
+ # Google Calendar
24
+ google_calendar_id: str = Field(default="pgits.job@gmail.com", env="GOOGLE_CALENDAR_ID")
25
+ google_client_id: Optional[str] = Field(None, env="GOOGLE_CLIENT_ID")
26
+ google_client_secret: Optional[str] = Field(None, env="GOOGLE_CLIENT_SECRET")
27
+
28
+ # Security
29
+ secret_key: str = Field(..., env="SECRET_KEY")
30
+
31
+ # Timezone
32
+ default_timezone: str = Field(default="America/New_York", env="DEFAULT_TIMEZONE")
33
+
34
+ # Working Hours Configuration
35
+ weekday_start_time: str = Field(default="07:30", env="WEEKDAY_START_TIME")
36
+ weekday_end_time: str = Field(default="18:30", env="WEEKDAY_END_TIME")
37
+ weekend_start_time: str = Field(default="10:30", env="WEEKEND_START_TIME")
38
+ weekend_end_time: str = Field(default="16:30", env="WEEKEND_END_TIME")
39
+ working_hours_timezone: str = Field(default="America/New_York", env="WORKING_HOURS_TIMEZONE")
40
+
41
+ # Chat Settings
42
+ max_conversation_history: int = Field(default=20, env="MAX_CONVERSATION_HISTORY")
43
+ session_timeout_minutes: int = Field(default=30, env="SESSION_TIMEOUT_MINUTES")
44
+
45
+ # Contact Information
46
+ my_phone_number: str = Field(..., env="MY_PHONE_NUMBER")
47
+ my_email_address: str = Field(..., env="MY_EMAIL_ADDRESS")
48
+
49
+ # Email Service Configuration
50
+ smtp_server: str = Field(default="smtp.gmail.com", env="SMTP_SERVER")
51
+ smtp_port: int = Field(default=587, env="SMTP_PORT")
52
+ smtp_username: Optional[str] = Field(None, env="SMTP_USERNAME")
53
+ smtp_password: Optional[str] = Field(None, env="SMTP_PASSWORD")
54
+ email_from_name: str = Field(default="ChatCal Voice Assistant", env="EMAIL_FROM_NAME")
55
+
56
+ # Testing Configuration
57
+ testing_mode: bool = Field(default=True, env="TESTING_MODE")
58
+
59
+ # Audio Services Configuration (Hugging Face spaces)
60
+ stt_service_url: str = Field(
61
+ default="https://huggingface.co/spaces/YOUR_USERNAME/stt-gpu-service",
62
+ env="STT_SERVICE_URL"
63
+ )
64
+ tts_service_url: str = Field(
65
+ default="https://huggingface.co/spaces/YOUR_USERNAME/tts-gpu-service",
66
+ env="TTS_SERVICE_URL"
67
+ )
68
+
69
+ # Voice Settings
70
+ default_voice: str = Field(default="v2/en_speaker_6", env="DEFAULT_VOICE")
71
+ enable_voice_responses: bool = Field(default=True, env="ENABLE_VOICE_RESPONSES")
72
+
73
+ class Config:
74
+ env_file = ".env"
75
+ env_file_encoding = "utf-8"
76
+ case_sensitive = False
77
+
78
+
79
+ # Global config instance
80
+ config = Config()
core/llm_provider.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM Provider - Handles different LLM services for ChatCal Voice.
3
+
4
+ Implements the same fallback chain as the original ChatCal:
5
+ Groq (primary) -> Anthropic (fallback) -> Mock (development)
6
+ """
7
+
8
+ import logging
9
+ from typing import Optional
10
+ from llama_index.core.llms import LLM
11
+ from llama_index.llms.groq import Groq
12
+ from llama_index.llms.anthropic import Anthropic
13
+
14
+ from .config import config
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class MockLLM:
20
+ """Mock LLM for development and testing."""
21
+
22
+ async def achat(self, messages):
23
+ """Mock async chat method."""
24
+ last_message = messages[-1].content if messages else "Hello"
25
+
26
+ # Simple rule-based responses for development
27
+ if any(word in last_message.lower() for word in ["book", "schedule", "appointment"]):
28
+ response = "I'd be happy to help you book an appointment! Please provide your name, preferred date and time."
29
+ elif any(word in last_message.lower() for word in ["cancel", "delete"]):
30
+ response = "I can help you cancel an appointment. Could you tell me which meeting you'd like to cancel?"
31
+ elif any(word in last_message.lower() for word in ["available", "availability", "free"]):
32
+ response = "Let me check Peter's availability for you. What dates are you considering?"
33
+ else:
34
+ response = "Hello! I'm ChatCal, your voice-enabled scheduling assistant. I can help you book appointments with Peter. What would you like to schedule?"
35
+
36
+ class MockResponse:
37
+ def __init__(self, content):
38
+ self.message = self
39
+ self.content = content
40
+
41
+ return MockResponse(response)
42
+
43
+ async def acomplete(self, prompt):
44
+ """Mock async completion method."""
45
+ class MockCompletion:
46
+ def __init__(self, content):
47
+ self.text = content
48
+
49
+ # Mock JSON response for booking parsing
50
+ if "Parse this booking request" in prompt:
51
+ return MockCompletion('{"date_time": "2024-01-01 14:00", "duration": 30, "meeting_type": "google_meet", "topic": "Meeting", "needs_clarification": true, "clarification_message": "Could you please specify the exact date and time?"}')
52
+
53
+ return MockCompletion("Mock response for development")
54
+
55
+
56
+ def get_llm() -> LLM:
57
+ """
58
+ Get the appropriate LLM based on available configuration.
59
+ Implements fallback chain: Groq -> Anthropic -> Mock
60
+ """
61
+
62
+ # Try Groq first (primary)
63
+ if config.groq_api_key:
64
+ try:
65
+ logger.info("πŸš€ Using Groq LLM (primary)")
66
+ return Groq(
67
+ model="llama-3.1-8b-instant",
68
+ api_key=config.groq_api_key,
69
+ temperature=0.1
70
+ )
71
+ except Exception as e:
72
+ logger.warning(f"❌ Groq LLM failed to initialize: {e}")
73
+
74
+ # Fallback to Anthropic
75
+ if config.anthropic_api_key:
76
+ try:
77
+ logger.info("🧠 Using Anthropic Claude (fallback)")
78
+ return Anthropic(
79
+ model="claude-3-sonnet-20240229",
80
+ api_key=config.anthropic_api_key,
81
+ temperature=0.1
82
+ )
83
+ except Exception as e:
84
+ logger.warning(f"❌ Anthropic LLM failed to initialize: {e}")
85
+
86
+ # Final fallback to Mock LLM
87
+ logger.warning("⚠️ Using Mock LLM (development/fallback)")
88
+ return MockLLM()
89
+
90
+
91
+ class LLMService:
92
+ """Service wrapper for LLM operations."""
93
+
94
+ def __init__(self):
95
+ self.llm = get_llm()
96
+ self.is_mock = isinstance(self.llm, MockLLM)
97
+
98
+ async def chat(self, messages, temperature: float = 0.1):
99
+ """Send chat messages to LLM."""
100
+ if self.is_mock:
101
+ return await self.llm.achat(messages)
102
+
103
+ # For real LLMs, set temperature if supported
104
+ try:
105
+ if hasattr(self.llm, 'temperature'):
106
+ original_temp = self.llm.temperature
107
+ self.llm.temperature = temperature
108
+ result = await self.llm.achat(messages)
109
+ self.llm.temperature = original_temp
110
+ return result
111
+ else:
112
+ return await self.llm.achat(messages)
113
+ except Exception as e:
114
+ logger.error(f"LLM chat error: {e}")
115
+ # Return a graceful error response
116
+ class ErrorResponse:
117
+ def __init__(self, content):
118
+ self.message = self
119
+ self.content = content
120
+
121
+ return ErrorResponse("I apologize, but I'm having trouble processing your request right now. Please try again.")
122
+
123
+ async def complete(self, prompt: str, temperature: float = 0.1):
124
+ """Send completion prompt to LLM."""
125
+ if self.is_mock:
126
+ return await self.llm.acomplete(prompt)
127
+
128
+ try:
129
+ if hasattr(self.llm, 'temperature'):
130
+ original_temp = self.llm.temperature
131
+ self.llm.temperature = temperature
132
+ result = await self.llm.acomplete(prompt)
133
+ self.llm.temperature = original_temp
134
+ return result
135
+ else:
136
+ return await self.llm.acomplete(prompt)
137
+ except Exception as e:
138
+ logger.error(f"LLM completion error: {e}")
139
+ class ErrorCompletion:
140
+ def __init__(self, content):
141
+ self.text = content
142
+
143
+ return ErrorCompletion("Error processing request")
144
+
145
+
146
+ # Global LLM service instance
147
+ llm_service = LLMService()
core/session.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session Data Model for ChatCal Voice.
3
+
4
+ Handles conversation state, user information, and session persistence
5
+ in the Hugging Face Gradio environment.
6
+ """
7
+
8
+ from typing import Dict, List, Any, Optional
9
+ from datetime import datetime
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class SessionData:
15
+ """Data structure for user sessions."""
16
+
17
+ session_id: str
18
+ created_at: datetime = field(default_factory=datetime.now)
19
+ last_activity: datetime = field(default_factory=datetime.now)
20
+
21
+ # User information extracted from conversation
22
+ user_info: Dict[str, Any] = field(default_factory=lambda: {
23
+ "name": None,
24
+ "email": None,
25
+ "phone": None,
26
+ "preferences": {},
27
+ "timezone": None
28
+ })
29
+
30
+ # Conversation history
31
+ conversation_history: List[Dict[str, str]] = field(default_factory=list)
32
+
33
+ # Session state for multi-turn operations
34
+ session_state: Dict[str, Any] = field(default_factory=lambda: {
35
+ "pending_operation": None, # "booking", "cancellation", "availability"
36
+ "operation_context": {}, # Context data for operations
37
+ "awaiting_clarification": False,
38
+ "last_voice_input": None,
39
+ "voice_enabled": True
40
+ })
41
+
42
+ # Booking history for this session
43
+ booking_history: List[Dict[str, Any]] = field(default_factory=list)
44
+
45
+ def add_message(self, role: str, content: str):
46
+ """Add a message to conversation history."""
47
+ self.conversation_history.append({
48
+ "role": role, # "user" or "assistant"
49
+ "content": content,
50
+ "timestamp": datetime.now().isoformat()
51
+ })
52
+
53
+ # Keep only recent messages to prevent memory issues
54
+ max_history = 50
55
+ if len(self.conversation_history) > max_history:
56
+ self.conversation_history = self.conversation_history[-max_history:]
57
+
58
+ self.last_activity = datetime.now()
59
+
60
+ def get_recent_messages(self, count: int = 10) -> List[Dict[str, str]]:
61
+ """Get recent conversation messages."""
62
+ return self.conversation_history[-count:] if self.conversation_history else []
63
+
64
+ def update_user_info(self, **kwargs):
65
+ """Update user information."""
66
+ for key, value in kwargs.items():
67
+ if key in self.user_info and value:
68
+ self.user_info[key] = value
69
+ self.last_activity = datetime.now()
70
+
71
+ def has_required_user_info(self) -> bool:
72
+ """Check if session has minimum required user information."""
73
+ return (
74
+ bool(self.user_info.get("name")) and
75
+ (bool(self.user_info.get("email")) or bool(self.user_info.get("phone")))
76
+ )
77
+
78
+ def get_user_summary(self) -> str:
79
+ """Get a summary of user information."""
80
+ name = self.user_info.get("name", "Unknown")
81
+ contact = self.user_info.get("email") or self.user_info.get("phone") or "No contact"
82
+ return f"{name} ({contact})"
83
+
84
+ def set_pending_operation(self, operation: str, context: Dict[str, Any] = None):
85
+ """Set a pending operation with context."""
86
+ self.session_state["pending_operation"] = operation
87
+ self.session_state["operation_context"] = context or {}
88
+ self.session_state["awaiting_clarification"] = False
89
+ self.last_activity = datetime.now()
90
+
91
+ def clear_pending_operation(self):
92
+ """Clear any pending operation."""
93
+ self.session_state["pending_operation"] = None
94
+ self.session_state["operation_context"] = {}
95
+ self.session_state["awaiting_clarification"] = False
96
+ self.last_activity = datetime.now()
97
+
98
+ def add_booking(self, booking_info: Dict[str, Any]):
99
+ """Add a booking to the session history."""
100
+ booking_info["session_id"] = self.session_id
101
+ booking_info["timestamp"] = datetime.now().isoformat()
102
+ self.booking_history.append(booking_info)
103
+ self.last_activity = datetime.now()
104
+
105
+ def get_session_duration_minutes(self) -> int:
106
+ """Get session duration in minutes."""
107
+ delta = datetime.now() - self.created_at
108
+ return int(delta.total_seconds() / 60)
109
+
110
+ def is_expired(self, timeout_minutes: int = 30) -> bool:
111
+ """Check if session is expired."""
112
+ delta = datetime.now() - self.last_activity
113
+ return delta.total_seconds() > (timeout_minutes * 60)
114
+
115
+ def to_dict(self) -> Dict[str, Any]:
116
+ """Convert session to dictionary for serialization."""
117
+ return {
118
+ "session_id": self.session_id,
119
+ "created_at": self.created_at.isoformat(),
120
+ "last_activity": self.last_activity.isoformat(),
121
+ "user_info": self.user_info,
122
+ "conversation_count": len(self.conversation_history),
123
+ "session_state": self.session_state,
124
+ "booking_count": len(self.booking_history)
125
+ }
126
+
127
+ @classmethod
128
+ def from_dict(cls, data: Dict[str, Any]) -> 'SessionData':
129
+ """Create session from dictionary."""
130
+ session = cls(session_id=data["session_id"])
131
+ session.created_at = datetime.fromisoformat(data["created_at"])
132
+ session.last_activity = datetime.fromisoformat(data["last_activity"])
133
+ session.user_info = data.get("user_info", {})
134
+ session.session_state = data.get("session_state", {})
135
+ return session
core/session_manager.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session Manager for ChatCal Voice - Handles user sessions in Gradio environment.
3
+
4
+ Since we're on Hugging Face without persistent storage, we'll use in-memory
5
+ session management with automatic cleanup.
6
+ """
7
+
8
+ import time
9
+ import uuid
10
+ from typing import Dict, List, Any, Optional
11
+ from datetime import datetime, timedelta
12
+
13
+ from .session import SessionData
14
+ from .config import config
15
+
16
+
17
+ class SessionManager:
18
+ """Manages user sessions for the voice-enabled ChatCal."""
19
+
20
+ def __init__(self):
21
+ self.sessions: Dict[str, SessionData] = {}
22
+ self.last_cleanup = time.time()
23
+ self.cleanup_interval = 300 # 5 minutes
24
+
25
+ async def get_session(self, session_id: Optional[str] = None) -> SessionData:
26
+ """Get or create a session."""
27
+ # Auto-cleanup old sessions periodically
28
+ await self._cleanup_expired_sessions()
29
+
30
+ # Create new session if none provided
31
+ if not session_id:
32
+ session_id = self._generate_session_id()
33
+
34
+ # Return existing session or create new one
35
+ if session_id in self.sessions:
36
+ session = self.sessions[session_id]
37
+ session.last_activity = datetime.now()
38
+ return session
39
+
40
+ # Create new session
41
+ session = SessionData(session_id=session_id)
42
+ self.sessions[session_id] = session
43
+ return session
44
+
45
+ def _generate_session_id(self) -> str:
46
+ """Generate a unique session ID."""
47
+ timestamp = int(time.time())
48
+ unique_id = str(uuid.uuid4())[:8]
49
+ return f"chatcal_{timestamp}_{unique_id}"
50
+
51
+ async def _cleanup_expired_sessions(self):
52
+ """Clean up expired sessions."""
53
+ current_time = time.time()
54
+
55
+ # Only run cleanup periodically
56
+ if current_time - self.last_cleanup < self.cleanup_interval:
57
+ return
58
+
59
+ cutoff_time = datetime.now() - timedelta(minutes=config.session_timeout_minutes)
60
+ expired_sessions = [
61
+ session_id for session_id, session in self.sessions.items()
62
+ if session.last_activity < cutoff_time
63
+ ]
64
+
65
+ for session_id in expired_sessions:
66
+ del self.sessions[session_id]
67
+
68
+ if expired_sessions:
69
+ print(f"🧹 Cleaned up {len(expired_sessions)} expired sessions")
70
+
71
+ self.last_cleanup = current_time
72
+
73
+ async def delete_session(self, session_id: str):
74
+ """Delete a specific session."""
75
+ if session_id in self.sessions:
76
+ del self.sessions[session_id]
77
+
78
+ def get_session_count(self) -> int:
79
+ """Get the number of active sessions."""
80
+ return len(self.sessions)
81
+
82
+ def get_session_stats(self) -> Dict[str, Any]:
83
+ """Get session statistics."""
84
+ return {
85
+ "active_sessions": len(self.sessions),
86
+ "total_messages": sum(len(s.conversation_history) for s in self.sessions.values()),
87
+ "sessions_with_user_info": sum(
88
+ 1 for s in self.sessions.values()
89
+ if s.user_info.get("name") or s.user_info.get("email")
90
+ )
91
+ }
92
+
93
+
94
+ # Global session manager instance
95
+ session_manager = SessionManager()
requirements.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core Gradio and web framework
2
+ gradio==4.44.0
3
+ fastapi==0.104.0
4
+ uvicorn==0.24.0
5
+ httpx==0.25.0
6
+
7
+ # LLM and AI libraries
8
+ llama-index==0.11.0
9
+ llama-index-llms-groq==0.2.0
10
+ llama-index-llms-anthropic==0.3.0
11
+ llama-index-llms-gemini==0.3.0
12
+ google-generativeai==0.5.2
13
+ llama-index-tools-google==0.2.0
14
+
15
+ # Google Calendar and Cloud services
16
+ google-api-python-client==2.100.0
17
+ google-auth==2.23.0
18
+ google-auth-oauthlib==1.1.0
19
+ google-auth-httplib2==0.2.0
20
+ google-cloud-secret-manager==2.20.0
21
+
22
+ # Data validation and parsing
23
+ pydantic==2.4.0
24
+ pydantic-settings==2.0.0
25
+ python-dateutil==2.8.2
26
+ pytz==2023.3
27
+
28
+ # Audio processing and WebRTC support
29
+ numpy>=1.24.0
30
+ scipy>=1.10.0
31
+ librosa>=0.10.0
32
+ soundfile>=0.12.0
33
+
34
+ # Gradio client for external service calls
35
+ gradio-client>=0.7.0
36
+
37
+ # Utilities
38
+ python-dotenv==1.0.0
39
+ python-multipart==0.0.6
40
+ python-jose==3.3.0
41
+ redis==5.0.0
42
+
43
+ # Email support (smtplib is built-in)
test_basic.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Basic test script to verify ChatCal Voice structure.
4
+ Run this to check if all imports work and basic functionality is available.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import asyncio
10
+ from datetime import datetime
11
+
12
+ # Add current directory to path for imports
13
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
+
15
+ def test_imports():
16
+ """Test that all core modules import correctly."""
17
+ print("πŸ” Testing imports...")
18
+
19
+ try:
20
+ from core.config import config
21
+ print("βœ… Config imported successfully")
22
+
23
+ from core.session import SessionData
24
+ print("βœ… SessionData imported successfully")
25
+
26
+ from core.session_manager import SessionManager
27
+ print("βœ… SessionManager imported successfully")
28
+
29
+ from core.llm_provider import get_llm
30
+ print("βœ… LLM Provider imported successfully")
31
+
32
+ from core.chat_agent import ChatCalAgent
33
+ print("βœ… ChatCalAgent imported successfully")
34
+
35
+ from core.calendar_service import CalendarService
36
+ print("βœ… CalendarService imported successfully")
37
+
38
+ from core.audio_handler import AudioHandler
39
+ print("βœ… AudioHandler imported successfully")
40
+
41
+ print("πŸŽ‰ All imports successful!")
42
+ return True
43
+
44
+ except Exception as e:
45
+ print(f"❌ Import error: {e}")
46
+ return False
47
+
48
+ def test_basic_functionality():
49
+ """Test basic functionality of core components."""
50
+ print("\nπŸ§ͺ Testing basic functionality...")
51
+
52
+ try:
53
+ # Test config
54
+ from core.config import config
55
+ print(f"πŸ“‹ App Name: {config.app_name}")
56
+ print(f"πŸ“‹ Default Voice: {config.default_voice}")
57
+
58
+ # Test session creation
59
+ from core.session import SessionData
60
+ session = SessionData(session_id="test_session")
61
+ session.add_message("user", "Hello test")
62
+ print(f"πŸ’¬ Session created with {len(session.conversation_history)} messages")
63
+
64
+ # Test LLM provider
65
+ from core.llm_provider import get_llm
66
+ llm = get_llm()
67
+ print(f"πŸ€– LLM initialized: {type(llm).__name__}")
68
+
69
+ # Test calendar service
70
+ from core.calendar_service import CalendarService
71
+ calendar = CalendarService()
72
+ print(f"πŸ“… Calendar service initialized (demo_mode: {calendar.demo_mode})")
73
+
74
+ # Test audio handler
75
+ from core.audio_handler import AudioHandler
76
+ audio = AudioHandler()
77
+ status = audio.get_audio_status()
78
+ print(f"🎡 Audio handler initialized (demo_mode: {status['demo_mode']})")
79
+
80
+ print("πŸŽ‰ Basic functionality tests passed!")
81
+ return True
82
+
83
+ except Exception as e:
84
+ print(f"❌ Functionality test error: {e}")
85
+ return False
86
+
87
+ async def test_chat_agent():
88
+ """Test the chat agent with a simple message."""
89
+ print("\nπŸ’¬ Testing chat agent...")
90
+
91
+ try:
92
+ from core.chat_agent import ChatCalAgent
93
+ from core.session import SessionData
94
+
95
+ agent = ChatCalAgent()
96
+ session = SessionData(session_id="test_chat")
97
+
98
+ # Test message processing
99
+ response = await agent.process_message("Hello, I'm John", session)
100
+ print(f"πŸ€– Agent response: {response[:100]}...")
101
+
102
+ print(f"πŸ‘€ User info extracted: {session.user_info}")
103
+ print("πŸŽ‰ Chat agent test passed!")
104
+ return True
105
+
106
+ except Exception as e:
107
+ print(f"❌ Chat agent test error: {e}")
108
+ return False
109
+
110
+ def test_gradio_compatibility():
111
+ """Test Gradio compatibility."""
112
+ print("\n🎨 Testing Gradio compatibility...")
113
+
114
+ try:
115
+ import gradio as gr
116
+ print(f"βœ… Gradio version: {gr.__version__}")
117
+
118
+ # Test basic Gradio components
119
+ with gr.Blocks() as demo:
120
+ gr.Markdown("# Test Interface")
121
+ chatbot = gr.Chatbot()
122
+ msg = gr.Textbox(label="Message")
123
+
124
+ print("βœ… Gradio interface creation successful")
125
+ print("πŸŽ‰ Gradio compatibility test passed!")
126
+ return True
127
+
128
+ except Exception as e:
129
+ print(f"❌ Gradio compatibility error: {e}")
130
+ return False
131
+
132
+ async def main():
133
+ """Run all tests."""
134
+ print("πŸš€ ChatCal Voice - Basic Structure Test")
135
+ print("=" * 50)
136
+
137
+ # Set minimal environment for testing
138
+ os.environ.setdefault("GROQ_API_KEY", "test_key")
139
+ os.environ.setdefault("MY_PHONE_NUMBER", "+1-555-123-4567")
140
+ os.environ.setdefault("MY_EMAIL_ADDRESS", "test@example.com")
141
+ os.environ.setdefault("SECRET_KEY", "test_secret")
142
+
143
+ tests = [
144
+ ("Imports", test_imports),
145
+ ("Basic Functionality", test_basic_functionality),
146
+ ("Chat Agent", test_chat_agent),
147
+ ("Gradio Compatibility", test_gradio_compatibility)
148
+ ]
149
+
150
+ passed = 0
151
+ total = len(tests)
152
+
153
+ for test_name, test_func in tests:
154
+ print(f"\n{'='*20} {test_name} {'='*20}")
155
+ try:
156
+ if asyncio.iscoroutinefunction(test_func):
157
+ result = await test_func()
158
+ else:
159
+ result = test_func()
160
+
161
+ if result:
162
+ passed += 1
163
+ except Exception as e:
164
+ print(f"❌ {test_name} failed with exception: {e}")
165
+
166
+ print(f"\n{'='*50}")
167
+ print(f"🏁 Test Results: {passed}/{total} tests passed")
168
+
169
+ if passed == total:
170
+ print("πŸŽ‰ All tests passed! ChatCal Voice structure is ready.")
171
+ print("\nπŸš€ Next steps:")
172
+ print("1. Update STT_SERVICE_URL and TTS_SERVICE_URL in .env")
173
+ print("2. Add your actual API keys")
174
+ print("3. Deploy to Hugging Face Spaces")
175
+ else:
176
+ print("❌ Some tests failed. Check the errors above.")
177
+ return False
178
+
179
+ return True
180
+
181
+ if __name__ == "__main__":
182
+ asyncio.run(main())