FabIndy commited on
Commit
fd2fa68
·
1 Parent(s): 401a3ff

Add guided tabs UI (LIST/FULLTEXT/EXPLAIN/ADVANCED)

Browse files
Files changed (1) hide show
  1. app.py +161 -95
app.py CHANGED
@@ -1,20 +1,25 @@
1
  # app.py — Gradio UI for hf-code-education (CPU / Hugging Face Spaces)
2
  # This file must NOT change the validated RAG logic.
3
  # It only calls src/rag_core.py:answer_query(query).
4
- # to launch http://localhost:7860
5
-
6
 
7
  import os
8
  import sys
9
  import traceback
10
- import gradio as gr
11
- from huggingface_hub import hf_hub_download
12
  import shutil
13
  from pathlib import Path
14
 
 
 
 
 
 
 
 
 
15
  def ensure_faiss_index_present():
16
  repo_id = os.environ.get("FAISS_REPO_ID", "FabIndy/code-education-faiss-index")
17
- token = os.environ.get("HF_TOKEN") # optionnel si dataset public
18
 
19
  local_dir = Path("db/faiss_code_edu_by_article")
20
  local_dir.mkdir(parents=True, exist_ok=True)
@@ -36,23 +41,15 @@ def ensure_faiss_index_present():
36
  shutil.copyfile(f_pkl, local_dir / "index.pkl")
37
 
38
 
39
-
40
-
41
- ensure_faiss_index_present()
42
-
43
-
44
-
45
  def ensure_model_present():
46
  os.makedirs("models", exist_ok=True)
47
 
48
- # Nouveau nom stable, cohérent avec rag_core.py
49
  local_path = os.path.join("models", "model.gguf")
50
  if os.path.exists(local_path):
51
  return
52
 
53
- # Repo HF contenant le GGUF
54
  repo_id = os.environ.get("MODEL_REPO_ID")
55
- # IMPORTANT: mets ici le VRAI filename du GGUF dans le repo HF
56
  filename = os.environ.get("MODEL_FILENAME")
57
 
58
  if not repo_id:
@@ -65,21 +62,22 @@ def ensure_model_present():
65
  )
66
 
67
  downloaded = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model")
68
-
69
  shutil.copyfile(downloaded, local_path)
70
 
71
 
 
72
  ensure_model_present()
73
 
74
 
 
 
 
75
 
76
- # Ensure we can import src/rag_core.py without requiring src/ to be a package
77
  ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
78
  SRC_DIR = os.path.join(ROOT_DIR, "src")
79
  if SRC_DIR not in sys.path:
80
  sys.path.insert(0, SRC_DIR)
81
 
82
- # Import the validated core
83
  try:
84
  import rag_core # src/rag_core.py
85
  except Exception as e:
@@ -89,6 +87,10 @@ except Exception as e:
89
  ) from e
90
 
91
 
 
 
 
 
92
  def _format_result(result) -> str:
93
  """
94
  Formats output robustly WITHOUT assuming a strict schema.
@@ -97,13 +99,10 @@ def _format_result(result) -> str:
97
  if result is None:
98
  return "Aucune réponse (result=None)."
99
 
100
- # Most common: a string answer
101
  if isinstance(result, str):
102
  return result
103
 
104
- # If the core returns a dict-like object
105
  if isinstance(result, dict):
106
- # Try common keys while staying generic
107
  parts = []
108
  if "mode" in result:
109
  parts.append(f"Mode: {result['mode']}")
@@ -112,145 +111,212 @@ def _format_result(result) -> str:
112
  elif "response" in result:
113
  parts.append(str(result["response"]))
114
  else:
115
- # Fallback: dump dict (readable)
116
  parts.append(str(result))
117
 
118
- # Optional: sources / context / citations
119
  for k in ["sources", "citations", "articles", "context_used", "context"]:
120
  if k in result and result[k]:
121
  parts.append(f"\n\n---\n{k}:\n{result[k]}")
122
  return "\n\n".join(parts)
123
 
124
- # If the core returns a tuple/list (e.g., (answer, meta))
125
  if isinstance(result, (tuple, list)):
126
  return "\n\n".join([str(x) for x in result])
127
 
128
- # Fallback
129
  return str(result)
130
 
131
 
132
- def chat_once(user_query: str) -> str:
133
- """
134
- Single-shot call to the validated RAG core.
135
- """
136
- q = (user_query or "").strip()
137
  if not q:
138
- return "Entre une question ou une demande (vide = rien à traiter)."
139
-
140
  try:
141
- # IMPORTANT: Do not change rag_core logic; just call it.
142
- result = rag_core.answer_query(q)
143
  return _format_result(result)
144
  except Exception:
145
- # Show error transparently (useful on HF Spaces logs)
146
- err = traceback.format_exc()
147
- return "Erreur côté application (pas côté utilisateur):\n\n" + err
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
 
 
 
149
 
150
  CSS = """
