File size: 8,134 Bytes
56a777c
 
453d231
 
 
 
47b8a7e
1533d23
e891d20
fd2fa68
 
 
 
 
56a777c
fd2fa68
56a777c
 
 
fd2fa68
302d68d
56a777c
 
 
 
1533d23
56a777c
302d68d
1533d23
 
e891d20
56a777c
e891d20
 
 
 
 
 
 
 
 
 
 
 
 
56a777c
1533d23
 
 
302d68d
56a777c
fd2fa68
453d231
 
fd2fa68
56a777c
fd2fa68
453d231
56a777c
 
 
 
453d231
 
56a777c
453d231
 
 
56a777c
453d231
 
 
fd2fa68
56a777c
fd2fa68
229fbd9
 
 
 
 
 
 
 
453d231
 
229fbd9
453d231
 
229fbd9
453d231
 
56a777c
 
 
229fbd9
 
56a777c
229fbd9
 
56a777c
 
229fbd9
56a777c
453d231
 
56a777c
 
 
fd2fa68
 
453d231
229fbd9
56a777c
 
 
 
 
 
 
 
 
 
453d231
56a777c
453d231
 
229fbd9
fd2fa68
 
 
56a777c
fd2fa68
 
 
 
229fbd9
fd2fa68
 
229fbd9
fd2fa68
 
 
 
 
 
229fbd9
 
fd2fa68
 
 
229fbd9
 
 
456aba5
 
 
 
 
 
 
229fbd9
fd2fa68
 
 
 
 
229fbd9
fd2fa68
56a777c
fd2fa68
453d231
fd2fa68
 
 
453d231
56a777c
 
 
 
 
453d231
 
fd2fa68
453d231
56a777c
453d231
 
229fbd9
453d231
56a777c
 
 
 
 
 
453d231
56a777c
453d231
 
 
56a777c
 
 
 
453d231
fd2fa68
229fbd9
56a777c
 
 
 
fd2fa68
456aba5
56a777c
 
 
 
fd2fa68
456aba5
56a777c
456aba5
56a777c
 
 
 
 
456aba5
fd2fa68
56a777c
 
 
 
 
456aba5
453d231
56a777c
 
453d231
 
56a777c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# app.py — Simplified (Groq-only, no GGUF)
from __future__ import annotations

import os
import sys
import traceback
import shutil
from pathlib import Path

import gradio as gr
from huggingface_hub import hf_hub_download


# ----------------------------
# Helpers
# ----------------------------
def groq_enabled() -> bool:
    return bool(os.environ.get("GROQ_API_KEY", "").strip())


def ensure_faiss_index_present():
    """

    FAISS index is needed for QA retrieval.

    (Groq replaces ONLY the generator, not the retrieval.)

    """
    repo_id = os.environ.get("FAISS_REPO_ID", "FabIndy/code-education-faiss-index")
    token = os.environ.get("HF_TOKEN")  # optional if index repo is public

    local_dir = Path("db/faiss_code_edu_by_article")
    local_dir.mkdir(parents=True, exist_ok=True)

    # Download to HF cache
    f_faiss = hf_hub_download(
        repo_id=repo_id,
        repo_type="dataset",
        filename="index.faiss",
        token=token,
    )
    f_pkl = hf_hub_download(
        repo_id=repo_id,
        repo_type="dataset",
        filename="index.pkl",
        token=token,
    )

    # Copy to expected local dir
    shutil.copyfile(f_faiss, local_dir / "index.faiss")
    shutil.copyfile(f_pkl, local_dir / "index.pkl")


# Always ensure FAISS (required for QA retrieval)
ensure_faiss_index_present()


# ----------------------------
# Import validated RAG core
# ----------------------------
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))

# rag_core imports "from src import ..." so we add project root (not /src)
if ROOT_DIR not in sys.path:
    sys.path.insert(0, ROOT_DIR)

try:
    from src import rag_core
except Exception as e:
    raise RuntimeError(
        "Impossible d'importer src/rag_core.py. "
        "Vérifie que le dossier src/ contient bien rag_core.py et qu'il n'y a pas d'erreurs d'import."
    ) from e


# ----------------------------
# Rendering helpers
# ----------------------------
def _render_list(articles) -> str:
    if not articles:
        return "Aucun article trouvé."
    arts = [str(a).strip() for a in articles if str(a).strip()]
    arts = sorted(set(arts))
    return "Articles proposés :\n" + "\n".join([f"- {a}" for a in arts])


def _format_result(result) -> str:
    if result is None:
        return "Aucune réponse."

    if isinstance(result, str):
        return result.strip() or "Aucune réponse."

    if isinstance(result, dict):
        mode = str(result.get("mode", "")).strip().upper()
        answer = result.get("answer", result.get("response", "")) or ""
        answer = str(answer).strip()
        articles = result.get("articles") or []

        if mode == "LIST":
            return _render_list(articles)

        tail = f"\n\nArticles : {', '.join(map(str, articles))}" if articles else ""
        return (answer or "Aucune réponse.") + tail

    return str(result).strip() or "Aucune réponse."


