diff --git a/deployment/api/__pycache__/main.cpython-312.pyc b/deployment/api/__pycache__/main.cpython-312.pyc index 12a61c2..ad93c11 100644 Binary files a/deployment/api/__pycache__/main.cpython-312.pyc and b/deployment/api/__pycache__/main.cpython-312.pyc differ diff --git a/deployment/api/email_service.py b/deployment/api/email_service.py new file mode 100644 index 0000000..58dfe5c --- /dev/null +++ b/deployment/api/email_service.py @@ -0,0 +1,106 @@ +import smtplib +import os +import json +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime + +def send_error_report_email(error_reports): + """ + Envoie un email avec le rapport d'erreurs et le JSON intégré + """ + smtp_server = os.getenv("SMTP_SERVER") + smtp_port = int(os.getenv("SMTP_PORT", 587)) + smtp_email = os.getenv("SMTP_EMAIL") + smtp_password = os.getenv("SMTP_PASSWORD") + smtp_from_alias = os.getenv("SMTP_FROM_ALIAS", smtp_email) + admin_email = os.getenv("ADMIN_EMAIL") + + if not all([smtp_server, smtp_email, smtp_password, admin_email]): + print("❌ Configuration email manquante dans .env") + return False + + try: + # Créer le message + msg = MIMEMultipart() + msg['From'] = f"Air Paradis Monitor <{smtp_from_alias}>" + msg['To'] = admin_email + msg['Subject'] = f"🚨 Rapport d'erreurs - Prédictions sentiments ({datetime.now().strftime('%d/%m/%Y %H:%M')})" + + # Préparer les données JSON + json_data = { + "rapport_genere_le": datetime.now().isoformat(), + "nombre_signalements": len(error_reports), + "signalements": [ + { + "id": i, + "tweet": tweet, + "prediction_incorrecte": prediction, + "longueur_tweet": len(tweet) + } + for i, (tweet, prediction) in enumerate(error_reports.items(), 1) + ], + "metadata": { + "modele": "SentimentAnalysisLSTM", + "version_api": "1.0", + "seuil_rapport": 3 + } + } + + # Corps du message avec JSON intégré + body = f""" +Bonjour, + +Un nouveau rapport d'erreurs a été généré pour l'API de prédiction de sentiments Air Paradis. + +📊 Nombre total de signalements : {len(error_reports)} +📅 Date du rapport : {datetime.now().strftime('%d/%m/%Y à %H:%M')} + +📋 Résumé des signalements : + +""" + + for i, (tweet, prediction) in enumerate(error_reports.items(), 1): + body += f"{i}. Tweet: \"{tweet[:100]}{'...' if len(tweet) > 100 else ''}\"\n" + body += f" Prédiction signalée comme incorrecte: {prediction.upper()}\n\n" + + body += """ +📈 Actions recommandées : +- Analyser les tweets signalés pour identifier des patterns +- Considérer un réentraînement du modèle si nécessaire +- Vérifier la qualité des données de training + +📊 DONNÉES COMPLÈTES (JSON) : +═══════════════════════════════════════════════════════════════════════════════ + +""" + + # ✅ Ajouter le JSON directement dans le corps + json_string = json.dumps(json_data, ensure_ascii=False, indent=2) + body += f"```json\n{json_string}\n```" + + body += """ + +═══════════════════════════════════════════════════════════════════════════════ + +--- +Cordialement, +🤖 API Air Paradis - Système de monitoring automatique +📧 Envoyé depuis : """ + smtp_from_alias + + # Ajouter le corps du message + msg.attach(MIMEText(body, 'plain', 'utf-8')) + + # Envoyer l'email + with smtplib.SMTP(smtp_server, smtp_port) as server: + server.starttls() + server.login(smtp_email, smtp_password) + server.send_message(msg) + + print(f"✅ Email de rapport envoyé avec succès depuis {smtp_from_alias}") + print(f"📊 JSON intégré dans le corps du message") + return True + + except Exception as e: + print(f"❌ Erreur lors de l'envoi de l'email : {e}") + return False \ No newline at end of file diff --git a/deployment/api/main.py b/deployment/api/main.py index fd63145..67ec948 100644 --- a/deployment/api/main.py +++ b/deployment/api/main.py @@ -14,6 +14,9 @@ PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) sys.path.append(PARENT_DIR) from functions import preprocess_tweet +# 📧 Import du service email +from email_service import send_error_report_email + # 🔐 Charger les variables d'environnement (.env à deux niveaux au-dessus) load_dotenv(dotenv_path=os.path.join(PARENT_DIR, ".env")) @@ -36,51 +39,52 @@ class ReportRequest(BaseModel): tweet: str prediction: str -# 🧠 Stockage des signalements d’erreurs +# 🧠 Stockage des signalements d'erreurs error_reports = {} # 🔍 Fonction pour charger modèle + tokenizer def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE): client = MlflowClient() versions = client.get_latest_versions(model_name, [stage]) + if not versions: raise RuntimeError(f"Aucune version du modèle '{model_name}' en stage '{stage}'") - + version_info = versions[0] run_id = version_info.run_id + if not run_id: raise RuntimeError("Run ID introuvable.") - + # Extraire le tag d'embedding model_version_info = client.get_model_version(model_name, version_info.version) embedding_type = model_version_info.tags.get("embedding_type", "glove").lower() - + # Déterminer les chemins des artefacts local_dir = "./downloaded_artifacts" os.makedirs(local_dir, exist_ok=True) - base_path = "local_artifacts" tokenizer_file = "tokenizer.pickle" - + if "glove" in embedding_type: keras_file = "final_model_LSTM_GloVe-300d-Fige.keras" elif "word2vec" in embedding_type: keras_file = "final_model_LSTM_Word2Vec-Fige.keras" else: raise ValueError(f"Embedding type inconnu : {embedding_type}") - + def dl(path): return client.download_artifacts(run_id=run_id, path=f"{base_path}/{path}", dst_path=local_dir) - + # 🔽 Télécharger les artefacts model_path = dl(keras_file) tokenizer_path = dl(tokenizer_file) - + # 📦 Chargement model = load_model(model_path) with open(tokenizer_path, "rb") as f: tokenizer = pickle.load(f) - + return model, tokenizer, embedding_type # 🚀 Lancer l'API @@ -100,15 +104,15 @@ def root(): def predict_sentiment(request: TweetRequest): if model is None or tokenizer is None: raise HTTPException(status_code=503, detail="Modèle ou artefacts indisponibles.") - + try: processed_tweet = preprocess_tweet(request.tweet) except Exception as e: raise HTTPException(status_code=500, detail=f"Erreur de prétraitement : {e}") - + if not processed_tweet.strip(): raise HTTPException(status_code=400, detail="Tweet vide ou invalide après prétraitement.") - + try: sequence = tokenizer.texts_to_sequences([processed_tweet]) sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1]) @@ -116,7 +120,7 @@ def predict_sentiment(request: TweetRequest): sentiment = "positif" if prediction_prob >= 0.5 else "negatif" except Exception as e: raise HTTPException(status_code=500, detail=f"Erreur d'inférence : {e}") - + return { "sentiment": sentiment, "probability": float(prediction_prob) @@ -126,16 +130,31 @@ def predict_sentiment(request: TweetRequest): def report_error(request: ReportRequest): tweet = request.tweet.strip() prediction = request.prediction.strip().lower() - + if not tweet or not prediction: raise HTTPException(status_code=400, detail="Tweet ou prédiction manquants.") - + # Ajouter ou mettre à jour le signalement du tweet error_reports[tweet] = prediction - + report_sent = False + # Condition : à chaque multiple de 3 signalements totaux if len(error_reports) % 3 == 0: report_sent = True - - return {"report_sent": report_sent} \ No newline at end of file + + # 📧 NOUVEAU : Envoyer l'email de rapport + try: + print(f"📧 Tentative d'envoi d'email pour {len(error_reports)} signalements...") + email_success = send_error_report_email(error_reports.copy()) + + if email_success: + print(f"✅ Email envoyé avec succès pour {len(error_reports)} signalements") + else: + print(f"❌ Échec de l'envoi de l'email pour {len(error_reports)} signalements") + + except Exception as e: + print(f"❌ Erreur lors de l'envoi de l'email : {e}") + # On continue même si l'email échoue pour ne pas bloquer l'API + + return {"report_sent": report_sent} diff --git a/deployment/api/requirements.txt b/deployment/api/requirements.txt new file mode 100644 index 0000000..891d063 --- /dev/null +++ b/deployment/api/requirements.txt @@ -0,0 +1,19 @@ +# === API FastAPI === +fastapi==0.115.14 +uvicorn==0.35.0 +pydantic==2.11.7 +starlette==0.46.2 +# === Interface Streamlit === +streamlit==1.46.1 +requests==2.32.4 +# === Machine Learning === +tensorflow==2.19.0 +mlflow==3.1.1 +numpy==2.1.3 +pandas==2.3.0 +# === Traitement de texte === +nltk==3.9.1 +# === Utilitaires === +python-dotenv==1.1.1 +# === Dépendances système === +protobuf==3.20.3 diff --git a/deployment/api/test_alias_email.py b/deployment/api/test_alias_email.py new file mode 100644 index 0000000..e6b5322 --- /dev/null +++ b/deployment/api/test_alias_email.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Sat Jul 5 15:03:24 2025 + +@author: madfuckinman +""" + +# test_alias_email.py +import os +from dotenv import load_dotenv +from email_service import send_error_report_email + +load_dotenv() + +print("🔧 Configuration Email:") +print(f"Serveur SMTP: {os.getenv('SMTP_SERVER')}") +print(f"Authentification: {os.getenv('SMTP_EMAIL')}") +print(f"Expéditeur (alias): {os.getenv('SMTP_FROM_ALIAS')}") +print(f"Destinataire: {os.getenv('ADMIN_EMAIL')}") + +# Test +test_reports = { + "Service client décevant": "positif", + "J'adore cette compagnie": "negatif", + "Vol annulé sans prévenir": "positif" +} + +print("\n📧 Test d'envoi depuis l'alias...") +result = send_error_report_email(test_reports) +print(f"Résultat : {'✅ Succès' if result else '❌ Échec'}") \ No newline at end of file