Chatbots offer a useful way to leverage the power of AI, and are now accessible for any size of application. The back-and-forth written nature of chatting is conducive to utilizing existing chatbot frameworks and AI models to automate interactions which would have required a whole team of agents just a short time ago. To demonstrate how easy it is, we’ll use a chatbot framework and a sentiment analysis model from the Serverless AI Cloud — both of which have free trial tiers.

Let’s create one now! It only takes a few minutes, and it’s free to get started. Simply head over to RunDexter.com and click “Make Your First Bot”. After signing up, click the “New Bot” button and pick “Blank Project”:

### Step 1: create a basic chatbot

Note the column on the left called “All Topics”. Each Topic is a script which defines your user interactions, and your chatbot can switch between Topics as needed. Initially, you’ll have just one Topic, called “default”.

We’ll start by replacing the entire content of the default Topic. Delete everything that’s there, and paste the following:

+ *
- Welcome!n <set input=<star>> {topic=faq}{@<star>}

This script contains a single Trigger, indicated by the “+“. Triggers are starting points for user interaction: if they type something which matches the expression after the “+“, then they get the response indicated by the ““. In this case, we’ve used the wildcard character “*” as our expression, so literally anything the user types will match this trigger.

The output of the response is pretty straightforward: it prints “Welcome!” and a newline. However, we’ve also added some code after the response text.

<set input=…>” sets a “user variable” which we can use elsewhere in our script. As you may have already guessed, “<star>” simply refers to the “*” on the Trigger line, so this bit of code simply takes whatever the user typed and stores it in a variable called “input” (we’ll use this later).

Next, “{topic=faq}” tells the script to switch over to the Topic called “faq” (which doesn’t exist yet – we’ll create it soon). Then “{@<star>}” tells it to send the input “<star>” into that Topic… so essentially we are just forwarding the user’s request on to the “faq” Topic for further processing. Let’s create that Topic now…

On the left, click “New Topic”. This creates a Topic called “new-topic”. Rename it to “faq”, then paste in this code:

+ [*] (hey|hi|hello|sup) [*]
- I'm here to answer your questions. {@help}
+ [*] (1|hours) [*]
- We're open Monday to Friday, 8am-6pm Pacific Time {topic=default}
+ [*] (2|services) [*]
- We host thousands of machine learning algorithms which work great with Dexter! {topic=default}
+ [*] (3|contact) [*]
- Check us out at https://algorithmia.com and https://rundexter.com {topic=default}
+ [help] [*]
- What would you like to hear about? ^buttons(Hours, Services, Contact Info) {topic=default}

Looks pretty similar to our first script, right? Our matches are a bit more complex: we are looking for specific phrases which might optionally be surrounded by other text. So regardless of whether the user types “hello” or “oh, hello there!”, they’ll get the first response, “I’m here to answer your questions.” Dexter has some great documentation about Trigger syntax, but in brief:

• everything should be lowercase and contain no punctuation
• [ ] indicates optional inputs
• (a|b) matches either a or b

Note that the first response’s text is followed by a bit of code which fires another Trigger. “{@help}” does exactly the same thing as if the user had just typed “help”… so the last Trigger in the script will get fired as well, asking them what they’d like to hear about and showing the user some suggestions.

All the other responses in this script end with “{topic=default}“, which redirect the user back to the “default” topic. Dexter chatbots are stateful, so if we didn’t send them back to the “default” topic at the end of our response, they would remain inside the “faq” topic indefinitely. Sometimes that’s desirable, but not for what we’re about to do.

Before moving on, take a minute to try out the test interface on the right-hand side of the page. This simulates what an actual user would see. Type “hi there”, and you should see a response similar to:

Welcome!
I'm here to answer your questions. What would you like to hear about?
[1] Hours
[2] Services
[3] Contact Info

### Step 2: create responses to reassure the user

As people chat with our bot, they may occasionally become annoyed: maybe they wish our business hours were different, or are dissatisfied with our services. We’re going to analyze the tone (sentiment) of each of their statements. If we see a negative trend, we’ll interject some responses to help them feel a little better, improving customer engagement and retention.

