# -*- 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 # -------------------------------------------------------------------------- @st.cache_resource(show_spinner=False) 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()