Chatbot Factory: Streamline Python Chatbot Creation with LLMs and Langchain : Kevin MacNeel

Chatbot Factory: Streamline Python Chatbot Creation with LLMs and Langchain
by: Kevin MacNeel
blow post content copied from  Be on the Right Side of Change
click here to view original post


Rate this post

This guide, based on my personal experience creating Chatbots, helps you build Chatbots that use advanced technologies like large language models, external tools such as Wolfram Alpha, Google search, and short-term and long-term memory.

I used all these technologies with simple code and a few easy-to-learn software design patterns. I’ll cover how to create these bots and how they can be run on the command line. I’ll show how they can be run on various platforms on Discord and on a Website in later articles.

You can use this project to learn, extend, or develop your own Chatbot. I’ll demo a bot for an entertaining imaginative StoryBot and a business bot for a chiropractor called the ChiropractorBot. 

An Overview 

I wanted to create a lot of different types of bots. In order to create many bots I decided to use the factory design pattern. This design pattern lets you create many objects or Chatbots easily. 

Below is a diagram of a ChatBot Factory:

(1) WebsiteClient, DiscordClient, CommandLineClient: These are the client classes that require objects of a Bot type. They can be different applications or interfaces that interact with the BotFactory.

Here’s an example of the command-line client:

from chatbot_factory import ChatBotFactory
from chatbot_settings import ChatBotSettings
import os
from langchain.chains.conversation.memory import (ConversationBufferMemory,
                                                  ConversationSummaryMemory,
                                                  ConversationBufferWindowMemory,
                                                  ConversationKGMemory)
from colorama import Fore, Style, init


# from colorama import Fore, Style, init
init(autoreset=True)


chatbot_factory = ChatBotFactory()


selected_bot = ChatBotFactory().select_chatbot(ChatBotFactory.services, "Please select a chatbot from the following options:")


chatbot = chatbot_factory.create_service(selected_bot, ChatBotSettings(llm=ChatBotFactory().llms[ChatBotFactory().LLM_CHAT_OPENAI],memory=ConversationBufferMemory(), tools=['serpapi','wolfram-alpha']))


print(Fore.GREEN + "Please enter a prompt:")


while True:
    # Get user input from the command line
    user_input = input()
   
    # If the user types 'exit', end the chatbot session
    if user_input.lower() == 'exit':
        print("Goodbye!")
        break
   
    # Get the chatbot's response using the get_bot_response_chat_completions method
    response = chatbot.get_bot_response(user_input)
    print(response)
    print(type(response))
    if(type(response).__name__ == "gTTS"):
       response.save("welcome.mp3")
       break
    elif(type(response) == "str"):
        # Print the chatbot's response
        print(Fore.GREEN + response)

(2) BotFactory: This class provides a method to create and return Bot objects. All clients use this same BotFactory to create bots.

import os
from typing import Callable, Dict, List, Optional, Union, Tuple
from chatbot_settings import ChatBotSettings
from bot_conversation_chain import BotConversationChain
from bot_knowledge_base import BotKnowledgeBase
from bot_dalle_imagine import BotDalle
from bot_gtts_audio import BotGtts
from bot_circumference_calculator import BotCirucumferenceTool
from bot_pinecone import BotPineCone
from bot_agent_tools import BotAgentTools
from bot_story_imagine import BotStoryImagine
from langchain import (HuggingFaceHub, Cohere)
from langchain.chat_models import ChatOpenAI
from colorama import Fore, Style, init




class ChatBotFactory:
    services = {
        BotConversationChain.__name__ : BotConversationChain,
        BotPineCone.__name__: BotPineCone,
        BotAgentTools.__name__: BotAgentTools,
        BotKnowledgeBase.__name__: BotKnowledgeBase,
        BotDalle.__name__: BotDalle,
        BotGtts.__name__: BotGtts,
        BotCirucumferenceTool.__name__: BotCirucumferenceTool,
        BotStoryImagine.__name__: BotStoryImagine
    }


    LLM_CHAT_OPENAI = "ChatOpenAI"
    LLM_COHERE = "Cohere"
    LLM_HUGGINGFACE_HUB = "HuggingFaceHub"


    llms = {
        LLM_CHAT_OPENAI: ChatOpenAI(
            temperature=0,
            openai_api_key=ChatBotSettings().OPENAI_API_KEY(),
            model_name="gpt-3.5-turbo"
        ),
        LLM_COHERE: Cohere(model='command-xlarge'),
        LLM_HUGGINGFACE_HUB: HuggingFaceHub(
            repo_id="facebook/mbart-large-50",
            model_kwargs={"temperature": 0, "max_length": 200},
            huggingfacehub_api_token=ChatBotSettings().HUGGING_FACE_API_KEY()
        )
    }


    @classmethod
    def create_service(cls, service_type, settings):
        if service_type in cls.services:
            return cls.services[service_type](settings)
        else:
            raise ValueError(f'Unknown service type {service_type}')




    @classmethod
    def select_chatbot(cls, chatbots, selection_text):
       # Print the options to the user
     
       for i, bot in enumerate(chatbots, start=1):
           print(Fore.GREEN + f"{i}. {bot}")


        # Get user input
       selection = input(Fore.GREEN + "Enter the number of your selection: ")


       selected_bot = list(chatbots)[int(selection) - 1]


       print(Fore.GREEN + f"You selected {selected_bot}")


       return selected_bot

