[How To] Building a Simple Webex Chatbot with Python Websockets & OpenWeather APIs

I've been spending some of my time lately working on a new project that involves an interactive chatbot - where you can send commands to the bot & ask for it to take certain actions or return information.

I didn't previously have a ton of experience building something like this. Mostly what I've done before is just using Webex or Discord APIs to send alerts or messages to a room.

As with anything new - I've stumbled my way through a couple of things, but have been enjoying the challenge of trying something different. Building out some of the more interactive parts of the bot hasn't been quite as difficult as I originally imagined, and some of the newer modules & SDKs have made the process much simpler.

With all that being said - I wanted to throw together a quick guide on how to get started building a basic chatbot, in case anyone else is interested but worried that it may be too complicated.

Repo here for all the sample code used below

Step 1: Getting API Keys

So the first thing we'll worry about is getting signed up for a Webex developer account & generating API keys.

So head on over to the Webex Developer site & log in (or sign up for a new account).

Once logged in, we'll go to the upper-right corner of the screen and click on our user icon - and select "My Webex Apps" from the drop down:

01---login

Then we'll be asked what type of app we're creating. In this case, we'll select bot:

02---appselection

Next we'll need to provide some details about our bot. This includes a display name, username, image, and some details. Once the bot is completed, we have the ability to publish it via the Webex App Hub where anyone could interact with it. If you're looking to do this, make sure you give a good description of your bot!

03---botdetails

After we finish all that, we'll finally get our API key!

04---apikey

Webhook vs Websocket: Which to use?

Next, I wanted to cover the two primary methods of sending & receiving messages from the Webex APIs: Webhooks and websockets.

Webhooks

Likely webhooks are the method most people are familiar with. Using this method, we need to run out bot code on a publicly reachable URL. When we run our bot, one of our first actions will be sending a request to the Webex APIs with our bot URL. This way, every time Webex receives a message from a user that wants to interact with our bot, the Webex cloud knows where to send that chat message.

Now, the potential downside of using webhooks is that we need to 1) manage a web server and 2) expose the bot publicly to the internet. Both of these could be overcome with easy workarounds, if needed. For example, we could use something like FastAPI to quickly build our web listener & handle incoming POST requests. We could also use a tunneling method (like ngrok) to avoid having to directly open ports to our webserver.

Websockets

On the other hand, we could use websockets to accomplish the same function. With a websocket, we're instead opening a direct, persistent TCP connection to the Webex APIs. Through this persistent connection, we can send & receive messages very quickly and easily.

This method offers a few advantages over webhooks. Primarily not having to expose our app publicly to the internet. Websockets act almost similar to something like ngrok, where we create an outbound tunnel - except in this case, only Webex knows about it and has access to it (where ngrok is still providing you a public URL that anyone could hit, if they knew about it). Additionally, this method removes the need for maintaining a web server & having to handle all of the incoming HTTP requests in your bot's code. There may also be a handful of API calls which are only supported via websockets today as well - like having your bot show that they've read a message, for example.

I've also found that the websocket method seems to be much more responsive, likely because there is a persistent TCP connection open - and you no longer have to rely on a full HTTP session establishment & teardown for every message to the bot.

Historically, websockets were only officially supported via the Webex JavaScript SDK. However, it seems recently that someone has built a Python equivalent earlier this year - which we'll be using throughout this blog post. You can find that package here

Getting started: Registering the Bot with Webex

So we'll start with some basic functionality. Let's grab the modules we need, apply our API key, and see if we can get a response from our bot.

Luckily, the module we're using makes this super easy. So let's install via pip:

pip install webex_bot

Once that's installed, we'll create a new Python file - I'll call mine bot.py

In that file, we'll start our script like this:

from webex_bot.webex_bot import WebexBot

api_token = "<TOKEN_HERE>"

bot = WebexBot(api_token, approved_domains=["0x2142.com"])

bot.run()

Looks easy enough, yeah? We'll import our webex_bot module first, as always. Then we'll define our API key. Obviously this can be stored as an environmental variable or another more secure location, but we'll define it here for demonstration.

Then we create a new instance of WebexBot, by providing our API key and an optional approved_domains value. In my case, I've provided the 0x2142.com domain - which means the bot will reject messages from anyone who isn't from that organization.

Last, we start our bot with bot.run().

Let's go ahead and run our bot with python bot.py and see what happens!

You should see something similar to this:

[email protected]:~/demo-webex-bot$ python bot.py
2021-10-25 15:42:52  [INFO]  [webex_bot.webex_bot.webex_bot.__init__]:39 Registering bot with Webex cloud
2021-10-25 15:42:52  [INFO]  [restsession.webexteamssdk.restsession.user_agent]:167 User-Agent: webexteamssdk/0+unknown {"implementation": {"name": "CPython", "version": "3.8.10"}, "system": {"name": "Linux", "release": "5.10.16.3-microsoft-standard-WSL2"}, "cpu": "x86_64"}
2021-10-25 15:42:52  [WARNING]  [command.webex_bot.models.command.__init__]:33 no card actions data so no entry for 'callback_keyword' for echo
2021-10-25 15:42:52  [INFO]  [command.webex_bot.models.command.set_default_card_callback_keyword]:47 Added default action for 'echo' callback_keyword=callback___echo
2021-10-25 15:42:53  [INFO]  [webex_bot.webex_bot.webex_bot.get_me_info]:74 Running as bot '0x2142 Bot' with email ['[email protected]']
2021-10-25 15:42:54  [INFO]  [webex_websocket_client.root._connect_and_listen]:151 Opening websocket connection to wss://mercury-connection-partition2-a.wbx2.com/v1/apps/wx2/registrations/06b5c858-174a-4977-a268-04530d777563/messages
2021-10-25 15:42:54  [INFO]  [webex_websocket_client.root._connect_and_listen]:154 WebSocket Opened.