151
- /* Police sérieuse, institutionnelle */
152
  :root {
153
  --font-sans: Inter, "Source Sans 3", Roboto, "Segoe UI", Arial, sans-serif;
154
  }
155
-
156
  body, .gradio-container {
157
  font-family: var(--font-sans) !important;
158
  font-size: 15px;
159
  line-height: 1.5;
160
  }
161
-
162
- /* Titres plus sobres */
163
  h1, h2, h3 {
164
  font-weight: 600;
165
  letter-spacing: -0.01em;
166
  }
167
-
168
- /* Page un peu plus compacte */
169
  .gradio-container {
170
  max-width: 980px !important;
171
  }
172
-
173
- /* Réponse : hauteur max + scroll */
174
  #answer textarea {
175
- max-height: 360px !important;
176
  overflow-y: auto !important;
177
  font-size: 14px;
178
  line-height: 1.55;
179
  }
180
-
181
- /* Moins d'espacement vertical */
182
  .wrap {
183
  gap: 0.6rem !important;
184
  }
185
  """
186
 
 
187
 
188
- with gr.Blocks(
189
- title="Assistant Code de l’éducation (RAG)",
190
- css=CSS,
191
- theme=gr.themes.Soft(),
192
- ) as demo:
193
  gr.Markdown(
194
  """
195
  # Assistant Code de l’éducation
196
- Cet outil recherche dans le Code de l’éducation (version du 7 janvier 2026) et répond uniquement à partir des articles retrouvés.
197
-
198
- ### Ce que l’outil fait
199
- - Cite des articles (ou liste des articles pertinents)
200
- - Répond à une question si le texte nécessaire est présent dans les articles retrouvés
201
 
202
- ### Ce que l’outil ne fait pas
203
- - N’invente pas : si le contexte est insuffisant, il refuse et le dit clairement
204
- - Ne remplace pas une validation juridique
 
205
 
206
- Conseil : pour une citation exacte, demande “Donne l’intégralité de l’article …”.
207
  """.strip()
208
  )
209
 
210
  gr.Markdown(
211
- """
212
  > **Information importante**
213
  > Lors du premier lancement, l’application peut nécessiter 1 à 2 minutes d’initialisation.
214
  > Ensuite, l’utilisation est immédiate.
215
  > En cas d’utilisation simultanée, les demandes sont traitées successivement afin de garantir la fiabilité des réponses.
