Build a chatbot using the Qlik Sense Natural language API

Qlik Sense has always been known as a visually compelling data and analytics platform helping users to discover and derive insights to actionable intelligence. To add to its charisma, Qlik built the Insight Advisor, an AI-powered natural language processing capability enabling users to ask questions in their own words through a chat interface.

This tutorial walks you through the steps to make your own custom chatbot using the Natural Language API.

Prerequisites:

Step 1: Start a new Node project

The aim here is to develop a chatbot using the NL API. For that NodeJS is used and the quickest way to create a Node project is to use the nebula.js command line interface like below:

npx @nebula.js/cli create mashup hello-saas

The command scaffolds a web project into the hello-saas folder with the following structure:

  • /src
    • configure.js - Initial configuration of nebula.js
    • connect.js - Connection setup with enigma.js
    • index.html - A minimal html page
    • index.js - Connect and visualize

index.js is where the code for connecting to the Natural Language API lies.

index.html defines the User interface for the Chatbot.

An additional chat.css style sheet needs to be added to the project in Step 2 for styling the chat interface.

Note: If any issues with executing the project is observed, it might be due to the latest version of parcel bundler(1.12.4). Do the following to resolve & rollback to previous version:

  • npm uninstall parcel-bundler
  • npm i --save-dev parcel-bundler@1.12.3

Step 2: Define the User Interface for the ChatBot

For this specific tutorial, the idea is to keep the chatbot's interface simple since the focus is to be able to communicate with the cognitive engine and get responses. So the chatbot is designed as a popup form and embedded at the bottom of a mashup page using the following HTML and CSS code.

HTML code

<!DOCTYPE html>
<html lang="en">

<head>
  <link rel="stylesheet" href="chat.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
  <script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
  <meta charset="UTF-8" />
  <title>Nebula mashup</title>
</head>

<body>
  <div class="content">
    <div class="toolbar"></div>
    <div class="object"></div>
  </div></br>

  <button type="button" class="btn btn-primary btn-floating" onclick="openForm()">Chat</button>

  <div class="chat-popup" id="myForm">
    <div class="form-container">

      <div class="chat-output" id="chat-output">
        <div class="user-message">
          <div class="message">Hi! I'm Qlik Bot, what's up?</div>
        </div>
      </div>
      <div class="chat-input">
        <form action="#0" id="user-input-form" autocomplete="off">
          <input type="text" id="user-input" class="user-input" placeholder="Talk to the bot.">
        </form>
      </div>
      </br></br>
      <button type="button" class="btn cancel" onclick="closeForm()">Close</button>
    </div>
  </div>

  <script src="./index.js"></script>
  <script>
    function openForm() {
      document.getElementById("myForm").style.display = "block";
    }
    function closeForm() {
      document.getElementById("myForm").style.display = "none";
    }
    function eraseText() {
      document.getElementById("user-input").value = "";
    }
  </script>
</body>

</html>

Now, create a new style sheet, chat.css in the /src directory and add the following code:

CSS code

@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700);
* {
  box-sizing: border-box;
}

html {
  background: rgba(0, 0, 0, 0.03);
}

.content {
  margin: 0 auto;
  width: 80%;
  max-width: 800px;
}

.object {
  position: relative;
  height: 600px;
  width: 1000px;
}

.toolbar {
  margin: 12px 0px;
  width: 1000px;
}

.open-button {
  background-color: #555;
  color: white;
  padding: 16px 20px;
  border: none;
  cursor: pointer;
  opacity: 0.8;
  position: fixed;
  bottom: 23px;
  right: 28px;
  width: 280px;
}

.btn {
  background-color: #555;
  color: white;
  padding: 16px 20px;
  border: none;
  cursor: pointer;
  opacity: 0.8;
  position: fixed;
  bottom: 23px;
  right: 28px;
  width: 280px;
}

/* The popup chat - hidden by default */
.chat-popup {
  display: none;
  position: fixed;
  bottom: 0;
  right: 15px;
  border: 3px solid #f1f1f1;
  z-index: 9;
}

/* Add styles to the form container */
.form-container {
  width: 400px;
  height: 450px;
  padding: 10px;
  background-color: white;
  overflow: scroll;
  position: relative;
}

/* When the textarea gets focus, do something */
.form-container textarea:focus {
  background-color: #ddd;
  outline: none;
}

/* Set a style for the submit/send button */
.form-container .btn {
  background-color: #04aa6d;
  color: white;
  padding: 16px 20px;
  border: none;
  cursor: pointer;
  width: 100px;
  margin-bottom: 10px;
  opacity: 0.8;
  right: 40px;
}