So long as we continue to run this script in the foreground - we'll be able to see the live log of anything that happens, including messages to our bot. So for now, we'll leave this up & running.

Now we should be able to try sending our bot a message in Webex.

In the Webex app, if we do a quick search for our bot (by email or name), we should see it pop up pretty quickly:

bot

With our new space, we can interact with our bot. I'll send a "Hello" message:

bot2

And the webex_bot module will automatically respond back with a built-in help card.

Currently the bot only supports one command: echo. We'll add our own commands shortly, but for now let's give that a try:

bot4

So in the example above, I used the echo command. The bot returns a card with an input field. Once you fill out the textbox & hit submit, the bot will echo back whatever text was provided!

So now we have a running bot. But what about implementing our own commands? Let's take a look!

Creating custom bot commands

Now we get to the fun part!! To demonstrate, let's add a command to ask our bot for the current weather conditions. To accomplish this, we'll use the OpenWeather APIs.

So to begin - We'll probably want a separate Python file for each command we want to add. First we'll add weather.py to our current directory.

Next, we'll throw in a bit of boilerplate code that will be used for each command:

from webex_bot.models.command import Command
import logging

log = logging.getLogger(__name__)

class WeatherByZIP(Command):
    def __init__(self):
        super().__init__(
            command_keyword="weather",
            help_message="Get current weather conditions by ZIP code.",
            card=None,
        )

    def execute(self, message, attachment_actions):
        return f"Got the message: {message}"

So first we'll import Command from our webex_bot module, which will be used to create our custom command class.

Next we create a new class, in this case WeatherByZIP, and inherit the Command class from webex_bot. Within this new class, we'll create our __init__ function & define some information about our command.

So using super().__init__(), we'll provide what keyword should trigger this command, any help message to be provided to the user, and an optional card. We saw how the card could work earlier with the echo command - but to keep this module simple, we'll go without a card for now.

After that - we just need to create an execute function. This is what gets called by webex_bot when our command is triggered. By default, webex_bot will pass us the user's message - and, in the case of an action (like a card submission), it will provide those details.

Two things to keep in mind with our execute function:

  1. the message variable will contain the user's message with the command keyword removed. So in our case, if the user's message was "weather 12345" - then the message variable would just contain "12345".
  2. Any text we return from our function will be sent via the bot as a chat response. If we wanted to provide extras, like cards or attachments, we would need to use the webex_bot Response object. I might cover this in a separate blog post later

Okay! So right now our new command is configured to just echo back any test that is sent to the bot - similar to the echo command earlier, except without the card.

We'll add our weather functionality shortly - but let's first add our command to the bot & give it a quick test.

In our bot.py file, we'll just need to import our command module and call bot.add_command():

from webex_bot.webex_bot import WebexBot
from weather import WeatherByZIP

api_token = "<TOKEN_HERE>"

bot = WebexBot(api_token, approved_domains=["0x2142.com"])

bot.add_command(WeatherByZIP())

bot.run()

That's it! Now we should be able to try it out. We'll restart our bot, and send the command weather 12345:

bot5

Sure enough, our bot gets the message, passes it to our custom command, and returns the stripped message - all as expected.

So let's add our weather query & actually make this thing work.

Using the OpenWeather APIs, I'll create a new API key - then create a variable in the command module called OPENWEATHER_KEY. Then, I've updated my execute function to look like this:

def execute(self, message, attachment_actions):
    # Message may include spaces. Strip whitespace:
    zip_code = message.strip()

    # Define our URL, with requested ZIP code & API Key
    url = "https://api.openweathermap.org/data/2.5/weather?"
    url += f"zip={zip_code}&units=imperial&appid={OPENWEATHER_KEY}"

    # Query weather
    response = requests.get(url)
    weather = response.json()

    # Pull out desired info
    city = weather['name']
    conditions = weather['weather'][0]['description']
    temperature = weather['main']['temp']
    humidity = weather['main']['humidity']
    wind = weather['wind']['speed']

    response_message = f"In {city}, it's currently {temperature}F with {conditions}. "
    response_message += f"Wind speed is {wind}mph. Humidity is {humidity}%"

    return response_message

So because of the way that message is passed to us, we'll first need to strip whitespace. This is because when the user enters a command like "weather 12345", webex_bot will only remove the "weather" portion - leaving us with " 12345". So by stripping that value, we're left with just the numeric ZIP Code: "12345".

Next we assemble our URL, supplying our parameters: zip, units, and appid. Zip is the numeric ZIP code provided by our user, units will be either imperial or metric, and appid is our API key.

After that, it's as simple as sending the GET request & parsing the data. In the code above, I assemble the final text response into response_message which is then returned to the user.

All that being done - Let's try it out!!

bot6

In this example, I used the ZIP code for Richfield, OH - and our bot quickly returned that information to us.


And that's it! In just a few steps we created a new Webex bot & added custom commands. Once we have that process down, it's easy enough to continue extending our bot by adding additional commands.

There are obviously more advanced use cases - and a whole lot of fun that comes with using Adaptive Cards... But I hope this post was helpful in getting you started!

If you're interested in seeing additional content around Cards, etc - please let me know!