Building a Chat Bot - Part 3: Interacting with Anaplan from Slack

AnaplanOEG
edited December 2022 in Best Practices

Since you’ve reached this point, I believe it is useful to remind you what we achieved so far. In both Part 1 for AWS and Part 1 for GCP, we used both platforms in order to host a function that can generate a new token every time that you need one.  This could be very helpful in any integration scenario that you can come up with involving those two platforms.  In Part 2, we built the foundations for a working bot whose code is currently handled on a local machine.

In that new chapter, we will begin to use Anaplan APIs in order to interact with an Anaplan model. Our bot will be able to retrieve a view from a model previously selected and if required, will offer the possibility to manipulate page filters.

Below, is what we want to achieve:

image

Let’s get started!

For the impatient ones: Using the provided python code (see attachments)

If you want to give an immediate try on the bot, download the 3 python files and place them inside the project you've built in the last chapter.

annejulie_0-1639070982792.png

Make sure you have configured your local server with ngrok. (You will likely need to run it again and update the URL in your Slack app as described in part 2).

Go to your lambda page in AWS or your Cloud Function in GCP.

Run the program to generate a token (by clicking on "Test") and copy the value.

annejulie_1-1639070982833.png

Paste this token value in the botExecution.py file:

annejulie_2-1639070982852.png

Trigger the botExecution.py file. You should receive the message: "Bot is running!"

annejulie_3-1639070982877.png

Go back to your bot app and via a direct message, write a simple message like this:

get connected to {modelInformations} 

where modelInformations can be model ID or model Name.

Keep in mind that the lookup via model name works only when the model is in your default tenant.

You should be getting a response asking you to choose a view. In that case, just follow the subsequent steps.

The following paragraphs give you more insight on how the code is structured and some advice we can give going through that exercise.

More information

Getting connected to the model and retrieving the views list.

In order to populate the drop-down list containing the views list from a model, you need to:

  • Get authenticated to the Anaplan API server
  • Perform the API request
  • Manipulate the response back from the previous request
  • Populate the drop-down list
  • The anaplanAPI.py file

Inside this file, there is a class called “AnaplanResourcesHandler”, dedicated to gathering the code for API requests to Anaplan.

There is also a function “runTests”, allowing the execution of that piece of code if you execute that Python file.

annejulie_4-1639070982913.png

A successful response would be getting the basic information of the model you’ve passed as parameters.

You’ll notice that the token is provided as a direct input. Why didn’t we implement a token generation function? Because when this code will be deployed in the cloud, we will leverage what we’ve built for the token generation.

Hence, for each call, we will have an up-to-date token and we will eschew timeouts as much as possible. We will come back on that later.

When opening up the class, we have 5 methods:

1. Function “performRequest”

It’s a very simple code, simply made to send a request to the API server and return the response from the latter.

2. Function "getModelBasicInfos"

@classmethod
def getModelBasicInfos(cls, token, modelCreds):
  modelCreds = modelCreds.****()
  endpoint = API_BASE_URL + "/models/"
  req = cls.performRequest(token, endpoint, "GET", "")
  if req.status < 401:
    response_dict = json.loads(req.data.decode('utf-8'))
    if response_dict["models"] is not None:
      modelIdById = [model for model in response_dict["models"] if model['id'].upper() == modelCreds.upper()]
      modelIdByName = [model for model in response_dict["models"] if model['name'].upper() == modelCreds.upper()]
      modelId = modelIdById if modelIdById != [] else modelIdByName
      return modelId[0] if modelId != [] else None
    return None
  return None

If the query is successful, the response should contain an object "models". In that case, we loop in that object to find the model ID we want, based either on the name or directly on the ID.

3. Function "getModelDetails"

 def getModelDetails(cls, token, modelId):
   if modelId is not None:
     print(modelId['id'])
     endpoint = API_BASE_URL + "/models/" + str(modelId["id"]) + "?modelDetails=true"
     headers = {"Authorization": "AnaplanAuthToken {}".format(token), "Content-Type": "application/json"}
     response = http.request('get', endpoint, headers=headers)
     print(response.data.decode('utf-8'))
     if response.status < 401:
       response_dict = json.loads(response.data.decode('utf-8'))
       return response_dict
   return None

Similar to the previous function, it uses the model ID you manage to obtain via this previous function and the suffix "?modelDetails=true" to the endpoint. It gives more information to the peculiar model, especially cells consumption (which could be very useful to know for tracking purposes).

4. Function "getList"

 def get_list(cls, token, modelId, suffix, resource):
   endpoint = API_BASE_URL + "/models/" + modelId + "/" + suffix
   *# print(endpoint)
\*   headers = {"Authorization": "AnaplanAuthToken {}".format(token), "Content-Type": "application/json"}
   response = http.request('get', endpoint, headers=headers)
   if response.status < 401:
     response_dict = json.loads(response.data.decode('utf-8'))
     listResources = None
     *# print(response_dict)
\*     if response_dict.get(resource) is not None:
       listResources = response_dict.get(resource)
     return listResources
   return None

This function is very generic: the suffix could be replaced by any Anaplan object like imports, exports, processes, views…

Using the suffix allows the code to find an object within the initial response that has a key named similar to the suffix and if it exists, the response will automatically be a list of that resource (i.e a list of processes, imports, or views).

5. Function "get_data"

 def get_data(cls, token, modelId, viewId, pageString):
   endpoint = API_BASE_URL + "/models/" + modelId + "/views/" + viewId + "/data"
   headers = {"Authorization": "AnaplanAuthToken {}".format(token), "Content-Type": "application/json"}
   if pageString != "":
     endpoint += "?pages=" + pageString + "&format=v1"
     response = http.request('get', endpoint, headers=headers)
     if response.status < 401:
       return response.data.decode('utf-8')
     return None
   else:
     response = http.request('get', endpoint, headers=headers)
     if response.status < 401:
       return response.data.decode('utf-8')
     return None

This function is about requesting the content of a view. You will notice that it separates two cases:

  • If the request contains page filtering values, the targeted endpoint will integrate those values.
  • If not, the endpoint will simply end with "/data"

For information, we use the urllib3 library. Our attention is to restrain as much as possible the requirements to make those files run on AWS. This library is already in any Lambda environment so there will be no need to add a layer. It might be a bit more complex to use than the famous requests library but for our use case, the difference is hardly noticeable.  If we were using the requests library (way more commonly used), we would have to add it to the lambda via layer.

The botExecution.py file

We will not be expanding on that code as it is very well explained on slack documentation itself.

What is important to keep in mind is that Slack interactions do not involve sessions per se so the real challenge was to keep track of the context of the model we are working on. For instance, once you get the list of filtering values, there is no easy way to remember the model information used in the first query.

The way we circumvent this issue is to add for each new item coming from a previous step the ID of that step.

Hence, when you select a filter page value in the pop-up modal view, under the hood, the values are not only designated with the internal Anaplan ID but also the model and the views IDs.

 

Again, this code is for demo purposes. You are free to use, edit and improve the code as you wish.

I hope you are enjoying that as much as we are.

 

The next part in our Chatbot series will be about pushing the whole thing into the cloud so that eventually, you'll have an always-on bot, available for your Anaplan queries.

 

Do not hesitate to give us your feedback and happy coding time!

Contributing authors: Christophe Keomanivong and Joey Morisette.

Comments

  • Very interesting article. Thanks for sharing! I look forward to see more use cases with slack integration.