From 799015be442beaa82a123a9f221bf0095e911b38 Mon Sep 17 00:00:00 2001 From: GrgMdmn Date: Mon, 10 Nov 2025 16:40:12 +0100 Subject: [PATCH] Initial commit of scripts --- README.MD | 450 +++++++++++++++---------------- azure-app/.funcignore | 16 +- azure-app/.gitignore | 2 +- azure-app/function_app.py | 168 ++++++------ azure-app/test_local.py | 182 ++++++------- azure-app/utils/content_based.py | 188 ++++++------- azure-app/utils/data_loader.py | 260 +++++++++--------- streamlit_app.py | 394 +++++++++++++-------------- streamlit_requirements.txt | 16 +- 9 files changed, 838 insertions(+), 838 deletions(-) diff --git a/README.MD b/README.MD index 7aaa535..fdbf375 100644 --- a/README.MD +++ b/README.MD @@ -1,226 +1,226 @@ -# Système de Recommandation de Contenu - My Content - -Projet de développement d'un système de recommandation d'articles basé sur une architecture serverless Azure Functions avec interface utilisateur Streamlit. - - -## Livrables du Projet -1. **Application fonctionnelle** : Streamlit + Azure Functions déployée -2. **Code source versionné** : Repository GitHub complet -3. **Présentation technique** : Support PDF (docs/presentation.pdf) - - -## Vue d'ensemble - -Ce projet implémente un système de recommandation de contenu utilisant des algorithmes de filtrage collaboratif et basé sur le contenu. L'architecture serverless permet une mise à l'échelle automatique et un coût optimisé. - -### Fonctionnalités principales - -- **API de recommandation** : Azure Function retournant le top 5 des articles recommandés pour un utilisateur -- **Interface utilisateur** : Application Streamlit pour tester et démontrer le système -- **Algorithmes implémentés** : Content-Based Filtering avec embeddings pré-calculés -- **Architecture serverless** : Déploiement sur Azure Functions avec plan Consumption - - -## Analyse et Développement - -### Notebook Jupyter -- **Exploration des données** : Analyse du dataset Kaggle Globo.com -- **Implémentation des algorithmes** : Content-Based et Collaborative Filtering -- **Comparaison des approches** : Évaluation et choix technique -- **Optimisation ACP** : Réduction dimensionnelle 250D → 50D - -Localisation : `notebook/p10_notebook.ipynb` - -## Architecture - -### MVP Actuel -``` -[Interface Streamlit] → [Azure Functions API] → [Algorithme de recommandation] - ↓ - [Données embeddings + historique utilisateur] -``` - -### Composants -- **Frontend** : Streamlit (interface de test) -- **Backend** : Azure Functions (Python 3.12, Linux) -- **Algorithme** : Content-Based Filtering avec similarité cosinus -- **Données** : Embeddings articles (50D via PCA vs 250D embeddings originaux) + historique des clics - -## Structure synthétique du Projet - -``` -Project_10/ -├── azure-app/ # Azure Function -│ ├── function_app.py # Point d'entrée Azure Function -│ ├── requirements.txt # Dépendances Python -│ ├── host.json # Configuration Azure Functions -│ ├── local.settings.json # Configuration locale (non versionnée) -│ ├── data/ -│ │ ├── embeddings_azure_50d.pkl # Embeddings articles (PCA 50D) -│ │ └── clicks_sample.csv # Historique des clics utilisateurs -│ └── utils/ -│ ├── data_loader.py # Chargement et cache des données -│ └── content_based.py # Algorithme de recommandation -├── notebook/ -│ └── p10_notebook.ipynb # Analyse données + développement modèles -├── docs/ -│ ├── presentation.pdf # Support de présentation (15-25 slides) -│ └── illustrations/ # Schémas d'architecture et captures -├── streamlit_app.py # Interface utilisateur -├── streamlit_requirements.txt # Dépendances Streamlit -└── README.md -``` - -## Installation et Déploiement - -### Prérequis -- Python 3.12+ -- Azure CLI -- Azure Functions Core Tools -- Compte Azure - -### Déploiement Azure Functions - -1. **Connexion à Azure** -```bash -az login -``` - -2. **Création de l'infrastructure** -```bash -# Groupe de ressources -az group create --name rg-content-recommender --location francecentral - -# Compte de stockage -az storage account create --name [votre-nom-stockage-unique] \ - --resource-group rg-content-recommender \ - --location francecentral \ - --sku Standard_LRS - -# Function App Linux avec Python 3.12 -az functionapp create \ - --resource-group rg-content-recommender \ - --consumption-plan-location francecentral \ - --runtime python \ - --runtime-version 3.12 \ - --functions-version 4 \ - --name contentrecommender-linux \ - --storage-account stcontentrecommend2025 \ - --os-type Linux -``` - -3. **Déploiement du code** -```bash -# Depuis le dossier azure-app -func azure functionapp publish contentrecommender-linux -``` - -### Interface Streamlit - -1. **Installation des dépendances** -```bash -pip install -r streamlit_requirements.txt -``` - -2. **Lancement de l'interface** -```bash -streamlit run streamlit_app.py -``` - -## Utilisation - -### API Azure Functions - -**Endpoint** : `https://contentrecommender-linux.azurewebsites.net/api/recommend` - -**Paramètres** : -- `user_id` (int) : Identifiant de l'utilisateur (0-706) - -**Exemple d'appel** : -```bash -curl "https://contentrecommender-linux.azurewebsites.net/api/recommend?user_id=0" -``` - -**Réponse** : -```json -{ - "user_id": 0, - "recommendations": [157519, 157944, 159495, 156690, 162857] -} -``` - -### Interface Streamlit - -1. Configurer l'URL de l'API (local ou Azure) -2. Sélectionner un ID utilisateur -3. Obtenir les recommandations -4. Tester les fonctionnalités avancées - -## Algorithme de Recommandation - -### Content-Based Filtering - -1. **Chargement des données** : Embeddings pré-calculés et historique utilisateur -2. **Calcul du profil utilisateur** : Moyenne des embeddings des articles consultés -3. **Similarité cosinus** : Calcul avec tous les articles du catalogue -4. **Filtrage** : Exclusion des articles déjà vus -5. **Sélection** : Top 5 des articles les plus similaires - -### Optimisations - -- **Réduction dimensionnelle** : PCA 250D → 50D pour optimiser les performances Azure -- **Cache en mémoire** : Embeddings et données utilisateur chargés une seule fois -- **Architecture serverless** : Mise à l'échelle automatique selon la demande - -## Tests et Validation - -### Tests locaux -```bash -# Démarrage Azure Functions local -func start - -# Test de l'algorithme -python test_local.py -``` - -### Métriques de performance -- **Temps de réponse** : ~200ms par recommandation -- **Utilisateurs supportés** : 707 dans le dataset actuel -- **Articles disponibles** : 364,047 avec embeddings - -## Problèmes Connus et Solutions - -### Déploiement Azure -- **Python sur Windows** : Non supporté, utiliser Linux obligatoirement -- **Authentification** : Configurer `http_auth_level=func.AuthLevel.ANONYMOUS` -- **Erreurs "Service unavailable"** : Problèmes temporaires Azure, réessayer plus tard - -### Limitations actuelles -- **Cold start** : Première requête lente après période d'inactivité -- **Nouveaux utilisateurs** : Pas de gestion du problème de démarrage à froid -- **Mise à jour des données** : Processus manuel - -## Architecture Cible (Future) - -### Améliorations prévues -- **Gestion cold start** : Recommandations populaires pour nouveaux utilisateurs -- **Pipeline de données** : Mise à jour automatique des embeddings -- **Base de données** : Stockage persistant des profils utilisateur -- **API hybride** : Combinaison Content-Based + Collaborative Filtering -- **Cache distribué** : Précalcul des recommandations populaires - -## Technologies Utilisées - -- **Backend** : Azure Functions, Python 3.12 -- **Frontend** : Streamlit -- **ML/Data** : scikit-learn, pandas, numpy -- **Infrastructure** : Azure CLI, Azure Storage -- **Déploiement** : Azure Functions Core Tools - -## Auteur - -Projet réalisé dans le cadre d'une formation OpenClassrooms - Ingénieur IA. - -## Licence - +# Système de Recommandation de Contenu - My Content + +Projet de développement d'un système de recommandation d'articles basé sur une architecture serverless Azure Functions avec interface utilisateur Streamlit. + + +## Livrables du Projet +1. **Application fonctionnelle** : Streamlit + Azure Functions déployée +2. **Code source versionné** : Repository GitHub complet +3. **Présentation technique** : Support PDF (docs/presentation.pdf) + + +## Vue d'ensemble + +Ce projet implémente un système de recommandation de contenu utilisant des algorithmes de filtrage collaboratif et basé sur le contenu. L'architecture serverless permet une mise à l'échelle automatique et un coût optimisé. + +### Fonctionnalités principales + +- **API de recommandation** : Azure Function retournant le top 5 des articles recommandés pour un utilisateur +- **Interface utilisateur** : Application Streamlit pour tester et démontrer le système +- **Algorithmes implémentés** : Content-Based Filtering avec embeddings pré-calculés +- **Architecture serverless** : Déploiement sur Azure Functions avec plan Consumption + + +## Analyse et Développement + +### Notebook Jupyter +- **Exploration des données** : Analyse du dataset Kaggle Globo.com +- **Implémentation des algorithmes** : Content-Based et Collaborative Filtering +- **Comparaison des approches** : Évaluation et choix technique +- **Optimisation ACP** : Réduction dimensionnelle 250D → 50D + +Localisation : `notebook/p10_notebook.ipynb` + +## Architecture + +### MVP Actuel +``` +[Interface Streamlit] → [Azure Functions API] → [Algorithme de recommandation] + ↓ + [Données embeddings + historique utilisateur] +``` + +### Composants +- **Frontend** : Streamlit (interface de test) +- **Backend** : Azure Functions (Python 3.12, Linux) +- **Algorithme** : Content-Based Filtering avec similarité cosinus +- **Données** : Embeddings articles (50D via PCA vs 250D embeddings originaux) + historique des clics + +## Structure synthétique du Projet + +``` +Project_10/ +├── azure-app/ # Azure Function +│ ├── function_app.py # Point d'entrée Azure Function +│ ├── requirements.txt # Dépendances Python +│ ├── host.json # Configuration Azure Functions +│ ├── local.settings.json # Configuration locale (non versionnée) +│ ├── data/ +│ │ ├── embeddings_azure_50d.pkl # Embeddings articles (PCA 50D) +│ │ └── clicks_sample.csv # Historique des clics utilisateurs +│ └── utils/ +│ ├── data_loader.py # Chargement et cache des données +│ └── content_based.py # Algorithme de recommandation +├── notebook/ +│ └── p10_notebook.ipynb # Analyse données + développement modèles +├── docs/ +│ ├── presentation.pdf # Support de présentation (15-25 slides) +│ └── illustrations/ # Schémas d'architecture et captures +├── streamlit_app.py # Interface utilisateur +├── streamlit_requirements.txt # Dépendances Streamlit +└── README.md +``` + +## Installation et Déploiement + +### Prérequis +- Python 3.12+ +- Azure CLI +- Azure Functions Core Tools +- Compte Azure + +### Déploiement Azure Functions + +1. **Connexion à Azure** +```bash +az login +``` + +2. **Création de l'infrastructure** +```bash +# Groupe de ressources +az group create --name rg-content-recommender --location francecentral + +# Compte de stockage +az storage account create --name [votre-nom-stockage-unique] \ + --resource-group rg-content-recommender \ + --location francecentral \ + --sku Standard_LRS + +# Function App Linux avec Python 3.12 +az functionapp create \ + --resource-group rg-content-recommender \ + --consumption-plan-location francecentral \ + --runtime python \ + --runtime-version 3.12 \ + --functions-version 4 \ + --name contentrecommender-linux \ + --storage-account stcontentrecommend2025 \ + --os-type Linux +``` + +3. **Déploiement du code** +```bash +# Depuis le dossier azure-app +func azure functionapp publish contentrecommender-linux +``` + +### Interface Streamlit + +1. **Installation des dépendances** +```bash +pip install -r streamlit_requirements.txt +``` + +2. **Lancement de l'interface** +```bash +streamlit run streamlit_app.py +``` + +## Utilisation + +### API Azure Functions + +**Endpoint** : `https://contentrecommender-linux.azurewebsites.net/api/recommend` + +**Paramètres** : +- `user_id` (int) : Identifiant de l'utilisateur (0-706) + +**Exemple d'appel** : +```bash +curl "https://contentrecommender-linux.azurewebsites.net/api/recommend?user_id=0" +``` + +**Réponse** : +```json +{ + "user_id": 0, + "recommendations": [157519, 157944, 159495, 156690, 162857] +} +``` + +### Interface Streamlit + +1. Configurer l'URL de l'API (local ou Azure) +2. Sélectionner un ID utilisateur +3. Obtenir les recommandations +4. Tester les fonctionnalités avancées + +## Algorithme de Recommandation + +### Content-Based Filtering + +1. **Chargement des données** : Embeddings pré-calculés et historique utilisateur +2. **Calcul du profil utilisateur** : Moyenne des embeddings des articles consultés +3. **Similarité cosinus** : Calcul avec tous les articles du catalogue +4. **Filtrage** : Exclusion des articles déjà vus +5. **Sélection** : Top 5 des articles les plus similaires + +### Optimisations + +- **Réduction dimensionnelle** : PCA 250D → 50D pour optimiser les performances Azure +- **Cache en mémoire** : Embeddings et données utilisateur chargés une seule fois +- **Architecture serverless** : Mise à l'échelle automatique selon la demande + +## Tests et Validation + +### Tests locaux +```bash +# Démarrage Azure Functions local +func start + +# Test de l'algorithme +python test_local.py +``` + +### Métriques de performance +- **Temps de réponse** : ~200ms par recommandation +- **Utilisateurs supportés** : 707 dans le dataset actuel +- **Articles disponibles** : 364,047 avec embeddings + +## Problèmes Connus et Solutions + +### Déploiement Azure +- **Python sur Windows** : Non supporté, utiliser Linux obligatoirement +- **Authentification** : Configurer `http_auth_level=func.AuthLevel.ANONYMOUS` +- **Erreurs "Service unavailable"** : Problèmes temporaires Azure, réessayer plus tard + +### Limitations actuelles +- **Cold start** : Première requête lente après période d'inactivité +- **Nouveaux utilisateurs** : Pas de gestion du problème de démarrage à froid +- **Mise à jour des données** : Processus manuel + +## Architecture Cible (Future) + +### Améliorations prévues +- **Gestion cold start** : Recommandations populaires pour nouveaux utilisateurs +- **Pipeline de données** : Mise à jour automatique des embeddings +- **Base de données** : Stockage persistant des profils utilisateur +- **API hybride** : Combinaison Content-Based + Collaborative Filtering +- **Cache distribué** : Précalcul des recommandations populaires + +## Technologies Utilisées + +- **Backend** : Azure Functions, Python 3.12 +- **Frontend** : Streamlit +- **ML/Data** : scikit-learn, pandas, numpy +- **Infrastructure** : Azure CLI, Azure Storage +- **Déploiement** : Azure Functions Core Tools + +## Auteur + +Projet réalisé dans le cadre d'une formation OpenClassrooms - Ingénieur IA. + +## Licence + Ce projet est à des fins éducatives uniquement. \ No newline at end of file diff --git a/azure-app/.funcignore b/azure-app/.funcignore index 6bf0072..d4ad652 100644 --- a/azure-app/.funcignore +++ b/azure-app/.funcignore @@ -1,9 +1,9 @@ -.git* -.vscode -__azurite_db*__.json -__blobstorage__ -__queuestorage__ -local.settings.json -test -.venv +.git* +.vscode +__azurite_db*__.json +__blobstorage__ +__queuestorage__ +local.settings.json +test +.venv test_local.py \ No newline at end of file diff --git a/azure-app/.gitignore b/azure-app/.gitignore index 7685fc4..3e4bf64 100644 --- a/azure-app/.gitignore +++ b/azure-app/.gitignore @@ -131,5 +131,5 @@ local.settings.json # Azurite artifacts __blobstorage__ __queuestorage__ -__azurite_db*__.json +__azurite_db*__.json .python_packages \ No newline at end of file diff --git a/azure-app/function_app.py b/azure-app/function_app.py index 5b8c625..d4b879b 100644 --- a/azure-app/function_app.py +++ b/azure-app/function_app.py @@ -1,85 +1,85 @@ -import azure.functions as func -import logging -import json -import os -import sys - -# Ajouter le dossier utils au path pour les imports -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from utils.data_loader import load_embeddings, load_user_data, get_articles_categories -from utils.content_based import get_recommendations_with_scores - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - -@app.route(route="recommend") -def recommend(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Recommendation request received.') - - # Récupérer le user_id depuis les paramètres de requête - user_id = req.params.get('user_id') - - if not user_id: - try: - req_body = req.get_json() - user_id = req_body.get('user_id') if req_body else None - except ValueError: - pass - - if not user_id: - return func.HttpResponse( - json.dumps({"error": "user_id parameter required"}), - status_code=400, - mimetype="application/json" - ) - - try: - # Convertir en entier - user_id = int(user_id) - - # Générer les recommandations avec scores - recommendations = get_recommendations_with_scores(user_id) - - if recommendations: - # Enrichir avec les catégories - article_ids = [rec['article_id'] for rec in recommendations] - categories = get_articles_categories(article_ids) - - # Ajouter les catégories aux recommandations - enriched_recommendations = [] - for rec in recommendations: - enriched_rec = rec.copy() - enriched_rec['category_id'] = categories.get(rec['article_id']) - enriched_rec['similarity_score_percent'] = round(rec['similarity_score'] * 100, 2) - enriched_recommendations.append(enriched_rec) - - return func.HttpResponse( - json.dumps({ - "user_id": user_id, - "recommendations": enriched_recommendations - }), - status_code=200, - mimetype="application/json" - ) - else: - return func.HttpResponse( - json.dumps({ - "error": f"No recommendations found for user {user_id}" - }), - status_code=404, - mimetype="application/json" - ) - - except ValueError: - return func.HttpResponse( - json.dumps({"error": "user_id must be a valid integer"}), - status_code=400, - mimetype="application/json" - ) - except Exception as e: - logging.error(f"Error generating recommendations: {str(e)}") - return func.HttpResponse( - json.dumps({"error": "Internal server error"}), - status_code=500, - mimetype="application/json" +import azure.functions as func +import logging +import json +import os +import sys + +# Ajouter le dossier utils au path pour les imports +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from utils.data_loader import load_embeddings, load_user_data, get_articles_categories +from utils.content_based import get_recommendations_with_scores + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +@app.route(route="recommend") +def recommend(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Recommendation request received.') + + # Récupérer le user_id depuis les paramètres de requête + user_id = req.params.get('user_id') + + if not user_id: + try: + req_body = req.get_json() + user_id = req_body.get('user_id') if req_body else None + except ValueError: + pass + + if not user_id: + return func.HttpResponse( + json.dumps({"error": "user_id parameter required"}), + status_code=400, + mimetype="application/json" + ) + + try: + # Convertir en entier + user_id = int(user_id) + + # Générer les recommandations avec scores + recommendations = get_recommendations_with_scores(user_id) + + if recommendations: + # Enrichir avec les catégories + article_ids = [rec['article_id'] for rec in recommendations] + categories = get_articles_categories(article_ids) + + # Ajouter les catégories aux recommandations + enriched_recommendations = [] + for rec in recommendations: + enriched_rec = rec.copy() + enriched_rec['category_id'] = categories.get(rec['article_id']) + enriched_rec['similarity_score_percent'] = round(rec['similarity_score'] * 100, 2) + enriched_recommendations.append(enriched_rec) + + return func.HttpResponse( + json.dumps({ + "user_id": user_id, + "recommendations": enriched_recommendations + }), + status_code=200, + mimetype="application/json" + ) + else: + return func.HttpResponse( + json.dumps({ + "error": f"No recommendations found for user {user_id}" + }), + status_code=404, + mimetype="application/json" + ) + + except ValueError: + return func.HttpResponse( + json.dumps({"error": "user_id must be a valid integer"}), + status_code=400, + mimetype="application/json" + ) + except Exception as e: + logging.error(f"Error generating recommendations: {str(e)}") + return func.HttpResponse( + json.dumps({"error": "Internal server error"}), + status_code=500, + mimetype="application/json" ) \ No newline at end of file diff --git a/azure-app/test_local.py b/azure-app/test_local.py index 623ab2c..2752017 100644 --- a/azure-app/test_local.py +++ b/azure-app/test_local.py @@ -1,92 +1,92 @@ -import logging -import sys -import os - -# Configuration du logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) - -# Ajouter le dossier utils au path -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from utils.content_based import get_recommendations -from utils.data_loader import get_available_users, check_data_files, load_embeddings - -def test_data_loading(): - """Test du chargement des données.""" - print("🔄 Test du chargement des données...") - - if not check_data_files(): - print("❌ Fichiers de données manquants") - return False - - try: - # Test chargement embeddings - embeddings = load_embeddings() - print(f"✅ Embeddings chargés: {embeddings.shape}") - - # Test chargement utilisateurs - users = get_available_users() - print(f"✅ Utilisateurs chargés: {len(users)} utilisateurs") - print(f" Premiers utilisateurs: {users[:5]}") - - return True - except Exception as e: - print(f"❌ Erreur lors du chargement: {e}") - return False - -def test_recommendations(): - """Test des recommandations.""" - print("\n🔄 Test des recommandations...") - - users = get_available_users() - test_users = users[:3] if len(users) >= 3 else users - - for user_id in test_users: - print(f"\n👤 Test pour utilisateur {user_id}:") - - try: - recommendations = get_recommendations(user_id, n_recommendations=5) - - if recommendations: - print(f"✅ {len(recommendations)} recommandations générées") - print(f" Articles recommandés: {recommendations}") - else: - print("⚠️ Aucune recommandation générée") - - except Exception as e: - print(f"❌ Erreur pour utilisateur {user_id}: {e}") - -def test_edge_cases(): - """Test des cas limites.""" - print("\n🔄 Test des cas limites...") - - # Test utilisateur inexistant - print("Test utilisateur inexistant (ID: 99999):") - result = get_recommendations(99999) - if result is None: - print("✅ Utilisateur inexistant géré correctement") - else: - print(f"⚠️ Résultat inattendu: {result}") - -def main(): - """Fonction principale de test.""" - print("🚀 Démarrage des tests locaux\n") - - # Test 1: Chargement des données - if not test_data_loading(): - print("\n❌ Tests interrompus - problème de chargement") - return - - # Test 2: Recommandations - test_recommendations() - - # Test 3: Cas limites - test_edge_cases() - - print("\n✨ Tests terminés") - -if __name__ == "__main__": +import logging +import sys +import os + +# Configuration du logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +# Ajouter le dossier utils au path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from utils.content_based import get_recommendations +from utils.data_loader import get_available_users, check_data_files, load_embeddings + +def test_data_loading(): + """Test du chargement des données.""" + print("🔄 Test du chargement des données...") + + if not check_data_files(): + print("❌ Fichiers de données manquants") + return False + + try: + # Test chargement embeddings + embeddings = load_embeddings() + print(f"✅ Embeddings chargés: {embeddings.shape}") + + # Test chargement utilisateurs + users = get_available_users() + print(f"✅ Utilisateurs chargés: {len(users)} utilisateurs") + print(f" Premiers utilisateurs: {users[:5]}") + + return True + except Exception as e: + print(f"❌ Erreur lors du chargement: {e}") + return False + +def test_recommendations(): + """Test des recommandations.""" + print("\n🔄 Test des recommandations...") + + users = get_available_users() + test_users = users[:3] if len(users) >= 3 else users + + for user_id in test_users: + print(f"\n👤 Test pour utilisateur {user_id}:") + + try: + recommendations = get_recommendations(user_id, n_recommendations=5) + + if recommendations: + print(f"✅ {len(recommendations)} recommandations générées") + print(f" Articles recommandés: {recommendations}") + else: + print("⚠️ Aucune recommandation générée") + + except Exception as e: + print(f"❌ Erreur pour utilisateur {user_id}: {e}") + +def test_edge_cases(): + """Test des cas limites.""" + print("\n🔄 Test des cas limites...") + + # Test utilisateur inexistant + print("Test utilisateur inexistant (ID: 99999):") + result = get_recommendations(99999) + if result is None: + print("✅ Utilisateur inexistant géré correctement") + else: + print(f"⚠️ Résultat inattendu: {result}") + +def main(): + """Fonction principale de test.""" + print("🚀 Démarrage des tests locaux\n") + + # Test 1: Chargement des données + if not test_data_loading(): + print("\n❌ Tests interrompus - problème de chargement") + return + + # Test 2: Recommandations + test_recommendations() + + # Test 3: Cas limites + test_edge_cases() + + print("\n✨ Tests terminés") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/azure-app/utils/content_based.py b/azure-app/utils/content_based.py index 151ba2a..1d7e9cd 100644 --- a/azure-app/utils/content_based.py +++ b/azure-app/utils/content_based.py @@ -1,95 +1,95 @@ -import numpy as np -from sklearn.metrics.pairwise import cosine_similarity -from typing import List, Optional, Tuple, Dict -import logging - -from .data_loader import load_embeddings, get_user_articles - -def _calculate_mean_embedding(embeddings: np.ndarray, article_ids: List[int]) -> np.ndarray: - """Calcule la moyenne des embeddings pour une liste d'articles.""" - valid_ids = [aid for aid in article_ids if aid < len(embeddings)] - if not valid_ids: - raise ValueError("No valid article IDs found") - - articles_embeddings = embeddings[valid_ids] - return np.mean(articles_embeddings, axis=0).reshape(1, -1) - -def _compute_similarities(reference_embedding: np.ndarray, embeddings: np.ndarray) -> np.ndarray: - """Calcule la similarité cosinus entre l'embedding de référence et tous les embeddings.""" - return cosine_similarity(reference_embedding, embeddings)[0] - -def _filter_candidates(similarities: np.ndarray, clicked_articles: List[int]) -> List[Tuple[int, float]]: - """Filtre les candidats en excluant les articles déjà vus.""" - candidates = [] - for article_id, similarity in enumerate(similarities): - if article_id not in clicked_articles: - candidates.append((article_id, similarity)) - return candidates - -def _get_top_recommendations_with_scores(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[Dict[str, any]]: - """Trie les candidats et retourne le top N avec les scores.""" - candidates.sort(key=lambda x: x[1], reverse=True) - recommendations = [] - for article_id, score in candidates[:n_recommendations]: - recommendations.append({ - 'article_id': article_id, - 'similarity_score': float(score) - }) - return recommendations - -def _validate_user_articles(clicked_articles: List[int], embeddings: np.ndarray) -> List[int]: - """Valide et filtre les articles dans les limites des embeddings.""" - valid_articles = [art for art in clicked_articles if 0 <= art < len(embeddings)] - if not valid_articles: - raise ValueError("No valid articles found for user") - return valid_articles - -def get_recommendations(user_id: int, n_recommendations: int = 5) -> Optional[List[int]]: - """ - Génère des recommandations pour un utilisateur donné (version compatibilité). - Retourne seulement les IDs pour maintenir la compatibilité. - """ - recommendations_with_scores = get_recommendations_with_scores(user_id, n_recommendations) - if recommendations_with_scores: - return [rec['article_id'] for rec in recommendations_with_scores] - return None - -def get_recommendations_with_scores(user_id: int, n_recommendations: int = 5) -> Optional[List[Dict[str, any]]]: - """ - Génère des recommandations avec scores pour un utilisateur donné. - - Args: - user_id: ID de l'utilisateur - n_recommendations: Nombre de recommandations à retourner - - Returns: - Liste des recommandations avec article_id et similarity_score ou None si erreur - """ - try: - # Charger les données - embeddings = load_embeddings() - clicked_articles = get_user_articles(user_id) - - if not clicked_articles: - logging.warning(f"No articles found for user {user_id}") - return None - - # Valider les articles - valid_articles = _validate_user_articles(clicked_articles, embeddings) - - # Calculer l'embedding de référence (stratégie mean) - reference_embedding = _calculate_mean_embedding(embeddings, valid_articles) - - # Calculer similarités - similarities = _compute_similarities(reference_embedding, embeddings) - - # Filtrer et trier - candidates = _filter_candidates(similarities, clicked_articles) - recommendations = _get_top_recommendations_with_scores(candidates, n_recommendations) - - logging.info(f"Generated {len(recommendations)} recommendations with scores for user {user_id}") - return recommendations - - except Exception as e: - logging.error(f"Error generating recommendations for user {user_id}: {str(e)}") +import numpy as np +from sklearn.metrics.pairwise import cosine_similarity +from typing import List, Optional, Tuple, Dict +import logging + +from .data_loader import load_embeddings, get_user_articles + +def _calculate_mean_embedding(embeddings: np.ndarray, article_ids: List[int]) -> np.ndarray: + """Calcule la moyenne des embeddings pour une liste d'articles.""" + valid_ids = [aid for aid in article_ids if aid < len(embeddings)] + if not valid_ids: + raise ValueError("No valid article IDs found") + + articles_embeddings = embeddings[valid_ids] + return np.mean(articles_embeddings, axis=0).reshape(1, -1) + +def _compute_similarities(reference_embedding: np.ndarray, embeddings: np.ndarray) -> np.ndarray: + """Calcule la similarité cosinus entre l'embedding de référence et tous les embeddings.""" + return cosine_similarity(reference_embedding, embeddings)[0] + +def _filter_candidates(similarities: np.ndarray, clicked_articles: List[int]) -> List[Tuple[int, float]]: + """Filtre les candidats en excluant les articles déjà vus.""" + candidates = [] + for article_id, similarity in enumerate(similarities): + if article_id not in clicked_articles: + candidates.append((article_id, similarity)) + return candidates + +def _get_top_recommendations_with_scores(candidates: List[Tuple[int, float]], n_recommendations: int) -> List[Dict[str, any]]: + """Trie les candidats et retourne le top N avec les scores.""" + candidates.sort(key=lambda x: x[1], reverse=True) + recommendations = [] + for article_id, score in candidates[:n_recommendations]: + recommendations.append({ + 'article_id': article_id, + 'similarity_score': float(score) + }) + return recommendations + +def _validate_user_articles(clicked_articles: List[int], embeddings: np.ndarray) -> List[int]: + """Valide et filtre les articles dans les limites des embeddings.""" + valid_articles = [art for art in clicked_articles if 0 <= art < len(embeddings)] + if not valid_articles: + raise ValueError("No valid articles found for user") + return valid_articles + +def get_recommendations(user_id: int, n_recommendations: int = 5) -> Optional[List[int]]: + """ + Génère des recommandations pour un utilisateur donné (version compatibilité). + Retourne seulement les IDs pour maintenir la compatibilité. + """ + recommendations_with_scores = get_recommendations_with_scores(user_id, n_recommendations) + if recommendations_with_scores: + return [rec['article_id'] for rec in recommendations_with_scores] + return None + +def get_recommendations_with_scores(user_id: int, n_recommendations: int = 5) -> Optional[List[Dict[str, any]]]: + """ + Génère des recommandations avec scores pour un utilisateur donné. + + Args: + user_id: ID de l'utilisateur + n_recommendations: Nombre de recommandations à retourner + + Returns: + Liste des recommandations avec article_id et similarity_score ou None si erreur + """ + try: + # Charger les données + embeddings = load_embeddings() + clicked_articles = get_user_articles(user_id) + + if not clicked_articles: + logging.warning(f"No articles found for user {user_id}") + return None + + # Valider les articles + valid_articles = _validate_user_articles(clicked_articles, embeddings) + + # Calculer l'embedding de référence (stratégie mean) + reference_embedding = _calculate_mean_embedding(embeddings, valid_articles) + + # Calculer similarités + similarities = _compute_similarities(reference_embedding, embeddings) + + # Filtrer et trier + candidates = _filter_candidates(similarities, clicked_articles) + recommendations = _get_top_recommendations_with_scores(candidates, n_recommendations) + + logging.info(f"Generated {len(recommendations)} recommendations with scores for user {user_id}") + return recommendations + + except Exception as e: + logging.error(f"Error generating recommendations for user {user_id}: {str(e)}") return None \ No newline at end of file diff --git a/azure-app/utils/data_loader.py b/azure-app/utils/data_loader.py index 3befc69..b18faa5 100644 --- a/azure-app/utils/data_loader.py +++ b/azure-app/utils/data_loader.py @@ -1,131 +1,131 @@ -import pickle -import pandas as pd -import os -import logging -from typing import Dict, List, Optional -import numpy as np - -# Chemins des fichiers -DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data') -EMBEDDINGS_PATH = os.path.join(DATA_DIR, 'embeddings_azure_50d.pkl') -CLICKS_PATH = os.path.join(DATA_DIR, 'clicks_sample.csv') -ARTICLES_METADATA_PATH = os.path.join(DATA_DIR, 'articles_metadata.csv') - -# Cache global -_embeddings_cache: Optional[np.ndarray] = None -_user_data_cache: Optional[Dict[int, List[int]]] = None -_articles_metadata_cache: Optional[pd.DataFrame] = None - -def _load_pickle_file(file_path: str) -> np.ndarray: - """Charge un fichier pickle.""" - with open(file_path, 'rb') as f: - return pickle.load(f) - -def _load_csv_file(file_path: str) -> pd.DataFrame: - """Charge un fichier CSV.""" - return pd.read_csv(file_path) - -def _process_clicks_data(clicks_df: pd.DataFrame) -> Dict[int, List[int]]: - """Convertit le DataFrame en dictionnaire user->articles.""" - user_data = {} - for _, row in clicks_df.iterrows(): - user_id = int(row['user_id']) - article_id = int(row['click_article_id']) - - if user_id not in user_data: - user_data[user_id] = [] - user_data[user_id].append(article_id) - - return user_data - -def load_embeddings() -> np.ndarray: - """Charge les embeddings avec cache.""" - global _embeddings_cache - - if _embeddings_cache is None: - try: - _embeddings_cache = _load_pickle_file(EMBEDDINGS_PATH) - logging.info(f"Embeddings loaded: {_embeddings_cache.shape}") - except Exception as e: - logging.error(f"Error loading embeddings: {str(e)}") - raise - - return _embeddings_cache - -def load_user_data() -> Dict[int, List[int]]: - """Charge les données utilisateur avec cache.""" - global _user_data_cache - - if _user_data_cache is None: - try: - clicks_df = _load_csv_file(CLICKS_PATH) - _user_data_cache = _process_clicks_data(clicks_df) - logging.info(f"User data loaded: {len(_user_data_cache)} users") - except Exception as e: - logging.error(f"Error loading user data: {str(e)}") - raise - - return _user_data_cache - -def load_articles_metadata() -> pd.DataFrame: - """Charge les métadonnées des articles avec cache.""" - global _articles_metadata_cache - - if _articles_metadata_cache is None: - try: - _articles_metadata_cache = _load_csv_file(ARTICLES_METADATA_PATH) - logging.info(f"Articles metadata loaded: {len(_articles_metadata_cache)} articles") - except Exception as e: - logging.error(f"Error loading articles metadata: {str(e)}") - raise - - return _articles_metadata_cache - -def get_user_articles(user_id: int) -> Optional[List[int]]: - """Récupère les articles d'un utilisateur.""" - user_data = load_user_data() - return user_data.get(user_id) - -def get_article_category(article_id: int) -> Optional[int]: - """Récupère la catégorie d'un article.""" - try: - metadata_df = load_articles_metadata() - article_row = metadata_df[metadata_df['article_id'] == article_id] - if not article_row.empty: - return int(article_row.iloc[0]['category_id']) - return None - except Exception as e: - logging.warning(f"Error getting category for article {article_id}: {str(e)}") - return None - -def get_articles_categories(article_ids: List[int]) -> Dict[int, Optional[int]]: - """Récupère les catégories de plusieurs articles.""" - try: - metadata_df = load_articles_metadata() - categories = {} - for article_id in article_ids: - article_row = metadata_df[metadata_df['article_id'] == article_id] - if not article_row.empty: - categories[article_id] = int(article_row.iloc[0]['category_id']) - else: - categories[article_id] = None - return categories - except Exception as e: - logging.warning(f"Error getting categories for articles: {str(e)}") - return {aid: None for aid in article_ids} - -def check_data_files() -> bool: - """Vérifie que tous les fichiers de données existent.""" - files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH, ARTICLES_METADATA_PATH] - - for file_path in files_to_check: - if not os.path.exists(file_path): - logging.error(f"Missing data file: {file_path}") - return False - - return True - -def get_available_users() -> List[int]: - """Retourne la liste de tous les user_ids disponibles.""" - user_data = load_user_data() +import pickle +import pandas as pd +import os +import logging +from typing import Dict, List, Optional +import numpy as np + +# Chemins des fichiers +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data') +EMBEDDINGS_PATH = os.path.join(DATA_DIR, 'embeddings_azure_50d.pkl') +CLICKS_PATH = os.path.join(DATA_DIR, 'clicks_sample.csv') +ARTICLES_METADATA_PATH = os.path.join(DATA_DIR, 'articles_metadata.csv') + +# Cache global +_embeddings_cache: Optional[np.ndarray] = None +_user_data_cache: Optional[Dict[int, List[int]]] = None +_articles_metadata_cache: Optional[pd.DataFrame] = None + +def _load_pickle_file(file_path: str) -> np.ndarray: + """Charge un fichier pickle.""" + with open(file_path, 'rb') as f: + return pickle.load(f) + +def _load_csv_file(file_path: str) -> pd.DataFrame: + """Charge un fichier CSV.""" + return pd.read_csv(file_path) + +def _process_clicks_data(clicks_df: pd.DataFrame) -> Dict[int, List[int]]: + """Convertit le DataFrame en dictionnaire user->articles.""" + user_data = {} + for _, row in clicks_df.iterrows(): + user_id = int(row['user_id']) + article_id = int(row['click_article_id']) + + if user_id not in user_data: + user_data[user_id] = [] + user_data[user_id].append(article_id) + + return user_data + +def load_embeddings() -> np.ndarray: + """Charge les embeddings avec cache.""" + global _embeddings_cache + + if _embeddings_cache is None: + try: + _embeddings_cache = _load_pickle_file(EMBEDDINGS_PATH) + logging.info(f"Embeddings loaded: {_embeddings_cache.shape}") + except Exception as e: + logging.error(f"Error loading embeddings: {str(e)}") + raise + + return _embeddings_cache + +def load_user_data() -> Dict[int, List[int]]: + """Charge les données utilisateur avec cache.""" + global _user_data_cache + + if _user_data_cache is None: + try: + clicks_df = _load_csv_file(CLICKS_PATH) + _user_data_cache = _process_clicks_data(clicks_df) + logging.info(f"User data loaded: {len(_user_data_cache)} users") + except Exception as e: + logging.error(f"Error loading user data: {str(e)}") + raise + + return _user_data_cache + +def load_articles_metadata() -> pd.DataFrame: + """Charge les métadonnées des articles avec cache.""" + global _articles_metadata_cache + + if _articles_metadata_cache is None: + try: + _articles_metadata_cache = _load_csv_file(ARTICLES_METADATA_PATH) + logging.info(f"Articles metadata loaded: {len(_articles_metadata_cache)} articles") + except Exception as e: + logging.error(f"Error loading articles metadata: {str(e)}") + raise + + return _articles_metadata_cache + +def get_user_articles(user_id: int) -> Optional[List[int]]: + """Récupère les articles d'un utilisateur.""" + user_data = load_user_data() + return user_data.get(user_id) + +def get_article_category(article_id: int) -> Optional[int]: + """Récupère la catégorie d'un article.""" + try: + metadata_df = load_articles_metadata() + article_row = metadata_df[metadata_df['article_id'] == article_id] + if not article_row.empty: + return int(article_row.iloc[0]['category_id']) + return None + except Exception as e: + logging.warning(f"Error getting category for article {article_id}: {str(e)}") + return None + +def get_articles_categories(article_ids: List[int]) -> Dict[int, Optional[int]]: + """Récupère les catégories de plusieurs articles.""" + try: + metadata_df = load_articles_metadata() + categories = {} + for article_id in article_ids: + article_row = metadata_df[metadata_df['article_id'] == article_id] + if not article_row.empty: + categories[article_id] = int(article_row.iloc[0]['category_id']) + else: + categories[article_id] = None + return categories + except Exception as e: + logging.warning(f"Error getting categories for articles: {str(e)}") + return {aid: None for aid in article_ids} + +def check_data_files() -> bool: + """Vérifie que tous les fichiers de données existent.""" + files_to_check = [EMBEDDINGS_PATH, CLICKS_PATH, ARTICLES_METADATA_PATH] + + for file_path in files_to_check: + if not os.path.exists(file_path): + logging.error(f"Missing data file: {file_path}") + return False + + return True + +def get_available_users() -> List[int]: + """Retourne la liste de tous les user_ids disponibles.""" + user_data = load_user_data() return list(user_data.keys()) \ No newline at end of file diff --git a/streamlit_app.py b/streamlit_app.py index 1c73ef8..433f532 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -1,198 +1,198 @@ -import streamlit as st -import requests -import json -import time - -# Configuration de la page -st.set_page_config( - page_title="Système de Recommandation", - page_icon="🎯", - layout="wide" -) - -# Configuration de l'API -API_BASE_URL = "http://localhost:7071/api" - -def call_recommendation_api(user_id, base_url=API_BASE_URL): - """ - Appelle l'API Azure Function pour obtenir des recommandations. - """ - try: - url = f"{base_url}/recommend" - params = {"user_id": user_id} - - start_time = time.time() - response = requests.get(url, params=params, timeout=30) - response_time = time.time() - start_time - - if response.status_code == 200: - return response.json(), response_time, None - else: - error_msg = f"Erreur {response.status_code}: {response.text}" - return None, response_time, error_msg - - except requests.exceptions.Timeout: - return None, None, "Timeout: L'API a mis trop de temps à répondre" - except requests.exceptions.ConnectionError: - return None, None, "Erreur de connexion: Vérifiez que l'Azure Function est démarrée" - except Exception as e: - return None, None, f"Erreur inattendue: {str(e)}" - -def main(): - # Titre principal - st.title("🎯 Système de Recommandation de Contenu") - st.markdown("---") - - # Configuration dans la sidebar - with st.sidebar: - st.header("⚙️ Configuration") - - # URL de l'API - api_url = st.text_input( - "URL de l'API", - value=API_BASE_URL, - help="URL de base de votre Azure Function" - ) - - # Test de connexion - if st.button("🔍 Tester la connexion"): - with st.spinner("Test en cours..."): - try: - test_response = requests.get(f"{api_url}/recommend?user_id=0", timeout=5) - if test_response.status_code in [200, 400, 404]: # API répond - st.success("✅ API accessible") - else: - st.error(f"❌ API répond avec le code {test_response.status_code}") - except Exception as e: - st.error(f"❌ Impossible de joindre l'API: {str(e)}") - - st.markdown("---") - st.markdown("**Instructions:**") - st.markdown("1. Assurez-vous que `func start` est lancé") - st.markdown("2. Entrez un ID utilisateur") - st.markdown("3. Cliquez sur 'Obtenir des recommandations'") - - # Interface principale - col1, col2 = st.columns([1, 2]) - - with col1: - st.header("👤 Sélection utilisateur") - - # Input pour l'ID utilisateur - user_id = st.number_input( - "ID de l'utilisateur", - min_value=0, - max_value=10000, - value=0, - step=1, - help="Entrez l'ID de l'utilisateur pour lequel obtenir des recommandations" - ) - - # Bouton pour lancer la recommandation - if st.button("🚀 Obtenir des recommandations", type="primary"): - with st.spinner(f"Génération des recommandations pour l'utilisateur {user_id}..."): - result, response_time, error = call_recommendation_api(user_id, api_url) - - # Stocker les résultats dans la session - st.session_state['last_result'] = result - st.session_state['last_response_time'] = response_time - st.session_state['last_error'] = error - st.session_state['last_user_id'] = user_id - - with col2: - st.header("📊 Résultats") - - # Affichage des résultats - if 'last_result' in st.session_state: - if st.session_state['last_error']: - st.error(f"❌ {st.session_state['last_error']}") - elif st.session_state['last_result']: - result = st.session_state['last_result'] - - # Métriques - col_metrics1, col_metrics2 = st.columns(2) - with col_metrics1: - st.metric("👤 Utilisateur", st.session_state['last_user_id']) - with col_metrics2: - if st.session_state['last_response_time']: - st.metric("⏱️ Temps de réponse", f"{st.session_state['last_response_time']:.2f}s") - - # Recommandations - if 'recommendations' in result: - st.subheader("🎯 Articles recommandés") - recommendations = result['recommendations'] - - # Vérifier si on a le format enrichi (avec scores et catégories) - if recommendations and isinstance(recommendations[0], dict) and 'similarity_score_percent' in recommendations[0]: - # Format enrichi - affichage en tableau - import pandas as pd - - df_data = [] - for i, rec in enumerate(recommendations, 1): - df_data.append({ - 'Rang': i, - 'Article ID': rec['article_id'], - 'Similarité (%)': f"{rec['similarity_score_percent']:.1f}%", - 'Catégorie': rec.get('category_id', 'N/A') - }) - - df = pd.DataFrame(df_data) - st.dataframe(df, hide_index=True, use_container_width=True) - else: - # Format simple - affichage basique pour compatibilité - for i, article_id in enumerate(recommendations, 1): - if isinstance(article_id, dict): - article_id = article_id.get('article_id', article_id) - st.write(f"**{i}.** Article ID: `{article_id}`") - - # JSON brut (collapsible) - with st.expander("📝 Réponse JSON complète"): - st.json(result) - else: - st.warning("⚠️ Aucune recommandation trouvée dans la réponse") - else: - st.info("👆 Sélectionnez un utilisateur et cliquez sur 'Obtenir des recommandations' pour commencer") - - # Section de test avancé - st.markdown("---") - st.header("🧪 Tests avancés") - - col_test1, col_test2 = st.columns(2) - - with col_test1: - st.subheader("📈 Test de performance") - if st.button("Tester avec plusieurs utilisateurs"): - test_users = [0, 1, 2, 5, 10] - progress_bar = st.progress(0) - results = [] - - for i, uid in enumerate(test_users): - result, resp_time, error = call_recommendation_api(uid, api_url) - results.append({ - 'user_id': uid, - 'success': error is None, - 'response_time': resp_time, - 'error': error - }) - progress_bar.progress((i + 1) / len(test_users)) - - # Affichage des résultats - success_count = sum(1 for r in results if r['success']) - avg_time = sum(r['response_time'] for r in results if r['response_time']) / len([r for r in results if r['response_time']]) - - st.success(f"✅ {success_count}/{len(test_users)} appels réussis") - st.info(f"⏱️ Temps moyen: {avg_time:.2f}s") - - with col_test2: - st.subheader("🔍 Test d'erreur") - if st.button("Tester utilisateur inexistant"): - with st.spinner("Test en cours..."): - result, resp_time, error = call_recommendation_api(99999, api_url) - if error: - st.warning(f"Erreur attendue: {error}") - elif result: - st.info("Réponse reçue (comportement inattendu)") - st.json(result) - -if __name__ == "__main__": +import streamlit as st +import requests +import json +import time + +# Configuration de la page +st.set_page_config( + page_title="Système de Recommandation", + page_icon="🎯", + layout="wide" +) + +# Configuration de l'API +API_BASE_URL = "http://localhost:7071/api" + +def call_recommendation_api(user_id, base_url=API_BASE_URL): + """ + Appelle l'API Azure Function pour obtenir des recommandations. + """ + try: + url = f"{base_url}/recommend" + params = {"user_id": user_id} + + start_time = time.time() + response = requests.get(url, params=params, timeout=30) + response_time = time.time() - start_time + + if response.status_code == 200: + return response.json(), response_time, None + else: + error_msg = f"Erreur {response.status_code}: {response.text}" + return None, response_time, error_msg + + except requests.exceptions.Timeout: + return None, None, "Timeout: L'API a mis trop de temps à répondre" + except requests.exceptions.ConnectionError: + return None, None, "Erreur de connexion: Vérifiez que l'Azure Function est démarrée" + except Exception as e: + return None, None, f"Erreur inattendue: {str(e)}" + +def main(): + # Titre principal + st.title("🎯 Système de Recommandation de Contenu") + st.markdown("---") + + # Configuration dans la sidebar + with st.sidebar: + st.header("⚙️ Configuration") + + # URL de l'API + api_url = st.text_input( + "URL de l'API", + value=API_BASE_URL, + help="URL de base de votre Azure Function" + ) + + # Test de connexion + if st.button("🔍 Tester la connexion"): + with st.spinner("Test en cours..."): + try: + test_response = requests.get(f"{api_url}/recommend?user_id=0", timeout=5) + if test_response.status_code in [200, 400, 404]: # API répond + st.success("✅ API accessible") + else: + st.error(f"❌ API répond avec le code {test_response.status_code}") + except Exception as e: + st.error(f"❌ Impossible de joindre l'API: {str(e)}") + + st.markdown("---") + st.markdown("**Instructions:**") + st.markdown("1. Assurez-vous que `func start` est lancé") + st.markdown("2. Entrez un ID utilisateur") + st.markdown("3. Cliquez sur 'Obtenir des recommandations'") + + # Interface principale + col1, col2 = st.columns([1, 2]) + + with col1: + st.header("👤 Sélection utilisateur") + + # Input pour l'ID utilisateur + user_id = st.number_input( + "ID de l'utilisateur", + min_value=0, + max_value=10000, + value=0, + step=1, + help="Entrez l'ID de l'utilisateur pour lequel obtenir des recommandations" + ) + + # Bouton pour lancer la recommandation + if st.button("🚀 Obtenir des recommandations", type="primary"): + with st.spinner(f"Génération des recommandations pour l'utilisateur {user_id}..."): + result, response_time, error = call_recommendation_api(user_id, api_url) + + # Stocker les résultats dans la session + st.session_state['last_result'] = result + st.session_state['last_response_time'] = response_time + st.session_state['last_error'] = error + st.session_state['last_user_id'] = user_id + + with col2: + st.header("📊 Résultats") + + # Affichage des résultats + if 'last_result' in st.session_state: + if st.session_state['last_error']: + st.error(f"❌ {st.session_state['last_error']}") + elif st.session_state['last_result']: + result = st.session_state['last_result'] + + # Métriques + col_metrics1, col_metrics2 = st.columns(2) + with col_metrics1: + st.metric("👤 Utilisateur", st.session_state['last_user_id']) + with col_metrics2: + if st.session_state['last_response_time']: + st.metric("⏱️ Temps de réponse", f"{st.session_state['last_response_time']:.2f}s") + + # Recommandations + if 'recommendations' in result: + st.subheader("🎯 Articles recommandés") + recommendations = result['recommendations'] + + # Vérifier si on a le format enrichi (avec scores et catégories) + if recommendations and isinstance(recommendations[0], dict) and 'similarity_score_percent' in recommendations[0]: + # Format enrichi - affichage en tableau + import pandas as pd + + df_data = [] + for i, rec in enumerate(recommendations, 1): + df_data.append({ + 'Rang': i, + 'Article ID': rec['article_id'], + 'Similarité (%)': f"{rec['similarity_score_percent']:.1f}%", + 'Catégorie': rec.get('category_id', 'N/A') + }) + + df = pd.DataFrame(df_data) + st.dataframe(df, hide_index=True, use_container_width=True) + else: + # Format simple - affichage basique pour compatibilité + for i, article_id in enumerate(recommendations, 1): + if isinstance(article_id, dict): + article_id = article_id.get('article_id', article_id) + st.write(f"**{i}.** Article ID: `{article_id}`") + + # JSON brut (collapsible) + with st.expander("📝 Réponse JSON complète"): + st.json(result) + else: + st.warning("⚠️ Aucune recommandation trouvée dans la réponse") + else: + st.info("👆 Sélectionnez un utilisateur et cliquez sur 'Obtenir des recommandations' pour commencer") + + # Section de test avancé + st.markdown("---") + st.header("🧪 Tests avancés") + + col_test1, col_test2 = st.columns(2) + + with col_test1: + st.subheader("📈 Test de performance") + if st.button("Tester avec plusieurs utilisateurs"): + test_users = [0, 1, 2, 5, 10] + progress_bar = st.progress(0) + results = [] + + for i, uid in enumerate(test_users): + result, resp_time, error = call_recommendation_api(uid, api_url) + results.append({ + 'user_id': uid, + 'success': error is None, + 'response_time': resp_time, + 'error': error + }) + progress_bar.progress((i + 1) / len(test_users)) + + # Affichage des résultats + success_count = sum(1 for r in results if r['success']) + avg_time = sum(r['response_time'] for r in results if r['response_time']) / len([r for r in results if r['response_time']]) + + st.success(f"✅ {success_count}/{len(test_users)} appels réussis") + st.info(f"⏱️ Temps moyen: {avg_time:.2f}s") + + with col_test2: + st.subheader("🔍 Test d'erreur") + if st.button("Tester utilisateur inexistant"): + with st.spinner("Test en cours..."): + result, resp_time, error = call_recommendation_api(99999, api_url) + if error: + st.warning(f"Erreur attendue: {error}") + elif result: + st.info("Réponse reçue (comportement inattendu)") + st.json(result) + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/streamlit_requirements.txt b/streamlit_requirements.txt index 12ea239..de6c9de 100644 --- a/streamlit_requirements.txt +++ b/streamlit_requirements.txt @@ -1,9 +1,9 @@ -# Azure Functions -azure-functions -numpy -pandas -scikit-learn - -# Interface Streamlit -streamlit>=1.28.0 +# Azure Functions +azure-functions +numpy +pandas +scikit-learn + +# Interface Streamlit +streamlit>=1.28.0 requests>=2.25.0 \ No newline at end of file