Chatbot Development With ChatGPT & LangChain A Context-Aware Approach DataCamp
Chatbot Development With ChatGPT & LangChain A Context-Aware Approach DataCamp
T U TO R I A L Category
Andrea Valenzuela
A data expert at CERN, democratizing tech learning. Skilled in data engineering and
analysis.
TO P I C S
In today’s fast-paced digital landscape, with the rise of Large Language Models (LLMs),
conversational applications have gained immense popularity. Chatbots have transformed
the way we interact with applications, websites, and even customer service channels in our
daily lives.
The ability to build your own chatbot has emerged as a game-changer, empowering
businesses and individuals to create personalized, efficient, and engaging conversational
interfaces tailored to their specific needs.
This article aims to break down how to build a chatbot in Python leveraging the power of
LLMs. We will explore how to create our own chatbot using ChatGPT and how to further
optimize it using the popular LangChain framework.
In the following sections, we will explore how to use the OpenAI API to do basic API calls to
ChatGPT as the building blocks of our conversation. Additionally, we will implement
context awareness for our chatbot using basic Python structures. Finally, we will optimize
both the chatbot implementation and the conversation history using LangChain and
explore its memory features.
The following Python function embeds our desired API call to ChatGPT using only the
openai library.
import os
import openai
openai_api_key = os.environ["OPENAI_API_KEY"]
Note that in this case, we are using the gpt-3.5-turbo model since it is the most capable
GPT-3.5 model and it is optimized for chat, being also one of the cheapest models. More
information about the different models available can be found in the OpenAI
documentation.
Do you need help getting your OpenAI API keys? Then the training Getting Started with
the OpenAI API and ChatGPT is for you.
The function chatgpt_call() is a wrapper around the original OpenAI function call
ChatCompletion.create that creates a chat completion request.
Given the desired prompt, the function reports the corresponding ChatGPT completion. For
a successful completion call, only the ID of the model to use ( model ) and the desired
prompt ( prompt ) must be provided.
The actual completion returned by ChatGPT contains metadata along with the response
itself. Nevertheless, the completion itself is always returned in the
response.choices[0].message[“content”] field. More information about the full completion
content can be found in the OpenAI documentation.
# Output: Nice to meet you, Andrea! How can I assist you today?
# Output: I'm sorry, as an AI language model, I don't have the ability to remember sp
Right, as we can observe, each API call is a standalone interaction. This means that the
model has no memory of the previous input prompts or completions already given. Each
time we call the chatgpt_call() function, we start a brand new conversation.
That is definitely an issue when trying to build a chatbot since it is impossible to have a
conversation with our model if it has no memory of what we have been discussing so far.
For that reason, our next step would be to make the model aware of our previous
interactions.
Context-awareness
Context-awareness enables chatbots to understand and respond appropriately based on
an ongoing conversation.
The original OpenAI API for ChatGPT already provides a way to send our conversation
history to the model along with each new prompt. In addition, the LangChain framework
also provides a way to manage ChatGPT’s memory more effectively and in a cost-
optimized way.
The messages list stores the conversation history as a collection of dictionaries. Each
dictionary within the list represents a message in the conversation by using two key-value
pairs.
!"The key role with the value user indicates that the message has been sent by the
user.
!"The key content with the value prompt contains the actual user prompt.
This is a very simple example that contains only one interaction. Although the key content
will always store either the user prompt or the chatbot’s completion, the key role is much
more versatile.
System, Assistant, and User Roles
The key role indicates the owner of the message in content . When using ChatGPT, we
can have up to three roles:
!"User. As we have already discussed, the user role corresponds to the actual user
that interacts with the chatbot.
!"System. The system role serves to provide instructions, guidance, and context to the
chatbot. System messages help guide user interactions, set chatbot behavior, offer
contextual information, and handle specific interactions within the conversation.
System messages can be seen as high-level instructions for the chatbot, transparent to
the user.
Self-made image. Interaction of the different roles in the conversation chain when building
a chatbot with chatGPT.
Given our test interactions, a properly formatted conversation history making use of the
three aforementioned roles could look as follows:
messages = [
{'role':'system', 'content':'You are friendly chatbot.'},
{'role':'user', 'content':'Hi, my name is Andrea'},
{'role':'assistant', 'content': "Nice to meet you, Andrea! How can I assist you today
{'role':'user', 'content':'Do you remember my name?'} ]
We can slightly modify our call_chatgpt() function to include the conversation history as
an input parameter:
response = call_chatgpt_with_memory(messages)
print(response)
Nice! We have managed to keep the model aware of our past interactions so that we can
actually have a conversation with the model. Nevertheless, we need to find a way to
automatically update our messages list.
def chatgpt_conversation(prompt):
context.append({'role':'user', 'content':f"{prompt}"})
response = call_chatgpt_with_memory(context)
context.append({'role':'assistant',
'content':f"{response}"})
return response
# Output: 'Nice to meet you, Andrea! How can I assist you today?'
Now the conversation history is automatically updated. Finally, just note that one can
observe the conversation history — and its structure — at any time just by printing the
messages list.
print(messages)
# Output:
[{'role': 'user', 'content': 'Hello, my name is Andrea'},
{'role': 'assistant', 'content': 'Nice to meet you, Andrea! How can I assist you toda
{'role': 'user', 'content': 'Do you remember my name?'},
{'role': 'assistant', 'content': 'Yes, your name is Andrea.'}]
Continuously feeding each and every previous prompt and completion messages can
quickly lead to a huge collection of tokens, and ChatGPT would need to process them all
before returning the latest completion.
So far, we have used .append operations for bundling together the past interactions so
that the chatbot has some kind of memory. Nevertheless, the memory management could
be done in a more effective way by using dedicated packages such as LangChain.
For more information on the LangChain framework itself, I really recommend the
introductory article Introduction to LangChain for Data Engineering & Data Applications.
The langchain library serves as a replacement for the openai library used above, since
the module langchain.llms already allows you to interact with ChatGPT in two simple lines
of code:
Once the llm model has been loaded, let’s import the memory object that will store our
conversation history. For storing each past interaction with the chatbot, LangChain
provides the so-called ConversationBufferMemory :
Now it is finally time to import the so-called ConversationChain , as a wrapper that will
make use of the llm and the memory to feed the user prompt to ChatGPT and return its
completions:
conversation = ConversationChain(
llm=llm,
memory=memory
)
Finally, to submit a user prompt to ChatGPT, we can use the .predict method as follows:
conversation.predict(input="Hello, my name is Andrea")
# Output: "Hi Andrea, I'm an AI created to help you with whatever you may need. What
Let’s set verbose=True to observe the information that the model considers to provide a
completion. Let’s try it in a follow-up interaction:
memory.save_context({"input": "Hi"},
{"output": "What's up"})
memory.save_context({"input": "Not much, just hanging"},
{"output": "Cool"})
In addition, we can also consult the memory content any time by running
print(memory.buffer) or memory.load_memory_variables({}) .
Bear in mind that aside from the fact that ChatGPT has a token limit per interaction, its
usage cost also depends on the number of tokens. Processing all the conversation history
in each new interaction will likely be expensive over time.
This type of memory is my favorite one since it can really lead to an optimized memory
management, without sacrificing any past interaction.
Just note that this type of memory uses the defined llm to generate the summary of the
previous interactions.
To test it, let’s create an interaction with a large number of tokens. Considering the
following schedule :
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
As we can observe, the chatbot keeps a summary of the original conversation history,
reducing the number of tokens processed by ChatGPT in each follow-up interaction.
That’s it! Now we can continue talking to our optimized chatbot for hours!
Now the floor is yours! It is time to use the building blocks presented in this article to
TO P I C S
customize a chatbot tailored to your specific use case!
Artificial Intelligence (AI)
If you are interested in building other applications using both LangChain and ChatGPT, the
webinar Building AI Applications with LangChain and GPT is for you. Looking to get up to
Related
speed with NLP? Then check out the modules on Natural Language Processing in Python
instead!
What is AI Literacy? A
Comprehensive Guide for…
Matt Crabtree
What is LaMDA?
See More
DATA C O U R S E S
WO R KS PA C E
C E R T I F I C AT I O N
RESOURCES
PLANS
S U P PO R T
ABOUT