Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException | |
| from pydantic import BaseModel | |
| from typing import List, Optional, Tuple, Dict | |
| import re | |
| import joblib | |
| import nltk | |
| from nltk.corpus import stopwords | |
| from nltk.stem import WordNetLemmatizer | |
| import os | |
| import pandas as pd | |
| import numpy as np | |
| # --- IMPORTA IL TUO ALGORITMO GENETICO --- | |
| from genetic_algorithm import GAParams, FitnessEvaluator, GeneticAlgorithm | |
| # 1. Setup Iniziale e Download NLTK | |
| nltk.download('stopwords', quiet=True) | |
| nltk.download('wordnet', quiet=True) | |
| nltk.download('omw-1.4', quiet=True) | |
| app = FastAPI(title="SINTON-IA Multi-Model API") | |
| # 2. Caricamento dei Modelli Addestrati | |
| MODELS = {} | |
| def load_models(): | |
| try: | |
| # Modello 1: Rischio Suicidario (NLP) | |
| MODELS['suicide_vec'] = joblib.load('tfidf_vectorizer.pkl') | |
| MODELS['suicide_model'] = joblib.load('logreg_model.pkl') | |
| if not hasattr(MODELS['suicide_model'], 'multi_class'): | |
| MODELS['suicide_model'].multi_class = 'auto' | |
| # Modello 2: Depression Prediction (LightGBM) | |
| if os.path.exists('final_model_depression.pkl'): | |
| MODELS['depression_model'] = joblib.load('final_model_depression.pkl') | |
| print("✅ Tutti i modelli caricati con successo!") | |
| except Exception as e: | |
| print(f"❌ Errore nel caricamento dei modelli: {e}") | |
| load_models() | |
| # ========================================== | |
| # MODELLO 1: RED FLAG (SUI RISK) | |
| # ========================================== | |
| default_stopwords = set(stopwords.words('english')) | |
| words_to_keep = {'not', 'no', 'nor', 'don', "don't", "isn't", "wasn't", 'never'} | |
| custom_stopwords = default_stopwords - words_to_keep | |
| lemmatizer = WordNetLemmatizer() | |
| def clean_text_pipeline(text: str) -> str: | |
| text = re.sub(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', '', text) | |
| text = re.sub(r'\n+', ' ', text) | |
| text = text.lower() | |
| text = re.sub(r'[^a-zA-Z0-9\s\.,!\?]', '', text) | |
| text = re.sub(r'([\.,!\?])', r' \1 ', text) | |
| text = re.sub(r'\s+', ' ', text).strip() | |
| words = text.split() | |
| cleaned_words = [lemmatizer.lemmatize(w) if w not in ['.',',','!','?'] else w for w in words if w not in custom_stopwords ] | |
| return ' '.join(cleaned_words) | |
| class RedFlagRequest(BaseModel): | |
| testo: str | |
| async def analyze_red_flag(request: RedFlagRequest): | |
| cleaned = clean_text_pipeline(request.testo) | |
| X = MODELS['suicide_vec'].transform([cleaned]) | |
| prob = MODELS['suicide_model'].predict_proba(X)[0][1] | |
| return {"risk_detected": bool(prob >= 0.2384), "probability": float(prob)} | |
| # ========================================== | |
| # MODELLO 2: PREDIZIONE DEPRESSIONE | |
| # ========================================== | |
| VA_MAP = { | |
| "Felice": 0.85, "Sereno": 0.7, "Energico": 0.5, "Neutro": 0.0, | |
| "Stanco": -0.2, "Triste": -0.8, "Ansioso": -0.55, "Arrabbiato": -0.7, | |
| "Spaventato": -0.65, "Confuso": -0.3 | |
| } | |
| DEPRESSION_SCALER = { | |
| "valence_mean": {"mean": 0.1564, "std": 0.2384}, | |
| "valence_std": {"mean": 0.3107, "std": 0.0866}, | |
| "valence_ema_3d": {"mean": 0.1433, "std": 0.3104}, | |
| "valence_trend_5d": {"mean": -0.0001, "std": 0.1230}, | |
| "max_neg_streak": {"mean": 2.5505, "std": 2.0963}, | |
| "missing_ratio": {"mean": 0.1056, "std": 0.1198}, | |
| "intensity_mean": {"mean": 4.9645, "std": 0.7972}, | |
| "dominant_mood_valence": {"mean": 0.0586, "std": 0.2910} | |
| } | |
| FEAT_ORDER = ["valence_mean", "valence_std", "valence_ema_3d", "valence_trend_5d", "max_neg_streak", "missing_ratio", "intensity_mean", "dominant_mood_valence"] | |
| class DailyLog(BaseModel): | |
| mood_state: str | |
| valence: float | |
| intensity: float | |
| is_missing: bool = False | |
| class DepressionRequest(BaseModel): | |
| logs: List[DailyLog] | |
| def extract_depression_features(logs: List[DailyLog]): | |
| df = pd.DataFrame([l.dict() for l in logs]) | |
| present = df[~df['is_missing']] | |
| if len(present) < 3: | |
| raise HTTPException(status_code=400, detail="Dati insufficienti (minimo 3 log validi)") | |
| valences = present['valence'].values | |
| intensities = present['intensity'].values | |
| f = {} | |
| f['valence_mean'] = np.mean(valences) | |
| f['valence_std'] = np.std(valences) if len(valences) > 1 else 0.0 | |
| f['valence_ema_3d'] = valences[-1] | |
| if len(valences) >= 2: | |
| x = np.arange(len(valences)) | |
| f['valence_trend_5d'], _ = np.polyfit(x, valences, 1) | |
| else: | |
| f['valence_trend_5d'] = 0.0 | |
| max_neg = 0; curr_neg = 0 | |
| for v in valences: | |
| if v < 0: | |
| curr_neg += 1 | |
| max_neg = max(max_neg, curr_neg) | |
| else: | |
| curr_neg = 0 | |
| f['max_neg_streak'] = max_neg | |
| f['missing_ratio'] = (len(df) - len(present)) / len(df) | |
| f['intensity_mean'] = np.mean(intensities) | |
| f['dominant_mood_valence'] = VA_MAP.get(present['mood_state'].mode()[0], 0.0) | |
| scaled = [] | |
| for k in FEAT_ORDER: | |
| val = (f[k] - DEPRESSION_SCALER[k]['mean']) / DEPRESSION_SCALER[k]['std'] | |
| scaled.append(val) | |
| return np.array(scaled).reshape(1, -1) | |
| async def predict_depression(request: DepressionRequest): | |
| try: | |
| features = extract_depression_features(request.logs) | |
| prediction = MODELS['depression_model'].predict(features)[0] | |
| return {"phq9_score": float(prediction), "risk_level": "High" if prediction > 15 else "Normal"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ========================================== | |
| # MODELLO 3: GENETIC ALGORITHM (CHURN PREVENTION) | |
| # ========================================== | |
| class GARequest(BaseModel): | |
| mood_frequency_7d: float | |
| avg_mood_valence_7d: float | |
| notification_read_rate: float | |
| night_activity_rate: float | |
| # "extra = allow" permette al backend NestJS di inviare anche eventuali altre feature | |
| # (es. badges_total, profil_assegnato) che il tuo script Python si aspetta, senza dare errore | |
| class Config: | |
| extra = "allow" | |
| async def run_genetic_algorithm(request: GARequest): | |
| try: | |
| # 1. Carica il Gold Standard generato da Optuna | |
| params = GAParams.load_gold_standard_config('ga_tuned_config.json') | |
| # 2. Trasforma il JSON in arrivo in una Serie Pandas | |
| patient_features = pd.Series(request.dict()) | |
| # 3. Inizializza l'algoritmo usando il tuo file esterno | |
| rng = np.random.default_rng(params.seed) | |
| evaluator = FitnessEvaluator(patient_features, params, rng=rng) | |
| ga = GeneticAlgorithm(evaluator, params, rng=rng) | |
| # 4. Trova la strategia perfetta per la settimana! | |
| best_chromosome = ga.run() | |
| best_strategy = best_chromosome.decode() | |
| return best_strategy | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # ========================================== | |
| # ROOT / HEALTH CHECK | |
| # ========================================== | |
| async def root(): | |
| return {"status": "SINTON-IA Multi-Model Engine (RedFlag, Depression, GA) is awake and running."} |