Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- .gitattributes +1 -0
- Accueil.py +42 -0
- Chatbot myinwi.xlsx +3 -0
- README.md +1 -12
- img/logo inwi celeverlytics.png +0 -0
- pages/1_Chatbot_FR.py +304 -0
- pages/2_Chatbot_AR.py +296 -0
- requirements.txt +11 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
Chatbot[[:space:]]myinwi.xlsx filter=lfs diff=lfs merge=lfs -text
|
Accueil.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import base64
|
2 |
+
import streamlit as st
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
def get_base64_of_bin_file(bin_file_path: str) -> str:
|
6 |
+
file_bytes = Path(bin_file_path).read_bytes()
|
7 |
+
return base64.b64encode(file_bytes).decode()
|
8 |
+
|
9 |
+
def main():
|
10 |
+
st.set_page_config(page_title="INWI Chatbot - Accueil", layout="wide")
|
11 |
+
|
12 |
+
# Read local image and convert to Base64
|
13 |
+
img_base64 = get_base64_of_bin_file("./img/logo inwi celeverlytics.png")
|
14 |
+
css_logo = f"""
|
15 |
+
<style>
|
16 |
+
[data-testid="stSidebarNav"]::before {{
|
17 |
+
content: "";
|
18 |
+
display: block;
|
19 |
+
margin: 0 auto 20px auto;
|
20 |
+
width: 80%;
|
21 |
+
height: 100px;
|
22 |
+
background-image: url("data:image/png;base64,{img_base64}");
|
23 |
+
background-size: contain;
|
24 |
+
background-repeat: no-repeat;
|
25 |
+
background-position: center;
|
26 |
+
}}
|
27 |
+
</style>
|
28 |
+
"""
|
29 |
+
|
30 |
+
st.markdown(css_logo, unsafe_allow_html=True)
|
31 |
+
|
32 |
+
st.title("👋 Bienvenue sur le Chatbot INWI")
|
33 |
+
st.markdown(
|
34 |
+
"""
|
35 |
+
Ceci est la page principale.
|
36 |
+
Vous pouvez choisir le **Chatbot en Français** ou le **Chatbot en Arabe** en naviguant dans le menu de gauche (sous "Pages" ou "Select a page").
|
37 |
+
"""
|
38 |
+
)
|
39 |
+
st.write("Veuillez sélectionner la langue désirée dans la barre latérale.")
|
40 |
+
|
41 |
+
if __name__ == "__main__":
|
42 |
+
main()
|
Chatbot myinwi.xlsx
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:090d7da624e8a495525ff7f76b7ada897e30f91cbe3a8871f42532ef5e45a323
|
3 |
+
size 435917
|
README.md
CHANGED
@@ -1,12 +1 @@
|
|
1 |
-
|
2 |
-
title: Test Inwi Cleverlytics V0
|
3 |
-
emoji: 🚀
|
4 |
-
colorFrom: green
|
5 |
-
colorTo: blue
|
6 |
-
sdk: streamlit
|
7 |
-
sdk_version: 1.41.1
|
8 |
-
app_file: app.py
|
9 |
-
pinned: false
|
10 |
-
---
|
11 |
-
|
12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
+
Test du prompte chatbot inwi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
img/logo inwi celeverlytics.png
ADDED
![]() |
pages/1_Chatbot_FR.py
ADDED
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import os
|
4 |
+
from pathlib import Path
|
5 |
+
import base64
|
6 |
+
|
7 |
+
# LangChain & Hugging Face
|
8 |
+
from langchain.embeddings import HuggingFaceEmbeddings
|
9 |
+
from langchain.vectorstores import Chroma
|
10 |
+
from langchain.schema import Document
|
11 |
+
from langchain.prompts import PromptTemplate
|
12 |
+
from langchain.llms import HuggingFaceHub
|
13 |
+
from langchain.chains import LLMChain
|
14 |
+
|
15 |
+
import pysqlite3
|
16 |
+
import sys
|
17 |
+
sys.modules["sqlite3"] = pysqlite3
|
18 |
+
|
19 |
+
#####################
|
20 |
+
# 1. HELPER FUNCTIONS
|
21 |
+
#####################
|
22 |
+
|
23 |
+
def get_base64_of_bin_file(bin_file_path: str) -> str:
|
24 |
+
file_bytes = Path(bin_file_path).read_bytes()
|
25 |
+
return base64.b64encode(file_bytes).decode()
|
26 |
+
|
27 |
+
def find_parent_fr(data, r, col):
|
28 |
+
"""
|
29 |
+
Trouve la question parente pour une ligne et colonne donnée dans le DataFrame (version FR).
|
30 |
+
"""
|
31 |
+
i = r - 1
|
32 |
+
parent = None
|
33 |
+
while i >= 0 and pd.isna(parent):
|
34 |
+
parent = data.iloc[i, col]
|
35 |
+
i -= 1
|
36 |
+
return parent
|
37 |
+
|
38 |
+
def create_contextual_fr(df, category, strat_id=0):
|
39 |
+
"""
|
40 |
+
Crée un DataFrame avec questions-réponses contextuelles (version FR).
|
41 |
+
"""
|
42 |
+
rows = []
|
43 |
+
columns_qna = list(df.columns)
|
44 |
+
|
45 |
+
for r, row in df.iterrows():
|
46 |
+
for level, col in enumerate(df.columns):
|
47 |
+
question = row[col]
|
48 |
+
if pd.isna(question):
|
49 |
+
continue
|
50 |
+
|
51 |
+
# Si la question est un "leaf node"
|
52 |
+
if level == 4 or pd.isna(row[columns_qna[level + 1]]):
|
53 |
+
# Gérer des sous-questions multiples
|
54 |
+
if "\n*Si" in question or "\n *" in question or "\n*" in question:
|
55 |
+
questions = question.replace("\n*Si", "\n*").replace("\n *", "\n*").split("\n*")
|
56 |
+
for subquestion in questions:
|
57 |
+
if len(subquestion.strip()) == 0:
|
58 |
+
continue
|
59 |
+
|
60 |
+
context = []
|
61 |
+
for i in range(level - 1, -1, -1):
|
62 |
+
parent = df.iloc[r, i]
|
63 |
+
if pd.isna(parent):
|
64 |
+
parent = find_parent_fr(df, r, i)
|
65 |
+
if pd.notna(parent):
|
66 |
+
context = [parent] + context
|
67 |
+
|
68 |
+
rows.append({
|
69 |
+
"id": strat_id + len(rows) + 1,
|
70 |
+
"question": " > ".join(context),
|
71 |
+
"answer": subquestion.strip(),
|
72 |
+
"category": category,
|
73 |
+
})
|
74 |
+
else:
|
75 |
+
context = []
|
76 |
+
for i in range(level - 1, -1, -1):
|
77 |
+
parent = df.iloc[r, i]
|
78 |
+
if pd.isna(parent):
|
79 |
+
parent = find_parent_fr(df, r, i)
|
80 |
+
if pd.notna(parent):
|
81 |
+
context = [parent] + context
|
82 |
+
|
83 |
+
rows.append({
|
84 |
+
"id": strat_id + len(rows) + 1,
|
85 |
+
"question": " > ".join(context),
|
86 |
+
"answer": question.strip(),
|
87 |
+
"category": category,
|
88 |
+
})
|
89 |
+
|
90 |
+
return pd.DataFrame(rows)
|
91 |
+
|
92 |
+
def load_excel_and_create_vectorstore_fr(excel_path: str, persist_dir: str = "./chroma_db_fr"):
|
93 |
+
"""
|
94 |
+
Charge les données depuis plusieurs feuilles Excel (version FR),
|
95 |
+
construit & stocke un Chroma VectorStore.
|
96 |
+
"""
|
97 |
+
# 1. Charger les feuilles Excel
|
98 |
+
qna_tree_fr0 = pd.read_excel(excel_path, sheet_name="Prépayé (FR)", skiprows=1).iloc[:, :5]
|
99 |
+
qna_tree_fr1 = pd.read_excel(excel_path, sheet_name="Postpayé (FR)", skiprows=1).iloc[:, :5]
|
100 |
+
qna_tree_fr2 = pd.read_excel(excel_path, sheet_name="Wifi (FR)", skiprows=1).iloc[:, :5]
|
101 |
+
|
102 |
+
# 2. Construire le contexte
|
103 |
+
context_fr0 = create_contextual_fr(qna_tree_fr0, "Prépayé", strat_id = 0)
|
104 |
+
context_fr1 = create_contextual_fr(qna_tree_fr1, "Postpayé", strat_id = len(context_fr0))
|
105 |
+
context_fr2 = create_contextual_fr(qna_tree_fr2, "Wifi", strat_id = len(context_fr0) + len(context_fr1))
|
106 |
+
|
107 |
+
# 3. Concaténer les DataFrame
|
108 |
+
context_fr = pd.concat([context_fr0, context_fr1, context_fr2], axis=0)
|
109 |
+
|
110 |
+
# 4. Créer une colonne "context"
|
111 |
+
context_fr["context"] = context_fr.apply(
|
112 |
+
lambda row: f"{row['question']} > {row['answer']}",
|
113 |
+
axis=1
|
114 |
+
)
|
115 |
+
|
116 |
+
# 5. Convertir chaque ligne en Document
|
117 |
+
documents_fr = [
|
118 |
+
Document(
|
119 |
+
page_content=row["context"],
|
120 |
+
metadata={"id": row["id"], "category": row["category"]}
|
121 |
+
)
|
122 |
+
for _, row in context_fr.iterrows()
|
123 |
+
]
|
124 |
+
|
125 |
+
# 6. Créer & persister le vecteur
|
126 |
+
embedding_model_fr = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
127 |
+
vectorstore_fr = Chroma.from_documents(documents_fr, embedding_model_fr, persist_directory=persist_dir)
|
128 |
+
vectorstore_fr.persist()
|
129 |
+
|
130 |
+
return vectorstore_fr
|
131 |
+
|
132 |
+
def load_existing_vectorstore_fr(persist_dir: str = "./chroma_db_fr"):
|
133 |
+
"""
|
134 |
+
Charge un VectorStore Chroma déjà stocké (version FR).
|
135 |
+
"""
|
136 |
+
embedding_model_fr = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
137 |
+
vectorstore_fr = Chroma(
|
138 |
+
persist_directory=persist_dir,
|
139 |
+
embedding_function=embedding_model_fr
|
140 |
+
)
|
141 |
+
return vectorstore_fr
|
142 |
+
|
143 |
+
def retrieve_context_fr(retriever_fr, query, top_k=5):
|
144 |
+
"""
|
145 |
+
Récupère les top_k résultats pour la question (version FR).
|
146 |
+
"""
|
147 |
+
results_fr = retriever_fr.get_relevant_documents(query)
|
148 |
+
context_fr_list = []
|
149 |
+
for _, result in enumerate(results_fr[:top_k], start=1):
|
150 |
+
context_fr_list.append(result.page_content)
|
151 |
+
return context_fr_list
|
152 |
+
|
153 |
+
|
154 |
+
#########################
|
155 |
+
# 2. PROMPT & LLM FR #
|
156 |
+
#########################
|
157 |
+
|
158 |
+
prompt_template_fr = PromptTemplate(
|
159 |
+
input_variables=["context", "query"],
|
160 |
+
template=(
|
161 |
+
"""[SYSTEM]
|
162 |
+
Vous êtes un assistant client professionnel, expérimenté et bienveillant pour l'opérateur téléphonique INWI.
|
163 |
+
Vous excellez dans la gestion des clients, en répondant à leurs problèmes et questions.
|
164 |
+
Fournir un service client et des conseils en se basant sur les contextes fournis :
|
165 |
+
- Répondre aux salutations de manière courtoise et amicale, par exemple : "Je suis l'assistant IA d'INWI'. Comment puis-je vous aider aujourd'hui ?"
|
166 |
+
- Identifier le besoin du client et demander des clarifications si nécessaire, tout en s'appuyant uniquement sur le contexte.
|
167 |
+
- Si la question n'est pas liée au contexte d'INWI, veuillez informer poliment que vous ne pouvez pas répondre à des questions hors contexte INWI.
|
168 |
+
- Si la réponse ne figure pas dans le contexte, vous pouvez dire "Je n'ai pas assez d'information" et proposer d'appeler le service client au 120.
|
169 |
+
- Structurer les réponses de manière concise et efficace. Et n'inventez pas d'infos non présentes dans le contexte.
|
170 |
+
- Informer le client qu’il peut vous recontacter pour toute assistance supplémentaire.
|
171 |
+
- Ne parlez pas des concurrents qui offrent la meme service d'INWI.
|
172 |
+
- Ne jamais insulter ou répondre à une insulte.
|
173 |
+
- Ne demandez pas d’informations personnelles ou d’identification du client.
|
174 |
+
- Orientez vers le catalogue sur le site web INWI si la question concerne une offre du catalogue.
|
175 |
+
- Donnez des solutions standard pour les problèmes techniques avec des options.
|
176 |
+
- Avant de générer votre réponse, éliminez toutes les structures comme '[Action] [texte]' et gardez uniquement les informations utiles.
|
177 |
+
- Ne jamais parler des sujets suivants : [
|
178 |
+
"politique", "élections", "partis", "gouvernement", "lois", "réformes",
|
179 |
+
"religion", "croyances", "pratiques religieuses", "théologie",
|
180 |
+
"moralité", "débat", "philosophie", "éthique", "discrimination",
|
181 |
+
"concurrence", "Maroc Telecom", "IAM", "Orange", "comparaison",
|
182 |
+
"sécurité", "fraude", "santé", "médicaments", "traitement", "diagnostic", "maladie",
|
183 |
+
"finance", "investissement", "bourse", "crypto", "banque", "assurance",
|
184 |
+
"violence", "haine", "contenu explicite", "sexe", "adultes",
|
185 |
+
"illégal", "faux documents", "streaming illégal"
|
186 |
+
]
|
187 |
+
INWI est un opérateur de télécommunications marocain offrant des services mobiles, Internet et solutions de télécommunications
|
188 |
+
pour les particuliers et les entreprises. Il se distingue par son engagement à fournir des services de qualité, innovants et
|
189 |
+
accessibles, tout en contribuant au développement numérique du pays.
|
190 |
+
Les clients sont notre priorité, et notre but est de résoudre leurs problèmes.
|
191 |
+
Votre rôle est de fournir un service client professionnel et efficace sans inventer d'informations.
|
192 |
+
|
193 |
+
[CONTEXTE]
|
194 |
+
{context}
|
195 |
+
|
196 |
+
[QUESTION DU CLIENT]
|
197 |
+
{query}
|
198 |
+
|
199 |
+
[RÉPONSE]"""
|
200 |
+
)
|
201 |
+
)
|
202 |
+
|
203 |
+
# Configuration du LLM HuggingFace (FR)
|
204 |
+
os.environ["HUGGINGFACEHUB_API"]
|
205 |
+
llm_fr = HuggingFaceHub(
|
206 |
+
repo_id="mistralai/Mistral-7B-Instruct-v0.3",
|
207 |
+
model_kwargs={
|
208 |
+
"temperature": 0.5,
|
209 |
+
"max_length": 500
|
210 |
+
}
|
211 |
+
)
|
212 |
+
|
213 |
+
# Chaîne FR
|
214 |
+
llm_chain_fr = LLMChain(llm=llm_fr, prompt=prompt_template_fr)
|
215 |
+
|
216 |
+
|
217 |
+
#########################
|
218 |
+
# 3. STREAMLIT MAIN APP #
|
219 |
+
#########################
|
220 |
+
|
221 |
+
def main():
|
222 |
+
st.subheader("INWI IA Chatbot - Français")
|
223 |
+
|
224 |
+
# Read local image and convert to Base64
|
225 |
+
img_base64 = get_base64_of_bin_file("./img/logo inwi celeverlytics.png")
|
226 |
+
css_logo = f"""
|
227 |
+
<style>
|
228 |
+
[data-testid="stSidebarNav"]::before {{
|
229 |
+
content: "";
|
230 |
+
display: block;
|
231 |
+
margin: 0 auto 20px auto;
|
232 |
+
width: 80%;
|
233 |
+
height: 100px;
|
234 |
+
background-image: url("data:image/png;base64,{img_base64}");
|
235 |
+
background-size: contain;
|
236 |
+
background-repeat: no-repeat;
|
237 |
+
background-position: center;
|
238 |
+
}}
|
239 |
+
</style>
|
240 |
+
"""
|
241 |
+
|
242 |
+
st.markdown(css_logo, unsafe_allow_html=True)
|
243 |
+
|
244 |
+
# Charger ou créer le retriever
|
245 |
+
if "retriever_fr" not in st.session_state:
|
246 |
+
st.session_state["retriever_fr"] = None
|
247 |
+
|
248 |
+
st.sidebar.header("Vector Store Options (FR)")
|
249 |
+
|
250 |
+
if st.sidebar.button("Créer la Vector Store (FR)"):
|
251 |
+
with st.spinner("Extraction et création de la vector store FR..."):
|
252 |
+
excel_path = "Chatbot myinwi.xlsx"
|
253 |
+
persist_directory_fr = "./chroma_db_fr"
|
254 |
+
vectorstore_fr = load_excel_and_create_vectorstore_fr(
|
255 |
+
excel_path=excel_path,
|
256 |
+
persist_dir=persist_directory_fr
|
257 |
+
)
|
258 |
+
st.session_state["retriever_fr"] = vectorstore_fr.as_retriever(
|
259 |
+
search_type="mmr",
|
260 |
+
search_kwargs={"k": 5, "lambda_mult": 0.5}
|
261 |
+
)
|
262 |
+
st.success("Vector store FR créée et chargée avec succès !")
|
263 |
+
|
264 |
+
if st.sidebar.button("Charger la Vector Store existante (FR)"):
|
265 |
+
with st.spinner("Chargement de la vector store FR existante..."):
|
266 |
+
persist_directory_fr = "./chroma_db_fr"
|
267 |
+
vectorstore_fr = load_existing_vectorstore_fr(persist_directory_fr)
|
268 |
+
st.session_state["retriever_fr"] = vectorstore_fr.as_retriever(
|
269 |
+
search_type="mmr",
|
270 |
+
search_kwargs={"k": 5, "lambda_mult": 0.5}
|
271 |
+
)
|
272 |
+
st.success("Vector store FR chargée avec succès !")
|
273 |
+
|
274 |
+
st.write("""Je suis là pour répondre à toutes vos questions concernant nos
|
275 |
+
services, nos offres mobiles et Internet, ainsi que nos solutions adaptées à vos besoins (FR).""")
|
276 |
+
|
277 |
+
# Zone de texte
|
278 |
+
user_query_fr = st.chat_input("Posez votre question ici (FR)...")
|
279 |
+
|
280 |
+
if user_query_fr:
|
281 |
+
if not st.session_state["retriever_fr"]:
|
282 |
+
st.warning("Veuillez d'abord créer ou charger la Vector Store (FR).")
|
283 |
+
return
|
284 |
+
|
285 |
+
# Récupération du contexte
|
286 |
+
context_fr_list = retrieve_context_fr(st.session_state["retriever_fr"], user_query_fr, top_k=5)
|
287 |
+
|
288 |
+
if context_fr_list:
|
289 |
+
with st.spinner("Génération de la réponse..."):
|
290 |
+
response_fr = llm_chain_fr.run({"context": "\n".join(context_fr_list), "query": user_query_fr})
|
291 |
+
# Séparer si jamais le prompt contient [RÉPONSE], sinon on affiche tout
|
292 |
+
response_fr = response_fr.split("[RÉPONSE]")[-1]
|
293 |
+
st.write("**Question :**")
|
294 |
+
st.write(user_query_fr)
|
295 |
+
st.write("**Réponse :**")
|
296 |
+
st.write(response_fr)
|
297 |
+
else:
|
298 |
+
st.write("Aucun contexte trouvé pour cette question. Essayez autre chose.")
|
299 |
+
|
300 |
+
|
301 |
+
if __name__ == "__main__":
|
302 |
+
main()
|
303 |
+
|
304 |
+
|
pages/2_Chatbot_AR.py
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import os
|
4 |
+
from pathlib import Path
|
5 |
+
import base64
|
6 |
+
|
7 |
+
# LangChain & Hugging Face
|
8 |
+
from langchain.embeddings import HuggingFaceEmbeddings
|
9 |
+
from langchain.vectorstores import Chroma
|
10 |
+
from langchain.schema import Document
|
11 |
+
from langchain.prompts import PromptTemplate
|
12 |
+
from langchain.llms import HuggingFaceHub
|
13 |
+
from langchain.chains import LLMChain
|
14 |
+
|
15 |
+
import pysqlite3
|
16 |
+
import sys
|
17 |
+
sys.modules["sqlite3"] = pysqlite3
|
18 |
+
|
19 |
+
#####################
|
20 |
+
# 1. HELPER FUNCTIONS
|
21 |
+
#####################
|
22 |
+
|
23 |
+
def get_base64_of_bin_file(bin_file_path: str) -> str:
|
24 |
+
file_bytes = Path(bin_file_path).read_bytes()
|
25 |
+
return base64.b64encode(file_bytes).decode()
|
26 |
+
|
27 |
+
def find_parent_ar(data, r, col):
|
28 |
+
"""
|
29 |
+
Trouve la question parente pour une ligne et colonne donnée dans le DataFrame (version AR).
|
30 |
+
"""
|
31 |
+
i = r - 1
|
32 |
+
parent = None
|
33 |
+
while i >= 0 and pd.isna(parent):
|
34 |
+
parent = data.iloc[i, col]
|
35 |
+
i -= 1
|
36 |
+
return parent
|
37 |
+
|
38 |
+
def create_contextual_ar(df, category, strat_id=0):
|
39 |
+
"""
|
40 |
+
Crée un DataFrame avec questions-réponses contextuelles (version AR).
|
41 |
+
"""
|
42 |
+
rows = []
|
43 |
+
columns_qna = list(df.columns)
|
44 |
+
|
45 |
+
for r, row in df.iterrows():
|
46 |
+
for level, col in enumerate(df.columns):
|
47 |
+
question = row[col]
|
48 |
+
if pd.isna(question):
|
49 |
+
continue
|
50 |
+
|
51 |
+
# Si la question est un "leaf node"
|
52 |
+
if level == 4 or pd.isna(row[columns_qna[level + 1]]):
|
53 |
+
# Gérer des sous-questions multiples
|
54 |
+
if "\n*Si" in question or "\n *" in question or "\n*" in question:
|
55 |
+
questions = question.replace("\n*Si", "\n*").replace("\n *", "\n*").split("\n*")
|
56 |
+
for subquestion in questions:
|
57 |
+
if len(subquestion.strip()) == 0:
|
58 |
+
continue
|
59 |
+
|
60 |
+
context = []
|
61 |
+
for i in range(level - 1, -1, -1):
|
62 |
+
parent = df.iloc[r, i]
|
63 |
+
if pd.isna(parent):
|
64 |
+
parent = find_parent_ar(df, r, i)
|
65 |
+
if pd.notna(parent):
|
66 |
+
context = [parent] + context
|
67 |
+
|
68 |
+
rows.append({
|
69 |
+
"id": strat_id + len(rows) + 1,
|
70 |
+
"question": " > ".join(context),
|
71 |
+
"answer": subquestion.strip(),
|
72 |
+
"category": category,
|
73 |
+
})
|
74 |
+
else:
|
75 |
+
context = []
|
76 |
+
for i in range(level - 1, -1, -1):
|
77 |
+
parent = df.iloc[r, i]
|
78 |
+
if pd.isna(parent):
|
79 |
+
parent = find_parent_ar(df, r, i)
|
80 |
+
if pd.notna(parent):
|
81 |
+
context = [parent] + context
|
82 |
+
|
83 |
+
rows.append({
|
84 |
+
"id": strat_id + len(rows) + 1,
|
85 |
+
"question": " > ".join(context),
|
86 |
+
"answer": question.strip(),
|
87 |
+
"category": category,
|
88 |
+
})
|
89 |
+
|
90 |
+
return pd.DataFrame(rows)
|
91 |
+
|
92 |
+
def load_excel_and_create_vectorstore_ar(excel_path: str, persist_dir: str = "./chroma_db_ar"):
|
93 |
+
"""
|
94 |
+
Charge les données depuis plusieurs feuilles Excel (version AR),
|
95 |
+
construit & stocke un Chroma VectorStore.
|
96 |
+
"""
|
97 |
+
# 1. Charger les feuilles Excel
|
98 |
+
qna_tree_ar0 = pd.read_excel(excel_path, sheet_name="Prépayé (AR)", skiprows=1).iloc[:, :5]
|
99 |
+
qna_tree_ar1 = pd.read_excel(excel_path, sheet_name="Postpayé (AR)", skiprows=1).iloc[:, :5]
|
100 |
+
qna_tree_ar2 = pd.read_excel(excel_path, sheet_name="Wifi (AR)", skiprows=1).iloc[:, :5]
|
101 |
+
|
102 |
+
# 2. Construire le contexte
|
103 |
+
context_ar0 = create_contextual_ar(qna_tree_ar0, "دفع مسبق", strat_id = 0)
|
104 |
+
context_ar1 = create_contextual_ar(qna_tree_ar1, "دفع لاحق", strat_id = len(context_ar0))
|
105 |
+
context_ar2 = create_contextual_ar(qna_tree_ar2, "واي فاي", strat_id = len(context_ar0) + len(context_ar1))
|
106 |
+
|
107 |
+
# 3. Concaténer les DataFrame
|
108 |
+
context_ar = pd.concat([context_ar0, context_ar1, context_ar2], axis=0)
|
109 |
+
|
110 |
+
# 4. Créer une colonne "context"
|
111 |
+
context_ar["context"] = context_ar.apply(
|
112 |
+
lambda row: f"{row['question']} > {row['answer']}",
|
113 |
+
axis=1
|
114 |
+
)
|
115 |
+
|
116 |
+
# 5. Convertir chaque ligne en Document
|
117 |
+
documents_ar = [
|
118 |
+
Document(
|
119 |
+
page_content=row["context"],
|
120 |
+
metadata={"id": row["id"], "category": row["category"]}
|
121 |
+
)
|
122 |
+
for _, row in context_ar.iterrows()
|
123 |
+
]
|
124 |
+
|
125 |
+
# 6. Créer & persister le vecteur
|
126 |
+
embedding_model_ar = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
127 |
+
vectorstore_ar = Chroma.from_documents(documents_ar, embedding_model_ar, persist_directory=persist_dir)
|
128 |
+
vectorstore_ar.persist()
|
129 |
+
|
130 |
+
return vectorstore_ar
|
131 |
+
|
132 |
+
def load_existing_vectorstore_ar(persist_dir: str = "./chroma_db_ar"):
|
133 |
+
"""
|
134 |
+
Charge un VectorStore Chroma déjà stocké (version AR).
|
135 |
+
"""
|
136 |
+
embedding_model_ar = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
137 |
+
vectorstore_ar = Chroma(
|
138 |
+
persist_directory=persist_dir,
|
139 |
+
embedding_function=embedding_model_ar
|
140 |
+
)
|
141 |
+
return vectorstore_ar
|
142 |
+
|
143 |
+
def retrieve_context_ar(retriever_ar, query, top_k=5):
|
144 |
+
"""
|
145 |
+
Récupère les top_k résultats pour la question (version AR).
|
146 |
+
"""
|
147 |
+
results_ar = retriever_ar.get_relevant_documents(query)
|
148 |
+
context_ar_list = []
|
149 |
+
for _, result in enumerate(results_ar[:top_k], start=1):
|
150 |
+
context_ar_list.append(result.page_content)
|
151 |
+
return context_ar_list
|
152 |
+
|
153 |
+
|
154 |
+
#########################
|
155 |
+
# 2. PROMPT & LLM (AR) #
|
156 |
+
#########################
|
157 |
+
|
158 |
+
prompt_template_ar = PromptTemplate(
|
159 |
+
input_variables=["context", "query"],
|
160 |
+
template=(
|
161 |
+
"""[SYSTEM]
|
162 |
+
أنت مساعد لخدمة عملاء INWI، محترف وخبير ومتعاون. تتقن التعامل مع استفسارات ومشاكل العملاء.
|
163 |
+
استند فقط إلى المعلومات المتوفرة في السياقات التالية دون اختراع معلومات غير موجودة:
|
164 |
+
- استخدم تحية مهذبة وودّية، على سبيل المثال: "مرحباً، أنا المساعد الذكي من إنوي. كيف يمكنني خدمتك اليوم؟"
|
165 |
+
- تعرّف على احتياج العميل واطلب التوضيح إذا لزم الأمر بالاعتماد على المعلومات المتوفرة فقط.
|
166 |
+
- إن لم يكن السؤال ضمن سياق إنوي، أخبر العميل بلطف أنك غير قادر على الإجابة خارج سياق إنوي.
|
167 |
+
- إذا لم تجد إجابة واضحة في السياق، يمكنك إبلاغ العميل بعدم توفر المعلومات واقتراح الاتصال بخدمة العملاء على الرقم 120.
|
168 |
+
- احرص على أن تكون ردودك موجزة وفعالة. وتجنّب اختلاق أي تفاصيل غير موجودة في السياق.
|
169 |
+
- أخبر العميل بأنه يمكنه التواصل معك مجدداً لمزيد من المساعدة.
|
170 |
+
- لا تتحدث عن المنافسين الذين يقدمون نفس خدمات إنوي.
|
171 |
+
- امتنع تماماً عن أي إهانة أو رد على إهانة.
|
172 |
+
- لا تطلب أي معلومات شخصية أو هوية العميل.
|
173 |
+
- وجّه العميل إلى كتالوج موقع إنوي إذا كان سؤاله يتعلق بعروض من الكتالوج.
|
174 |
+
- قدّم حلولاً قياسية للمشكلات التقنية مع عرض الخيارات المتاحة.
|
175 |
+
- قبل إرسال الجواب، تجنب أي تنسيق مثل "[Action] [نص]" واحتفظ فقط بالمعلومات المفيدة.
|
176 |
+
- لا تتحدث عن المواضيع التالية إطلاقاً: [
|
177 |
+
"السياسة", "الانتخابات", "الأحزاب", "الحكومة", "القوانين", "الإصلاحات",
|
178 |
+
"الدين", "العقائد", "الممارسات الدينية", "علم اللاهوت",
|
179 |
+
"الأخلاق", "الجدل", "الفلسفة", "المعايير", "التمييز",
|
180 |
+
"المنافسة", "مقارنة إنوي مع شركات أخرى",
|
181 |
+
"الأمن", "الاحتيال", "الصحة", "الأدوية", "التشخيص الطبي",
|
182 |
+
"التمويل", "الاستثمار", "البورصة", "العملات الرقمية", "البنوك", "التأمين",
|
183 |
+
"العنف", "الكراهية", "المحتوى الفاضح", "الجنس",
|
184 |
+
"المخالفات القانونية", "الوثائق المزورة", "البث غير الشرعي"
|
185 |
+
]
|
186 |
+
إنوي (INWI) هي شركة اتصالات مغربية تقدم خدمات الهاتف المحمول والإنترنت وحلول الاتصالات للأفراد والشركات.
|
187 |
+
تتميز بالتزامها بتوفير خدمات عالية الجودة ومبتكرة، والمساهمة في التطور الرقمي في المغرب.
|
188 |
+
العملاء هم أولويتنا، وهدفنا مساعدتهم وحل مشاكلهم.
|
189 |
+
دورك هو تقديم خدمة عملاء احترافية وفعالة بدون اختراع معلومات من خارج السياق.
|
190 |
+
|
191 |
+
[السياق]
|
192 |
+
{context}
|
193 |
+
|
194 |
+
[سؤال العميل]
|
195 |
+
{query}
|
196 |
+
|
197 |
+
[الإجابة]"""
|
198 |
+
)
|
199 |
+
)
|
200 |
+
|
201 |
+
# Configuration du LLM HuggingFace (AR)
|
202 |
+
os.environ["HUGGINGFACEHUB_API_TOKEN"]
|
203 |
+
llm_ar = HuggingFaceHub(
|
204 |
+
repo_id="MBZUAI-Paris/Atlas-Chat-9B",
|
205 |
+
model_kwargs={
|
206 |
+
"temperature": 0.5,
|
207 |
+
"max_length": 500
|
208 |
+
}
|
209 |
+
)
|
210 |
+
|
211 |
+
# Chaîne AR
|
212 |
+
llm_chain_ar = LLMChain(llm=llm_ar, prompt=prompt_template_ar)
|
213 |
+
|
214 |
+
|
215 |
+
#########################
|
216 |
+
# 3. STREAMLIT MAIN APP #
|
217 |
+
#########################
|
218 |
+
|
219 |
+
def main():
|
220 |
+
st.subheader("INWI IA Chatbot - Arabe")
|
221 |
+
|
222 |
+
# Read local image and convert to Base64
|
223 |
+
img_base64 = get_base64_of_bin_file("./img/logo inwi celeverlytics.png")
|
224 |
+
css_logo = f"""
|
225 |
+
<style>
|
226 |
+
[data-testid="stSidebarNav"]::before {{
|
227 |
+
content: "";
|
228 |
+
display: block;
|
229 |
+
margin: 0 auto 20px auto;
|
230 |
+
width: 80%;
|
231 |
+
height: 100px;
|
232 |
+
background-image: url("data:image/png;base64,{img_base64}");
|
233 |
+
background-size: contain;
|
234 |
+
background-repeat: no-repeat;
|
235 |
+
background-position: center;
|
236 |
+
}}
|
237 |
+
</style>
|
238 |
+
"""
|
239 |
+
|
240 |
+
st.markdown(css_logo, unsafe_allow_html=True)
|
241 |
+
|
242 |
+
if "retriever_ar" not in st.session_state:
|
243 |
+
st.session_state["retriever_ar"] = None
|
244 |
+
|
245 |
+
st.sidebar.subheader("Vector Store Options (AR)")
|
246 |
+
|
247 |
+
if st.sidebar.button("Créer la Vector Store (AR)"):
|
248 |
+
with st.spinner("Extraction et création de la vector store AR..."):
|
249 |
+
excel_path = "Chatbot myinwi.xlsx"
|
250 |
+
persist_directory_ar = "./chroma_db_ar"
|
251 |
+
vectorstore_ar = load_excel_and_create_vectorstore_ar(
|
252 |
+
excel_path=excel_path,
|
253 |
+
persist_dir=persist_directory_ar
|
254 |
+
)
|
255 |
+
st.session_state["retriever_ar"] = vectorstore_ar.as_retriever(
|
256 |
+
search_type="mmr",
|
257 |
+
search_kwargs={"k": 5, "lambda_mult": 0.5}
|
258 |
+
)
|
259 |
+
st.success("Vector store FR créée et chargée avec succès !")
|
260 |
+
|
261 |
+
if st.sidebar.button("Charger la Vector Store existante (AR)"):
|
262 |
+
with st.spinner("Chargement de la vector store FR existante..."):
|
263 |
+
persist_directory_ar = "./chroma_db_ar"
|
264 |
+
vectorstore_ar = load_existing_vectorstore_ar(persist_directory_ar)
|
265 |
+
st.session_state["retriever_ar"] = vectorstore_ar.as_retriever(
|
266 |
+
search_type="mmr",
|
267 |
+
search_kwargs={"k": 5, "lambda_mult": 0.5}
|
268 |
+
)
|
269 |
+
st.success("Vector store AR chargée avec succès !")
|
270 |
+
|
271 |
+
st.write("""مرحباً! أنا هنا للإجابة على جميع أسئلتك المتعلقة بخدمات إنوي
|
272 |
+
وعروض الهاتف المحمول والإنترنت، وأي حلول أخرى قد تناسب احتياجاتك (AR).""")
|
273 |
+
|
274 |
+
user_query_ar = st.chat_input("Posez votre question ici (AR)...")
|
275 |
+
|
276 |
+
if user_query_ar:
|
277 |
+
if not st.session_state["retriever_ar"]:
|
278 |
+
st.warning("Veuillez d'abord créer ou charger la Vector Store (AR).")
|
279 |
+
return
|
280 |
+
|
281 |
+
# Récupération du contexte
|
282 |
+
context_ar_list = retrieve_context_ar(st.session_state["retriever_ar"], user_query_ar, top_k=5)
|
283 |
+
|
284 |
+
if context_ar_list:
|
285 |
+
with st.spinner("Génération de la réponse..."):
|
286 |
+
response_ar = llm_chain_ar.run({"context": "\n".join(context_ar_list), "query": user_query_ar})
|
287 |
+
response_ar = response_ar.split("[الإجابة]")[-1]
|
288 |
+
st.write("**سؤال العميل:**")
|
289 |
+
st.write(user_query_ar)
|
290 |
+
st.write("**الإجابة:**")
|
291 |
+
st.write(response_ar)
|
292 |
+
else:
|
293 |
+
st.write("Aucun contexte trouvé pour cette question. Essayez autre chose.")
|
294 |
+
|
295 |
+
if __name__ == "__main__":
|
296 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit
|
2 |
+
pandas
|
3 |
+
langchain
|
4 |
+
huggingface_hub
|
5 |
+
torch
|
6 |
+
langchain_community
|
7 |
+
openpyxl
|
8 |
+
sentence_transformers
|
9 |
+
pysqlite3-binary
|
10 |
+
chromadb==0.4.0
|
11 |
+
langchain-chroma==0.1.4
|