Spaces:
Sleeping
Sleeping
#region# import libs | |
import streamlit as st | |
import os | |
from mistralai import Mistral | |
import numpy as np | |
# import fitz # PyMuPDF pour extraction PDF | |
import faiss | |
import pickle | |
from mistralai import Mistral | |
# from sklearn.manifold import TSNE | |
# from llama_index.core import VectorStoreIndex, SimpleDirectoryReader | |
# from dotenv import load_dotenv | |
MISTRAL_API_KEY = os.getenv("api_mistral") | |
model = "mistral-large-latest" #"ministral-8b-latest" # ancien model : 'mistral-large-latest' | |
mistral_client = Mistral(api_key=MISTRAL_API_KEY) | |
MAX_TOKENS = 1500 | |
#endregion | |
#region# rag | |
model_embedding = "mistral-embed" | |
# 📌 Paramètres de segmentation | |
chunk_size = 256 # Réduction du chunk size pour un meilleur contrôle du contexte | |
chunk_overlap = 15 | |
# 📌 Définition des chemins de stockage | |
index_path = "files/faiss_index.bin" | |
chunks_path = "files/chunked_docs.pkl" | |
metadata_path = "files/metadata.pkl" | |
embeddings_path = "files/embeddings.npy" | |
print("🔄 Chargement des données existantes...") | |
index = faiss.read_index(index_path) | |
with open(chunks_path, "rb") as f: | |
chunked_docs = pickle.load(f) | |
with open(metadata_path, "rb") as f: | |
metadata_list = pickle.load(f) | |
embeddings = np.load(embeddings_path) | |
print("✅ Index, chunks, embeddings et métadonnées chargés avec succès !") | |
# 📌 Récupération des chunks les plus pertinents | |
def retrieve_relevant_chunks(question, k=5): | |
question_embedding_response = mistral_client.embeddings.create( | |
model=model_embedding, | |
inputs=[question], | |
) | |
question_embedding = np.array(question_embedding_response.data[0].embedding).astype('float32').reshape(1, -1) | |
distances, indices = index.search(question_embedding, k) | |
if len(indices[0]) == 0: | |
print("⚠️ Avertissement : Aucun chunk pertinent trouvé, réponse possible moins précise.") | |
return [], [] | |
return [chunked_docs[i] for i in indices[0]] | |
#endregion | |
#region# Définition des prompts | |
def generate_prompts(score:str, type: str, annee_min: str, annee_max:str, context) -> dict: | |
""" | |
Genere les prefixes et suffixes des prompts pour Mistral en fonction du score de vulgarisation, du type d'espece, et les années des documents | |
Args: | |
score (str): 1 = vulgarisé, 2 = intermédiaire, 3 = technique | |
type (str): 'Ponte' ou 'Chair' | |
annee_min (str): annee min de publication | |
annee_max (str): annee max de publication | |
""" | |
if type == "Ponte": | |
type_description = "volailles pondeuses" | |
elif type == "Chair": | |
type_description = "volailles de chair" | |
else: | |
raise ValueError("Type must be either 'Ponte' or 'Chair'") | |
if score == "1": | |
prefix_prompt = f"""Tu es un assistant IA spécialisé en nutrition de la volaille. Ton utilisateur est un chercheur travaillant sur | |
l'amélioration des régimes alimentaires pour optimiser la santé et la croissance des {type_description}. Voici les informations extraites des documents à utiliser avec priorité : {context}. | |
Réponds en vulgarisant les informations. | |
Pour fournir la réponse, tu dois te baser sur des publications/articles qui ont une date de publication entre {annee_min} et {annee_max}.""" | |
suffix_prompt = """Réponds en français et donne une réponse directe et claire. | |
N'inclus pas de bibliographie dans ta réponse. Intègre les numéros de tes sources dans le texte.""" | |
elif score == "2": | |
prefix_prompt = f"""Tu es un assistant IA spécialisé en nutrition de la volaille. Ton utilisateur est un chercheur travaillant sur | |
l'amélioration des régimes alimentaires pour optimiser la santé et la croissance des {type_description}. Voici les informations extraites des documents à utiliser avec priorité : {context}. | |
Réponds en fournissant des explications claires et concises, adaptées à la question posée. | |
Pour fournir la réponse, tu dois te baser sur des publications/articles qui ont une date de publication entre {annee_min} et {annee_max}. | |
Tes réponses doivent être structurées, complètes et adaptées aux professionnels du secteur.""" | |
suffix_prompt = """Présente une réponse claire et concise. Utilise un ton | |
professionnel, clair et rigoureux. Réponds en français. | |
N'inclus pas de bibliographie dans ta réponse. Intègre les numéros de tes sources dans le texte.""" | |
elif score == "3": | |
prefix_prompt = f"""Tu es un assistant IA spécialisé en nutrition de la volaille. Ton utilisateur est un chercheur travaillant sur | |
l'amélioration des régimes alimentaires pour optimiser la santé et la croissance des {type_description}. Voici les informations extraites des documents à utiliser avec priorité : {context}. | |
Réponds en fournissant des explications détaillées et précises, adaptées à la complexité de la question posée. | |
N'oublie pas de citer à la fin de ta réponse les références sur lesquelles tu t'es basé avec son année (entre {annee_min} et {annee_max}). | |
Tes réponses doivent être structurées, complètes et adaptées aux professionnels du secteur.""" | |
suffix_prompt = """Présente une réponse détaillée et complète. Utilise un ton | |
professionnel, clair et rigoureux. Si possible, inclue des chiffres, des études ou des références pertinentes pour renforcer | |
la crédibilité de la réponse. Réponds en français. | |
N'inclus pas de bibliographie dans ta réponse. Intègre les numéros de tes sources dans le texte.""" | |
else: | |
raise ValueError("Score must be 1, 2, or 3") | |
return prefix_prompt, suffix_prompt | |
def send_prompt_to_mistral(type_reponse: str, user_prompt: str, temperature:int, n_comp:int, prefix_prompt: str, suffix_prompt:str, verbose=True) -> str: | |
""" | |
Envoie un prompt à Mistral pour obtenir une réponse | |
Args: | |
type_reponse (str): Le rôle de l'utilisateur, peut être 'technicien' ou 'chercheur'. | |
Si le rôle ne correspond pas à l'un de ces deux, une exception sera levée. | |
user_prompt (str): Le texte du prompt à envoyer à Mistral. | |
temperature (int): Lower values make the model more deterministic, focusing on likely responses for accuracy | |
verbose (bool): Print la reponse avant le return | |
prefixe (str): Prefixe du prompt | |
suffixe (str): Suffixe du prompt | |
Returns: | |
dict: La réponse du modèle Mistral à partir du prompt fourni. | |
Raises: | |
ValueError: Si le rôle spécifié n'est pas 'technicien' ou 'chercheur'. | |
""" | |
# Création du message à envoyer à Mistral | |
messages = [{"role": type_reponse, "content": suffix_prompt}] | |
# Envoi de la requête à Mistral et récupération de la réponse | |
chat_response = mistral_client.chat.complete( | |
model = model, | |
messages=[ | |
{"role": "system", "content": prefix_prompt}, | |
{"role": "user", "content": user_prompt + suffix_prompt}, | |
], | |
#response_format={ | |
# 'type': 'json_object' | |
#}, | |
temperature=temperature, | |
n=n_comp, | |
max_tokens=MAX_TOKENS, | |
stream=False | |
#stop='\n' | |
) | |
if verbose: | |
print(chat_response) | |
return chat_response | |
def is_valid_mistral_response(response: dict) -> bool: | |
""" | |
Vérifie si la réponse de l'API Mistral est valide. | |
Critères de validité : | |
- La clé "choices" existe et contient au moins un élément. | |
- Le texte généré n'est pas vide et ne contient pas uniquement des tabulations ou espaces. | |
- La génération ne s'est pas arrêtée pour une raison autre que 'stop'. | |
- La réponse ne contient pas de texte générique inutile. | |
:param response: Dictionnaire représentant la réponse de l'API Mistral | |
:return: True si la réponse est valide, False sinon | |
""" | |
if not isinstance(response, dict): | |
return False | |
choices = response.get("choices") | |
if not choices or not isinstance(choices, list): | |
return False | |
first_choice = choices[0] | |
if not isinstance(first_choice, dict): | |
return False | |
text = first_choice.get("message", {}).get("content", "").strip() | |
if not text or text in ["\t\t", "", "N/A"]: | |
return False | |
finish_reason = first_choice.get("finish_reason", "") | |
if finish_reason in ["error", "tool_calls"]: | |
return False | |
# Vérification du contenu inutile | |
invalid_responses = [ | |
"Je suis une IA", "Désolé, je ne peux pas répondre", "Je ne sais pas" | |
] | |
if any(invalid in text for invalid in invalid_responses): | |
return False | |
return True | |
def print_pretty_response(response: dict, verbose=True): | |
pretty = response.choices[0].message.content | |
if verbose: | |
print(pretty) | |
return pretty | |
def response_details(response, verbose=True): | |
""" | |
Envoie les details techniques du prompt | |
""" | |
details = {} | |
details["id"] = response.id | |
details["total_tokens"] = response.usage.total_tokens | |
details["prefix"] = response.choices[0].message.prefix | |
details["role"] = response.choices[0].message.role | |
if verbose: | |
print(details) | |
return details | |
def prompt_pipeline(user_prompt: str, niveau_detail: str, type_reponse: str, souche: str, annee_publication_min: str, annee_publication_max: str, context) -> dict: | |
""" | |
Fonction visible de l'application pour appeler un prompt et obtenir sa reponse | |
Args: | |
prompt (str): Prompt utilisateur | |
niveau_detail (str): Niveau de detail de la requete : 1, 2, 3. Plus haut = plus d'infos | |
type_reponse (str): 'Ponte', 'Chair' | |
Returns: | |
Dict | |
""" | |
prefix_prompt, suffix_prompt = generate_prompts(score=niveau_detail, type=type_reponse, annee_min=annee_publication_min, annee_max=annee_publication_max, context= context) | |
reponse_mistral = send_prompt_to_mistral( | |
type_reponse=type_reponse, | |
user_prompt=user_prompt, | |
temperature=0.05, | |
n_comp=1, | |
verbose=False, | |
prefix_prompt=prefix_prompt, | |
suffix_prompt=suffix_prompt | |
) | |
to_return = {} | |
to_return["reponse_propre"] = print_pretty_response(reponse_mistral, verbose=True) | |
to_return["details"] = response_details(reponse_mistral, verbose=False) | |
to_return["prefix"] = str(prefix_prompt) | |
return to_return | |
#endregion | |
#region# Titre de l'application et mise en page des éléments graphiques | |
st.set_page_config(page_title="VolAI", page_icon="🤖") | |
st.title("VolAI, le chatbot expert en nutrition de volailles") | |
st.sidebar.image("img/avril_logo_rvb.jpg") | |
st.sidebar.header("") | |
#Choix production | |
choix_prod = st.sidebar.pills( | |
"Sur quelle espèce voulez-vous avoir des renseignements ?", | |
("Ponte", "Chair"),) | |
#Niveau vulgarisation | |
choix_vulgarisation = st.sidebar.pills( | |
"Quel niveau de vulgarisation souhaitez-vous ? (1- Vulgarisé 2-Intermédiaire 3-Technique)", | |
("1", "2", "3"),) | |
#Années de publication | |
choix_annee = st.sidebar.slider("Années de publication", | |
min_value=1980, | |
max_value=2025, | |
value=(2010,2025)) | |
#endregion | |
#region# Interface utilisateur | |
user_input = st.text_area("Entrez votre question:", placeholder="E.g.: Quels sont les impact et la toxicité spécifique sur les volaille aux doses d’alkaloides tropaniques ?") | |
if st.button("Envoyer la question..."): | |
if user_input and choix_prod and choix_vulgarisation and choix_annee : | |
with st.spinner("Veuillez patienter quelques instants..."): | |
# Génération de la réponse | |
#todo mettre relevant chunks et context = | |
relevant_chunks= retrieve_relevant_chunks(user_input) | |
# context = "\n".join([chunk["text"] for chunk in relevant_chunks]) | |
chunk_references = [f"[{i+1}]" for i in range(len(relevant_chunks))] | |
context = "\n\n".join([f"{chunk_references[i]} (Source: {src['metadata']['source']}) :\n{src['text']}" for i, src in enumerate(relevant_chunks)]) | |
response0 = prompt_pipeline( | |
user_prompt = user_input, | |
niveau_detail=choix_vulgarisation, | |
type_reponse=choix_prod, | |
souche=None, | |
context=context, | |
annee_publication_max=max(choix_annee), | |
annee_publication_min=min(choix_annee) | |
) | |
# st.markdown(f""" | |
# <div style="border: 2px solid #453103; padding: 15px; border-radius: 10px;"> | |
# {response0["prefix"]} | |
# </div> | |
# """, unsafe_allow_html=True) | |
# print("prefix = ",response0["prefix"]) | |
# st.markdown("**Bot :** \\t " + bot_response) | |
# Afficher un titre | |
st.subheader("Réponse :") | |
# Ajouter du texte Markdown avec un cadre | |
st.markdown(f""" | |
<div style="border: 2px solid #453103; padding: 15px; border-radius: 10px;"> | |
{response0["reponse_propre"]} | |
</div> | |
""", unsafe_allow_html=True) | |
#print du contexte | |
st.subheader("Sources :") | |
st.markdown(f""" | |
<div style="border: 2px solid #453103; padding: 15px; border-radius: 10px;"> | |
{context} | |
</div> | |
""", unsafe_allow_html=True) | |
# #réponse sans contexte | |
# response1 = prompt_pipeline( | |
# user_prompt = user_input, | |
# niveau_detail=choix_vulgarisation, | |
# type_reponse=choix_prod, | |
# souche=None, | |
# context='', | |
# annee_publication_max=max(choix_annee), | |
# annee_publication_min=min(choix_annee) | |
# ) | |
# st.subheader("Réponse sans contexte :") | |
# st.markdown(f""" | |
# <div style="border: 2px solid #453103; padding: 15px; border-radius: 10px;"> | |
# {response1['reponse_propre']} | |
# </div> | |
# """, unsafe_allow_html=True) | |
#encadré sources | |
# # Afficher un titre | |
# st.subheader("Sources :") | |
# # Ajouter du texte Markdown avec un cadre | |
# st.markdown(f""" | |
# <div style="border: 2px solid #453103; padding: 15px; border-radius: 10px;"> | |
# {bot_response[1]} | |
# </div> | |
# """, unsafe_allow_html=True) | |
#encadré Reviews | |
# st.markdown(""" | |
# <div style="border: 2px solid #453103; padding: 15px; border-radius: 10px;"> | |
# Reviews | |
# </div> | |
# """, unsafe_allow_html=True) | |
else: | |
if not user_input: | |
st.warning("Veuillez entrer un message!") | |
if not choix_prod or not choix_vulgarisation or not choix_annee: | |
st.warning("Veuillez compléter les paramètres dans le bandeau latéral de gauche!") | |
#endregion | |