Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """ | |
| Life Coach Web Application | |
| Flask-based web interface for the Phi-4 Life Coach model | |
| """ | |
| import os | |
| import threading | |
| from datetime import datetime | |
| from flask import Flask, render_template, redirect, url_for, flash, request # AGGIUNTO 'request' | |
| from flask_login import LoginManager, current_user | |
| import logging | |
| from werkzeug.middleware.proxy_fix import ProxyFix # NUOVA RIGA | |
| from flask_cors import CORS | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(levelname)s - %(message)s' | |
| ) | |
| logger = logging.getLogger(__name__) | |
| HF_CACHE_DIR = '/data/hf_cache' | |
| os.makedirs(HF_CACHE_DIR, exist_ok=True) | |
| os.environ['HF_HOME'] = HF_CACHE_DIR | |
| os.environ['TRANSFORMERS_CACHE'] = HF_CACHE_DIR | |
| os.environ['HF_DATASETS_CACHE'] = HF_CACHE_DIR | |
| os.environ['TORCH_HOME'] = HF_CACHE_DIR # Bonus: anche per PyTorch | |
| # Fix per CUDA mismatch su HF Spaces (CUDA 12.x) | |
| os.environ['LD_LIBRARY_PATH'] = '/usr/local/cuda/lib64:/usr/local/cuda/lib:/usr/local/lib:' + os.environ.get('LD_LIBRARY_PATH', '') | |
| logger.info("--- LD_LIBRARY_PATH: Aggiornato per CUDA 12.x") | |
| os.environ['BITSANDBYTES_NOWELCOME'] = '1' # Silenzia warning | |
| # --- DISABILITA TORCH.COMPILE (RISOLVE getpwuid() CRASH SU HF SPACES) --- | |
| os.environ['TORCH_COMPILE_DISABLE'] = '1' | |
| logger.info("--- TORCH.COMPILE: Disabilitato per compatibilità HF Spaces") | |
| # --- FINE --- | |
| logger.info(f"--- CACHE HF: Configurata in {HF_CACHE_DIR} (PERSISTENTE)") | |
| # Initialize Flask app | |
| app = Flask(__name__) | |
| # Applica il ProxyFix con tutti i parametri per garantire che Flask veda HTTPS | |
| app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) | |
| # Legge la chiave segreta dai "Secrets" di Hugging Face | |
| secret_key_from_env = os.environ.get('SECRET_KEY') | |
| # --- BLOCCO DI DEBUG PER SECRET_KEY --- | |
| if secret_key_from_env: | |
| logger.info("--- SECRET_KEY: Chiave segreta caricata con successo dall'ambiente.") | |
| else: | |
| logger.error("--- SECRET_KEY: ERRORE CRITICO! La variabile d'ambiente SECRET_KEY non è stata trovata.") | |
| logger.error("--- SECRET_KEY: Il login fallirà. Controlla i 'Secrets' nelle impostazioni dello Space.") | |
| app.config['SECRET_KEY'] = secret_key_from_env | |
| # --- FINE BLOCCO DI DEBUG --- | |
| # Config per HTTPS su HF Spaces (cookie non scartati dal browser) | |
| app.config['SESSION_COOKIE_SECURE'] = True | |
| app.config['REMEMBER_COOKIE_SECURE'] = True # Per "remember me" | |
| app.config['SESSION_COOKIE_HTTPONLY'] = True | |
| app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Bilancia sicurezza e redirect | |
| # --- INIZIO BLOCCO CORS (AGGIUNGI QUI) --- | |
| # Inizializza CORS con supporto ai cookie | |
| CORS( | |
| app, | |
| supports_credentials=True, | |
| origins=["*"], # In produzione, sostituisci con il tuo dominio HF: "https://tuo-nome.hf.space" | |
| allow_headers=["Content-Type", "Authorization", "X-Requested-With"], | |
| expose_headers=["Set-Cookie"] | |
| ) | |
| logger.info("--- CORS: Configurato con supports_credentials=True") | |
| # --- FINE BLOCCO CORS --- | |
| # Disable caching for static files in debug mode | |
| app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 | |
| # Percorso allo storage persistente e scrivibile /data | |
| PERSISTENT_STORAGE_PATH = '/data/lifecoach.db' | |
| app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{PERSISTENT_STORAGE_PATH}' | |
| app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False | |
| # Import db from models and initialize it | |
| from models import db | |
| db.init_app(app) | |
| # Initialize login manager | |
| login_manager = LoginManager(app) | |
| login_manager.login_view = 'auth.login' | |
| login_manager.login_message = 'Please log in to access this page.' | |
| login_manager.session_protection = 'basic' | |
| # Global model instance and lock for thread-safe access | |
| model_instance = None | |
| model_lock = threading.Lock() | |
| def get_life_coach_model(): | |
| """ | |
| Get or initialize the Life Coach model (singleton pattern). | |
| Thread-safe model loading with automatic path detection. | |
| """ | |
| global model_instance | |
| if model_instance is None: | |
| with model_lock: | |
| # Double-check locking pattern | |
| if model_instance is None: | |
| logger.info("Loading Life Coach model...") | |
| from life_coach_v1 import LifeCoachModel | |
| from pathlib import Path | |
| # Detect where the model is actually saved (same logic as life_coach_v1.py) | |
| preferred_path = "/data/life_coach_model" | |
| fallback_path = "./data/life_coach_model" # Questo è il percorso nel repo Git | |
| # Check if model exists in fallback location (il nostro repo) | |
| if Path(fallback_path).exists() and (Path(fallback_path) / "adapter_model.safetensors").exists(): | |
| model_path = fallback_path | |
| logger.info(f"Found model in fallback/repo location: {model_path}") | |
| # Check preferred location (storage persistente, se mai lo useremo) | |
| elif Path(preferred_path).exists() and (Path(preferred_path) / "adapter_model.safetensors").exists(): | |
| model_path = preferred_path | |
| logger.info(f"Found model in preferred location: {model_path}") | |
| else: | |
| # Default to fallback path (quello del repo) | |
| model_path = fallback_path | |
| logger.warning(f"Model not found, will attempt to use default path: {model_path}") | |
| model_instance = LifeCoachModel( | |
| model_name="microsoft/Phi-4", | |
| model_save_path=model_path, | |
| train_file="mixed_lifecoach_dataset_100000.jsonl.gz" | |
| ) | |
| # Load tokenizer and model | |
| model_instance.load_tokenizer() | |
| model_instance.load_model(fine_tuned=True) | |
| logger.info("Life Coach model loaded successfully!") | |
| return model_instance | |
| def generate_response_threadsafe(prompt: str, conversation_history: list) -> str: | |
| """ | |
| Generate a response using the model with thread-safe access. | |
| """ | |
| logger.info(f"--- GENERATE_RESPONSE: Chiamata per utente {current_user.username}") | |
| model = get_life_coach_model() | |
| # Use lock to ensure only one inference at a time (GPU limitation) | |
| with model_lock: | |
| logger.info("--- GENERATE_RESPONSE: Acquisito lock, chiamata a model.generate_response()...") | |
| response = model.generate_response( | |
| prompt=prompt, | |
| max_new_tokens=256, | |
| conversation_history=conversation_history | |
| ) | |
| logger.info(f"--- GENERATE_RESPONSE: Risposta ricevuta.") | |
| return response | |
| # Import models after db is initialized | |
| from models import User, Conversation, Message | |
| # | |
| # --- MODIFICA CHIAVE DI DEBUG --- | |
| # | |
| # User loader for Flask-Login | |
| def load_user(user_id): | |
| """ | |
| Questa funzione è CHIAMATA AD OGNI RICHIESTA dopo il login. | |
| """ | |
| logger.info(f"--- USER LOADER [START]: Invocato per caricare user_id: {user_id}") | |
| try: | |
| # FONDAMENTALE in Gunicorn/Proxy: Garantisce una sessione DB valida | |
| with app.app_context(): # <--- QUESTO DEVE ESSERCI | |
| user = User.query.get(int(user_id)) | |
| if user: | |
| logger.info(f"--- USER LOADER [SUCCESS]: Utente trovato nel DB: {user.username}") | |
| else: | |
| logger.warning(f"--- USER LOADER [FAIL]: User ID {user_id} NON trovato nel DB.") | |
| return user | |
| except Exception as e: | |
| logger.error(f"--- USER LOADER [ERROR]: ERRORE durante il caricamento: {e}", exc_info=True) | |
| return None | |
| # --- FINE MODIFICA DI DEBUG --- | |
| # | |
| # Register blueprints | |
| from auth import auth_bp | |
| from chat import chat_bp | |
| app.register_blueprint(auth_bp, url_prefix='/auth') | |
| app.register_blueprint(chat_bp, url_prefix='/chat') | |
| def add_header(response): | |
| """Add headers to prevent caching of static files.""" | |
| if 'Cache-Control' not in response.headers: | |
| response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' | |
| response.headers['Pragma'] = 'no-cache' | |
| response.headers['Expires'] = '-1' | |
| return response | |
| # --- INIZIO BLOCCO FORZA SET-COOKIE (AGGIUNGI QUI) --- | |
| from flask.sessions import SecureCookieSessionInterface | |
| def force_session_cookie(response): | |
| """ | |
| Forza l'invio del cookie di sessione solo dopo un login riuscito. | |
| """ | |
| # Controlla se è un redirect dopo login | |
| if request.path == '/auth/login' and response.status_code == 302: | |
| from flask import session | |
| from flask.sessions import SecureCookieSessionInterface | |
| serializer = SecureCookieSessionInterface().get_signing_serializer(app) | |
| if serializer is None: | |
| logger.error("--- FORCE_COOKIE: Serializer non disponibile (SECRET_KEY mancante?)") | |
| return response | |
| session_data = dict(session) | |
| if session_data.get('_user_id'): | |
| cookie_value = serializer.dumps(session_data) | |
| response.set_cookie( | |
| 'session', | |
| value=cookie_value, | |
| secure=True, | |
| httponly=True, | |
| samesite='None', | |
| path='/', | |
| max_age=60*60*24*7 # 7 giorni | |
| ) | |
| logger.info("--- FORCE_COOKIE: Cookie sessione FORZATO con SameSite=None") | |
| else: | |
| logger.warning("--- FORCE_COOKIE: Nessun _user_id nella sessione dopo login") | |
| return response | |
| def index(): | |
| """Home page - redirect to chat if logged in, otherwise to login.""" | |
| logger.info(f"--- INDEX ROUTE: Accesso alla root '/'. Utente autenticato: {current_user.is_authenticated}") | |
| if current_user.is_authenticated: | |
| return redirect(url_for('chat.chat_interface')) | |
| return redirect(url_for('auth.login')) | |
| def initialize_database(): | |
| """Initialize database and create tables.""" | |
| # La nostra logica DB ora punta a /data/lifecoach.db (storage persistente) | |
| # Questa funzione deve solo assicurarsi che le tabelle esistano. | |
| logger.info("--- INIT_DB: Chiamata a initialize_database()...") | |
| try: | |
| with app.app_context(): | |
| db.create_all() | |
| logger.info("--- INIT_DB: db.create_all() completato con successo.") | |
| except Exception as e: | |
| logger.error(f"--- INIT_DB: ERRORE durante db.create_all(): {e}", exc_info=True) | |
| ## --- MODIFICA CHIAVE DI DEBUG (FIX PER GUNICORN) ---# | |
| def shutdown_session(exception=None): | |
| """ | |
| Rimuove la sessione del DB alla fine di ogni richiesta. | |
| Questo è FONDAMENTALE per far funzionare SQLAlchemy con Gunicorn. | |
| """ | |
| logger.info("--- SHUTDOWN SESSION: Rimuovendo la sessione DB...") # <-- AGGIUNGI QUESTA RIGA | |
| db.session.remove() | |
| ## --- FINE MODIFICA DI DEBUG ---# | |
| if __name__ == '__main__': | |
| logger.info("=" * 80) | |
| logger.info("LIFE COACH WEB APPLICATION (AVVIO LOCALE)") | |
| logger.info("=" * 80) | |
| # Questo blocco viene eseguito solo in locale, non su Gunicorn | |
| # Initialize database | |
| initialize_database() | |
| # Run Flask app | |
| app.run( | |
| host='0.0.0.0', | |
| port=8085, | |
| debug=True, | |
| threaded=True | |
| ) |