Recently I was tasked with creating a slack chatbot which would help users query an ecommerce website as part of a second round interview process for a position at an AI company. While that task was interesting, I got more enjoyment out of applying the NLP and LLM technologies I used towards a cooking chatbot. So for my first legitimate, tech oriented post on this site, let's do a deep dive and demo how to create a cooking chatbot using OpenAI's GPT-3 API.

The cooking website which I am pulling all the recipes and such from is based.cooking (Thanks Luke Smith!). I actually found that this was a good website to scrape data from for this project due to its minimalist/small nature. In total, there's around ~300 recipes, meaning it isn't necessary to take extra steps or precautions to handle the data. In particular, what is needed to scrape the website for its recipes is the sitemap, which is located at: https://based.cooking/sitemap.xml.

As far as software goes, I'm developing in Python3 and utilizing Langchain, Faiss, and OpenAI. These packages make developing a complex chatbot fairly simplistic. Langchain is a framework for developing applications with language models. It has a bunch of different tools/classes for easy development. You can find out more about Langchain here. Faiss is a library for vectory similarity search and clustering of dense vectors. It is what I am using to store the website data. You can find out more about Faiss here. Finally, OpenAI (as I am sure you already know) is an AI research laboratory which makes their AI and LLMs open for use to the public, albeit through an API. You can find out more about their API here. Moving on, as far as the mental framework for how I am developing this chatbot goes, I've divided development into three separate steps:

  1. Data -- How do we get it? What preprocessing do we need to do? How do we represent the data? How do we make it easily queryable?
  2. Backend Logic -- How do we implement a chatbot? How do we connect our questions/queries with OpenAI and our data?
  3. Frontend -- How exactly are we interacting with this chatbot?


First, let's start with data. As stated previously, I am pulling all the recipes from the website using the sitemap. The Langchain framework provides a Data Loader (SitemapLoader) which automates website scraping for us. Using this Data Loader, I will then parse each webpage with a recipe into a string, which subsequently gets split into a manageable size, turned into embeddings (dense vector which represents document), and stored in a vector store. This will allow the chatbot to easily interact and query the data. Here is the code for retrieving, cleaning, and processing our data:

   ### Imports ###
   
   from langchain.text_splitter import TextSplitter
   from langchain.text_splitter import CharacterTextSplitter
   from langchain.vectorstores import FAISS
   from langchain.embeddings import OpenAIEmbeddings
   from langchain.document_loaders.sitemap import SitemapLoader
   import yaml
   import os
   from bs4 import BeautifulSoup
   import requests
   import xml.etree.ElementTree as ET
   
   ## Main Function: Scrape sites -> parse into docs -> embed docs as vectors -> construct vector store ##
   
   if __name__ == "__main__":
   
       ## Step 1: Open Config File
   
       with open('config.yaml', 'r') as config:
           config_file = yaml.safe_load(config)
   
       ## Step 2: Get necessary api keys
   
       os.environ['OPENAI_API_KEY'] = config_file["openai_token"]
       embeddings = OpenAIEmbeddings()
   
       ## Step 3: Use SitemapLoader to scrape webpages
   
       docs = []
   
       for site in config_file["web_sites"]:
   
           sitemap_loader = SitemapLoader(web_path = site)
   
           sitemap_loader.requests_per_second = 25
   
           docs = docs + sitemap_loader.load()
   
       ## Step 4: Split docs into manageable sizes
       
       text_splitter = CharacterTextSplitter(chunk_size=8000, chunk_overlap=3000)
   
       docs = text_splitter.split_documents(docs)
   
       ## Final Step: Create vector database with openai embeddings
   
       db = FAISS.from_documents(docs, embeddings)
   
       db.save_local("./vectorstore")
   


To use this script, it is necessary to define a config file named "config.yaml". This file contains API keys, file paths, and the sitemaps to scrape:


   #OpenAI Verification Tokens
   
   openai_token: ""
   
   # Vectorstore Path
   
   vectorstore: "./vectorstore/"
   
   # Website Sitemaps to scrape:
   
   web_sites: ["https://based.cooking/sitemap.xml"]
   


