From 89997585c67860e974082071dbbc44eaa7e26b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Mureau?= Date: Sat, 5 Jul 2025 16:05:46 +0200 Subject: [PATCH] email sending implementation --- .../api/__pycache__/main.cpython-312.pyc | Bin 6599 -> 7423 bytes deployment/api/email_service.py | 106 ++++++++++++++++++ deployment/api/main.py | 57 ++++++---- deployment/api/requirements.txt | 19 ++++ deployment/api/test_alias_email.py | 31 +++++ 5 files changed, 194 insertions(+), 19 deletions(-) create mode 100644 deployment/api/email_service.py create mode 100644 deployment/api/requirements.txt create mode 100644 deployment/api/test_alias_email.py diff --git a/deployment/api/__pycache__/main.cpython-312.pyc b/deployment/api/__pycache__/main.cpython-312.pyc index 12a61c21322e17e75baa446d08d6abc19b7e2e3d..ad93c1169627db8ca1b90aa7bf06c70b27e6c6cb 100644 GIT binary patch delta 2137 zcmb7FZ){Ul6u-BxuU-4vuItvdYuEAihizr7AR7WY{uH(%bAK4j7#$%xU64}4gnW(%75<>4@p7ZQc&2V+bIVl?_e?|t3dib{+( zdFP&Y&+m8cdH0-q-sdfUZgYLJ*b8!E$utl>l5K{`Q-vD3ES8ehvVzrG83=U}FI5klMDH{d))J={vHX(Ha4Xc? zXxM9KmZKRU{)7ghQ3y-*BZvMOq8QMbIbsom)4-q!%>+L9FYroZBQYe^kRYn2%`2P@ z58q;LWaqHG0?&#;%!B zI-Ny5gq*-1xqL7%`-Q8X@$RLY?xTW+jp)Te9?W=pU@t!4X^+qX(hqGp97GR7D_K6N zI63*OJW2!_ChNy$7d#1we{|XGtt5YMfJra{aH=eVkC)9pbyBoWF*cAbCP;mVMOmkz zXZ0&f-;64_07TnG#(0AuOLM4?RqRM9K57l*4A7K$(u_g{8`%G21Fmp`RAm9v@jsZP zvA8A~86I4jYeBZmh?dlOaEdK(?VMyEtaei4V2VA(u6WVk3M#$02leA-Z)2{HIF0>4 z8wR4ytADJH$fdXNdwf61!BAEnmw2&3N{y#wI@3$QXVPPcf6mP-^0CxVQliOHB9Vwr zz4PX2zC%Y*&cyr&{Lr|R;FYmNV*Y()nd)4j<|ucpp6q$&;yHf)<-~CU79~DuY^kfO zyN=OnrqB+ABoak3jg*JrALBJ{`R>x13Ui2F0yIEH6%|{E$mvC@ISa44qA^J+rLh?# zlPcs0N;4u&-luc-Eue@yw|&uCaXxy@8hX3rD{IZo^1xYc$zr$dhPNCgYT4Gg(z(IK zZBMFOyVR}$)iZeAapYEI%jNDHt+8vZvBlOW7AyCi?Y>=HdCTop{cUpx7eb5Md)2o6 z>i$8s{K(ht_;=K>V@_GvzPPhb-ElxY5Le5OUU%n)mK?wtIRCiHHK^u>v)zXF+kG2w zSKI^82%_yu-<3V8XV(o!RCPp`ECsATmTVOEyFy#VXIrAJds@JJi>=+q%(uDPJ{_-@7n8v95b<*=}zD~Dsm=YS76B6QZuWwThB`MI}-P_*L74M05 zcWCUv{^;Hgy%Fd8=mfPeKPPZexD-+T{{>1PI*f_F_- b_+-Uq-#RV^!Bxn{c;>3b9Sbs7gH-6ux(7w+l0~%QCRQ&Mq(oWOquT{wP+(($W-)SQeEU%an)>GYyM%cXMV9 z60O_NH8v?P=K4UQP5R^u_@JBkA6OrJaMc$aUwAN@#s^dDyPh-KZSmJl=9@F;eBa48 z_uQF3pZ@BJ=+}{m%y4ZSUs-uo`ykrIpZP33$HfMJtSoN_sKnTgvd!xeX94E0)uZ*N z{b;KVj+Jzh(1tvD^{M@gdh8?{JkIh>$m+Hu4>NYu>akm_-T*tp%66>6>{h3FTdGSf zgVMH!*|vSzgE?mPIXzDArGVXD;Z{F3Jz&RoIvQ=Qfo8JZzkT1$BKET$u?DTQGqC#d zzb)DcUs=YAtyfq>W(^^I;XmjP-pEcl2Ab%u1RrRI|3}l)Fo&9hsOjCO8CmE3%*mV@ zV4z#3zeSyn`4PJrXoxtoyB2Xc^cD-u=9YQE+`Lr3ukkhZmbAduxM>KG-mkU?H@Fc) z-#}pC<%SX7W4lVUG6M;WAtt^Ez4&}9hMrsm$IdO6a)o>rG%9ux_%xlOR0H}ka8(4q z)wP-3SX*VTPE1u_I*jO2vvEUr^N~76K;r~ z6@!n6usRwWLN**G7$!)Ivucv-Vnyu(XuqQc?C=%IZhA+`@p)!8 zXJKA^*pVr}fRw2_LL!$8x33k2N+PZE$NFX8|cV5!b_J?XNO zH-nI+y07u+EBNS=6LJYOI8VLb-s-l zdQH#1QLx{krZexJhL_YJHy*Oi3O5(11+MO a8toiVCdb&9W1%Tsy3Jx!ap`uP;=cedfdusc 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