/* Add a red background color to the cancel button */
.form-container .cancel {
  background-color: red;
}

/* Add some hover effects to buttons */
.form-container .btn:hover,
.open-button:hover {
  opacity: 1;
}

body,
html {
  height: 100%;
}

body {
  background: #eee;
  font-family: "Source Sans Pro", sans-serif;
  font-size: 115%;
  display: flex;
  flex-direction: column;
  max-width: 700px;
  margin: 0 auto;
}

.chat-output {
  flex: 1;
  padding: 50px;
  display: flex;
  background: white;
  flex-direction: column;
}
.chat-output div {
  margin: 0 0 20px 0;
}
.chat-output .user-message .message {
  background: #00cc44;
  color: white;
}
.chat-output .bot-message {
  text-align: right;
}
.chat-output .bot-message .message {
  background: #eee;
}
.chat-output .message {
  display: inline-block;
  padding: 12px 20px;
  border-radius: 10px;
}

.chat-input {
  padding: 20px;
  background: #eee;
  border: 1px solid #ccc;
  border-bottom: 0;
}
.chat-input .user-input {
  width: 100%;
  font-size: 2rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 8px;
}

The result is below:

QS Mashup with chatbot embedded

Step 3: API and connection

The final step is to use the API to communicate with the cognitive engine. To do that, /src/index.js file is used. Below is the exposed public endpoint and the nitty-gritty in the request-response process.

API endpoint: /api/v1/questions/actions/ask

REQUEST: first, the text that is being typed in the chatbot needs to be read to be passed as the body of the POST request. To do that, do as shown below. Now, the variable 'message' contains the input text.

var outputArea = $("#chat-output");

