How to Build an Inventory Chatbot on WhatsApp with FastAPI

October 20, 2022
Written by
Ezzeddin Abdullah
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Mia Adjei
Twilion

How to Build an Inventory Chatbot on WhatsApp with FastAPI

Owning an inventory is a big deal only if you're able to manage it efficiently.

You can manage it on your own but you'll lose a lot of your time keeping track of products there. Or you can hire someone else to work but they might not be honest enough.

Today, you're going to learn how to manage your products in the inventory with a WhatsApp chatbot. With this chatbot, you'll retain your time and get a reliable result. This chatbot will help you engage with clients ordering your products and will tell them exactly about the status of the product — if it's there in the inventory or not.

In this tutorial, you will build this service using FastAPI, Twilio's WhatsApp messaging API, Pyngrok, and SQLAlchemy.

You'll use Twilio's API to access the WhatsApp messaging product to let the clients send messages via WhatsApp and start the chatbot. This chatbot is built using a FastAPI backend. It is then communicated with Pyngrok, which is a Python wrapper for ngrok that puts the FastAPI localhost on the internet. This chatbot will fetch data from the PostgreSQL database and update inventory data in the database using the SQLAlchemy ORM.

At the end of this tutorial, you'll be able to create a chatbot like so:

WhatsApp chat, showing conversation between customer and chatbot

The app is straightforward. You have a product in your inventory. When the client orders that product, the chatbot replies to place that order for them or says it's not available.

Saying that the product is not available means one of two options:

  • This particular product is finished and it's no longer in the inventory.
  • Or, your inventory didn't have that particular product in the first place.

Prerequisites

To follow along with this tutorial, you need to have the following:

Create your inventory

Start with setting up your database. Say, you have two products in your inventory:

  • 2 collapsible umbrellas
  • 1 hybrid smartwatch

To be able to create these two records in the database, you should define a schema and then create a table based on that schema.

Initiate the Python project and the PostgreSQL database

Download and set up PostgreSQL if you haven’t already. Create a new database — for this project, we would call it mydb.

createdb mydb

Create a new directory called inventory_chatbot and initiate a virtual environment inside:

mkdir inventory_chatbot
cd inventory_chatbot
python3 -m venv venv; . venv/bin/activate; pip install --upgrade pip

For now, you only need to install these two libraries:

pip install sqlalchemy psycopg2-binary

The psycopg2-binary library is a binary version of psycopg2, which is a Python driver for the PostgreSQL database.

Set up the SQLAlchemy model

For this project, you will use the SQLAlchemy ORM to access a PostgreSQL database engine and interact with it. Create a new file called models.py. Model the data and create the schema in the models.py file:

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.engine import URL
from sqlalchemy.orm import declarative_base, sessionmaker


url = URL.create(
    drivername="postgresql",
    username="postgres",
    password="postgres",
    host="localhost",
    database="mydb",
    port=5432
)

engine = create_engine(url)
Session = sessionmaker(bind=engine)
session = Session()

Base = declarative_base()

class Product(Base):
    __tablename__ = "products"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    amount = Column(Integer)


Base.metadata.create_all(engine)

As you can see, the url object defines the database URL with the following options:

  • The drivername as the database engine
  • The username and password of your database
  • The host in this example is localhost
  • The database is the name of the database you need to access; in this tutorial it's mydb
  • The port which is 5432 by default for PostgreSQL

You can choose your own specifications depending on your case.

You then define the engine instance which is the starting point of your application. That instance is passed into the sessionmaker function to create a Session class which is instantiated to create a session object.

The session object manages the persistence layer and does the ORM operations.

The Product class inherits from the Base class, which declares the mapped classes into tables. Inside that Product table, there is a definition of its name and its attributes.

Similarly, if you want to define another table, you can create a class that inherits from the base class and declare your attributes.

The Base.metadata.create_all() function creates all the tables defined in the mapped classes.

Insert data in SQLAlchemy

Now, insert records into the products table. To do that, create a file called insert_db.py and add the following code to the file:

from models import Product, session

umbrella = Product(name="Collapsible Umbrella", amount=2)
smartwatch = Product(name="Hybrid Smartwatch", amount=1)

session.add_all([umbrella, smartwatch])
session.commit()

The umbrella and smartwatch objects are products in the database. The former amounts to two in the inventory, while the latter is just one piece.

