hfchat / app.py
Phitographix's picture
Upload 14 files
bda6294 verified
Raw
History Blame Contribute Delete
7.4 kB
# -*- 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()