Added prediction probabilities on error reports + added unit tests (not finished yet)

This commit is contained in:
Grégoire Mureau 2025-07-07 10:30:41 +02:00
parent 89997585c6
commit 6416bcd075
9 changed files with 258 additions and 60 deletions

View file

@ -53,7 +53,7 @@ if st.session_state.prediction_result:
sentiment = st.session_state.prediction_result["sentiment"].capitalize() sentiment = st.session_state.prediction_result["sentiment"].capitalize()
prob = st.session_state.prediction_result["probability"] prob = st.session_state.prediction_result["probability"]
st.success(f"Sentiment : {sentiment}") 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 # Gestion du bouton signalement
if st.session_state.show_report_button and not st.session_state.report_sent: 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: try:
response = requests.post( response = requests.post(
"http://localhost:8000/report_error", "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: if response.status_code == 200:
data = response.json() data = response.json()
# Mettre à jour les états # Mettre à jour les états
st.session_state.show_report_button = False st.session_state.show_report_button = False
st.session_state.report_sent = True st.session_state.report_sent = True
st.session_state.last_report_sent = data.get("report_sent", False) st.session_state.last_report_sent = data.get("report_sent", False)
# Forcer un re-run # Forcer un re-run
st.rerun() st.rerun()
else: else:

View file

@ -9,16 +9,20 @@ from mlflow.tracking import MlflowClient
from tensorflow.keras.models import load_model from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing.sequence import pad_sequences 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__), "../../")) PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
sys.path.append(PARENT_DIR) 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 # 📧 Import du service email
from email_service import send_error_report_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 # 🔗 Configurer MLflow
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI") mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI")
@ -38,6 +42,7 @@ class TweetRequest(BaseModel):
class ReportRequest(BaseModel): class ReportRequest(BaseModel):
tweet: str tweet: str
prediction: str prediction: str
probability: float
# 🧠 Stockage des signalements d'erreurs # 🧠 Stockage des signalements d'erreurs
error_reports = {} error_reports = {}
@ -58,6 +63,7 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE):
# Extraire le tag d'embedding # Extraire le tag d'embedding
model_version_info = client.get_model_version(model_name, version_info.version) 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() embedding_type = model_version_info.tags.get("embedding_type", "glove").lower()
# Déterminer les chemins des artefacts # Déterminer les chemins des artefacts
@ -73,6 +79,8 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE):
else: else:
raise ValueError(f"Embedding type inconnu : {embedding_type}") 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): def dl(path):
return client.download_artifacts(run_id=run_id, path=f"{base_path}/{path}", dst_path=local_dir) 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 = tokenizer.texts_to_sequences([processed_tweet])
sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1]) sequence_padded = pad_sequences(sequence, maxlen=model.input_shape[1])
prediction_prob = model.predict(sequence_padded)[0][0] 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: except Exception as e:
raise HTTPException(status_code=500, detail=f"Erreur d'inférence : {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) "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") @app.post("/report_error")
def report_error(request: ReportRequest): def report_error(request: ReportRequest):
tweet = request.tweet.strip() tweet = request.tweet.strip()
prediction = request.prediction.strip().lower() prediction = request.prediction.strip().lower()
probability = request.probability
if not tweet or not prediction: if not tweet or not prediction:
raise HTTPException(status_code=400, detail="Tweet ou prédiction manquants.") 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 # Ajouter ou mettre à jour le signalement du tweet avec la probabilité
error_reports[tweet] = prediction error_reports[tweet] = prediction + f' (p = {probability:.2f} ) '
report_sent = False report_sent = False
# Condition : à chaque multiple de 3 signalements totaux # Condition : À chaque multiple de 3 signalements, un mail est envoyé
if len(error_reports) % 3 == 0: try:
report_sent = True if len(error_reports) % 3 == 0: # La condition reste
report_sent = True
# 📧 NOUVEAU : Envoyer l'email de rapport
try:
print(f"📧 Tentative d'envoi d'email pour {len(error_reports)} signalements...") print(f"📧 Tentative d'envoi d'email pour {len(error_reports)} signalements...")
email_success = send_error_report_email(error_reports.copy()) email_success = send_error_report_email(error_reports.copy())
if email_success: if email_success:
print(f"✅ Email envoyé avec succès pour {len(error_reports)} signalements") print(f"✅ Email envoyé avec succès pour {len(error_reports)} signalements")
else: else:
print(f"❌ Échec de l'envoi de l'email pour {len(error_reports)} signalements") print(f"❌ Échec de l'envoi de l'email pour {len(error_reports)} signalements")
except Exception as e:
except Exception as e: print(f"❌ Erreur lors de l'envoi de l'email : {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
# On continue même si l'email échoue pour ne pas bloquer l'API
return {"report_sent": report_sent} return {"report_sent": report_sent}

View file

@ -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+', '<URL>', tweet)
tweet = re.sub(r'@\w+', '<MENTION>', 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)

View file

@ -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'}")

View file

@ -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"

View file

@ -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."

View file

@ -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 &#128640; &amp; I&#8217;m in LOVE! &Eacute;pic service, comfy seats.
#bestflight #AirParadis
"""
assert preprocess_tweet(tweet) == """"
aze
"""
def test_preprocess_tweet_removes_mentions():
tweet = """
&quot;Flight of dreams&quot; w/ @AirParadis! Got food, space &amp; smiles &#128516; I&#8217;ll book again!
#AirParadis
"""
assert preprocess_tweet(tweet) == "merci"
def test_preprocess_tweet_lowercases_text():
tweet = """
My 3rd time w/ @AirParadis &amp; still amazed &#128525; Crew = lovely &agrave; every step!
#flyhappy #AirParadis
"""
assert preprocess_tweet(tweet) == "je suis content"
tweet = """
Just flew w/ @AirParadis &#128640; &amp; I&#8217;m in LOVE! &Eacute;pic service, comfy seats.
#bestflight #AirParadis
"""
preprocess_tweet(tweet)

View file

@ -39,12 +39,11 @@ def downsample_data(df, n_samples=50000, random_state=42):
def preprocess_tweet(tweet): def preprocess_tweet(tweet):
if not isinstance(tweet, str): if not isinstance(tweet, str):
return "" return ""
tweet = html.unescape(tweet)
tweet = tweet.lower() tweet = tweet.lower()
tweet = re.sub(r'https?://\S+|www\.\S+', '<URL>', tweet) tweet = re.sub(r'https?://\S+|www\.\S+', '<URL>', tweet)
tweet = re.sub(r'@\w+', '<MENTION>', tweet) tweet = re.sub(r'@\w+', '<MENTION>', tweet)
tweet = re.sub(r'#(\w+)', r'# \1', tweet) tweet = re.sub(r'#(\w+)', r'# \1', tweet)
tweet = html.unescape(tweet)
tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet) tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet)
tokens = word_tokenize(tweet) tokens = word_tokenize(tweet)