(3) BotAbstract: This is the abstract class that defines the interface for Bots. This class tells you how to structure the bot. The methods are abstract and require a constructor and a get_bot_reponse method. 

from abc import ABC, abstractmethod
from typing import Any


class BotAbstract(ABC):
    @abstractmethod
    def __init__(self, settings: Any):
        pass


    @abstractmethod
    def get_bot_response(self, text: str) -> str:
        pass

(4) ConcreteBot1, ConcreteBot2: These are the subclasses of the BotAbstract class that the BotFactory can produce. Each ConcreteBot class implements the BotAbstract interface in its own way.

Here’s an example of a concrete bot called BotConversationChain. This bot uses ConversationBufferMemory and will remember what you’ve said to the bot until it hits its memory limit. This bot uses langchain.

Here’s an excellent introduction to Langchain

from typing import Callable, Dict, List, Optional, Union
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from chatbot_settings import ChatBotSettings
from bot_abstract_class import BotAbstract






class BotConversationChain(BotAbstract):
    def __init__(self, chatBotSettings: ChatBotSettings()):
        self.chatbotSettings = chatBotSettings


        self.llm = chatBotSettings.llm
        self.memory = chatBotSettings.memory
       
        self.conversation_buf: ConversationChain = ConversationChain(
            llm=self.llm,
            memory=self.memory
        )


    def get_bot_response(self, text: str):
        reply = self.conversation_buf(text)
     
        return reply['response']

Each client, instead of calling the constructors of the Bot classes directly, calls the BotFactory‘s creation method to get an instance of a Bot.

The clients are only aware of the BotAbstract interface and do not need to know about the ConcreteBot classes. This decouples the clients from the ConcreteBot classes and makes the client code easier to maintain and extend. This allows for the easy creation of many bots. 

In the case of the command line client, it makes it easy to create chatbots with a common interface of a constructor and get_bot_response. There is also a ChatBotSettings file that contains the keys that get instantiated with each creation of the bot.

Here’s how you can run the factory code. 

Getting Started

Requirements for Getting Started:

  • Python 3.7 or greater
  • Git
  • OpenAI API  key

 Running the Chatbot Locally

To run the chatbot locally, you need to clone the chatbot repository, set up a virtual environment, install the required dependencies, and enter the API key. Here are the steps:

# Clone the chatbot repository
git clone https://github.com/ggrow3/ExtensibleChatBot

# Change directory to the cloned repository
cd ExtensibleChatBot

# Create a virtual environment
python -m venv .venv

# Activate the virtual environment
# On Windows, use 
.venv\Scripts\activate

#On Unix/Linux Use
#source .venv/bin/activate  

# Install the required dependencies
pip install -r requirements.txt

#Rename the environ settings file which contains the keys
cp .\chatbot_settings_placeholder.py chatbot_settings.py

#open chatbot_settings.py 
#Put in key for OPENAI_API_KEY in the os.environ["OPENAI_API_KEY"]


#Run chatbot_client and type in a command and test with queries
python client_command_line.py

#In the command line
#Select 1. BotConversationChain
#Experiment talking with this bot.
#This bot has short term memory

The Story Bot: The story bot example bot takes in text from an input and generates a story and puts that story in a PDF with a representative image of the prompt from Dalle and audio.

I made several stories for my sons so they can get a full story in a PDF form and audio to go along with it. 

from abc import ABC, abstractmethod
from typing import Any
from gtts import gTTS
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph, Image
from reportlab.lib.styles import getSampleStyleSheet
from langchain.chains import LLMChain, ConversationChain
import openai
from langchain.chat_models import ChatOpenAI
from chatbot_settings import ChatBotSettings
from langchain.schema import (
    SystemMessage,
    HumanMessage,
    AIMessage
)