To keep things clean, we’ll put these responses into a separate Topic, so hit that “New Topic” button and call this one “sentiments”. Now add this code, which contains the responses we’ll interject when we detect changes in their mood:

+ happy
- Great! Let's continue.nn{topic=faq}{@<get input>}
+ angry
- Sorry things aren't working for you. I'll do my best to fix it!nn{topic=faq}{@<get input>}
- Stick with me. We'll figure this out together.nn{topic=faq}{@<get input>}
+ veryangry
- I realize this is frustrating. Can I apologize with a coupon for 20% off your next purchase?nn{topic=faq}{@<get input>}
- It sounds like I'm not doing a good job of fixing the problem. Would it be easier for us to call you?nn{topic=faq}{@<get input>}
+ ok
- {topic=faq}{@<get input>}

Unlike our earlier scripts, these Triggers don’t contain “*“, because these Triggers won’t be fired off by the user’s actual typing. Later, they’ll be triggered explicitly by come code we’re about to add to the “default” Topic. When any of these Triggers is hit, it will reply with one of the responses (chosen randomly) from directly underneath the Trigger.

After we respond with a phrase to reassure the user, we want to continue processing their original input. Fortunately, we already have this stored in the user variable “input“, (which we set in the “default” script). So, we simply add “{topic=faq}{@<get input>}” to each response, indicating that we’ll switch them over to the “faq” Topic and send it the value of the “input” variable.

### Step 3: detect changes in mood

Each time the user sends a message, we’ll analyze its tone. This is known as Sentiment Analysis, and is too complex to implement directly inside a chatbot script. Fortunately, Dexter makes it easy to call APIs, so we can make use of Algorithmia’s SentimentAnalysis algorithm to determine how positive or negative the tone of each incoming message is.

Since we want to detect at changes in the user’s mood over time, we’ll also need to do a little bit of time series analysis. For this, we can use Algorithmia’s ExponentialMovingAverage. This API allows us to pass in several values (in this case, the sentiment level of the user’s last few statements) and get back a weighted moving average in which the most recent values have much greater importance than older ones.

Calling an API from Dexter requires just one line of code, placed after any Trigger. Grab this example, and place it in your “default” topic, replacing the code which is already there:

+ *
$POST:SA https://api.algorithmia.com/v1/algo/nlp/SentimentAnalysis/1.0.3 {"headers": {"Content-Type": "application/json", "Authorization": "Simple YOUR_API_KEY"}, "body": {"document":"<star>"}} - <set input=<star>>${{SA.result.0.sentiment}}

Before you can use this, you have to make one manual change. Calls to Algorithmia’s API require an API key, which you can get for free by heading to Algorithmia.com/signup. Once you’ve signed up, head to your profile page. Under the “Credentials” tab, copy your API key (starting with “sim…”). Use this to replace “YOUR_API_KEY” in the code above.

Now test this out, using the bot tester next to your script editor on Dexter. If everything is wired up properly, you can type in a positive statement like “This is Amazing!!” and get back a positive number (about 0.5859). Typing in a negative statement, such as “That’s terrible”, should yield a negative number (appx -0.4767).

Let’s break down the code so we understand what’s happening. First, the “+ *” Trigger catches the input. On the next line, “$POST:SA https://api.algorithmia.com/…” indicates that we’ll be calling an API (via the POST method), and assigns its output to the local variable “SA“. Following this, we have the data which will be passed into the POST — in this case, A JSON block: { "headers":{ "Content-Type":"application/json", "Authorization":"Simple YOUR_API_KEY" }, "body":{ "document":"<star>" } } The headers merely indicate that we’re sending JSON, and what API key it should be run under — this part will be the same regardless of which Algorithmia API you’re calling. The body, however, will vary, because that’s where we specify the actual information we need it to process. In this case, that information is quite simple: the SentimentAnalysis algorithm requires only a single parameter, “document“, whose content is the message that we want it to analyze. Since “<star>;” contains the user’s entire message, we simply pass this as the value — Dexter will replace it with the value of “*” when the POST is made. Lastly, the response line “–${{SA.result.0.sentiment}}” prints out the response from the API call. Specifically, “${{variable}}” gets the value of a local variable. But we don’t want to just print “${{SA}}“, because the API’s response contains several pieces of data. Successful responses from Algorithmia contain a “results” property. To know what’s inside this, we look at the Algorithm’s documentation, and see that it gives a result which looks like:

