A basic toolkit to experiment with LLM-powered process-driven chatbots. It is Experimental and not meant to be used in a prod setup and is based on Pydantic and Langchain
Process-driven chatbots assist users in completing tasks by guiding them through a sequence of steps, such as opening a bank account or scheduling an appointment.
This expermiments with:
Example:
AI: Hello! I'm here to help you book an appointment at our hair salon. To get started, could you please let me know your availability? We have the following options: Thursday 20 at 23:45, Saturday 15 at 03:45, or Wednesday 19 at 00:45.
USER: last one is great
entities:
- availability: Wednesday 19 at 00:45
The Process
which collects data from the user (an approach borrowed from Rasa forms).
The experiment consists here in using the business logic to format the prompt such that
the model has the essential information to "reason" and formulate a next AI message.
Currently it works with gpt-3.5-turbo
and shines with gpt-4
.
- 🧑 Customer-Service-GPT
Requirements: python 3.10 and Poetry
git clone https://github.com/znat/customer-service-GPT/
poetry install
Create an .env
file at the root containing your OpenAI API key.
OPENAI_API_KEY=<key>
An AI medical assistant helps users book appointments at the clinic by trying to find matching availability, answering questions, and collecting necessary information to confirm the appointment
poetry run python -m examples.appointment_booking.example_booking_bot
This bot replicates the Google Duplex demo, in which an AI schedules a hair salon appointment. The AI is the customer assistant and the User is the hair salon attendant.
poetry run python -m examples.appointment_booking.example_duplex_bot
The ProcessChain
is a sequential Langchain chain working as follows:
NERChain
extracts entities from the user response based on examplesValidationChain
stores entity values in variables if they are validConversationChain
evaluates the context and asks the next question or provides feedback.
The ProcessChain
outputs a result
object with the information collected when the task is completed.
The process is a pydantic model where fields are described. The fields will be collected during the process using the questions provided, validated with the validators and the errors messages will be surfaced in the conversation.
class SimpleForm(Process):
process_description = """
You are a retail bank account executive AI.
Your goal is to collect the required information from the User to open a bank account.
"""
first_name: Optional[str] = Field(
name="First name",
description="First name of the user, required to open an account",
question="What is your first name?",
)
age: Optional[int] = Field(
name="Age",
description="Age of the user, required to open an account",
question="What is your age?",
)
@validator("first_name")
def validate_first_name(cls, v):
assert v[0].isalpha(), "First name must start with a letter."
return v.capitalize()
@validator("age")
def validate_age(cls, v):
assert v is None or v >= 18, "Age must be 18 or older"
return v
Although entities generally match form fields, they are not the same. Entities are extracted from the user input, then processed by the ValidationChain
which may decide to save values to variables.
examples = [
{"text": "I'm Nathan", "entities": [{"name": "first_name", "value": "Nathan"}]},
{
"text": "I'm jenny and I'm 98 yo",
"entities": [
{"name": "first_name", "value": "Jenny"},
{"name": "age", "value": 98},
],
},
{
"text": "I'm Jo Neville and I'm a plumber",
"entities": [
{"name": "first_name", "value": "Jo"},
{"name": "last_name", "value": "Neville"},
],
},
{"context": "How old are you?", "text": "Yes", "entities": []},
]
Notice that the value
might not be related to a particular substring in the text
. Observe the last two examples which aim to capture a confirmation based on a yes or no. We want the entity to be extracted only in the context
of confirming data, but not if the context
of answering a specific question.
The ProcessChain
glues everything together.
openai_entity_extractor_llm = ChatOpenAI(temperature=0, client=None, max_tokens=256)
openai_chat_llm = ChatOpenAI(temperature=0, client=None, max_tokens=256, model="gpt-4) # Note: wor
process_chain = ProcessChain(
ner_llm=openai_entity_extractor_llm,
chat_llm=openai_chat_llm,
entities={
"first_name": Entity,
"age": IntEntity,
"last_name": Entity,
"confirmed": BooleanEntity,
},
entity_examples=[EntityExample.parse_obj(e) for e in examples],
form=MyForm,
memory=ConversationMemory(),
verbose=True,
)
Then launch the bot and open the url provided:
gradio_bot(chain=process_chain, initial_input="hey", title="FormBot").launch() # Hey is a trick to get the bot to start the conversation as it normally reacts to a user input
See code examples for more details and more complex entities such as dates.
A process contains a process_description
that will be injected in the prompt. Here is an example:
process_description = f"""
You are a hair salon attendant AI. The User wants to book and appointment
You can share the following information with the User if the User asks:
- Address of the salon is 123 Main Street, CoolVille, QC, H3Z 2Y7
- The salon phone number is 514-666-7777
To all other questions reply you don't know.
"""
A process yield a completion Result
so the ProcessChain
can be invoked by other chain. The ProcessChain
can execute in a while
loop until a Result
object is output.
To ouput a result you can use the two following Process
methods:
class MyProcess(Process):
...
def is_completed(self) -> bool:
return ... # A sucess condition, e.g a confirmation set to `True`
def is_failed(self) -> bool:
return ... # A failure condition, e.g an error counter reaching a certain value
A Process
is a pydantic
model and thus leverages built-in validation methods.
One way to validate is to make an assertion in a field @validator
function.
The error message will be surfaced to the user.
phone_number: Optional[str] = Field(
name="Phone number",
description="Phone number of the user, required to contact a patient in case of unexpected events, such as delays or cancellations",
question="What is your phone number?",
)
@validator("phone_number")
def validate_phone_number(cls, v):
import re
pattern = re.compile(r"^\d{3}-\d{3}-\d{4}$")
assert v is None or pattern.match(
v
), f"Can you please provide a valid phone number using the XXX-XXX-XXXX format?"
return v
Note that you can also interpolate variables in the error message as follows: {{variables}}
If you need to use another variable for your validation or want to set a variable based on the content of another you can use the pydantic @root_validator
:
class MyProcess(Process):
# This field has a question, so the question will be asked to the user
availability: Optional[dict | str] = Field(
title="Availability for doctor appointment",
question="Would you be available {{matching_slots_in_human_friendly_format}}",
description=f"Providing availability helps finding an available slot in the salon's calendar",
)
# This field has NO question, so it will not be surfaced by the user
# but can used as a working/internal processing variable.
appointment_time: Optional[str] = Field(
title="Appointment in human frendly format",
aknowledgement="Great, we have booked an appointment on {{human_friendly_appointment_time}}",
)
@root_validator(pre=True)
def validate(cls, values: dict):
...
# Let's define a default value in case no availability is provided yet or
# there is no matching slot between the user availability and the salon availability
values["appointment_time"] = ... # Add some logic to set appointement time
...
return values
If you want to surface validation errors but still need the object to be successfully instantiated so that updated variables are available in the conversation or can be interpolated, you can also add errors as follows in the @root_validator
@root_validator(pre=True)
def validate(cls, values: dict):
...
values["_errors"] = {
# the key is the variable for which the feedback is given
"availability": "Sorry, we're not available at these dates, but we can offer {{other_slots}}
}
...
return values
This should be surfaced to the user in the next AI message.
All variables can be interpolated in questions. For example, the appointment_time
could be used later in the process.
class MyProcess(process):
...
# This var is set by the validator.
appointment_time: Optional[str] = Field(
title="Appointment in human frendly format",
)
# And can be used in another question
confirm: Optional[str] = Field(
title="Do you confirm the appointment on {{appointment_time}}?",
)
The NERChain
is a simple few-shots named entity regognition. It is used by the ProcessChain
but you can also experiment directly with it.
examples =[
{
"text": "I'm Jo Neville and I'm a plumber",
"entities": [
{"name": "first_name", "value": "Jo"},
{"name": "last_name", "value": "Neville"},
{"name": "occupation", "value": "plumber"},
],
},
...
]
entities = {
"first_name": Entity,
"last_name": Entity,
"occupation": Entity,
"email": EmailEntity,
}
ner_chain = NERChain(
llm=entity_extractor_llm,
additional_instructions="""
"confirmation" entity should only be `true` if the gives an explicity confirmation that all the collected information is correct
and not if the user just says "yes" or confirms but asks follow-up questions.
""",
verbose=True,
entities=entities,
examples=[EntityExample.parse_obj(e) for e in examples],
)
The context is the last bot utterance prior the user utterance were entities are extracted,
A benefit of LLMs is they pickup simple transformations quite well. The entity value does not need to be
a substring of the user utterance (text
). In the following example, the context helps the model understand what
"The second option" is referring to
- context: 'I can propose Wednesday 14 at 09:00, Friday 16 at 11:00 or Sunday 18 at 11:00.'
text: 'the second option is great'
entities:
- name: availability
value: 'Friday 16 at 11:00'
The DateTimeEntity
is an illlustration of how an LLM can be use for more rigourous and structured transformation.
DateTimeEntity
will output an ISO formatted timespan. Here are a few examples:
text: "Any availability next week?"
entities:
[
{
"name":"availability",
"value":{
"start":"2023-06-26T00:00:00",
"end":"2023-07-02T23:59:59",
"grain":604800
}
}
]
You can try it with:
poetry run python example_ner_chain.py