$("#user-input-form").on("submit", function(e) {
   e.preventDefault();
   var message = $("#user-input").val();
  outputArea.append(`
    <div class='bot-message'>
      <div class='message'>
        ${message}
      </div>
    </div>
  `);

To initiate the POST request,a few important parameters needs to be passed as shown in the code snippet below.

const https = require('https');
const data = JSON.stringify({"text":message,"app":{"id":"daf3a831-2437-45ca-95c3-928adfd2e6d9","name":"ABC Sales"}})
const options = {
   hostname: 'open-lib-services.eu.qlik-stage.com',
   port: 443,
   path: '/api/v1/questions/actions/ask',
   method: 'POST',
   headers: {
     'Content-Type': 'application/json',
     'Authorization': 'Bearer <API Key>',
     'qlik-web-integration-id': 'XXcljARnWPJpCqo2finVsAVBbqHLfXo1',
   },
 }

Here is a closer look at these parameters:

  • The data is basically the Body of the POST request, and the 'message' variable is assigned to the "text" property to pass whatever is typed in the chatbot. Also, since in this case, only one particular Qlik Sense app(not cross-apps) is used, the "id" and "name" of the app is passed.

  • The next important parameter is the options. Here the communication arguments such as hostname, port, API path, method type, and Qlik-specific qlik-web-integration-id, and API Key(authorization) are specified as part of the Headers.

RESPONSE: finally, the JSON response is parsed and can be used as per the requirements. Typically, the responses are of three types and have some important properties that can be returned to the chatbot.

  • narrative - "text": contains the textual description generated by the Natural Language Generation component
  • chart - "imageUrl": gives the path of the associated visualization chart
  • info - "infoValues"/ "recId": gives the ID of the recommendations based on the input text

For this user scenario, the focus is on 'narrative' text and 'chart'; so these two properties are sent as responses to the chatbot interaction in the UI.

So, how to do that?

First step is to verify if the engine's response is just a 'narrative'. If so, return the 'text' response like below:

const req = https.request(options, res => {
res.setEncoding('utf8');
res.on('data', d=>{
const myobj = JSON.parse(d)
if('narrative' in myobj.conversationalResponse.responses[0]){
  const temp = myobj.conversationalResponse.responses[0].narrative.text
  outputArea.append(`
      <div class='user-message'>
        <div class='message'>
          ${temp}
        </div>
      </div>
    `);
  }

If the engine's response also has a visualization image associated with it, do the following:

  else if('imageUrl' in myobj.conversationalResponse.responses[0]){
  const img = myobj.conversationalResponse.responses[0].imageUrl
    if('narrative' in myobj.conversationalResponse.responses[1]){
    const text_r = myobj.conversationalResponse.responses[1].narrative.text
    outputArea.append(`
      <div class='user-message'>
      <div class ="message">
      ${text_r}
      <a href="https://open-lib-services.eu.qlik-stage.com/${img}"><img src="https://open-lib-services.eu.qlik-stage.com/${img}" width="300" height="200"></a>
      </div>
      </div>
    `,

   );
  }
  else{
outputArea.append(`
      <div class='user-message'>
        <div class='message'>
         <img src="https://open-lib-services.eu.qlik-stage.com/${img}" width="300" height="200">
        </div>
      </div>
    `);
  }
  }
})
 })

Here is the complete code. Copy the following to your index.js file

var outputArea = $("#chat-output");

$("#user-input-form").on("submit", function (e) {
  e.preventDefault();

  var message = $("#user-input").val();

  outputArea.append(`
    <div class='bot-message'>
      <div class='message'>
        ${message}
      </div>
    </div>
  `);

  const https = require("https");
  const data = JSON.stringify({
    text: message,
    app: { id: "daf3a831-2437-45ca-95c3-928adfd2e6d9", name: "ABC Sales" },
  });
  const options = {
    hostname: "open-lib-services.eu.qlik-stage.com",
    port: 443,
    path: "/api/v1/questions/actions/ask",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization:
        "Bearer <API_key>",
      "qlik-web-integration-id": "XXcljARnWPJpCqo2finVsAVBbqHLfXo1",
    },
  };

  const req = https.request(options, (res) => {
    res.setEncoding("utf8");
    res.on("data", (d) => {
      const myobj = JSON.parse(d);
      if ("narrative" in myobj.conversationalResponse.responses[0]) {
        const temp = myobj.conversationalResponse.responses[0].narrative.text;
        outputArea.append(`
      <div class='user-message'>
        <div class='message'>
          ${temp}
        </div>
      </div>
    `);
      } else if ("imageUrl" in myobj.conversationalResponse.responses[0]) {
        const img = myobj.conversationalResponse.responses[0].imageUrl;
        if ("narrative" in myobj.conversationalResponse.responses[1]) {
          const text_r =
            myobj.conversationalResponse.responses[1].narrative.text;
          outputArea.append(`
      <div class='user-message'>
      <div class ="message">
      ${text_r}
      <a href="https://open-lib-services.eu.qlik-stage.com/${img}"><img src="https://open-lib-services.eu.qlik-stage.com/${img}" width="300" height="200"></a>
      </div>
      </div>
    `);
        } else {
          outputArea.append(`
      <div class='user-message'>
        <div class='message'>
         <img src="https://open-lib-services.eu.qlik-stage.com/${img}" width="300" height="200">
        </div>
      </div>
    `);
        }
      }
    });
  });

  req.on("error", (error) => {
    console.error(error);
  });

  req.write(data);
  req.end();

  $("#user-input").val("");
});

Make sure to input the right API Key under 'Bearer' authorization in the preceding code.

And here's the first Qlik chatbot in action:

Qlik ChatBot in a Mashup

This brings to the end of this tutorial on developing a chatbot using the new Natural Language API. The tutorial aims to serve as a boilerplate for the future development of chatbots using the API.

In case you missed anything from the preceding steps, the complete code for this project is shared below:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <link rel="stylesheet" href="chat.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
  <script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
  <meta charset="UTF-8" />
  <title>Nebula mashup</title>
</head>

<body>
  <div class="content">
    <div class="toolbar"></div>
    <div class="object"></div>
  </div></br>

  <button type="button" class="btn btn-primary btn-floating" onclick="openForm()">Chat</button>

  <div class="chat-popup" id="myForm">
    <div class="form-container">
      <img src="qlik_logo.png" width="55" height="55" alt="qlik">

      <div class="chat-output" id="chat-output">
        <div class="user-message">
          <div class="message">Hi! I'm Qlik Bot, what's up?</div>
        </div>
      </div>
      <div class="chat-input">
        <form action="#0" id="user-input-form" autocomplete="off">
          <input type="text" id="user-input" class="user-input" placeholder="Talk to the bot.">
        </form>
      </div>
      </br></br>
      <button type="button" class="btn cancel" onclick="closeForm()">Close</button>
    </div>
  </div>

  <script src="./index.js"></script>
  <script>
    function openForm() {
      document.getElementById("myForm").style.display = "block";
    }
    function closeForm() {
      document.getElementById("myForm").style.display = "none";
    }
    function eraseText() {
      document.getElementById("user-input").value = "";
    }
  </script>
</body>

</html>

index.js

var outputArea = $("#chat-output");

$("#user-input-form").on("submit", function (e) {
  e.preventDefault();

  var message = $("#user-input").val();

  outputArea.append(`
    <div class='bot-message'>
      <div class='message'>
        ${message}
      </div>
    </div>
  `);

  const https = require("https");
  const data = JSON.stringify({
    text: message,
    app: { id: "daf3a831-2437-45ca-95c3-928adfd2e6d9", name: "ABC Sales" },
  });
  const options = {
    hostname: "open-lib-services.eu.qlik-stage.com",
    port: 443,
    path: "/api/v1/questions/actions/ask",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization:
        "Bearer <API_key>",
      "qlik-web-integration-id": "XXcljARnWPJpCqo2finVsAVBbqHLfXo1",
    },
  };

  const req = https.request(options, (res) => {
    res.setEncoding("utf8");
    res.on("data", (d) => {
      const myobj = JSON.parse(d);
      if ("narrative" in myobj.conversationalResponse.responses[0]) {
        const temp = myobj.conversationalResponse.responses[0].narrative.text;
        outputArea.append(`
      <div class='user-message'>
        <div class='message'>
          ${temp}
        </div>
      </div>
    `);
      } else if ("imageUrl" in myobj.conversationalResponse.responses[0]) {
        const img = myobj.conversationalResponse.responses[0].imageUrl;
        if ("narrative" in myobj.conversationalResponse.responses[1]) {
          const text_r =
            myobj.conversationalResponse.responses[1].narrative.text;
          outputArea.append(`
      <div class='user-message'>
      <div class ="message">
      ${text_r}
      <a href="https://open-lib-services.eu.qlik-stage.com/${img}"><img src="https://open-lib-services.eu.qlik-stage.com/${img}" width="300" height="200"></a>
      </div>
      </div>
    `);
        } else {
          outputArea.append(`
      <div class='user-message'>
        <div class='message'>
         <img src="https://open-lib-services.eu.qlik-stage.com/${img}" width="300" height="200">
        </div>
      </div>
    `);
        }
      }
    });
  });

  req.on("error", (error) => {
    console.error(error);
  });

  req.write(data);
  req.end();

  $("#user-input").val("");
});

