FabIndy's picture
Improve AI summary UX and tighten prompt to prevent truncation
845e1f0
# 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)