Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| π JAY'S MOBILE WASH - GRADIO + FASTAPI VERSION | |
| iPhone Forwarding + AI Assistant - HuggingFace Spaces Optimized | |
| """ | |
| import os | |
| import json | |
| import requests | |
| import logging | |
| import threading | |
| import time | |
| from datetime import datetime, timedelta | |
| from collections import defaultdict | |
| from threading import Lock | |
| import gradio as gr | |
| from fastapi import FastAPI, Request, Form | |
| from fastapi.responses import PlainTextResponse | |
| import uvicorn | |
| # Load environment variables | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| # Configuration | |
| SIGNALWIRE_PROJECT_ID = os.environ.get('SIGNALWIRE_PROJECT_ID', '') | |
| SIGNALWIRE_AUTH_TOKEN = os.environ.get('SIGNALWIRE_AUTH_TOKEN', '') | |
| SIGNALWIRE_SPACE_URL = os.environ.get('SIGNALWIRE_SPACE_URL', '') | |
| DEEPSEEK_API_KEY = os.environ.get('DEEPSEEK_API_KEY', '') | |
| # Business settings | |
| BUSINESS_NAME = "Jay's Mobile Wash" | |
| JAY_PHONE = os.environ.get('JAY_PHONE', '+15622289429') | |
| SIGNALWIRE_PHONE = os.environ.get('SIGNALWIRE_PHONE', '+17149278841') | |
| FORWARDING_ENABLED = True | |
| FORWARDING_DELAY = 20 | |
| # Services and pricing | |
| SERVICES = { | |
| 'basic_wash': {'price': 25.00, 'name': 'Basic Wash', 'description': 'Exterior wash and dry'}, | |
| 'premium_wash': {'price': 45.00, 'name': 'Premium Wash', 'description': 'Wash, wax, interior vacuum'}, | |
| 'full_detail': {'price': 85.00, 'name': 'Full Detail', 'description': 'Complete interior & exterior'}, | |
| 'ceramic_coating': {'price': 150.00, 'name': 'Ceramic Coating', 'description': 'Premium protection'}, | |
| 'headlight_restoration': {'price': 35.00, 'name': 'Headlight Restoration', 'description': 'Clear headlight restoration'} | |
| } | |
| # Initialize SignalWire | |
| signalwire_client = None | |
| VoiceResponse = None | |
| MessagingResponse = None | |
| try: | |
| from signalwire.rest import Client | |
| from signalwire.voice_response import VoiceResponse | |
| from signalwire.messaging_response import MessagingResponse | |
| if SIGNALWIRE_PROJECT_ID and SIGNALWIRE_AUTH_TOKEN and SIGNALWIRE_SPACE_URL: | |
| signalwire_client = Client( | |
| SIGNALWIRE_PROJECT_ID, | |
| SIGNALWIRE_AUTH_TOKEN, | |
| signalwire_space_url=SIGNALWIRE_SPACE_URL | |
| ) | |
| print("β SignalWire initialized") | |
| else: | |
| print("β οΈ SignalWire credentials not configured") | |
| except ImportError: | |
| print("β οΈ SignalWire library not available") | |
| except Exception as e: | |
| print(f"β οΈ SignalWire error: {e}") | |
| # Logging setup | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Thread-safe system state | |
| class SystemState: | |
| def __init__(self): | |
| self._lock = Lock() | |
| self._data = { | |
| 'calls_today': 0, | |
| 'sms_today': 0, | |
| 'calls_forwarded': 0, | |
| 'ai_responses': 0, | |
| 'revenue_today': 0.0, | |
| 'active_calls': {}, | |
| 'active_sms': {}, | |
| 'start_time': datetime.now(), | |
| 'status': 'healthy', | |
| 'call_log': [], | |
| 'sms_log': [] | |
| } | |
| def get(self, key): | |
| with self._lock: | |
| return self._data.get(key, 0) | |
| def set(self, key, value): | |
| with self._lock: | |
| self._data[key] = value | |
| def increment(self, key): | |
| with self._lock: | |
| self._data[key] = self._data.get(key, 0) + 1 | |
| def get_all(self): | |
| with self._lock: | |
| return self._data.copy() | |
| def add_call_log(self, entry): | |
| with self._lock: | |
| self._data['call_log'].insert(0, entry) | |
| if len(self._data['call_log']) > 50: | |
| self._data['call_log'] = self._data['call_log'][:50] | |
| def add_sms_log(self, entry): | |
| with self._lock: | |
| self._data['sms_log'].insert(0, entry) | |
| if len(self._data['sms_log']) > 50: | |
| self._data['sms_log'] = self._data['sms_log'][:50] | |
| # Initialize system state | |
| system_state = SystemState() | |
| # Simple AI processor | |
| class SimpleAI: | |
| def __init__(self): | |
| self.response_cache = {} | |
| def analyze_sentiment(self, text): | |
| """Simple sentiment analysis""" | |
| try: | |
| from textblob import TextBlob | |
| blob = TextBlob(text) | |
| polarity = blob.sentiment.polarity | |
| return { | |
| 'score': polarity, | |
| 'label': 'positive' if polarity > 0.1 else 'negative' if polarity < -0.1 else 'neutral' | |
| } | |
| except: | |
| return {'score': 0.0, 'label': 'neutral'} | |
| def detect_intent(self, text): | |
| """Detect customer intent""" | |
| text_lower = text.lower() | |
| if any(word in text_lower for word in ['price', 'cost', 'how much', 'pricing']): | |
| return 'pricing' | |
| elif any(word in text_lower for word in ['book', 'schedule', 'appointment', 'when']): | |
| return 'booking' | |
| elif any(word in text_lower for word in ['service', 'wash', 'detail', 'clean']): | |
| return 'services' | |
| elif any(word in text_lower for word in ['location', 'where', 'area', 'address']): | |
| return 'location' | |
| elif any(word in text_lower for word in ['urgent', 'emergency', 'asap', 'now']): | |
| return 'urgent' | |
| elif any(word in text_lower for word in ['jay', 'owner', 'human', 'person']): | |
| return 'human_request' | |
| else: | |
| return 'general' | |
| def generate_response(self, user_input, context=None): | |
| """Generate AI response""" | |
| intent = self.detect_intent(user_input) | |
| sentiment = self.analyze_sentiment(user_input) | |
| # Add forwarding acknowledgment if needed | |
| forwarding_ack = "" | |
| if context and context.get('forwarded'): | |
| forwarding_ack = "Thank you for your patience while the call was connected. " | |
| # Generate response based on intent | |
| if intent == 'pricing': | |
| response = f"{forwarding_ack}I'd be happy to help with pricing! Our basic wash is $25, premium wash is $45, and full detail is $85. Which service interests you most?" | |
| elif intent == 'booking': | |
| response = f"{forwarding_ack}I can help you schedule an appointment! We're available Monday-Saturday 8AM-6PM, Sunday 10AM-4PM. What day works best for you?" | |
| elif intent == 'services': | |
| response = f"{forwarding_ack}We offer mobile car detailing services: Basic wash ($25), Premium wash with wax ($45), Full detail ($85), and ceramic coating ($150). Which would you like to know more about?" | |
| elif intent == 'location': | |
| response = f"{forwarding_ack}We provide mobile service within a 15-mile radius of Los Angeles. We come to your location! Where are you located?" | |
| elif intent == 'urgent': | |
| response = f"{forwarding_ack}I understand this is urgent. Let me connect you with Jay directly right away for immediate assistance." | |
| elif intent == 'human_request': | |
| response = f"{forwarding_ack}Let me connect you with Jay personally. He'll be right with you!" | |
| else: | |
| response = f"{forwarding_ack}Hi! Thanks for contacting Jay's Mobile Wash! I can help with pricing, scheduling, or questions about our mobile car detailing services. How can I assist you today?" | |
| # Try DeepSeek if available for better responses | |
| if DEEPSEEK_API_KEY and intent not in ['urgent', 'human_request']: | |
| try: | |
| deepseek_response = self.get_deepseek_response(user_input, context) | |
| if deepseek_response: | |
| response = deepseek_response | |
| except Exception as e: | |
| logger.warning(f"DeepSeek failed: {e}") | |
| return response | |
| def get_deepseek_response(self, prompt, context=None): | |
| """Get response from DeepSeek API""" | |
| try: | |
| headers = { | |
| "Authorization": f"Bearer {DEEPSEEK_API_KEY}", | |
| "Content-Type": "application/json" | |
| } | |
| system_prompt = f"""You are Jay's Mobile Wash AI assistant. Be friendly and professional. | |
| BUSINESS INFO: | |
| - Services: Basic wash ($25), Premium wash ($45), Full detail ($85), Ceramic coating ($150), Headlight restoration ($35) | |
| - Hours: Mon-Sat 8AM-6PM, Sun 10AM-4PM | |
| - Service area: 15 mile radius from Los Angeles | |
| - Owner: Jay, Phone: {JAY_PHONE} | |
| {"IMPORTANT: Customer's call was forwarded from Jay's phone. Be empathetic about any wait time." if context and context.get('forwarded') else ""}""" | |
| data = { | |
| "model": "deepseek-chat", | |
| "messages": [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| "max_tokens": 150, | |
| "temperature": 0.7 | |
| } | |
| response = requests.post( | |
| "https://api.deepseek.com/v1/chat/completions", | |
| headers=headers, | |
| json=data, | |
| timeout=10 | |
| ) | |
| if response.status_code == 200: | |
| result = response.json() | |
| return result['choices'][0]['message']['content'].strip() | |
| except Exception as e: | |
| logger.error(f"DeepSeek error: {e}") | |
| return None | |
| # Initialize AI processor | |
| ai = SimpleAI() | |
| # FastAPI app for webhooks | |
| fastapi_app = FastAPI() | |
| async def handle_voice(request: Request): | |
| """Handle incoming voice calls""" | |
| try: | |
| form = await request.form() | |
| if not signalwire_client or not VoiceResponse: | |
| return PlainTextResponse("Service unavailable", status_code=503) | |
| call_sid = form.get('CallSid') | |
| from_number = form.get('From') | |
| to_number = form.get('To') | |
| forwarded_from = form.get('ForwardedFrom') | |
| # Detect forwarding | |
| is_forwarded = bool(forwarded_from) or (to_number == SIGNALWIRE_PHONE) | |
| # Log the call | |
| call_entry = { | |
| 'time': datetime.now().strftime('%H:%M:%S'), | |
| 'from': from_number, | |
| 'type': 'Forwarded Call' if is_forwarded else 'Direct Call', | |
| 'status': 'Incoming' | |
| } | |
| system_state.add_call_log(call_entry) | |
| # Update stats | |
| system_state.increment('calls_today') | |
| if is_forwarded: | |
| system_state.increment('calls_forwarded') | |
| response = VoiceResponse() | |
| # Greeting | |
| if is_forwarded: | |
| greeting = "Hi! Thanks for calling Jay's Mobile Wash. I'm Jay's AI assistant ready to help while Jay is with another customer. How can I assist you?" | |
| else: | |
| greeting = "Hi! Welcome to Jay's Mobile Wash! I'm your AI assistant ready to help with pricing, scheduling, or any questions. How can I help you?" | |
| response.say(greeting, voice='alice') | |
| # Gather speech | |
| response.gather( | |
| input='speech', | |
| timeout=10, | |
| action=f'/voice/process?call_sid={call_sid}&forwarded={is_forwarded}', | |
| speech_timeout=3 | |
| ) | |
| # Fallback | |
| response.say("I didn't catch that. Let me connect you with Jay.", voice='alice') | |
| response.dial(JAY_PHONE, timeout=30) | |
| return PlainTextResponse(str(response), media_type="application/xml") | |
| except Exception as e: | |
| logger.error(f"Voice error: {e}") | |
| response = VoiceResponse() | |
| response.say("Technical issue. Connecting you with Jay.", voice='alice') | |
| response.dial(JAY_PHONE, timeout=30) | |
| return PlainTextResponse(str(response), media_type="application/xml") | |
| async def process_speech(request: Request): | |
| """Process customer speech""" | |
| try: | |
| form = await request.form() | |
| query_params = request.query_params | |
| call_sid = query_params.get('call_sid') | |
| is_forwarded = query_params.get('forwarded') == 'True' | |
| speech_result = form.get('SpeechResult', '') | |
| confidence = float(form.get('Confidence', '0.0')) | |
| if not speech_result or confidence < 0.4: | |
| response = VoiceResponse() | |
| response.say("I didn't understand clearly. Let me connect you with Jay.", voice='alice') | |
| response.dial(JAY_PHONE, timeout=30) | |
| return PlainTextResponse(str(response), media_type="application/xml") | |
| # Analyze intent | |
| intent = ai.detect_intent(speech_result) | |
| # Check for escalation | |
| if intent in ['urgent', 'human_request'] or 'jay' in speech_result.lower(): | |
| response = VoiceResponse() | |
| response.say("Let me connect you with Jay right away.", voice='alice') | |
| response.dial(JAY_PHONE, timeout=30) | |
| return PlainTextResponse(str(response), media_type="application/xml") | |
| # Generate AI response | |
| context = {'forwarded': is_forwarded, 'call_sid': call_sid} | |
| ai_response = ai.generate_response(speech_result, context) | |
| system_state.increment('ai_responses') | |
| response = VoiceResponse() | |
| response.say(ai_response, voice='alice') | |
| # Continue conversation | |
| response.gather( | |
| input='speech', | |
| timeout=8, | |
| action=f'/voice/process?call_sid={call_sid}&forwarded={is_forwarded}', | |
| speech_timeout=3 | |
| ) | |
| response.say("Thank you for calling Jay's Mobile Wash! Have a great day!", voice='alice') | |
| return PlainTextResponse(str(response), media_type="application/xml") | |
| except Exception as e: | |
| logger.error(f"Speech processing error: {e}") | |
| response = VoiceResponse() | |
| response.say("Technical issue. Connecting with Jay.", voice='alice') | |
| response.dial(JAY_PHONE, timeout=30) | |
| return PlainTextResponse(str(response), media_type="application/xml") | |
| async def handle_sms(request: Request): | |
| """Handle incoming SMS""" | |
| try: | |
| form = await request.form() | |
| message_sid = form.get('MessageSid') | |
| from_number = form.get('From') | |
| message_body = form.get('Body', '').strip() | |
| # Log the SMS | |
| sms_entry = { | |
| 'time': datetime.now().strftime('%H:%M:%S'), | |
| 'from': from_number, | |
| 'message': message_body[:50] + '...' if len(message_body) > 50 else message_body, | |
| 'status': 'Received' | |
| } | |
| system_state.add_sms_log(sms_entry) | |
| system_state.increment('sms_today') | |
| if not message_body: | |
| await send_sms("Hi! How can I help you with Jay's Mobile Wash today?", from_number) | |
| return PlainTextResponse(str(MessagingResponse()) if MessagingResponse else "", media_type="application/xml") | |
| # Analyze message | |
| intent = ai.detect_intent(message_body) | |
| sentiment = ai.analyze_sentiment(message_body) | |
| # Check for escalation | |
| if (intent in ['urgent', 'human_request'] or | |
| sentiment['score'] < -0.6 or | |
| 'jay' in message_body.lower()): | |
| response_text = f"I'll have Jay respond to you personally. For immediate assistance, call {JAY_PHONE}." | |
| await send_sms(response_text, from_number) | |
| return PlainTextResponse(str(MessagingResponse()) if MessagingResponse else "", media_type="application/xml") | |
| # Generate AI response | |
| ai_response = ai.generate_response(message_body, {'sms': True}) | |
| # Optimize for SMS | |
| if len(ai_response) > 320: | |
| ai_response = ai_response[:317] + "..." | |
| system_state.increment('ai_responses') | |
| await send_sms(ai_response, from_number) | |
| return PlainTextResponse(str(MessagingResponse()) if MessagingResponse else "", media_type="application/xml") | |
| except Exception as e: | |
| logger.error(f"SMS error: {e}") | |
| await send_sms("Thanks for your message! Jay will get back to you soon.", from_number) | |
| return PlainTextResponse(str(MessagingResponse()) if MessagingResponse else "", media_type="application/xml") | |
| async def send_sms(message, to_number): | |
| """Send SMS response""" | |
| try: | |
| if signalwire_client: | |
| signalwire_client.messages.create( | |
| body=message, | |
| from_=SIGNALWIRE_PHONE, | |
| to=to_number | |
| ) | |
| # Log sent SMS | |
| sms_entry = { | |
| 'time': datetime.now().strftime('%H:%M:%S'), | |
| 'from': 'AI Assistant', | |
| 'message': message[:50] + '...' if len(message) > 50 else message, | |
| 'status': 'Sent' | |
| } | |
| system_state.add_sms_log(sms_entry) | |
| except Exception as e: | |
| logger.error(f"SMS send error: {e}") | |
| async def health_check(): | |
| """Health check endpoint""" | |
| health = { | |
| 'status': 'healthy', | |
| 'signalwire': bool(signalwire_client), | |
| 'deepseek': bool(DEEPSEEK_API_KEY), | |
| 'uptime': int((datetime.now() - system_state.get_all()['start_time']).total_seconds()), | |
| 'timestamp': datetime.now().isoformat() | |
| } | |
| return health | |
| # Gradio Interface Functions | |
| def get_dashboard_data(): | |
| """Get current dashboard data""" | |
| stats = system_state.get_all() | |
| uptime = datetime.now() - stats['start_time'] | |
| dashboard_html = f""" | |
| <div style="font-family: Arial, sans-serif; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; color: white;"> | |
| <h1 style="text-align: center; margin-bottom: 30px;">π {BUSINESS_NAME} - AI Dashboard</h1> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;"> | |
| <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 10px; text-align: center;"> | |
| <h2 style="color: #4facfe; font-size: 2em; margin: 0;">{stats['calls_today']}</h2> | |
| <p style="margin: 5px 0 0 0;">π Calls Today</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 10px; text-align: center;"> | |
| <h2 style="color: #4facfe; font-size: 2em; margin: 0;">{stats['sms_today']}</h2> | |
| <p style="margin: 5px 0 0 0;">π± SMS Today</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 10px; text-align: center;"> | |
| <h2 style="color: #4facfe; font-size: 2em; margin: 0;">{stats['calls_forwarded']}</h2> | |
| <p style="margin: 5px 0 0 0;">π Calls Forwarded</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 10px; text-align: center;"> | |
| <h2 style="color: #4facfe; font-size: 2em; margin: 0;">{stats['ai_responses']}</h2> | |
| <p style="margin: 5px 0 0 0;">π€ AI Responses</p> | |
| </div> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; margin-bottom: 20px;"> | |
| <h3 style="color: #4facfe; margin-bottom: 15px;">π iPhone Forwarding Status</h3> | |
| <p><strong>Jay's iPhone:</strong> {JAY_PHONE}</p> | |
| <p><strong>AI Assistant Number:</strong> {SIGNALWIRE_PHONE}</p> | |
| <p><strong>Forwarding:</strong> {'β Enabled' if FORWARDING_ENABLED else 'β Disabled'}</p> | |
| <p><strong>Delay:</strong> {FORWARDING_DELAY} seconds</p> | |
| <p><strong>SignalWire:</strong> {'β Connected' if signalwire_client else 'β Not configured'}</p> | |
| <p><strong>DeepSeek AI:</strong> {'β Connected' if DEEPSEEK_API_KEY else 'β οΈ Fallback mode'}</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px;"> | |
| <h3 style="color: #4facfe; margin-bottom: 15px;">πΌ Services & Pricing</h3> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;"> | |
| """ | |
| for service_id, service in SERVICES.items(): | |
| dashboard_html += f""" | |
| <div style="background: rgba(79, 172, 254, 0.2); padding: 15px; border-radius: 8px; text-align: center;"> | |
| <h4 style="margin: 0 0 5px 0; color: white;">{service['name']}</h4> | |
| <p style="margin: 0 0 10px 0; font-size: 0.9em; opacity: 0.8;">{service['description']}</p> | |
| <p style="margin: 0; font-size: 1.5em; font-weight: bold; color: #00ff88;">${service['price']:.0f}</p> | |
| </div> | |
| """ | |
| dashboard_html += """ | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return dashboard_html | |
| def get_call_log(): | |
| """Get recent call log""" | |
| stats = system_state.get_all() | |
| call_log = stats.get('call_log', []) | |
| if not call_log: | |
| return "No calls logged yet." | |
| log_html = """ | |
| <div style="font-family: Arial, sans-serif;"> | |
| <h3>π Recent Calls</h3> | |
| <div style="max-height: 300px; overflow-y: auto;"> | |
| """ | |
| for call in call_log[:10]: # Show last 10 calls | |
| log_html += f""" | |
| <div style="background: #f5f5f5; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #4facfe;"> | |
| <strong>{call['time']}</strong> | {call['from']} | {call['type']} | {call['status']} | |
| </div> | |
| """ | |
| log_html += "</div></div>" | |
| return log_html | |
| def get_sms_log(): | |
| """Get recent SMS log""" | |
| stats = system_state.get_all() | |
| sms_log = stats.get('sms_log', []) | |
| if not sms_log: | |
| return "No SMS logged yet." | |
| log_html = """ | |
| <div style="font-family: Arial, sans-serif;"> | |
| <h3>π± Recent SMS</h3> | |
| <div style="max-height: 300px; overflow-y: auto;"> | |
| """ | |
| for sms in sms_log[:10]: # Show last 10 SMS | |
| log_html += f""" | |
| <div style="background: #f5f5f5; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #00ff88;"> | |
| <strong>{sms['time']}</strong> | {sms['from']}<br> | |
| <em>{sms['message']}</em> | {sms['status']} | |
| </div> | |
| """ | |
| log_html += "</div></div>" | |
| return log_html | |
| def test_ai_response(message): | |
| """Test AI response generation""" | |
| if not message.strip(): | |
| return "Please enter a message to test." | |
| try: | |
| intent = ai.detect_intent(message) | |
| sentiment = ai.analyze_sentiment(message) | |
| response = ai.generate_response(message, {'test': True}) | |
| result = f""" | |
| <div style="font-family: Arial, sans-serif; padding: 15px; background: #f9f9f9; border-radius: 10px;"> | |
| <h3>AI Response Test</h3> | |
| <p><strong>Customer Message:</strong> {message}</p> | |
| <p><strong>Detected Intent:</strong> {intent}</p> | |
| <p><strong>Sentiment:</strong> {sentiment['label']} ({sentiment['score']:.2f})</p> | |
| <div style="background: #e3f2fd; padding: 10px; border-radius: 5px; margin-top: 10px;"> | |
| <strong>AI Response:</strong><br> | |
| {response} | |
| </div> | |
| </div> | |
| """ | |
| return result | |
| except Exception as e: | |
| return f"Error testing AI response: {e}" | |
| # Start FastAPI in background thread | |
| def run_fastapi(): | |
| uvicorn.run(fastapi_app, host="0.0.0.0", port=8000, log_level="info") | |
| fastapi_thread = threading.Thread(target=run_fastapi, daemon=True) | |
| fastapi_thread.start() | |
| # Create Gradio Interface | |
| with gr.Blocks( | |
| title=f"{BUSINESS_NAME} - AI Dashboard", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .gradio-container { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| """ | |
| ) as interface: | |
| gr.Markdown(f""" | |
| # π {BUSINESS_NAME} - iPhone Forwarding AI System | |
| **Real-time dashboard for your AI assistant with iPhone call forwarding** | |
| π **Jay's iPhone:** {JAY_PHONE} | |
| π€ **AI Assistant:** {SIGNALWIRE_PHONE} | |
| π **Forwarding:** {'β Enabled' if FORWARDING_ENABLED else 'β Disabled'} | |
| """) | |
| with gr.Tabs(): | |
| with gr.Tab("π Dashboard"): | |
| dashboard_display = gr.HTML(value=get_dashboard_data()) | |
| refresh_btn = gr.Button("π Refresh Dashboard", variant="primary") | |
| refresh_btn.click(fn=get_dashboard_data, outputs=dashboard_display) | |
| with gr.Tab("π Call Log"): | |
| call_log_display = gr.HTML(value=get_call_log()) | |
| refresh_calls_btn = gr.Button("π Refresh Calls", variant="secondary") | |
| refresh_calls_btn.click(fn=get_call_log, outputs=call_log_display) | |
| with gr.Tab("π± SMS Log"): | |
| sms_log_display = gr.HTML(value=get_sms_log()) | |
| refresh_sms_btn = gr.Button("π Refresh SMS", variant="secondary") | |
| refresh_sms_btn.click(fn=get_sms_log, outputs=sms_log_display) | |
| with gr.Tab("π§ͺ Test AI"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| test_message = gr.Textbox( | |
| label="Test Message", | |
| placeholder="Enter a customer message to test AI response...", | |
| lines=3 | |
| ) | |
| test_btn = gr.Button("π€ Test AI Response", variant="primary") | |
| with gr.Column(): | |
| test_result = gr.HTML(label="AI Response") | |
| test_btn.click(fn=test_ai_response, inputs=test_message, outputs=test_result) | |
| with gr.Tab("βΉοΈ Setup Info"): | |
| gr.Markdown(f""" | |
| ## π§ Webhook Configuration | |
| **Set these URLs in your SignalWire dashboard:** | |
| - **Voice Webhook:** `https://YOUR-SPACE-NAME.hf.space/voice/incoming` | |
| - **SMS Webhook:** `https://YOUR-SPACE-NAME.hf.space/sms/incoming` | |
| ## π± iPhone Setup | |
| **On Jay's iPhone:** | |
| 1. Go to **Settings** β **Phone** β **Call Forwarding** | |
| 2. Turn **ON** Call Forwarding | |
| 3. Set forward number to: `{SIGNALWIRE_PHONE}` | |
| 4. Choose: Forward when **unanswered** after {FORWARDING_DELAY} seconds | |
| ## π― How It Works | |
| 1. Customer calls Jay's iPhone: `{JAY_PHONE}` | |
| 2. iPhone rings for {FORWARDING_DELAY} seconds | |
| 3. If no answer β forwards to AI: `{SIGNALWIRE_PHONE}` | |
| 4. AI handles call with forwarding awareness | |
| 5. Can escalate back to Jay if needed | |
| ## π System Status | |
| - **SignalWire:** {'β Connected' if signalwire_client else 'β Not configured'} | |
| - **DeepSeek AI:** {'β Connected' if DEEPSEEK_API_KEY else 'β οΈ Using fallback responses'} | |
| - **FastAPI Webhooks:** β Running on port 8000 | |
| - **Health Check:** [/health](./health) | |
| """) | |
| # Auto-refresh every 30 seconds | |
| interface.load(fn=get_dashboard_data, outputs=dashboard_display, every=30) | |
| interface.load(fn=get_call_log, outputs=call_log_display, every=30) | |
| interface.load(fn=get_sms_log, outputs=sms_log_display, every=30) | |
| # Launch the interface | |
| if __name__ == "__main__": | |
| print("\n" + "="*60) | |
| print("π JAY'S MOBILE WASH AI SYSTEM") | |
| print("="*60) | |
| print(f"π Jay's Phone: {JAY_PHONE}") | |
| print(f"π€ AI Phone: {SIGNALWIRE_PHONE}") | |
| print(f"π Forwarding: {'β Enabled' if FORWARDING_ENABLED else 'β Disabled'}") | |
| print(f"π SignalWire: {'β Connected' if signalwire_client else 'β Not configured'}") | |
| print(f"π§ DeepSeek: {'β Connected' if DEEPSEEK_API_KEY else 'β Not configured'}") | |
| print("="*60) | |
| print("π Starting Gradio interface on port 7860...") | |
| print("π FastAPI webhooks running on port 8000") | |
| print("="*60) | |
| interface.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| show_error=True | |
| ) |