Added prediction probabilities on error reports + added unit tests (not finished yet)
This commit is contained in:
parent
89997585c6
commit
6416bcd075
9 changed files with 258 additions and 60 deletions
Binary file not shown.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -73,6 +79,8 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE):
|
|||
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.")
|
||||
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
|
||||
error_reports[tweet] = prediction
|
||||
# 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
|
||||
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}
|
||||
44
deployment/api/preprocessing.py
Normal file
44
deployment/api/preprocessing.py
Normal 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)
|
||||
|
|
@ -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'}")
|
||||
77
deployment/api/tests/test_email_service.py
Normal file
77
deployment/api/tests/test_email_service.py
Normal 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"
|
||||
27
deployment/api/tests/test_main.py
Normal file
27
deployment/api/tests/test_main.py
Normal 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."
|
||||
36
deployment/api/tests/test_preprocessing.py
Normal file
36
deployment/api/tests/test_preprocessing.py
Normal 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 🚀 & 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)
|
||||
|
|
@ -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+', '<URL>', tweet)
|
||||
tweet = re.sub(r'@\w+', '<MENTION>', tweet)
|
||||
tweet = re.sub(r'#(\w+)', r'# \1', tweet)
|
||||
tweet = html.unescape(tweet)
|
||||
tweet = re.sub(r'[^\w\s<>@#!?]', '', tweet)
|
||||
|
||||
tokens = word_tokenize(tweet)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue