Créez un chatbot LLM RAG avec LangChain
Vous avez probablement interagi avec de grands modèles de langage (LLM), comme ceux derrière ChatGPT d’OpenAI, et avez expérimenté leur remarquable capacité à répondre à des questions, à résumer des documents, à écrire du code et bien plus encore. Bien que les LLM soient remarquables en eux-mêmes, avec un peu de connaissances en programmation, vous pouvez exploiter des bibliothèques comme LangChain pour créer vos propres chatbots basés sur LLM qui peuvent faire à peu près tout.
Dans un environnement d'entreprise, l'un des moyens les plus populaires de créer un chatbot alimenté par LLM consiste à utiliser la génération augmentée par récupération (RAG). Lorsque vous concevez un système RAG, vous utilisez un modèle de récupération pour récupérer des informations pertinentes, généralement à partir d'une base de données ou d'un corpus, et vous fournissez ces informations récupérées à un LLM pour générer des réponses contextuellement pertinentes.
Dans ce didacticiel, vous vous mettrez dans la peau d’un ingénieur en IA travaillant pour un grand système hospitalier. Vous créerez un chatbot RAG dans LangChain qui utilise Neo4j pour récupérer des données sur les patients, leurs expériences, les emplacements des hôpitaux, les visites, les assureurs et les médecins de votre système hospitalier.
Dans ce didacticiel, vous apprendrez à :
- Utilisez LangChain pour créer des chatbots personnalisés
- Concevoir un chatbot en utilisant votre compréhension des exigences commerciales et des données du système hospitalier
- Travailler avec des bases de données de graphiques
- Configurer une instance AuraDB Neo4j
- Créez un chatbot RAG qui récupère à la fois les données structurées et non structurées de Neo4j
- Déployez votre chatbot avec FastAPI et Streamlit
Cliquez sur le lien ci-dessous pour télécharger le code source complet et les données de ce projet :
Démo : un chatbot LLM RAG avec LangChain et Neo4j
À la fin de ce didacticiel, vous disposerez d'une API REST qui sert votre chatbot LangChain. Vous disposerez également d'une application Streamlit qui fournit une interface de chat agréable pour interagir avec votre API :
Sous le capot, l'application Streamlit envoie vos messages à l'API du chatbot, et le chatbot génère et renvoie une réponse à l'application Streamlit, qui l'affiche à l'utilisateur.
Vous obtiendrez plus tard un aperçu détaillé des données auxquelles votre chatbot a accès, mais si vous avez hâte de le tester, vous pouvez poser des questions similaires aux exemples donnés dans la barre latérale :
Vous apprendrez comment aborder chaque étape, de la compréhension des exigences et des données de l'entreprise à la création de l'application Streamlit. Il y a beaucoup de choses à découvrir dans ce didacticiel, mais ne vous sentez pas dépassé. Vous obtiendrez des informations générales sur chaque concept présenté, ainsi que des liens vers des sources externes qui approfondiront votre compréhension. Maintenant, il est temps de plonger !
Conditions préalables
Ce didacticiel est particulièrement adapté aux développeurs Python intermédiaires qui souhaitent acquérir une expérience pratique en matière de création de chatbots personnalisés. Outre les connaissances intermédiaires de Python, vous bénéficierez d'une compréhension de haut niveau des concepts et technologies suivants :
- Grands modèles de langage (LLM) et ingénierie rapide
- Intégrations de texte et bases de données vectorielles
- Bases de données graphiques et Neo4j
- L'écosystème des développeurs OpenAI
- API REST et FastAPI
- Programmation asynchrone
- Docker et Docker Composer
Rien de ce qui précède n’est une condition préalable difficile, alors ne vous inquiétez pas si vous ne vous sentez bien informé sur aucun d’entre eux. Vous serez présenté à chaque concept et technologie tout au long du parcours. De plus, il n’y a pas de meilleur moyen d’apprendre ces prérequis que de les mettre en œuvre vous-même dans ce tutoriel.
Ensuite, vous obtiendrez un bref aperçu du projet et commencerez à en apprendre davantage sur LangChain.
Aperçu du projet
Tout au long de ce didacticiel, vous créerez quelques répertoires qui constitueront votre chatbot final. Voici une répartition de chaque répertoire :
-
langchain_intro/
vous aidera à vous familiariser avec LangChain et vous équipera des outils dont vous avez besoin pour créer le chatbot que vous avez vu dans la démo, et il ne sera pas inclus dans votre chatbot final. Vous aborderez cela à l’étape 1. data/
contient les données brutes du système hospitalier stockées sous forme de fichiers CSV. Vous explorerez ces données à l'étape 2. À l'étape 3, vous déplacerez ces données dans une base de données Neo4j que votre chatbot interrogera pour répondre aux questions.hospital_neo4j_etl/
contient un script qui charge les données brutes dedata/
dans votre base de données Neo4j. Vous devez l'exécuter avant de créer votre chatbot, et vous apprendrez tout ce que vous devez savoir sur la configuration d'une instance Neo4j à l'étape 3.chatbot_api/
est votre application FastAPI qui sert votre chatbot comme point de terminaison REST, et c'est le principal livrable de ce projet. Les sous-répertoireschatbot_api/src/agents/
etchatbot_api/src/chains/
contiennent les objets LangChain qui composent votre chatbot. Vous apprendrez plus tard ce que sont les agents et les chaînes, mais pour l’instant, sachez simplement que votre chatbot est en fait un agent LangChain composé de chaînes et de fonctions.tests/
comprend deux scripts qui testent la rapidité avec laquelle votre chatbot peut répondre à une série de questions. Cela vous donnera une idée du temps que vous gagnez en effectuant des requêtes asynchrones auprès de fournisseurs LLM comme OpenAI.chatbot_frontend/
est votre application Streamlit qui interagit avec le point de terminaison du chatbot danschatbot_api/
. Il s'agit de l'interface utilisateur que vous avez vue dans la démo et vous la créerez à l'étape 5.
Toutes les variables d'environnement nécessaires à la création et à l'exécution de votre chatbot seront stockées dans un fichier .env
. Vous déployerez le code dans hospital_neo4j_etl/
, chatbot_api
et chatbot_frontend
en tant que conteneurs Docker qui seront orchestrés avec Docker Compose. Si vous souhaitez expérimenter le chatbot avant de parcourir le reste de ce didacticiel, vous pouvez télécharger le matériel et suivre les instructions du fichier README pour faire fonctionner les choses :
Une fois l’aperçu du projet et les conditions préalables derrière vous, vous êtes prêt à passer à la première étape : vous familiariser avec LangChain.
Étape 1 : Familiarisez-vous avec LangChain
Avant de concevoir et développer votre chatbot, vous devez savoir comment utiliser LangChain. Dans cette section, vous découvrirez les principaux composants et fonctionnalités de LangChain en créant une version préliminaire du chatbot de votre système hospitalier. Cela vous donnera tous les outils nécessaires pour créer votre chatbot complet.
Utilisez votre éditeur de code préféré pour créer un nouveau projet Python et assurez-vous de créer un environnement virtuel pour ses dépendances. Assurez-vous que Python 3.10 ou version ultérieure est installé. Activez votre environnement virtuel et installez les bibliothèques suivantes :
(venv) $ python -m pip install langchain==0.1.0 openai==1.7.2 langchain-openai==0.0.2 langchain-community==0.0.12 langchainhub==0.1.14
Vous souhaiterez également installer python-dotenv
pour vous aider à gérer les variables d'environnement :
(venv) $ python -m pip install python-dotenv
Python-dotenv charge les variables d'environnement à partir des fichiers .env
dans votre environnement Python, et vous trouverez cela pratique lorsque vous développerez votre chatbot. Cependant, vous finirez par déployer votre chatbot avec Docker, qui peut gérer les variables d'environnement pour vous, et vous n'aurez plus besoin de Python-dotenv.
Si vous ne l'avez pas déjà fait, vous devrez télécharger reviews.csv
à partir des documents ou du dépôt GitHub pour ce didacticiel :
Ensuite, ouvrez le répertoire du projet et ajoutez les dossiers et fichiers suivants :
./
│
├── data/
│ └── reviews.csv
│
├── langchain_intro/
│ ├── chatbot.py
│ ├── create_retriever.py
│ └── tools.py
│
└── .env
Le fichier reviews.csv
dans data/
est celui que vous venez de télécharger, et les fichiers restants que vous voyez devraient être vides.
Vous êtes maintenant prêt à commencer à créer votre premier chatbot avec LangChain !
Modèles de discussion
Vous avez peut-être deviné que le composant principal de LangChain est le LLM. LangChain fournit une interface modulaire pour travailler avec des fournisseurs LLM tels que OpenAI, Cohere, HuggingFace, Anthropic, Together AI et autres. Dans la plupart des cas, tout ce dont vous avez besoin est une clé API du fournisseur LLM pour commencer à utiliser le LLM avec LangChain. LangChain prend également en charge les LLM ou d'autres modèles de langage hébergés sur votre propre machine.
Vous utiliserez OpenAI pour ce didacticiel, mais gardez à l’esprit qu’il existe de nombreux excellents fournisseurs open source et fermés. Vous pouvez toujours tester différents fournisseurs et optimiser en fonction des besoins de votre application et des contraintes de coûts. Avant de continuer, assurez-vous que vous êtes inscrit à un compte OpenAI et que vous disposez d'une clé API valide.
Une fois que vous avez votre clé API OpenAI, ajoutez-la à votre fichier .env
:
OPENAI_API_KEY=<YOUR-OPENAI-API-KEY>
Bien que vous puissiez interagir directement avec les objets LLM dans LangChain, une abstraction plus courante est le modèle de discussion. Les modèles de chat utilisent des LLM sous le capot, mais ils sont conçus pour les conversations et s'interfacent avec des messages de chat plutôt qu'avec du texte brut.
À l'aide de messages de chat, vous fournissez à un LLM des détails supplémentaires sur le type de message que vous envoyez. Tous les messages ont les propriétés role
et content
. Le role
indique au LLM qui envoie le message, et le content
est le message lui-même. Voici les messages les plus couramment utilisés :
HumanMessage
: un message de l'utilisateur interagissant avec le modèle de langage.AIMessage
: un message du modèle de langage.SystemMessage
: un message qui indique au modèle de langage comment se comporter. Tous les fournisseurs ne prennent pas en charge leSystemMessage
.
Il existe d'autres types de messages, comme FunctionMessage
et ToolMessage
, mais vous en apprendrez plus à leur sujet lorsque vous créerez un agent.
Démarrer avec les modèles de chat dans LangChain est simple. Pour instancier un modèle de chat OpenAI, accédez à langchain_intro
et ajoutez le code suivant à chatbot.py
:
import dotenv
from langchain_openai import ChatOpenAI
dotenv.load_dotenv()
chat_model = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
Vous importez d’abord dotenv
et ChatOpenAI
. Ensuite, vous appelez dotenv.load_dotenv()
qui lit et stocke les variables d'environnement de .env
. Par défaut, dotenv.load_dotenv()
suppose que .env
se trouve dans le répertoire de travail actuel, mais vous pouvez transmettre le chemin vers d'autres répertoires si .env
se trouve ailleurs.
Vous instanciez ensuite un modèle ChatOpenAI
en utilisant GPT 3.5 Turbo comme LLM de base, et vous définissez la température
sur 0. OpenAI propose une diversité de modèles avec différents prix, fonctionnalités et les performances. GPT 3.5 turbo est un excellent modèle pour commencer car il fonctionne bien dans de nombreux cas d'utilisation et est moins cher que les modèles plus récents comme GPT 4 et au-delà.
Remarque : On croit souvent, à tort, que définir temperature=0
garantit des réponses déterministes des modèles GPT. Bien que les réponses soient plus proches du déterminisme lorsque temperature=0
, rien ne garantit que vous obtiendrez la même réponse pour des requêtes identiques. Pour cette raison, les modèles GPT peuvent produire des résultats légèrement différents de ceux que vous voyez dans les exemples de ce didacticiel.
Pour utiliser chat_model
, ouvrez le répertoire du projet, démarrez un interpréteur Python et exécutez le code suivant :
>>> from langchain.schema.messages import HumanMessage, SystemMessage
>>> from langchain_intro.chatbot import chat_model
>>> messages = [
... SystemMessage(
... content="""You're an assistant knowledgeable about
... healthcare. Only answer healthcare-related questions."""
... ),
... HumanMessage(content="What is Medicaid managed care?"),
... ]
>>> chat_model.invoke(messages)
AIMessage(content='Medicaid managed care is a healthcare delivery system
in which states contract with managed care organizations (MCOs) to provide
healthcare services to Medicaid beneficiaries. Under this system, MCOs are
responsible for coordinating and delivering healthcare services to enrollees,
including primary care, specialty care, hospital services, and prescription
drugs. Medicaid managed care aims to improve care coordination, control costs,
and enhance the quality of care for Medicaid beneficiaries.')
Dans ce bloc, vous importez HumanMessage
et SystemMessage
, ainsi que votre modèle de discussion. Vous définissez ensuite une liste avec un SystemMessage
et un HumanMessage
et les exécutez via chat_model
avec chat_model.invoke()
. Sous le capot, chat_model
envoie une requête à un point de terminaison OpenAI desservant gpt-3.5-turbo-0125
, et les résultats sont renvoyés sous forme de AIMessage
. .
Remarque : Vous trouverez peut-être un peu fastidieux de copier et coller du code multiligne de ce didacticiel dans votre REPL Python standard. Pour une meilleure expérience, vous pouvez installer un REPL Python alternatif, tel que IPython, bpython ou ptpython, dans votre environnement virtuel et exécuter les interactions REPL avec ceux-ci.
Comme vous pouvez le voir, le modèle de chat a répondu à la question Qu'est-ce que les soins gérés par Medicaid ? fournie dans le HumanMessage
. Vous vous demandez peut-être ce que le modèle de chat a fait avec le SystemMessage
dans ce contexte. Remarquez ce qui se passe lorsque vous posez la question suivante :
>>> messages = [
... SystemMessage(
... content="""You're an assistant knowledgeable about
... healthcare. Only answer healthcare-related questions."""
... ),
... HumanMessage(content="How do I change a tire?"),
... ]
>>> chat_model.invoke(messages)
AIMessage(content='I apologize, but I can only provide assistance
and answer questions related to healthcare.')
Comme décrit précédemment, le SystemMessage
indique au modèle comment se comporter. Dans ce cas, vous avez demandé au modèle de répondre uniquement aux questions liées aux soins de santé. C'est pourquoi il refuse de vous indiquer comment changer votre pneu. La capacité de contrôler la manière dont un LLM se rapporte à l'utilisateur via des instructions textuelles est puissante, et constitue la base de la création de chatbots personnalisés grâce à une ingénierie rapide.
Bien que les messages de chat soient une belle abstraction et permettent de garantir que vous transmettez au LLM le bon type de message, vous pouvez également transmettre des chaînes brutes dans les modèles de chat :
>>> chat_model.invoke("What is blood pressure?")
AIMessage(content='Blood pressure is the force exerted by
the blood against the walls of the blood vessels, particularly
the arteries, as it is pumped by the heart. It is measured in
millimeters of mercury (mmHg) and is typically expressed as two
numbers: systolic pressure over diastolic pressure. The systolic
pressure represents the force when the heart contracts and pumps
blood into the arteries, while the diastolic pressure represents
the force when the heart is at rest between beats. Blood pressure
is an important indicator of cardiovascular health and can be influenced
by various factors such as age, genetics, lifestyle, and underlying medical
conditions.')
Dans ce bloc de code, vous transmettez la chaîne Qu'est-ce que la tension artérielle ? directement à chat_model.invoke()
. Si vous souhaitez contrôler le comportement du LLM sans SystemMessage
ici, vous pouvez inclure des instructions dans l'entrée de chaîne.
Remarque : Dans ces exemples, vous avez utilisé .invoke()
, mais LangChain dispose d'autres méthodes qui interagissent avec les LLM. Par exemple, .stream()
renvoie la réponse un jeton à la fois, et .batch()
accepte une liste de messages auxquels le LLM répond en un seul appel.
Chaque méthode possède également une méthode asynchrone analogue. Par exemple, vous pouvez exécuter .invoke()
de manière asynchrone avec ainvoke()
.
Ensuite, vous apprendrez une manière modulaire de guider la réponse de votre modèle, comme vous l'avez fait avec le SystemMessage
, facilitant ainsi la personnalisation de votre chatbot.
Modèles d'invite
LangChain vous permet de concevoir des invites modulaires pour votre chatbot avec des modèles d'invite. En citant la documentation de LangChain, vous pouvez considérer les modèles d'invites comme des recettes prédéfinies pour générer des invites pour les modèles de langage.
Supposons que vous souhaitiez créer un chatbot qui répond aux questions sur les expériences des patients à partir de leurs avis. Voici à quoi pourrait ressembler un modèle d’invite pour cela :
>>> from langchain.prompts import ChatPromptTemplate
>>> review_template_str = """Your job is to use patient
... reviews to answer questions about their experience at a hospital.
... Use the following context to answer questions. Be as detailed
... as possible, but don't make up any information that's not
... from the context. If you don't know an answer, say you don't know.
...
... {context}
...
... {question}
... """
>>> review_template = ChatPromptTemplate.from_template(review_template_str)
>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"
>>> review_template.format(context=context, question=question)
"Human: Your job is to use patient\nreviews to answer questions about
their experience at a hospital.\nUse the following context to
answer questions. Be as detailed\nas possible, but don't make
up any information that's not\nfrom the context. If you don't
know an answer, say you don't know.\n\nI had a great
stay!\n\nDid anyone have a positive experience?\n"
Vous importez d'abord ChatPromptTemplate
et définissez review_template_str
, qui contient les instructions que vous transmettrez au modèle, ainsi que les variables context
et question
dans les champs de remplacement que LangChain délimite par des accolades ({}
). Vous créez ensuite un objet ChatPromptTemplate
à partir de review_template_str
en utilisant la méthode de classe .from_template()
.
Avec review_template
instancié, vous pouvez transmettre context
et question
dans le modèle de chaîne avec review_template.format()
. Les résultats peuvent donner l’impression que vous n’avez rien fait de plus qu’une interpolation de chaîne Python standard, mais les modèles d’invite disposent de nombreuses fonctionnalités utiles qui leur permettent de s’intégrer aux modèles de discussion.
Remarquez comment votre précédent appel à review_template.format()
a généré une chaîne avec Human au début. En effet, ChatPromptTemplate.from_template()
suppose que le modèle de chaîne est un message humain par défaut. Pour modifier cela, vous pouvez créer des modèles d'invites plus détaillés pour chaque message de discussion que vous souhaitez que le modèle traite :
>>> from langchain.prompts import (
... PromptTemplate,
... SystemMessagePromptTemplate,
... HumanMessagePromptTemplate,
... ChatPromptTemplate,
... )
>>> review_system_template_str = """Your job is to use patient
... reviews to answer questions about their experience at a
... hospital. Use the following context to answer questions.
... Be as detailed as possible, but don't make up any information
... that's not from the context. If you don't know an answer, say
... you don't know.
...
... {context}
... """
>>> review_system_prompt = SystemMessagePromptTemplate(
... prompt=PromptTemplate(
... input_variables=["context"], template=review_system_template_str
... )
... )
>>> review_human_prompt = HumanMessagePromptTemplate(
... prompt=PromptTemplate(
... input_variables=["question"], template="{question}"
... )
... )
>>> messages = [review_system_prompt, review_human_prompt]
>>> review_prompt_template = ChatPromptTemplate(
... input_variables=["context", "question"],
... messages=messages,
... )
>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"
>>> review_prompt_template.format_messages(context=context, question=question)
[SystemMessage(content="Your job is to use patient\nreviews to answer
questions about their experience at a\nhospital. Use the following context
to answer questions.\nBe as detailed as possible, but don't make up any
information\nthat's not from the context. If you don't know an answer, say
\nyou don't know.\n\nI had a great stay!\n"), HumanMessage(content='Did anyone
have a positive experience?')]
Dans ce bloc, vous importez des modèles d'invite distincts pour HumanMessage
et SystemMessage
. Vous définissez ensuite une chaîne, review_system_template_str
, qui sert de modèle pour un SystemMessage
. Remarquez comment vous déclarez uniquement une variable context
dans review_system_template_str
.
À partir de là, vous créez review_system_prompt
qui est un modèle d'invite spécifiquement pour SystemMessage
. Ensuite, vous créez un review_human_prompt
pour le HumanMessage
. Remarquez comment le paramètre template
n'est qu'une chaîne avec la variable question
.
Vous ajoutez ensuite review_system_prompt
et review_human_prompt
à une liste appelée messages
et créez review_prompt_template
, qui est l'objet final qui englobe les modèles d'invite pour SystemMessage
et HumanMessage
. L'appel de review_prompt_template.format_messages(context=context, question=question)
génère une liste avec un SystemMessage
et un HumanMessage
, qui peuvent être transmis à une discussion. modèle.
Pour voir comment combiner des modèles de discussion et des modèles d’invites, vous allez créer une chaîne avec le LangChain Expression Language (LCEL). Cela vous aide à débloquer la fonctionnalité de base de LangChain consistant à créer des interfaces modulaires personnalisées sur des modèles de discussion.
Chaînes et langage d'expression LangChain (LCEL)
Le ciment qui relie les modèles de discussion, les invites et autres objets dans LangChain est la chaîne. Une chaîne n'est rien de plus qu'une séquence d'appels entre objets dans LangChain. La méthode recommandée pour créer des chaînes consiste à utiliser le LangChain Expression Language (LCEL).
Pour voir comment cela fonctionne, découvrez comment créer une chaîne avec un modèle de discussion et un modèle d'invite :
import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
PromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate,
)
dotenv.load_dotenv()
review_template_str = """Your job is to use patient
reviews to answer questions about their experience at
a hospital. Use the following context to answer questions.
Be as detailed as possible, but don't make up any information
that's not from the context. If you don't know an answer, say
you don't know.
{context}
"""
review_system_prompt = SystemMessagePromptTemplate(
prompt=PromptTemplate(
input_variables=["context"],
template=review_template_str,
)
)
review_human_prompt = HumanMessagePromptTemplate(
prompt=PromptTemplate(
input_variables=["question"],
template="{question}",
)
)
messages = [review_system_prompt, review_human_prompt]
review_prompt_template = ChatPromptTemplate(
input_variables=["context", "question"],
messages=messages,
)
chat_model = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
review_chain = review_prompt_template | chat_model
Les lignes 1 à 42 correspondent à ce que vous avez déjà fait. À savoir, vous définissez review_prompt_template
qui est un modèle d'invite pour répondre aux questions sur les avis des patients, et vous instanciez un modèle de discussion gpt-3.5-turbo-0125
. À la ligne 44, vous définissez review_chain
avec le symbole |
, qui est utilisé pour enchaîner review_prompt_template
et chat_model
ensemble.
Cela crée un objet, review_chain
, qui peut transmettre des questions via review_prompt_template
et chat_model
en un seul appel de fonction. Essentiellement, cela supprime tous les détails internes de review_chain
, vous permettant d'interagir avec la chaîne comme s'il s'agissait d'un modèle de discussion.
Après avoir enregistré le chatbot.py
mis à jour, démarrez une nouvelle session REPL dans le dossier de votre projet de base. Voici comment utiliser review_chain
:
>>> from langchain_intro.chatbot import review_chain
>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"
>>> review_chain.invoke({"context": context, "question": question})
AIMessage(content='Yes, the patient had a great stay and had a
positive experience at the hospital.')
Dans ce bloc, vous importez review_chain
et définissez context
et question
comme auparavant. Vous passez ensuite un dictionnaire avec les clés context
et question
dans review_chan.invoke()
. Cela transmet context
et question
via le modèle d'invite et le modèle de discussion pour générer une réponse.
Remarque : lorsque vous appelez des chaînes, vous pouvez utiliser toutes les mêmes méthodes prises en charge par un modèle de chat.
En général, le LCEL vous permet de créer des chaînes de longueur arbitraire avec le symbole du tuyau (|
). Par exemple, si vous souhaitez formater la réponse du modèle, vous pouvez alors ajouter un analyseur de sortie à la chaîne :
import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
PromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate,
)
from langchain_core.output_parsers import StrOutputParser
# ...
output_parser = StrOutputParser()
review_chain = review_prompt_template | chat_model | output_parser
Ici, vous ajoutez une instance StrOutputParser()
à review_chain
, ce qui rendra la réponse du modèle plus lisible. Démarrez une nouvelle session REPL et essayez-la :
>>> from langchain_intro.chatbot import review_chain
>>> context = "I had a great stay!"
>>> question = "Did anyone have a positive experience?"
>>> review_chain.invoke({"context": context, "question": question})
'Yes, the patient had a great stay and had a
positive experience at the hospital.'
Ce bloc est le même qu'avant, sauf que vous pouvez maintenant voir que review_chain
renvoie une chaîne bien formatée plutôt qu'un AIMessage
.
Le pouvoir des chaînes réside dans la créativité et la flexibilité qu’elles vous offrent. Vous pouvez enchaîner des pipelines complexes pour créer votre chatbot, et vous vous retrouvez avec un objet qui exécute votre pipeline en un seul appel de méthode. Ensuite, vous superposerez un autre objet dans review_chain
pour récupérer des documents à partir d'une base de données vectorielle.
Objets de récupération
L'objectif de review_chain
est de répondre aux questions sur les expériences des patients à l'hôpital à partir de leurs avis. Jusqu'à présent, vous avez transmis manuellement les avis dans le contexte de la question. Bien que cela puisse fonctionner pour un petit nombre d’avis, cela ne s’adapte pas bien. De plus, même si vous pouvez insérer tous les avis dans la fenêtre contextuelle du modèle, rien ne garantit qu’il utilisera les bons avis pour répondre à une question.
Pour surmonter cela, vous avez besoin d'un retriever. Le processus de récupération des documents pertinents et de leur transmission à un modèle de langage pour répondre aux questions est connu sous le nom de génération augmentée par récupération (RAG).
Pour cet exemple, vous stockerez tous les avis dans une base de données vectorielle appelée ChromaDB. Si vous n'êtes pas familier avec cet outil et ces sujets de base de données, consultez Embeddings and Vector Databases avec ChromaDB avant de continuer.
Vous pouvez installer ChromaDB avec la commande suivante :
(venv) $ python -m pip install chromadb==0.4.22
Une fois installé, vous pouvez utiliser le code suivant pour créer une base de données vectorielles ChromaDB avec les avis des patients :
import dotenv
from langchain.document_loaders.csv_loader import CSVLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
REVIEWS_CSV_PATH = "data/reviews.csv"
REVIEWS_CHROMA_PATH = "chroma_data"
dotenv.load_dotenv()
loader = CSVLoader(file_path=REVIEWS_CSV_PATH, source_column="review")
reviews = loader.load()
reviews_vector_db = Chroma.from_documents(
reviews, OpenAIEmbeddings(), persist_directory=REVIEWS_CHROMA_PATH
)
Aux lignes 2 à 4, vous importez les dépendances nécessaires à la création de la base de données vectorielles. Vous définissez ensuite VIEW_CSV_PATH
et VIEW_CHROMA_PATH
, qui sont des chemins où les données brutes des avis sont stockées et où la base de données vectorielle stockera les données, respectivement.
Vous obtiendrez un aperçu des données du système hospitalier plus tard, mais tout ce que vous devez savoir pour l'instant est que reviews.csv
stocke les avis des patients. La colonne review
dans reviews.csv
est une chaîne avec l'avis du patient.
Aux lignes 11 et 12, vous chargez les avis à l'aide du CSVLoader
de LangChain. Aux lignes 14 à 16, vous créez une instance ChromaDB à partir de reviews
en utilisant le modèle d'intégration OpenAI par défaut, et vous stockez les intégrations d'avis dans VIEWS_CHROMA_PATH
.
Remarque : En pratique, si vous intégrez un document volumineux, vous devez utiliser un séparateur de texte. Les séparateurs de texte divisent le document en morceaux plus petits avant de les exécuter via un modèle d'intégration. Ceci est important car les modèles d’intégration ont une fenêtre contextuelle de taille fixe et, à mesure que la taille du texte augmente, la capacité d’une intégration à représenter avec précision le texte diminue.
Pour cet exemple, vous pouvez intégrer chaque avis individuellement car ils sont relativement petits.
Ensuite, ouvrez un terminal et exécutez la commande suivante depuis le répertoire du projet :
(venv) $ python langchain_intro/create_retriever.py
L'exécution ne devrait prendre qu'une minute environ, et vous pouvez ensuite commencer à effectuer une recherche sémantique sur les intégrations d'avis :
>>> import dotenv
>>> from langchain_community.vectorstores import Chroma
>>> from langchain_openai import OpenAIEmbeddings
>>> REVIEWS_CHROMA_PATH = "chroma_data/"
>>> dotenv.load_dotenv()
True
>>> reviews_vector_db = Chroma(
... persist_directory=REVIEWS_CHROMA_PATH,
... embedding_function=OpenAIEmbeddings(),
... )
>>> question = """Has anyone complained about
... communication with the hospital staff?"""
>>> relevant_docs = reviews_vector_db.similarity_search(question, k=3)
>>> relevant_docs[0].page_content
'review_id: 73\nvisit_id: 7696\nreview: I had a frustrating experience
at the hospital. The communication between the medical staff and me was
unclear, leading to misunderstandings about my treatment plan. Improvement
is needed in this area.\nphysician_name: Maria Thompson\nhospital_name:
Little-Spencer\npatient_name: Terri Smith'
>>> relevant_docs[1].page_content
'review_id: 521\nvisit_id: 631\nreview: I had a challenging time at the
hospital. The medical care was adequate, but the lack of communication
between the staff and me left me feeling frustrated and confused about my
treatment plan.\nphysician_name: Samantha Mendez\nhospital_name:
Richardson-Powell\npatient_name: Kurt Gordon'
>>> relevant_docs[2].page_content
'review_id: 785\nvisit_id: 2593\nreview: My stay at the hospital was challenging.
The medical care was adequate, but the lack of communication from the staff
created some frustration.\nphysician_name: Brittany Harris\nhospital_name:
Jones, Taylor and Garcia\npatient_name: Ryan Jacobs'
Vous importez les dépendances nécessaires pour appeler ChromaDB et spécifiez le chemin d'accès aux données ChromaDB stockées dans VIEWS_CHROMA_PATH
. Vous chargez ensuite les variables d'environnement à l'aide de dotenv.load_dotenv()
et créez une nouvelle instance Chroma
pointant vers votre base de données vectorielles. Remarquez comment vous devez à nouveau spécifier une fonction d'intégration lors de la connexion à votre base de données vectorielle. Assurez-vous qu'il s'agit de la même fonction d'intégration que celle que vous avez utilisée pour créer les intégrations.
Ensuite, vous définissez une question et appelez .similarity_search()
sur reviews_vector_db
, en transmettant question
et k=3
. Cela crée une intégration pour la question et recherche dans la base de données vectorielles les trois intégrations d'avis les plus similaires à l'intégration de questions. Dans ce cas, vous voyez trois avis dans lesquels des patients se plaignent de la communication, ce qui est exactement ce que vous avez demandé !
La dernière chose à faire est d'ajouter votre récupérateur d'avis à review_chain
afin que les avis pertinents soient transmis à l'invite en tant que contexte. Voici comment procéder :
import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
PromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate,
)
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.schema.runnable import RunnablePassthrough
REVIEWS_CHROMA_PATH = "chroma_data/"
# ...
reviews_vector_db = Chroma(
persist_directory=REVIEWS_CHROMA_PATH,
embedding_function=OpenAIEmbeddings()
)
reviews_retriever = reviews_vector_db.as_retriever(k=10)
review_chain = (
{"context": reviews_retriever, "question": RunnablePassthrough()}
| review_prompt_template
| chat_model
| StrOutputParser()
)
Comme auparavant, vous importez les dépendances de ChromaDB, spécifiez le chemin d'accès à vos données ChromaDB et instanciez un nouvel objet Chroma
. Vous créez ensuite reviews_retriever
en appelant .as_retriever()
sur reviews_vector_db
pour créer un objet retriever que vous ajouterez à review_chain
. Étant donné que vous avez spécifié k=10
, le récupérateur récupérera les dix avis les plus similaires à la question de l'utilisateur.
Vous ajoutez ensuite un dictionnaire avec les clés context
et question
au début de review_chain
. Au lieu de transmettre manuellement context
, review_chain
transmettra votre question au récupérateur pour qu'il extraie les avis pertinents. L'attribution de question
à un objet RunnablePassthrough
garantit que la question est transmise inchangée à l'étape suivante de la chaîne.
Vous disposez désormais d’une chaîne entièrement fonctionnelle qui peut répondre aux questions sur les expériences des patients à partir de leurs avis. Démarrez une nouvelle session REPL et essayez-la :
>>> from langchain_intro.chatbot import review_chain
>>> question = """Has anyone complained about
... communication with the hospital staff?"""
>>> review_chain.invoke(question)
'Yes, several patients have complained about communication
with the hospital staff. Terri Smith mentioned that the
communication between the medical staff and her was unclear,
leading to misunderstandings about her treatment plan.
Kurt Gordon also mentioned that the lack of communication
between the staff and him left him feeling frustrated and
confused about his treatment plan. Ryan Jacobs also experienced
frustration due to the lack of communication from the staff.
Shannon Williams also mentioned that the lack of communication
between the staff and her made her stay at the hospital less enjoyable.'
Comme vous pouvez le voir, vous appelez uniquement review_chain.invoke(question)
pour obtenir des réponses améliorées par récupération sur les expériences des patients à partir de leurs avis. Vous améliorerez cette chaîne plus tard en stockant les intégrations d'avis, ainsi que d'autres métadonnées, dans Neo4j.
Maintenant que vous comprenez les modèles de discussion, les invites, les chaînes et la récupération, vous êtes prêt à vous plonger dans le dernier concept de LangChain : les agents.
Agents
Jusqu’à présent, vous avez créé une chaîne pour répondre aux questions à l’aide des avis des patients. Et si vous souhaitiez que votre chatbot réponde également à des questions sur d’autres données hospitalières, comme les temps d’attente à l’hôpital ? Idéalement, votre chatbot peut basculer de manière transparente entre les réponses aux questions d’évaluation des patients et aux questions sur les temps d’attente en fonction de la requête de l’utilisateur. Pour ce faire, vous aurez besoin des composants suivants :
- La chaîne d'avis des patients que vous avez déjà créée
- Une fonction qui permet de rechercher les temps d'attente dans un hôpital
- Un moyen pour un LLM de savoir quand il doit répondre aux questions sur les expériences des patients ou rechercher les temps d'attente
Pour réaliser la troisième capacité, vous avez besoin d’un agent.
Un agent est un modèle de langage qui décide d'une séquence d'actions à exécuter. Contrairement aux chaînes où la séquence d’actions est codée en dur, les agents utilisent un modèle de langage pour déterminer quelles actions entreprendre et dans quel ordre.
Avant de créer l'agent, créez la fonction suivante pour générer de faux temps d'attente pour un hôpital :
import random
import time
def get_current_wait_time(hospital: str) -> int | str:
"""Dummy function to generate fake wait times"""
if hospital not in ["A", "B", "C", "D"]:
return f"Hospital {hospital} does not exist"
# Simulate API call delay
time.sleep(1)
return random.randint(0, 10000)
Dans get_current_wait_time()
, vous transmettez le nom d'un hôpital, vérifiez s'il est valide, puis générez un nombre aléatoire pour simuler un temps d'attente. En réalité, il s'agirait d'une sorte de requête de base de données ou d'appel API, mais cela servira le même objectif pour cette démonstration.
Vous pouvez maintenant créer un agent qui décide entre get_current_wait_time()
et review_chain.invoke()
en fonction de la question :
import dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import (
PromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate,
)
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain.schema.runnable import RunnablePassthrough
from langchain.agents import (
create_openai_functions_agent,
Tool,
AgentExecutor,
)
from langchain import hub
from langchain_intro.tools import get_current_wait_time
# ...
tools = [
Tool(
name="Reviews",
func=review_chain.invoke,
description="""Useful when you need to answer questions
about patient reviews or experiences at the hospital.
Not useful for answering questions about specific visit
details such as payer, billing, treatment, diagnosis,
chief complaint, hospital, or physician information.
Pass the entire question as input to the tool. For instance,
if the question is "What do patients think about the triage system?",
the input should be "What do patients think about the triage system?"
""",
),
Tool(
name="Waits",
func=get_current_wait_time,
description="""Use when asked about current wait times
at a specific hospital. This tool can only get the current
wait time at a hospital and does not have any information about
aggregate or historical wait times. This tool returns wait times in
minutes. Do not pass the word "hospital" as input,
only the hospital name itself. For instance, if the question is
"What is the wait time at hospital A?", the input should be "A".
""",
),
]
hospital_agent_prompt = hub.pull("hwchase17/openai-functions-agent")
agent_chat_model = ChatOpenAI(
model="gpt-3.5-turbo-1106",
temperature=0,
)
hospital_agent = create_openai_functions_agent(
llm=agent_chat_model,
prompt=hospital_agent_prompt,
tools=tools,
)
hospital_agent_executor = AgentExecutor(
agent=hospital_agent,
tools=tools,
return_intermediate_steps=True,
verbose=True,
)
Dans ce bloc, vous importez quelques dépendances supplémentaires dont vous aurez besoin pour créer l’agent. Vous définissez ensuite une liste d'objets Tool
. Un Outil
est une interface qu'un agent utilise pour interagir avec une fonction. Par exemple, le premier outil s'appelle Reviews
et il appelle review_chain.invoke()
si la question répond aux critères de description
.
Remarquez comment description
donne à l'agent des instructions quant au moment où il doit appeler l'outil. C'est là que de bonnes compétences en ingénierie rapide sont primordiales pour garantir que le LLM appelle le bon outil avec les bonnes entrées.
Le deuxième Tool
dans tools
est nommé Waits
et il appelle get_current_wait_time()
. Encore une fois, l'agent doit savoir quand utiliser l'outil Waits
et quelles entrées lui transmettre en fonction de la description
.
Ensuite, vous initialisez un objet ChatOpenAI
en utilisant gpt-3.5-turbo-1106 comme modèle de langage. Vous créez ensuite un agent de fonctions OpenAI avec create_openai_functions_agent()
. Cela crée un agent conçu pour transmettre des entrées aux fonctions. Pour ce faire, il renvoie des objets JSON valides qui stockent les entrées de fonction et leur valeur correspondante.
Pour créer l'environnement d'exécution de l'agent, vous transmettez l'agent et les outils dans AgentExecutor
. Définir return_intermediate_steps
et verbose
sur True
vous permettra de voir le processus de réflexion de l'agent et les outils qu'il appelle.
Démarrez une nouvelle session REPL pour donner un tour à votre nouvel agent :
>>> from langchain_intro.chatbot import hospital_agent_executor
>>> hospital_agent_executor.invoke(
... {"input": "What is the current wait time at hospital C?"}
... )
> Entering new AgentExecutor chain...
Invoking: `Waits` with `C`
1374The current wait time at Hospital C is 1374 minutes.
> Finished chain.
{'input': 'What is the current wait time at hospital C?',
'output': 'The current wait time at Hospital C is 1374 minutes.',
'intermediate_steps': [(AgentActionMessageLog(tool='Waits',
tool_input='C', log='\nInvoking: `Waits` with `C`\n\n\n',
message_log=[AIMessage(content='', additional_kwargs={'function_call':
{'arguments': '{"__arg1":"C"}', 'name': 'Waits'}})]), 1374)]}
>>> hospital_agent_executor.invoke(
... {"input": "What have patients said about their comfort at the hospital?"}
... )
> Entering new AgentExecutor chain...
Invoking: `Reviews` with `What have patients said about their comfort at the
hospital?`
Patients have mentioned both positive and negative aspects of their comfort at
the hospital. One patient mentioned that the hospital's dedication to patient
comfort was evident in the well-designed private rooms and comfortable furnishings,
which made their recovery more bearable and contributed to an overall positive
experience. However, other patients mentioned that the uncomfortable beds made
it difficult for them to get a good night's sleep during their stay, affecting
their overall comfort. Another patient mentioned that the outdated and
uncomfortable beds affected their overall comfort, despite the doctors being
knowledgeable and the hospital having a clean environment. Patients have shared
mixed feedback about their comfort at the hospital. Some have praised the well-designed
private rooms and comfortable furnishings, which contributed to a positive experience.
However, others have mentioned discomfort due to the outdated and uncomfortable beds,
affecting their overall comfort despite the hospital's clean environment and knowledgeable
doctors.
> Finished chain.
{'input': 'What have patients said about their comfort at the hospital?', 'output':
"Patients have shared mixed feedback about their comfort at the hospital. Some have
praised the well-designed private rooms and comfortable furnishings, which contributed
to a positive experience. However, others have mentioned discomfort due to the outdated
and uncomfortable beds, affecting their overall comfort despite the hospital's clean
environment and knowledgeable doctors.", 'intermediate_steps':
[(AgentActionMessageLog(tool='Reviews', tool_input='What have patients said about their
comfort at the hospital?', log='\nInvoking: `Reviews` with `What have patients said about
their comfort at the hospital?`\n\n\n', message_log=[AIMessage(content='',
additional_kwargs={'function_call': {'arguments': '{"__arg1":"What have patients said about
their comfort at the hospital?"}', 'name': 'Reviews'}})]), "Patients have mentioned both
positive and negative aspects of their comfort at the hospital. One patient mentioned that
the hospital's dedication to patient comfort was evident in the well-designed private rooms
and comfortable furnishings, which made their recovery more bearable and contributed to an
overall positive experience. However, other patients mentioned that the uncomfortable beds
made it difficult for them to get a good night's sleep during their stay, affecting their
overall comfort. Another patient mentioned that the outdated and uncomfortable beds affected
their overall comfort, despite the doctors being knowledgeable and the hospital having a clean
environment.")]}
Vous importez d'abord l'agent, puis appelez hospital_agent_executor.invoke()
avec une question sur le temps d'attente. Comme indiqué dans le résultat, l'agent sait que vous demandez un temps d'attente et il transmet C
en entrée à l'outil Waits
. L'outil Waits
appelle ensuite get_current_wait_time(hospital="C")
et renvoie le temps d'attente correspondant à l'agent. L'agent utilise ensuite ce temps d'attente pour générer sa sortie finale.
Un processus similaire se produit lorsque vous interrogez l'agent sur les évaluations de l'expérience des patients, sauf que cette fois, l'agent sait qu'il doit appeler l'outil Avis
avec Qu'ont dit les patients à propos de leur confort au hôpital ? comme entrée. L'outil Reviews
exécute review_chain.invoke()
en utilisant votre question complète comme entrée, et l'agent utilise la réponse pour générer sa sortie.
C’est une capacité profonde. Les agents donnent aux modèles de langage la possibilité d'effectuer à peu près n'importe quelle tâche pour laquelle vous pouvez écrire du code. Imaginez tous les chatbots étonnants et potentiellement dangereux que vous pourriez créer avec des agents.
Vous disposez désormais de toutes les connaissances LangChain préalables nécessaires pour créer un chatbot personnalisé. Ensuite, vous enfilerez votre casquette d’ingénieur en IA et découvrirez les exigences commerciales et les données nécessaires à la création de votre chatbot de système hospitalier.
Tout le code que vous avez écrit jusqu’à présent était destiné à vous enseigner les principes fondamentaux de LangChain, et il ne sera pas inclus dans votre chatbot final. N'hésitez pas à commencer avec un répertoire vide à l'étape 2, où vous commencerez à créer votre chatbot.
Étape 2 : Comprendre les exigences commerciales et les données
Avant de commencer à travailler sur un projet d’IA, vous devez comprendre le problème que vous souhaitez résoudre et élaborer un plan sur la manière dont vous allez le résoudre. Cela implique de définir clairement le problème, de rassembler les exigences, de comprendre les données et la technologie dont vous disposez et de définir des attentes claires avec les parties prenantes. Pour ce projet, vous commencerez par définir le problème et rassembler les exigences commerciales de votre chatbot.
Comprendre le problème et les exigences
Imaginez que vous êtes un ingénieur en IA travaillant pour un grand système hospitalier aux États-Unis. Vos parties prenantes aimeraient avoir plus de visibilité sur les données en constante évolution qu’elles collectent. Ils veulent des réponses à des questions ponctuelles sur les patients, les visites, les médecins, les hôpitaux et les assureurs sans avoir à comprendre un langage de requête comme SQL, à demander un rapport à un analyste ou à attendre que quelqu'un construise un tableau de bord.
Pour ce faire, vos parties prenantes souhaitent un outil de chatbot interne, similaire à ChatGPT, capable de répondre aux questions sur les données de votre entreprise. Après une réunion pour recueillir les exigences, vous recevez une liste des types de questions auxquelles votre chatbot doit répondre :
- Quel est le temps d’attente actuel à l’hôpital XYZ ?
- Quel hôpital a actuellement le temps d’attente le plus court ?
- Dans quels hôpitaux les patients se plaignent-ils de problèmes de facturation et d’assurance ?
- Des patients se sont-ils plaints de l’insalubrité de l’hôpital ?
- Qu’ont dit les patients sur la façon dont les médecins et les infirmières communiquent avec eux ?
- Que disent les patients du personnel infirmier de l’hôpital XYZ ?
- Quel a été le montant total facturé aux payeurs de Cigna en 2023 ?
- Combien de patients le Dr John Doe a-t-il traités ?
- Combien de visites sont ouvertes et quelle est leur durée moyenne en jours ?
- Quel médecin a la durée moyenne de visite en jours la plus courte ?
- Quel montant a été facturé pour le séjour du patient 789 ?
- Quel hôpital a travaillé avec le plus de patients Cigna en 2023 ?
- Quel est le montant moyen facturé pour les visites aux urgences par hôpital ?
- Quel État a connu le pourcentage d’augmentation le plus important des visites inedicaid entre 2022 et 2023 ?
Vous pouvez répondre à des questions telles que Quel était le montant total facturé aux payeurs de Cigna en 2023 ? avec des statistiques globales à l'aide d'un langage de requête tel que SQL. Fondamentalement, ces questions ont une seule réponse objective. Vous pouvez exécuter des requêtes prédéfinies pour y répondre, mais chaque fois qu'une partie prenante a une question nouvelle ou légèrement nuancée, vous devez rédiger une nouvelle requête. Pour éviter cela, votre chatbot doit générer dynamiquement des requêtes précises.
Des questions telles que Des patients se sont-ils plaints de l'impureté de l'hôpital ? ou Qu'ont dit les patients sur la façon dont les médecins et les infirmières communiquent avec eux ? sont plus subjectives et peuvent avoir de nombreuses réponses acceptables. Votre chatbot devra lire des documents, tels que les avis des patients, pour répondre à ce type de questions.
En fin de compte, vos parties prenantes souhaitent une interface de discussion unique capable de répondre de manière transparente aux questions subjectives et objectives. Cela signifie que lorsqu’on lui présente une question, votre chatbot doit savoir quel type de question est posé et de quelle source de données extraire.
Par exemple, si on lui demande Combien a été facturé pour le séjour du patient 789 ?, votre chatbot doit savoir qu'il doit interroger une base de données pour trouver la réponse. Si on vous demande Qu'ont dit les patients sur la façon dont les médecins et les infirmières communiquent avec eux ?, votre chatbot doit savoir qu'il doit lire et résumer les avis des patients.
Ensuite, vous explorerez les données enregistrées par votre système hospitalier, ce qui est sans doute la condition préalable la plus importante à la création de votre chatbot.
Explorez les données disponibles
Avant de créer votre chatbot, vous devez bien comprendre les données qu’il utilisera pour répondre aux requêtes des utilisateurs. Cela vous aidera à déterminer ce qui est réalisable et comment vous souhaitez structurer les données afin que votre chatbot puisse y accéder facilement. Toutes les données que vous utiliserez dans cet article ont été générées de manière synthétique et une grande partie est dérivée d'un ensemble de données de soins de santé populaire sur Kaggle.
En pratique, les ensembles de données suivants seront probablement stockés sous forme de tables dans une base de données SQL, mais vous travaillerez avec des fichiers CSV pour vous concentrer sur la création du chatbot. Cette section vous donnera une description détaillée de chaque fichier CSV.
Vous devrez placer tous les fichiers CSV qui font partie de ce projet dans votre dossier data/
avant de continuer le didacticiel. Assurez-vous de les avoir téléchargés à partir des documents et de les avoir placés dans votre dossier data/
:
hôpitaux.csv
Le fichier hospitals.csv
enregistre des informations sur chaque hôpital géré par votre entreprise. Il y a 30 hôpitaux et trois champs dans ce fichier :
hospital_id
: nombre entier qui identifie de manière unique un hôpital.hospital_name
: le nom de l'hôpital.hospital_state
: l'état dans lequel se trouve l'hôpital.
Si vous êtes familier avec les bases de données SQL traditionnelles et le schéma en étoile, vous pouvez considérer hospitals.csv
comme une table de dimensions. Les tables de dimension sont relativement courtes et contiennent des informations descriptives ou des attributs qui fournissent un contexte aux données dans les tables de faits. Les tables de faits enregistrent les événements concernant les entités stockées dans les tables de dimensions et ont tendance à être des tables plus longues.
Dans ce cas, hospitals.csv
enregistre des informations spécifiques aux hôpitaux, mais vous pouvez les joindre à des tableaux de faits pour répondre aux questions sur les patients, les médecins et les payeurs liés à l'hôpital. Cela sera plus clair lorsque vous explorerez visits.csv
.
Si vous êtes curieux, vous pouvez inspecter les premières lignes de hospitals.csv
à l'aide d'une bibliothèque de trames de données comme Polars. Assurez-vous que Polars est installé dans votre environnement virtuel et exécutez le code suivant :
>>> import polars as pl
>>> HOSPITAL_DATA_PATH = "data/hospitals.csv"
>>> data_hospitals = pl.read_csv(HOSPITAL_DATA_PATH)
>>> data_hospitals.shape
(30, 3)
>>> data_hospitals.head()
shape: (5, 3)
┌─────────────┬───────────────────────────┬────────────────┐
│ hospital_id ┆ hospital_name ┆ hospital_state │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ str │
╞═════════════╪═══════════════════════════╪════════════════╡
│ 0 ┆ Wallace-Hamilton ┆ CO │
│ 1 ┆ Burke, Griffin and Cooper ┆ NC │
│ 2 ┆ Walton LLC ┆ FL │
│ 3 ┆ Garcia Ltd ┆ NC │
│ 4 ┆ Jones, Brown and Murray ┆ NC │
└─────────────┴───────────────────────────┴────────────────┘
Dans ce bloc de code, vous importez des Polars, définissez le chemin vers hospitals.csv
, lisez les données dans un Polars DataFrame, affichez la forme des données et affichez les 5 premières lignes. Cela vous montre, par exemple, que l'hôpital Walton, LLC a un identifiant 2 et est situé dans l'État de Floride, FL.
médecins.csv
Le fichier physicians.csv
contient des données sur les médecins qui travaillent pour votre système hospitalier. Cet ensemble de données comporte les champs suivants :
physician_id
: nombre entier qui identifie de manière unique chaque médecin.physician_name
: le nom du médecin.physician_dob
: date de naissance du médecin.physician_grad_year
: année où le médecin a obtenu son diplôme de médecine.medical_school
: lieu où le médecin a fréquenté une école de médecine.salaire
: Le salaire du médecin.
Ces données peuvent à nouveau être considérées comme une table de dimensions, et vous pouvez inspecter les premières lignes à l'aide de Polars :
>>> PHYSICIAN_DATA_PATH = "data/physicians.csv"
>>> data_physician = pl.read_csv(PHYSICIAN_DATA_PATH)
>>> data_physician.shape
(500, 6)
>>> data_physician.head()
shape: (5, 6)
┌──────────────────┬──────────────┬───────────────┬─────────────────────┬───────────────────────────────────┬───────────────┐
│ physician_name ┆ physician_id ┆ physician_dob ┆ physician_grad_year ┆ medical_school ┆ salary │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ str ┆ str ┆ str ┆ f64 │
╞══════════════════╪══════════════╪═══════════════╪═════════════════════╪═══════════════════════════════════╪═══════════════╡
│ Joseph Johnson ┆ 0 ┆ 1970-02-22 ┆ 2000-02-22 ┆ Johns Hopkins University School … ┆ 309534.155076 │
│ Jason Williams ┆ 1 ┆ 1982-12-22 ┆ 2012-12-22 ┆ Mayo Clinic Alix School of Medic… ┆ 281114.503559 │
│ Jesse Gordon ┆ 2 ┆ 1959-06-03 ┆ 1989-06-03 ┆ David Geffen School of Medicine … ┆ 305845.584636 │
│ Heather Smith ┆ 3 ┆ 1965-06-15 ┆ 1995-06-15 ┆ NYU Grossman Medical School ┆ 295239.766689 │
│ Kayla Hunter DDS ┆ 4 ┆ 1978-10-19 ┆ 2008-10-19 ┆ David Geffen School of Medicine … ┆ 298751.355201 │
└──────────────────┴──────────────┴───────────────┴─────────────────────┴───────────────────────────────────┴───────────────┘
Comme vous pouvez le voir sur le bloc de code, il y a 500 médecins dans physicians.csv
. Les premières lignes de physicians.csv
vous donnent une idée de ce à quoi ressemblent les données. Par exemple, Heather Smith a un numéro de médecin de 3, est née le 15 juin 1965, a obtenu son diplôme de médecine le 15 juin 1995, a fréquenté la NYU Grossman Medical School et son salaire est d'environ 295 239 $.
payeurs.csv
Le fichier suivant, payers.csv
, enregistre des informations sur les compagnies d'assurance que vos hôpitaux facturent pour les visites des patients. Semblable à hospitals.csv
, il s'agit d'un petit fichier avec quelques champs :
payer_id
: un nombre entier qui identifie de manière unique chaque payeur.payer_name
: le nom de l'entreprise du payeur.
Les cinq seuls payeurs figurant dans les données sont Medicaid, UnitedHealthcare, Aetna, Cigna et Blue Cross. . Vos parties prenantes sont très intéressées par l'activité des payeurs, donc payers.csv
sera utile une fois connecté aux patients, aux hôpitaux et aux médecins.
avis.csv
Le fichier reviews.csv
contient les avis des patients sur leur expérience à l'hôpital. Il contient ces champs :
review_id
: nombre entier qui identifie de manière unique un avis.visit_id
: nombre entier qui identifie la visite du patient sur laquelle portait l'avis.révision
: il s'agit de la révision sous forme de texte libre laissée par le patient.physician_name
: Le nom du médecin qui a traité le patient.hospital_name
: l'hôpital où le patient a séjourné.patient_name
: le nom du patient.
Cet ensemble de données est le premier que vous voyez qui contient le champ de texte libre avis, et votre chatbot doit l'utiliser pour répondre aux questions sur les détails de l'avis et les expériences des patients.
Voici à quoi ressemble reviews.csv
:
>>> REVIEWS_DATA_PATH = "data/reviews.csv"
>>> data_reviews = pl.read_csv(REVIEWS_DATA_PATH)
>>> data_reviews.shape
(1005, 6)
>>> data_reviews.head()
shape: (5, 6)
┌───────────┬──────────┬───────────────────────────────────┬─────────────────────┬──────────────────┬──────────────────┐
│ review_id ┆ visit_id ┆ review ┆ physician_name ┆ hospital_name ┆ patient_name │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ str ┆ str ┆ str ┆ str │
╞═══════════╪══════════╪═══════════════════════════════════╪═════════════════════╪══════════════════╪══════════════════╡
│ 0 ┆ 6997 ┆ The medical staff at the hospita… ┆ Laura Brown ┆ Wallace-Hamilton ┆ Christy Johnson │
│ 9 ┆ 8138 ┆ The hospital's commitment to pat… ┆ Steven Watson ┆ Wallace-Hamilton ┆ Anna Frazier │
│ 11 ┆ 680 ┆ The hospital's commitment to pat… ┆ Chase Mcpherson Jr. ┆ Wallace-Hamilton ┆ Abigail Mitchell │
│ 892 ┆ 9846 ┆ I had a positive experience over… ┆ Jason Martinez ┆ Wallace-Hamilton ┆ Kimberly Rivas │
│ 822 ┆ 7397 ┆ The medical team at the hospital… ┆ Chelsey Davis ┆ Wallace-Hamilton ┆ Catherine Yang │
└───────────┴──────────┴───────────────────────────────────┴─────────────────────┴──────────────────┴──────────────────┘
Il y a 1 005 avis dans cet ensemble de données et vous pouvez voir comment chaque avis est lié à une visite. Par exemple, l’avis portant l’ID 9 correspond à la visite ID 8138, et les premiers mots sont « L’engagement de l’hôpital à pat… ». Vous vous demandez peut-être comment connecter un examen à un patient, ou plus généralement, comment connecter tous les ensembles de données décrits jusqu'à présent les uns aux autres. C'est là qu'intervient visits.csv
.
visites.csv
Le dernier fichier, visits.csv
, enregistre les détails de chaque visite à l'hôpital effectuée par votre entreprise. En poursuivant l'analogie du schéma en étoile, vous pouvez considérer visits.csv
comme une table de faits qui relie les hôpitaux, les médecins, les patients et les payeurs. Voici les champs :
visit_id
: l'identifiant unique d'une visite à l'hôpital.patient_id
: l'identifiant du patient associé à la visite.date_of_admission
: date à laquelle le patient a été admis à l'hôpital.room_number
: Le numéro de chambre du patient.admission_type
: un parmi « Électif », « Urgence » ou « Urgent ».chief_complaint
: chaîne décrivant la principale raison pour laquelle le patient est à l'hôpital.primary_diagnosis
: une chaîne décrivant le diagnostic principal posé par le médecin.treatment_description
: un résumé textuel du traitement administré par le médecin.test_results
: un parmi « Non concluant », « Normal » ou « Anormal ».discharge_date
: date à laquelle le patient est sorti de l'hôpitalphysician_id
: l'identifiant du médecin qui a traité le patient.hospital_id
: l'identifiant de l'hôpital où le patient a séjourné.payer_id
: l'identifiant du payeur d'assurance utilisé par le patient.billing_amount
: le montant facturé au payeur pour la visite.visit_status
: un de « OUVERT » ou « DÉCHARGEÉ ».
Cet ensemble de données vous donne tout ce dont vous avez besoin pour répondre aux questions sur la relation entre chaque entité hospitalière. Par exemple, si vous connaissez l'identifiant d'un médecin, vous pouvez utiliser visits.csv
pour déterminer à quels patients, payeurs et hôpitaux le médecin est associé. Jetez un œil à ce à quoi ressemble visits.csv
dans Polars :
>>> VISITS_DATA_PATH = "data/visits.csv"
>>> data_visits = pl.read_csv(VISITS_DATA_PATH)
>>> data_visits.shape
(9998, 15)
>>> data_visits.head()
shape: (5, 15)
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ patient ┆ date_of ┆ billing ┆ room_nu ┆ admissi ┆ dischar ┆ test_r ┆ visit_ ┆ physic ┆ payer_ ┆ hospit ┆ chief_ ┆ treatm ┆ primar ┆ visit_ │
│ _id ┆ _admiss ┆ _amount ┆ mber ┆ on_type ┆ ge_date ┆ esults ┆ id ┆ ian_id ┆ id ┆ al_id ┆ compla ┆ ent_de ┆ y_diag ┆ status │
│ --- ┆ ion ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ int ┆ script ┆ nosis ┆ --- │
│ i64 ┆ --- ┆ f64 ┆ i64 ┆ str ┆ str ┆ str ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ --- ┆ ion ┆ --- ┆ str │
│ ┆ str ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ str ┆ --- ┆ str ┆ │
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ str ┆ ┆ │
╞═════════╪═════════╪═════════╪═════════╪═════════╪═════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╪════════╡
│ 0 ┆ 2022-11 ┆ 37490.9 ┆ 146 ┆ Electiv ┆ 2022-12 ┆ Inconc ┆ 0 ┆ 102 ┆ 1 ┆ 0 ┆ null ┆ null ┆ null ┆ DISCHA │
│ ┆ -17 ┆ 83364 ┆ ┆ e ┆ -01 ┆ lusive ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ RGED │
│ 1 ┆ 2023-06 ┆ 47304.0 ┆ 404 ┆ Emergen ┆ null ┆ Normal ┆ 1 ┆ 435 ┆ 4 ┆ 5 ┆ null ┆ null ┆ null ┆ OPEN │
│ ┆ -01 ┆ 64845 ┆ ┆ cy ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │
│ 2 ┆ 2019-01 ┆ 36874.8 ┆ 292 ┆ Emergen ┆ 2019-02 ┆ Normal ┆ 2 ┆ 348 ┆ 2 ┆ 6 ┆ null ┆ null ┆ null ┆ DISCHA │
│ ┆ -09 ┆ 96997 ┆ ┆ cy ┆ -08 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ RGED │
│ 3 ┆ 2020-05 ┆ 23303.3 ┆ 480 ┆ Urgent ┆ 2020-05 ┆ Abnorm ┆ 3 ┆ 270 ┆ 4 ┆ 15 ┆ null ┆ null ┆ null ┆ DISCHA │
│ ┆ -02 ┆ 22092 ┆ ┆ ┆ -03 ┆ al ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ RGED │
│ 4 ┆ 2021-07 ┆ 18086.3 ┆ 477 ┆ Urgent ┆ 2021-08 ┆ Normal ┆ 4 ┆ 106 ┆ 2 ┆ 29 ┆ Persis ┆ Prescr ┆ J45.90 ┆ DISCHA │
│ ┆ -09 ┆ 44184 ┆ ┆ ┆ -02 ┆ ┆ ┆ ┆ ┆ ┆ tent ┆ ibed a ┆ 9 - ┆ RGED │
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ cough ┆ combin ┆ Unspec ┆ │
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ and ┆ ation ┆ ified ┆ │
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ shortn ┆ of ┆ asthma ┆ │
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ess o… ┆ inha… ┆ , un… ┆ │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
Vous pouvez voir qu'il y a 9998 visites enregistrées avec les 15 champs décrits ci-dessus. Notez que chief_complaint
, treatment_description
et primary_diagnosis
peuvent être manquants pour une visite. Vous devrez garder cela à l’esprit, car vos parties prenantes ne savent peut-être pas que de nombreuses visites manquent de données critiques – cela peut être une information précieuse en soi ! Notez enfin que lorsqu'une visite est encore ouverte, la discharged_date
sera manquante.
Vous comprenez désormais les données que vous utiliserez pour créer le chatbot souhaité par vos parties prenantes. Pour récapituler, les fichiers sont répartis pour simuler à quoi pourrait ressembler une base de données SQL traditionnelle. Chaque hôpital, patient, médecin, examen et payeur sont connectés via visits.csv
.
Temps d'attente
Vous avez peut-être remarqué qu'il n'existe aucune donnée permettant de répondre à des questions telles que Quel est le temps d'attente actuel à l'hôpital XYZ ? ou Quel hôpital a actuellement le temps d'attente le plus court ?. Malheureusement, le système hospitalier n’enregistre pas l’historique des temps d’attente. Votre chatbot devra appeler une API pour obtenir les informations actuelles sur les temps d'attente. Vous verrez comment cela fonctionne plus tard.
Avec une compréhension des exigences commerciales, des données disponibles et des fonctionnalités de LangChain, vous pouvez créer une conception pour votre chatbot.
Concevoir le chatbot
Maintenant que vous connaissez les exigences commerciales, les données et les prérequis de LangChain, vous êtes prêt à concevoir votre chatbot. Une bonne conception vous donne, à vous et aux autres, une compréhension conceptuelle des composants nécessaires à la création de votre chatbot. Votre conception doit clairement illustrer la manière dont les données circulent via votre chatbot et doit servir de référence utile pendant le développement.
Votre chatbot utilisera plusieurs outils pour répondre à diverses questions sur votre système hospitalier. Voici un organigramme illustrant comment vous y parviendrez :
Cet organigramme illustre comment les données circulent dans votre chatbot, depuis la requête d'entrée de l'utilisateur jusqu'à la réponse finale. Voici un résumé de chaque composant :
- Agent LangChain : L'agent LangChain est le cerveau de votre chatbot. Face à une requête utilisateur, l'agent décide quel outil appeler et quoi donner à l'outil en entrée. L’agent observe ensuite le résultat de l’outil et décide ce qu’il doit renvoyer à l’utilisateur : c’est la réponse de l’agent.
- Neo4j AuraDB : vous stockerez à la fois les données structurées du système hospitalier et les avis des patients dans une base de données graphique Neo4j AuraDB. Vous apprendrez tout cela dans la section suivante.
- LangChain Neo4j Cypher Chain : cette chaîne tente de convertir la requête utilisateur en Cypher, le langage de requête de Neo4j, et d'exécuter la requête Cypher dans Neo4j. La chaîne répond ensuite à la requête de l'utilisateur en utilisant les résultats de la requête Cypher. La réponse de la chaîne est renvoyée à l'agent LangChain et envoyée à l'utilisateur.
- Chaîne vectorielle d'avis LangChain Neo4j : elle est très similaire à la chaîne que vous avez créée à l'étape 1, sauf que les intégrations d'avis de patients sont désormais stockées dans Neo4j. La chaîne recherche les avis pertinents en fonction de ceux sémantiquement similaires à la requête de l'utilisateur, et les avis sont utilisés pour répondre à la requête de l'utilisateur.
- Fonction Temps d'attente : Semblable à la logique de l'étape 1, l'agent LangChain tente d'extraire un nom d'hôpital de la requête de l'utilisateur. Le nom de l'hôpital est transmis en entrée à une fonction Python qui obtient les temps d'attente, et le temps d'attente est renvoyé à l'agent.
Pour parcourir un exemple, supposons qu'un utilisateur demande Combien de visites d'urgence y a-t-il eu en 2023 ? L'agent LangChain recevra cette question et décidera à quel outil, le cas échéant, transmettre la question. Dans ce cas, l'agent doit transmettre la question à la LangChain Neo4j Cypher Chain. La chaîne tentera de convertir la question en requête Cypher, exécutera la requête Cypher dans Neo4j et utilisera les résultats de la requête pour répondre à la question.
Une fois que la chaîne de chiffrement LangChain Neo4j répond à la question, elle renverra la réponse à l'agent, et l'agent transmettra la réponse à l'utilisateur.
Avec cette conception en tête, vous pouvez commencer à créer votre chatbot. Votre première tâche consiste à configurer une instance Neo4j AuraDB à laquelle votre chatbot pourra accéder.
Étape 3 : Configurer une base de données graphique Neo4j
Comme vous l'avez vu à l'étape 2, les données de votre système hospitalier sont actuellement stockées dans des fichiers CSV. Avant de créer votre chatbot, vous devez stocker ces données dans une base de données que votre chatbot peut interroger. Vous utiliserez Neo4j AuraDB pour cela.
Avant d'apprendre à configurer une instance Neo4j AuraDB, vous aurez un aperçu des bases de données graphiques et vous comprendrez pourquoi l'utilisation d'une base de données graphique peut être un meilleur choix qu'une base de données relationnelle pour ce projet.
Un bref aperçu des bases de données graphiques
Les bases de données graphiques, telles que Neo4j, sont des bases de données conçues pour représenter et traiter les données stockées sous forme de graphique. Les données graphiques sont constituées de nœuds, de arêtes ou de relations et de propriétés. Les nœuds représentent des entités, les relations connectent les entités et les propriétés fournissent des métadonnées supplémentaires sur les nœuds et les relations.
Par exemple, voici comment vous pouvez représenter les nœuds et les relations du système hospitalier dans un graphique :
Ce graphique comporte trois nœuds : Patient, Visite et Payeur. Patient et Visite sont liés par la relation HAS, indiquant qu'un patient hospitalisé reçoit une visite. De même, Visite et Payeur sont liés par la relation COVERED_BY, indiquant qu'un payeur d'assurance couvre une visite à l'hôpital.
Remarquez comment les relations sont représentées par une flèche indiquant leur direction. Par exemple, le sens de la relation HAS vous indique qu'un patient peut avoir une visite, mais qu'une visite ne peut pas avoir de patient.
Les nœuds et les relations peuvent avoir des propriétés. Dans cet exemple, les nœuds Patient ont des propriétés d'identifiant, de nom et de date de naissance, et la relation COVERED_BY a des propriétés de date de service et de montant de facturation. Stocker des données dans un graphique comme celui-ci présente plusieurs avantages :
Simplicité : la modélisation des relations réelles entre entités est naturelle dans les bases de données graphiques, ce qui réduit le besoin de schémas complexes nécessitant plusieurs opérations de jointure pour répondre aux requêtes.
Relations : les bases de données graphiques excellent dans la gestion des relations complexes. La traversée des relations est efficace, ce qui facilite l'interrogation et l'analyse des données connectées.
Flexibilité : les bases de données graphiques sont sans schéma, ce qui permet une adaptation facile aux structures de données changeantes. Cette flexibilité est bénéfique pour les modèles de données évolutifs.
Performances : la récupération de données connectées est plus rapide dans les bases de données graphiques que dans les bases de données relationnelles, en particulier pour les scénarios impliquant des requêtes complexes avec plusieurs relations.
Correspondance de modèles : les bases de données graphiques prennent en charge de puissantes requêtes de correspondance de modèles, ce qui facilite l'expression et la recherche de structures spécifiques dans les données.
Lorsque vous disposez de données comportant de nombreuses relations complexes, la simplicité et la flexibilité des bases de données graphiques les rendent plus faciles à concevoir et à interroger par rapport aux bases de données relationnelles. Comme vous le verrez plus tard, la spécification de relations dans les requêtes de bases de données graphiques est concise et n'implique pas de jointures compliquées. Si vous êtes intéressé, Neo4j illustre bien cela avec un exemple de base de données réaliste dans leur documentation.
En raison de cette représentation concise des données, il y a moins de place à l’erreur lorsqu’un LLM génère des requêtes de base de données graphiques. En effet, il vous suffit d'informer le LLM des nœuds, des relations et des propriétés de votre base de données graphique. Comparez cela avec les bases de données relationnelles dans lesquelles le LLM doit naviguer et conserver la connaissance des schémas de table et des relations de clés étrangères dans toute votre base de données, laissant plus de place aux erreurs dans la génération SQL.
Ensuite, vous commencerez à travailler avec des bases de données graphiques en configurant une instance Neo4j AuraDB. Après cela, vous déplacerez le système hospitalier dans votre instance Neo4j et apprendrez à l'interroger.
Créer un compte Neo4j et une instance AuraDB
Pour commencer à utiliser Neo4j, vous pouvez créer un compte Neo4j AuraDB gratuit. La page de destination devrait ressembler à ceci :
Cliquez sur le bouton Démarrer gratuitement et créez un compte. Une fois connecté, vous devriez voir la console Neo4j Aura :
Cliquez sur Nouvelle instance et créez une instance gratuite. Un modal devrait apparaître semblable à celui-ci :
Après avoir cliqué sur Télécharger et continuer, votre instance doit être créée et un fichier texte contenant les informations d'identification de la base de données Neo4j doit être téléchargé. Une fois l'instance créée, vous verrez que son statut est En cours d'exécution. Il ne devrait pas encore y avoir de nœuds ou de relations :
Ensuite, ouvrez le fichier texte que vous avez téléchargé avec vos informations d'identification Neo4j et copiez les NEO4J_URI
, NEO4J_USERNAME
et NEO4J_PASSWORD
dans votre .env
. fichier :
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_URI>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>
Vous utiliserez ces variables d'environnement pour vous connecter à votre instance Neo4j en Python afin que votre chatbot puisse exécuter des requêtes.
Remarque : Par défaut, votre NEO4J_URI doit être similaire à neo4j+s://
Vous avez désormais tout en place pour interagir avec votre instance Neo4j. Ensuite, vous concevrez la base de données des graphiques du système hospitalier. Cela vous indiquera comment les entités hospitalières sont liées et vous indiquera les types de requêtes que vous pouvez exécuter.
Concevoir la base de données graphique du système hospitalier
Maintenant que vous disposez d'une instance Neo4j AuraDB en cours d'exécution, vous devez décider quels nœuds, relations et propriétés vous souhaitez stocker. L’un des moyens les plus populaires de représenter cela consiste à utiliser un organigramme. Sur la base de votre compréhension des données du système hospitalier, vous proposez la conception suivante :
Ce diagramme vous montre tous les nœuds et relations dans les données du système hospitalier. Une façon utile de réfléchir à cet organigramme consiste à commencer par le nœud Patient et à suivre les relations. Un Patient a une visite dans un hôpital, et l'hôpital emploie un médecin pour traiter la visite qui est couverte par un payeur d’assurance.
Voici les propriétés stockées dans chaque nœud :
La majorité de ces propriétés proviennent directement des champs que vous avez explorés à l'étape 2. Une différence notable est que les nœuds Review ont une propriété embedding, qui est une représentation vectorielle du Review. Propriétéspatient_name, physician_name et text. Cela vous permet d'effectuer des recherches vectorielles sur les nœuds de révision comme vous l'avez fait avec ChromaDB.
Voici les propriétés de la relation :
Comme vous pouvez le constater, COVERED_BY est la seule relation avec plus qu'une propriété id. Le service_date est la date à laquelle le patient est sorti d'une visite et le billing_amount est le montant facturé au payeur pour la visite.
Remarque : Ces fausses données du système hospitalier comportent un nombre de nœuds et de relations relativement petit par rapport à ce que vous verriez généralement dans un environnement d'entreprise. Cependant, vous pouvez facilement imaginer combien de nœuds et de relations supplémentaires vous pourriez ajouter pour un véritable système hospitalier. Par exemple, les infirmières, les pharmaciens, les pharmacies, les médicaments sur ordonnance, les cabinets médicaux, les proches des patients et bien d'autres entités hospitalières pourraient être représentés sous forme de nœuds.
Vous pouvez également repenser cela afin que les diagnostics et les symptômes soient représentés sous forme de nœuds plutôt que de propriétés, ou vous pouvez ajouter davantage de propriétés de relation. Vous pouvez faire tout cela sans modifier le design que vous avez déjà. C'est là la beauté des graphiques : vous ajoutez simplement plus de nœuds et de relations à mesure que vos données évoluent.
Maintenant que vous avez un aperçu de la conception du système hospitalier que vous utiliserez, il est temps de déplacer vos données vers Neo4j !
Télécharger des données sur Neo4j
Avec une instance Neo4j en cours d'exécution et une compréhension des nœuds, des propriétés et des relations que vous souhaitez stocker, vous pouvez déplacer les données du système hospitalier vers Neo4j. Pour cela, vous allez créer un dossier appelé hospital_neo4j_etl
avec quelques fichiers vides. Vous souhaiterez également créer un fichier docker-compose.yml
dans le répertoire racine de votre projet :
./
│
├── hospital_neo4j_etl/
│ │
│ ├── src/
│ │ ├── entrypoint.sh
│ │ └── hospital_bulk_csv_write.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── .env
└── docker-compose.yml
Votre fichier .env
doit avoir les variables d'environnement suivantes :
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_URI>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>
HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv
Notez que vous avez stocké tous les fichiers CSV dans un emplacement public sur GitHub. Étant donné que votre instance Neo4j AuraDB s'exécute dans le cloud, elle ne peut pas accéder aux fichiers sur votre machine locale et vous devez utiliser HTTP ou télécharger les fichiers directement sur votre instance. Pour cet exemple, vous pouvez soit utiliser le lien ci-dessus, soit télécharger les données vers un autre emplacement.
Remarque : Si vous téléchargez des données propriétaires sur Neo4j, assurez-vous toujours qu'elles sont stockées dans un emplacement sécurisé et transférées de manière appropriée. Les données utilisées pour ce projet sont toutes synthétiques et non propriétaires, il n'y a donc aucun problème à les télécharger via une connexion HTTP publique. Toutefois, cela ne serait pas une bonne idée en pratique. Vous pouvez en savoir plus sur les moyens sécurisés d'importer des données dans Neo4j dans leur documentation.
Une fois votre fichier .env
rempli, ouvrez pyproject.toml
, qui fournit la configuration, les métadonnées et les dépendances définies au format TOML :
[project]
name = "hospital_neo4j_etl"
version = "0.1"
dependencies = [
"neo4j==5.14.1",
"retry==0.9.2"
]
[project.optional-dependencies]
dev = ["black", "flake8"]
Ce projet est un processus d'extraction, de transformation et de chargement (ETL) simple qui déplace les données dans Neo4j, donc seules les dépendances sont neo4j et réessayez. Le script principal de l'ETL est hospital_neo4j_etl/src/hospital_bulk_csv_write.py
. Il est trop long d'inclure le script complet ici, mais vous aurez une idée des principales étapes que hospital_neo4j_etl/src/hospital_bulk_csv_write.py
exécute. Vous pouvez copier le script complet à partir des matériaux :
Tout d'abord, vous importez les dépendances, chargez les variables d'environnement et configurez la journalisation :
import os
import logging
from retry import retry
from neo4j import GraphDatabase
HOSPITALS_CSV_PATH = os.getenv("HOSPITALS_CSV_PATH")
PAYERS_CSV_PATH = os.getenv("PAYERS_CSV_PATH")
PHYSICIANS_CSV_PATH = os.getenv("PHYSICIANS_CSV_PATH")
PATIENTS_CSV_PATH = os.getenv("PATIENTS_CSV_PATH")
VISITS_CSV_PATH = os.getenv("VISITS_CSV_PATH")
REVIEWS_CSV_PATH = os.getenv("REVIEWS_CSV_PATH")
NEO4J_URI = os.getenv("NEO4J_URI")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s]: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
LOGGER = logging.getLogger(__name__)
# ...
Vous importez la classe GraphDatabase
depuis neo4j
pour vous connecter à votre instance en cours d'exécution. Notez ici que vous n'utilisez plus Python-dotenv pour charger des variables d'environnement. Au lieu de cela, vous transmettrez les variables d'environnement dans le conteneur Docker qui exécute votre script. Ensuite, vous définirez des fonctions pour déplacer les données hospitalières vers Neo4j selon votre conception :
# ...
NODES = ["Hospital", "Payer", "Physician", "Patient", "Visit", "Review"]
def _set_uniqueness_constraints(tx, node):
query = f"""CREATE CONSTRAINT IF NOT EXISTS FOR (n:{node})
REQUIRE n.id IS UNIQUE;"""
_ = tx.run(query, {})
@retry(tries=100, delay=10)
def load_hospital_graph_from_csv() -> None:
"""Load structured hospital CSV data following
a specific ontology into Neo4j"""
driver = GraphDatabase.driver(
NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD)
)
LOGGER.info("Setting uniqueness constraints on nodes")
with driver.session(database="neo4j") as session:
for node in NODES:
session.execute_write(_set_uniqueness_constraints, node)
# ...
# ...
Tout d'abord, vous définissez une fonction d'assistance, _set_uniqueness_constraints()
, qui crée et exécute des requêtes obligeant chaque nœud à avoir un identifiant unique. Dans load_hospital_graph_from_csv()
, vous instanciez un pilote qui se connecte à votre instance Neo4j et définissez des contraintes d'unicité pour chaque nœud du système hospitalier.
Notez le décorateur @retry
attaché à load_hospital_graph_from_csv()
. Si load_hospital_graph_from_csv()
échoue pour une raison quelconque, ce décorateur le réexécutera cent fois avec un délai de dix secondes entre les tentatives. Cela s'avère pratique lorsqu'il y a des problèmes de connexion intermittents à Neo4j qui sont généralement résolus en recréant une connexion. Cependant, assurez-vous de vérifier les journaux de script pour voir si une erreur se reproduit plusieurs fois.
Ensuite, load_hospital_graph_from_csv()
charge les données pour chaque nœud et relation :
# ...
@retry(tries=100, delay=10)
def load_hospital_graph_from_csv() -> None:
"""Load structured hospital CSV data following
a specific ontology into Neo4j"""
# ...
LOGGER.info("Loading hospital nodes")
with driver.session(database="neo4j") as session:
query = f"""
LOAD CSV WITH HEADERS
FROM '{HOSPITALS_CSV_PATH}' AS hospitals
MERGE (h:Hospital {{id: toInteger(hospitals.hospital_id),
name: hospitals.hospital_name,
state_name: hospitals.hospital_state}});
"""
_ = session.run(query, {})
# ...
if __name__ == "__main__":
load_hospital_graph_from_csv()
Chaque nœud et relation est chargé à partir de leurs fichiers csv respectifs et écrit dans Neo4j selon la conception de votre base de données graphique. À la fin du script, vous appelez load_hospital_graph_from_csv()
dans l'idiome name-main, et toutes les données doivent être renseignées dans votre instance Neo4j.
Après avoir écrit hospital_neo4j_etl/src/hospital_bulk_csv_write.py
, vous pouvez définir un fichier entrypoint.sh
qui s'exécutera au démarrage de votre conteneur Docker :
#!/bin/bash
# Run any setup steps or pre-processing tasks here
echo "Running ETL to move hospital data from csvs to Neo4j..."
# Run the ETL script
python hospital_bulk_csv_write.py
Ce fichier de point d'entrée n'est pas techniquement nécessaire pour ce projet, mais c'est une bonne pratique lors de la création de conteneurs car il vous permet d'exécuter les commandes shell nécessaires avant d'exécuter votre script principal.
Le dernier fichier à écrire pour votre ETL est le fichier Docker. Cela ressemble à ceci :
FROM python:3.11-slim
WORKDIR /app
COPY ./src/ /app
COPY ./pyproject.toml /code/pyproject.toml
RUN pip install /code/.
CMD ["sh", "entrypoint.sh"]
Ce Dockerfile
indique à votre conteneur d'utiliser la distribution python:3.11-slim
, copiez le contenu de hospital_neo4j_etl/src/
dans le / app
dans le conteneur, installez les dépendances à partir de pyproject.toml
et exécutez entrypoint.sh
.
Vous pouvez maintenant ajouter ce projet à docker-compose.yml
:
version: '3'
services:
hospital_neo4j_etl:
build:
context: ./hospital_neo4j_etl
env_file:
- .env
L'ETL s'exécutera en tant que service appelé hospital_neo4j_etl
, et il exécutera le Dockerfile dans ./hospital_neo4j_etl
en utilisant les variables d'environnement de .env
. Puisque vous n’avez qu’un seul conteneur, vous n’avez pas encore besoin de docker-compose. Cependant, vous ajouterez d'autres conteneurs à orchestrer avec votre ETL dans la section suivante, il est donc utile de commencer avec docker-compose.yml
.
Pour exécuter votre ETL, ouvrez un terminal et exécutez :
$ docker-compose up --build
Une fois l'exécution de l'ETL terminée, revenez à votre console Aura :
Cliquez sur Ouvrir et vous serez invité à saisir votre mot de passe Neo4j. Après vous être connecté avec succès à l'instance, vous devriez voir un écran similaire à celui-ci :
Comme vous pouvez le voir sous Informations sur la base de données, tous les nœuds, relations et propriétés ont été chargés. Il existe 21 187 nœuds et 48 259 relations. Vous êtes prêt à commencer à rédiger des requêtes !
Interroger le graphique du système hospitalier
La dernière chose que vous devez faire avant de créer votre chatbot est de vous familiariser avec la syntaxe Cypher. Cypher est le langage de requête de Neo4j, et son apprentissage est assez intuitif, surtout si vous êtes familier avec SQL. Cette section couvrira les bases, et c'est tout ce dont vous avez besoin pour créer le chatbot. Vous pouvez consulter la documentation de Neo4j pour un aperçu plus complet de Cypher.
Le mot clé le plus couramment utilisé pour lire des données dans Cypher est MATCH
, et il est utilisé pour spécifier les modèles à rechercher dans le graphique. Le modèle le plus simple est celui avec un seul nœud. Par exemple, si vous souhaitez rechercher les cinq premiers nœuds patient écrits dans le graphique, vous pouvez exécuter la requête Cypher suivante :
MATCH (p:Patient)
RETURN p LIMIT 5;
Dans cette requête, vous effectuez une correspondance sur les nœuds Patient
. Dans Cypher, les nœuds sont toujours indiqués par des parenthèses. Le p
dans (p:Patient)
est un alias que vous pourrez référencer ultérieurement dans la requête. RETURN p LIMIT 5;
indique à Neo4j de renvoyer uniquement cinq nœuds patients. Vous pouvez exécuter cette requête dans l'interface utilisateur de Neo4j, et les résultats devraient ressembler à ceci :
La vue Table vous montre les cinq nœuds Patient renvoyés ainsi que leurs propriétés. Vous pouvez également explorer le graphique et la vue brute si vous êtes intéressé.
Bien que la correspondance sur un seul nœud soit simple, c'est parfois tout ce dont vous avez besoin pour obtenir des informations utiles. Par exemple, si votre partie prenante vous a dit donnez-moi un résumé de la visite 56, la requête suivante vous donne la réponse :
MATCH (v:Visit)
WHERE v.id = 56
RETURN v;
Cette requête correspond aux nœuds Visite
qui ont un id
de 56, spécifié par WHERE v.id=56
. Vous pouvez filtrer sur des propriétés de nœuds et de relations arbitraires dans les clauses WHERE
. Les résultats de cette requête ressemblent à ceci :
À partir du résultat de la requête, vous pouvez voir que la Visite renvoyée a effectivement l'id 56. Vous pouvez ensuite examiner toutes les propriétés de la visite pour dresser un résumé verbal de la visite. - c'est ce que fera votre chaîne Cypher.
La correspondance sur les nœuds est excellente, mais la véritable puissance de Cypher vient de sa capacité à correspondre sur des modèles de relations. Cela vous donne un aperçu des relations sophistiquées, en exploitant la puissance des bases de données graphiques. En poursuivant la requête Visite, vous souhaitez probablement savoir à quel Patient appartient la Visite. Vous pouvez obtenir ceci à partir de la relation HAS :
MATCH (p:Patient)-[h:HAS]->(v:Visit)
WHERE v.id = 56
RETURN v,h,p;
Cette requête Cypher recherche le Patient
qui a une Visite
avec l'id
56. Vous remarquerez que la relation HAS
est entouré de crochets au lieu de parenthèses et sa directionnalité est indiquée par une flèche. Si vous avez essayé MATCH (p:Patient)<-[h:HAS]-(v:Visit)
, la requête ne renverra rien car la direction de la relation HAS est incorrecte. .
Les résultats de la requête ressemblent à ceci :
Notez que le résultat inclut des données pour la relation Visite, HAS et Patient. Cela vous donne plus d'informations que si vous effectuez uniquement une correspondance sur les nœuds Visiter. Si vous souhaitez voir quels médecins ont traité le patient lors de la visite, vous pouvez ajouter la relation suivante à la requête :
MATCH (p:Patient)-[h:HAS]->(v:Visit)<-[t:TREATS]-(ph:Physician)
WHERE v.id = 56
RETURN v,p,ph
Cette instruction (p:Patient)-[h:HAS]->(v:Visit)<-[t:TREATS]-(ph:Physician)
indique à Neo4j de trouver tous les modèles où un Le patient
a une visite
qui est traitée par un médecin
. Si vous souhaitez faire correspondre toutes les relations entrant et sortant du nœud Visite
, vous pouvez exécuter cette requête :
MATCH (v:Visit)-[r]-(n)
WHERE v.id = 56
RETURN r,n;
Notez maintenant que la relation [r]
n'a aucune direction par rapport à (v:Visit)
ou (n)
. Essentiellement, cette instruction de correspondance recherchera toutes les relations qui entrent et sortent de Visite
56, ainsi que les nœuds connectés à ces relations. Voici les résultats :
Cela vous donne une belle vue de toutes les relations et nœuds associés à la Visite 56. Pensez à la puissance de cette représentation. Au lieu d'effectuer plusieurs jointures SQL, comme vous auriez à le faire dans une base de données relationnelle, vous obtenez toutes les informations sur la manière dont une visite est connectée à l'ensemble du système hospitalier avec trois courtes lignes de chiffrement.
Vous pouvez imaginer à quel point cela deviendrait plus puissant à mesure que davantage de nœuds et de relations seraient ajoutés à la base de données graphique. Par exemple, vous pouvez enregistrer les infirmières, les pharmacies, les médicaments ou les interventions chirurgicales associés à la visite. Chaque relation que vous ajoutez nécessiterait une autre jointure dans SQL, mais la requête Cypher ci-dessus concernant la Visite 56 resterait inchangée.
La dernière chose que vous aborderez dans cette section est de savoir comment effectuer des agrégations dans Cypher. Jusqu'à présent, vous avez uniquement interrogé des données brutes provenant de nœuds et de relations, mais vous pouvez également calculer des statistiques globales dans Cypher.
Supposons que vous souhaitiez répondre à la question Quel est le nombre total de visites et le montant total de facturation pour les visites couvertes par Aetna au Texas ? Voici la requête Cypher qui répondrait à cette question :
MATCH (p:Payer)<-[c:COVERED_BY]-(v:Visit)-[:AT]->(h:Hospital)
WHERE p.name = "Aetna"
AND h.state_name = "TX"
RETURN COUNT(*) as num_visits,
SUM(c.billing_amount) as total_billing_amount;
Dans cette requête, vous faites d'abord correspondre toutes les Visites
qui ont lieu dans un Hôpital
et qui sont couvertes par un Payeur
. Vous filtrez ensuite sur Payeurs
avec une propriété name
de Aetna et Hôpitaux
avec un state_name
de TX. Enfin, COUNT(*)
compte le nombre de modèles correspondants et SUM(c.billing_amount)
vous donne le montant total de facturation. Le résultat ressemble à ceci :
Les résultats vous indiquent qu'il y a eu 198 Visites correspondant à ce modèle, pour un montant total de facturation d'environ 5 056 439 $.
Vous avez désormais une solide compréhension des principes fondamentaux de Cypher, ainsi que des types de questions auxquelles vous pouvez répondre. En bref, Cypher est excellent pour faire correspondre des relations complexes sans nécessiter de requête verbeuse. Vous pouvez faire beaucoup plus avec Neo4j et Cypher, mais les connaissances que vous avez acquises dans cette section sont suffisantes pour commencer à créer le chatbot, et c'est ce que vous ferez ensuite.
Étape 4 : Créer un chatbot Graph RAG dans LangChain
Après tout le travail préparatoire de conception et de données que vous avez effectué jusqu’à présent, vous êtes enfin prêt à créer votre chatbot ! Vous remarquerez probablement qu'avec les données du système hospitalier stockées dans Neo4j et la puissance des abstractions LangChain, la création de votre chatbot ne demande pas beaucoup de travail. Il s'agit d'un thème commun dans les projets d'IA et de ML : la plupart du travail concerne la conception, la préparation des données et le déploiement plutôt que la construction de l'IA elle-même.
Avant de vous lancer, ajoutez un dossier chatbot_api/
à votre projet avec les fichiers et dossiers suivants :
./
│
├── chatbot_api/
│ │
│ ├── src/
│ │ │
│ │ ├── agents/
│ │ │ └── hospital_rag_agent.py
│ │ │
│ │ ├── chains/
│ │ │ ├── hospital_cypher_chain.py
│ │ │ └── hospital_review_chain.py
│ │ │
│ │ ├── tools/
│ │ │ └── wait_times.py
│ │
│ └── pyproject.toml
│
├── hospital_neo4j_etl/
│ │
│ ├── src/
│ │ ├── entrypoint.sh
│ │ └── hospital_bulk_csv_write.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── .env
└── docker-compose.yml
Vous souhaiterez également ajouter quelques variables d'environnement supplémentaires à votre fichier .env
:
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_URI>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>
HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv
HOSPITAL_AGENT_MODEL=gpt-3.5-turbo-1106
HOSPITAL_CYPHER_MODEL=gpt-3.5-turbo-1106
HOSPITAL_QA_MODEL=gpt-3.5-turbo-0125
Votre fichier .env
inclut désormais des variables qui spécifient le LLM que vous utiliserez pour les différents composants de votre chatbot. Vous avez spécifié ces modèles en tant que variables d'environnement afin que vous puissiez facilement basculer entre différents modèles OpenAI sans modifier aucun code. Gardez toutefois à l’esprit que chaque LLM peut bénéficier d’une stratégie d’invite unique. Vous devrez donc peut-être modifier vos invites si vous envisagez d’utiliser une suite différente de LLM.
Vous devriez déjà avoir le dossier hospital_neo4j_etl/
terminé, et docker-compose.yml
et .env
sont les mêmes qu'avant. Ouvrez chatbot_api/pyproject.toml
et ajoutez les dépendances suivantes :
[project]
name = "chatbot_api"
version = "0.1"
dependencies = [
"asyncio==3.4.3",
"fastapi==0.109.0",
"langchain==0.1.0",
"langchain-openai==0.0.2",
"langchainhub==0.1.14",
"neo4j==5.14.1",
"numpy==1.26.2",
"openai==1.7.2",
"opentelemetry-api==1.22.0",
"pydantic==2.5.1",
"uvicorn==0.25.0"
]
[project.optional-dependencies]
dev = ["black", "flake8"]
Vous pouvez certainement utiliser des versions plus récentes de ces dépendances si elles sont disponibles, mais gardez à l’esprit toutes les fonctionnalités qui pourraient être obsolètes. Ouvrez un terminal, activez votre environnement virtuel, accédez à votre dossier chatbot_api/
et installez les dépendances à partir du pyproject.toml
du projet :
(venv) $ python -m pip install .
Une fois que tout est installé, vous êtes prêt à construire la chaîne d’avis !
Créer une chaîne vectorielle Neo4j
À l'étape 1, vous avez bénéficié d'une introduction pratique à LangChain en créant une chaîne qui répond aux questions sur les expériences des patients à l'aide de leurs avis. Dans cette section, vous allez construire une chaîne similaire, sauf que vous utiliserez Neo4j comme index vectoriel.
Les index de recherche de vecteurs ont été publiés en version bêta publique dans Neo4j 5.11. Ils vous permettent d'exécuter des requêtes sémantiques directement sur votre graphique. C'est très pratique pour votre chatbot car vous pouvez stocker les intégrations d'avis au même endroit que les données structurées de votre système hospitalier.
Dans LangChain, vous pouvez utiliser Neo4jVector pour créer des intégrations de révision et le récupérateur nécessaire à votre chaîne. Voici le code pour créer la chaîne d’avis :
import os
from langchain.vectorstores.neo4j_vector import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
from langchain.prompts import (
PromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate,
)
HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
neo4j_vector_index = Neo4jVector.from_existing_graph(
embedding=OpenAIEmbeddings(),
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD"),
index_name="reviews",
node_label="Review",
text_node_properties=[
"physician_name",
"patient_name",
"text",
"hospital_name",
],
embedding_node_property="embedding",
)
review_template = """Your job is to use patient
reviews to answer questions about their experience at a hospital. Use
the following context to answer questions. Be as detailed as possible, but
don't make up any information that's not from the context. If you don't know
an answer, say you don't know.
{context}
"""
review_system_prompt = SystemMessagePromptTemplate(
prompt=PromptTemplate(input_variables=["context"], template=review_template)
)
review_human_prompt = HumanMessagePromptTemplate(
prompt=PromptTemplate(input_variables=["question"], template="{question}")
)
messages = [review_system_prompt, review_human_prompt]
review_prompt = ChatPromptTemplate(
input_variables=["context", "question"], messages=messages
)
reviews_vector_chain = RetrievalQA.from_chain_type(
llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
chain_type="stuff",
retriever=neo4j_vector_index.as_retriever(k=12),
)
reviews_vector_chain.combine_documents_chain.llm_chain.prompt = review_prompt
Aux lignes 1 à 11, vous importez les dépendances nécessaires pour construire votre chaîne de révision avec Neo4j. À la ligne 13, vous chargez le nom du modèle de chat que vous utiliserez pour la chaîne de révision et le stockez dans HOSPITAL_QA_MODEL
. Les lignes 15 à 29 créent l'index vectoriel dans Neo4j. Voici une répartition de chaque paramètre :
embedding
: le modèle utilisé pour créer les intégrations – vous utilisezOpenAIEmeddings()
dans cet exemple.url
,username
etpassword
: vos identifiants d'instance Neo4j.index_name
: Le nom donné à votre index vectoriel.node_label
: le nœud pour lequel créer des intégrations.text_node_properties
: les propriétés du nœud à inclure dans l'intégration.embedding_node_property
: le nom de la propriété du nœud d'intégration.
Une fois Neo4jVector.from_existing_graph()
exécuté, vous verrez que chaque nœud Review dans Neo4j possède une propriété embedding qui est une représentation vectorielle du Propriétés physician_name, patient_name, text et hospital_name. Cela vous permet de répondre à des questions telles que Quels hôpitaux ont reçu des avis positifs ? Cela permet également au LLM de vous dire quel patient et quel médecin a rédigé des avis correspondant à votre question.
Les lignes 31 à 50 créent le modèle d'invite pour votre chaîne de révision de la même manière que vous l'avez fait à l'étape 1.
Enfin, les lignes 52 à 57 créent votre chaîne de vecteurs d'avis à l'aide d'un récupérateur d'index vectoriel Neo4j qui renvoie 12 intégrations d'avis à partir d'une recherche de similarité. En définissant chain_type
sur "stuff"
dans .from_chain_type()
, vous dites à la chaîne de transmettre les 12 avis à l'invite. Vous pouvez explorer d’autres types de chaînes dans la documentation de LangChain sur les chaînes.
Vous êtes prêt à essayer votre nouvelle chaîne d'avis. Accédez au répertoire racine de votre projet, démarrez un interpréteur Python et exécutez les commandes suivantes :
>>> import dotenv
>>> dotenv.load_dotenv()
True
>>> from chatbot_api.src.chains.hospital_review_chain import (
... reviews_vector_chain
... )
>>> query = """What have patients said about hospital efficiency?
... Mention details from specific reviews."""
>>> response = reviews_vector_chain.invoke(query)
>>> response.get("result")
"Patients have mentioned different aspects of hospital efficiency in their
reviews. In Kevin Cox's review of Wallace-Hamilton hospital, he mentioned
that the hospital staff was efficient. However, he also mentioned a lack of
personalized attention and communication, which left him feeling neglected.
This suggests that while the hospital may have been efficient in terms of
completing tasks and providing services, they may have lacked in terms of
individualized care and communication with patients.
On the other hand, Beverly Johnson's review of Brown Inc. hospital mentioned
that the hospital had a modern feel and the staff was attentive. However,
she also mentioned that the bureaucratic procedures for check-in and
discharge were cumbersome. This suggests that while the hospital may have
been efficient in terms of its facilities and staff attentiveness, the
administrative processes may have been inefficient and caused inconvenience
for patients. It is important to note that the specific reviews do not
provide a comprehensive picture of hospital efficiency, as they focus on
specific aspects of the hospital experience."
Dans ce bloc, vous importez dotenv
et chargez les variables d'environnement depuis .env
. Vous importez ensuite reviews_vector_chain
depuis hospital_review_chain
et l'invoquez avec une question sur l'efficacité de l'hôpital. La réponse de votre chaîne n'est peut-être pas identique à celle-ci, mais le LLM devrait renvoyer un résumé détaillé et intéressant, comme vous le lui avez demandé.
Dans cet exemple, remarquez comment les noms spécifiques des patients et des hôpitaux sont mentionnés dans la réponse. Cela se produit parce que vous avez intégré les noms des hôpitaux et des patients avec le texte de l'évaluation, afin que le LLM puisse utiliser ces informations pour répondre aux questions.
Remarque : Avant de continuer, vous devez jouer avec reviews_vector_chain
pour voir comment il répond aux différentes requêtes. Les réponses semblent-elles correctes ? Comment pourriez-vous évaluer la qualité de reviews_vector_chain
? Vous n'apprendrez pas comment évaluer les systèmes RAG dans ce didacticiel, mais vous pouvez consulter cet exemple Python complet avec MLFlow pour avoir une idée de la façon dont cela se fait.
Ensuite, vous créerez la chaîne de génération Cypher que vous utiliserez pour répondre aux requêtes sur les données structurées du système hospitalier.
Créer une chaîne de chiffrement Neo4j
Comme vous l'avez vu à l'étape 2, votre chaîne Neo4j Cypher acceptera la requête en langage naturel d'un utilisateur, convertira la requête en langage naturel en requête Cypher, exécutera la requête Cypher dans Neo4j et utilisera les résultats de la requête Cypher pour répondre à la requête de l'utilisateur. Vous utiliserez pour cela GraphCypherQAChain
de LangChain.
Remarque : Chaque fois que vous autorisez les utilisateurs à interroger une base de données, comme vous le ferez avec votre chaîne Cypher, vous devez vous assurer qu'ils disposent uniquement des autorisations nécessaires. Les informations d'identification Neo4j que vous utilisez dans ce projet permettent aux utilisateurs de lire, écrire, mettre à jour et supprimer des données de votre base de données.
Si vous construisiez cette application pour un projet réel, vous souhaiteriez créer des informations d'identification qui restreignent les autorisations de votre utilisateur aux lectures uniquement, les empêchant d'écrire ou de supprimer des données précieuses.
Utiliser des LLM pour générer des requêtes Cypher précises peut s'avérer difficile, surtout si vous disposez d'un graphique compliqué. Pour cette raison, beaucoup d'ingénierie rapide est nécessaire pour montrer votre structure graphique et vos cas d'utilisation de requêtes au LLM. Affiner un LLM pour générer des requêtes est également une option, mais cela nécessite des données organisées et étiquetées manuellement.
Pour commencer à créer votre chaîne de génération Cypher, importez les dépendances et instanciez un Neo4jGraph
:
import os
from langchain_community.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate
HOSPITAL_QA_MODEL = os.getenv("HOSPITAL_QA_MODEL")
HOSPITAL_CYPHER_MODEL = os.getenv("HOSPITAL_CYPHER_MODEL")
graph = Neo4jGraph(
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD"),
)
graph.refresh_schema()
L'objet Neo4jGraph
est un wrapper LangChain qui permet aux LLM d'exécuter des requêtes sur votre instance Neo4j. Vous instanciez graph
à l'aide de vos informations d'identification Neo4j et vous appelez graph.refresh_schema()
pour synchroniser toutes les modifications récentes apportées à votre instance.
Le composant suivant et le plus important de votre chaîne de génération Cypher est le modèle d'invite. Voici à quoi cela ressemble :
# ...
cypher_generation_template = """
Task:
Generate Cypher query for a Neo4j graph database.
Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
Schema:
{schema}
Note:
Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything other than
for you to construct a Cypher statement. Do not include any text except
the generated Cypher statement. Make sure the direction of the relationship is
correct in your queries. Make sure you alias both entities and relationships
properly. Do not run any queries that would add to or delete from
the database. Make sure to alias all statements that follow as with
statement (e.g. WITH v as visit, c.billing_amount as billing_amount)
If you need to divide numbers, make sure to
filter the denominator to be non zero.
Examples:
# Who is the oldest patient and how old are they?
MATCH (p:Patient)
RETURN p.name AS oldest_patient,
duration.between(date(p.dob), date()).years AS age
ORDER BY age DESC
LIMIT 1
# Which physician has billed the least to Cigna
MATCH (p:Payer)<-[c:COVERED_BY]-(v:Visit)-[t:TREATS]-(phy:Physician)
WHERE p.name = 'Cigna'
RETURN phy.name AS physician_name, SUM(c.billing_amount) AS total_billed
ORDER BY total_billed
LIMIT 1
# Which state had the largest percent increase in Cigna visits
# from 2022 to 2023?
MATCH (h:Hospital)<-[:AT]-(v:Visit)-[:COVERED_BY]->(p:Payer)
WHERE p.name = 'Cigna' AND v.admission_date >= '2022-01-01' AND
v.admission_date < '2024-01-01'
WITH h.state_name AS state, COUNT(v) AS visit_count,
SUM(CASE WHEN v.admission_date >= '2022-01-01' AND
v.admission_date < '2023-01-01' THEN 1 ELSE 0 END) AS count_2022,
SUM(CASE WHEN v.admission_date >= '2023-01-01' AND
v.admission_date < '2024-01-01' THEN 1 ELSE 0 END) AS count_2023
WITH state, visit_count, count_2022, count_2023,
(toFloat(count_2023) - toFloat(count_2022)) / toFloat(count_2022) * 100
AS percent_increase
RETURN state, percent_increase
ORDER BY percent_increase DESC
LIMIT 1
# How many non-emergency patients in North Carolina have written reviews?
MATCH (r:Review)<-[:WRITES]-(v:Visit)-[:AT]->(h:Hospital)
WHERE h.state_name = 'NC' and v.admission_type <> 'Emergency'
RETURN count(*)
String category values:
Test results are one of: 'Inconclusive', 'Normal', 'Abnormal'
Visit statuses are one of: 'OPEN', 'DISCHARGED'
Admission Types are one of: 'Elective', 'Emergency', 'Urgent'
Payer names are one of: 'Cigna', 'Blue Cross', 'UnitedHealthcare', 'Medicare',
'Aetna'
A visit is considered open if its status is 'OPEN' and the discharge date is
missing.
Use abbreviations when
filtering on hospital states (e.g. "Texas" is "TX",
"Colorado" is "CO", "North Carolina" is "NC",
"Florida" is "FL", "Georgia" is "GA", etc.)
Make sure to use IS NULL or IS NOT NULL when analyzing missing properties.
Never return embedding properties in your queries. You must never include the
statement "GROUP BY" in your query. Make sure to alias all statements that
follow as with statement (e.g. WITH v as visit, c.billing_amount as
billing_amount)
If you need to divide numbers, make sure to filter the denominator to be non
zero.
The question is:
{question}
"""
cypher_generation_prompt = PromptTemplate(
input_variables=["schema", "question"], template=cypher_generation_template
)
Lisez attentivement le contenu de cypher_generation_template
. Remarquez comment vous fournissez au LLM des instructions très spécifiques sur ce qu'il doit et ne doit pas faire lors de la génération de requêtes Cypher. Plus important encore, vous affichez au LLM la structure de votre graphique avec le paramètre schema
, quelques exemples de requêtes et les valeurs catégorielles de quelques propriétés de nœud.
Tous les détails que vous fournissez dans votre modèle d'invite améliorent les chances du LLM de générer une requête Cypher correcte pour une question donnée. Si vous êtes curieux de savoir à quel point tous ces détails sont nécessaires, essayez de créer votre propre modèle d'invite avec le moins de détails possible. Ensuite, exécutez des questions via votre chaîne Cypher et voyez si elle génère correctement les requêtes Cypher.
À partir de là, vous pouvez mettre à jour de manière itérative votre modèle d'invite pour corriger les requêtes que le LLM a du mal à générer, mais assurez-vous également de connaître le nombre de jetons d'entrée que vous utilisez. Comme pour votre chaîne de révision, vous aurez besoin d’un système solide pour évaluer les modèles d’invite et l’exactitude des requêtes Cypher générées par votre chaîne. Cependant, comme vous le verrez, le modèle que vous avez ci-dessus est un excellent point de départ.
Remarque : Le modèle d'invite ci-dessus fournit au LLM quatre exemples de requêtes Cypher valides pour votre graphique. Donner quelques exemples au LLM, puis lui demander d'effectuer une tâche est connu sous le nom d'invite en quelques tirs, et c'est une technique simple mais puissante pour améliorer la précision de la génération.
Cependant, les invites en quelques étapes peuvent ne pas suffire pour la génération de requêtes Cypher, surtout si vous disposez d'un graphique complexe. Une façon d'améliorer cela consiste à créer une base de données vectorielle qui intègre des exemples de questions/requêtes d'utilisateurs et stocke leurs requêtes Cypher correspondantes sous forme de métadonnées.
Lorsqu'un utilisateur pose une question, vous injectez des requêtes Cypher provenant de questions sémantiquement similaires dans l'invite, fournissant ainsi au LLM les exemples les plus pertinents nécessaires pour répondre à la question actuelle.
Ensuite, vous définissez le modèle d'invite pour le composant question-réponse de votre chaîne. Ce modèle indique au LLM d'utiliser les résultats de la requête Cypher pour générer une réponse bien formatée à la requête de l'utilisateur :
# ...
qa_generation_template = """You are an assistant that takes the results
from a Neo4j Cypher query and forms a human-readable response. The
query results section contains the results of a Cypher query that was
generated based on a user's natural language question. The provided
information is authoritative, you must never doubt it or try to use
your internal knowledge to correct it. Make the answer sound like a
response to the question.
Query Results:
{context}
Question:
{question}
If the provided information is empty, say you don't know the answer.
Empty information looks like this: []
If the information is not empty, you must provide an answer using the
results. If the question involves a time duration, assume the query
results are in units of days unless otherwise specified.
When names are provided in the query results, such as hospital names,
beware of any names that have commas or other punctuation in them.
For instance, 'Jones, Brown and Murray' is a single hospital name,
not multiple hospitals. Make sure you return any list of names in
a way that isn't ambiguous and allows someone to tell what the full
names are.
Never say you don't have the right information if there is data in
the query results. Always use the data in the query results.
Helpful Answer:
"""
qa_generation_prompt = PromptTemplate(
input_variables=["context", "question"], template=qa_generation_template
)
Ce modèle nécessite beaucoup moins de détails que votre modèle de génération Cypher, et vous ne devriez le modifier que si vous souhaitez que le LLM réponde différemment, ou si vous remarquez qu'il n'utilise pas les résultats de la requête comme vous le souhaitez. La dernière étape de la création de votre chaîne Cypher consiste à instancier un objet GraphCypherQAChain
:
# ...
hospital_cypher_chain = GraphCypherQAChain.from_llm(
cypher_llm=ChatOpenAI(model=HOSPITAL_CYPHER_MODEL, temperature=0),
qa_llm=ChatOpenAI(model=HOSPITAL_QA_MODEL, temperature=0),
graph=graph,
verbose=True,
qa_prompt=qa_generation_prompt,
cypher_prompt=cypher_generation_prompt,
validate_cypher=True,
top_k=100,
)
Voici un aperçu des paramètres utilisés dans GraphCypherQAChain.from_llm()
:
cypher_llm
: Le LLM utilisé pour générer des requêtes Cypher.qa_llm
: le LLM utilisé pour générer une réponse en fonction des résultats de la requête Cypher.graph
: L'objetNeo4jGraph
qui se connecte à votre instance Neo4j.verbose
: indique si les étapes intermédiaires effectuées par votre chaîne doivent être imprimées.qa_prompt
: le modèle d'invite pour répondre aux questions/requêtes.cypher_prompt
: le modèle d'invite pour générer des requêtes Cypher.validate_cypher
: si vrai, la requête Cypher sera inspectée pour détecter les erreurs et corrigée avant son exécution. Notez que cela ne garantit pas que la requête Cypher sera valide. Au lieu de cela, il corrige des erreurs de syntaxe simples qui sont facilement détectables à l’aide d’expressions régulières.top_k
: le nombre de résultats de requête à inclure dansqa_prompt
.
La chaîne de génération de chiffrement de votre système hospitalier est prête à l'emploi ! Cela fonctionne de la même manière que votre chaîne d’avis. Accédez au répertoire de votre projet et démarrez une nouvelle session d'interpréteur Python, puis essayez :
>>> import dotenv
>>> dotenv.load_dotenv()
True
>>> from chatbot_api.src.chains.hospital_cypher_chain import (
... hospital_cypher_chain
... )
>>> question = """What is the average visit duration for
... emergency visits in North Carolina?"""
>>> response = hospital_cypher_chain.invoke(question)
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (v:Visit)-[:AT]->(h:Hospital)
WHERE h.state_name = 'NC' AND v.admission_type = 'Emergency'
AND v.status = 'DISCHARGED'
WITH v, duration.between(date(v.admission_date),
date(v.discharge_date)).days AS visit_duration
RETURN AVG(visit_duration) AS average_visit_duration
Full Context:
[{'average_visit_duration': 15.072972972972991}]
> Finished chain.
>>> response.get("result")
'The average visit duration for emergency visits in North
Carolina is 15.07 days.'
Après avoir chargé les variables d'environnement, importé hospital_cypher_chain
et l'avoir invoqué avec une question, vous pouvez voir les étapes suivies par votre chaîne pour répondre à la question. Prenez une seconde pour apprécier quelques réalisations de votre chaîne lors de la génération de la requête Cypher :
- Le LLM de génération Cypher a compris la relation entre les visites et les hôpitaux à partir du schéma graphique fourni.
- Même si vous l'avez posé sur la Caroline du Nord, le LLM savait, grâce à l'invite, qu'il fallait utiliser l'abréviation d'État NC.
- Le LLM savait que les propriétés admission_type n'ont que la première lettre en majuscule, tandis que les propriétés status sont toutes en majuscules.
- Le LLM de génération d'assurance qualité savait, grâce à votre invite, que les résultats de la requête étaient en unités de jours.
Vous pouvez expérimenter toutes sortes de requêtes sur le système hospitalier. Par exemple, voici une question relativement difficile à convertir en Cypher :
>>> question = """Which state had the largest percent increase
... in Medicaid visits from 2022 to 2023?"""
>>> response = hospital_cypher_chain.invoke(question)
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (h:Hospital)<-[:AT]-(v:Visit)-[:COVERED_BY]->(p:Payer)
WHERE p.name = 'Medicaid' AND v.admission_date >= '2022-01-01'
AND v.admission_date < '2024-01-01'
WITH h.state_name AS state, COUNT(v) AS visit_count,
SUM(CASE WHEN v.admission_date >= '2022-01-01'
AND v.admission_date < '2023-01-01' THEN 1 ELSE 0 END) AS count_2022,
SUM(CASE WHEN v.admission_date >= '2023-01-01'
AND v.admission_date < '2024-01-01' THEN 1 ELSE 0 END) AS count_2023
WITH state, visit_count, count_2022, count_2023,
(toFloat(count_2023) - toFloat(count_2022)) / toFloat(count_2022) * 100
AS percent_increase
RETURN state, percent_increase
ORDER BY percent_increase DESC
LIMIT 1
Full Context:
[{'state': 'TX', 'percent_increase': 8.823529411764707}]
> Finished chain.
>>> response.get("result")
'The state with the largest percent increase in Medicaid visits
from 2022 to 2023 is Texas (TX), with a percent increase of 8.82%.'
Pour répondre à la question Quel État a enregistré le pourcentage d'augmentation le plus important des visites Medicaid entre 2022 et 2023 ?, le LLM a dû générer une requête Cypher assez détaillée impliquant plusieurs nœuds, relations et filtres. Néanmoins, il a réussi à trouver la bonne réponse.
La dernière capacité dont votre chatbot a besoin est de répondre aux questions sur les temps d’attente, et c’est ce que vous aborderez ensuite.
Créer des fonctions de temps d'attente
Cette dernière capacité dont votre chatbot a besoin est de répondre aux questions sur les temps d’attente à l’hôpital. Comme indiqué précédemment, votre organisation ne stocke nulle part les données sur les temps d’attente, votre chatbot devra donc les récupérer à partir d’une source externe. Vous allez écrire deux fonctions à cet effet : une qui simule la recherche du temps d'attente actuel dans un hôpital et une autre qui trouve l'hôpital avec le temps d'attente le plus court.
Remarque : Le but de la création de fonctions de temps d'attente est de vous montrer que les agents LangChain peuvent exécuter du code Python arbitraire, et pas seulement des chaînes ou d'autres méthodes LangChain. Cette fonctionnalité est extrêmement précieuse car elle signifie, en théorie, que vous pouvez créer un agent pour faire à peu près tout ce qui peut être exprimé dans le code.
Commencez par définir des fonctions pour récupérer les temps d'attente actuels dans un hôpital :
import os
from typing import Any
import numpy as np
from langchain_community.graphs import Neo4jGraph
def _get_current_hospitals() -> list[str]:
"""Fetch a list of current hospital names from a Neo4j database."""
graph = Neo4jGraph(
url=os.getenv("NEO4J_URI"),
username=os.getenv("NEO4J_USERNAME"),
password=os.getenv("NEO4J_PASSWORD"),
)
current_hospitals = graph.query(
"""
MATCH (h:Hospital)
RETURN h.name AS hospital_name
"""
)
return [d["hospital_name"].lower() for d in current_hospitals]
def _get_current_wait_time_minutes(hospital: str) -> int:
"""Get the current wait time at a hospital in minutes."""
current_hospitals = _get_current_hospitals()
if hospital.lower() not in current_hospitals:
return -1
return np.random.randint(low=0, high=600)
def get_current_wait_times(hospital: str) -> str:
"""Get the current wait time at a hospital formatted as a string."""
wait_time_in_minutes = _get_current_wait_time_minutes(hospital)
if wait_time_in_minutes == -1:
return f"Hospital '{hospital}' does not exist."
hours, minutes = divmod(wait_time_in_minutes, 60)
if hours > 0:
return f"{hours} hours {minutes} minutes"
else:
return f"{minutes} minutes"
La première fonction que vous définissez est _get_current_hospitals()
qui renvoie une liste de noms d'hôpitaux de votre base de données Neo4j. Ensuite, _get_current_wait_time_minutes()
prend le nom d'un hôpital en entrée. Si le nom de l'hôpital n'est pas valide, _get_current_wait_time_minutes()
renvoie -1. Si le nom de l'hôpital est valide, _get_current_wait_time_minutes()
renvoie un entier aléatoire compris entre 0 et 600 simulant un temps d'attente en minutes.
Vous définissez ensuite get_current_wait_times()
qui est un wrapper autour de _get_current_wait_time_minutes()
qui renvoie le temps d'attente formaté sous forme de chaîne.
Vous pouvez utiliser _get_current_wait_time_minutes()
pour définir une deuxième fonction qui trouve l'hôpital avec le temps d'attente le plus court :
# ...
def get_most_available_hospital(_: Any) -> dict[str, float]:
"""Find the hospital with the shortest wait time."""
current_hospitals = _get_current_hospitals()
current_wait_times = [
_get_current_wait_time_minutes(h) for h in current_hospitals
]
best_time_idx = np.argmin(current_wait_times)
best_hospital = current_hospitals[best_time_idx]
best_wait_time = current_wait_times[best_time_idx]
return {best_hospital: best_wait_time}
Ici, vous définissez get_most_available_hospital()
qui appelle _get_current_wait_time_minutes()
sur chaque hôpital et renvoie l'hôpital avec le temps d'attente le plus court. Remarquez comment get_most_available_hospital()
a une entrée jetable _
. Votre agent en aura besoin plus tard car il est conçu pour transmettre des entrées aux fonctions.
Voici comment utiliser get_current_wait_times()
et get_most_available_hospital()
:
>>> import dotenv
>>> dotenv.load_dotenv()
True
>>> from chatbot_api.src.tools.wait_times import (
... get_current_wait_times,
... get_most_available_hospital,
... )
>>> get_current_wait_times("Wallace-Hamilton")
'1 hours 35 minutes'
>>> get_current_wait_times("fake hospital")
"Hospital 'fake hospital' does not exist."
>>> get_most_available_hospital(None)
{'cunningham and sons': 24}
Après avoir chargé les variables d'environnement, vous appelez get_current_wait_times("Wallace-Hamilton")
qui renvoie le temps d'attente actuel en minutes à l'hôpital Wallace-Hamilton. Lorsque vous essayez get_current_wait_times("fake hospital")
, vous obtenez une chaîne vous indiquant que le faux hôpital n'existe pas dans la base de données.
Enfin, get_most_available_hospital()
renvoie un dictionnaire stockant le temps d'attente pour l'hôpital avec le temps d'attente le plus court en minutes. Ensuite, vous allez créer un agent qui utilise ces fonctions, ainsi que la chaîne de chiffrement et de révision, pour répondre à des questions arbitraires sur le système hospitalier.
Créer l'agent Chatbot
Donnez-vous une tape dans le dos si vous êtes arrivé jusqu'ici. Vous avez couvert beaucoup d’informations et vous êtes enfin prêt à tout reconstituer et à assembler l’agent qui vous servira de chatbot. En fonction de la requête que vous lui posez, votre agent doit choisir entre vos fonctions de chaîne Cypher, de chaîne d'avis et de temps d'attente.
Commencez par charger les dépendances de votre agent, en lisant le nom du modèle d'agent à partir d'une variable d'environnement et en chargeant un modèle d'invite depuis LangChain Hub :
import os
from langchain_openai import ChatOpenAI
from langchain.agents import (
create_openai_functions_agent,
Tool,
AgentExecutor,
)
from langchain import hub
from chains.hospital_review_chain import reviews_vector_chain
from chains.hospital_cypher_chain import hospital_cypher_chain
from tools.wait_times import (
get_current_wait_times,
get_most_available_hospital,
)
HOSPITAL_AGENT_MODEL = os.getenv("HOSPITAL_AGENT_MODEL")
hospital_agent_prompt = hub.pull("hwchase17/openai-functions-agent")
Remarquez comment vous importez reviews_vector_chain
, hospital_cypher_chain
, get_current_wait_times()
et get_most_available_hospital()
. Votre agent les utilisera directement comme outils. HOSPITAL_AGENT_MODEL
est le LLM qui agira comme le cerveau de votre agent, décidant quels outils appeler et quelles entrées leur transmettre.
Au lieu de définir votre propre invite pour l'agent, ce que vous pouvez certainement faire, vous chargez une invite prédéfinie depuis LangChain Hub. Le hub LangChain vous permet de télécharger, parcourir, extraire, tester et gérer les invites. Dans ce cas, l'invite par défaut pour les agents de fonction OpenAI fonctionne très bien.
Ensuite, vous définissez une liste d'outils que votre agent peut utiliser :
# ...
tools = [
Tool(
name="Experiences",
func=reviews_vector_chain.invoke,
description="""Useful when you need to answer questions
about patient experiences, feelings, or any other qualitative
question that could be answered about a patient using semantic
search. Not useful for answering objective questions that involve
counting, percentages, aggregations, or listing facts. Use the
entire prompt as input to the tool. For instance, if the prompt is
"Are patients satisfied with their care?", the input should be
"Are patients satisfied with their care?".
""",
),
Tool(
name="Graph",
func=hospital_cypher_chain.invoke,
description="""Useful for answering questions about patients,
physicians, hospitals, insurance payers, patient review
statistics, and hospital visit details. Use the entire prompt as
input to the tool. For instance, if the prompt is "How many visits
have there been?", the input should be "How many visits have
there been?".
""",
),
Tool(
name="Waits",
func=get_current_wait_times,
description="""Use when asked about current wait times
at a specific hospital. This tool can only get the current
wait time at a hospital and does not have any information about
aggregate or historical wait times. Do not pass the word "hospital"
as input, only the hospital name itself. For example, if the prompt
is "What is the current wait time at Jordan Inc Hospital?", the
input should be "Jordan Inc".
""",
),
Tool(
name="Availability",
func=get_most_available_hospital,
description="""
Use when you need to find out which hospital has the shortest
wait time. This tool does not have any information about aggregate
or historical wait times. This tool returns a dictionary with the
hospital name as the key and the wait time in minutes as the value.
""",
),
]
Votre agent dispose de quatre outils : Expériences, Graphique, Attentes et Disponibilité. Les outils Expériences et Graph appellent .invoke()
à partir de leurs chaînes respectives, tandis que Waits et Availability appelle les fonctions de temps d'attente que vous avez définies. Notez que de nombreuses descriptions d'outils comportent des invites succinctes, indiquant à l'agent quand il doit utiliser l'outil et lui fournissant un exemple des entrées à transmettre.
Comme pour les chaînes, une bonne ingénierie des délais est cruciale pour le succès de votre agent. Vous devez décrire clairement chaque outil et comment l'utiliser afin que votre agent ne soit pas dérouté par une requête.
La dernière étape consiste à instancier votre agent :
# ...
chat_model = ChatOpenAI(
model=HOSPITAL_AGENT_MODEL,
temperature=0,
)
hospital_rag_agent = create_openai_functions_agent(
llm=chat_model,
prompt=hospital_agent_prompt,
tools=tools,
)
hospital_rag_agent_executor = AgentExecutor(
agent=hospital_rag_agent,
tools=tools,
return_intermediate_steps=True,
verbose=True,
)
Vous initialisez d'abord un objet ChatOpenAI
en utilisant HOSPITAL_AGENT_MODEL
comme LLM. Vous créez ensuite un agent de fonctions OpenAI avec create_openai_functions_agent()
. Cela crée un agent conçu par OpenAI pour transmettre des entrées aux fonctions. Pour ce faire, il renvoie des objets JSON qui stockent les entrées de fonction et leur valeur correspondante.
Pour créer l'environnement d'exécution de l'agent, vous transmettez votre agent et vos outils dans AgentExecutor
. Définir return_intermediate_steps
et verbose
sur true vous permet de voir le processus de réflexion de l'agent et les outils qu'il appelle.
Avec cela, vous avez terminé la création de l’agent du système hospitalier. Pour l'essayer, vous devrez naviguer dans le dossier chatbot_api/src/
et démarrer une nouvelle session REPL à partir de là.
Remarque : Ceci est nécessaire car vous configurez des importations relatives dans hospital_rag_agent.py
qui seront ensuite exécutées dans un conteneur Docker. Pour l'instant, cela signifie que vous devrez démarrer votre interpréteur Python uniquement après avoir navigué dans chatbot_api/src/
pour que les importations fonctionnent.
Vous pouvez maintenant tester votre agent système hospitalier sur votre ligne de commande :
>>> import dotenv
>>> dotenv.load_dotenv()
True
>>> from agents.hospital_rag_agent import hospital_rag_agent_executor
>>> response = hospital_rag_agent_executor.invoke(
... {"input": "What is the wait time at Wallace-Hamilton?"}
... )
> Entering new AgentExecutor chain...
Invoking: `Waits` with `Wallace-Hamilton`
54The current wait time at Wallace-Hamilton is 54 minutes.
> Finished chain.
>>> response.get("output")
'The current wait time at Wallace-Hamilton is 54 minutes.'
>>> response = hospital_rag_agent_executor.invoke(
... {"input": "Which hospital has the shortest wait time?"}
... )
> Entering new AgentExecutor chain...
Invoking: `Availability` with `shortest wait time`
{'smith, edwards and obrien': 2}The hospital with the shortest
wait time is Smith, Edwards and O'Brien, with a wait time of 2 minutes.
> Finished chain.
>>> response.get("output")
"The hospital with the shortest wait time is Smith, Edwards
and O'Brien, with a wait time of 2 minutes."
Après avoir chargé les variables d'environnement, vous interrogez l'agent sur les temps d'attente. Vous pouvez voir exactement ce qu’il fait en réponse à chacune de vos requêtes. Par exemple, lorsque vous demandez « Quel est le temps d'attente à Wallace-Hamilton ? », l'outil Attente est invoqué et passe Wallace-Hamilton. comme entrée. Cela signifie que l'agent appelle get_current_wait_times("Wallace-Hamilton")
, observe la valeur de retour et utilise la valeur de retour pour répondre à votre question.
Pour voir toutes les capacités de l'agent, vous pouvez lui poser des questions sur les expériences des patients auxquelles les avis des patients doivent répondre :
>>> response = hospital_rag_agent_executor.invoke(
... {
... "input": (
... "What have patients said about their "
... "quality of rest during their stay?"
... )
... }
... )
> Entering new AgentExecutor chain...
Invoking: `Experiences` with `What have patients said about their quality of
rest during their stay?`
{'query': 'What have patients said about their quality of rest during their
stay?','result': "Patients have mentioned that the constant interruptions
for routine checks and the noise level at night were disruptive and made
it difficult for them to get a good night's sleep during their stay.
Additionally, some patients have complained about uncomfortable beds
affecting their quality of rest."}Patients have mentioned that the
constant interruptions for routine checks and the noise level at night
were disruptive and made it difficult for them to get a good night's sleep
during their stay. Additionally, some patients have complained about
uncomfortable beds affecting their quality of rest.
> Finished chain.
>>> response.get("output")
"Patients have mentioned that the constant interruptions for routine checks
and the noise level at night were disruptive and made it difficult for them
to get a good night's sleep during their stay. Additionally, some patients
have complained about uncomfortable beds affecting their quality of rest."
Remarquez ici que vous ne mentionnez jamais explicitement les avis ou les expériences dans votre question. L'agent sait, sur la base de la description de l'outil, qu'il doit invoquer des Expériences. Enfin, vous pouvez poser à l'agent une question nécessitant une requête Cypher pour répondre :
>>> response = hospital_rag_agent_executor.invoke(
... {
... "input": (
... "Which physician has treated the "
... "most patients covered by Cigna?"
... )
... }
... )
> Entering new AgentExecutor chain...
Invoking: `Graph` with `Which physician has treated the most patients
covered by Cigna?`
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (phy:Physician)-[:TREATS]->(v:Visit)-[:COVERED_BY]->(p:Payer)
WHERE p.name = 'Cigna'
WITH phy, COUNT(DISTINCT v) AS patient_count
RETURN phy.name AS physician_name, patient_count
ORDER BY patient_count DESC
LIMIT 1
Full Context:
[{'physician_name': 'Renee Brown', 'patient_count': 10}]
> Finished chain.
{'query': 'Which physician has treated the most patients covered by Cigna?',
'result': 'The physician who has treated the most patients covered by Cigna
is Dr. Renee Brown. She has treated a total of 10 patients.'}The
physician who has treated the most patients covered by Cigna is Dr. Renee
Brown. She has treated a total of 10 patients.
> Finished chain.
>>> response.get("output")
'The physician who has treated the most patients covered by
Cigna is Dr. Renee Brown.
She has treated a total of 10 patients.'
Votre agent a une capacité remarquable à savoir quels outils utiliser et quelles entrées transmettre en fonction de votre requête. Il s'agit de votre chatbot entièrement fonctionnel. Il a le potentiel de répondre à toutes les questions que vos parties prenantes pourraient poser en fonction des exigences données, et il semble faire un excellent travail jusqu'à présent.
Au fur et à mesure que vous posez davantage de questions à votre chatbot, vous rencontrerez presque certainement des situations dans lesquelles il appelle le mauvais outil ou génère une réponse incorrecte. Bien que la modification de vos invites puisse aider à corriger les réponses incorrectes, vous pouvez parfois modifier votre requête de saisie pour aider votre chatbot. Jetez un œil à cet exemple :
>>> response = hospital_rag_agent_executor.invoke(
... {"input": "Show me reviews written by patient 7674."}
... )
> Entering new AgentExecutor chain...
Invoking: `Experiences` with `Show me reviews written by patient 7674.`
{'query': 'Show me reviews written by patient 7674.', 'result': 'I\'m sorry,
but there are no reviews provided by a patient with the identifier "7674" in
the context given. If you have any other questions or need information about
the reviews provided, feel free to ask.'}I'm sorry, but there are no reviews
provided by a patient with the identifier "7674" in the context given. If
you have any other questions or need information about the reviews provided,
feel free to ask.
> Finished chain.
>>> response.get("output")
'I\'m sorry, but there are no reviews provided by a patient with the identifier
"7674" in the context given. If you have any other questions or need information
about the reviews provided, feel free to ask.'
Dans cet exemple, vous demandez à l'agent de vous montrer les avis rédigés par le patient 7674. Votre agent appelle Expériences
et ne trouve pas la réponse que vous recherchez. Bien qu'il soit possible de trouver la réponse à l'aide de la recherche vectorielle sémantique, vous pouvez obtenir une réponse exacte en générant une requête Cypher pour rechercher les avis correspondant à l'ID de patient 7674. Pour aider votre agent à comprendre cela, vous pouvez ajouter des détails supplémentaires à votre requête. :
>>> response = hospital_rag_agent_executor.invoke(
... {
... "input": (
... "Query the graph database to show me "
... "the reviews written by patient 7674"
... )
... }
... )
> Entering new AgentExecutor chain...
Invoking: `Graph` with `Show me reviews written by patient 7674`
> Entering new GraphCypherQAChain chain...
Generated Cypher:
MATCH (p:Patient {id: 7674})-[:HAS]->(v:Visit)-[:WRITES]->(r:Review)
RETURN r.text AS review_written
Full Context:
[{'review_written': 'The hospital provided exceptional care,
but the billing process was confusing and frustrating. Clearer
communication about costs would have been appreciated.'}]
> Finished chain.
{'query': 'Show me reviews written by patient 7674', 'result': 'Here
is a review written by patient 7674: "The hospital provided exceptional
care, but the billing process was confusing and frustrating. Clearer
communication about costs would have been appreciated."'}Patient 7674
wrote the following review: "The hospital provided exceptional
care, but the billing process was confusing and frustrating.
Clearer communication about costs would have been appreciated."
> Finished chain.
>>> response.get("output")
'Patient 7674 wrote the following review: "The hospital provided exceptional
care, but the billing process was confusing and frustrating. Clearer
communication about costs would have been appreciated."'
Ici, vous dites explicitement à votre agent que vous souhaitez interroger la base de données graphique, qui appelle correctement Graph
pour trouver l'avis correspondant à l'ID patient 7674. Fournir plus de détails dans vos requêtes comme celle-ci est un moyen simple mais efficace. pour guider votre agent lorsqu'il invoque clairement les mauvais outils.
Comme pour vos avis et votre chaîne Cypher, avant de présenter cela aux parties prenantes, vous souhaitez élaborer un cadre pour évaluer votre agent. La principale fonctionnalité que vous souhaitez évaluer est la capacité de l’agent à appeler les bons outils avec les bonnes entrées, ainsi que sa capacité à comprendre et interpréter les sorties des outils qu’il appelle.
Dans la dernière étape, vous apprendrez comment déployer votre agent système hospitalier avec FastAPI et Streamlit. Cela rendra votre agent accessible à toute personne qui appelle le point de terminaison de l'API ou interagit avec l'interface utilisateur Streamlit.
Étape 5 : Déployer l'agent LangChain
Vous disposez enfin d’un agent LangChain fonctionnel qui sert de chatbot pour votre système hospitalier. La dernière chose que vous devez faire est de présenter votre chatbot aux parties prenantes. Pour cela, vous déployerez votre chatbot en tant que point de terminaison FastAPI et créerez une interface utilisateur Streamlit pour interagir avec le point de terminaison.
Avant de commencer, créez deux nouveaux dossiers appelés chatbot_frontend/
et tests/
dans le répertoire racine de votre projet. Vous devrez également ajouter des fichiers et dossiers supplémentaires à chatbot_api/
:
./
│
├── chatbot_api/
│ │
│ ├── src/
│ │ │
│ │ ├── agents/
│ │ │ └── hospital_rag_agent.py
│ │ │
│ │ ├── chains/
│ │ │ ├── hospital_cypher_chain.py
│ │ │ └── hospital_review_chain.py
│ │ │
│ │ ├── models/
│ │ │ └── hospital_rag_query.py
│ │ │
│ │ ├── tools/
│ │ │ └── wait_times.py
│ │ │
│ │ ├── utils/
│ │ │ └── async_utils.py
│ │ │
│ │ ├── entrypoint.sh
│ │ └── main.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── chatbot_frontend/
│ │
│ ├── src/
│ │ ├── entrypoint.sh
│ │ └── main.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── hospital_neo4j_etl/
│ │
│ ├── src/
│ │ ├── entrypoint.sh
│ │ └── hospital_bulk_csv_write.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── tests/
│ ├── async_agent_requests.py
│ └── sync_agent_requests.py
│
├── .env
└── docker-compose.yml
Vous avez besoin des nouveaux fichiers dans chatbot_api
pour créer votre application FastAPI, et tests/
dispose de deux scripts pour démontrer la puissance des requêtes asynchrones adressées à votre agent. Enfin, chatbot_frontend/
contient le code de l'interface utilisateur Streamlit qui s'interfacera avec votre chatbot. Vous commencerez par créer une application FastAPI pour servir votre agent.
Servir l'agent avec FastAPI
FastAPI est un framework Web moderne et performant permettant de créer des API avec Python basées sur des astuces de type standard. Il est livré avec de nombreuses fonctionnalités intéressantes, notamment la vitesse de développement, la vitesse d'exécution et un excellent support communautaire, ce qui en fait un excellent choix pour servir votre agent chatbot.
Vous servirez votre agent via une requête POST. La première étape consiste donc à définir les données que vous souhaitez obtenir dans le corps de la requête et les données renvoyées par la requête. FastAPI fait cela avec Pydantic :
from pydantic import BaseModel
class HospitalQueryInput(BaseModel):
text: str
class HospitalQueryOutput(BaseModel):
input: str
output: str
intermediate_steps: list[str]
Dans ce script, vous définissez les modèles Pydantic HospitalQueryInput
et HospitalQueryOutput
. HospitalQueryInput
est utilisé pour vérifier que le corps de la requête POST inclut un champ text
, représentant la requête à laquelle répond votre chatbot. HospitalQueryOutput
vérifie que le corps de la réponse renvoyé à votre utilisateur inclut les champs input
, output
et intermediate_step
.
L'une des fonctionnalités intéressantes de FastAPI réside dans ses capacités de service asynchrone. Étant donné que votre agent appelle des modèles OpenAI hébergés sur un serveur externe, il y aura toujours une latence pendant que votre agent attend une réponse. C’est l’occasion idéale pour vous d’utiliser la programmation asynchrone.
Au lieu d'attendre qu'OpenAI réponde à chacune des demandes de votre agent, vous pouvez demander à votre agent de faire plusieurs demandes d'affilée et de stocker les réponses au fur et à mesure de leur réception. Cela vous fera gagner beaucoup de temps si vous avez plusieurs requêtes auxquelles votre agent doit répondre.
Comme indiqué précédemment, il peut parfois y avoir des problèmes de connexion intermittents avec Neo4j qui sont généralement résolus en établissant une nouvelle connexion. Pour cette raison, vous souhaiterez implémenter une logique de nouvelle tentative qui fonctionne pour les fonctions asynchrones :
import asyncio
def async_retry(max_retries: int=3, delay: int=1):
def decorator(func):
async def wrapper(*args, **kwargs):
for attempt in range(1, max_retries + 1):
try:
result = await func(*args, **kwargs)
return result
except Exception as e:
print(f"Attempt {attempt} failed: {str(e)}")
await asyncio.sleep(delay)
raise ValueError(f"Failed after {max_retries} attempts")
return wrapper
return decorator
Ne vous inquiétez pas des détails de @async_retry
. Tout ce que vous devez savoir, c'est qu'il réessayera une fonction asynchrone en cas d'échec. Vous verrez où cela sera utilisé ensuite.
La logique de pilotage de votre API chatbot se trouve dans chatbot_api/src/main.py
:
from fastapi import FastAPI
from agents.hospital_rag_agent import hospital_rag_agent_executor
from models.hospital_rag_query import HospitalQueryInput, HospitalQueryOutput
from utils.async_utils import async_retry
app = FastAPI(
title="Hospital Chatbot",
description="Endpoints for a hospital system graph RAG chatbot",
)
@async_retry(max_retries=10, delay=1)
async def invoke_agent_with_retry(query: str):
"""Retry the agent if a tool fails to run.
This can help when there are intermittent connection issues
to external APIs.
"""
return await hospital_rag_agent_executor.ainvoke({"input": query})
@app.get("/")
async def get_status():
return {"status": "running"}
@app.post("/hospital-rag-agent")
async def query_hospital_agent(query: HospitalQueryInput) -> HospitalQueryOutput:
query_response = await invoke_agent_with_retry(query.text)
query_response["intermediate_steps"] = [
str(s) for s in query_response["intermediate_steps"]
]
return query_response
Vous importez FastAPI
, votre exécuteur d'agent, les modèles Pydantic que vous avez créés pour la requête POST et @async_retry
. Ensuite, vous instanciez un objet FastAPI
et définissez invoke_agent_with_retry()
, une fonction qui exécute votre agent de manière asynchrone. Le décorateur @async_retry
ci-dessus invoke_agent_with_retry()
garantit que la fonction sera réessayée dix fois avec un délai d'une seconde avant d'échouer.
Enfin, vous définissez query_hospital_agent()
qui envoie des requêtes POST à votre agent sur /hospital-rag-agent. Cette fonction extrait le champ text
du corps de la requête, le transmet à l'agent et renvoie la réponse de l'agent à l'utilisateur.
Vous servirez cette API avec Docker et vous souhaiterez définir le fichier de point d'entrée suivant à exécuter dans le conteneur :
#!/bin/bash
# Run any setup steps or pre-processing tasks here
echo "Starting hospital RAG FastAPI service..."
# Start the main application
uvicorn main:app --host 0.0.0.0 --port 8000
La commande uvicorn main:app --host 0.0.0.0 --port 8000
exécute l'application FastAPI sur le port 8000 de votre machine. Le Dockerfile
pilote de votre application FastAPI ressemble à ceci :
# chatbot_api/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY ./src/ /app
COPY ./pyproject.toml /code/pyproject.toml
RUN pip install /code/.
EXPOSE 8000
CMD ["sh", "entrypoint.sh"]
Ce Dockerfile
indique à votre conteneur d'utiliser la distribution python:3.11-slim
, copiez le contenu de chatbot_api/src/
dans le / app
dans le conteneur, installez les dépendances à partir de pyproject.toml
et exécutez entrypoint.sh
.
La dernière chose que vous devrez faire est de mettre à jour le fichier docker-compose.yml
pour inclure votre conteneur FastAPI :
version: '3'
services:
hospital_neo4j_etl:
build:
context: ./hospital_neo4j_etl
env_file:
- .env
chatbot_api:
build:
context: ./chatbot_api
env_file:
- .env
depends_on:
- hospital_neo4j_etl
ports:
- "8000:8000"
Ici, vous ajoutez le service chatbot_api
qui est dérivé du Dockerfile
dans ./chatbot_api
. Cela dépend de hospital_neo4j_etl
et fonctionnera sur le port 8000.
Pour exécuter l'API, ainsi que l'ETL que vous avez créé précédemment, ouvrez un terminal et exécutez :
$ docker-compose up --build
Si tout se déroule correctement, vous verrez un écran similaire à celui-ci sur http://localhost:8000/docs#/
:
Vous pouvez utiliser la page de documentation pour tester le point de terminaison hospital-rag-agent
, mais vous ne pourrez pas effectuer de requêtes asynchrones ici. Pour voir comment votre point de terminaison gère les requêtes asynchrones, vous pouvez le tester avec une bibliothèque telle que httpx.
Remarque : Vous devez installer httpx dans votre environnement virtuel avant d'exécuter les tests ci-dessous.
Pour voir combien de temps les requêtes asynchrones vous font gagner, commencez par établir un benchmark à l'aide de requêtes synchrones. Créez le script suivant :
import time
import requests
CHATBOT_URL = "http://localhost:8000/hospital-rag-agent"
questions = [
"What is the current wait time at Wallace-Hamilton hospital?",
"Which hospital has the shortest wait time?",
"At which hospitals are patients complaining about billing and insurance issues?",
"What is the average duration in days for emergency visits?",
"What are patients saying about the nursing staff at Castaneda-Hardy?",
"What was the total billing amount charged to each payer for 2023?",
"What is the average billing amount for Medicaid visits?",
"How many patients has Dr. Ryan Brown treated?",
"Which physician has the lowest average visit duration in days?",
"How many visits are open and what is their average duration in days?",
"Have any patients complained about noise?",
"How much was billed for patient 789's stay?",
"Which physician has billed the most to cigna?",
"Which state had the largest percent increase in Medicaid visits from 2022 to 2023?",
]
request_bodies = [{"text": q} for q in questions]
start_time = time.perf_counter()
outputs = [requests.post(CHATBOT_URL, json=data) for data in request_bodies]
end_time = time.perf_counter()
print(f"Run time: {end_time - start_time} seconds")
Dans ce script, vous importez des demandes
et du temps
, définissez l'URL de votre chatbot, créez une liste de questions et enregistrez le temps nécessaire pour obtenir une réponse. toutes les questions de la liste. Si vous ouvrez un terminal et exécutez sync_agent_requests.py
, vous verrez combien de temps il faut pour répondre aux 14 questions :
(venv) $ python tests/sync_agent_requests.py
Run time: 68.20339595794212 seconds
Vous pouvez obtenir des résultats légèrement différents en fonction de votre vitesse Internet et de la disponibilité du modèle de chat, mais vous pouvez voir que l'exécution de ce script a pris environ 68 secondes. Ensuite, vous obtiendrez des réponses aux mêmes questions de manière asynchrone :
import asyncio
import time
import httpx
CHATBOT_URL = "http://localhost:8000/hospital-rag-agent"
async def make_async_post(url, data):
timeout = httpx.Timeout(timeout=120)
async with httpx.AsyncClient() as client:
response = await client.post(url, json=data, timeout=timeout)
return response
async def make_bulk_requests(url, data):
tasks = [make_async_post(url, payload) for payload in data]
responses = await asyncio.gather(*tasks)
outputs = [r.json()["output"] for r in responses]
return outputs
questions = [
"What is the current wait time at Wallace-Hamilton hospital?",
"Which hospital has the shortest wait time?",
"At which hospitals are patients complaining about billing and insurance issues?",
"What is the average duration in days for emergency visits?",
"What are patients saying about the nursing staff at Castaneda-Hardy?",
"What was the total billing amount charged to each payer for 2023?",
"What is the average billing amount for Medicaid visits?",
"How many patients has Dr. Ryan Brown treated?",
"Which physician has the lowest average visit duration in days?",
"How many visits are open and what is their average duration in days?",
"Have any patients complained about noise?",
"How much was billed for patient 789's stay?",
"Which physician has billed the most to cigna?",
"Which state had the largest percent increase in Medicaid visits from 2022 to 2023?",
]
request_bodies = [{"text": q} for q in questions]
start_time = time.perf_counter()
outputs = asyncio.run(make_bulk_requests(CHATBOT_URL, request_bodies))
end_time = time.perf_counter()
print(f"Run time: {end_time - start_time} seconds")
Dans async_agent_requests.py
, vous effectuez la même requête que dans sync_agent_requests.py
, sauf que vous utilisez maintenant httpx
pour effectuer les requêtes de manière asynchrone. Voici les résultats:
(venv) $ python tests/async_agent_requests.py
Run time: 17.766680584056303 seconds
Encore une fois, le temps exact d'exécution peut varier pour vous, mais vous pouvez constater que l'exécution de 14 requêtes de manière asynchrone était environ quatre fois plus rapide. Le déploiement asynchrone de votre agent vous permet d'évoluer vers un volume de requêtes élevé sans avoir à augmenter les demandes de votre infrastructure. Bien qu'il y ait toujours des exceptions, servir les points de terminaison REST de manière asynchrone est généralement une bonne idée lorsque votre code effectue des requêtes liées au réseau.
Grâce au fonctionnement de ce point de terminaison FastAPI, vous avez rendu votre agent accessible à toute personne pouvant accéder au point de terminaison. C’est idéal pour intégrer votre agent dans les interfaces utilisateur des chatbots, ce que vous ferez ensuite avec Streamlit.
Créez une interface utilisateur de chat avec Streamlit
Vos parties prenantes ont besoin d'un moyen d'interagir avec votre agent sans faire de requêtes API manuelles. Pour répondre à cela, vous créerez une application Streamlit qui agit comme une interface entre vos parties prenantes et votre API. Voici les dépendances de l'interface utilisateur Streamlit :
[project]
name = "chatbot_frontend"
version = "0.1"
dependencies = [
"requests==2.31.0",
"streamlit==1.29.0"
]
[project.optional-dependencies]
dev = ["black", "flake8"]
Le code de conduite de votre application Streamlit se trouve dans chatbot_frontend/src/main.py
:
import os
import requests
import streamlit as st
CHATBOT_URL = os.getenv("CHATBOT_URL", "http://localhost:8000/hospital-rag-agent")
with st.sidebar:
st.header("About")
st.markdown(
"""
This chatbot interfaces with a
[LangChain](https://python.langchain.com/docs/get_started/introduction)
agent designed to answer questions about the hospitals, patients,
visits, physicians, and insurance payers in a fake hospital system.
The agent uses retrieval-augment generation (RAG) over both
structured and unstructured data that has been synthetically generated.
"""
)
st.header("Example Questions")
st.markdown("- Which hospitals are in the hospital system?")
st.markdown("- What is the current wait time at wallace-hamilton hospital?")
st.markdown(
"- At which hospitals are patients complaining about billing and "
"insurance issues?"
)
st.markdown("- What is the average duration in days for closed emergency visits?")
st.markdown(
"- What are patients saying about the nursing staff at "
"Castaneda-Hardy?"
)
st.markdown("- What was the total billing amount charged to each payer for 2023?")
st.markdown("- What is the average billing amount for medicaid visits?")
st.markdown("- Which physician has the lowest average visit duration in days?")
st.markdown("- How much was billed for patient 789's stay?")
st.markdown(
"- Which state had the largest percent increase in medicaid visits "
"from 2022 to 2023?"
)
st.markdown("- What is the average billing amount per day for Aetna patients?")
st.markdown("- How many reviews have been written from patients in Florida?")
st.markdown(
"- For visits that are not missing chief complaints, "
"what percentage have reviews?"
)
st.markdown(
"- What is the percentage of visits that have reviews for each hospital?"
)
st.markdown(
"- Which physician has received the most reviews for this visits "
"they've attended?"
)
st.markdown("- What is the ID for physician James Cooper?")
st.markdown(
"- List every review for visits treated by physician 270. Don't leave any out."
)
st.title("Hospital System Chatbot")
st.info(
"Ask me questions about patients, visits, insurance payers, hospitals, "
"physicians, reviews, and wait times!"
)
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
if "output" in message.keys():
st.markdown(message["output"])
if "explanation" in message.keys():
with st.status("How was this generated", state="complete"):
st.info(message["explanation"])
if prompt := st.chat_input("What do you want to know?"):
st.chat_message("user").markdown(prompt)
st.session_state.messages.append({"role": "user", "output": prompt})
data = {"text": prompt}
with st.spinner("Searching for an answer..."):
response = requests.post(CHATBOT_URL, json=data)
if response.status_code == 200:
output_text = response.json()["output"]
explanation = response.json()["intermediate_steps"]
else:
output_text = """An error occurred while processing your message.
Please try again or rephrase your message."""
explanation = output_text
st.chat_message("assistant").markdown(output_text)
st.status("How was this generated", state="complete").info(explanation)
st.session_state.messages.append(
{
"role": "assistant",
"output": output_text,
"explanation": explanation,
}
)
L'apprentissage de Streamlit n'est pas l'objet de ce didacticiel, vous n'obtiendrez donc pas de description détaillée de ce code. Cependant, voici un aperçu général de ce que fait cette interface utilisateur :
- L'intégralité de l'historique des discussions est stockée et affichée chaque fois que l'utilisateur effectue une nouvelle requête.
- L’interface utilisateur prend les entrées de l’utilisateur et envoie une requête POST synchrone au point de terminaison de l’agent.
- La réponse la plus récente de l'agent est affichée au bas du chat et ajoutée à l'historique du chat.
- Une explication de la façon dont l'agent a généré sa réponse fournie à l'utilisateur. C'est idéal à des fins d'audit, car vous pouvez voir si l'agent a appelé le bon outil et vous pouvez vérifier si l'outil a fonctionné correctement.
Une fois que vous l'avez fait, vous allez créer un fichier de point d'entrée pour exécuter l'interface utilisateur :
#!/bin/bash
# Run any setup steps or pre-processing tasks here
echo "Starting hospital chatbot frontend..."
# Run the ETL script
streamlit run main.py
Et enfin, le fichier Docker pour créer une image pour l'UI :
FROM python:3.11-slim
WORKDIR /app
COPY ./src/ /app
COPY ./pyproject.toml /code/pyproject.toml
RUN pip install /code/.
CMD ["sh", "entrypoint.sh"]
Ce Dockerfile
est identique aux précédents que vous avez créés. Avec cela, vous êtes prêt à exécuter l’intégralité de votre application chatbot de bout en bout.
Orchestrer le projet avec Docker Compose
À ce stade, vous avez écrit tout le code nécessaire pour exécuter votre chatbot. Cette dernière étape consiste à créer et exécuter votre projet avec docker-compose
. Avant de faire cela, assurez-vous que vous disposez de tous les fichiers et dossiers suivants dans le répertoire de votre projet :
./
│
├── chatbot_api/
│ │
│ │
│ ├── src/
│ │ │
│ │ ├── agents/
│ │ │ └── hospital_rag_agent.py
│ │ │
│ │ ├── chains/
│ │ │ │
│ │ │ ├── hospital_cypher_chain.py
│ │ │ └── hospital_review_chain.py
│ │ │
│ │ ├── models/
│ │ │ └── hospital_rag_query.py
│ │ │
│ │ ├── tools/
│ │ │ └── wait_times.py
│ │ │
│ │ ├── utils/
│ │ │ └── async_utils.py
│ │ │
│ │ ├── entrypoint.sh
│ │ └── main.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── chatbot_frontend/
│ │
│ ├── src/
│ │ ├── entrypoint.sh
│ │ └── main.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── hospital_neo4j_etl/
│ │
│ ├── src/
│ │ ├── entrypoint.sh
│ │ └── hospital_bulk_csv_write.py
│ │
│ ├── Dockerfile
│ └── pyproject.toml
│
├── tests/
│ ├── async_agent_requests.py
│ └── sync_agent_requests.py
│
├── .env
└── docker-compose.yml
Votre fichier .env
doit avoir les variables d'environnement suivantes. Vous avez créé la plupart d'entre eux plus tôt dans ce didacticiel, mais vous devrez également en ajouter un nouveau pour CHATBOT_URL
afin que votre application Streamlit puisse trouver votre API :
OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
NEO4J_URI=<YOUR_NEO4J_URI>
NEO4J_USERNAME=<YOUR_NEO4J_USERNAME>
NEO4J_PASSWORD=<YOUR_NEO4J_PASSWORD>
HOSPITALS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/hospitals.csv
PAYERS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/payers.csv
PHYSICIANS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/physicians.csv
PATIENTS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/patients.csv
VISITS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/visits.csv
REVIEWS_CSV_PATH=https://raw.githubusercontent.com/hfhoffman1144/langchain_neo4j_rag_app/main/data/reviews.csv
HOSPITAL_AGENT_MODEL=gpt-3.5-turbo-1106
HOSPITAL_CYPHER_MODEL=gpt-3.5-turbo-1106
HOSPITAL_QA_MODEL=gpt-3.5-turbo-0125
CHATBOT_URL=http://host.docker.internal:8000/hospital-rag-agent
Pour compléter votre fichier docker-compose.yml
, vous devrez ajouter un service chatbot_frontend
. Votre fichier docker-compose.yml
final devrait ressembler à ceci :
version: '3'
services:
hospital_neo4j_etl:
build:
context: ./hospital_neo4j_etl
env_file:
- .env
chatbot_api:
build:
context: ./chatbot_api
env_file:
- .env
depends_on:
- hospital_neo4j_etl
ports:
- "8000:8000"
chatbot_frontend:
build:
context: ./chatbot_frontend
env_file:
- .env
depends_on:
- chatbot_api
ports:
- "8501:8501"
Enfin, ouvrez un terminal et exécutez :
$ docker-compose up --build
Une fois que tout est construit et exécuté, vous pouvez accéder à l'interface utilisateur sur http://localhost:8501/
et commencer à discuter avec votre chatbot :
Vous avez créé un chatbot pour un système hospitalier entièrement fonctionnel de bout en bout. Prenez le temps de lui poser des questions, de voir à quels types de questions il sait répondre, de découvrir où il échoue et de réfléchir à la façon dont vous pourriez l'améliorer avec de meilleures invites ou de meilleures données. Vous pouvez commencer par vous assurer que les exemples de questions dans la barre latérale reçoivent une réponse réussie.
Conclusion
Félicitations pour avoir terminé ce didacticiel approfondi !
Vous avez conçu, construit et servi avec succès un chatbot RAG LangChain qui répond aux questions sur un faux système hospitalier. Il existe certainement de nombreuses façons d'améliorer le chatbot que vous avez créé dans ce didacticiel, mais vous comprenez maintenant bien comment intégrer LangChain à vos propres données, vous donnant ainsi la liberté créative de créer toutes sortes de chatbots personnalisés.
Dans ce didacticiel, vous avez appris à :
- Utilisez LangChain pour créer des chatbots personnalisés.
- Créez un chatbot pour un faux système hospitalier en vous alignant sur les exigences commerciales et en exploitant les données disponibles.
- Envisagez la mise en œuvre de bases de données graphiques dans la conception de votre chatbot.
- Configurez une instance Neo4j AuraDB pour votre projet.
- Développer un chatbot RAG capable de récupérer des données structurées et non structurées de Neo4j.
- Déployez votre chatbot à l'aide de FastAPI et Streamlit.
Vous pouvez trouver le code source complet et les données de ce projet, dans les documents de support, que vous pouvez télécharger en utilisant le lien ci-dessous :