GDPR Cookie Consent by Free Privacy Policy Generator
post thumb
Asset Control
by Matthias Hanitzsch-Moonlight/ on 14 Jun 2020

A chatbot for Asset Control

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:

Create chatbot

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.

Stock entity

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:

Stock entity

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:

Stock entity

If done right, the result will look like this to ensure that Dialogflow captures the Stock entity correctly:

Stock entity

IMPORTANT! Then scroll down, open up the Fulfillment section, click Enable Fulfillment and then toggle to Enable webhook call for this intent like so:

Stock entity

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:

Stock entity

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:

Stock entity

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:

Stock entity

Then head over to Slack and start chatting to the bot:

Stock entity

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.

comments powered by Disqus