In this article I am putting together a chatbot for Asset Control as a fun little project. Why? Because I had been wanting to implement a chatbot for a while - if only to understand how to do that - and doing so in the AC domain just seems natural. I will also integrate it with Slack to see how this could fit in with my already existing tools and workflow.
A chatbot to do what?
You will have come across chatbots in multiple places by now. They might be on company websites and help you to get commonly asked questions answered, allow you to make bookings etc.
In this case, I want to implement a prototype of a chatbot to integrate with Slack that allows me to communicate with Asset Control. I might want to ask it for status information of my AC env or request data.
To guide the implementation, I used the following questions the bot should be able to answer:
- What is the last price for XYZ?
- What was this months high for XYZ?
- How many suspects today?
- Is replication up to date?
We will see that once we have the first one in place, other questions are just a variation of the same pattern.
So, are we implementing the bot from scratch? Of course not. There are a number of solutions out there and I quickly settled for Google Dialogflow. Let’s see how this works.
Building the bot in Dialogflow
To build our bot in Google Dialogflow we need to understand a few concepts:
- Intents
- Entities
- Fulfillment
We’ll refer back to the first question “What is the last price for XYZ?" to explain these.
An intent bundles everything that belongs to a certain narrowly defined topic that the bot should handle. In our case that is the retrieval of the last available price of a stock from our system. We’ll use Dialogflow to train the bot to understand when the conversation is about such an intent by giving him training phrases. We’ll see that in a minute.
Then entities are building blocks that make up our conversation and are of particular interest so that we want to catch them as parameters. Now, in our case that would be the ticker symbol AAPL when we ask the question “What is the last price for AAPL?"
Lastly, the fulfillment concept allows us to reach out to services at the back of the chatbot via webhooks. That is were we will communicate with Asset Control to query for the given stock and retrieve the last available price.
Now let’s build our bot.
Step 1: Set up your bot.
Head to https://dialogflow.cloud.google.com/#/login and sign in. Then create an Agent AcChatBot:
Step 2: Create a Stock entity
From the menu on the left, click Plus next to Entities, call it Stock and enter [A-Z][A-Z0-9]{1,6}
as a regular
expression. While not entirely a match for ticker symbols it is sufficient for our example and works with the checks
on regular expressions carried out by Dialogflow itself (e.g. not too broad etc.). Then save it.
Step 3: Create the Last price intent
Time for our Last price intent. Again, from the menu on the left select the Plus next to Intents, call it Last price
and click on ADD TRAINING PHRASES:
Enter What is the last price for AAPL?
. Then highlight AAPL
with your mouse. This will bring up a pop-up. Search
for Stock
, then select the presented option:
If done right, the result will look like this to ensure that Dialogflow captures the Stock entity correctly:
IMPORTANT! Then scroll down, open up the Fulfillment section, click Enable Fulfillment and then toggle to
Enable webhook call for this intent
like so:
This allows us to call our own backend function and communicate with Asset Control.
Make sure to save the intent before moving on.
Step 4: Implement a REST service to retrieve Asset Control data
Again, there are many ways to do this. We will use Spring Boot as it does all the heavy lifting for us and we stay in Java and hence can make immediate use of the same Asset Control Java API that Adetta uses.
If you don’t know it yet, Adetta is our test automation software for Asset Control. Make sure to have a look at the Introduction to Adetta here.
The interesting bit is a class AcBotController
and its handleIntent
method as shown below:
@RestController
public class AcBotController {
private static JacksonFactory jacksonFactory = JacksonFactory.getDefaultInstance();
private AcBotIntents intents = new AcBotIntents();
@PostMapping(path = "/ac-bot", consumes = "application/json", produces = "application/json")
public ResponseEntity<?> handleIntent(@RequestBody String requestStr,
HttpServletRequest servletRequest) {
try {
GoogleCloudDialogflowV2WebhookRequest request =
jacksonFactory.createJsonParser(requestStr)
.parse(GoogleCloudDialogflowV2WebhookRequest.class);
AcBotIntentHandler intentHandler = intents.getHandlerFor(request);
GoogleCloudDialogflowV2WebhookResponse response =
intentHandler.handleIntent(request);
return new ResponseEntity<>(response, HttpStatus.OK);
} catch (Exception ex) {
return new ResponseEntity<Object>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}
}
}
You can see that the webhook for the Dialogflow fulfillment is handling a POST request. Both its input and output
are JSON. And we use GoogleCloudDialogflowV2Webhook*
classes to handle both the request and response JSON for us.
The actual handling of the intent is passed on to a AcBotIntentHandler we get from calling AcBotIntents.getHandlerFor(request)
.
Let’s look at this class:
public class AcBotIntents {
private static Logger log = Logger.getLogger(AcBotIntents.class);
private static final String LAST_PRICE = "Last price";
private AcConnection conn;
private AcQueryService ac;
private Map<String, AcBotIntentHandler> mapping;
public AcBotIntents() {
try {
conn = AcConnection.getDefaultConnection();
ac = new AcQueryService(conn);
} catch (AcException e) {
log.error("Could not initialise connection to AC!", e);
}
mapping = new HashMap<>();
mapping.put(LAST_PRICE, new AcBotLastPriceIntentHandler(ac));
}
public AcBotIntentHandler getHandlerFor(GoogleCloudDialogflowV2WebhookRequest request) {
String intent = request.getQueryResult().getIntent().getDisplayName();
return mapping.getOrDefault(intent, new AcBotDefaultIntentHandler());
}
}
We build up a mapping of intents to handlers (remember our Last price
intent?) and implement a method to retrieve
a suitable handler given the intent encoded in the request object. We also define a default handler in case we could
not find an actual handler:
public class AcBotDefaultIntentHandler implements AcBotIntentHandler{
@Override
public GoogleCloudDialogflowV2WebhookResponse handleIntent(GoogleCloudDialogflowV2WebhookRequest request) {
GoogleCloudDialogflowV2WebhookResponse response = new GoogleCloudDialogflowV2WebhookResponse();
response.setFulfillmentText("I am not sure I understand. Sorry.");
return response;
}
}
Now, to get our last price for a given stock, lets look at AcBotLastPriceIntentHandler
:
public class AcBotLastPriceIntentHandler implements AcBotIntentHandler {
private final AcQueryService ac;
private final String TREE = "CONSOLIDATION_C0";
private final String CLOSE = "CLOSE";
public AcBotLastPriceIntentHandler(AcQueryService ac) {
this.ac = ac;
}
@Override
public GoogleCloudDialogflowV2WebhookResponse handleIntent(GoogleCloudDialogflowV2WebhookRequest request) {
GoogleCloudDialogflowV2WebhookResponse response = new GoogleCloudDialogflowV2WebhookResponse();
String stock = (String) request.getQueryResult().getParameters().getOrDefault("Stock", "");
try {
response.setFulfillmentText(getLastPriceMessageForStock(stock));
} catch (AcException e) {
response.setFulfillmentText("I am afraid something went wrong requesting the data from AC. Sorry.");
}
return response;
}
// see below
}
You see how we retrieve our Stock entity parameter from the request object using request.getQueryResult().getParameters().getOrDefault("Stock", "")
and then call getLastPriceMessageForStock
as shown here:
private String getLastPriceMessageForStock(String stock) throws AcException {
if (stock.isEmpty()) {
return "I am not sure which stock you mean. Sorry.";
}
List<Ado> ados = ac.adoBrowse(String.format("symbol like 'C0.EQY%%' and attribute('C0#SA001', now) = '%s'", stock));
if (ados.isEmpty()) {
return String.format("I am afraid we don't have any data for '%s'. Sorry.", stock);
} else {
return ados.stream().map(ado -> this.getLastPriceMessageForAdo(stock, ado)).collect(Collectors.joining("\n"));
}
}
We guard ourselves against a missing value for stock. Otherwise we browse our Asset Control system for equities with the given ticker. In case we do not have any such ADO, we display the corresponding message. Otherwise we continue to retrieve pricing data for each ADO we have found (while that should only be a single ADO, the implementation would just return multiple messages if needed.)
Here is how we find the last price:
private String getLastPriceMessageForAdo(String stock, Ado ado) {
try {
return ado.loadTimeseries(TREE, attributes(CLOSE)).getLast().map(
lastRecord ->
String.format("The last price for %s (%s) is from %s at $%.2f.",
stock, ado.getId(), lastRecord.getDateStr("yyyy-MM-dd"), lastRecord.get(0).toDouble())
).orElse(String.format("Could not retrieve a last price for %s (%s). Sorry.", stock, ado.getId()));
} catch (AcException e) {
return String.format("An error occurred for %s!", ado.getId());
}
}
We load the ADO’s timeseries, get the last record and - if present - create a message that will read as The last price for AAPL (C0.EQY.100101) is from 2020-06-12 at $338.00
. And again we guard ourselves against cases where we cannot find a price.
Using Spring Boot we can fire this up and have our REST service running on localhost:8080/ac-bot. What next?
Well, we are obviously cheating a little as we are not deploying this service anywhere, so in order to make it available from the outside, we will use ngrok to set up a tunnel for us:
$> ngrok http 8080
grok by @inconshreveable (Ctrl+C to quit)
Session Status online
Session Expires 7 hours, 59 minutes
Version 2.3.35
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://3cb86ef0e935.ngrok.io -> http://localhost:8080
Forwarding https://3cb86ef0e935.ngrok.io -> http://localhost:8080
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Step 5: Set up our own fulfillment webhook
Next we have to let Dialogflow know where our REST service is, so head back there and click Fulfillment from the menu on the left:
Then enter the https://...
address given by ngrok and make sure you add the /ac-bot
at the end.
Ok, the moment of truth. In the top right corner where it says Try it now
, we can type What is the last price for AAPL?
and if everything is working ok we should get the following answer:
This is exciting!
Step 6: Integrate our chatbot with Slack
Now, we are not going to continue using the Dialogflow console to play with our bot. Instead we will integrate it with Slack. So, pick Integrations from the menu on the left, find Slack and start a test bot:
Then head over to Slack and start chatting to the bot:
Then we can repeat the cycle to implement the other questions/intents outlined at the start of the article.
Closing words
I thought that was fun to do. And easier than I thought, too. Of course, I took a number of short cuts:
- Only running the service locally.
- No consideration for security, access control and choice of Asset Control environment.
- Very limited use of the bot.
But it shows the underlying concepts and could easily be extended. I am actually quite keen to look into a more powerful Slack integration for Asset Control soon.
I hope you enjoyed this article. Get in touch to share your thoughts, ask questions etc. We are here to help.