chat_new.css

@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700);
* {
  box-sizing: border-box;
}

html {
  background: rgba(0, 0, 0, 0.03);
}

.content {
  margin: 0 auto;
  width: 80%;
  max-width: 800px;
}

.object {
  position: relative;
  height: 600px;
  width: 1000px;
}

.toolbar {
  margin: 12px 0px;
  width: 1000px;
}

.open-button {
  background-color: #555;
  color: white;
  padding: 16px 20px;
  border: none;
  cursor: pointer;
  opacity: 0.8;
  position: fixed;
  bottom: 23px;
  right: 28px;
  width: 280px;
}

.btn {
  background-color: #555;
  color: white;
  padding: 16px 20px;
  border: none;
  cursor: pointer;
  opacity: 0.8;
  position: fixed;
  bottom: 23px;
  right: 28px;
  width: 280px;
}

/* The popup chat - hidden by default */
.chat-popup {
  display: none;
  position: fixed;
  bottom: 0;
  right: 15px;
  border: 3px solid #f1f1f1;
  z-index: 9;
}

/* Add styles to the form container */
.form-container {
  width: 400px;
  height: 450px;
  padding: 10px;
  background-color: white;
  overflow: scroll;
  position: relative;
}

/* When the textarea gets focus, do something */
.form-container textarea:focus {
  background-color: #ddd;
  outline: none;
}

/* Set a style for the submit/send button */
.form-container .btn {
  background-color: #04aa6d;
  color: white;
  padding: 16px 20px;
  border: none;
  cursor: pointer;
  width: 100px;
  margin-bottom: 10px;
  opacity: 0.8;
  right: 40px;
}

/* Add a red background color to the cancel button */
.form-container .cancel {
  background-color: red;
}

/* Add some hover effects to buttons */
.form-container .btn:hover,
.open-button:hover {
  opacity: 1;
}

body,
html {
  height: 100%;
}

body {
  background: #eee;
  font-family: "Source Sans Pro", sans-serif;
  font-size: 115%;
  display: flex;
  flex-direction: column;
  max-width: 700px;
  margin: 0 auto;
}

.chat-output {
  flex: 1;
  padding: 50px;
  display: flex;
  background: white;
  flex-direction: column;
}
.chat-output div {
  margin: 0 0 20px 0;
}
.chat-output .user-message .message {
  background: #00cc44;
  color: white;
}
.chat-output .bot-message {
  text-align: right;
}
.chat-output .bot-message .message {
  background: #eee;
}
.chat-output .message {
  display: inline-block;
  padding: 12px 20px;
  border-radius: 10px;
}

.chat-input {
  padding: 20px;
  background: #eee;
  border: 1px solid #ccc;
  border-bottom: 0;
}
.chat-input .user-input {
  width: 100%;
  font-size: 2rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 8px;
}
Was this page helpful?