""" PolyglotRAG — assistant documentaire multilingue cross-lingue. Point d'entrée de l'application Gradio (Hugging Face Spaces, SDK docker). Onglets : - Assistant : conversation multilingue avec citations et analyse. - Architecture : schéma du pipeline complet. - Administration: métriques du RAG (volume, latence, langues, abstention). L'interface elle-même est traduite dans les 6 langues du projet (sélecteur en haut à droite ; choix mémorisé dans le navigateur, détection automatique de la langue du navigateur, paramètre d'URL `?lang=xx`). """ from __future__ import annotations import json import os from pathlib import Path import gradio as gr from config import settings from core.retrieval.vector_store import get_vector_store from core.ui.admin_tab import admin_i18n_updates, build_admin_tab from core.ui.architecture_tab import architecture_i18n_updates, build_architecture_tab from core.ui.chat_tab import build_chat_tab, chat_i18n_updates from core.ui.components import ( render_brand, render_config_banner, render_footer, render_status, ) from core.ui.i18n import UI_LANGUAGES, normalize_lang, t, ui_language_choices from core.ui.theme import CUSTOM_CSS, HEAD_HTML, build_theme ASSETS_DIR = Path(__file__).resolve().parent / "core" / "assets" AVATAR_PATH = ASSETS_DIR / "assistant.svg" DEFAULT_LANG = normalize_lang(settings.default_ui_language) _TAB_KEYS = ("tab_chat", "tab_arch", "tab_admin") _TAB_LABELS_JSON = json.dumps( {code: [t(code, k) for k in _TAB_KEYS] for code in UI_LANGUAGES}, ensure_ascii=False ) _SUPPORTED_JSON = json.dumps(list(UI_LANGUAGES)) # Applique côté navigateur : sens d'écriture (RTL pour l'arabe), attribut # `lang` du document, libellés des onglets et mémorisation du choix. APPLY_LANG_JS = f""" (lang) => {{ const labels = {_TAB_LABELS_JSON}; const root = document.querySelector('.gradio-container'); if (root) root.setAttribute('dir', lang === 'ar' ? 'rtl' : 'ltr'); document.documentElement.setAttribute('lang', lang); try {{ localStorage.setItem('pg-ui-lang', lang); }} catch (e) {{}} const tabs = document.querySelectorAll('#pg-tabs button[role="tab"]'); if (labels[lang] && tabs.length === labels[lang].length) {{ tabs.forEach((b, i) => {{ b.textContent = labels[lang][i]; }}); }} return lang; }} """ # Au chargement : ?lang=xx > choix mémorisé > langue du navigateur > défaut. INIT_LANG_JS = f""" () => {{ const supported = {_SUPPORTED_JSON}; let lang = new URLSearchParams(window.location.search).get('lang'); if (!lang) {{ try {{ lang = localStorage.getItem('pg-ui-lang'); }} catch (e) {{}} }} if (!lang) lang = (navigator.language || '').slice(0, 2).toLowerCase(); if (!supported.includes(lang)) lang = '{DEFAULT_LANG}'; const root = document.querySelector('.gradio-container'); if (root) root.setAttribute('dir', lang === 'ar' ? 'rtl' : 'ltr'); document.documentElement.setAttribute('lang', lang); return lang; }} """ def _config_ok() -> bool: return not settings.validate() def status_html(lang: str) -> str: try: corpus = get_vector_store().corpus_stats() except Exception: # noqa: BLE001 - l'en-tête ne doit jamais casser l'app corpus = {"total_chunks": 0, "documents": 0} return render_status(normalize_lang(lang), corpus, _config_ok()) def build_app() -> gr.Blocks: lang = DEFAULT_LANG config_ok = _config_ok() with gr.Blocks( title="PolyglotRAG — Multilingual Document Assistant", theme=build_theme(), css=CUSTOM_CSS, head=HEAD_HTML, analytics_enabled=False, ) as demo: # ------------------------------ En-tête ------------------------------ with gr.Row(elem_id="pg-header", equal_height=True): with gr.Column(scale=1, min_width=320): brand = gr.HTML(render_brand(lang), elem_classes=["pg-html-flush"]) with gr.Column(scale=0, min_width=240, elem_id="pg-header-right"): ui_lang_dd = gr.Dropdown( choices=ui_language_choices(), value=lang, label=t(lang, "ui_language"), show_label=False, container=False, interactive=True, elem_id="pg-ui-lang", ) status = gr.HTML( render_status(lang, {"total_chunks": 0}, config_ok) if not config_ok else "", elem_classes=["pg-html-flush"], ) banner = gr.HTML(render_config_banner(lang, config_ok), elem_classes=["pg-html-flush"]) # ------------------------------ Onglets ------------------------------ with gr.Tabs(elem_id="pg-tabs"): with gr.Tab(t(lang, "tab_chat"), id="chat") as tab_chat: chat = build_chat_tab( lang, ui_lang_dd, str(AVATAR_PATH) if AVATAR_PATH.exists() else None ) with gr.Tab(t(lang, "tab_arch"), id="architecture") as tab_arch: arch = build_architecture_tab(lang) with gr.Tab(t(lang, "tab_admin"), id="admin") as tab_admin: admin = build_admin_tab(lang, ui_lang_dd) footer = gr.HTML(render_footer(lang), elem_classes=["pg-html-flush"]) # ------------------------ Changement de langue ------------------------ def apply_language(new_lang, last, admin_state, response_value, scope_value): new_lang = normalize_lang(new_lang) return [ render_brand(new_lang), status_html(new_lang), render_config_banner(new_lang, _config_ok()), gr.update(label=t(new_lang, "tab_chat")), gr.update(label=t(new_lang, "tab_arch")), gr.update(label=t(new_lang, "tab_admin")), render_footer(new_lang), *chat_i18n_updates(new_lang, last, response_value, scope_value), *architecture_i18n_updates(new_lang), *admin_i18n_updates(new_lang, admin_state), ] i18n_outputs = [ brand, status, banner, tab_chat, tab_arch, tab_admin, footer, *chat["i18n_outputs"], *arch["i18n_outputs"], *admin["i18n_outputs"], ] ui_lang_dd.change(None, inputs=[ui_lang_dd], outputs=None, js=APPLY_LANG_JS) ui_lang_dd.change( apply_language, inputs=[ui_lang_dd, chat["last_state"], admin["state"], chat["response_lang"], chat["scope"]], outputs=i18n_outputs, queue=False, ) # ------------------------------ Chargement ---------------------------- demo.load(None, inputs=None, outputs=[ui_lang_dd], js=INIT_LANG_JS) demo.load(status_html, inputs=[ui_lang_dd], outputs=[status]) return demo demo = build_app() if __name__ == "__main__": # 0.0.0.0 + $PORT (par défaut 7860) : fonctionne à l'identique en local, # dans le conteneur Docker fourni, et sur un Hugging Face Space. demo.queue(default_concurrency_limit=8).launch( server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)), show_api=False, allowed_paths=[str(ASSETS_DIR)], favicon_path=str(ASSETS_DIR / "favicon.svg") if (ASSETS_DIR / "favicon.svg").exists() else None, )