216
- """.strip()
217
- )
218
-
219
 
220
- with gr.Row():
221
- inp = gr.Textbox(
222
- label="Votre demande",
223
- placeholder="Ex : Donne l’intégralité de l’article D454-14",
224
- lines=2,
225
- max_lines=4,
226
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
  with gr.Row():
229
- out = gr.Textbox(
230
- label="Réponse",
231
- elem_id="answer",
232
- lines=10,
233
- max_lines=14,
234
- )
 
 
 
 
 
235
 
236
- with gr.Row():
237
- btn = gr.Button("Répondre", variant="primary")
238
- clear = gr.Button("Effacer")
239
-
240
- btn.click(chat_once, inputs=inp, outputs=out)
241
- clear.click(lambda: ("", ""), outputs=[inp, out])
242
-
243
- with gr.Accordion("Exemples de requêtes", open=False):
244
- gr.Examples(
245
- examples=[
246
- "Donne l'intégralité de l'article D454-14",
247
- "Liste les articles qui parlent de l'obligation scolaire",
248
- "Quelles sont les conditions de nomination d'un chef d'établissement ? Cite uniquement les articles fournis.",
249
- ],
250
- inputs=inp,
251
  )
252
 
253
 
254
- # HF Spaces expects launch on 0.0.0.0:7860
255
  if __name__ == "__main__":
256
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
1
  # app.py — Gradio UI for hf-code-education (CPU / Hugging Face Spaces)
2
  # This file must NOT change the validated RAG logic.
3
  # It only calls src/rag_core.py:answer_query(query).
4
+ # HF Spaces expects launch on 0.0.0.0:7860
 
5
 
6
  import os
7
  import sys
8
  import traceback
 
 
9
  import shutil
10
  from pathlib import Path
11
 
12
+ import gradio as gr
13
+ from huggingface_hub import hf_hub_download
14
+
15
+
16
+ # ----------------------------
17
+ # Assets download (FAISS + GGUF)
18
+ # ----------------------------
19
+
20
  def ensure_faiss_index_present():
21
  repo_id = os.environ.get("FAISS_REPO_ID", "FabIndy/code-education-faiss-index")
22
+ token = os.environ.get("HF_TOKEN") # optional if public dataset
23
 
24
  local_dir = Path("db/faiss_code_edu_by_article")
25
  local_dir.mkdir(parents=True, exist_ok=True)
 
41
  shutil.copyfile(f_pkl, local_dir / "index.pkl")
42
 
43
 
 
 
 
 
 
 
44
  def ensure_model_present():
45
  os.makedirs("models", exist_ok=True)
46
 
47
+ # Stable local name expected by rag_core.py
48
  local_path = os.path.join("models", "model.gguf")
49
  if os.path.exists(local_path):
50
  return
51
 
 
52
  repo_id = os.environ.get("MODEL_REPO_ID")
 
53
  filename = os.environ.get("MODEL_FILENAME")
54
 
55
  if not repo_id:
 
62
  )
63
 
64
  downloaded = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model")
 
65
  shutil.copyfile(downloaded, local_path)
66
 
67
 
68
+ ensure_faiss_index_present()
69
  ensure_model_present()
70
 
71
 
72
+ # ----------------------------
73
+ # Import validated RAG core (do not modify)
74
+ # ----------------------------
75
 
 
76
  ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
77
  SRC_DIR = os.path.join(ROOT_DIR, "src")
78
  if SRC_DIR not in sys.path:
79
  sys.path.insert(0, SRC_DIR)
80
 
 
81
  try:
82
  import rag_core # src/rag_core.py
83
  except Exception as e:
 
87
  ) from e
88
 
89
 
90
+ # ----------------------------
91
+ # Helpers
92
+ # ----------------------------
93
+
94
  def _format_result(result) -> str:
95
  """
96
  Formats output robustly WITHOUT assuming a strict schema.
 
99
  if result is None:
100
  return "Aucune réponse (result=None)."
101
 
 
102
  if isinstance(result, str):
103
  return result
104
 
 
105
  if isinstance(result, dict):
 
106
  parts = []
107
  if "mode" in result:
108
  parts.append(f"Mode: {result['mode']}")
 
111
  elif "response" in result:
112
  parts.append(str(result["response"]))
113
  else:
 
114
  parts.append(str(result))
115
 
 
116
  for k in ["sources", "citations", "articles", "context_used", "context"]:
117
  if k in result and result[k]:
118
  parts.append(f"\n\n---\n{k}:\n{result[k]}")
119
  return "\n\n".join(parts)
120
 
 
121
  if isinstance(result, (tuple, list)):
122
  return "\n\n".join([str(x) for x in result])
123
 
 
124
  return str(result)
125
 
126
 
127
+ def call_core(query: str) -> str:
128
+ q = (query or "").strip()
 
 
 
129
  if not q:
130
+ return "Entre une demande (vide = rien à traiter)."
 
131
  try:
132
+ result = rag_core.answer_query(q) # validated logic
 
133
  return _format_result(result)
