# 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)