PHE_Outil_IA / functions_rag_chat_v3.py
clairedhx's picture
Upload folder using huggingface_hub
ce5d3ba verified
import os
import gradio as gr
from gradio.themes.base import Base
import glob
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import GPT4AllEmbeddings
from langchain_community.chat_models import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
import getpass
import json
# Import necessary modules
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda, RunnableParallel, RunnablePassthrough
from typing import Sequence, Any, Dict
from langchain.schema import Document
def chunks_from_pdf(pdf_directory):
"""
Chunks all pdfs from a directory
:param pdf_directory: directory of pdfs
:return: list of chunks
"""
# fetching all pdfs from the directory and storing them as strings in a list
docs = []
for file in glob.glob(pdf_directory + "/*.pdf"):
loader = PyPDFLoader(file)
doc = loader.load()
docs.extend(doc)
# split texts into chunks with overlap
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=500, chunk_overlap=100)
splits = splitter.split_documents(docs)
print(f"Loaded {len(docs)} documents")
return splits
def chunks_from_text(text_directory):
"""
Chunks all text files from a directory
:param text_directory: directory of text files
:return: list of chunks
"""
# fetch all txt files from the firectory and store them in a list
loader = DirectoryLoader(text_directory, loader_cls=TextLoader) # , glob="**/*.txt")
docs = loader.load()
from langchain_community.document_transformers import LongContextReorder
reordering = LongContextReorder()
reordered_docs = reordering.transform_documents(docs)
# split texts into chunks with overlap
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=500, chunk_overlap=100)
splits = splitter.split_documents(reordered_docs)
return splits
def chunking(data_directory, type):
"""
Automatically calls the correct chunking function, either for pdfs or for txt files
:param data_directory: directory of data, either ../pdf or ../text
:return: result from the corresponding chunking function
"""
if type == "pdf":
return chunks_from_pdf(data_directory)
else:
return chunks_from_text(data_directory)
def create_vector_store(db_directory, chunks, embedding):
"""
Creates a chromaDB vector embedding store for all chunks of the data
:param db_directory: directory to persistently store the resulting vector store
:param chunks: list of chunks of data
:param embedding: embedding function
:return: retriever on vector store
"""
print("Creating vector store (this may take a while)")
print(f"Creating vector store with {len(chunks)} chunks")
# create vector store and index
vectorstore = Chroma.from_documents(documents=chunks, collection_name="chromemwah", embedding=embedding,
persist_directory=db_directory)
return vectorstore #.as_retriever(search_type="similarity")
def fetch_vector_store(db_directory, embedding):
"""
Fetches a chromaDB vector embedding store of the data
:param db_directory: directory where vector store is persistently stored
:param embedding: embedding function
:return: retriever on vector store
"""
print("Fetching vector store")
print(f"Fetching vector store from {db_directory}")
vectorstore = Chroma(collection_name="chromemwah", embedding_function=embedding, persist_directory=db_directory)
return vectorstore #.as_retriever(search_type="similarity")
def retrieve(retrieving, question):
"""
Retrieve relevant documents from vector store based on query/question
:param retrieving: retriever
:param question: user query
:return: relevant documents
"""
print("Retrieving")
documents = retrieving.get_relevant_documents(question)
print(f"Retrieved {len(documents)} documents for the question: {question}")
return documents
def context_formatting(documents):
"""
Formats retrieved documents to be used as context for the LLM
:param documents: retrieved documents
:return: formatted documents
"""
content = ""
for index, document in enumerate(documents):
content = content + "[Extrait " + str(index + 1) + "]=" +"Type du document : " + document.metadata["type"] +". Produit concerné : " + document.metadata["nom_med"] +". Texte extrait : " + document.page_content.replace("\n", " ") + "\n\n"
return content
def source_formatting_v0(documents):
"""
Formats retrieved documents to be used as sources for the user
:param documents: retrieved documents
:return: formatted documents
"""
sources = ""
for i, document in enumerate(documents):
sources= sources + "Avis numéro " + str(i + 1)+ " (id de l'avis : " + str(document[0].metadata["avis_id"]) + ")\n\n" + "Date de l'avis : " + document[0].metadata["date_avis"] + "\n" + "Medicament : " + document[0].metadata["nom_med"] + "\n" + "Exploitant : " + document[0].metadata["exploitant"] + "\n" + "Indication : " + document[0].metadata["indication"] + "\n\n" #+ "Lien avis : " + document[0].metadata["lien_avis"] +"\n\n"
for index, doc in enumerate(document):
sources = sources + "[extrait " + str(index + 1) + "] " + " [" + doc.metadata["type"] + "] " + doc.page_content.replace("\n", " ").replace("+", " ") + "\n"
sources = sources + "---------------------------------------------------------------------------------- \n"
return sources.strip()
def source_formatting(documents, scores, docs_ejected, scores_ejected):
"""
Formats retrieved documents to be used as sources for the user
:param documents: retrieved documents
:param scores: scores associated with the documents
:param docs_ejected: documents that were not selected due to low scores
:param scores_ejected: scores of the rejected documents
:return: formatted documents
"""
sources = ""
for i, (document_group, score_group) in enumerate(zip(documents, scores)):
sources += (f"Avis numéro {i + 1} (id de l'avis : {document_group[0].metadata['avis_id']}) \n" # Deux espaces pour forcer le saut de ligne
f"Date de l'avis : {document_group[0].metadata['date_avis']} \n"
f"Médicament : {document_group[0].metadata['nom_med']} \n"
f"Exploitant : {document_group[0].metadata['exploitant']} \n"
f"Lien de l'avis : [{document_group[0].metadata['lien_avis']}]"
f"({document_group[0].metadata['lien_avis']}) \n" # Lien cliquable en Markdown
f"Indication : {document_group[0].metadata['indication']} \n\n")
for j, (doc, score) in enumerate(zip(document_group, score_group)):
sources += (f"[Extrait {j + 1}] (Score: {score}) [{doc.metadata['type']}] \n"
f"\n{doc.page_content.replace('+', ' ')}\n\n")
sources += "---------------------------------------------------------------------------------- \n"
# Adding ejected chunks
sources += "\n**Chunks non récupérés lors du scoring de pertinence :**\n"
for doc, score in zip(docs_ejected, scores_ejected):
sources += (f"\n(Score: {score}) \n"
f"<div style='font-size:0.9em;'>\n{doc.page_content.replace('+', ' ')}\n</div> \n")
return sources.strip()
def generate_sous_questions(question):
use_llm = "mistral"
# Charger les données d'exemple
with open('/home/onyxia/phe/scripts/modeles/text_to_SQL/entrainement_initial.json', 'r') as f:
exemples = json.load(f)
# Construire le prompt avec les exemples
rag_prompt_template = """
Tu es un assistant pour générer des sous-questions à partir d'une question donnée. On veut séparer la question_donnée en deux parties :
1. La partie permettant de filtrer les documents sur leurs metadatas (question_to_sql).
2. La partie permettant de récupérer les éléments à analyser et à récupérer dans les textes (question_to_llm).
Pour la question_to_sql, il faut générer une question permettant de sélectionner les id des documents concernés par la question_posée, en spécifiant les matadatas à séléctionner cités dans question_posée. Les filtres appliqués ne doivent faire référence qu'à la maladie, l'aire thérapeutique, l'indication, la date, l'asmr, le smr ou le type.
Sachant que l'asmr ne peut prendre comme valeur que 'I','II','III','IV' ou'V'. Le smr ne peut prendre comme valeur que 'important','modéré','faible' ou 'insuffisant'. Le type ne peut prendre comme valeur que 'avis_ct','transcription_ct','avis_ceesp','transcription_ceesp','questionnaire' ou 'efficience'.
Dans question_to_sql, il ne doit pas faire mention des informations à chercher dans le texte, mais seulement des metadatas (maladie, l'aire thérapeutique, l'indication, la date, l'asmr, le smr, le type).
Pour la question_to_llm, il faut récupérer le fond de la question et ce qui doit être récupéré dans le texte des documents sélectionnés, elle ne doit pas mentionner les informations relatives à la requête SQL, présentent dans question_sql.
Tu ne devras génerer des réponses qu'en minuscules, il ne doit y avoir aucune majuscule.
En te basant sur ces exemples d'entraînement, tu devras générer en output 'question_to_sql' et 'question_to_llm' en prenant en input 'question_posée'. Tu génereras l'output en suivant ce format : 'question_to_llm # question_to_sql'.
Exemples d'entraînement :
{examples}
Maintenant, à ton tour de générer question_to_llm et question_to_sql en suivant la mise en forme 'question_to_llm # question_to_sql' à partir de la question_posée suivante :
question_posée donnée en input : {question}
question_to_llm et question_to_sql générées en output en lettres minuscules, aucune majuscule :
"""
examples = ""
for exemple in exemples[0:6]:
examples += (
f"question_posée donnée en input : {exemple['question_posee']}\n"
f"question_to_llm et question_to_sql générées en output: {exemple['question_to_llm']} # {exemple['question_to_sql']}\n\n"
)
rag_prompt = ChatPromptTemplate.from_template(rag_prompt_template)
# define LLM to be used and the temperature (creativity/randomness) of the model
print("llm")
llm = ChatOllama(model=use_llm, temperature=0.8, num_predict=500)
print("chain")
# define a LangChain chain
chain = rag_prompt | llm | StrOutputParser()
print("invoke")
# invoke chain with retrieved documents and the question (user query)
output = chain.invoke({ "question": question, "examples": examples }).split("#")
return output
def generate(question, documents, use_llm):
"""
LLM generates a response based on the question (user query), added context (retrieved documents), and a prompt
:param question: user query
:param documents: retrieved documents, formatted
:param use_llm: which llm to use
:return: LLM generated response
"""
# adapted from https://smith.langchain.com/hub/rlm/rag-prompt
rag_prompt = ChatPromptTemplate.from_template("Tu es un assistant devant répondre à la question d'un client qui souhaîte récupérer et analyser des informations sur des documents de la Comission de la Transparence la Haute Autorité de Santé française,"
" qui est une réunion d'experts médicaux ayant en charge d'évaluer les nouveaux médicaments avant qu'ils ne soient mis sur le marché. Utilise les "
"extraits des documents récupérés en contexte pour répondre à la question. Si"
"l'extrait n'est pas utile pour répondre à la question, dis qu'il n'est pas utile. Garde la"
"réponse conçise, véridique et informative. Réponds toujours en français."
"Réponds plusieurs fois à la question, à chaque fois en considérant un seul extrait, puis agrège les différentes réponses en une conclusion. Base toi sur l'exemple de mise en forme de réponse pour rédiger la tienne."
"Chaque extrait donné en contexte est donné avec son titre, son type et son lien. Prends en considération le morceau de texte en considérant son type :"
"s'il s'agit d'un type avis_ct il s'agit d'une synthèse publiée après la Comission avec les avis finaux des experts sur le médicament. "
"S'il s'agit d'un type transcription_ct il s'agit d'une retranscription des dialogues ayants eut lieux pendant la comission, condidère alors qu'il s'agit de paroles à interpréter, tu dois donc répondre à la question en disant 'les experts disent --- donc nous pouvons en déduire que ---'."
"Les sources (extraits) sont indiquées dans le contexte par : "
"[doc<doc_number>]. TITRE DOCUMENT : --- \n\n TYPE DU DOCUMENT : --- \n\n LIEN DOCUMENT : --- \n\n EXTRAIT : ---- \n\n\n"
"Exemple de mise en forme de réponse : \n"
"Dans l' [extrait1], [réponse à la question en ne considérant que l'extrait 1].\n"
"Dans l' [extrait2], [réponse à la question en ne considérant que l'extrait 2].\n"
"Dans l' [extrait3], [réponse à la question en ne considérant que l'extrait 3].\n"
"Ainsi, en considérant les différents extrait, on en déduit que [synthèse des trois réponses précédentes].\n\n\n\n"
"Question posée par le client : {question} \n"
"Contexte associé : {context} \n"
"Réponse générée en français et en suivant la mise en forme de l'exemple :")
# define LLM to be used and the temperature (creativity/randomness) of the model
llm = ChatOllama(model=use_llm, temperature=0.5, num_predict=3000)
# define a LangChain chain
chain = rag_prompt | llm | StrOutputParser()
# invoke chain with retrieved documents and the question (user query)
output = chain.invoke({"context": documents, "question": question})
return output
def generate_agregated(reponses, use_llm):
"""
LLM generates a response based on the question (user query), added context (retrieved documents), and a prompt
:param liste_rep: liste des réponses individuelles
:param use_llm: which llm to use
:return: LLM generated response
"""
# adapted from https://smith.langchain.com/hub/rlm/rag-prompt
rag_prompt = ChatPromptTemplate.from_template("Tu es un assistant qui doit synthétiser plusieurs réponses à une question donnée par un client qui souhaîte récupérer et analyser des informations sur des documents de la Comission de la Transparence la Haute Autorité de Santé française,"
" qui est une réunion d'experts médicaux ayant en charge d'évaluer les nouveaux médicaments avant qu'ils ne soient mis sur le marché sur la base d'un dossier d'étude qui leur est présenté."
"Pour répondre tu as en contexte plusieurs réponses à la question qui t'ai posée, qui ont été générées par un llm en se basant à chaque fois sur 3 extraits d'un document donné."
"Ton rôle est de récupérer ces réponses, de vérifier si elles répondent bien à la question posée et d'agréger les informations issues de ces réponses en une petite synthèse pour répondre à la question."
"Les réponses sont indiquées dans le contexte par : "
"Document [titre_doc] : 1. [réponse en se basant sur le premier extrait du document [titre_doc]] 2. [réponse en se basant sur le deuxième extrait du document [titre_doc]] 3. [réponse en se basant sur le troisième extrait du document [titre_doc]] "
"Pour chaque document, vérifie que chaque réponse réponds bien à la question donnée et récupère les informations qui y répondent bien et font sens par rapport à la question."
" Réponds toujours en français."
"Question posée par le client : {question} \n"
"Contexte (réponses générées préalablement) : {context} \n"
"Synthèse générée en français pour répondre précisemment à la question et à rien d'autre :")
# define LLM to be used and the temperature (creativity/randomness) of the model
llm = ChatOllama(model=use_llm, temperature=0.5, num_predict=3000)
# define a LangChain chain
chain = rag_prompt | llm | StrOutputParser()
# invoke chain with retrieved documents and the question (user query)
output = chain.invoke({"context": reponses, "question": question})
return output
def generate_2(question, documents, use_llm):
"""
LLM generates a response based on the question (user query), added context (retrieved documents), and a prompt
:param question: user query
:param documents: retrieved documents, formatted
:param use_llm: which llm to use
:return: LLM generated response
"""
# adapted from https://smith.langchain.com/hub/rlm/rag-prompt
rag_prompt = ChatPromptTemplate.from_template("Tu es un assistant devant répondre à la question d'un client qui souhaîte récupérer et analyser des informations sur des documents de la Comission de la Transparence la Haute Autorité de Santé française,"
" qui est une réunion d'experts médicaux ayant en charge d'évaluer les nouveaux médicaments avant qu'ils ne soient mis sur le marché. Utilise les "
"extraits de document récupéré en contexte pour répondre à la question."
"Réponds à la question, et seulement à la question. N'ajoute aucune information qui ne répond pas à la question. Sois concis et clair."
"Ne cite pas les extraits de document."
"Si les etraits donnés en contexte ne permettent pas de répondre à la question, renvoie 'Pas d'élément de réponse dans ces extraits'."
"Le ou les extraits de document sont indiqués dans le contexte par : "
"[Extrait num_doc]= Type du document : [type]. Produit concerné : [nom du médicament]. Texte extrait : [extrait du document] "
"Question : {question} \n"
"Contexte : {context} \n"
"Réponse concise à la question '{question}', générée en français:")
# define LLM to be used and the temperature (creativity/randomness) of the model
llm = ChatOllama(model=use_llm, temperature=0.35, num_predict=600)
# define a LangChain chain
chain = rag_prompt | llm | StrOutputParser()
# invoke chain with retrieved documents and the question (user query)
output = chain.invoke({"context": documents, "question": question})
return output
def generate_agregated_2(reponses, question, use_llm):
"""
LLM generates a response based on the question (user query), added context (retrieved documents), and a prompt
:param liste_rep: liste des réponses individuelles
:param use_llm: which llm to use
:return: LLM generated response
"""
# adapted from https://smith.langchain.com/hub/rlm/rag-prompt
rag_prompt = ChatPromptTemplate.from_template("Tu es un assistant qui doit synthétiser plusieurs réponses à une question donnée par un client qui souhaîte récupérer et analyser des informations sur des documents de la Comission de la Transparence la Haute Autorité de Santé française,"
" qui est une réunion d'experts médicaux ayant en charge d'évaluer les nouveaux médicaments avant qu'ils ne soient mis sur le marché sur la base d'un dossier d'étude qui leur est présenté."
"Pour répondre tu as en contexte plusieurs réponses à la question qui t'ai posée, qui ont été générées par un llm en se basant à chaque fois sur un extrait de document différent. "
"Tu as en contexte toutes les réponses générées individuellement sur chaque extrait de document, avec le type de document et le médicament concerné par ce document."
"Tu citeras les documents avec '[num_doc]' lorsque tu utiliseras une information provennant d'une réponse générée sur un document."
"Ton rôle est de récupérer ces réponses, de vérifier si elles répondent bien à la question posée et d'agréger les informations issues de ces réponses en une petite synthèse pour répondre à la question."
"Les réponses sont indiquées dans le contexte par : "
"Document numéro [num_doc] = Type du document : [type]. Produit concerné : [nom du médicament]. Réponse générée : [réponse générée par llm en se basant sur le document [num_doc]] "
"Pour chaque document, vérifie que chaque réponse réponds bien à la question donnée, si ce n'est pas le cas ne considère pas cette réponse pour ta synthèse. Récupère les informations qui répondent bien à la question et font sens par rapport à la question."
" Réponds toujours en français."
"Question posée par le client : {question} \n"
"Contexte (réponses générées préalablement) : {context} \n"
"Synthèse générée en français pour répondre précisemment à la question et à rien d'autre :")
# define LLM to be used and the temperature (creativity/randomness) of the model
llm = ChatOllama(model=use_llm, temperature=0.5, num_predict=3000)
# define a LangChain chain
chain = rag_prompt | llm | StrOutputParser()
# invoke chain with retrieved documents and the question (user query)
output = chain.invoke({"context": reponses, "question": question})
return output
def ind_relevant_doc(question, documents, use_llm):
"""
LLM generates a response based on the question (user query), added context (retrieved documents), and a prompt
:param question: user query
:param documents: retrieved documents, formatted
:param use_llm: which llm to use
:return: LLM generated response
"""
# adapted from https://smith.langchain.com/hub/rlm/rag-prompt
rag_prompt = ChatPromptTemplate.from_template("Juge la pertinence entre la requête et le document : le document permet-il de répondre à la question? Renvoie 'oui' ou 'non' et rien d'autre."
"Requête : {question} \n"
"Document : {context} \n"
"réponse :")
# define LLM to be used and the temperature (creativity/randomness) of the model
llm = ChatOllama(model=use_llm, temperature=0.7, num_predict=3)
# define a LangChain chain
chain = rag_prompt | llm | StrOutputParser()
# invoke chain with retrieved documents and the question (user query)
output = chain.invoke({"context": documents, "question": question})
return output
def generate_score(question, documents, use_llm):
"""
LLM generates a response based on the question (user query), added context (retrieved documents), and a prompt
:param question: user query
:param documents: retrieved documents, formatted
:param use_llm: which llm to use
:return: LLM generated response
"""
# adapted from https://smith.langchain.com/hub/rlm/rag-prompt
rag_prompt = ChatPromptTemplate.from_template("Sur une échelle de 0 à 5, juge la pertinence entre la requête et le document. Ne renvoie que la note attribuée et rien d'autre."
"Requête : {question} \n"
"Document : {context} \n"
"réponse :")
# define LLM to be used and the temperature (creativity/randomness) of the model
llm = ChatOllama(model=use_llm, temperature=0.7, num_predict=1)
# define a LangChain chain
chain = rag_prompt | llm | StrOutputParser()
# invoke chain with retrieved documents and the question (user query)
output = chain.invoke({"context": documents, "question": question})
return output