[
{
"sentiment": 0.5859,
"document": "This is Amazing!!"
}
]

So to get just the number, we access the first element of the result array, and the property “sentiment” inside this: “${{SA.result.0.sentiment}}”. Great! Now we can figure out the sentiment of a single message… but how do we remember it, so that we can look at changes over time? To do this, we need to add a javascript function to our “default” script. This function will accept a single sentiment value, append it to an array containing all past sentiment values, and return that array: > object getSentiment javascript var sentiment = Math.floor(100*parseFloat(args[0],10)); var sentimentHistory = rs.getUservar(rs.currentUser(), "sentimentHistory"); if(sentimentHistory=="undefined") {sentimentHistory=[0,0];} sentimentHistory.push(sentiment); rs.setUservar(rs.currentUser(), "sentimentHistory", sentimentHistory); return sentimentHistory < object Aside from the enclosing “> object functionName javascript” and “< object” tags, this is fairly straightforward javascript code. However, Dexter has a few differences worth noting: • All passed-in parameters end up in the locally-scoped variable args, and are Strings • There are no globally-scoped javascript variables: to store and retrieve data as user variables, use rs.setUservar(rs.currentUser(), “variableName”, variableValue); and rs.getUservar(rs.currentUser(), “variableName”); • Undefined user variables have the value “undefined” (a String), not the JS primitive undefined Now that we have a new function, we’ll want to use it in our Trigger responses. After adding the above code to the top of your “default” file, modify your response to the default “*” trigger to read: - <call>getSentiment${{SA.result.0.sentiment}}</call>

Test your bot again, and you should see results which are an array of values: “0,0,58” (note that we’ve seeded the array with [0,0] and are converting all decimals to 100x integers; this is because SentimentAnalysis requires at least three values to work, and because some of the comparisons we’ll be using later in the script don’t work well on fractions).

Now we can inspect the entire sentiment history of the conversation: if you make a positive statement, then a negative one, then a very positive one, your results will look something like “0,0,58,-42,80”. In order to detect whether the conversation is trending upward or downward, we can look at the moving average of this data series — specifically, we’ll use a technique called Exponential Moving Average, which considers the entire history of the data but places greater importance on the more recent responses.

Since Algorithmia has an Exponential Moving Average API, we can simply make another POST. To do so, we create another Trigger, then pass the results from our first POST into the new Trigger.

+ *
$POST:SA https://api.algorithmia.com/v1/algo/nlp/SentimentAnalysis/1.0.3 {"headers": {"Content-Type": "application/json", "Authorization": "Simple simjvRFrigTudCBoknDIhFap+YQ1"}, "body": {"document":"<star>"}} - <set input=<star>> {@movingaverage <call>getSentiment${{SA.result.0.sentiment}}</call>}
+ movingaverage *
$POST:MA https://api.algorithmia.com/v1/algo/TimeSeries/ExponentialMovingAverage/0.1.0 {"headers": {"Content-Type": "application/json", "Authorization": "Simple simjvRFrigTudCBoknDIhFap+YQ1"}, "body": [<star>]} -${{MA.result}}

Notice that we assigned the variable “MA” in the second POST, so we can use “${{MA.result}}” to get the result. Try this out, and you’ll get another comma-separated list of values… this one representing the moving averages over the values passed back from getSentiment. Since we only want to consider the last item in this list, we’ll add one more javascript function, and change our second Trigger response to use that function instead of referencing MA/results directly (note: when inside a javascript function, we use this.httpData.MA.result instead of${{MA.result}}):