class BotStoryImagine(ABC):
   
    def __init__(self, settings: Any):
         self.llm = settings.llm
       


    def get_bot_response(self, text: str) -> str:
        language = 'en'
       
        test_llm = ChatOpenAI(
            temperature=0,
            openai_api_key=ChatBotSettings().OPENAI_API_KEY(),
            model_name="gpt-3.5-turbo"
        )


       
        messages = [
            SystemMessage(content="You are an adventure mystery story telling bot for young teens."),
            HumanMessage(content="Hi AI, what are your main themes?"),
            AIMessage(content="My theme and things is doing good and solving puzzles and learning about science in the world."),
            HumanMessage(content=text)
        ]


        reply = test_llm(messages)


        rs = reply.content


        myobj = gTTS(text=rs, lang=language, slow=False)
        myobj.save("story.mp3")


        response = openai.Image.create(
            prompt=text,
            n=1,
            size="256x256",
        )
        image_path = response["data"][0]["url"]
        print(image_path)
       
        self.create_pdf("Story Bot", "story.pdf", image_path, rs)
       
        return myobj
   
    def create_pdf(self, doc_title, doc_filename, image_path, doc_text):
        document = SimpleDocTemplate(doc_filename, pagesize=letter)
   
        # Container for the 'Flowable' objects
        elements = []
   
        styles = getSampleStyleSheet()


        bodytext_style = styles['BodyText']
   
        # Add title
        title = Paragraph(doc_title, styles['Title'])
        elements.append(title)
   
        # Add image
        img = Image(image_path, 200, 200)  # The numbers 200, 200 are the width and height in points
        elements.append(img)


        text = Paragraph(doc_text, styles['BodyText'])
        elements.append(text)
   


        # Generate PDF
        pdf = document.build(elements)


        response = ResponseMultiModal(audio, pdf, text, image)


        return response

The Chiropractor Bots

The purpose of the Chiropractor bots is to gather information from the patient first and then have a separate bot that helps the Chiropractor evaluate the data collected from the patients. A patient bot takes in input from patients and stores it in short-term memory in the form of ConversationBufferMemory and long-term memory in a JSON file. 

 There is a bot for the Chiropractor to give that input to Chiropractor to evaluate his patient’s status. 

The bot uses JSON as a store of its questions and patient responses. The bot gets basic information and stores it in JSON as memory. It also saves the responses in a patient_responses.json

For enhanced conversation, a Chiropractor could use professionally curated knowledge from a PDF and load it to the bots’ long-term memory. While this bot does not do this is a possible way to enhance the conversation. Instead, this bot relies on data that OpenAI trained it on.

Here’s how to load the memory store in a Pinecone vector database.  

The Patient view 

Run the Chiropractor patient view first. Enter in data for a couple patients. 

python .\bot_chiropractor_patient.py

import json
import os
import datetime
from abc import ABC, abstractmethod
from typing import Any
from gtts import gTTS
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph, Image
from reportlab.lib.styles import getSampleStyleSheet
from langchain.chains import LLMChain, ConversationChain
import openai
from langchain.chat_models import ChatOpenAI
from chatbot_settings import ChatBotSettings
from langchain.schema import (
    SystemMessage,
    HumanMessage,
    AIMessage
)
from langchain.prompts.prompt import PromptTemplate
from langchain.chains.conversation.memory import (ConversationBufferMemory,
                                                  ConversationSummaryMemory,
                                                  ConversationBufferWindowMemory,
                                                  ConversationKGMemory)
from pandasai import PandasAI


import pandas as pd












llm = ChatOpenAI(
            temperature=0,
            openai_api_key=ChatBotSettings().OPENAI_API_KEY(),
            model_name="gpt-3.5-turbo"
)




def load_patient_questions():
    # Define the file path
    file_path = 'patient_questions.json'


    # Check if the file exists
    if os.path.exists(file_path):
        # Load questions from the existing file
        with open(file_path, 'r') as f:
            loaded_data = json.load(f)
            loaded_questions = loaded_data["questions"]
    else:
        # Put questions into a list
        questions = [
            "How are you feeling today?",
            "Have you experienced any symptoms?",
            "Are you taking any medications?",
            "What is your level of pain? (1-5)",
            "Do you have any other symptoms?"
            # Add more questions here...
        ]


        # Create the JSON object
        data = {
            "questions": questions
        }


        # Save the initial questions to the file
        with open(file_path, 'w') as f:
            json.dump(data, f)


        # Assign the initial questions to loaded_questions
        loaded_questions = questions


    return loaded_questions


# Call the function to load patient questions
patient_questions = load_patient_questions()
# Print the loaded questions
print(patient_questions)