134
  except Exception:
135
+ return "Erreur côté application (pas côté utilisateur):\n\n" + traceback.format_exc()
136
+
137
+
138
+ # ----------------------------
139
+ # Tab-specific wrappers (implicit routing without touching rag_core)
140
+ # ----------------------------
141
+
142
+ def tab_list(theme: str) -> str:
143
+ t = (theme or "").strip()
144
+ if not t:
145
+ return "Entre un thème (ex : vacances scolaires, obligation scolaire, conseil de classe)."
146
+ # Force LIST trigger
147
+ return call_core(f"Quels articles parlent de {t} ?")
148
+
149
+ def tab_fulltext(article_id: str) -> str:
150
+ a = (article_id or "").strip()
151
+ if not a:
152
+ return "Entre un identifiant d’article (ex : D422-5, L111-1, R421-10)."
153
+ # Force FULLTEXT trigger
154
+ return call_core(f"Donne l’intégralité de l’article {a}")
155
+
156
+ def tab_explain(article_id: str, level: str) -> str:
157
+ a = (article_id or "").strip()
158
+ if not a:
159
+ return "Entre un identifiant d’article (ex : D422-5)."
160
+ lvl = (level or "simple").strip().lower()
161
+ if lvl == "très simple":
162
+ prompt = f"Explique en termes très simples l’article {a}."
163
+ elif lvl == "détaillé":
164
+ prompt = f"Explique l’article {a} de façon détaillée mais concise."
165
+ else:
166
+ prompt = f"Explique en termes simples l’article {a}."
167
+ # Still RAG: rag_core will retrieve the article and enforce citations/refusal rules
168
+ return call_core(prompt)
169
+
170
+ def tab_advanced(question: str) -> str:
171
+ q = (question or "").strip()
172
+ if not q:
173
+ return "Entre une question."
174
+ # Free QA (slow) – clearly labeled in UI
175
+ return call_core(q)
176
+
177
+ def clear_all():
178
+ return "", "", "", "simple", "", ""
179
+
180
 
181
+ # ----------------------------
182
+ # UI
183
+ # ----------------------------
184
 
185
  CSS = """
 
186
  :root {
187
  --font-sans: Inter, "Source Sans 3", Roboto, "Segoe UI", Arial, sans-serif;
188
  }
 
189
  body, .gradio-container {
190
  font-family: var(--font-sans) !important;
191
  font-size: 15px;
192
  line-height: 1.5;
193
  }
 
 
194
  h1, h2, h3 {
195
  font-weight: 600;
196
  letter-spacing: -0.01em;
197
  }
 
 
198
  .gradio-container {
199
  max-width: 980px !important;
200
  }
 
 
201
  #answer textarea {
202
+ max-height: 420px !important;
203
  overflow-y: auto !important;
204
  font-size: 14px;
205
  line-height: 1.55;
206
  }
 
 
207
  .wrap {
208
  gap: 0.6rem !important;
209
  }
210
  """
211
 
212
+ THEME = gr.themes.Soft()
213
 
214
+ with gr.Blocks(title="Assistant Code de l’éducation (RAG)") as demo:
 
 
 
 
215
  gr.Markdown(
216
  """
217
  # Assistant Code de l’éducation
218
+ Cet outil recherche dans le Code de l’éducation et répond **uniquement** à partir des articles retrouvés.
 
 
 
 
219
 
220
+ ### Pour de meilleures performances (recommandé)
221
+ 1) **Commencez par lister les articles** (onglet “Trouver des articles”)
222
+ 2) **Consultez le texte exact** d’un article (onglet “Texte exact d’un article”)
223
+ 3) **Demandez une explication uniquement sur un article précis** (onglet “Expliquer un article”)
224
 
225
+ > Le mode “Question avancée” est **lent sur CPU**. Utilisez-le seulement si nécessaire.
226
  """.strip()
227
  )
228
 
229
  gr.Markdown(
230
+ """
231
  > **Information importante**
232
  > Lors du premier lancement, l’application peut nécessiter 1 à 2 minutes d’initialisation.
233
  > Ensuite, l’utilisation est immédiate.
234
  > En cas d’utilisation simultanée, les demandes sont traitées successivement afin de garantir la fiabilité des réponses.
235
+ """.strip()
236
+ )
 
