Alessandro Piana
dockerfile con logging 59
86b211a
#!/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
)