diff --git a/deployment/api/main.py b/deployment/api/main.py index 20a22fa..b6288c2 100644 --- a/deployment/api/main.py +++ b/deployment/api/main.py @@ -11,33 +11,31 @@ from tensorflow.keras.preprocessing.sequence import pad_sequences import pandas as pd from datetime import datetime, timedelta -# A DECOMMENTER POUR DEVELOPPER. AUTREMENT, LAISSER AINSI -# Accéder à .env +# TO UNCOMMENT FOR DEVELOPMENT. OTHERWISE, LEAVE AS IS +# Access the .env file PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) sys.path.append(PARENT_DIR) -# 🔐 Charger les variables d'environnement (.env à deux niveaux au-dessus) +# 🔐 Load environment variables (.env two levels above) load_dotenv() - -# 🔁 Import de la fonction de prétraitement (LOCAL) +# 🔁 Import the preprocessing function (LOCAL) from preprocessing import preprocess_tweet -# 📧 Import du service email +# 📧 Import the email service from email_service import send_error_report_email - -# 🔗 Configurer MLflow +# 🔗 Configure MLflow mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI") if mlflow_tracking_uri: mlflow.set_tracking_uri(mlflow_tracking_uri) 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" STAGE = "Production" -# 🧾 Structures des requêtes POST +# 🧾 Structure of POST requests class TweetRequest(BaseModel): tweet: str @@ -46,29 +44,29 @@ class ReportRequest(BaseModel): prediction: str probability: float -# 🧠 Stockage des signalements d'erreurs +# 🧠 Storage for 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): 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}'") + raise RuntimeError(f"No version found for model '{model_name}' in stage '{stage}'") version_info = versions[0] run_id = version_info.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) - # 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() - # Déterminer les chemins des artefacts + # Determine paths to artifacts local_dir = "./downloaded_artifacts" os.makedirs(local_dir, exist_ok=True) base_path = "local_artifacts" @@ -79,72 +77,71 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE): elif "word2vec" in embedding_type: keras_file = "final_model_LSTM_Word2Vec-Fige.keras" 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): 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) tokenizer_path = dl(tokenizer_file) - print("Contenu de downloaded_artifacts :", os.listdir(local_dir)) + print("Contents of downloaded_artifacts:", os.listdir(local_dir)) - - # 📦 Chargement + # 📦 Load model = load_model(model_path) with open(tokenizer_path, "rb") as f: tokenizer = pickle.load(f) return model, tokenizer, embedding_type -# 🚀 Lancer l'API -app = FastAPI(title="API Prédiction Sentiment - Air Paradis") -# app = FastAPI(title="API Prédiction Sentiment - Air Paradis", +# 🚀 Launch the API +app = FastAPI(title="Sentiment Prediction API - Air Paradis") +# app = FastAPI(title="Sentiment Prediction API - Air Paradis", # root_path="/api") try: model, tokenizer, embedding_type = load_model_and_artifacts() 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 @app.get("/") def root(): - return {"message": "API Sentiment - Air Paradis - en ligne"} + return {"message": "Sentiment API - Air Paradis - online"} @app.post("/predict") def predict_sentiment(request: TweetRequest): 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: processed_tweet = preprocess_tweet(request.tweet) 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(): - 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: 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] - if prediction_prob >=0.5: + if prediction_prob >= 0.5: sentiment = "positive" else: sentiment = "negative" - # prediction_prob = 1-prediction_prob + # prediction_prob = 1 - prediction_prob 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 { "sentiment": sentiment, "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"]) @app.post("/report_error") @@ -156,22 +153,22 @@ def report_error(request: ReportRequest): probability = request.probability 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: - 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() - # 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() if existing_idx: - # Mettre à jour la ligne existante + # Update existing row idx = existing_idx[0] error_reports_df.at[idx, "prediction"] = prediction + f' (p = {probability:.2f} ) ' error_reports_df.at[idx, "probability"] = probability error_reports_df.at[idx, "timestamp"] = now else: - # Ajouter une nouvelle ligne + # Add new row new_row = { "tweet": tweet, "prediction": prediction + f' (p = {probability:.2f} ) ', @@ -182,7 +179,7 @@ def report_error(request: ReportRequest): 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: if len(error_reports_df) % 3 == 0: 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["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) 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 - # SUPPRESSION des 3 derniers signalements envoyés - # Identifier les index à supprimer + # DELETE the last 3 reports sent + # Identify indexes to delete to_remove_idx = last_three.index error_reports_df = error_reports_df.drop(to_remove_idx).reset_index(drop=True) 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: - print(f"❌ Erreur lors de l'envoi de l'email : {e}") - # Continuer même si échec + print(f"❌ Error while sending email: {e}") + # Continue even if it fails - return {"report_sent": report_sent} \ No newline at end of file + return {"report_sent": report_sent} diff --git a/deployment/api/tests/test_main.py b/deployment/api/tests/test_main.py index 42d2a7c..f5a4882 100644 --- a/deployment/api/tests/test_main.py +++ b/deployment/api/tests/test_main.py @@ -12,7 +12,7 @@ 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"} + assert response.json() == {"message": "Sentiment API - Air Paradis - online"} def test_predict_sentiment(): response = client.post("/predict", json={"tweet": "I love this airline !"}) @@ -29,4 +29,4 @@ def test_missing_tweet(): def test_empty_tweet(): response = client.post("/predict", json={"tweet": ""}) assert response.status_code == 400 - assert response.json()["detail"] == "Empty or invalid tweet after pre-processing." \ No newline at end of file + assert response.json()["detail"] == "Tweet is empty or invalid after preprocessing." \ No newline at end of file