#!/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 @login_manager.user_loader 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') @app.after_request 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 @app.after_request 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 @app.route('/') 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) ---# @app.teardown_appcontext 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 )