abdelom's picture
Update pages/2_Chatbot_AR.py
98894ba verified
import streamlit as st
import pandas as pd
import os
from pathlib import Path
import base64
# LangChain & Hugging Face
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
from langchain.schema import Document
from langchain.prompts import PromptTemplate
from langchain.llms import HuggingFaceHub
from langchain.chains import LLMChain
import pysqlite3
import sys
sys.modules["sqlite3"] = pysqlite3
####
from langchain_openai import ChatOpenAI
#####################
# 1. HELPER FUNCTIONS
#####################
def get_base64_of_bin_file(bin_file_path: str) -> str:
file_bytes = Path(bin_file_path).read_bytes()
return base64.b64encode(file_bytes).decode()
def find_parent_ar(data, r, col):
"""
Trouve la question parente pour une ligne et colonne donnée dans le DataFrame (version AR).
"""
i = r - 1
parent = None
while i >= 0 and pd.isna(parent):
parent = data.iloc[i, col]
i -= 1
return parent
def create_contextual_ar(df, category, strat_id=0):
"""
Crée un DataFrame avec questions-réponses contextuelles (version AR).
"""
rows = []
columns_qna = list(df.columns)
for r, row in df.iterrows():
for level, col in enumerate(df.columns):
question = row[col]
if pd.isna(question):
continue
# Si la question est un "leaf node"
if level == 4 or pd.isna(row[columns_qna[level + 1]]):
# Gérer des sous-questions multiples
if "\n*Si" in question or "\n *" in question or "\n*" in question:
questions = question.replace("\n*Si", "\n*").replace("\n *", "\n*").split("\n*")
for subquestion in questions:
if len(subquestion.strip()) == 0:
continue
context = []
for i in range(level - 1, -1, -1):
parent = df.iloc[r, i]
if pd.isna(parent):
parent = find_parent_ar(df, r, i)
if pd.notna(parent):
context = [parent] + context
rows.append({
"id": strat_id + len(rows) + 1,
"question": " > ".join(context),
"answer": subquestion.strip(),
"category": category,
})
else:
context = []
for i in range(level - 1, -1, -1):
parent = df.iloc[r, i]
if pd.isna(parent):
parent = find_parent_ar(df, r, i)
if pd.notna(parent):
context = [parent] + context
rows.append({
"id": strat_id + len(rows) + 1,
"question": " > ".join(context),
"answer": question.strip(),
"category": category,
})
return pd.DataFrame(rows)
def load_excel_and_create_vectorstore_ar(excel_path: str, persist_dir: str = "./chroma_db_ar"):
"""
Charge les données depuis plusieurs feuilles Excel (version AR),
construit & stocke un Chroma VectorStore.
"""
# 1. Charger les feuilles Excel
qna_tree_ar0 = pd.read_excel(excel_path, sheet_name="Prépayé (AR)", skiprows=1).iloc[:, :5]
qna_tree_ar1 = pd.read_excel(excel_path, sheet_name="Postpayé (AR)", skiprows=1).iloc[:, :5]
qna_tree_ar2 = pd.read_excel(excel_path, sheet_name="Wifi (AR)", skiprows=1).iloc[:, :5]
# 2. Construire le contexte
context_ar0 = create_contextual_ar(qna_tree_ar0, "دفع مسبق", strat_id = 0)
context_ar1 = create_contextual_ar(qna_tree_ar1, "دفع لاحق", strat_id = len(context_ar0))
context_ar2 = create_contextual_ar(qna_tree_ar2, "واي فاي", strat_id = len(context_ar0) + len(context_ar1))
# 3. Concaténer les DataFrame
context_ar = pd.concat([context_ar0, context_ar1, context_ar2], axis=0)
# 4. Créer une colonne "context"
context_ar["context"] = context_ar.apply(
lambda row: f"{row['question']} > {row['answer']}",
axis=1
)
# 5. Convertir chaque ligne en Document
documents_ar = [
Document(
page_content=row["context"],
metadata={"id": row["id"], "category": row["category"]}
)
for _, row in context_ar.iterrows()
]
# 6. Créer & persister le vecteur
embedding_model_ar = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
vectorstore_ar = Chroma.from_documents(documents_ar, embedding_model_ar, persist_directory=persist_dir)
vectorstore_ar.persist()
return vectorstore_ar
def load_existing_vectorstore_ar(persist_dir: str = "./chroma_db_ar"):
"""
Charge un VectorStore Chroma déjà stocké (version AR).
"""
embedding_model_ar = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
vectorstore_ar = Chroma(
persist_directory=persist_dir,
embedding_function=embedding_model_ar
)
return vectorstore_ar
def retrieve_context_ar(retriever_ar, query, top_k=5):
"""
Récupère les top_k résultats pour la question (version AR).
"""
results_ar = retriever_ar.get_relevant_documents(query)
context_ar_list = []
for _, result in enumerate(results_ar[:top_k], start=1):
context_ar_list.append(result.page_content)
return context_ar_list
#########################
# 2. PROMPT & LLM (AR) #
#########################
prompt_template_ar = PromptTemplate(
input_variables=["context", "query"],
template=(
"""[SYSTEM]
أنت مساعد لخدمة عملاء INWI، محترف وخبير ومتعاون. تتقن التعامل مع استفسارات ومشاكل العملاء.
استند فقط إلى المعلومات المتوفرة في السياقات التالية دون اختراع معلومات غير موجودة:
- استخدم تحية مهذبة وودّية، على سبيل المثال: "مرحباً، أنا المساعد الذكي من إنوي. كيف يمكنني خدمتك اليوم؟"
- تعرّف على احتياج العميل واطلب التوضيح إذا لزم الأمر بالاعتماد على المعلومات المتوفرة فقط.
- إن لم يكن السؤال ضمن سياق إنوي، أخبر العميل بلطف أنك غير قادر على الإجابة خارج سياق إنوي.
- إذا لم تجد إجابة واضحة في السياق، يمكنك إبلاغ العميل بعدم توفر المعلومات واقتراح الاتصال بخدمة العملاء على الرقم 120.
- احرص على أن تكون ردودك موجزة وفعالة. وتجنّب اختلاق أي تفاصيل غير موجودة في السياق.
- أخبر العميل بأنه يمكنه التواصل معك مجدداً لمزيد من المساعدة.
- لا تتحدث عن المنافسين الذين يقدمون نفس خدمات إنوي.
- امتنع تماماً عن أي إهانة أو رد على إهانة.
- لا تطلب أي معلومات شخصية أو هوية العميل.
- وجّه العميل إلى كتالوج موقع إنوي إذا كان سؤاله يتعلق بعروض من الكتالوج.
- قدّم حلولاً قياسية للمشكلات التقنية مع عرض الخيارات المتاحة.
- قبل إرسال الجواب، تجنب أي تنسيق مثل "[Action] [نص]" واحتفظ فقط بالمعلومات المفيدة.
- لا تتحدث عن المواضيع التالية إطلاقاً: [
"السياسة", "الانتخابات", "الأحزاب", "الحكومة", "القوانين", "الإصلاحات",
"الدين", "العقائد", "الممارسات الدينية", "علم اللاهوت",
"الأخلاق", "الجدل", "الفلسفة", "المعايير", "التمييز",
"المنافسة", "مقارنة إنوي مع شركات أخرى",
"الأمن", "الاحتيال", "الصحة", "الأدوية", "التشخيص الطبي",
"التمويل", "الاستثمار", "البورصة", "العملات الرقمية", "البنوك", "التأمين",
"العنف", "الكراهية", "المحتوى الفاضح", "الجنس",
"المخالفات القانونية", "الوثائق المزورة", "البث غير الشرعي"
]
إنوي (INWI) هي شركة اتصالات مغربية تقدم خدمات الهاتف المحمول والإنترنت وحلول الاتصالات للأفراد والشركات.
تتميز بالتزامها بتوفير خدمات عالية الجودة ومبتكرة، والمساهمة في التطور الرقمي في المغرب.
العملاء هم أولويتنا، وهدفنا مساعدتهم وحل مشاكلهم.
دورك هو تقديم خدمة عملاء احترافية وفعالة بدون اختراع معلومات من خارج السياق.
[السياق]
{context}
[سؤال العميل]
{query}
[الإجابة]"""
)
)
# Configuration du LLM HuggingFace (AR)
#os.environ["HUGGINGFACEHUB_API"]
from langchain_openai import ChatOpenAI
# llm_ar = ChatOpenAI(
# model="Atlas-Chat-9B",
# base_url="https://api.friendli.ai/dedicated",
# api_key=os.environ["FRIENDLI_TOKEN"],
# )
llm_ar = HuggingFaceHub(
repo_id="MBZUAI-Paris/Atlas-Chat-2B", #"MBZUAI-Paris/Atlas-Chat-9B",
huggingfacehub_api_token=os.environ["HUGGINGFACEHUB_API"],
model_kwargs={
"temperature": 0.5,
"max_length": 500,
"timeout": 600
}
)
# Chaîne AR
llm_chain_ar = LLMChain(llm=llm_ar, prompt=prompt_template_ar)
#########################
# 3. STREAMLIT MAIN APP #
#########################
def main():
st.subheader("INWI IA Chatbot - Arabe")
# Read local image and convert to Base64
img_base64 = get_base64_of_bin_file("./img/logo inwi celeverlytics.png")
css_logo = f"""
<style>
[data-testid="stSidebarNav"]::before {{
content: "";
display: block;
margin: 0 auto 20px auto;
width: 80%;
height: 100px;
background-image: url("data:image/png;base64,{img_base64}");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}}
</style>
"""
st.markdown(css_logo, unsafe_allow_html=True)
if "retriever_ar" not in st.session_state:
st.session_state["retriever_ar"] = None
st.sidebar.subheader("Vector Store Options (AR)")
if st.sidebar.button("Créer la Vector Store (AR)"):
with st.spinner("Extraction et création de la vector store AR..."):
excel_path = "Chatbot myinwi.xlsx"
persist_directory_ar = "./chroma_db_ar"
vectorstore_ar = load_excel_and_create_vectorstore_ar(
excel_path=excel_path,
persist_dir=persist_directory_ar
)
st.session_state["retriever_ar"] = vectorstore_ar.as_retriever(
search_type="mmr",
search_kwargs={"k": 5, "lambda_mult": 0.5}
)
st.success("Vector store FR créée et chargée avec succès !")
if st.sidebar.button("Charger la Vector Store existante (AR)"):
with st.spinner("Chargement de la vector store FR existante..."):
persist_directory_ar = "./chroma_db_ar"
vectorstore_ar = load_existing_vectorstore_ar(persist_directory_ar)
st.session_state["retriever_ar"] = vectorstore_ar.as_retriever(
search_type="mmr",
search_kwargs={"k": 5, "lambda_mult": 0.5}
)
st.success("Vector store AR chargée avec succès !")
st.write("""مرحباً! أنا هنا للإجابة على جميع أسئلتك المتعلقة بخدمات إنوي
وعروض الهاتف المحمول والإنترنت، وأي حلول أخرى قد تناسب احتياجاتك (AR).""")
user_query_ar = st.chat_input("Posez votre question ici (AR)...")
if user_query_ar:
if not st.session_state["retriever_ar"]:
st.warning("Veuillez d'abord créer ou charger la Vector Store (AR).")
return
# Récupération du contexte
context_ar_list = retrieve_context_ar(st.session_state["retriever_ar"], user_query_ar, top_k=3)
if context_ar_list:
with st.spinner("Génération de la réponse..."):
response_ar = llm_chain_ar.run({"context": "\n".join(context_ar_list), "query": user_query_ar + "?"})
response_ar = response_ar.split("[الإجابة]")[-1]
st.write("**سؤال العميل:**")
st.write(user_query_ar)
st.write("**الإجابة:**")
st.write(response_ar)
else:
st.write("Aucun contexte trouvé pour cette question. Essayez autre chose.")
if __name__ == "__main__":
main()