Spaces:
Paused
Paused
File size: 7,396 Bytes
bda6294 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | # -*- 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()
|