patients = [{"name": "John Doe"}, {"name": "Jane Doe"}, {"name":"Steve Smith"}]  # List of patients


responses = []


for patient in patients:
   
    patient_responses = {
        "name": patient["name"],
        "date": str(datetime.date.today()),
        "responses": {},
        "questions": []
    }
    questions = patient_questions


    print('Hi ' + patient["name"])
    # Predefined questions
    for question in questions:
        answer = input(question + " ")
        patient_responses["responses"][question] = answer


   
    question_response_pairs = [f"{question}: {response}" for question, response in patient_responses['responses'].items()]
    questions_and_responses = ' '.join(question_response_pairs)


    template = """The following is a response from an AI Chiropractor. The AI Chiropractor has an excellent bedside manner provides specific details from its context.
    If the AI does not know the answer to a question, it truthfully says it does not know. The AI does notd emands that someone asks a question.
    {history}
    Patient: {input} """ + "Patient: " + questions_and_responses


    prompt = PromptTemplate(
            input_variables=["history","input"], template=template
    )
   
    conversation_with_chain = ConversationChain(
        llm=llm,
        verbose=True,
        prompt=prompt,
        memory=ConversationBufferMemory()
    )


    # Open-ended questions
    while True:
        question = input(patient["name"] +  ", What are your questions? (type 'done' when you are finished) ")
        if question.lower() == 'done':
            break


        answer = conversation_with_chain(question)
        print(answer['response'])


        patient_responses["responses"][question] = answer
        patient_responses["questions"].append(question)


    responses.append(patient_responses)


# Load existing responses from the file
with open('patient_responses.json') as f:
    existing_responses = json.load(f)


# Append the new responses to the existing ones
existing_responses.extend(responses)


# Save the updated responses back to the file in JSON format
with open('patient_responses.json', 'w') as f:
    json.dump(existing_responses, f)

This is the bot for the Chiropractor. It uses PandasAI for the chiropractor to evaluate the data frame. Run python .\bot_chiropractor.py to see it run.

from pandasai import PandasAI


import pandas as pd


chatbotSettings = ChatBotSettings()
with open('patient_responses.json', 'r') as f:
    all_responses = json.load(f)




# Open the JSON file
with open('patient_questions.json') as file:
    data = json.load(file)


# Get the list of questions
questions = data['questions']


# Flatten the dictionary inside the 'responses' key and match questions to knowledge base
flattened_data = []
for item in all_responses:
    flattened_dict = {}
    flattened_dict['name'] = item['name']
    flattened_dict['date'] = item['date']
    responses = item['responses']
    for question, response in responses.items():
       
        print(question)
        if question in questions:
            # If a match is found, add the match as a new entry in the dictionary
            flattened_dict[question] = response
    flattened_data.append(flattened_dict)




print("Hello Chiropractor")
# Create DataFrame
df = pd.DataFrame(flattened_data)
df.to_csv('patient_responses.csv', index=False)
print(df)


from pandasai.llm.openai import OpenAI
llm = OpenAI()


# Create an empty list to store questions and responses
questions_and_responses = []


while True:
    pandas_ai = PandasAI(llm, conversational=False)
    user_input = input("What data do you want from your patients? ")
    response = pandas_ai.run(df, prompt=user_input)
    print(response)


    # Store the question and response in a dictionary
    question_response = {
        "question": user_input,
        "response": response
    }


    # Append the question and response to the list
    questions_and_responses.append(question_response)


    # Check if the user wants to continue
    cont = input("Do you want to ask another question? (yes/no): ")
    if cont.lower() != "yes":
        break


# Save the questions and responses to a JSON file
with open('questions_and_responses.json', 'w') as f:
    json.dump(questions_and_responses, f)

Here’s an example dataframe and the query made by the Chiropractor “What’s John Does average pain level?” and the output of the query:

Conclusion

With Langchain and the Factory Design Pattern, it’s truly remarkable how effortless and streamlined bot creation can become.

As we progress in our discussion, our next piece will delve into the implementation of a demonstration bot on a live website. This journey will further illustrate the immense capabilities of chatbot technology today with the usage of LLMs, Langchain, vector databases, and other APIs.

With minimal lines of code, we can unlock an array of functionalities that go a long way in enhancing user interactions. Stay tuned as we continue to explore the fascinating world of chatbot development.


May 15, 2023 at 05:36PM
Click here for more details...

=============================
The original post is available in Be on the Right Side of Change by Kevin MacNeel
this post has been published as it is through automation. Automation script brings all the top bloggers post under a single umbrella.
The purpose of this blog, Follow the top Salesforce bloggers and collect all blogs in a single place through automation.
============================

Salesforce