Spaces:
Running
Running
| # 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) | |