> object getLastElement javascript
return _.last(this.httpData.MA.result);
< object
+ movingaverage *
$POST:MA https://api.algorithmia.com/v1/algo/TimeSeries/ExponentialMovingAverage/0.1.0 {"headers": {"Content-Type": "application/json", "Authorization": "Simple simjvRFrigTudCBoknDIhFap+YQ1"}, "body": [<star>]} - <call>getLastElement</call> Test this code, and you should see only a single value as the result. This number is the last value of the moving averages of the user’s messages thus far. Conceptually, it gives us an idea of whether the user is trending toward happiness or annoyance about the conversation so far. ### Step 4: interject an appropriate response Given this last value, which might be anywhere between -100 (exceptionally angry) and 100 (ecstatic), we now know what kind of response we might want to interject into the conversation, before moving on to the usual informational response. If the user is happy, we’ll acknowledge that. If they’re annoyed, we might apologize. Since we already have a set of mood responses in the “sentiments” Topic, we just need to map the numeric response into “veryangry”, “angry”, “ok”, or “happy” in that Topic. To do so, we make use of conditionals, which use the syntax “* value1 comparator value2 => response” to compare any two values, then execute the response on the right-hand side if the comparison’s is True. Using several such comparisons, followed by a default result, we can pick which response should be used: + movingaverage *$ POST:MA https://api.algorithmia.com/v1/algo/TimeSeries/ExponentialMovingAverage/0.1.0 {"headers": {"Content-Type": "application/json", "Authorization": "Simple YOUR_API_KEY"}, "body": [<star>]}
- {@respond <call>getLastElement</call>}
+ respond *
* <star> <= -30 => {topic=sentiments}{@veryangry}
* <star> <= -10 => {topic=sentiments}{@angry}
* <star> >= 30 => {topic=sentiments}{@happy}
- {topic=sentiments}{@ok}

For example, if the value returned by getLastElement was -20, the first response would get skipped (because -20 is not less than -30), and the second response would get selected, causing us to jump to the “sentiments” topic at the Trigger “angry”.

### Putting it all together

If you’ve followed each step above, your default Topic should contain the following code (if not, just copy-and-paste this all into “default”):

> object getSentiment javascript
var sentiment = Math.floor(100*parseFloat(args[0],10));
var sentimentHistory = rs.getUservar(rs.currentUser(), "sentimentHistory");
if(sentimentHistory=="undefined") {sentimentHistory=[0,0];}
sentimentHistory.push(sentiment);
rs.setUservar(rs.currentUser(), "sentimentHistory", sentimentHistory);
return sentimentHistory
< object
> object getLastElement javascript
return _.last(this.httpData.MA.result);
< object
+ *
$POST:SA https://api.algorithmia.com/v1/algo/nlp/SentimentAnalysis/1.0.3 {"headers": {"Content-Type": "application/json", "Authorization": "Simple YOUR_API_KEY"}, "body": {"document":"<star>"}} - <set input=<star>> {@movingaverage <call>getSentiment${{SA.result.0.sentiment}}</call>}
+ movingaverage *
\$ POST:MA https://api.algorithmia.com/v1/algo/TimeSeries/ExponentialMovingAverage/0.1.0 {"headers": {"Content-Type": "application/json", "Authorization": "Simple YOUR_API_KEY"}, "body": [<star>]}
- {@respond <call>getLastElement</call>}
+ respond *
* <star> <= -30 => {topic=sentiments}{@veryangry}
* <star> <= -10 => {topic=sentiments}{@angry}
* <star> >= 30 => {topic=sentiments}{@happy}
- {topic=sentiments}{@ok}

Try interacting with your bot. Say “hi”, ask what hours the company is open, and send it a few negative messages. It will provide you the information you ask for each time, but will also apologize or offer to help make things better as your tone becomes more irate. Begin praising the bot’s responses, and it’ll interject a congratulatory phrase (“Great! Let’s continue”).