Ismailzeine's picture
Upload 37 files
6dab339 verified
Raw
History Blame Contribute Delete
17.1 kB
import os
os.environ["HF_HUB_DISABLE_SYMLINKS"] = "1"
import re
import numpy as np
import gradio as gr
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_groq import ChatGroq
from rank_bm25 import BM25Okapi
from sentence_transformers import CrossEncoder
# ==========================================
# 1. INITIALISATION
# ==========================================
print("Démarrage de l'application...")
# --- A. Configuration de l'API Groq ---
groq_api_key = os.getenv("rag_api")
if not groq_api_key:
print("ATTENTION : Clé rag_api introuvable !")
llm = ChatGroq(
groq_api_key=groq_api_key,
model_name="llama-3.1-8b-instant",
temperature=0.1
)
# --- B. Chargement des Modèles de Recherche ---
print("Chargement des modèles de recherche...")
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
rerank_model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2', device='cpu')
# --- C. Chargement des Données et Indexation ---
path_to_data = './data'
def load_docs(directory):
docs = []
if not os.path.exists(directory):
print(f"⚠️ Dossier {directory} non trouvé. L'application démarrera avec des données vides.")
return docs
for filename in [f for f in os.listdir(directory) if f.endswith('.md')]:
filepath = os.path.join(directory, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
text = f.read()
if text.startswith('---'):
title_match = re.search(r"Title:\s*(.+)", text)
content_match = re.search(r"Content:\s*(.+?)(?=\n---|\Z)", text, re.DOTALL)
content = content_match.group(1).strip() if content_match else text
else:
title_match = re.search(r"Title:\s*(.+)", text)
content = text.split("Content:")[-1].strip() if "Content:" in text else text
doc_title = title_match.group(1).strip() if title_match else filename
docs.append(Document(
page_content=f"Document: {doc_title}\n{content}",
metadata={"title": doc_title, "source": filename}
))
except Exception as e:
print(f"⚠️ Erreur lors du chargement de {filename}: {e}")
continue
return docs
raw_docs = load_docs(path_to_data)
if raw_docs:
print("Création des index...")
text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
documents = text_splitter.split_documents(raw_docs)
vector_db = FAISS.from_documents(documents, embeddings)
tokenized_corpus = [doc.page_content.lower().split() for doc in documents]
bm25 = BM25Okapi(tokenized_corpus)
else:
documents = [Document(page_content="Données non trouvées.", metadata={"title": "Erreur"})]
vector_db = FAISS.from_documents(documents, embeddings)
bm25 = BM25Okapi([["erreur"]])
print("✅ Initialisation terminée.")
# ==========================================
# 2. LOGIQUE MÉTIER (RAG)
# ==========================================
def hybrid_search(query, k=10):
vector_results = vector_db.similarity_search(query, k=k)
tokenized_query = query.lower().split()
bm25_scores = bm25.get_scores(tokenized_query)
top_bm25_indices = np.argsort(bm25_scores)[::-1][:k]
bm25_results = [documents[i] for i in top_bm25_indices]
combined_results = []
seen_content = set()
for doc in vector_results + bm25_results:
if doc.page_content not in seen_content:
combined_results.append(doc)
seen_content.add(doc.page_content)
return combined_results[:k]
def get_reranked_documents(query, initial_docs, top_n=3):
if not initial_docs:
return []
pairs = [[query, doc.page_content] for doc in initial_docs]
scores = rerank_model.predict(pairs)
for i, doc in enumerate(initial_docs):
doc.metadata['rerank_score'] = scores[i]
reranked_docs = sorted(initial_docs, key=lambda x: x.metadata['rerank_score'], reverse=True)
return reranked_docs[:top_n]
def ask_bankily_with_memory(query):
initial_docs = hybrid_search(query, k=10)
final_docs = get_reranked_documents(query, initial_docs, top_n=3)
context_text = "\n\n".join([f"SOURCE: {d.metadata['title']}\n{d.page_content}" for d in final_docs])
prompt_complet = f"""Tu es l'assistant expert de Bankily.
Consignes :
- Tu DOIS répondre UNIQUEMENT en utilisant les informations présentes dans les "Documents de référence" ci-dessous.
- Si la réponse ne s'y trouve pas, dis simplement : "Je ne sais pas". N'invente jamais rien.
- Ne mentionne pas le mot "documents", agis comme si tu savais la réponse naturellement.
Documents de référence :
{context_text}
Question actuelle : {query}"""
try:
response = llm.invoke(prompt_complet)
clean_response = response.content.strip()
except Exception as e:
clean_response = f"Erreur avec l'API Groq: {str(e)}"
return clean_response
# ==========================================
# 3. INTERFACE GRADIO
# ==========================================
DEVICE_LABEL = "CPU (Groq API)"
LOGO_URL = "https://www.elmourageb.com/sites/default/files/IMG_20250429_114353.jpg"
USER_AVATAR = "https://api.dicebear.com/7.x/initials/svg?seed=Vous&backgroundColor=0e6b4e"
_AR = re.compile(r'[\u0600-\u06FF]')
_FR_HINTS = set("comment quel quelle quels pourquoi mon ma mes le la les un une des du de pour avec je vous nous ouvrir compte argent carte numero telephone est faire puis envoyer payer combien ou quand".split())
_EN_HINTS = set("how what why is are can do does my the a an of for with you we open account money card number phone send pay where when which".split())
def detect_lang(text):
if _AR.search(text):
return 'ar'
words = re.findall(r"[a-zA-Zàâçéèêëîïôûùüÿœæ']+", text.lower())
fr = sum(w in _FR_HINTS for w in words)
en = sum(w in _EN_HINTS for w in words)
if re.search(r'[àâçéèêëîïôûùüÿœæ]', text):
fr += 2
return 'en' if en > fr else 'fr'
_DIRECTIVE = {
'fr': " (IMPORTANT : réponds uniquement et entièrement en français.)",
'ar': " (مهم جداً: أجب بالكامل وحصراً باللغة العربية.)",
'en': " (IMPORTANT: answer only and entirely in English.)",
}
def rag_answer(message):
lang = detect_lang(message)
return ask_bankily_with_memory(message + _DIRECTIVE[lang])
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700;800&family=Inter:wght@400;500;600&display=swap');
html, body {height:100%; margin:0; background:#EEF3F0;}
.gradio-container {
font-family:'Inter',sans-serif !important;
width:100% !important; max-width:100% !important;
margin:0 !important; padding:0 !important;
min-height:100vh !important; overflow:hidden !important;
background:linear-gradient(180deg,#F4F8F5 0%, #E9F1EC 100%) !important;
color:#143028 !important;
}
footer {display:none !important;}
/* BARRE HAUT */
.navbar {
position:fixed !important; top:0; left:0; right:0; z-index:50;
background:linear-gradient(135deg,#0B6E4F 0%, #11A077 100%) !important;
padding:8px 22px !important; gap:6px !important;
box-shadow:0 4px 16px rgba(11,110,79,0.22);
border-radius:0 0 16px 16px !important; margin:0 !important;
}
.navrow {display:flex; align-items:center; gap:20px; width:100%;}
.nav-brand {display:flex; align-items:center; gap:10px;}
.nav-brand img {width:38px; height:38px; border-radius:10px; border:2px solid rgba(255,255,255,0.55); object-fit:cover;}
.nav-name {font-family:'Sora',sans-serif; font-weight:800; font-size:1.02rem; color:#fff; line-height:1.05;}
.nav-name span {color:#FFE9A8;}
.nav-sub {font-size:0.60rem; color:#D8F0E5; letter-spacing:1.2px; text-transform:uppercase;}
.nav-menu a {color:rgba(255,255,255,0.95); text-decoration:none; font-weight:600; font-size:0.86rem; margin:0 9px;}
.nav-menu a:hover {color:#FFE9A8;}
.badge-live {display:inline-flex; align-items:center; gap:6px; background:rgba(255,255,255,0.16);
border:1px solid rgba(255,255,255,0.30); color:#FFE9A8; font-size:0.60rem; font-weight:700;
letter-spacing:1px; text-transform:uppercase; padding:3px 9px; border-radius:999px;}
.dot {width:6px; height:6px; background:#9CFFB0; border-radius:50%; display:inline-block;
box-shadow:0 0 8px #9CFFB0; animation:pulse 1.8s infinite;}
@keyframes pulse {0%,100%{opacity:1;} 50%{opacity:.4;}}
.nav-info {margin-top:5px; display:flex; align-items:center; flex-wrap:wrap; gap:4px;}
.tech-chip {display:inline-block; background:rgba(255,255,255,0.15); border:1px solid rgba(255,255,255,0.30);
color:#EAFBF2; border-radius:999px; padding:2px 9px; font-size:0.66rem; font-weight:600;}
.sep {color:rgba(255,255,255,0.5); margin:0 6px;}
.sess {color:#EAFBF2; font-size:0.74rem;}
.sess b {color:#FFE9A8;}
#clear-btn {background:rgba(255,255,255,0.16) !important; color:#fff !important;
border:1px solid rgba(255,255,255,0.45) !important; border-radius:11px !important;
font-weight:600 !important; font-size:0.80rem !important; white-space:nowrap !important;
min-width:max-content !important; flex:0 0 auto !important; padding:6px 12px !important;}
#clear-btn:hover {background:rgba(255,255,255,0.28) !important;}
/* ZONE CHAT */
#chatwrap {margin-top:104px !important; padding:0 22px !important;}
#chatbot {
height:calc(100vh - 300px) !important; min-height:160px !important;
background:#FFFFFF !important; border:1px solid #D7E6DE !important;
border-radius:16px !important; box-shadow:0 6px 22px rgba(11,110,79,0.08) !important;
}
#chatbot .bubble-wrap {height:100% !important; max-height:none !important; overflow-y:auto !important;
background:transparent !important; padding:10px !important;}
#chatbot .message-row {margin-bottom:10px !important;}
#chatbot .message, #chatbot .message * {opacity:1 !important;}
#chatbot .message {border-radius:15px !important; padding:11px 15px !important;
font-size:0.96rem !important; line-height:1.5 !important;}
#chatbot .user-row .message, #chatbot .user .message, #chatbot [data-testid="user"] .message {
background:#0E6B4E !important; border:1px solid #0B6E4F !important;}
#chatbot .user-row .message *, #chatbot .user .message *, #chatbot [data-testid="user"] .message * {color:#FFFFFF !important;}
#chatbot .bot-row .message, #chatbot .bot .message, #chatbot [data-testid="bot"] .message {
background:#F1F7F4 !important; border:1px solid #CFE0D7 !important;}
#chatbot .bot-row .message *, #chatbot .bot .message *, #chatbot [data-testid="bot"] .message * {color:#143028 !important;}
#chatbot .avatar-container img {border-radius:12px !important; border:1px solid #BBD9C9;}
/* BARRE BAS */
.bottombar {
position:fixed !important; bottom:0; left:0; right:0; z-index:50;
background:#EAF2EC !important; border-top:1px solid #D3E4DB !important;
padding:8px 22px 12px 22px !important; margin:0 !important;
box-shadow:0 -4px 18px rgba(11,110,79,0.08);
}
.section-label {font-family:'Sora',sans-serif; font-size:0.68rem; font-weight:700; letter-spacing:1.2px;
text-transform:uppercase; color:#0B6E4F; margin:2px 0 5px 2px;}
.sugg-btn {background:#FFFFFF !important; color:#0B6E4F !important; border:1px solid #BBD9C9 !important;
border-radius:11px !important; padding:7px 9px !important; font-size:0.77rem !important;
font-weight:600 !important; transition:all .2s ease !important; min-width:0 !important;}
.sugg-btn:hover {background:#E4F1EA !important; border-color:#0E8A63 !important; transform:translateY(-1px);}
#user-input textarea {
background:#FFFFFF !important; color:#143028 !important;
border:1.5px solid #9FC6B4 !important; border-radius:13px !important;
font-size:1rem !important; box-shadow:0 4px 14px rgba(11,110,79,0.06) !important;}
#user-input textarea::placeholder {color:#7C9488 !important; opacity:1 !important;}
#send-btn {background:linear-gradient(135deg,#0B6E4F,#11A077) !important; color:#fff !important;
border:none !important; border-radius:13px !important; font-weight:700 !important;}
#send-btn:hover {filter:brightness(1.07);}
"""
THINKING = "✍️ Rédaction de la réponse en cours…"
CHIPS = ('<span class="tech-chip">Mixtral-8x7B</span><span class="tech-chip">Groq API</span>'
'<span class="tech-chip">BGE-M3</span><span class="tech-chip">FAISS</span>'
'<span class="tech-chip">BM25</span>')
def session_line(history):
return (f'<div class="nav-info">{CHIPS}<span class="sep">·</span>'
f'<span class="sess">💬 <b>{len(history)}</b> messages '
f'<span class="sep">·</span> 🖥️ <b>{DEVICE_LABEL}</b></span></div>')
def add_user(message, history):
message = (message or "").strip()
if not message:
return history, ""
history = history + [{"role": "user", "content": message},
{"role": "assistant", "content": THINKING}]
return history, ""
def add_user_text(text, history):
return history + [{"role": "user", "content": text},
{"role": "assistant", "content": THINKING}]
def bot_reply(history):
if not history:
return history, session_line(history)
user_msg = None
for msg in reversed(history):
if msg.get("role") == "user":
user_msg = msg.get("content")
break
if user_msg:
answer = rag_answer(user_msg)
else:
answer = "Je n'ai pas compris votre question."
new_history = history.copy()
if new_history and new_history[-1].get("role") == "assistant" and new_history[-1].get("content") == THINKING:
new_history[-1] = {"role": "assistant", "content": answer}
else:
new_history.append({"role": "assistant", "content": answer})
return new_history, session_line(new_history)
def clear_chat():
return [], session_line([])
with gr.Blocks(css=CSS, title="Bankily AI Assistant", theme=gr.themes.Base()) as demo:
with gr.Column(elem_classes="navbar"):
with gr.Row():
gr.HTML(f"""
<div class="navrow">
<div class="nav-brand">
<img src="{LOGO_URL}" alt="Bankily">
<div><div class="nav-name">Bankily <span>AI</span></div>
<div class="nav-sub">Assistant · BPM</div></div>
</div>
<span class="badge-live"><span class="dot"></span> En ligne</span>
<div class="nav-menu">
<a href="https://www.bankily.mr/" target="_blank">Accueil</a>
<a href="https://www.bankily.mr/" target="_blank">Services</a>
<a href="https://www.bankily.mr/" target="_blank">Aide</a>
</div>
</div>""")
clear_btn = gr.Button("🗑️ Nouvelle conversation", elem_id="clear-btn", scale=0)
session_html = gr.HTML(session_line([]))
with gr.Column(elem_id="chatwrap"):
chatbot = gr.Chatbot(elem_id="chatbot", type="messages", show_label=False,
avatar_images=(USER_AVATAR, LOGO_URL))
with gr.Column(elem_classes="bottombar"):
gr.HTML('<div class="section-label">💡 Questions fréquentes</div>')
with gr.Row():
s1 = gr.Button("📱 Ouvrir un compte", elem_classes="sugg-btn")
s2 = gr.Button("💸 Envoyer de l'argent", elem_classes="sugg-btn")
s3 = gr.Button("🔐 Code PIN oublié", elem_classes="sugg-btn")
s4 = gr.Button("🏪 Payer chez un marchand", elem_classes="sugg-btn")
with gr.Row():
user_input = gr.Textbox(placeholder="✍️ Posez votre question sur Bankily...",
elem_id="user-input", show_label=False, scale=8, container=False)
send_btn = gr.Button("Envoyer", elem_id="send-btn", scale=2)
send_btn.click(add_user, [user_input, chatbot], [chatbot, user_input]) \
.then(bot_reply, chatbot, [chatbot, session_html])
user_input.submit(add_user, [user_input, chatbot], [chatbot, user_input]) \
.then(bot_reply, chatbot, [chatbot, session_html])
clear_btn.click(clear_chat, None, [chatbot, session_html])
s1.click(lambda h: add_user_text("Comment ouvrir un compte Bankily ?", h), chatbot, chatbot) \
.then(bot_reply, chatbot, [chatbot, session_html])
s2.click(lambda h: add_user_text("Comment envoyer de l'argent ?", h), chatbot, chatbot) \
.then(bot_reply, chatbot, [chatbot, session_html])
s3.click(lambda h: add_user_text("Que faire si j'oublie mon code PIN ?", h), chatbot, chatbot) \
.then(bot_reply, chatbot, [chatbot, session_html])
s4.click(lambda h: add_user_text("Comment payer chez un marchand ?", h), chatbot, chatbot) \
.then(bot_reply, chatbot, [chatbot, session_html])
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False
)