Spaces:
Sleeping
Sleeping
| from flask import Flask, render_template, request, jsonify, url_for | |
| import os | |
| import sys | |
| import time | |
| import json | |
| from dotenv import load_dotenv | |
| import logging | |
| from threading import Thread, Lock | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from collections import deque | |
| import nltk | |
| import requests | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| # Load environment variables | |
| load_dotenv() | |
| # Debug: Log environment variable status | |
| logging.info("=" * 60) | |
| logging.info("ENVIRONMENT VARIABLES CHECK") | |
| logging.info("=" * 60) | |
| deepseek_key = os.environ.get('DEEPSEEK_API_KEY') | |
| missing_deepseek_key = False | |
| if deepseek_key: | |
| logging.info(f"✓ DEEPSEEK_API_KEY found (length: {len(deepseek_key)} chars)") | |
| logging.info(f" First 10 chars: {deepseek_key[:10]}...") | |
| else: | |
| missing_deepseek_key = True | |
| logging.error("=" * 60) | |
| logging.error("✗ DEEPSEEK_API_KEY not found in environment!") | |
| logging.error("=" * 60) | |
| logging.error("") | |
| logging.error("The DEEPSEEK_API_KEY environment variable is required for full functionality.") | |
| logging.error("") | |
| logging.error("For Hugging Face Spaces:") | |
| logging.error(" 1. Go to Settings → Repository secrets") | |
| logging.error(" 2. Click 'New secret'") | |
| logging.error(" 3. Name: DEEPSEEK_API_KEY") | |
| logging.error(" 4. Value: [Your DeepSeek API key]") | |
| logging.error(" 5. Get a key from: https://platform.deepseek.com/") | |
| logging.error("") | |
| logging.error("For local development:") | |
| logging.error(" 1. Copy .env.example to .env") | |
| logging.error(" 2. Add your API key to the .env file") | |
| logging.error("") | |
| logging.error("Available env vars starting with 'DEEPSEEK': " + | |
| str([k for k in os.environ.keys() if 'DEEPSEEK' in k.upper()])) | |
| logging.error("=" * 60) | |
| logging.info("=" * 60) | |
| # Download required NLTK data on startup | |
| def download_nltk_data(): | |
| """Download all required NLTK data for the application""" | |
| required_data = ['punkt', 'vader_lexicon'] | |
| for data_name in required_data: | |
| try: | |
| nltk.data.find(f'tokenizers/{data_name}' if data_name == 'punkt' else f'sentiment/{data_name}.zip') | |
| logging.info(f"NLTK data '{data_name}' already downloaded") | |
| except LookupError: | |
| logging.info(f"Downloading NLTK data: {data_name}") | |
| nltk.download(data_name, quiet=True) | |
| logging.info(f"Successfully downloaded NLTK data: {data_name}") | |
| # Download NLTK data before app initialization | |
| download_nltk_data() | |
| # Initialize Flask app | |
| app = Flask(__name__, static_folder='static', template_folder='templates') | |
| # Global variables to hold components | |
| galatea_ai = None | |
| dialogue_engine = None | |
| avatar_engine = None | |
| is_initialized = False | |
| initializing = False | |
| deepseek_initialized = False | |
| max_init_retries = 3 | |
| current_init_retry = 0 | |
| # Random numbers queue for /api/avatar endpoint | |
| random_numbers_queue = deque(maxlen=100) | |
| random_queue_lock = Lock() | |
| random_filling = False | |
| # Check for required environment variables | |
| required_env_vars = ['DEEPSEEK_API_KEY'] | |
| missing_vars = [var for var in required_env_vars if not os.environ.get(var)] | |
| if missing_vars: | |
| logging.error(f"Missing required environment variables: {', '.join(missing_vars)}") | |
| logging.error("Please set these in your .env file or environment") | |
| print(f"⚠️ Missing required environment variables: {', '.join(missing_vars)}") | |
| print("Please set these in your .env file or environment") | |
| def initialize_deepseek(): | |
| """Initialize DeepSeek API specifically""" | |
| global deepseek_initialized | |
| if not galatea_ai: | |
| logging.warning("Cannot initialize DeepSeek: GalateaAI instance not created yet") | |
| return False | |
| if missing_deepseek_key: | |
| logging.error("Cannot initialize DeepSeek: DEEPSEEK_API_KEY is missing") | |
| return False | |
| try: | |
| # Check for DEEPSEEK_API_KEY | |
| if not os.environ.get('DEEPSEEK_API_KEY'): | |
| logging.error("DEEPSEEK_API_KEY not found in environment variables") | |
| return False | |
| # Check if DeepSeek agent is ready (initialization happens automatically in GalateaAI.__init__) | |
| deepseek_success = hasattr(galatea_ai, 'deepseek_agent') and galatea_ai.deepseek_agent.is_ready() | |
| if deepseek_success: | |
| deepseek_initialized = True | |
| logging.info("DeepSeek API initialized successfully") | |
| return True | |
| else: | |
| logging.error("Failed to initialize DeepSeek API") | |
| return False | |
| except Exception as e: | |
| logging.error(f"Error initializing DeepSeek API: {e}") | |
| return False | |
| # Global status tracking for parallel initialization | |
| init_status = { | |
| 'json_memory': {'ready': False, 'error': None}, | |
| 'sentiment_analyzer': {'ready': False, 'error': None}, | |
| 'deepseek_api': {'ready': False, 'error': None}, | |
| 'inflection_api': {'ready': False, 'error': None}, | |
| } | |
| def initialize_json_memory(): | |
| """Initialize JSON memory database""" | |
| try: | |
| logging.info("🔄 [JSON Memory] Initializing...") | |
| print("🔄 [JSON Memory] Initializing...") | |
| json_path = "./memory.json" | |
| if os.path.exists(json_path): | |
| with open(json_path, 'r', encoding='utf-8') as f: | |
| memory = json.load(f) | |
| logging.info(f"✓ [JSON Memory] Loaded {len(memory)} entries") | |
| print(f"✓ [JSON Memory] Loaded {len(memory)} entries") | |
| else: | |
| with open(json_path, 'w', encoding='utf-8') as f: | |
| json.dump({}, f) | |
| logging.info("✓ [JSON Memory] Created new database") | |
| print("✓ [JSON Memory] Created new database") | |
| init_status['json_memory']['ready'] = True | |
| return True | |
| except Exception as e: | |
| error_msg = f"JSON memory initialization failed: {e}" | |
| logging.error(f"✗ [JSON Memory] {error_msg}") | |
| print(f"✗ [JSON Memory] {error_msg}") | |
| init_status['json_memory']['error'] = str(e) | |
| return False | |
| def initialize_sentiment_analyzer(): | |
| """Initialize sentiment analyzer""" | |
| try: | |
| logging.info("🔄 [Sentiment Analyzer] Starting initialization...") | |
| print("🔄 [Sentiment Analyzer] Starting initialization...") | |
| from agents.sentiment_agent import SentimentAgent | |
| agent = SentimentAgent() | |
| if agent.azure_agent.is_ready(): | |
| logging.info("✓ [Sentiment Analyzer] Azure Text Analytics ready") | |
| print("✓ [Sentiment Analyzer] Azure Text Analytics ready") | |
| else: | |
| logging.info("✓ [Sentiment Analyzer] Using fallback (NLTK VADER)") | |
| print("✓ [Sentiment Analyzer] Using fallback (NLTK VADER)") | |
| init_status['sentiment_analyzer']['ready'] = True | |
| return True | |
| except Exception as e: | |
| error_msg = f"Sentiment analyzer initialization failed: {e}" | |
| logging.warning(f"⚠ [Sentiment Analyzer] {error_msg} - using fallback") | |
| print(f"⚠ [Sentiment Analyzer] Using fallback") | |
| init_status['sentiment_analyzer']['error'] = str(e) | |
| init_status['sentiment_analyzer']['ready'] = True | |
| return True | |
| def validate_deepseek_api(): | |
| """Validate DeepSeek API key""" | |
| try: | |
| logging.info("🔄 [DeepSeek API] Validating API key...") | |
| print("🔄 [DeepSeek API] Validating API key...") | |
| api_key = os.getenv("DEEPSEEK_API_KEY") | |
| if not api_key: | |
| logging.warning("⚠ [DeepSeek API] API key not found") | |
| print("⚠ [DeepSeek API] API key not found") | |
| init_status['deepseek_api']['ready'] = False | |
| return False | |
| try: | |
| from llm_wrapper import LLMWrapper | |
| from config import MODEL_CONFIG | |
| # Get model from config | |
| deepseek_config = MODEL_CONFIG.get('deepseek', {}) if MODEL_CONFIG else {} | |
| deepseek_model = deepseek_config.get('model', 'deepseek-reasoner') | |
| wrapper = LLMWrapper(deepseek_model=deepseek_model) | |
| response = wrapper.call_deepseek( | |
| messages=[{"role": "user", "content": "test"}], | |
| max_tokens=5 | |
| ) | |
| if response: | |
| logging.info("✓ [DeepSeek API] API key validated") | |
| print("✓ [DeepSeek API] API key validated") | |
| init_status['deepseek_api']['ready'] = True | |
| return True | |
| else: | |
| logging.warning("⚠ [DeepSeek API] Validation failed - no response") | |
| print("⚠ [DeepSeek API] Validation failed - key exists, may be network issue") | |
| return False | |
| except Exception as e: | |
| error_msg = str(e) | |
| error_type = type(e).__name__ | |
| # Check status code from exception if available | |
| status_code = getattr(e, 'status_code', None) | |
| response_text = getattr(e, 'response_text', error_msg) | |
| # Log the full error for debugging | |
| logging.error(f"✗ [DeepSeek API] Validation exception: {error_type}: {error_msg}") | |
| print(f"✗ [DeepSeek API] Validation exception: {error_type}: {error_msg}") | |
| # Check if it's a 404 (model not found) - this is a real error | |
| if status_code == 404 or '404' in error_msg or 'NOT_FOUND' in error_msg: | |
| logging.error(f"✗ [DeepSeek API] Model not found: {error_msg}") | |
| print(f"✗ [DeepSeek API] Model not found - check models.yaml configuration") | |
| init_status['deepseek_api']['error'] = error_msg | |
| return False | |
| # Check if it's a 429 (rate limit/quota exceeded) - API key is valid, just quota issue | |
| elif status_code == 429 or '429' in error_msg or 'RESOURCE_EXHAUSTED' in error_msg or 'quota' in response_text.lower(): | |
| logging.info("ℹ️ [DeepSeek API] Rate limit/quota exceeded (API key is valid)") | |
| print("ℹ️ [DeepSeek API] Rate limit/quota exceeded (API key is valid, will work when quota resets)") | |
| init_status['deepseek_api']['ready'] = True # Key is valid, just quota issue | |
| init_status['deepseek_api']['error'] = "Rate limit/quota exceeded" | |
| return True # Don't fail initialization - key is valid | |
| # Check if it's an authentication error (401) - invalid API key | |
| elif status_code == 401 or '401' in error_msg or 'unauthorized' in error_msg.lower() or 'authentication' in error_msg.lower(): | |
| logging.error(f"✗ [DeepSeek API] Authentication failed - invalid API key: {error_msg}") | |
| print(f"✗ [DeepSeek API] Authentication failed - check your DEEPSEEK_API_KEY") | |
| init_status['deepseek_api']['error'] = f"Authentication failed: {error_msg}" | |
| return False | |
| else: | |
| logging.warning(f"⚠ [DeepSeek API] Validation failed: {e}") | |
| print(f"⚠ [DeepSeek API] Validation failed - key exists, may be network issue. Error: {error_msg}") | |
| init_status['deepseek_api']['ready'] = False | |
| init_status['deepseek_api']['error'] = error_msg | |
| return False | |
| except Exception as e: | |
| error_msg = f"DeepSeek API validation failed: {e}" | |
| logging.error(f"✗ [DeepSeek API] {error_msg}") | |
| print(f"✗ [DeepSeek API] {error_msg}") | |
| init_status['deepseek_api']['error'] = str(e) | |
| return False | |
| def validate_inflection_api(): | |
| """Validate Inflection AI API key""" | |
| try: | |
| logging.info("🔄 [Inflection AI] Validating API key...") | |
| print("🔄 [Inflection AI] Validating API key...") | |
| api_key = os.getenv("INFLECTION_AI_API_KEY") | |
| if not api_key: | |
| logging.warning("⚠ [Inflection AI] API key not found") | |
| print("⚠ [Inflection AI] API key not found") | |
| init_status['inflection_api']['ready'] = False | |
| return False | |
| url = "https://api.inflection.ai/external/api/inference" | |
| headers = { | |
| "Authorization": f"Bearer {api_key}", | |
| "Content-Type": "application/json" | |
| } | |
| data = { | |
| "context": [{"text": "test", "type": "Human"}], | |
| "config": "Pi-3.1" | |
| } | |
| response = requests.post(url, headers=headers, json=data, timeout=10) | |
| if response.status_code == 200: | |
| logging.info("✓ [Inflection AI] API key validated") | |
| print("✓ [Inflection AI] API key validated") | |
| init_status['inflection_api']['ready'] = True | |
| return True | |
| else: | |
| logging.warning(f"⚠ [Inflection AI] Validation failed: {response.status_code}") | |
| print(f"⚠ [Inflection AI] Validation failed: {response.status_code}") | |
| init_status['inflection_api']['ready'] = False | |
| return False | |
| except Exception as e: | |
| error_msg = f"Inflection AI validation failed: {e}" | |
| logging.warning(f"⚠ [Inflection AI] {error_msg}") | |
| print(f"⚠ [Inflection AI] {error_msg}") | |
| init_status['inflection_api']['ready'] = False | |
| return False | |
| def run_parallel_initialization(): | |
| """Run all initialization steps in parallel""" | |
| start_time = time.time() | |
| logging.info("=" * 70) | |
| logging.info("GALATEA AI PARALLEL INITIALIZATION") | |
| logging.info("=" * 70) | |
| logging.info("Starting parallel initialization of all components...") | |
| logging.info("") | |
| print("=" * 70) | |
| print("GALATEA AI PARALLEL INITIALIZATION") | |
| print("=" * 70) | |
| print("Starting parallel initialization of all components...") | |
| print("") | |
| tasks = [ | |
| ("JSON Memory", initialize_json_memory), | |
| ("Sentiment Analyzer", initialize_sentiment_analyzer), | |
| ("DeepSeek API", validate_deepseek_api), | |
| ("Inflection AI", validate_inflection_api), | |
| ] | |
| completed_count = 0 | |
| total_tasks = len(tasks) | |
| with ThreadPoolExecutor(max_workers=5) as executor: | |
| futures = {executor.submit(task[1]): task[0] for task in tasks} | |
| for future in as_completed(futures): | |
| task_name = futures[future] | |
| completed_count += 1 | |
| try: | |
| result = future.result() | |
| if result: | |
| logging.info(f"✅ [{task_name}] Completed successfully") | |
| print(f"✅ [{task_name}] Completed successfully") | |
| else: | |
| logging.warning(f"⚠️ [{task_name}] Completed with warnings") | |
| print(f"⚠️ [{task_name}] Completed with warnings") | |
| except Exception as e: | |
| logging.error(f"❌ [{task_name}] Failed: {e}") | |
| print(f"❌ [{task_name}] Failed: {e}") | |
| # Verify all tasks completed | |
| if completed_count < total_tasks: | |
| logging.warning(f"⚠️ Only {completed_count}/{total_tasks} tasks completed. Some tasks may have timed out or failed silently.") | |
| print(f"⚠️ Only {completed_count}/{total_tasks} tasks completed. Some tasks may have timed out or failed silently.") | |
| else: | |
| logging.info(f"✓ All {total_tasks} tasks completed") | |
| print(f"✓ All {total_tasks} tasks completed") | |
| elapsed_time = time.time() - start_time | |
| logging.info("") | |
| logging.info("=" * 70) | |
| logging.info("INITIALIZATION SUMMARY") | |
| logging.info("=" * 70) | |
| print("") | |
| print("=" * 70) | |
| print("INITIALIZATION SUMMARY") | |
| print("=" * 70) | |
| all_ready = True | |
| critical_ready = True | |
| for component, status in init_status.items(): | |
| status_icon = "✓" if status['ready'] else "✗" | |
| error_info = f" - {status['error']}" if status['error'] else "" | |
| status_msg = f"{status_icon} {component.upper()}: {'READY' if status['ready'] else 'FAILED'}{error_info}" | |
| logging.info(status_msg) | |
| print(status_msg) | |
| if component in ['json_memory', 'sentiment_analyzer', 'deepseek_api']: | |
| if not status['ready']: | |
| critical_ready = False | |
| if not status['ready']: | |
| all_ready = False | |
| logging.info("") | |
| logging.info(f"⏱️ Total initialization time: {elapsed_time:.2f} seconds") | |
| logging.info("") | |
| print("") | |
| print(f"⏱️ Total initialization time: {elapsed_time:.2f} seconds") | |
| print("") | |
| if critical_ready: | |
| if all_ready: | |
| logging.info("✅ ALL COMPONENTS INITIALIZED SUCCESSFULLY") | |
| logging.info("🎉 Galatea AI is ready to use!") | |
| print("✅ ALL COMPONENTS INITIALIZED SUCCESSFULLY") | |
| print("🎉 Galatea AI is ready to use!") | |
| return True | |
| else: | |
| logging.info("⚠️ CRITICAL COMPONENTS READY (some optional components failed)") | |
| logging.info("✅ Galatea AI is ready to use (with limited features)") | |
| print("⚠️ CRITICAL COMPONENTS READY (some optional components failed)") | |
| print("✅ Galatea AI is ready to use (with limited features)") | |
| return True | |
| else: | |
| logging.error("❌ CRITICAL COMPONENTS FAILED") | |
| logging.error("⚠️ Galatea AI may not function properly") | |
| print("❌ CRITICAL COMPONENTS FAILED") | |
| print("⚠️ Galatea AI may not function properly") | |
| return False | |
| def initialize_components(): | |
| """Initialize Galatea components""" | |
| global galatea_ai, dialogue_engine, avatar_engine, is_initialized, initializing | |
| global current_init_retry, deepseek_initialized | |
| if initializing or is_initialized: | |
| return | |
| if missing_deepseek_key: | |
| logging.error("Initialization aborted: DEEPSEEK_API_KEY missing") | |
| return | |
| initializing = True | |
| logging.info("Starting to initialize Galatea components...") | |
| try: | |
| # Import here to avoid circular imports and ensure errors are caught | |
| from galatea_ai import GalateaAI | |
| from dialogue import DialogueEngine | |
| from avatar import AvatarEngine | |
| # Initialize components | |
| logging.info("=" * 60) | |
| logging.info("INITIALIZING GALATEA AI SYSTEM") | |
| logging.info("=" * 60) | |
| galatea_ai = GalateaAI() | |
| dialogue_engine = DialogueEngine(galatea_ai) | |
| avatar_engine = AvatarEngine() | |
| avatar_engine.update_avatar(galatea_ai.emotional_state) | |
| # Check if all components are fully initialized | |
| init_status = galatea_ai.get_initialization_status() | |
| logging.info("=" * 60) | |
| logging.info("INITIALIZATION STATUS") | |
| logging.info("=" * 60) | |
| logging.info(f"Memory System (JSON): {init_status['memory_system']}") | |
| logging.info(f"Sentiment Analyzer: {init_status['sentiment_analyzer']}") | |
| logging.info(f"Models Ready: {init_status['models']}") | |
| logging.info(f" - DeepSeek available: {init_status['deepseek_available']}") | |
| logging.info(f" - Inflection AI available: {init_status['inflection_ai_available']}") | |
| logging.info(f"API Keys Valid: {init_status['api_keys']}") | |
| logging.info(f"Fully Initialized: {init_status['fully_initialized']}") | |
| logging.info("=" * 60) | |
| # CRITICAL: Only mark as initialized if ALL components are ready | |
| # If any component fails, EXIT the application immediately | |
| if init_status['fully_initialized']: | |
| is_initialized = True | |
| logging.info("✓ Galatea AI system fully initialized and ready") | |
| logging.info(f"Emotions initialized: {galatea_ai.emotional_state}") | |
| logging.info("=" * 60) | |
| else: | |
| logging.error("=" * 60) | |
| logging.error("❌ INITIALIZATION FAILED - EXITING APPLICATION") | |
| logging.error("=" * 60) | |
| logging.error("One or more critical components failed to initialize:") | |
| if not init_status['memory_system']: | |
| logging.error(" ✗ Memory System (JSON) - FAILED") | |
| if not init_status['sentiment_analyzer']: | |
| logging.error(" ✗ Sentiment Analyzer - FAILED") | |
| if not init_status['models']: | |
| logging.error(" ✗ Models - FAILED") | |
| if not init_status['api_keys']: | |
| logging.error(" ✗ API Keys - FAILED") | |
| logging.error("=" * 60) | |
| logging.error("EXITING APPLICATION - All systems must be operational") | |
| logging.error("=" * 60) | |
| import sys | |
| sys.exit(1) # Exit immediately - no retries, no partial functionality | |
| except Exception as e: | |
| logging.error("=" * 60) | |
| logging.error(f"❌ CRITICAL ERROR INITIALIZING GALATEA: {e}") | |
| logging.error("=" * 60) | |
| logging.error("EXITING APPLICATION - Cannot continue with initialization failure") | |
| logging.error("=" * 60) | |
| print(f"CRITICAL ERROR: {e}") | |
| print("Application exiting due to initialization failure") | |
| import sys | |
| sys.exit(1) # Exit immediately - no retries | |
| finally: | |
| initializing = False | |
| def home(): | |
| # Add error handling for template rendering | |
| try: | |
| # Start component initialization if not already started | |
| if not is_initialized and not initializing and not missing_deepseek_key: | |
| Thread(target=initialize_components, daemon=True).start() | |
| return render_template('index.html') | |
| except Exception as e: | |
| logging.error(f"Error rendering template: {e}") | |
| return f"Error loading the application: {e}. Make sure templates/index.html exists.", 500 | |
| def chat(): | |
| # CRITICAL: Do not allow chat if system is not fully initialized | |
| if not is_initialized: | |
| return jsonify({ | |
| 'error': 'System is not initialized yet. Please wait for initialization to complete.', | |
| 'is_initialized': False, | |
| 'status': 'initializing' | |
| }), 503 # Service Unavailable | |
| # Check if API key is missing | |
| if missing_deepseek_key: | |
| return jsonify({ | |
| 'error': 'DEEPSEEK_API_KEY is missing. Chat is unavailable.', | |
| 'status': 'missing_deepseek_key', | |
| 'is_initialized': False | |
| }), 503 | |
| data = request.json | |
| user_input = data.get('message', '') | |
| if not user_input: | |
| return jsonify({'error': 'No message provided'}), 400 | |
| try: | |
| # Process the message through Galatea | |
| response = dialogue_engine.get_response(user_input) | |
| # CRITICAL: If response is None, Pi-3.1 failed - exit application | |
| if response is None: | |
| error_msg = "CRITICAL: Pi-3.1 (PHI) model failed to generate response. Application cannot continue." | |
| logging.error("=" * 60) | |
| logging.error(error_msg) | |
| logging.error("=" * 60) | |
| import sys | |
| sys.exit(1) # Exit immediately | |
| # Update avatar | |
| avatar_engine.update_avatar(galatea_ai.emotional_state) | |
| avatar_shape = avatar_engine.avatar_model | |
| # Get emotional state for frontend | |
| emotions = {k: round(v, 2) for k, v in galatea_ai.emotional_state.items()} | |
| logging.info(f"Chat response: {response}, avatar: {avatar_shape}, emotions: {emotions}") | |
| return jsonify({ | |
| 'response': response, | |
| 'avatar_shape': avatar_shape, | |
| 'emotions': emotions, | |
| 'is_initialized': True | |
| }) | |
| except RuntimeError as e: | |
| # CRITICAL: RuntimeError means a system failure - exit application | |
| error_msg = f"CRITICAL SYSTEM FAILURE: {e}" | |
| logging.error("=" * 60) | |
| logging.error(error_msg) | |
| logging.error("EXITING APPLICATION") | |
| logging.error("=" * 60) | |
| import sys | |
| sys.exit(1) # Exit immediately | |
| except Exception as e: | |
| # Any other exception is also critical - exit application | |
| error_msg = f"CRITICAL ERROR processing chat: {e}" | |
| logging.error("=" * 60) | |
| logging.error(error_msg) | |
| logging.error("EXITING APPLICATION") | |
| logging.error("=" * 60) | |
| import sys | |
| sys.exit(1) # Exit immediately | |
| # Import Azure Text Analytics with fallback to NLTK VADER | |
| try: | |
| from azure.ai.textanalytics import TextAnalyticsClient | |
| from azure.core.credentials import AzureKeyCredential | |
| azure_available = True | |
| except ImportError: | |
| azure_available = False | |
| logging.warning("Azure Text Analytics not installed, will use NLTK VADER for sentiment analysis") | |
| # Set up NLTK VADER as fallback | |
| import nltk | |
| from nltk.sentiment.vader import SentimentIntensityAnalyzer | |
| # Download VADER lexicon on first run | |
| try: | |
| nltk.data.find('sentiment/vader_lexicon.zip') | |
| except LookupError: | |
| logging.info("Downloading NLTK VADER lexicon for offline sentiment analysis") | |
| nltk.download('vader_lexicon') | |
| # Initialize VADER | |
| vader_analyzer = SentimentIntensityAnalyzer() | |
| # Azure Text Analytics client setup | |
| def get_text_analytics_client(): | |
| if not azure_available: | |
| return None | |
| key = os.environ.get("AZURE_TEXT_ANALYTICS_KEY") | |
| endpoint = os.environ.get("AZURE_TEXT_ANALYTICS_ENDPOINT") | |
| if not key or not endpoint: | |
| logging.warning("Azure Text Analytics credentials not found in environment variables") | |
| return None | |
| try: | |
| credential = AzureKeyCredential(key) | |
| client = TextAnalyticsClient(endpoint=endpoint, credential=credential) | |
| return client | |
| except Exception as e: | |
| logging.error(f"Error creating Azure Text Analytics client: {e}") | |
| return None | |
| # Analyze sentiment using Azure with VADER fallback | |
| def analyze_sentiment(text): | |
| # Try Azure first | |
| client = get_text_analytics_client() | |
| if client and text: | |
| try: | |
| response = client.analyze_sentiment([text])[0] | |
| sentiment_scores = { | |
| "positive": response.confidence_scores.positive, | |
| "neutral": response.confidence_scores.neutral, | |
| "negative": response.confidence_scores.negative, | |
| "sentiment": response.sentiment | |
| } | |
| logging.info(f"Using Azure sentiment analysis: {sentiment_scores}") | |
| return sentiment_scores | |
| except Exception as e: | |
| logging.error(f"Error with Azure sentiment analysis: {e}") | |
| # Fall through to VADER | |
| # Fallback to NLTK VADER | |
| if text: | |
| try: | |
| scores = vader_analyzer.polarity_scores(text) | |
| # Map VADER scores to Azure-like format | |
| positive = scores['pos'] | |
| negative = scores['neg'] | |
| neutral = scores['neu'] | |
| # Keywords that indicate anger | |
| anger_keywords = ["angry", "mad", "furious", "outraged", "annoyed", "irritated", | |
| "frustrated", "hate", "hatred", "despise", "resent", "enraged"] | |
| # Check for anger keywords | |
| has_anger = any(word in text.lower() for word in anger_keywords) | |
| # Determine overall sentiment with enhanced anger detection | |
| if scores['compound'] >= 0.05: | |
| sentiment = "positive" | |
| elif scores['compound'] <= -0.05: | |
| if has_anger: | |
| sentiment = "angry" # Special anger category | |
| else: | |
| sentiment = "negative" | |
| else: | |
| sentiment = "neutral" | |
| sentiment_scores = { | |
| "positive": positive, | |
| "neutral": neutral, | |
| "negative": negative, | |
| "angry": 1.0 if has_anger else 0.0, # Add special anger score | |
| "sentiment": sentiment | |
| } | |
| logging.info(f"Using enhanced VADER sentiment analysis: {sentiment_scores}") | |
| return sentiment_scores | |
| except Exception as e: | |
| logging.error(f"Error with VADER sentiment analysis: {e}") | |
| return None | |
| # Track avatar updates with timestamp | |
| last_avatar_update = time.time() | |
| def fill_random_queue(): | |
| """Asynchronously fill the random numbers queue with 100 numbers""" | |
| global random_filling, random_numbers_queue | |
| with random_queue_lock: | |
| if random_filling: | |
| return # Already filling | |
| random_filling = True | |
| def _fill_queue(): | |
| global random_filling | |
| try: | |
| import random | |
| logging.debug("[Random Queue] Filling with regular random numbers") | |
| with random_queue_lock: | |
| while len(random_numbers_queue) < 100: | |
| random_numbers_queue.append(random.random()) | |
| logging.info(f"[Random Queue] Filled queue with 100 random numbers") | |
| except Exception as e: | |
| logging.error(f"[Random Queue] Error filling queue: {e}") | |
| finally: | |
| with random_queue_lock: | |
| random_filling = False | |
| # Start filling in background thread | |
| Thread(target=_fill_queue, daemon=True).start() | |
| def pop_random_number(): | |
| """Pop a random number from the queue, or generate one if empty""" | |
| global random_numbers_queue | |
| with random_queue_lock: | |
| if len(random_numbers_queue) > 0: | |
| return random_numbers_queue.popleft() | |
| else: | |
| # Queue is empty, generate on the fly | |
| import random | |
| return random.random() | |
| def get_avatar(): | |
| """Endpoint to get the current avatar shape and state with enhanced responsiveness""" | |
| global last_avatar_update, random_numbers_queue, random_filling | |
| if not is_initialized: | |
| return jsonify({ | |
| 'avatar_shape': 'Circle', | |
| 'is_initialized': False, | |
| 'last_updated': last_avatar_update, | |
| 'status': 'initializing' | |
| }) | |
| try: | |
| # Check if random queue is empty, fill it asynchronously if needed | |
| with random_queue_lock: | |
| queue_empty = len(random_numbers_queue) == 0 | |
| if queue_empty and not random_filling: | |
| fill_random_queue() | |
| # Pop one random number to update emotions | |
| random_num = pop_random_number() | |
| avatar_shape = avatar_engine.avatar_model if avatar_engine else 'Circle' | |
| # Update timestamp when the avatar changes (you would track this in AvatarEngine normally) | |
| current_timestamp = time.time() | |
| # Get the last message for sentiment analysis if available | |
| last_message = getattr(dialogue_engine, 'last_user_message', '') | |
| sentiment_data = None | |
| # Analyze sentiment if we have a message | |
| if last_message: | |
| sentiment_data = analyze_sentiment(last_message) | |
| # Force avatar update based on emotions if available | |
| if avatar_engine and galatea_ai: | |
| # Apply random influence to emotions | |
| emotions = ["joy", "sadness", "anger", "fear", "curiosity"] | |
| # Use random number to influence a random emotion | |
| import random | |
| emotion_index = int(random_num * len(emotions)) % len(emotions) | |
| selected_emotion = emotions[emotion_index] | |
| # Apply subtle random influence (-0.05 to +0.05) | |
| influence = (random_num - 0.5) * 0.1 | |
| current_value = galatea_ai.emotional_state[selected_emotion] | |
| new_value = max(0.05, min(1.0, current_value + influence)) | |
| galatea_ai.emotional_state[selected_emotion] = new_value | |
| # If we have sentiment data, incorporate it into emotional state | |
| if sentiment_data: | |
| # Update emotional state based on sentiment (enhanced mapping) | |
| if sentiment_data["sentiment"] == "positive": | |
| galatea_ai.emotional_state["joy"] = max(galatea_ai.emotional_state["joy"], sentiment_data["positive"]) | |
| elif sentiment_data["sentiment"] == "negative": | |
| galatea_ai.emotional_state["sadness"] = max(galatea_ai.emotional_state["sadness"], sentiment_data["negative"]) | |
| elif sentiment_data["sentiment"] == "angry": | |
| # Amplify anger emotion when detected | |
| galatea_ai.emotional_state["anger"] = max(galatea_ai.emotional_state["anger"], 0.8) | |
| # Save emotional state to JSON | |
| if hasattr(galatea_ai, 'emotional_agent'): | |
| galatea_ai.emotional_agent._save_to_json() | |
| avatar_engine.update_avatar(galatea_ai.emotional_state) | |
| avatar_shape = avatar_engine.avatar_model | |
| last_avatar_update = current_timestamp | |
| return jsonify({ | |
| 'avatar_shape': avatar_shape, | |
| 'emotions': {k: round(v, 2) for k, v in galatea_ai.emotional_state.items()} if galatea_ai else {}, | |
| 'sentiment': sentiment_data, | |
| 'is_initialized': is_initialized, | |
| 'last_updated': last_avatar_update, | |
| 'status': 'ready' | |
| }) | |
| except Exception as e: | |
| logging.error(f"Error getting avatar: {e}") | |
| return jsonify({ | |
| 'error': 'Failed to get avatar information', | |
| 'avatar_shape': 'Circle', | |
| 'status': 'error' | |
| }), 500 | |
| def health(): | |
| """Simple health check endpoint to verify the server is running""" | |
| return jsonify({ | |
| 'status': 'ok', | |
| 'deepseek_available': hasattr(galatea_ai, 'deepseek_available') and galatea_ai.deepseek_available if galatea_ai else False, | |
| 'is_initialized': is_initialized, | |
| 'missing_deepseek_key': missing_deepseek_key | |
| }) | |
| def availability(): | |
| """Report overall availability state to the frontend""" | |
| if missing_deepseek_key: | |
| return jsonify({ | |
| 'available': False, | |
| 'status': 'missing_deepseek_key', | |
| 'is_initialized': False, | |
| 'initializing': False, | |
| 'missing_deepseek_key': True, | |
| 'error_page': url_for('error_page') | |
| }) | |
| if initializing or not is_initialized: | |
| return jsonify({ | |
| 'available': False, | |
| 'status': 'initializing', | |
| 'is_initialized': is_initialized, | |
| 'initializing': initializing, | |
| 'missing_deepseek_key': False | |
| }) | |
| return jsonify({ | |
| 'available': True, | |
| 'status': 'ready', | |
| 'is_initialized': True, | |
| 'initializing': False, | |
| 'missing_deepseek_key': False | |
| }) | |
| def is_initialized_endpoint(): | |
| """Lightweight endpoint for polling initialization progress""" | |
| # Determine current initialization state | |
| if missing_deepseek_key: | |
| return jsonify({ | |
| 'is_initialized': False, | |
| 'initializing': False, | |
| 'missing_deepseek_key': True, | |
| 'error_page': url_for('error_page'), | |
| 'status': 'missing_api_key' | |
| }) | |
| # Check if components are initializing | |
| if initializing: | |
| return jsonify({ | |
| 'is_initialized': False, | |
| 'initializing': True, | |
| 'missing_deepseek_key': False, | |
| 'status': 'initializing_components', | |
| 'message': 'Initializing AI components...' | |
| }) | |
| # Check if fully initialized | |
| if is_initialized: | |
| return jsonify({ | |
| 'is_initialized': True, | |
| 'initializing': False, | |
| 'missing_deepseek_key': False, | |
| 'status': 'ready', | |
| 'message': 'System ready' | |
| }) | |
| # Still waiting | |
| return jsonify({ | |
| 'is_initialized': False, | |
| 'initializing': True, | |
| 'missing_deepseek_key': False, | |
| 'status': 'waiting', | |
| 'message': 'Waiting for initialization...' | |
| }) | |
| def status(): | |
| """Status endpoint to check initialization progress""" | |
| return jsonify({ | |
| 'is_initialized': is_initialized, | |
| 'initializing': initializing, | |
| 'emotions': galatea_ai.emotional_state if galatea_ai else {'joy': 0.2, 'sadness': 0.2, 'anger': 0.2, 'fear': 0.2, 'curiosity': 0.2}, | |
| 'avatar_shape': avatar_engine.avatar_model if avatar_engine and is_initialized else 'Circle', | |
| 'missing_deepseek_key': missing_deepseek_key | |
| }) | |
| def error_page(): | |
| """Render an informative error page when the app is unavailable""" | |
| return render_template('error.html', missing_deepseek_key=missing_deepseek_key) | |
| if __name__ == '__main__': | |
| print("Starting Galatea Web Interface...") | |
| # Run parallel initialization BEFORE starting Flask app | |
| logging.info("=" * 70) | |
| logging.info("STARTING GALATEA AI APPLICATION") | |
| logging.info("=" * 70) | |
| logging.info("Running parallel initialization...") | |
| print("=" * 70) | |
| print("STARTING GALATEA AI APPLICATION") | |
| print("=" * 70) | |
| print("Running parallel initialization...") | |
| print("") | |
| # Run parallel initialization synchronously | |
| init_success = run_parallel_initialization() | |
| if not init_success: | |
| logging.error("=" * 70) | |
| logging.error("CRITICAL: Parallel initialization failed") | |
| logging.error("Application will exit") | |
| logging.error("=" * 70) | |
| print("=" * 70) | |
| print("CRITICAL: Parallel initialization failed") | |
| print("Application will exit") | |
| print("=" * 70) | |
| sys.exit(1) | |
| # Now initialize Galatea components | |
| logging.info("Initializing Galatea AI components...") | |
| print("Initializing Galatea AI components...") | |
| initialize_components() | |
| if not is_initialized: | |
| logging.error("=" * 70) | |
| logging.error("CRITICAL: Component initialization failed") | |
| logging.error("Application will exit") | |
| logging.error("=" * 70) | |
| print("=" * 70) | |
| print("CRITICAL: Component initialization failed") | |
| print("Application will exit") | |
| print("=" * 70) | |
| sys.exit(1) | |
| # Add debug logs for avatar shape changes | |
| logging.info("Avatar system initialized with default shape.") | |
| # Get port from environment variable (for Hugging Face Spaces compatibility) | |
| port = int(os.environ.get('PORT', 7860)) | |
| logging.info(f"Flask server starting on port {port}...") | |
| logging.info("Frontend will poll /api/is_initialized for status") | |
| print(f"\nFlask server starting on port {port}...") | |
| print("Frontend will poll /api/is_initialized for status\n") | |
| # Bind to 0.0.0.0 for external access (required for Heroku/Hugging Face Spaces) | |
| # Disable debug mode in production (Heroku sets DYNO environment variable) | |
| debug_mode = os.environ.get('DYNO') is None # Only debug when not on Heroku | |
| app.run(host='0.0.0.0', port=port, debug=debug_mode) |