352 lines
13 KiB
Plaintext
352 lines
13 KiB
Plaintext
---
|
|
id: development
|
|
title: Building Your Chatbot
|
|
sidebar_label: Building Your Chatbot
|
|
---
|
|
|
|
In this section, we are going to create a **multi-turn** chatbot that can use various tools to diagnose and schedule appointments for users based on their symptoms.
|
|
We will be using `langchain` and `qdrant` to build our chatbot, with functionalies including a:
|
|
|
|
- **RAG pipeline** to retrieve medical knowledge to diagnose patients
|
|
- **Custom tools** to create new appointments based on patient symptoms
|
|
- **Memory system** to keep track of chat histories
|
|
|
|
We'll also implement our chatbot with an independent **model and system prompt** variable - which we'll be evaluating in the next section.
|
|
|
|
:::tip
|
|
If you already have a multi-turn chatbot that you want to evaluate, feel free to skip to the [**evaluation section of this tutorial**](/tutorials/medical-chatbot/evaluation).
|
|
:::
|
|
|
|
## Setup Your Model
|
|
|
|
First create a `MedicalChatbot` class and use `langchain`'s chat models to call `OpenAI`:
|
|
|
|
```python title="main.py"
|
|
from langchain_openai import ChatOpenAI
|
|
|
|
class MedicalChatbot:
|
|
def __init__(self, model: str):
|
|
self.model = ChatOpenAI(model=model)
|
|
# Choose the LLM that will drive the agent
|
|
# Only certain models support this so ensure your model supports it as well
|
|
```
|
|
|
|
:::note
|
|
You can also use other interfaces to call OpenAI, or any other model.
|
|
:::
|
|
|
|
Try prompting it with a messages array:
|
|
|
|
```python title="main.py"
|
|
chatbot = MedicalChatbot(model="gpt-4o-mini")
|
|
chatbot.model.invoke([{"user": "Hi!"}])
|
|
```
|
|
|
|
Which should let you see something like this:
|
|
|
|
```text
|
|
AIMessage(
|
|
content="Hey, how can I help you today?",
|
|
additional_kwargs={},
|
|
response_metadata={
|
|
'prompt_feedback': {'block_reason': 0, 'safety_ratings': []},
|
|
'finish_reason': 'STOP',
|
|
'model_name': 'gpt-4o-mini',
|
|
'safety_ratings': []
|
|
},
|
|
id='run--c2786aa1-75c4-4644-ae59-9327a2e8c153-0',
|
|
usage_metadata={'input_tokens': 23, 'output_tokens': 417, 'total_tokens': 440, 'input_token_details': {'cache_read': 0}}
|
|
)
|
|
```
|
|
|
|
✅ Done. Now let's create some tools for the chatbot to start booking appointments.
|
|
|
|
## Create RAG Pipeline For Diagnosis
|
|
|
|
Since OpenAI models weren't specifically trained on medical knowledge, we'll need to leverage RAG to provide additional context at runtime to diagnose patients that are grounded in context.
|
|
|
|
:::info
|
|
We'll be using a text version of [The Gale Encyclopedia of Alternative Medicine](https://dl.icdst.org/pdfs/files/03cb46934164321f675385fb74ac1bed.pdf) as our knowledge base in this example. You will need to download it locally and convert it to a `.txt` file.
|
|
:::
|
|
|
|
### Index medical knowledge
|
|
|
|
We'll ingest "The Gale Encyclopedia of Alternative Medicine" to Qdrant, a popular vector database choice for fast and accurate retrievals:
|
|
|
|
```python title="main.py"
|
|
from qdrant_client import models, QdrantClient
|
|
from sentence_transformers import SentenceTransformer
|
|
from langchain_openai import ChatOpenAI
|
|
|
|
class MedicalChatbot:
|
|
def __init__(self, model: str):
|
|
self.model = ChatOpenAI(model=model)
|
|
# For RAG engine
|
|
self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
|
|
self.client = QdrantClient(":memory:")
|
|
|
|
def index_knowledge(self, document_path: str):
|
|
with open(document_path) as file:
|
|
documents = file.readlines()
|
|
|
|
# Create namespace in qdrant
|
|
self.client.create_collection(
|
|
collection_name="gale_encyclopedia",
|
|
vectors_config=models.VectorParams(size=self.encoder.get_sentence_embedding_dimension(), distance=models.Distance.COSINE),
|
|
)
|
|
|
|
# Vectorize and index into qdrant
|
|
self.client.upload_points(
|
|
collection_name="gale_encyclopedia",
|
|
points=[models.PointStruct(id=idx, vector=self.encoder.encode(doc).tolist(), payload={"content": doc}) for idx, doc in enumerate(documents)],
|
|
)
|
|
```
|
|
|
|
Then, simply run your `index_knowledge` method using the encyclopedia you've downloaded as `.txt`:
|
|
|
|
```python title="main.py"
|
|
chatbot = MedicalChatbot()
|
|
chatbot.index_knowledge("path-to-your-encyclopedia.txt")
|
|
```
|
|
|
|
✅ Done. Now let's try querying it to sanity check yourself.
|
|
|
|
:::note
|
|
You only have the run `index_knowledge` once.
|
|
:::
|
|
|
|
### Query your knowledge base
|
|
|
|
Simply implement a **TOOL** to query from qdrant. in this case `retrieve_knowledge`:
|
|
|
|
```python title="main.py" {14}
|
|
from qdrant_client import models, QdrantClient
|
|
from sentence_transformers import SentenceTransformer
|
|
from langchain_openai import ChatOpenAI
|
|
from langchain_core.tools import tool
|
|
|
|
class MedicalChatbot:
|
|
def __init__(self, model: str):
|
|
self.model = ChatOpenAI(model=model)
|
|
# For RAG engine
|
|
self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
|
|
self.client = QdrantClient(":memory:")
|
|
|
|
@tool
|
|
def retrieve_knowledge(self, query: str) -> str:
|
|
""""A tool to retrieve data on various diagnosis methods from gale encyclopedia"""
|
|
hits = self.client.query_points(collection_name="gale_encyclopedia", query=self.encoder.encode(query).tolist(), limit=3).points
|
|
|
|
contexts = [hit.payload['content'] for hit in hits]
|
|
return "\n".join(contexts)
|
|
|
|
def index_knowledge(self, document_path: str):
|
|
# Same as above
|
|
pass
|
|
```
|
|
|
|
:::info
|
|
The `@tool` decorator tells `langchain` that the `retrieve_knowledge` method can be called as a function call and will come in handy in later sections.
|
|
:::
|
|
|
|
Now try calling it:
|
|
|
|
```python title="main.py"
|
|
chatbot = MedicalChatbot()
|
|
chatbot.retrieve_knowledge("Cough, fever, and diarrhea.")
|
|
```
|
|
|
|
Great! Now that we have the essentials for making a diagnosis, time to move on to implementing a way to book appointments after a diagnosis.
|
|
|
|
## Create Tool To Book Appointments
|
|
|
|
Since we need a way for our chatbot to book appointments based on the diagnosis at hand, this section will focus on creating the tools required to do so. There's only one tool for booking appointments for the sake of simplicity:
|
|
|
|
- `create_appointment`: Creates a new appointment **in memory** (you can also use something like SQLite for persistent storage)
|
|
|
|
First, let's create a simple data model for appointments:
|
|
|
|
```python title="main.py"
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, List
|
|
from datetime import date
|
|
|
|
class Appointment(BaseModel):
|
|
id: str
|
|
name: str
|
|
email: str
|
|
date: date
|
|
symptoms: Optional[List[str]] = Field(default=None)
|
|
diagnosis: Optional[str] = Field(default=None)
|
|
```
|
|
|
|
Now let's implement the `create_appointment` tool:
|
|
|
|
```python title="main.py" {14}
|
|
import uuid
|
|
|
|
...
|
|
|
|
class MedicalChatbot:
|
|
def __init__(self, model: str):
|
|
self.model = ChatOpenAI(model=model)
|
|
# For RAG engine
|
|
self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
|
|
self.client = QdrantClient(":memory:")
|
|
# For managing appointments
|
|
self.appointments: List[Appointment] = []
|
|
|
|
@tool
|
|
def create_appointment(self, name: str, email: str, date: str) -> str:
|
|
"""Create a new appointment with the given ID, name, email, and date"""
|
|
try:
|
|
appointment = Appointment(
|
|
id=str(uuid.uuid4()),
|
|
name=name,
|
|
email=email,
|
|
date=date.fromisoformat(date)
|
|
)
|
|
self.appointments.append(appointment)
|
|
return f"Created new appointment with ID: {appointment.id} for {name} on {date}."
|
|
except ValueError:
|
|
return f"Invalid date format. Please use YYYY-MM-DD format."
|
|
|
|
@tool
|
|
def retrieve_knowledge(self, query: str) -> str:
|
|
# Same as above
|
|
pass
|
|
|
|
def index_knowledge(self, document_path: str):
|
|
# Same as above
|
|
pass
|
|
```
|
|
|
|
Great! Now let's glue everything together using LangChain.
|
|
|
|
## Implementing Chat Histories
|
|
|
|
First create a helper method that retrieves conversation histories, which would be required for our LLM:
|
|
|
|
```python title"main.py"
|
|
from langchain_community.chat_message_histories import ChatMessageHistory
|
|
from langchain_core.chat_history import BaseChatMessageHistory
|
|
|
|
# Simple in-memory store for chat histories
|
|
chat_store = {}
|
|
def get_session_history(session_id: str) -> BaseChatMessageHistory:
|
|
if session_id not in chat_store:
|
|
chat_store[session_id] = ChatMessageHistory()
|
|
return chat_store[session_id]
|
|
```
|
|
|
|
Then we'll combine the agent setup and memory functionality into one clean implementation, including the `retrieve_knowledge` and `create_appointment` tools in our agent:
|
|
|
|
```python title="main.py" {20,28-29,33}
|
|
from langchain.agents import create_tool_calling_agent, AgentExecutor
|
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
from langchain_core.runnables.history import RunnableWithMessageHistory
|
|
from langchain_core.tools import StructuredTool
|
|
...
|
|
|
|
class MedicalChatbot:
|
|
def __init__(self, model: str, system_prompt: str):
|
|
self.model = ChatOpenAI(model=model)
|
|
self.system_prompt = system_prompt
|
|
# For RAG engine
|
|
self.encoder = SentenceTransformer("all-MiniLM-L6-v2")
|
|
self.client = QdrantClient(":memory:")
|
|
# For managing appointments
|
|
self.appointments: List[Appointment] = []
|
|
|
|
# Setup agent with memory
|
|
self.setup_agent()
|
|
|
|
def setup_agent(self):
|
|
"""Setup the agent with tools and memory"""
|
|
|
|
# Create prompt messages
|
|
prompt = ChatPromptTemplate.from_messages([("system", self.system_prompt), MessagesPlaceholder(variable_name="chat_history"), ("human", "{input}")])
|
|
|
|
# Create agent
|
|
tools = [
|
|
StructuredTool.from_function(func=self.retrieve_knowledge),
|
|
StructuredTool.from_function(func=self.create_appointment)
|
|
]
|
|
agent = create_tool_calling_agent(self.model, tools, prompt)
|
|
agent_executor = AgentExecutor(agent=agent, tools=tools)
|
|
self.agent_with_memory = RunnableWithMessageHistory(
|
|
agent_executor,
|
|
get_session_history,
|
|
input_messages_key="input",
|
|
history_messages_key="chat_history",
|
|
)
|
|
|
|
# Other methods from above goes here
|
|
...
|
|
```
|
|
|
|
🎉🥳 Congratulations! You've just created a fully functional medical chatbot with memory, the ability to diagnose users, and book appointments when needed.
|
|
|
|
## Eyeball Your First Output
|
|
|
|
Now that you have your chatbot, its time to query it to see if it lives up to your expectations. Create a method so you can interact with it in the CLI, and **supply your model and choice and system prompt**:
|
|
|
|
```python title="main.py" {22,23,28}
|
|
def start_session(session_id: Optional[str] = None):
|
|
"""Start an interactive session with the chatbot"""
|
|
print("Hello! I am Baymax, your personal healthcare companion.")
|
|
print("How are you feeling today? (type 'exit' to quit."))
|
|
|
|
while True:
|
|
if session_id is None:
|
|
session_id = str(uuid.uuid4())
|
|
|
|
user_input = input("Your query: ")
|
|
if user_input.lower() == 'exit':
|
|
break
|
|
|
|
response = chatbot.agent_with_memory.invoke(
|
|
{"input": user_input},
|
|
config={"configurable": {"session_id": session_id}}
|
|
)
|
|
print("Baymax:", response["output"])
|
|
|
|
|
|
# These parameters will be evaluated later
|
|
MODEL = "gpt-4o-mini"
|
|
SYSTEM_PROMPT = """"""
|
|
|
|
# Initialize chatbot and start session
|
|
chatbot = MedicalChatbot(model=MODEL, system_prompt=SYSTEM_PROMPT)
|
|
chatbot.index_knowledge("path-to-your-encyclopedia.txt")
|
|
start_session()
|
|
```
|
|
|
|
:::tip
|
|
The model and system prompt are the variables you'll want to be improving in the next section.
|
|
:::
|
|
|
|
Then simply execute the file in the CLI:
|
|
|
|
```bash
|
|
python main.py
|
|
```
|
|
|
|
Here's an example conversation:
|
|
|
|
```text
|
|
Hello! I am Baymax, your personal healthcare companion.
|
|
How are you feeling today? (type 'exit' to quit.")
|
|
|
|
Your query: Hello Baymax, I've been feeling unwell. I have a fever and a sore throat.
|
|
Baymax: I'm sorry to hear you're not feeling well. A fever and sore throat are common symptoms of upper respiratory infections. Based on medical knowledge, rest, fluids, and over-the-counter pain relievers can help. If your symptoms are severe or persistent, it's best to consult a doctor. Would you like me to help you schedule an appointment?
|
|
|
|
Your query: Yes, please book an appointment for tomorrow at 2 PM.
|
|
Baymax: I'll help you create an appointment. I need your name and email to proceed. Could you please provide those details?
|
|
|
|
Your query: My name is John Doe and my email is john@example.com
|
|
Baymax: Created new appointment with ID: 550e8400-e29b-41d4-a716-446655440000 for John Doe on 2024-01-16.
|
|
```
|
|
|
|
Was this what you really wanted? Is this diagnosis a good one? Was the appointment booked appropriately? The process of making these judgements yourself are known as **eyeballing** LLM outputs. It works, but isn't very scalable nor reliable - especially when conversations get long and you find yourself skimming instead of evaluating.
|