|
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 |
|
|
|
|
|
|
|
|
|
|
|
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_fr(data, r, col): |
|
""" |
|
Trouve la question parente pour une ligne et colonne donnée dans le DataFrame (version FR). |
|
""" |
|
i = r - 1 |
|
parent = None |
|
while i >= 0 and pd.isna(parent): |
|
parent = data.iloc[i, col] |
|
i -= 1 |
|
return parent |
|
|
|
def create_contextual_fr(df, category, strat_id=0): |
|
""" |
|
Crée un DataFrame avec questions-réponses contextuelles (version FR). |
|
""" |
|
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_fr(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_fr(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_fr(excel_path: str, persist_dir: str = "./chroma_db_fr"): |
|
""" |
|
Charge les données depuis plusieurs feuilles Excel (version FR), |
|
construit & stocke un Chroma VectorStore. |
|
""" |
|
|
|
qna_tree_fr0 = pd.read_excel(excel_path, sheet_name="Prépayé (FR)", skiprows=1).iloc[:, :5] |
|
qna_tree_fr1 = pd.read_excel(excel_path, sheet_name="Postpayé (FR)", skiprows=1).iloc[:, :5] |
|
qna_tree_fr2 = pd.read_excel(excel_path, sheet_name="Wifi (FR)", skiprows=1).iloc[:, :5] |
|
|
|
|
|
context_fr0 = create_contextual_fr(qna_tree_fr0, "Prépayé", strat_id = 0) |
|
context_fr1 = create_contextual_fr(qna_tree_fr1, "Postpayé", strat_id = len(context_fr0)) |
|
context_fr2 = create_contextual_fr(qna_tree_fr2, "Wifi", strat_id = len(context_fr0) + len(context_fr1)) |
|
|
|
|
|
context_fr = pd.concat([context_fr0, context_fr1, context_fr2], axis=0) |
|
|
|
|
|
context_fr["context"] = context_fr.apply( |
|
lambda row: f"{row['question']} > {row['answer']}", |
|
axis=1 |
|
) |
|
|
|
|
|
documents_fr = [ |
|
Document( |
|
page_content=row["context"], |
|
metadata={"id": row["id"], "category": row["category"]} |
|
) |
|
for _, row in context_fr.iterrows() |
|
] |
|
|
|
|
|
embedding_model_fr = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") |
|
vectorstore_fr = Chroma.from_documents(documents_fr, embedding_model_fr, persist_directory=persist_dir) |
|
vectorstore_fr.persist() |
|
|
|
return vectorstore_fr |
|
|
|
def load_existing_vectorstore_fr(persist_dir: str = "./chroma_db_fr"): |
|
""" |
|
Charge un VectorStore Chroma déjà stocké (version FR). |
|
""" |
|
embedding_model_fr = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") |
|
vectorstore_fr = Chroma( |
|
persist_directory=persist_dir, |
|
embedding_function=embedding_model_fr |
|
) |
|
return vectorstore_fr |
|
|
|
def retrieve_context_fr(retriever_fr, query, top_k=5): |
|
""" |
|
Récupère les top_k résultats pour la question (version FR). |
|
""" |
|
results_fr = retriever_fr.get_relevant_documents(query) |
|
context_fr_list = [] |
|
for _, result in enumerate(results_fr[:top_k], start=1): |
|
context_fr_list.append(result.page_content) |
|
return context_fr_list |
|
|
|
|
|
|
|
|
|
|
|
|
|
prompt_template_fr = PromptTemplate( |
|
input_variables=["context", "query"], |
|
template=( |
|
"""[SYSTEM] |
|
Vous êtes un assistant client professionnel, expérimenté et bienveillant pour l'opérateur téléphonique INWI. |
|
Vous excellez dans la gestion des clients, en répondant à leurs problèmes et questions. |
|
Fournir un service client et des conseils en se basant sur les contextes fournis : |
|
- Répondre aux salutations de manière courtoise et amicale, par exemple : "Bonjour! Je suis l'assistant IA d'INWI'. Comment puis-je vous aider aujourd'hui ?" |
|
- Identifier le besoin du client et demander des clarifications si nécessaire, tout en s'appuyant uniquement sur le contexte. |
|
- 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. |
|
- 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. |
|
- Structurer les réponses de manière concise et efficace. Et n'inventez pas d'infos non présentes dans le contexte. |
|
- Informer le client qu’il peut vous recontacter pour toute assistance supplémentaire. |
|
- Ne parlez pas des concurrents qui offrent la meme service d'INWI. |
|
- Ne jamais insulter ou répondre à une insulte. |
|
- Ne demandez pas d’informations personnelles ou d’identification du client. |
|
- Orientez vers le catalogue sur le site web INWI si la question concerne une offre du catalogue. |
|
- Donnez des solutions standard pour les problèmes techniques avec des options. |
|
- Avant de générer votre réponse, éliminez toutes les structures comme '[Action] [texte]' et gardez uniquement les informations utiles. |
|
- Ne jamais parler des sujets suivants : [ |
|
"politique", "élections", "partis", "gouvernement", "lois", "réformes", |
|
"religion", "croyances", "pratiques religieuses", "théologie", |
|
"moralité", "débat", "philosophie", "éthique", "discrimination", |
|
"concurrence", "Maroc Telecom", "IAM", "Orange", "comparaison", |
|
"sécurité", "fraude", "santé", "médicaments", "traitement", "diagnostic", |
|
"finance", "investissement", "bourse", "crypto", "maladie", |
|
"violence", "haine", "contenu explicite", "sexe", "adultes", |
|
"illégal", "faux documents", "streaming illégal" |
|
] |
|
INWI est un opérateur de télécommunications marocain offrant des services mobiles, Internet et solutions de télécommunications |
|
pour les particuliers et les entreprises. Il se distingue par son engagement à fournir des services de qualité, innovants et |
|
accessibles, tout en contribuant au développement numérique du pays. |
|
Les clients sont notre priorité, et notre but est de résoudre leurs problèmes. |
|
Votre rôle est de fournir un service client professionnel et efficace sans inventer d'informations. |
|
|
|
[CONTEXTE] |
|
{context} |
|
|
|
[QUESTION DU CLIENT] |
|
{query} |
|
|
|
[RÉPONSE]""" |
|
) |
|
) |
|
|
|
|
|
llm_fr = HuggingFaceHub( |
|
repo_id="mistralai/Mistral-7B-Instruct-v0.3", |
|
huggingfacehub_api_token=os.environ["HUGGINGFACEHUB_API"], |
|
model_kwargs={ |
|
"temperature": 0.5, |
|
"max_length": 500 |
|
} |
|
) |
|
|
|
|
|
llm_chain_fr = LLMChain(llm=llm_fr, prompt=prompt_template_fr) |
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
st.subheader("INWI IA Chatbot - Français") |
|
|
|
|
|
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_fr" not in st.session_state: |
|
st.session_state["retriever_fr"] = None |
|
|
|
st.sidebar.header("Vector Store Options (FR)") |
|
|
|
if st.sidebar.button("Créer la Vector Store (FR)"): |
|
with st.spinner("Extraction et création de la vector store FR..."): |
|
excel_path = "Chatbot myinwi.xlsx" |
|
persist_directory_fr = "./chroma_db_fr" |
|
vectorstore_fr = load_excel_and_create_vectorstore_fr( |
|
excel_path=excel_path, |
|
persist_dir=persist_directory_fr |
|
) |
|
st.session_state["retriever_fr"] = vectorstore_fr.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 (FR)"): |
|
with st.spinner("Chargement de la vector store FR existante..."): |
|
persist_directory_fr = "./chroma_db_fr" |
|
vectorstore_fr = load_existing_vectorstore_fr(persist_directory_fr) |
|
st.session_state["retriever_fr"] = vectorstore_fr.as_retriever( |
|
search_type="mmr", |
|
search_kwargs={"k": 5, "lambda_mult": 0.5} |
|
) |
|
st.success("Vector store FR chargée avec succès !") |
|
|
|
st.write("""Je suis là pour répondre à toutes vos questions concernant nos |
|
services, nos offres mobiles et Internet, ainsi que nos solutions adaptées à vos besoins (FR).""") |
|
|
|
|
|
user_query_fr = st.chat_input("Posez votre question ici (FR)...") |
|
|
|
if user_query_fr: |
|
if not st.session_state["retriever_fr"]: |
|
st.warning("Veuillez d'abord créer ou charger la Vector Store (FR).") |
|
return |
|
|
|
|
|
context_fr_list = retrieve_context_fr(st.session_state["retriever_fr"], user_query_fr, top_k=5) |
|
|
|
if context_fr_list: |
|
with st.spinner("Génération de la réponse..."): |
|
response_fr = llm_chain_fr.run({"context": "\n".join(context_fr_list), "query": user_query_fr + "?"}) |
|
|
|
response_fr = response_fr.split("[RÉPONSE]")[-1] |
|
st.write("**Question :**") |
|
st.write(user_query_fr) |
|
st.write("**Réponse :**") |
|
st.write(response_fr) |
|
else: |
|
st.write("Aucun contexte trouvé pour cette question. Essayez autre chose.") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|
|
|
|
|