Moving on, we now need to implement the backend logic which defines the chatbot. Using Langchain, this is fairly straightforward. By using ConversationalRetrievalChain we can easily define a chatbot, which uses GPT-3, that can interact and query the recipe documents. As to how I am implementing this to make a chatbot we can easily interact with, I created a class QABot:

   from langchain.embeddings.openai import OpenAIEmbeddings
   from langchain.vectorstores import Chroma
   from langchain.text_splitter import CharacterTextSplitter
   from langchain.llms import OpenAI
   from langchain.chains import RetrievalQA
   from langchain.memory import ConversationBufferMemory
   from langchain.chat_models import ChatOpenAI
   from langchain.chains import ConversationalRetrievalChain
   
   class QABot:
       """
       A class to represent an AI Conversation bot
   
       ...
   
       Attributes
       ----------
       chat_history : list of str
           previous messages in chat
       qa: ConversationalRetrievalChain
           LLM Conversation Chain object -- what gives us chatbot functionality
   
       Methods
       -------
       query_answer(str):
           Reads a query and returns a chatbot answer
       clear_history():
           clears chatbot history
       """# 'ada' 'gpt-3.5-turbo' 'gpt-4',
       def __init__(self, vectorstore):
           
           # Set chat history
           
           self.chat_history = []
   
           # Define ConversationRetrievalChain using our vector store, use GPT-3.5-Turbo as our model 
   
           self.qa = ConversationalRetrievalChain.from_llm(ChatOpenAI(model='gpt-3.5-turbo'),
                                                           retriever=vectorstore.as_retriever())
   
       
       def query_answer(self, query):
           """ 
           Respond to query using ConversationalRetrievalChain LLM model
   
           Keyword arguments:
           query -- the query to be answered
           
           returns:
           result -- response to user's query
           """
           
           # Use ConversationalRetrievalChain to generate answer to query
           
           result = self.qa({"question": query, "chat_history": self.chat_history})
           
           # Append response to chat history
           
           self.chat_history.append((query, result['answer']))
           
           # Return answer
           
           return result['answer']
       
       def clear_history(self):
           """
           Clears the bot's chat history
           """
   
           # Set chat_history to empty
           
           self.chat_history = []
   
           # Return None
   
           return 
   


This class has two different functions, query_answer and clear_history, which are fairly self-explanatory. Query answer takes a user's queries and then funnels them to GPT-3. Clear History clears the chat history, which is used by GPT-3 as context for delivering answers.

Finally, we need a means of interacting with the chatbot. For my implementation, I kept things fairly simple and created a console user interface:

   import qa_ai
   from langchain.vectorstores import FAISS
   from langchain.embeddings import OpenAIEmbeddings
   import yaml
   import os
   
   
   if __name__ == "__main__":
   
       ## Step 1: Read Config
   
       with open('config.yaml', 'r') as config:
           config_file = yaml.safe_load(config)
   
       ## Step 2: Set openai api key as enviroment token    
   
       os.environ['OPENAI_API_KEY'] = config_file["openai_token"]
   
       ## Step 3: Read vectorstore from disk
   
       vector_store = FAISS.load_local(config_file["vectorstore"], OpenAIEmbeddings())
   
       ## Step 4: Instantiate QABot
   
       qa = qa_ai.QABot(vector_store)
   
       ## Step 5: Run chatbot loop
   
       while True:
           
           question = input("Input your Question: ")
           
           if question == "quit":
               break
           elif question == "clear history":
               qa.clear_history()
           else:
               print(f"{qa.query_answer(question)}\n")
   

Testing this chatbot, here's the result:

chatbot output


Overall, I enjoyed working on this micro project and you can find my code here: https://gitlab.com/ns-1/cooking-langbot. Enjoy 👍


--- ns1