Spaces:
Running
Running
""" | |
Wrapper pour intégrer Modal ML dans Gradio | |
Ce module fait le pont entre l'interface Gradio et les fonctions Modal ML | |
Avec authentification automatique Modal depuis HuggingFace Space | |
""" | |
import asyncio | |
import json | |
import logging | |
import os | |
from typing import Dict, List, Any, Optional, Tuple | |
from datetime import datetime | |
import pandas as pd | |
# Import Modal pour l'API distante avec authentification | |
try: | |
import modal | |
MODAL_AVAILABLE = True | |
# Configuration automatique de l'authentification Modal | |
# depuis les variables d'environnement HuggingFace Space | |
modal_token_id = os.environ.get("MODAL_TOKEN_ID") | |
modal_token_secret = os.environ.get("MODAL_TOKEN_SECRET") | |
if modal_token_id and modal_token_secret: | |
# Configurer l'authentification Modal pour HuggingFace Space | |
os.environ["MODAL_TOKEN_ID"] = modal_token_id | |
os.environ["MODAL_TOKEN_SECRET"] = modal_token_secret | |
logging.info("🔐 Authentification Modal configurée depuis HuggingFace Space") | |
else: | |
logging.warning("⚠️ Tokens Modal non trouvés dans les secrets HuggingFace Space") | |
# Utiliser l'API Modal distante au lieu des imports locaux | |
# Ceci permet la communication depuis HuggingFace Space | |
APP_NAME = "odoo-lead-analysis-improved" | |
# Références aux fonctions Modal distantes avec authentification | |
try: | |
generate_synthetic_leads = modal.Function.from_name(APP_NAME, "generate_synthetic_leads") | |
train_improved_model = modal.Function.from_name(APP_NAME, "train_improved_model") | |
predict_lead_conversion_improved = modal.Function.from_name(APP_NAME, "predict_lead_conversion_improved") | |
monitor_model_performance = modal.Function.from_name(APP_NAME, "monitor_model_performance") | |
logging.info(f"✅ Connexion Modal établie avec l'app '{APP_NAME}'") | |
except Exception as e: | |
logging.error(f"❌ Erreur connexion Modal: {e}") | |
MODAL_AVAILABLE = False | |
except ImportError: | |
logging.warning("Modal non disponible - fonctionnalités ML désactivées") | |
MODAL_AVAILABLE = False | |
logger = logging.getLogger(__name__) | |
class ModalMLWrapper: | |
"""Wrapper pour les fonctions Modal ML distantes""" | |
def __init__(self): | |
self.is_model_trained = False | |
self.model_metadata = None | |
self.reference_data = None | |
self.app_name = APP_NAME | |
def check_modal_availability(self) -> bool: | |
"""Vérifie si Modal est disponible""" | |
return MODAL_AVAILABLE and generate_synthetic_leads is not None | |
async def train_model_async(self, num_synthetic_leads: int = 1000) -> Dict[str, Any]: | |
""" | |
Entraîne le modèle ML de manière asynchrone via l'API Modal distante | |
Args: | |
num_synthetic_leads: Nombre de leads synthétiques à générer | |
Returns: | |
Métadonnées du modèle entraîné | |
""" | |
try: | |
if not self.check_modal_availability(): | |
return { | |
"error": "Modal ML non disponible ou app non déployée", | |
"status": "error" | |
} | |
logger.info(f"🚀 Début d'entraînement du modèle ML avec {num_synthetic_leads} leads (API distante)") | |
# 1. Générer les données synthétiques via l'API distante | |
logger.info("📊 Génération des données synthétiques via Modal...") | |
leads_data = await generate_synthetic_leads.remote.aio(num_synthetic_leads) | |
# Sauvegarder les données de référence pour le drift | |
self.reference_data = leads_data[:100] if leads_data else [] | |
# 2. Entraîner le modèle via l'API distante | |
logger.info("🤖 Entraînement du modèle via Modal...") | |
model_metadata = await train_improved_model.remote.aio(leads_data) | |
# Sauvegarder les métadonnées | |
self.model_metadata = model_metadata | |
self.is_model_trained = True | |
logger.info("✅ Modèle entraîné avec succès via Modal") | |
return { | |
"status": "success", | |
"model_metadata": model_metadata, | |
"synthetic_data_count": len(leads_data), | |
"reference_data_count": len(self.reference_data), | |
"training_date": datetime.now().isoformat(), | |
"modal_app": self.app_name | |
} | |
except Exception as e: | |
logger.error(f"❌ Erreur entraînement modèle Modal: {e}") | |
return { | |
"error": f"Erreur Modal: {str(e)}", | |
"status": "error", | |
"modal_app": self.app_name | |
} | |
def train_model(self, num_synthetic_leads: int = 1000) -> Dict[str, Any]: | |
"""Version synchrone de l'entraînement Modal distant""" | |
try: | |
# Utiliser la version synchrone de Modal | |
if not self.check_modal_availability(): | |
return { | |
"error": "Modal ML non disponible ou app non déployée", | |
"status": "error" | |
} | |
logger.info(f"🚀 Entraînement synchrone Modal: {num_synthetic_leads} leads") | |
# 1. Génération synchrone | |
leads_data = generate_synthetic_leads.remote(num_synthetic_leads) | |
self.reference_data = leads_data[:100] if leads_data else [] | |
# 2. Entraînement synchrone | |
model_metadata = train_improved_model.remote(leads_data) | |
self.model_metadata = model_metadata | |
self.is_model_trained = True | |
return { | |
"status": "success", | |
"model_metadata": model_metadata, | |
"synthetic_data_count": len(leads_data), | |
"reference_data_count": len(self.reference_data), | |
"training_date": datetime.now().isoformat(), | |
"modal_app": self.app_name | |
} | |
except Exception as e: | |
logger.error(f"❌ Erreur entraînement synchrone Modal: {e}") | |
return { | |
"error": f"Erreur Modal synchrone: {str(e)}", | |
"status": "error", | |
"modal_app": self.app_name | |
} | |
def predict_lead(self, lead_data: Dict[str, Any]) -> Dict[str, Any]: | |
"""Prédiction synchrone via Modal distant""" | |
try: | |
if not self.check_modal_availability(): | |
return { | |
"error": "Modal ML non disponible ou app non déployée", | |
"status": "error" | |
} | |
if not self.is_model_trained: | |
return { | |
"error": "Modèle non entraîné. Lancez d'abord l'entraînement.", | |
"status": "error" | |
} | |
logger.info(f"🔮 Prédiction Modal pour: {lead_data.get('name', 'Inconnu')}") | |
# Prédiction via l'API Modal distante | |
prediction = predict_lead_conversion_improved.remote(lead_data) | |
# Ajouter des métadonnées | |
prediction["prediction_date"] = datetime.now().isoformat() | |
prediction["model_version"] = self.model_metadata.get("training_date") if self.model_metadata else None | |
prediction["modal_app"] = self.app_name | |
logger.info(f"✅ Prédiction Modal réussie: {prediction.get('classification', 'N/A')}") | |
return prediction | |
except Exception as e: | |
logger.error(f"❌ Erreur prédiction Modal: {e}") | |
return { | |
"error": f"Erreur Modal prédiction: {str(e)}", | |
"status": "error", | |
"modal_app": self.app_name | |
} | |
async def monitor_performance_async(self, predictions: List[Dict[str, Any]]) -> Dict[str, Any]: | |
""" | |
Monitoring asynchrone des performances | |
Args: | |
predictions: Liste des prédictions à analyser | |
Returns: | |
Résultats du monitoring | |
""" | |
try: | |
if not MODAL_AVAILABLE: | |
return { | |
"error": "Modal ML n'est pas disponible", | |
"status": "error" | |
} | |
if not predictions: | |
return { | |
"error": "Aucune prédiction à analyser", | |
"status": "error" | |
} | |
logger.info(f"📊 Monitoring de {len(predictions)} prédictions") | |
monitoring_results = await monitor_model_performance.remote.aio(predictions) | |
# Ajouter des métadonnées | |
monitoring_results["monitoring_date"] = datetime.now().isoformat() | |
monitoring_results["model_version"] = self.model_metadata.get("training_date") if self.model_metadata else None | |
logger.info(f"✅ Monitoring terminé: {len(monitoring_results.get('performance_alerts', []))} alertes") | |
return monitoring_results | |
except Exception as e: | |
logger.error(f"❌ Erreur monitoring: {e}") | |
return { | |
"error": str(e), | |
"status": "error" | |
} | |
def monitor_performance(self, predictions: List[Dict[str, Any]]) -> Dict[str, Any]: | |
"""Version synchrone du monitoring""" | |
try: | |
loop = asyncio.new_event_loop() | |
asyncio.set_event_loop(loop) | |
result = loop.run_until_complete(self.monitor_performance_async(predictions)) | |
loop.close() | |
return result | |
except Exception as e: | |
logger.error(f"❌ Erreur monitoring synchrone: {e}") | |
return {"error": str(e), "status": "error"} | |
def get_model_status(self) -> Dict[str, Any]: | |
"""Retourne le statut du modèle""" | |
return { | |
"is_trained": self.is_model_trained, | |
"model_metadata": self.model_metadata, | |
"reference_data_count": len(self.reference_data) if self.reference_data else 0, | |
"modal_available": MODAL_AVAILABLE | |
} | |
def format_prediction_for_gradio(self, prediction: Dict[str, Any]) -> str: | |
"""Formate la prédiction pour l'affichage Gradio""" | |
if "error" in prediction: | |
return f"❌ Erreur: {prediction['error']}" | |
result = f""" | |
🎯 **Prédiction pour {prediction.get('lead_name', 'Inconnu')}** | |
📊 **Résultats principaux:** | |
• Classification: {prediction.get('classification', 'N/A')} | |
• Probabilité de conversion: {prediction.get('conversion_probability', 0):.1%} | |
• Priorité: {prediction.get('priority', 'N/A')} | |
• Confiance: {prediction.get('confidence_score', 0):.1%} | |
🔍 **Détails avancés:** | |
• Prédiction: {'Oui' if prediction.get('prediction') else 'Non'} | |
• Version du modèle: {prediction.get('model_version', 'N/A')} | |
• Date de prédiction: {prediction.get('prediction_date', 'N/A')} | |
""" | |
# Ajouter l'analyse de drift si disponible | |
drift_analysis = prediction.get('drift_analysis') | |
if drift_analysis: | |
result += "\n🔄 **Analyse de drift:**\n" | |
for feature, analysis in drift_analysis.items(): | |
if isinstance(analysis, dict): | |
if analysis.get('is_outlier') or analysis.get('is_new_category'): | |
result += f"• ⚠️ {feature}: Anomalie détectée\n" | |
else: | |
result += f"• ✅ {feature}: Normal\n" | |
return result | |
def format_monitoring_for_gradio(self, monitoring: Dict[str, Any]) -> str: | |
"""Formate les résultats de monitoring pour l'affichage Gradio""" | |
if "error" in monitoring: | |
return f"❌ Erreur monitoring: {monitoring['error']}" | |
stats = monitoring.get('probability_stats', {}) | |
alerts = monitoring.get('performance_alerts', []) | |
distribution = monitoring.get('classification_distribution', {}) | |
result = f""" | |
📊 **Rapport de monitoring** | |
📈 **Statistiques des prédictions:** | |
• Nombre total: {monitoring.get('total_predictions', 0)} | |
• Probabilité moyenne: {stats.get('mean', 0):.1%} | |
• Variance: {stats.get('std', 0):.3f} | |
• Min/Max: {stats.get('min', 0):.1%} - {stats.get('max', 0):.1%} | |
🏷️ **Distribution des classifications:** | |
""" | |
for classification, count in distribution.items(): | |
result += f"• {classification}: {count}\n" | |
if alerts: | |
result += f"\n⚠️ **Alertes ({len(alerts)}):**\n" | |
for alert in alerts: | |
severity_emoji = "🚨" if alert.get('severity') == 'ERROR' else "⚠️" | |
result += f"• {severity_emoji} {alert.get('message', 'Alerte inconnue')}\n" | |
else: | |
result += "\n✅ **Aucune alerte détectée**\n" | |
result += f"\n🕐 **Dernière analyse:** {monitoring.get('monitoring_date', 'N/A')}" | |
return result | |
# Instance globale du wrapper | |
modal_wrapper = ModalMLWrapper() | |
# Fonctions helper pour Gradio | |
def gradio_train_model(num_leads: int = 1000) -> Tuple[str, str]: | |
"""Interface Gradio pour l'entraînement""" | |
try: | |
num_leads = max(100, min(5000, int(num_leads))) # Limiter entre 100 et 5000 | |
result = modal_wrapper.train_model(num_leads) | |
if result.get("status") == "success": | |
metadata = result.get("model_metadata", {}) | |
performance = metadata.get("model_performance", {}) | |
success_msg = f""" | |
✅ **Modèle entraîné avec succès !** | |
📊 **Données d'entraînement:** | |
• Leads synthétiques générés: {result.get('synthetic_data_count', 0)} | |
• Données de référence: {result.get('reference_data_count', 0)} | |
🎯 **Performances du modèle:** | |
• Score de test: {performance.get('test_score', 0):.1%} | |
• Validation croisée: {performance.get('cv_mean', 0):.1%} | |
• Score AUC: {performance.get('auc_score', 0):.1%} | |
🕐 **Date d'entraînement:** {result.get('training_date', 'N/A')} | |
""" | |
return success_msg, "✅ Modèle prêt pour les prédictions" | |
else: | |
error_msg = f"❌ Erreur d'entraînement: {result.get('error', 'Erreur inconnue')}" | |
return error_msg, error_msg | |
except Exception as e: | |
error_msg = f"❌ Erreur: {str(e)}" | |
return error_msg, error_msg | |
def gradio_predict_lead(name: str, industry: str, company_size: str, | |
budget_range: str, urgency: str, source: str, | |
expected_revenue: float, response_time: float) -> str: | |
"""Interface Gradio pour la prédiction""" | |
try: | |
if not name.strip(): | |
return "❌ Veuillez entrer un nom de lead" | |
lead_data = { | |
"name": name, | |
"industry": industry, | |
"company_size": company_size, | |
"budget_range": budget_range, | |
"urgency": urgency, | |
"source": source, | |
"expected_revenue": max(0, expected_revenue), | |
"response_time_hours": max(0.1, response_time) | |
} | |
prediction = modal_wrapper.predict_lead(lead_data) | |
return modal_wrapper.format_prediction_for_gradio(prediction) | |
except Exception as e: | |
return f"❌ Erreur de prédiction: {str(e)}" | |
def gradio_get_model_status() -> str: | |
"""Interface Gradio pour le statut du modèle""" | |
try: | |
status = modal_wrapper.get_model_status() | |
if not status.get("modal_available"): | |
return """ | |
❌ **Modal ML non disponible** | |
Pour utiliser les fonctions d'IA avancées, installez Modal: | |
```bash | |
pip install modal | |
``` | |
""" | |
if status.get("is_trained"): | |
metadata = status.get("model_metadata", {}) | |
performance = metadata.get("model_performance", {}) | |
return f""" | |
✅ **Modèle entraîné et prêt** | |
🎯 **Performances:** | |
• Score de test: {performance.get('test_score', 0):.1%} | |
• Validation croisée: {performance.get('cv_mean', 0):.1%} | |
• Score AUC: {performance.get('auc_score', 0):.1%} | |
📊 **Données:** | |
• Données de référence: {status.get('reference_data_count', 0)} leads | |
• Date d'entraînement: {metadata.get('training_date', 'N/A')} | |
""" | |
else: | |
return """ | |
⚠️ **Modèle non entraîné** | |
Lancez d'abord l'entraînement pour utiliser les prédictions avancées. | |
""" | |
except Exception as e: | |
return f"❌ Erreur: {str(e)}" |