OUAREDAEK commited on
Commit
dada8b0
·
verified ·
1 Parent(s): a115caa

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +243 -0
app.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import random
3
+ import torch
4
+ import pandas as pd
5
+ from functools import lru_cache
6
+
7
+ from flask import Flask, render_template, request, jsonify
8
+ from sentence_transformers import SentenceTransformer
9
+
10
+ from fastapi import FastAPI
11
+ from fastapi.middleware.wsgi import WSGIMiddleware
12
+ import uvicorn
13
+ import nest_asyncio
14
+
15
+ # ===============================
16
+ # OPTIONAL FAISS
17
+ # ===============================
18
+ try:
19
+ import faiss
20
+ FAISS_AVAILABLE = True
21
+ print("✅ FAISS available")
22
+ except ImportError:
23
+ FAISS_AVAILABLE = False
24
+ print("⚠️ FAISS not available → torch fallback")
25
+
26
+ # ===============================
27
+ # CONFIG
28
+ # ===============================
29
+ BASE_DIR = os.path.abspath(os.path.dirname(__file__))
30
+ CSV_DATA = "dataset_2026.csv"
31
+ EMB_FILE = "embeddings_questions.pt"
32
+ TOP_K_RECOMMANDATIONS = 5
33
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
34
+
35
+ # ===============================
36
+ # APP
37
+ # ===============================
38
+ app = Flask(
39
+ __name__,
40
+ template_folder=os.path.join(BASE_DIR, "templates"),
41
+ static_folder=os.path.join(BASE_DIR, "static")
42
+ )
43
+
44
+ # ===============================
45
+ # LOAD MODEL (ONCE)
46
+ # ===============================
47
+ print("🔹 Loading model...")
48
+ model = SentenceTransformer(
49
+ "OrdalieTech/Solon-embeddings-mini-beta-1.1",
50
+ device=DEVICE,
51
+ trust_remote_code=True
52
+ )
53
+
54
+ # ===============================
55
+ # LOAD & CLEAN DATASET
56
+ # ===============================
57
+ print("🔹 Loading dataset...")
58
+ df = pd.read_csv(CSV_DATA, low_memory=False)
59
+
60
+ df = df.dropna(subset=["question"]).reset_index(drop=True)
61
+
62
+ if len(df) == 0:
63
+ raise RuntimeError("❌ Dataset has no valid questions")
64
+
65
+ questions = df["question"].astype(str).tolist()
66
+ NB_QUESTIONS = len(questions)
67
+
68
+ print(f"✅ Valid questions: {NB_QUESTIONS}")
69
+
70
+ # ===============================
71
+ # LOAD / CREATE EMBEDDINGS
72
+ # ===============================
73
+ if os.path.exists(EMB_FILE):
74
+ emb_base = torch.load(EMB_FILE, map_location=DEVICE)
75
+ else:
76
+ emb_base = model.encode(
77
+ questions,
78
+ convert_to_tensor=True,
79
+ normalize_embeddings=True,
80
+ batch_size=64
81
+ )
82
+ torch.save(emb_base, EMB_FILE)
83
+
84
+ if emb_base.shape[0] != NB_QUESTIONS:
85
+ raise RuntimeError("❌ Embedding count mismatch")
86
+
87
+ # ===============================
88
+ # INDEX SETUP
89
+ # ===============================
90
+ K_SEARCH = max(1, min(TOP_K_RECOMMANDATIONS + 1, NB_QUESTIONS))
91
+
92
+ if FAISS_AVAILABLE:
93
+ emb_np = emb_base.cpu().numpy()
94
+ dim = emb_np.shape[1]
95
+ index = faiss.IndexFlatIP(dim)
96
+ index.add(emb_np)
97
+ else:
98
+ emb_base_cpu = emb_base.cpu()
99
+
100
+ # ===============================
101
+ # CACHE QUESTION EMBEDDING
102
+ # ===============================
103
+ @lru_cache(maxsize=1000)
104
+ def encode_question_cached(q: str):
105
+ return model.encode(
106
+ q,
107
+ convert_to_tensor=True,
108
+ normalize_embeddings=True
109
+ )
110
+
111
+ # ===============================
112
+ # UTILS
113
+ # ===============================
114
+ def enrich_message(base):
115
+ return random.choice([
116
+ f"Bonne question 🙂 {base}",
117
+ f"Voici ce que je peux vous dire : {base}",
118
+ f"Intéressant ! {base}",
119
+ base
120
+ ])
121
+
122
+ # ===============================
123
+ # CORE LOGIC (BULLETPROOF)
124
+ # ===============================
125
+ def process_question(question: str):
126
+
127
+ if not question or not question.strip():
128
+ return {
129
+ "response": "Veuillez poser une question.",
130
+ "confidence": 0,
131
+ "matched": "—",
132
+ "intent": "Vide",
133
+ "recs": []
134
+ }
135
+
136
+ emb_q = encode_question_cached(question)
137
+
138
+ # ---------- FAISS ----------
139
+ if FAISS_AVAILABLE:
140
+ D, I = index.search(
141
+ emb_q.cpu().numpy().reshape(1, -1),
142
+ K_SEARCH
143
+ )
144
+
145
+ if D.size == 0:
146
+ return {
147
+ "response": "Aucune réponse trouvée",
148
+ "confidence": 0,
149
+ "matched": "—",
150
+ "intent": "Inconnu",
151
+ "recs": []
152
+ }
153
+
154
+ idxs = I[0].tolist()
155
+ scores = D[0].tolist()
156
+
157
+ # ---------- TORCH FALLBACK ----------
158
+ else:
159
+ # ensure shape [1, dim]
160
+ if emb_q.dim() == 1:
161
+ emb_q = emb_q.unsqueeze(0)
162
+
163
+ scores_all = torch.matmul(
164
+ emb_q,
165
+ emb_base_cpu.T
166
+ ).squeeze(0)
167
+
168
+ nb_scores = scores_all.numel()
169
+ k = min(K_SEARCH, nb_scores)
170
+
171
+ if k == 0:
172
+ return {
173
+ "response": "Aucune réponse trouvée",
174
+ "confidence": 0,
175
+ "matched": "—",
176
+ "intent": "Inconnu",
177
+ "recs": []
178
+ }
179
+
180
+ values, indices = torch.topk(scores_all, k)
181
+ idxs = indices.tolist()
182
+ scores = values.tolist()
183
+
184
+ # ---------- DECISION ----------
185
+ best_idx = idxs[0]
186
+ score = int(scores[0] * 100)
187
+
188
+ if score < 40:
189
+ return {
190
+ "response": "Aucune réponse trouvée",
191
+ "confidence": score,
192
+ "matched": "—",
193
+ "intent": "Inconnu",
194
+ "recs": []
195
+ }
196
+
197
+ if score < 80:
198
+ recs = [
199
+ df["question"].iloc[i]
200
+ for i in idxs[1:]
201
+ if i < NB_QUESTIONS
202
+ ][:TOP_K_RECOMMANDATIONS]
203
+
204
+ return {
205
+ "response": "Je ne suis pas totalement sûr.",
206
+ "confidence": score,
207
+ "matched": df["question"].iloc[best_idx],
208
+ "intent": "Incertain",
209
+ "recs": recs
210
+ }
211
+
212
+ return {
213
+ "response": enrich_message(df["rationale"].iloc[best_idx]),
214
+ "confidence": score,
215
+ "matched": df["question"].iloc[best_idx],
216
+ "intent": df["intent"].iloc[best_idx],
217
+ "recs": []
218
+ }
219
+
220
+ # ===============================
221
+ # ROUTES
222
+ # ===============================
223
+ @app.route("/")
224
+ def index():
225
+ return render_template("index.html")
226
+
227
+ @app.route("/ask", methods=["POST"])
228
+ def ask():
229
+ return jsonify(process_question(request.json.get("question", "")))
230
+
231
+ # ===============================
232
+ # FASTAPI WRAPPER
233
+ # ===============================
234
+ fastapi_app = FastAPI()
235
+ fastapi_app.mount("/", WSGIMiddleware(app))
236
+
237
+ # ===============================
238
+ # MAIN
239
+ # ===============================
240
+ if __name__ == "__main__":
241
+ nest_asyncio.apply()
242
+ print("🚀 Server running on http://localhost:7860")
243
+ uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)