diff --git a/deployment/api/__pycache__/main.cpython-312.pyc b/deployment/api/__pycache__/main.cpython-312.pyc index ad93c11..08c455a 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/app.py b/deployment/api/app.py index e5b5991..1284019 100644 --- a/deployment/api/app.py +++ b/deployment/api/app.py @@ -53,7 +53,7 @@ if st.session_state.prediction_result: sentiment = st.session_state.prediction_result["sentiment"].capitalize() prob = st.session_state.prediction_result["probability"] st.success(f"Sentiment : {sentiment}") - st.info(f"Probabilité : {prob:.2f}") + st.info(f"Probabilité que le tweet soit positif: {prob:.2f}") # Gestion du bouton signalement if st.session_state.show_report_button and not st.session_state.report_sent: @@ -61,15 +61,19 @@ if st.session_state.show_report_button and not st.session_state.report_sent: try: response = requests.post( "http://localhost:8000/report_error", - json={"tweet": tweet, "prediction": sentiment} + json={ + "tweet": tweet, + "prediction": sentiment, + "probability": st.session_state.prediction_result["probability"] # Transmettre la probabilité + } ) + if response.status_code == 200: data = response.json() # Mettre à jour les états st.session_state.show_report_button = False st.session_state.report_sent = True st.session_state.last_report_sent = data.get("report_sent", False) - # Forcer un re-run st.rerun() else: diff --git a/deployment/api/main.py b/deployment/api/main.py index 67ec948..a123e43 100644 --- a/deployment/api/main.py +++ b/deployment/api/main.py @@ -9,16 +9,20 @@ from mlflow.tracking import MlflowClient from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.sequence import pad_sequences -# 🔁 Import de la fonction de prétraitement depuis ../../functions.py +# A DECOMMENTER POUR DEVELOPPER. AUTREMENT, LAISSER AINSI +# Accéder à .env PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) sys.path.append(PARENT_DIR) -from functions import preprocess_tweet +# 🔐 Charger les variables d'environnement (.env à deux niveaux au-dessus) +load_dotenv(dotenv_path=os.path.join(PARENT_DIR, ".env")) + + +# 🔁 Import de la fonction de prétraitement (LOCAL) +from preprocessing 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")) # 🔗 Configurer MLflow mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI") @@ -38,6 +42,7 @@ class TweetRequest(BaseModel): class ReportRequest(BaseModel): tweet: str prediction: str + probability: float # 🧠 Stockage des signalements d'erreurs error_reports = {} @@ -58,6 +63,7 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE): # Extraire le tag d'embedding model_version_info = client.get_model_version(model_name, version_info.version) + # glove est ici la valeur par défaut si embedding_type n'est pas trouvé. embedding_type = model_version_info.tags.get("embedding_type", "glove").lower() # Déterminer les chemins des artefacts @@ -72,6 +78,8 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE): keras_file = "final_model_LSTM_Word2Vec-Fige.keras" else: raise ValueError(f"Embedding type inconnu : {embedding_type}") + + print(f"Modèle {embedding_type} trouvé. Téléchargement du modèle lié au run_id suivant : {run_id}") def dl(path): return client.download_artifacts(run_id=run_id, path=f"{base_path}/{path}", dst_path=local_dir) @@ -117,7 +125,11 @@ def predict_sentiment(request: TweetRequest): sequence = tokenizer.texts_to_sequences([processed_tweet]) sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1]) prediction_prob = model.predict(sequence_padded)[0][0] - sentiment = "positif" if prediction_prob >= 0.5 else "negatif" + if prediction_prob >=0.5: + sentiment = "positif" + else: + sentiment = "negatif" + # prediction_prob = 1-prediction_prob except Exception as e: raise HTTPException(status_code=500, detail=f"Erreur d'inférence : {e}") @@ -126,35 +138,65 @@ def predict_sentiment(request: TweetRequest): "probability": float(prediction_prob) } +# @app.post("/report_error") +# def report_error(request: ReportRequest): +# tweet = request.tweet.strip() +# prediction = request.prediction.strip().lower() +# probability = request.probability + +# if not tweet or not prediction: +# raise HTTPException(status_code=400, detail="Tweet ou prédiction manquants.") +# if probability < 0 or probability > 1: +# raise HTTPException(status_code=400, detail="Probabilité invalide. Elle doit être entre 0 et 1.") + +# # Ajouter ou mettre à jour le signalement du tweet avec la probabilité +# error_reports[tweet] = prediction + ' (p = ' + str(probability) + ') ' + +# # ✅ Envoi de l'email sans attendre un multiple de 3 +# try: +# email_success = send_error_report_email(error_reports.copy()) +# if email_success: +# print(f"✅ Email envoyé avec succès pour {len(error_reports)} signalements") +# report_sent = True +# else: +# print(f"❌ Échec de l'envoi de l'email") +# report_sent = False +# except Exception as e: +# print(f"❌ Erreur lors de l'envoi de l'email : {e}") +# report_sent = False + +# return {"report_sent": report_sent} + + + @app.post("/report_error") def report_error(request: ReportRequest): tweet = request.tweet.strip() prediction = request.prediction.strip().lower() - + probability = request.probability + 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 - + if probability < 0 or probability > 1: + raise HTTPException(status_code=400, detail="Probabilité invalide. Elle doit être entre 0 et 1.") + + # Ajouter ou mettre à jour le signalement du tweet avec la probabilité + error_reports[tweet] = prediction + f' (p = {probability:.2f} ) ' + report_sent = False - - # Condition : à chaque multiple de 3 signalements totaux - if len(error_reports) % 3 == 0: - report_sent = True - - # 📧 NOUVEAU : Envoyer l'email de rapport - try: + + # Condition : À chaque multiple de 3 signalements, un mail est envoyé + try: + if len(error_reports) % 3 == 0: # La condition reste + report_sent = True 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} + 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} \ No newline at end of file diff --git a/deployment/api/preprocessing.py b/deployment/api/preprocessing.py new file mode 100644 index 0000000..5c54737 --- /dev/null +++ b/deployment/api/preprocessing.py @@ -0,0 +1,44 @@ +import re +import html +import nltk +from nltk.corpus import stopwords +from nltk.tokenize import word_tokenize +from nltk.stem import WordNetLemmatizer + +# Assurer que les ressources nécessaires sont disponibles +try: + stopwords.words('english') + word_tokenize("test") + WordNetLemmatizer().lemmatize("testing") +except LookupError: + nltk.download('punkt') + nltk.download('punkt_tab') + nltk.download('stopwords') + nltk.download('wordnet') + nltk.download('omw-1.4') + +def preprocess_tweet(tweet): + """ + Fonction de prétraitement des tweets pour l'API de sentiment analysis + """ + if not isinstance(tweet, str): + return "" + tweet = html.unescape(tweet) + tweet = tweet.lower() + tweet = re.sub(r'https?://\S+|www\.\S+', '', tweet) + tweet = re.sub(r'@\w+', '', tweet) + tweet = re.sub(r'#(\w+)', r'# \1', tweet) + tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet) + + tokens = word_tokenize(tweet) + + lemmatizer = WordNetLemmatizer() + tokens = [lemmatizer.lemmatize(token) for token in tokens] + + stop_words = set(stopwords.words('english')) + important_words = {'no', 'not', 'nor', 'neither', 'never', 'nobody', 'none', 'nothing', 'nowhere'} + stop_words = stop_words - important_words + + tokens = [token for token in tokens if token not in stop_words] + + return ' '.join(tokens) \ No newline at end of file diff --git a/deployment/api/test_alias_email.py b/deployment/api/test_alias_email.py deleted file mode 100644 index e6b5322..0000000 --- a/deployment/api/test_alias_email.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/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 diff --git a/deployment/api/tests/test_email_service.py b/deployment/api/tests/test_email_service.py new file mode 100644 index 0000000..dae5002 --- /dev/null +++ b/deployment/api/tests/test_email_service.py @@ -0,0 +1,77 @@ +#!/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'}") + + +import os +import sys +from dotenv import load_dotenv + +# Ajouter le répertoire parent au path pour importer email_service +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) + +from email_service import send_error_report_email + +# Charger le fichier .env situé un niveau au-dessus +dotenv_path = '../../../.env' +load_dotenv(dotenv_path) + +def test_email_service_with_alias(): + """ + Teste l'envoi d'un email avec la fonction send_error_report_email + """ + # ⚙️ Vérifier si les variables d'environnement nécessaires sont présentes + smtp_server = os.getenv("SMTP_SERVER") + smtp_email = os.getenv("SMTP_EMAIL") + admin_email = os.getenv("ADMIN_EMAIL") + + assert smtp_server, "SMTP_SERVER non défini dans le fichier .env" + assert smtp_email, "SMTP_EMAIL non défini dans le fichier .env" + assert admin_email, "ADMIN_EMAIL non défini dans le fichier .env" + + # Configuration de test + print("🔧 Configuration Email :") + print(f"Serveur SMTP : {smtp_server}") + print(f"Authentification : {smtp_email}") + print(f"Destinataire : {admin_email}") + + # Cas de test + test_reports = { + "Service client décevant": "positif", + "Erreur à l'embarquement": "negatif", + "Vol annulé sans prévenir": "positif" + } + + # 📧 Tester la fonction d'envoi + result = send_error_report_email(test_reports) + + # ✅ Assertion pour vérifier que la fonction fonctionne correctement + assert result is True, "L'email n'a pas été envoyé avec succès" \ No newline at end of file diff --git a/deployment/api/tests/test_main.py b/deployment/api/tests/test_main.py new file mode 100644 index 0000000..1d66f8b --- /dev/null +++ b/deployment/api/tests/test_main.py @@ -0,0 +1,27 @@ +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_root_endpoint(): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"message": "API Sentiment - Air Paradis - en ligne"} + +def test_predict_sentiment(): + response = client.post("/predict", json={"tweet": "J'adore cette compagnie aérienne !"}) + assert response.status_code == 200 + data = response.json() + assert "sentiment" in data + assert "probability" in data + assert data["sentiment"] in ["positif", "negatif"] + +def test_missing_tweet(): + response = client.post("/predict", json={}) + assert response.status_code == 422 # Unprocessable Entity (FastAPI) + +def test_empty_tweet(): + response = client.post("/predict", json={"tweet": ""}) + assert response.status_code == 400 + assert response.json()["detail"] == "Tweet vide ou invalide après prétraitement." \ No newline at end of file diff --git a/deployment/api/tests/test_preprocessing.py b/deployment/api/tests/test_preprocessing.py new file mode 100644 index 0000000..a5ae557 --- /dev/null +++ b/deployment/api/tests/test_preprocessing.py @@ -0,0 +1,36 @@ +import sys +import os + +# Ajouter le répertoire parent au path pour importer email_service +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) +from preprocessing import preprocess_tweet + + +def test_preprocess_tweet_removes_hashtags(): + tweet = """ + Just flew w/ @AirParadis 🚀 & I’m in LOVE! Épic service, comfy seats. + #bestflight #AirParadis + """ + assert preprocess_tweet(tweet) == """" + aze + """ + +def test_preprocess_tweet_removes_mentions(): + tweet = """ + "Flight of dreams" w/ @AirParadis! Got food, space & smiles 😄 I’ll book again! + #AirParadis + """ + assert preprocess_tweet(tweet) == "merci" + +def test_preprocess_tweet_lowercases_text(): + tweet = """ + My 3rd time w/ @AirParadis & still amazed 😍 Crew = lovely à every step! + #flyhappy #AirParadis + """ + assert preprocess_tweet(tweet) == "je suis content" + +tweet = """ +Just flew w/ @AirParadis 🚀 & I’m in LOVE! Épic service, comfy seats. +#bestflight #AirParadis +""" +preprocess_tweet(tweet) \ No newline at end of file diff --git a/functions.py b/functions.py index e97040b..7663a65 100644 --- a/functions.py +++ b/functions.py @@ -39,12 +39,11 @@ def downsample_data(df, n_samples=50000, random_state=42): def preprocess_tweet(tweet): if not isinstance(tweet, str): return "" - + tweet = html.unescape(tweet) tweet = tweet.lower() tweet = re.sub(r'https?://\S+|www\.\S+', '', tweet) tweet = re.sub(r'@\w+', '', tweet) tweet = re.sub(r'#(\w+)', r'# \1', tweet) - tweet = html.unescape(tweet) tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet) tokens = word_tokenize(tweet)