In the code above, you add them into the database with the session.add_all() method and then commit it into the PostgreSQL database with session.commit().

To run this script, make sure you have the Python virtual environment activated, then run the script with this command:

python insert_db.py

To check if everything worked correctly, you can access your PostgreSQL database through a SQL client or a PostgreSQL client like psql.

Alternatively, you can of course use any database administration tool like pgAdmin, or a multi-platform database tool like DBeaver.

Here is how you can use psql to explore the data in your database.

Running psql -U newuser -d mydb lets you use the psql client to connect to the database named mydb as the user newuser. You can replace newuser with your own username. Take a look at the psql documentation to learn more about the commands you can run.

Once you are logged in and connected to your database, you can run the following query to list the products in your inventory:

select * from products;

Below is what the psql log statements might look like if you're running them on a Linux machine:

$ sudo -u postgres psql
[sudo] password for <username>:     
psql (10.19 (<operating_system>))
Type "help" for help.

postgres=# \c mydb
You are now connected to database "mydb" as user "postgres".

mydb=# select * from products;
 id |         name         | amount 
----+----------------------+--------
  1 | Collapsible Umbrella |      2
  2 | Hybrid Smartwatch    |      1
(2 rows)

As you can see, both records are now created.

Once you are finished, you can enter \q and then press the Enter key to quit psql.

Create the chatbot

The database is now set up and the tiny inventory is already there. Now you need to track the orders for this simple data. Let's configure the Twilio WhatsApp API first.

Configure the Twilio Sandbox for WhatsApp

Assuming you've already set up a new Twilio account, go to the Twilio Console and choose the Messaging tab on the left panel. Under Try it out, choose Send a WhatsApp message. You'll be redirected to the Sandbox tab by default, and you’ll see a phone number "+14155238886" with a code to join next to it on the left side of the page. You’ll also see a QR code to scan with your phone on the right side of that page.

To enable the Twilio testing environment, send a WhatsApp message with this code's text to the displayed phone number. You can click on the hyperlink to direct you to the WhatsApp chat if you are using the web version. Otherwise, you can scan the QR code with your phone.

Now, the Twilio sandbox is set up, and it's configured so that you can try out your application after setting up the backend.

Set up the FastAPI backend

To set up FastAPI, navigate to the project directory and create a new file called main.py. Inside that file add this very minimal FastAPI backend:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def index():
    return {"message": "hello"}

To run this backend, you need to install fastapi (for the API) and uvicorn (the Python ASGI web server):

pip install fastapi uvicorn

To run the app:

uvicorn main:app --reload

Open your browser to localhost:8000. The result you should see is a JSON response of {"message": "hello"}.

To connect Twilio with your backend, you need to host your app on a public server. An easy way to do that is to use Ngrok.

If you’re new to Ngrok, you can consult this blog post and create a new account.

To use it, open a new terminal window, activate the same virtual environment you created earlier, and then install pyngrok, which is a Python wrapper for Ngrok:

pip install pyngrok

Leave the FastAPI app running on port 8000, and run this ngrok command:

ngrok http 8000

This will create a tunnel between your localhost port 8000 and a public domain created on the ngrok.io site. When a client visits your Ngrok forwarding URL, the Ngrok service will automatically forward that request to your backend.

Go to the URL (preferably, with the https prefix) as shown below:

Ngrok log in the terminal

After you click on the forwarding URL, Ngrok will redirect you to your FastAPI app’s index endpoint.

Configure the Twilio webhook

To be able to receive a reply when you message the Twilio WhatsApp sandbox number, you need to configure a webhook known to Twilio.

To do that, head over to the Twilio Console and choose the Messaging tab on the left panel. Under the Try it out tab, click on Send a WhatsApp message. Next to the Sandbox tab, choose the Sandbox settings tab.

Copy the ngrok.io forwarding URL and append /message. Paste it into the box next to WHEN A MESSAGE COMES IN:

WhatsApp sandbox settings

The full URL will be something like: https://d8c1-197-36-101-223.ngrok.io/message.

Note: The /message is the endpoint we will set up in the FastAPI application. This endpoint will have the chatbot logic.

Once you finish, click the Save button.

Authenticate your Twilio account