237
 
238
+ with gr.Tabs():
239
+ with gr.Tab("Trouver des articles (rapide)"):
240
+ list_inp = gr.Textbox(
241
+ label="Thème",
242
+ placeholder="Ex : vacances scolaires, obligation scolaire, conseil de classe…",
243
+ lines=1,
244
+ )
245
+ list_btn = gr.Button("Lister les articles", variant="primary")
246
+ gr.Markdown(
247
+ "Astuce : ce mode est le plus rapide. Il sert à identifier les bons articles avant tout le reste."
248
+ )
249
+
250
+ with gr.Tab("Texte exact d’un article (rapide)"):
251
+ full_inp = gr.Textbox(
252
+ label="Identifiant d’article",
253
+ placeholder="Ex : D422-5",
254
+ lines=1,
255
+ )
256
+ full_btn = gr.Button("Afficher le texte exact", variant="primary")
257
+ gr.Markdown(
258
+ "Astuce : utilise ce mode pour obtenir une citation exacte (zéro hallucination)."
259
+ )
260
+
261
+ with gr.Tab("Expliquer un article (plus lent)"):
262
+ exp_inp = gr.Textbox(
263
+ label="Identifiant d’article",
264
+ placeholder="Ex : D422-5",
265
+ lines=1,
266
+ )
267
+ exp_level = gr.Dropdown(
268
+ label="Niveau d’explication",
269
+ choices=["simple", "très simple", "détaillé"],
270
+ value="simple",
271
+ )
272
+ exp_btn = gr.Button("Expliquer (LLM)", variant="primary")
273
+ gr.Markdown(
274
+ "Important : ce mode appelle le LLM → c’est plus lent sur CPU. "
275
+ "Pour de bonnes performances, reste sur **un article précis**."
276
+ )
277
+
278
+ with gr.Tab("Question avancée (lent)"):
279
+ adv_inp = gr.Textbox(
280
+ label="Votre question",
281
+ placeholder="Ex : Un chef d’établissement peut-il organiser un conseil de classe après 18 h ?",
282
+ lines=2,
283
+ max_lines=4,
284
+ )
285
+ adv_btn = gr.Button("Poser la question (lent)", variant="secondary")
286
+ gr.Markdown(
287
+ "Ce mode peut être lent sur CPU et peut refuser si les articles retrouvés ne suffisent pas."
288
+ )
289
+
290
+ # Shared output (single place to read answers)
291
+ out = gr.Textbox(label="Réponse", elem_id="answer", lines=12, max_lines=18)
292
 
293
  with gr.Row():
294
+ clear = gr.Button("Effacer", variant="secondary")
295
+
296
+ # Wire actions
297
+ list_btn.click(tab_list, inputs=list_inp, outputs=out)
298
+ full_btn.click(tab_fulltext, inputs=full_inp, outputs=out)
299
+ exp_btn.click(tab_explain, inputs=[exp_inp, exp_level], outputs=out)
300
+ adv_btn.click(tab_advanced, inputs=adv_inp, outputs=out)
301
+ clear.click(
302
+ clear_all,
303
+ outputs=[list_inp, full_inp, exp_inp, exp_level, adv_inp, out],
304
+ )
305
 
306
+ with gr.Accordion("Exemples", open=False):
307
+ gr.Markdown(
308
+ "- Trouver : `vacances scolaires`\n"
309
+ "- Texte exact : `D422-5`\n"
310
+ "- Expliquer : `D422-5`\n"
311
+ "- Avancé : `Quelles sont les conditions de nomination d'un chef d'établissement ? Cite uniquement les articles fournis.`"
 
 
 
 
 
 
 
 
 
312
  )
313
 
314
 
 
315
  if __name__ == "__main__":
316
+ # Pass css/theme to launch (Gradio 6.x)
317
+ demo.launch(
318
+ server_name="0.0.0.0",
319
+ server_port=7860,
320
+ css=CSS,
321
+ theme=THEME,
322
+ )