debugged unit tests
This commit is contained in:
parent
f1e55e19a3
commit
7bc95f26b9
2 changed files with 49 additions and 52 deletions
|
|
@ -11,33 +11,31 @@ from tensorflow.keras.preprocessing.sequence import pad_sequences
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
# A DECOMMENTER POUR DEVELOPPER. AUTREMENT, LAISSER AINSI
|
# TO UNCOMMENT FOR DEVELOPMENT. OTHERWISE, LEAVE AS IS
|
||||||
# Accéder à .env
|
# Access the .env file
|
||||||
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)
|
||||||
# 🔐 Charger les variables d'environnement (.env à deux niveaux au-dessus)
|
# 🔐 Load environment variables (.env two levels above)
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
# 🔁 Import the preprocessing function (LOCAL)
|
||||||
# 🔁 Import de la fonction de prétraitement (LOCAL)
|
|
||||||
from preprocessing import preprocess_tweet
|
from preprocessing import preprocess_tweet
|
||||||
|
|
||||||
# 📧 Import du service email
|
# 📧 Import the email service
|
||||||
from email_service import send_error_report_email
|
from email_service import send_error_report_email
|
||||||
|
|
||||||
|
# 🔗 Configure MLflow
|
||||||
# 🔗 Configurer MLflow
|
|
||||||
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI")
|
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI")
|
||||||
if mlflow_tracking_uri:
|
if mlflow_tracking_uri:
|
||||||
mlflow.set_tracking_uri(mlflow_tracking_uri)
|
mlflow.set_tracking_uri(mlflow_tracking_uri)
|
||||||
else:
|
else:
|
||||||
print("⚠️ MLFLOW_TRACKING_URI non défini dans .env")
|
print("⚠️ MLFLOW_TRACKING_URI not defined in .env")
|
||||||
|
|
||||||
# 📌 Nom du modèle et stage
|
# 📌 Model name and stage
|
||||||
MODEL_NAME = "SentimentAnalysisLSTM"
|
MODEL_NAME = "SentimentAnalysisLSTM"
|
||||||
STAGE = "Production"
|
STAGE = "Production"
|
||||||
|
|
||||||
# 🧾 Structures des requêtes POST
|
# 🧾 Structure of POST requests
|
||||||
class TweetRequest(BaseModel):
|
class TweetRequest(BaseModel):
|
||||||
tweet: str
|
tweet: str
|
||||||
|
|
||||||
|
|
@ -46,29 +44,29 @@ class ReportRequest(BaseModel):
|
||||||
prediction: str
|
prediction: str
|
||||||
probability: float
|
probability: float
|
||||||
|
|
||||||
# 🧠 Stockage des signalements d'erreurs
|
# 🧠 Storage for error reports
|
||||||
error_reports = {}
|
error_reports = {}
|
||||||
|
|
||||||
# 🔍 Fonction pour charger modèle + tokenizer
|
# 🔍 Function to load model + tokenizer
|
||||||
def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE):
|
def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE):
|
||||||
client = MlflowClient()
|
client = MlflowClient()
|
||||||
versions = client.get_latest_versions(model_name, [stage])
|
versions = client.get_latest_versions(model_name, [stage])
|
||||||
|
|
||||||
if not versions:
|
if not versions:
|
||||||
raise RuntimeError(f"Aucune version du modèle '{model_name}' en stage '{stage}'")
|
raise RuntimeError(f"No version found for model '{model_name}' in stage '{stage}'")
|
||||||
|
|
||||||
version_info = versions[0]
|
version_info = versions[0]
|
||||||
run_id = version_info.run_id
|
run_id = version_info.run_id
|
||||||
|
|
||||||
if not run_id:
|
if not run_id:
|
||||||
raise RuntimeError("Run ID introuvable.")
|
raise RuntimeError("Run ID not found.")
|
||||||
|
|
||||||
# Extraire le tag d'embedding
|
# Extract embedding tag
|
||||||
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é.
|
# glove is used as default if embedding_type is not found
|
||||||
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
|
# Determine paths to artifacts
|
||||||
local_dir = "./downloaded_artifacts"
|
local_dir = "./downloaded_artifacts"
|
||||||
os.makedirs(local_dir, exist_ok=True)
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
base_path = "local_artifacts"
|
base_path = "local_artifacts"
|
||||||
|
|
@ -79,72 +77,71 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE):
|
||||||
elif "word2vec" in embedding_type:
|
elif "word2vec" in embedding_type:
|
||||||
keras_file = "final_model_LSTM_Word2Vec-Fige.keras"
|
keras_file = "final_model_LSTM_Word2Vec-Fige.keras"
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Embedding type inconnu : {embedding_type}")
|
raise ValueError(f"Unknown embedding type: {embedding_type}")
|
||||||
|
|
||||||
print(f"Modèle {embedding_type} trouvé. Téléchargement du modèle lié au run_id suivant : {run_id}")
|
print(f"{embedding_type} model found. Downloading model associated with run_id: {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)
|
||||||
|
|
||||||
# 🔽 Télécharger les artefacts
|
# 🔽 Download artifacts
|
||||||
model_path = dl(keras_file)
|
model_path = dl(keras_file)
|
||||||
tokenizer_path = dl(tokenizer_file)
|
tokenizer_path = dl(tokenizer_file)
|
||||||
print("Contenu de downloaded_artifacts :", os.listdir(local_dir))
|
print("Contents of downloaded_artifacts:", os.listdir(local_dir))
|
||||||
|
|
||||||
|
# 📦 Load
|
||||||
# 📦 Chargement
|
|
||||||
model = load_model(model_path)
|
model = load_model(model_path)
|
||||||
with open(tokenizer_path, "rb") as f:
|
with open(tokenizer_path, "rb") as f:
|
||||||
tokenizer = pickle.load(f)
|
tokenizer = pickle.load(f)
|
||||||
|
|
||||||
return model, tokenizer, embedding_type
|
return model, tokenizer, embedding_type
|
||||||
|
|
||||||
# 🚀 Lancer l'API
|
# 🚀 Launch the API
|
||||||
app = FastAPI(title="API Prédiction Sentiment - Air Paradis")
|
app = FastAPI(title="Sentiment Prediction API - Air Paradis")
|
||||||
# app = FastAPI(title="API Prédiction Sentiment - Air Paradis",
|
# app = FastAPI(title="Sentiment Prediction API - Air Paradis",
|
||||||
# root_path="/api")
|
# root_path="/api")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model, tokenizer, embedding_type = load_model_and_artifacts()
|
model, tokenizer, embedding_type = load_model_and_artifacts()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n❌ Erreur lors du chargement des artefacts : {e}")
|
print(f"\n❌ Error loading artifacts: {e}")
|
||||||
model, tokenizer = None, None
|
model, tokenizer = None, None
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {"message": "API Sentiment - Air Paradis - en ligne"}
|
return {"message": "Sentiment API - Air Paradis - online"}
|
||||||
|
|
||||||
@app.post("/predict")
|
@app.post("/predict")
|
||||||
def predict_sentiment(request: TweetRequest):
|
def predict_sentiment(request: TweetRequest):
|
||||||
if model is None or tokenizer is None:
|
if model is None or tokenizer is None:
|
||||||
raise HTTPException(status_code=503, detail="Modèle ou artefacts indisponibles.")
|
raise HTTPException(status_code=503, detail="Model or artifacts unavailable.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processed_tweet = preprocess_tweet(request.tweet)
|
processed_tweet = preprocess_tweet(request.tweet)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Erreur de prétraitement : {e}")
|
raise HTTPException(status_code=500, detail=f"Preprocessing error: {e}")
|
||||||
|
|
||||||
if not processed_tweet.strip():
|
if not processed_tweet.strip():
|
||||||
raise HTTPException(status_code=400, detail="Tweet vide ou invalide après prétraitement.")
|
raise HTTPException(status_code=400, detail="Tweet is empty or invalid after preprocessing.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
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]
|
||||||
if prediction_prob >=0.5:
|
if prediction_prob >= 0.5:
|
||||||
sentiment = "positive"
|
sentiment = "positive"
|
||||||
else:
|
else:
|
||||||
sentiment = "negative"
|
sentiment = "negative"
|
||||||
# prediction_prob = 1-prediction_prob
|
# 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"Inference error: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"sentiment": sentiment,
|
"sentiment": sentiment,
|
||||||
"probability": float(prediction_prob)
|
"probability": float(prediction_prob)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remplacer le dict par un DataFrame global initial vide
|
# Replace the dict with a global empty DataFrame initially
|
||||||
error_reports_df = pd.DataFrame(columns=["tweet", "prediction", "probability", "timestamp"])
|
error_reports_df = pd.DataFrame(columns=["tweet", "prediction", "probability", "timestamp"])
|
||||||
|
|
||||||
@app.post("/report_error")
|
@app.post("/report_error")
|
||||||
|
|
@ -156,22 +153,22 @@ def report_error(request: ReportRequest):
|
||||||
probability = request.probability
|
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="Missing tweet or prediction.")
|
||||||
if probability < 0 or probability > 1:
|
if probability < 0 or probability > 1:
|
||||||
raise HTTPException(status_code=400, detail="Probabilité invalide. Elle doit être entre 0 et 1.")
|
raise HTTPException(status_code=400, detail="Invalid probability. Must be between 0 and 1.")
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
|
|
||||||
# Vérifier si le tweet existe déjà pour éviter doublons
|
# Check if tweet already exists to avoid duplicates
|
||||||
existing_idx = error_reports_df.index[error_reports_df["tweet"] == tweet].tolist()
|
existing_idx = error_reports_df.index[error_reports_df["tweet"] == tweet].tolist()
|
||||||
if existing_idx:
|
if existing_idx:
|
||||||
# Mettre à jour la ligne existante
|
# Update existing row
|
||||||
idx = existing_idx[0]
|
idx = existing_idx[0]
|
||||||
error_reports_df.at[idx, "prediction"] = prediction + f' (p = {probability:.2f} ) '
|
error_reports_df.at[idx, "prediction"] = prediction + f' (p = {probability:.2f} ) '
|
||||||
error_reports_df.at[idx, "probability"] = probability
|
error_reports_df.at[idx, "probability"] = probability
|
||||||
error_reports_df.at[idx, "timestamp"] = now
|
error_reports_df.at[idx, "timestamp"] = now
|
||||||
else:
|
else:
|
||||||
# Ajouter une nouvelle ligne
|
# Add new row
|
||||||
new_row = {
|
new_row = {
|
||||||
"tweet": tweet,
|
"tweet": tweet,
|
||||||
"prediction": prediction + f' (p = {probability:.2f} ) ',
|
"prediction": prediction + f' (p = {probability:.2f} ) ',
|
||||||
|
|
@ -182,7 +179,7 @@ def report_error(request: ReportRequest):
|
||||||
|
|
||||||
report_sent = False
|
report_sent = False
|
||||||
|
|
||||||
# Condition : multiples de 3 signalements et délai max 5 minutes entre les 3 derniers
|
# Condition: every 3 reports and max 5 min interval between the last 3
|
||||||
try:
|
try:
|
||||||
if len(error_reports_df) % 3 == 0:
|
if len(error_reports_df) % 3 == 0:
|
||||||
last_three = error_reports_df.sort_values("timestamp", ascending=False).head(3)
|
last_three = error_reports_df.sort_values("timestamp", ascending=False).head(3)
|
||||||
|
|
@ -193,20 +190,20 @@ def report_error(request: ReportRequest):
|
||||||
last_three["tweet"],
|
last_three["tweet"],
|
||||||
last_three["prediction"]
|
last_three["prediction"]
|
||||||
))
|
))
|
||||||
print(f"📧 Tentative d'envoi d'email pour {len(error_reports_df)} signalements...")
|
print(f"📧 Attempting to send email for {len(error_reports_df)} reports...")
|
||||||
email_success = send_error_report_email(reports_dict)
|
email_success = send_error_report_email(reports_dict)
|
||||||
if email_success:
|
if email_success:
|
||||||
print(f"✅ Email envoyé avec succès pour {len(error_reports_df)} signalements")
|
print(f"✅ Email successfully sent for {len(error_reports_df)} reports")
|
||||||
report_sent = True
|
report_sent = True
|
||||||
# SUPPRESSION des 3 derniers signalements envoyés
|
# DELETE the last 3 reports sent
|
||||||
# Identifier les index à supprimer
|
# Identify indexes to delete
|
||||||
to_remove_idx = last_three.index
|
to_remove_idx = last_three.index
|
||||||
error_reports_df = error_reports_df.drop(to_remove_idx).reset_index(drop=True)
|
error_reports_df = error_reports_df.drop(to_remove_idx).reset_index(drop=True)
|
||||||
else:
|
else:
|
||||||
print(f"❌ Échec de l'envoi de l'email pour {len(error_reports_df)} signalements")
|
print(f"❌ Failed to send email for {len(error_reports_df)} reports")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur lors de l'envoi de l'email : {e}")
|
print(f"❌ Error while sending email: {e}")
|
||||||
# Continuer même si échec
|
# Continue even if it fails
|
||||||
|
|
||||||
return {"report_sent": report_sent}
|
return {"report_sent": report_sent}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ client = TestClient(app)
|
||||||
def test_root_endpoint():
|
def test_root_endpoint():
|
||||||
response = client.get("/")
|
response = client.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"message": "API Sentiment - Air Paradis - en ligne"}
|
assert response.json() == {"message": "Sentiment API - Air Paradis - online"}
|
||||||
|
|
||||||
def test_predict_sentiment():
|
def test_predict_sentiment():
|
||||||
response = client.post("/predict", json={"tweet": "I love this airline !"})
|
response = client.post("/predict", json={"tweet": "I love this airline !"})
|
||||||
|
|
@ -29,4 +29,4 @@ def test_missing_tweet():
|
||||||
def test_empty_tweet():
|
def test_empty_tweet():
|
||||||
response = client.post("/predict", json={"tweet": ""})
|
response = client.post("/predict", json={"tweet": ""})
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.json()["detail"] == "Empty or invalid tweet after pre-processing."
|
assert response.json()["detail"] == "Tweet is empty or invalid after preprocessing."
|
||||||
Loading…
Add table
Reference in a new issue