OUAREDAEK commited on
Commit
b67bf8f
·
verified ·
1 Parent(s): 17d4b30

Upload app.py with huggingface_hub

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