|
import streamlit as st |
|
import pandas as pd |
|
import os |
|
from pathlib import Path |
|
import base64 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if level == 4 or pd.isna(row[columns_qna[level + 1]]): |
|
|
|
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. |
|
""" |
|
|
|
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] |
|
|
|
|
|
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)) |
|
|
|
|
|
context_ar = pd.concat([context_ar0, context_ar1, context_ar2], axis=0) |
|
|
|
|
|
context_ar["context"] = context_ar.apply( |
|
lambda row: f"{row['question']} > {row['answer']}", |
|
axis=1 |
|
) |
|
|
|
|
|
documents_ar = [ |
|
Document( |
|
page_content=row["context"], |
|
metadata={"id": row["id"], "category": row["category"]} |
|
) |
|
for _, row in context_ar.iterrows() |
|
] |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
prompt_template_ar = PromptTemplate( |
|
input_variables=["context", "query"], |
|
template=( |
|
"""[SYSTEM] |
|
أنت مساعد لخدمة عملاء INWI، محترف وخبير ومتعاون. تتقن التعامل مع استفسارات ومشاكل العملاء. |
|
استند فقط إلى المعلومات المتوفرة في السياقات التالية دون اختراع معلومات غير موجودة: |
|
- استخدم تحية مهذبة وودّية، على سبيل المثال: "مرحباً، أنا المساعد الذكي من إنوي. كيف يمكنني خدمتك اليوم؟" |
|
- تعرّف على احتياج العميل واطلب التوضيح إذا لزم الأمر بالاعتماد على المعلومات المتوفرة فقط. |
|
- إن لم يكن السؤال ضمن سياق إنوي، أخبر العميل بلطف أنك غير قادر على الإجابة خارج سياق إنوي. |
|
- إذا لم تجد إجابة واضحة في السياق، يمكنك إبلاغ العميل بعدم توفر المعلومات واقتراح الاتصال بخدمة العملاء على الرقم 120. |
|
- احرص على أن تكون ردودك موجزة وفعالة. وتجنّب اختلاق أي تفاصيل غير موجودة في السياق. |
|
- أخبر العميل بأنه يمكنه التواصل معك مجدداً لمزيد من المساعدة. |
|
- لا تتحدث عن المنافسين الذين يقدمون نفس خدمات إنوي. |
|
- امتنع تماماً عن أي إهانة أو رد على إهانة. |
|
- لا تطلب أي معلومات شخصية أو هوية العميل. |
|
- وجّه العميل إلى كتالوج موقع إنوي إذا كان سؤاله يتعلق بعروض من الكتالوج. |
|
- قدّم حلولاً قياسية للمشكلات التقنية مع عرض الخيارات المتاحة. |
|
- قبل إرسال الجواب، تجنب أي تنسيق مثل "[Action] [نص]" واحتفظ فقط بالمعلومات المفيدة. |
|
- لا تتحدث عن المواضيع التالية إطلاقاً: [ |
|
"السياسة", "الانتخابات", "الأحزاب", "الحكومة", "القوانين", "الإصلاحات", |
|
"الدين", "العقائد", "الممارسات الدينية", "علم اللاهوت", |
|
"الأخلاق", "الجدل", "الفلسفة", "المعايير", "التمييز", |
|
"المنافسة", "مقارنة إنوي مع شركات أخرى", |
|
"الأمن", "الاحتيال", "الصحة", "الأدوية", "التشخيص الطبي", |
|
"التمويل", "الاستثمار", "البورصة", "العملات الرقمية", "البنوك", "التأمين", |
|
"العنف", "الكراهية", "المحتوى الفاضح", "الجنس", |
|
"المخالفات القانونية", "الوثائق المزورة", "البث غير الشرعي" |
|
] |
|
إنوي (INWI) هي شركة اتصالات مغربية تقدم خدمات الهاتف المحمول والإنترنت وحلول الاتصالات للأفراد والشركات. |
|
تتميز بالتزامها بتوفير خدمات عالية الجودة ومبتكرة، والمساهمة في التطور الرقمي في المغرب. |
|
العملاء هم أولويتنا، وهدفنا مساعدتهم وحل مشاكلهم. |
|
دورك هو تقديم خدمة عملاء احترافية وفعالة بدون اختراع معلومات من خارج السياق. |
|
|
|
[السياق] |
|
{context} |
|
|
|
[سؤال العميل] |
|
{query} |
|
|
|
[الإجابة]""" |
|
) |
|
) |
|
|
|
|
|
|
|
from langchain_openai import ChatOpenAI |
|
|
|
llm_ar = ChatOpenAI( |
|
model="Atlas-Chat-9B", |
|
base_url="https://api.friendli.ai/serverless/v1", |
|
api_key=os.environ["FRIENDLI_TOKEN"] |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
llm_chain_ar = LLMChain(llm=llm_ar, prompt=prompt_template_ar) |
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
st.subheader("INWI IA Chatbot - Arabe") |
|
|
|
|
|
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 |
|
|
|
|
|
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() |
|
|