saidouu commited on
Commit
6ec767d
·
1 Parent(s): f15e05f

Deploy: Backend IA v1.0 avec secrets

Browse files
Files changed (5) hide show
  1. app.py +274 -0
  2. email_template.py +21 -0
  3. opportunities_vectors.pkl +3 -0
  4. recommender.py +69 -0
  5. requirements.txt +12 -0
app.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+ # # version update
9
+ # import os
10
+ # import pickle
11
+ # import threading
12
+ # from flask import Flask, request, jsonify
13
+ # from flask_cors import CORS
14
+ # from supabase import create_client, Client
15
+ # from sentence_transformers import SentenceTransformer
16
+ # from sklearn.metrics.pairwise import cosine_similarity
17
+ # import resend
18
+
19
+ # app = Flask(__name__)
20
+ # CORS(app)
21
+
22
+ # # ==========================================
23
+ # # ⚠️ CONFIGURATION (À VÉRIFIER)
24
+ # # ==========================================
25
+ # SUPABASE_URL = "https://dvddftdtrkidsulcxaqp.supabase.co"
26
+ # SUPABASE_KEY = "sb_secret_CoFpwT9q6IrR-lfzjXynKg_DCoyB8F0" #
27
+ # resend.api_key = "re_CYUPs5Nt_A3L3t2EDX1UT5JbBLLycqTHM" #
28
+
29
+ # supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
30
+
31
+ # print("⏳ Chargement du modèle IA...")
32
+ # model_ia = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2') #
33
+
34
+ # def load_vectors():
35
+ # with open("opportunities_vectors.pkl", "rb") as f: #
36
+ # return pickle.load(f)
37
+
38
+ # vector_data = load_vectors()
39
+
40
+ # # 📧 Notification Email en Arrière-plan
41
+ # def send_email_background(nom, email, domaine, opportunites):
42
+ # # C'est cette ligne qui définit le nom vu par l'utilisateur
43
+ # # sender_email = "EduConnect Afrika <contact@educonnectafrika.com>"
44
+ # sender_email = "EduConnect Afrika <contact@afriaisolutions.com>"
45
+
46
+
47
+ # bourses_html = "".join([
48
+ # f"<li style='margin-bottom: 10px;'><strong>[{b.get('type', 'Opportunité')}] {b.get('titre', '')}</strong> - 📍 {b.get('pays', 'En ligne')}</li>"
49
+ # for b in opportunites
50
+ # ])
51
+
52
+ # try:
53
+ # resend.Emails.send({
54
+ # "from": sender_email,
55
+ # "to": email,
56
+ # "subject": f"🎯 Vos opportunités en {domaine} sont prêtes !",
57
+ # "html": f"""
58
+ # <div style="font-family: Arial, sans-serif; border: 1px solid #e2e8f0; padding: 25px; border-radius: 20px; max-width: 600px; color: #1e293b;">
59
+ # <h2 style="color: #1e40af;">Félicitations {nom} !</h2>
60
+ # <p>Notre IA a analysé votre profil. Voici les meilleures opportunités pour vous :</p>
61
+ # <ul style="background-color: #f8fafc; padding: 20px; border-radius: 12px; list-style-type: none;">
62
+ # {bourses_html}
63
+ # </ul>
64
+ # <p>Accédez à votre espace pour postuler :</p>
65
+ # <div style="text-align: center; margin: 30px 0;">
66
+ # <a href="http://localhost:8080" style="background-color: #2563eb; color: white; padding: 12px 25px; text-decoration: none; border-radius: 10px; font-weight: bold;">Accéder au Dashboard</a>
67
+ # </div>
68
+ # <hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
69
+ # <p style="font-size: 11px; color: #64748b; text-align: center;">
70
+ # <strong>EduConnect Afrika</strong><br>
71
+ # L'avenir de l'orientation académique en Afrique.<br>
72
+ # Responsable : Lauryane
73
+ # </p>
74
+ # </div>
75
+ # """
76
+ # })
77
+ # print(f"✅ Email envoyé avec succès via EduConnect à {email}")
78
+ # except Exception as e:
79
+ # print(f"❌ Erreur d'envoi : {e}")
80
+
81
+ # @app.route('/api/recommend', methods=['POST'])
82
+ # def get_recommendations():
83
+ # data = request.json
84
+ # user_id = data.get('user_id')
85
+
86
+ # try:
87
+ # # 1. Profil utilisateur
88
+ # res_profile = supabase.table('profiles').select('*').eq('user_id', user_id).execute()
89
+ # if not res_profile.data: return jsonify({"error": "Profil introuvable"}), 404
90
+
91
+ # user = res_profile.data[0]
92
+ # filiere = user.get('filiere') or "votre domaine"
93
+ # nom_etudiant = user.get('name') or "Étudiant"
94
+ # email_etudiant = user.get('email')
95
+
96
+ # # 2. Vectorisation du profil
97
+ # profil_text = f"Niveau: {user.get('niveau')}. Domaine: {filiere}. Intérêts: {user.get('interets')}."
98
+ # user_vector = model_ia.encode([profil_text])
99
+
100
+ # # 3. Similarité et Scoring
101
+ # similarities = cosine_similarity(user_vector, vector_data["vectors"])[0]
102
+ # top_indices = similarities.argsort()[-15:][::-1] # On prend un peu plus pour mixer
103
+
104
+ # scores_dict = {int(vector_data["ids"][idx]): min(0.99, float(similarities[idx]) + 0.35) for idx in top_indices}
105
+
106
+ # # 4. Récupération unifiée des opportunités
107
+ # top_ids = list(scores_dict.keys())
108
+ # res_opps = supabase.table('opportunities').select('*').in_('id', top_ids).execute()
109
+
110
+ # recommandations = []
111
+ # for opp in res_opps.data:
112
+ # opp['score_ia'] = scores_dict[opp['id']]
113
+ # recommandations.append(opp)
114
+
115
+ # # Tri final par score
116
+ # recommandations = sorted(recommandations, key=lambda x: x['score_ia'], reverse=True)
117
+
118
+ # # 🚀 5. Email en arrière-plan
119
+ # if email_etudiant:
120
+ # thread = threading.Thread(target=send_email_background, args=(nom_etudiant, email_etudiant, filiere, recommandations[:3]))
121
+ # thread.start()
122
+
123
+ # return jsonify({"status": "success", "recommandations": recommandations})
124
+
125
+ # except Exception as e:
126
+ # print(f"❌ Erreur: {e}")
127
+ # return jsonify({"error": str(e)}), 500
128
+
129
+ # if __name__ == '__main__':
130
+ # app.run(port=5000, debug=True)
131
+
132
+
133
+
134
+
135
+
136
+
137
+ # code pour la prod
138
+ # version update - Sécurisée pour Hugging Face Spaces
139
+ import os
140
+ import pickle
141
+ import threading
142
+ from flask import Flask, request, jsonify
143
+ from flask_cors import CORS
144
+ from supabase import create_client, Client
145
+ from sentence_transformers import SentenceTransformer
146
+ from sklearn.metrics.pairwise import cosine_similarity
147
+ import resend
148
+
149
+ app = Flask(__name__)
150
+ # Autorise ton frontend Vercel à appeler cette API
151
+ CORS(app)
152
+
153
+ # ==========================================
154
+ # 🔐 CONFIGURATION SÉCURISÉE (VIA SECRETS HF)
155
+ # ==========================================
156
+ # On récupère les clés depuis l'environnement du serveur
157
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
158
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
159
+ resend.api_key = os.getenv("RESEND_API_KEY")
160
+
161
+ # Vérification au démarrage pour éviter les crashs silencieux
162
+ if not SUPABASE_URL or not SUPABASE_KEY:
163
+ print("❌ ERREUR : Les variables d'environnement Supabase sont manquantes !")
164
+
165
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
166
+
167
+ print("⏳ Chargement du modèle IA (paraphrase-multilingual)...")
168
+ model_ia = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
169
+
170
+ def load_vectors():
171
+ try:
172
+ with open("opportunities_vectors.pkl", "rb") as f:
173
+ return pickle.load(f)
174
+ except FileNotFoundError:
175
+ print("❌ ERREUR : Le fichier opportunities_vectors.pkl est introuvable !")
176
+ return None
177
+
178
+ vector_data = load_vectors()
179
+
180
+ # 📧 Notification Email en Arrière-plan
181
+ def send_email_background(nom, email, domaine, opportunites):
182
+ # Expéditeur utilisant ton domaine afriaisolutions.com validé sur Resend
183
+ sender_email = "EduConnect Afrika <contact@afriaisolutions.com>"
184
+
185
+ bourses_html = "".join([
186
+ f"<li style='margin-bottom: 10px;'><strong>[{b.get('type', 'Opportunité')}] {b.get('titre', '')}</strong> - 📍 {b.get('pays', 'En ligne')}</li>"
187
+ for b in opportunites
188
+ ])
189
+
190
+ try:
191
+ resend.Emails.send({
192
+ "from": sender_email,
193
+ "to": email,
194
+ "subject": f"🎯 Vos opportunités en {domaine} sont prêtes !",
195
+ "html": f"""
196
+ <div style="font-family: Arial, sans-serif; border: 1px solid #e2e8f0; padding: 25px; border-radius: 20px; max-width: 600px; color: #1e293b;">
197
+ <h2 style="color: #1e40af;">Félicitations {nom} !</h2>
198
+ <p>Notre IA a analysé votre profil. Voici les meilleures opportunités pour vous :</p>
199
+ <ul style="background-color: #f8fafc; padding: 20px; border-radius: 12px; list-style-type: none;">
200
+ {bourses_html}
201
+ </ul>
202
+ <p>Accédez à votre espace pour postuler :</p>
203
+ <div style="text-align: center; margin: 30px 0;">
204
+ <a href="https://app.educonnectafrika.com" style="background-color: #2563eb; color: white; padding: 12px 25px; text-decoration: none; border-radius: 10px; font-weight: bold;">Accéder au Dashboard</a>
205
+ </div>
206
+ <hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
207
+ <p style="font-size: 11px; color: #64748b; text-align: center;">
208
+ <strong>EduConnect Afrika</strong><br>
209
+ L'avenir de l'orientation académique en Afrique.<br>
210
+ Responsable : Lauryane
211
+ </p>
212
+ </div>
213
+ """
214
+ })
215
+ print(f"✅ Email envoyé avec succès à {email}")
216
+ except Exception as e:
217
+ print(f"❌ Erreur d'envoi Resend : {e}")
218
+
219
+ @app.route('/api/recommend', methods=['POST'])
220
+ def get_recommendations():
221
+ data = request.json
222
+ user_id = data.get('user_id')
223
+
224
+ if not user_id:
225
+ return jsonify({"error": "user_id manquant"}), 400
226
+
227
+ try:
228
+ # 1. Récupération du profil
229
+ res_profile = supabase.table('profiles').select('*').eq('user_id', user_id).execute()
230
+ if not res_profile.data:
231
+ return jsonify({"error": "Profil introuvable"}), 404
232
+
233
+ user = res_profile.data[0]
234
+ filiere = user.get('filiere') or "votre domaine"
235
+ nom_etudiant = user.get('name') or "Étudiant"
236
+ email_etudiant = user.get('email')
237
+
238
+ # 2. Vectorisation du profil utilisateur
239
+ profil_text = f"Niveau: {user.get('niveau')}. Domaine: {filiere}. Intérêts: {user.get('interets')}."
240
+ user_vector = model_ia.encode([profil_text])
241
+
242
+ # 3. Calcul de similarité (Cosine Similarity)
243
+ similarities = cosine_similarity(user_vector, vector_data["vectors"])[0]
244
+ top_indices = similarities.argsort()[-15:][::-1]
245
+
246
+ # Scoring IA ajusté
247
+ scores_dict = {int(vector_data["ids"][idx]): min(0.99, float(similarities[idx]) + 0.35) for idx in top_indices}
248
+
249
+ # 4. Récupération des données depuis Supabase
250
+ top_ids = list(scores_dict.keys())
251
+ res_opps = supabase.table('opportunities').select('*').in_('id', top_ids).execute()
252
+
253
+ recommandations = []
254
+ for opp in res_opps.data:
255
+ opp['score_ia'] = scores_dict[opp['id']]
256
+ recommandations.append(opp)
257
+
258
+ # Tri final par score décroissant
259
+ recommandations = sorted(recommandations, key=lambda x: x['score_ia'], reverse=True)
260
+
261
+ # 🚀 5. Notification Email (Asynchrone via Threading)
262
+ if email_etudiant:
263
+ thread = threading.Thread(target=send_email_background, args=(nom_etudiant, email_etudiant, filiere, recommandations[:3]))
264
+ thread.start()
265
+
266
+ return jsonify({"status": "success", "recommandations": recommandations})
267
+
268
+ except Exception as e:
269
+ print(f"❌ Erreur API : {e}")
270
+ return jsonify({"error": str(e)}), 500
271
+
272
+ if __name__ == '__main__':
273
+ # Configuration obligatoire pour Hugging Face Spaces (Port 7860)
274
+ app.run(host='0.0.0.0', port=7860, debug=False)
email_template.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def get_email_html(user_name, bourses, formations):
2
+ items_html = ""
3
+ for _, b in bourses.iterrows():
4
+ items_html += f"""
5
+ <div style="padding: 15px; border-left: 4px solid #2563eb; background: #f8fafc; margin-bottom: 10px;">
6
+ <h4 style="margin: 0; color: #1e3a8a;">{b['institution']}</h4>
7
+ <p style="font-size: 14px; margin: 5px 0;">🎯 Match : <strong>{int(b['score_ia']*100)}%</strong></p>
8
+ </div>"""
9
+
10
+ return f"""
11
+ <div style="font-family: sans-serif; max-width: 600px; margin: auto; border: 1px solid #e2e8f0; padding: 20px; border-radius: 12px;">
12
+ <h2 style="color: #2563eb;">Bonjour {user_name} ! 🚀</h2>
13
+ <p>Notre IA a analysé votre profil. Voici les meilleures opportunités pour votre carrière :</p>
14
+ <h3>🏆 Bourses recommandées</h3>
15
+ {items_html}
16
+ <h3>📚 Formations suggérées</h3>
17
+ <p>Pour renforcer votre dossier, nous vous conseillons : <strong>{formations.iloc[0]['titre']}</strong></p>
18
+ <hr style="margin-top: 20px; border: 0; border-top: 1px solid #eee;" />
19
+ <p style="font-size: 12px; color: #64748b;">Propulsé par <strong>AfriAI Solutions</strong></p>
20
+ </div>
21
+ """
opportunities_vectors.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7ef57cdff5adb67a5209d100c390d67b8cffb536d663f30601d9031488cacff
3
+ size 213969
recommender.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import pandas as pd
3
+ import pickle
4
+ from sentence_transformers import SentenceTransformer
5
+ from sklearn.metrics.pairwise import cosine_similarity
6
+
7
+ class EduRecommender:
8
+ def __init__(self, db_path="bourses_reco.db", vector_path="bourses_vectors.pkl"):
9
+ self.db_path = db_path
10
+ # 1. Chargement du modèle IA (Une seule fois)
11
+ print("⏳ Chargement du modèle multilingue...")
12
+ self.model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
13
+
14
+ # 2. Chargement des vecteurs de bourses
15
+ with open(vector_path, "rb") as f:
16
+ data = pickle.load(f)
17
+ self.bourse_ids = data["ids"]
18
+ self.bourse_vectors = data["vectors"]
19
+
20
+ def _get_connection(self):
21
+ return sqlite3.connect(self.db_path)
22
+
23
+ def recommander_tout(self, user_id, top_n=3):
24
+ conn = self._get_connection()
25
+
26
+ # --- RÉCUPÉRATION ÉTUDIANT ---
27
+ user = pd.read_sql(f"SELECT * FROM etudiants WHERE user_id = '{user_id}'", conn).iloc[0]
28
+ print(f"\n🎯 Analyse pour : {user['nom']} ({user['interet_majeur']})")
29
+
30
+ # --- PARTIE 1 : BOURSES (Matching Sémantique) ---
31
+ # On crée un texte riche pour la recherche
32
+ texte_recherche = f"Bourse en {user['interet_majeur']} pour niveau {user['niveau_actuel']}"
33
+ user_vector = self.model.encode([texte_recherche])
34
+
35
+ sim_bourses = cosine_similarity(user_vector, self.bourse_vectors)[0]
36
+ df_bourses_res = pd.DataFrame({'bourse_id': self.bourse_ids, 'score_ia': sim_bourses})
37
+
38
+ # Jointure SQL pour avoir les détails et filtrer par Statut OUVERT
39
+ bourses_final = pd.merge(df_bourses_res, pd.read_sql("SELECT * FROM bourses WHERE statut='OUVERT'", conn), on='bourse_id')
40
+ top_bourses = bourses_final.sort_values(by='score_ia', ascending=False).head(top_n)
41
+
42
+ # --- PARTIE 2 : FORMATIONS (Gap Filling) ---
43
+ df_form = pd.read_sql("SELECT * FROM formations", conn)
44
+ form_vectors = self.model.encode(df_form['competence_cible'].tolist())
45
+
46
+ sim_form = cosine_similarity(user_interest_vec := self.model.encode([user['interet_majeur']]), form_vectors)[0]
47
+ df_form['score_formation'] = sim_form
48
+ top_formations = df_form.sort_values(by='score_formation', ascending=False).head(top_n)
49
+
50
+ conn.close()
51
+ return top_bourses, top_formations
52
+
53
+ # --- EXÉCUTION DU SYSTÈME ---
54
+ if __name__ == "__main__":
55
+ # ICI : tu l'as appelé 'recommender' (anglais)
56
+ recommender = EduRecommender()
57
+
58
+ conn = sqlite3.connect("bourses_reco.db")
59
+ test_user_id = pd.read_sql("SELECT user_id FROM etudiants LIMIT 1", conn).iloc[0]['user_id']
60
+ conn.close()
61
+
62
+ # ERREUR ICI : Change 'recommander' en 'recommender' pour matcher l'objet ci-dessus
63
+ bourses, formations = recommender.recommander_tout(test_user_id)
64
+
65
+ print("\n🏆 TOP BOURSES TROUVÉES :")
66
+ print(bourses[['institution', 'domaine_principal', 'score_ia']])
67
+
68
+ print("\n📚 FORMATIONS SUGGÉRÉES POUR VOTRE PROFIL :")
69
+ print(formations[['titre', 'plateforme', 'score_formation']])
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask==3.1.3
2
+ flask-cors==6.0.2
3
+ supabase==2.28.0
4
+ resend==2.23.0
5
+ sentence-transformers==5.2.3
6
+ pandas==2.3.3
7
+ numpy==2.2.6
8
+ python-dotenv==1.0.1
9
+ torch==2.10.0
10
+ transformers==5.2.0
11
+ scikit-learn==1.7.2
12
+ gunicorn