Earlier, you created the /index endpoint to just test that your backend is working with Ngrok. From this point onwards, you won’t need it anymore.

Before setting up the /message endpoint, authenticate your Twilio account to be able to use the Twilio client. Open the main.py file and add the following highlighted lines of code:


import os
from dotenv import load_dotenv
from twilio.rest import Client
from fastapi import FastAPI

load_dotenv()

account_sid = os.environ["TWILIO_ACCOUNT_SID"]
auth_token = os.environ["TWILIO_AUTH_TOKEN"]
client = Client(account_sid, auth_token)
app = FastAPI()

In your Twilio console, find your Account SID and Auth Token. In the next step, you will set these as environment variables.

Account SID and Auth Token in Twilio Console

You will use dotenv to read and set the environment variables and twilio to create the Twilio client. Install these two packages in your virtual environment with the following:

pip install twilio python-dotenv

To use the environment variables, create a .env file and fill it like so, replacing the placeholder text with your values from the Console:

TWILIO_ACCOUNT_SID=<your-twilio-account-sid>
TWILIO_AUTH_TOKEN=<your-twilio-auth-token>

Now, the app is ready to use Twilio capabilities to create the chatbot through WhatsApp.

Add the chatbot logic

Open the main.py file and update the imports at the top of the file as follows:


import os
import uuid
from dotenv import load_dotenv
from twilio.rest import Client
from fastapi import FastAPI, Form
from models import Product, session

load_dotenv()

Since you're using Form data here, you'll also need to install one more Python package in your activated virtual environment:

pip install python-multipart

Now, in main.py, below the index() endpoint, add the following logic:

def send_message(body_text):
    client.messages.create(
        from_="whatsapp:+14155238886", body=body_text, to="whatsapp:+<your-country-code><your-phone-number>"
    )

@app.post("/message")
async def reply(Body: str = Form()):
    message = Body.lower()
    products = session.query(Product)
    for product in products:
        if product.name.lower() in message:
            if product.amount >= 1:
                id = str(uuid.uuid4())
                message = f"Your order is placed for {product.name}. This is the tracking id: {id}."
                ordered_product = products.filter(Product.name == product.name).first()
                ordered_product.amount -= 1
                session.commit()
                return send_message(message)
    message = "The product you mentioned is not available at the moment."
    return send_message(message)

The send_message() function takes the text message that you want to send from the Twilio WhatsApp sandbox account to your phone number. Change the placeholder text in the to option value to your phone number in E.164 format (country code followed by your phone number).

The /message endpoint will receive a POST request that takes the Form as an argument, which represents the body of the incoming message. The message instance is a lowered-case version of the body text in the WhatsApp message coming from the client.

The products object is a Query object in SQLAlchemy. Here, you loop over each product in the database. You check if the incoming message contains the names of any products in your database, and then, if it does, check if that product is available in the inventory (if its amount is greater than 1). If the product is available, you prepare a useful message that informs the client that their order is placed, and generate a new unique id for tracking (using the uuid library).

Once the product is ordered, you reduce its amount in the database by 1 and commit the new change to the database. Then you send the message to the user.

If no product matches the text in the client's message, or if the product matches but its amount in the inventory is zero, you then send the user a message that the product is not available at the moment.

Test the final app

Earlier, you defined that there were 2 different products in the inventory:

  • 2 collapsible umbrellas
  • 1 hybrid smartwatch

Let's test the app by sending a rubbish message to the app. What should its response be?

Yes, this text: "The product you mentioned is not available at the moment."

Now, send "collapsible umbrella". The chatbot responds with:

"Your order is placed for Collapsible Umbrella. This is the tracking id: <unique-identifier>"

Send this message next: "I want another collapsible umbrella". The chatbot then responds with a similar message to the previous one, with another tracking id.

If you message with "I want one more collapsible umbrella", you'll get back: "The product you mentioned is not available at the moment." because the last two pieces were already ordered and there is no more of this product available in the inventory now.

You can try your own experiments with the other products and investigate even further with your own specifications.

Finally, if your tech stack is Flask, not FastAPI, be sure to check out this blog post that explains how to create a chatbot using WhatsApp and access third-party APIs.

Ezz is an AWS Certified Machine Learning Specialist and a Data Platform Engineer. He helps tech companies generate developer leads through his technical blog posts. Check out his website for more.