Spaces:
Paused
Paused
| # -*- coding: utf-8 -*- | |
| """ | |
| Assistant Vizyon Ayiti 360 - interface epuree pour Hugging Face Space. | |
| Chat + televersement de documents. RAG fonde uniquement sur le contenu du | |
| site (public + non public selon les droits). FR / EN / Kreyol. | |
| Lancer / run: streamlit run app.py | |
| """ | |
| import streamlit as st | |
| import config | |
| import gdpr | |
| import ingest | |
| st.set_page_config(page_title=config.APP_TITLE, | |
| page_icon=config.FAVICON_URL or "馃寠", layout="centered") | |
| # -------------------------------------------------------------------------- | |
| # Traductions / translations | |
| # -------------------------------------------------------------------------- | |
| T = { | |
| "fr": { | |
| "subtitle": "Posez vos questions. Reponses fondees sur le site Vizyon Ayiti 360.", | |
| "ask": "Votre question...", | |
| "sources": "Sources", | |
| "upload": "Joindre un document (PDF, DOCX, TXT, CSV)", | |
| "doc_hint": "Un document est joint : votre prochaine question portera dessus.", | |
| "clear": "Effacer la conversation", | |
| "thinking": "Recherche...", | |
| "building": "Indexation du contenu du site (premiere fois)...", | |
| "internal": "interne", | |
| "index_err": "Impossible de construire l'index du site.", | |
| }, | |
| "en": { | |
| "subtitle": "Ask your questions. Answers grounded in the Vizyon Ayiti 360 site.", | |
| "ask": "Your question...", | |
| "sources": "Sources", | |
| "upload": "Attach a document (PDF, DOCX, TXT, CSV)", | |
| "doc_hint": "A document is attached: your next question will use it.", | |
| "clear": "Clear conversation", | |
| "thinking": "Searching...", | |
| "building": "Indexing the site content (first run)...", | |
| "internal": "internal", | |
| "index_err": "Could not build the site index.", | |
| }, | |
| "ht": { | |
| "subtitle": "Poze kesyon ou yo. Repons yo baze sou sit Vizyon Ayiti 360.", | |
| "ask": "Kesyon ou...", | |
| "sources": "Sous", | |
| "upload": "Mete yon dokiman (PDF, DOCX, TXT, CSV)", | |
| "doc_hint": "Yon dokiman mete la : pwochen kesyon ou an ap sou li.", | |
| "clear": "Efase konv猫sasyon an", | |
| "thinking": "Y ap ch猫che...", | |
| "building": "Endeksasyon kontni sit la (premye fwa)...", | |
| "internal": "enten", | |
| "index_err": "Pa kapab konstwi end猫ks sit la.", | |
| }, | |
| } | |
| _LANG_LABELS = {"Francais": "fr", "English": "en", "Kreyol": "ht"} | |
| _LANG_ORDER = ["Francais", "English", "Kreyol"] | |
| if "lang" not in st.session_state: | |
| st.session_state.lang = config.DEFAULT_LANG | |
| if "messages" not in st.session_state: | |
| st.session_state.messages = [] | |
| def t(key): | |
| return T[st.session_state.lang][key] | |
| # -------------------------------------------------------------------------- | |
| # Portillon d'acces optionnel (jeton partage passe dans l'URL de l'iframe) | |
| # Optional access gate (shared token passed in the iframe URL) | |
| # -------------------------------------------------------------------------- | |
| def _check_access(): | |
| if not config.APP_ACCESS_TOKEN: | |
| return # portillon desactive | |
| try: | |
| provided = st.query_params.get("access", "") | |
| except Exception: | |
| provided = st.experimental_get_query_params().get("access", [""])[0] | |
| if provided != config.APP_ACCESS_TOKEN: | |
| st.error("Acces restreint. / Restricted access.") | |
| st.stop() | |
| _check_access() | |
| # -------------------------------------------------------------------------- | |
| # Indexation automatique (une fois par conteneur) / auto-index once per container | |
| # -------------------------------------------------------------------------- | |
| def ensure_index(): | |
| if config.REBUILD_ON_START or not ingest.index_exists(): | |
| ingest.build_index() | |
| return ingest.load_meta() | |
| # -------------------------------------------------------------------------- | |
| # Barre laterale / sidebar | |
| # -------------------------------------------------------------------------- | |
| with st.sidebar: | |
| if config.LOGO_URL: | |
| st.image(config.LOGO_URL, use_container_width=True) | |
| st.markdown("### Langue / Language / Lang") | |
| _cur = {"fr": 0, "en": 1, "ht": 2}.get(st.session_state.lang, 0) | |
| choice = st.radio("lang", _LANG_ORDER, index=_cur, | |
| label_visibility="collapsed") | |
| st.session_state.lang = _LANG_LABELS[choice] | |
| st.markdown("---") | |
| up = st.file_uploader(t("upload"), type=["pdf", "docx", "txt", "md", "csv"]) | |
| if up: | |
| st.caption(t("doc_hint")) | |
| st.markdown("---") | |
| st.info(gdpr.notice(st.session_state.lang)) | |
| meta = ingest.load_meta() | |
| if meta: | |
| extra = "" | |
| if meta.get("non_public"): | |
| extra = f" 路 {meta['non_public']} {t('internal')}" | |
| st.caption(f"{meta['chunks']} passages{extra}\n\n{meta['built_at']}") | |
| # -------------------------------------------------------------------------- | |
| # En-tete / header | |
| # -------------------------------------------------------------------------- | |
| st.caption(t("subtitle")) | |
| # Construction de l'index (avec retour visuel au premier lancement). | |
| try: | |
| with st.spinner(t("building")): | |
| ensure_index() | |
| except Exception as e: | |
| st.error(f"{t('index_err')} ({e})") | |
| st.stop() | |
| # -------------------------------------------------------------------------- | |
| # Historique / history | |
| # -------------------------------------------------------------------------- | |
| for msg in st.session_state.messages: | |
| with st.chat_message(msg["role"]): | |
| st.markdown(msg["content"]) | |
| if msg.get("sources"): | |
| with st.expander(t("sources")): | |
| for s in msg["sources"]: | |
| tag = "" if s.get("status", "publish") == "publish" \ | |
| else f" _({t('internal')})_" | |
| st.markdown(f"- [{s['title']}]({s['url']}){tag} 路 " | |
| f"score {s['score']:.2f}") | |
| # -------------------------------------------------------------------------- | |
| # Saisie / input | |
| # -------------------------------------------------------------------------- | |
| prompt = st.chat_input(t("ask")) | |
| if prompt: | |
| st.session_state.messages.append({"role": "user", "content": prompt}) | |
| with st.chat_message("user"): | |
| st.markdown(prompt) | |
| with st.chat_message("assistant"): | |
| with st.spinner(t("thinking")): | |
| try: | |
| if up is not None: | |
| import document_analyzer | |
| resp, sources, _ = document_analyzer.analyze_document( | |
| up.name, up.getvalue(), prompt, lang=st.session_state.lang) | |
| else: | |
| import rag_engine | |
| resp, sources = rag_engine.answer( | |
| prompt, lang=st.session_state.lang) | |
| except Exception as e: | |
| resp, sources = f"Erreur : {e}", [] | |
| st.markdown(resp) | |
| if sources: | |
| with st.expander(t("sources")): | |
| for s in sources: | |
| tag = "" if s.get("status", "publish") == "publish" \ | |
| else f" _({t('internal')})_" | |
| st.markdown(f"- [{s['title']}]({s['url']}){tag} 路 " | |
| f"score {s['score']:.2f}") | |
| st.session_state.messages.append( | |
| {"role": "assistant", "content": resp, "sources": sources}) | |
| if st.session_state.messages: | |
| if st.button(t("clear")): | |
| st.session_state.messages = [] | |
| st.rerun() | |