# ----------------------------
# Core call
# ----------------------------
def call_core(query: str) -> str:
    q = (query or "").strip()
    if not q:
        return "Entre une demande."

    # Groq-only: if missing, fail fast with a clear message
    if not groq_enabled():
        return (
            "Groq n'est pas configuré.\n\n"
            "Ajoute la variable d'environnement GROQ_API_KEY dans le Space "
            "(Settings → Variables).\n"
            "Optionnel : GROQ_MODEL, GROQ_MAX_TOKENS_SUMMARY, GROQ_MAX_TOKENS_QA, GROQ_TEMPERATURE."
        )

    try:
        result = rag_core.answer_query(q)
        return _format_result(result)
    except Exception:
        return "Erreur côté application :\n\n" + traceback.format_exc()


# ----------------------------
# Tab wrappers
# ----------------------------
def tab_list(theme: str) -> str:
    t = (theme or "").strip()
    if not t:
        return "Entre un thème (ex : vacances scolaires, conseil de classe, obligation scolaire)."
    return call_core(f"Quels articles parlent de {t} ?")


def tab_fulltext(article_id: str) -> str:
    a = (article_id or "").strip()
    if not a:
        return "Entre un identifiant d’article (ex : D422-5, L111-1, R421-10)."
    return call_core(f"Donne l’intégralité de l’article {a}")


def tab_synthese(article_id: str) -> str:
    a = (article_id or "").strip()
    if not a:
        return "Entre un identifiant d’article (ex : D422-5)."
    return call_core(f"Synthèse (points clés) de l’article {a}")


def tab_summary_ai(article_id: str) -> str:
    a = (article_id or "").strip()
    if not a:
        return "Entre un identifiant d’article (ex : D422-5)."
    return call_core(f"Résumé IA de l’article {a}")


def tab_qa(question: str) -> str:
    q = (question or "").strip()
    if not q:
        return "Entre une question."
    return call_core(q)


def clear_all():
    return "", "", "", ""


# ----------------------------
# UI
# ----------------------------
CSS = """

:root { --font-sans: Inter, "Source Sans 3", Roboto, "Segoe UI", Arial, sans-serif; }

body, .gradio-container { font-family: var(--font-sans) !important; font-size: 15px; line-height: 1.5; }

.gradio-container { max-width: 980px !important; }

#answer textarea { max-height: 480px !important; overflow-y: auto !important; font-size: 14px; line-height: 1.55; }

.small-note { font-size: 13px; opacity: 0.9; }

"""

THEME = gr.themes.Soft()

with gr.Blocks(title="Code de l’éducation — Assistant (Groq)", css=CSS, theme=THEME) as demo:
    gr.Markdown(
        """

# Code de l’éducation — Assistant (RAG)



- **LIST** : trouve des articles (recherche explicable)

- **Texte officiel** : affiche l’article exact

- **Résumé** :

  - **Extraits officiels** : fiable (sans reformulation)

  - **Résumé IA** : rapide (reformulation, peut comporter des erreurs)

- **Question (IA)** : interprétatif → toujours vérifier sur le texte officiel



> Génération **100% via Groq**.

        """.strip()
    )

    if groq_enabled():
        gr.Markdown("Groq configuré.")
    else:
        gr.Markdown("Groq non configuré : ajoute `GROQ_API_KEY` dans les Variables du Space.")

    with gr.Tabs():
        with gr.Tab("Trouver des articles"):
            list_inp = gr.Textbox(label="Thème", placeholder="Ex : vacances scolaires, conseil de classe…")
            list_btn = gr.Button("Chercher")
            list_out = gr.Textbox(label="Résultat", elem_id="answer", lines=18)
            list_btn.click(fn=tab_list, inputs=list_inp, outputs=list_out)

        with gr.Tab("Texte officiel"):
            ft_inp = gr.Textbox(label="Identifiant d’article", placeholder="Ex : D521-5")
            ft_btn = gr.Button("Afficher")
            ft_out = gr.Textbox(label="Texte officiel", elem_id="answer", lines=18)
            ft_btn.click(fn=tab_fulltext, inputs=ft_inp, outputs=ft_out)

        with gr.Tab("Résumé"):
            s_inp = gr.Textbox(label="Identifiant d’article", placeholder="Ex : D521-5")
            with gr.Row():
                s_btn = gr.Button("Extraits officiels (fiable)")
                ai_btn = gr.Button("Résumé IA (rapide)")
            s_out = gr.Textbox(label="Résumé", elem_id="answer", lines=18)
            s_btn.click(fn=tab_synthese, inputs=s_inp, outputs=s_out)
            ai_btn.click(fn=tab_summary_ai, inputs=s_inp, outputs=s_out)


        with gr.Tab("Question (IA)"):
            qa_inp = gr.Textbox(label="Question", placeholder="Ex : Qui décide des dates de vacances scolaires ?")
            qa_btn = gr.Button("Répondre")
            qa_out = gr.Textbox(label="Réponse", elem_id="answer", lines=18)
            qa_btn.click(fn=tab_qa, inputs=qa_inp, outputs=qa_out)

    with gr.Row():
        clear_btn = gr.Button("Effacer")
    clear_btn.click(fn=clear_all, inputs=None, outputs=[list_inp, ft_inp, s_inp, qa_inp])

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)