How to integrate a voice assistant using CRMScript, ClientCrossMessaging, Custom API & Alan AI

Von Dennis Pabst, Aktualisiert am 24. Jan. 2021
push_pin
star

As voice assistants become more robust, I had the idea to integrate one into SuperOffice.

At the end of this tutorial, you’ll find a short video demonstrating all examples and the full ready-to-go package as a download.

 

Getting started

The first thing we need to do is sign up to Alan and create a voice assistant where we can add some code to let Alan listen to our speeches.

In the first example, we want to send SuperOffice data to Alan. 

We add a simple “Hey SuperOffice” and Alan should respond with “Hello [SuperOffice User Firstname]!”. 

Unfortunately, I couldn’t use “Hey Hugo”, because Alan recognized my spoken word “Hugo” as “Google” all the time :(

Let’s start by receiving data from SuperOffice in the voice script. For this, we use Alan’s Project API to send information from our CRMScript to the voice script. 

We store the SuperOffice user’s first name (more on this later) in the special userData variable which persists between all voice sessions:

projectAPI.setClientData = function(p, params, callback) {
  p.userData.user = params.user;
  callback(null, "Data received by Alan");
};

Next, we add our first intent and make it more dynamic so that Alan not only recognize “Hey” but also “Hello”, “Hi”, “Good morning”, etc. And then we grab the user’s first name from our userData object:

intent("(Hey|Hi|Hello|Good morning) SuperOffice", p => {
    p.play("Hello " + p.userData.user.firstname + "!")
});

This is it, now let’s jump over to SuperOffice, create a new CRMScript in which we initialize Alan and print the active user’s first name into the parameters of the callProjectApi function so that we send it to Alan every time a user opens our script through a web panel:

%EJSCRIPT_START%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Voice Assistant</title>
</head>
<body>
<div class="alan-btn"></div>
<script type="text/javascript" src="https://studio.alan.app/web/lib/alan_lib.min.js"></script>
<script>
  var alanBtnInstance = alanBtn({
    key: "ENTER YOUR KEY HERE (ALAN STUDIO -> INTEGRATIONS)",
    onCommand: function (commandData) {},
    rootEl: document.getElementById("alan-btn"),
  });

  alanBtnInstance.callProjectApi("setClientData", { user: { firstname: "<%print(getActiveUser().getValue('firstname'));%>" }}, function (error, result){
    if (error) 
    {
      console.log(error); 
      return; 
    }
  });
</script>
</body>
</html>
%EJSCRIPT_END%

Last, we create the web panel with the includeId of the script:

Yeah, works great. Alan answers with “Hello Dennis!”. (see example in video below)

 

ClientCrossMessaging

In this example we want to let Alan navigate us through different SuperOffice screens. For example if we say “Go to contact”, “Go to person”, “Go to sale”, “Go to service” etc. it should automatically jump to the requested screen.

To be able to navigate, we use the ClientCrossMessaging js library to communicate from our embedded web panel script to the SuperOffice client.

So first, let’s add the ClientCrossMessaging snippet to our CRMScript:

%EJSCRIPT_START%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Voice Assistant</title>
</head>
<body>
<div class="alan-btn"></div>
<script>
var SuperOffice = SuperOffice || {};
SuperOffice.ClientCrossMessaging = SuperOffice.ClientCrossMessaging || {};
(function(ns)
{
    var sendCommand = function(command, args) {
        var message = { "command": command, "arguments": args};
        parent.postMessage(message, "*");
    }

    ns.refresh = function() {
        sendCommand("refresh");
    }

    ns.executeSoProtocol = function(protocol) {
        sendCommand("soprotocol", protocol);
    }

    ns.openDocument = function(documentId) {
        sendCommand("openDocument", documentId);
    }

    ns.ajaxMethod = function(method,...args) {
        var a = [method,...args];
        sendCommand("ajaxMethod", a);
    }
}(SuperOffice.ClientCrossMessaging));  
</script>
<script type="text/javascript" src="https://studio.alan.app/web/lib/alan_lib.min.js"></script>
<script>
  var alanBtnInstance = alanBtn({
    key: "ENTER YOUR KEY HERE (ALAN STUDIO -> INTEGRATIONS)",
    onCommand: function (commandData) {},
    rootEl: document.getElementById("alan-btn"),
  });

  alanBtnInstance.callProjectApi("setClientData", { user: { firstname: "<%print(getActiveUser().getValue('firstname'));%>" }}, function (error, result){
    if (error) 
    {
      console.log(error); 
      return; 
    }
  });
</script>
</body>
</html>
%EJSCRIPT_END%

Next we've to add some more code to our Alan instance onCommand function so that we can receive the user’s requested screen and use the executeSoProtocol function from ClientCrossMessaging to navigate. 

If the user wants to go to Service we’ll not use executeSoProtocol, but window.open to open Service in a new tab like the existing Service button at the left navigator. We simply print getProgramTicket() to get the Service url.

%EJSCRIPT_START%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Voice Assistant</title>
</head>
<body>
<div class="alan-btn"></div>
<script>
var SuperOffice = SuperOffice || {};
SuperOffice.ClientCrossMessaging = SuperOffice.ClientCrossMessaging || {};
(function(ns)
{
    var sendCommand = function(command, args) {
        var message = { "command": command, "arguments": args};
        parent.postMessage(message, "*");
    }

    ns.refresh = function() {
        sendCommand("refresh");
    }

    ns.executeSoProtocol = function(protocol) {
        sendCommand("soprotocol", protocol);
    }

    ns.openDocument = function(documentId) {
        sendCommand("openDocument", documentId);
    }

    ns.ajaxMethod = function(method,...args) {
        var a = [method,...args];
        sendCommand("ajaxMethod", a);
    }
}(SuperOffice.ClientCrossMessaging));  
</script>
<script type="text/javascript" src="https://studio.alan.app/web/lib/alan_lib.min.js"></script>
<script>
  var alanBtnInstance = alanBtn({
    key: "ENTER YOUR KEY HERE (ALAN STUDIO -> INTEGRATIONS)",
    onCommand: function (commandData) {
      if (commandData.command === "navigate")
      {
        if (commandData.screen === "service") 
        {
          window.open("<%print(getProgramTicket());%>"); 
          return;
        }
        SuperOffice.ClientCrossMessaging.executeSoProtocol(commandData.screen);
      }
    },
    rootEl: document.getElementById("alan-btn"),
  });

  alanBtnInstance.callProjectApi("setClientData", { user: { firstname: "<%print(getActiveUser().getValue('firstname'));%>" }}, function (error, result){
    if (error) 
    {
      console.log(error); 
      return; 
    }
  });
</script>
</body>
</html>
%EJSCRIPT_END%

Last but not least jump over to your voice script and add another intents. Again we want to accept more words than only “Go”. We add “Jump” and “Open” as well.

If we put an object into the play function Alan will send this back to the client and not just say the text. The value for the screen key is simply the string that the executeSoProtocol function from the ClientCrossMessaging library can process. You can get the correct soprotocol string in the url search parameters while navigating in SuperOffice yourself.

intent("(Go|Jump|Open) (to) Dashboard", p => {
  p.play({"command": "navigate", "screen": "dashboard"})  
});

intent("(Go|Jump|Open) (to) (Contact|Company)", p => {
  p.play({"command": "navigate", "screen": "contact.main"})  
});

intent("(Go|Jump|Open) (to) Person", p => {
  p.play({"command": "navigate", "screen": "person.main"})  
});

intent("(Go|Jump|Open) (to) Sale", p => {
  p.play({"command": "navigate", "screen": "sale.main"})  
});

intent("(Go|Jump|Open) (to) Project", p => {
  p.play({"command": "navigate", "screen": "project.main"})  
});

intent("(Go|Jump|Open) (to) Selection", p => {
  p.play({"command": "navigate", "screen": "selectionsearch.main"})  
});

intent("(Go|Jump|Open) (to) Service", p => {
  p.play({"command": "navigate", "screen": "service"})  
});

Additionally let’s add another intent with more logic. We want to let the user choose between Diary: Day, Week, Month or View. We store the option in a variable and do a switch to send the correct soprotocol:

intent("(Go|Jump|Open) (to) (my|the) (Diary|Calendar) $(C day|week|month|view)", p => {
  switch (p.C.value) {
      case "day":
        p.play({"command": "navigate", "screen": "diary.day"})
        break
      case "week":
        p.play({"command": "navigate", "screen": "diary.week"})
        break 
      case "month":
        p.play({"command": "navigate", "screen": "diary.month"})
        break 
      case "view":
        p.play({"command": "navigate", "screen": "diary.view"})
        break 
      default:
         p.play({"command": "navigate", "screen": "diary"}) 
  } 
});

Great this works smoothly. See the gifs:

 

Custom API

In the last example we want to let Alan get data from SuperOffice and say the amount of user’s appointments for today if I ask him: “How many appointments do I have today?”

To make this work we create another CRMScript as Rest API which Alan can call, get and recite the data.

I don’t go into further detail regarding creating Custom API’s with CRMScript, because Frode created this great tutorial here. Of course, you can use our Official API as well, but I want to give you a quick reminder on what great things you can do with CRMScript.

In the new script we grab the user`s associateId from the parameters via getCgiVariable() and use the SearchEngine to get the amount of user`s appointments for today. 

Additionally I use the CRMScripts HTTP library in another CRMScript and include it here to simply respond with predefined headers and status codes.

%EJSCRIPT_START%
<%
#setLanguageLevel 3;
#include "voiceAssistantHTTP";

Integer associateId = getCgiVariable("associateId").toInteger();

if (associateId > 0)
{
  SearchEngine se;
  se.addField("appointment.appointment_id");
  se.addCriteria("appointment.associate_id", "equals", associateId.toString());
  se.addCriteria("appointment.do_by", "gte", getCurrentDateTime().moveToDayStart().toString());
  se.addCriteria("appointment.do_by", "lte", getCurrentDateTime().moveToDayEnd().toString());
  se.execute();
  print('{"count":' + se.countRows().toString() + '}');              
}
else
{
  print('{"message":"Invalid associateId"}');
  BadRequest();
}
%>
%EJSCRIPT_END%

Next we've to do some adjustments to our main web panel CRMScript’s callProjectApi function. Remember we only send the first name to Alan? Let`s add the associateId to our user object that is required by our custom api and the url of our custom api in a new object called client.

alanBtnInstance.callProjectApi("setClientData", { client: { apiUrl: "<%print(getProgramCustomer()+'&action=safeParse&includeId=voiceAssistantAPI&key=voiceAssistantAPI');%>" }, user: { associateId: "<%print(getActiveUser().getValue('associateId'));%>", firstname: "<%print(getActiveUser().getValue('firstname'));%>" }}, function (error, result){
  	if (error) 
    {
     	console.log(error); 
      return; 
    }
});

Now we've to adjust our voice script to include the client object so that we make our api url accessible through the userData object as well.

projectAPI.setClientData = function(p, params, callback) {
  p.userData.client = params.client
  p.userData.user = params.user;
  callback(null, "Data received by Alan");
};

Last we've to add our new command. This time we use the question function and it’s really great that Alan supports axios to make API calls.

question("How many appointments do I have today?", p => {
    const url = `${p.userData.client.apiUrl}&associateId=${p.userData.user.associateId}`;
    api.axios.get(url)
        .then((res) => {
            p.play(`You have ${res.data.count} appointment${res.data.count === 1 ? "" : "s"} today`);
        })
        .catch((err) => {
            console.log(err);
            p.play(`Could not get appointment information`);
    });
});

Done. When I ask Alan now “How many appointments do I have today?”, Alan responds with “You have two appointments today”. When you add another appointment for today and ask Alan again then you’ll get the new amount. It's really fun to play around with it. (see example in video below)

Of course, you can also expand this example to get any data from SuperOffice or write data to SuperOffice.

Alan AI not only provides intent and question, but also reply and follow to create certain conversational circumstances or dialog branches like one idea that came directly into my mind:

You: “Create a Contact”

Alan: “What is the name of the contact?”

You: “SuperOffice GmbH”

Alan: “In which country is the contact located?”

You: “Germany” 

... ->

  • Call Google Places API with the contact name and country to collect all the information about the contact like address etc.
  • Post to SuperOffice API to create the contact with the user’s inputs and Google Places API data.
  • In the end navigate to the created contact via ClientCrossMessaging.

 

A million possibilies

The thing that strikes me about using voice assistants on the web is that there is so much under-explored territory. I've been experimenting with it for a while now, and I still feel like I'm just scratching the surface.

You've been given the examples to start experimenting and I'd encourage you to have some fun with this, and see where it takes you :)

If you enjoyed this tutorial, comment with your ideas, business logics and I'll add them here sooner or later. Let me know if you've any questions.

Best,
Dennis

 

Links

 

Video

https://vimeo.com/503917365

 

Package

You can import this package with Service. There you'll find the Alan voice script (paste this code into your voice assistant editor), the web panel script, custom api script & http library script.

Remember to put in your Alan key in the top variable of the voiceAssistantPage script to make it fully functional.

Download

Cool Dennis!
Georg Diczig 4. Mai 2021
Very cool! Will give it a try!
Boyan Yordanov 10. Feb. 2021
This is indeed super cool!!! Love it!
Pierre Van Mever 26. Jan. 2021
That's awesome! Great job, Dennis! There are lots of ideas on how it could be utilized.
Denis Schönfeld 25. Jan. 2021
Very impressive. Maybe, this could fit into our Hugo AI strategy? This shows once again where the limits are. Exactly, your creativity and imagination are the limits. A cool example of how to connect cloud to cloud solutions.
Thumbs up, Dennis.
Christian Wyrwich 25. Jan. 2021
Nice Dennis!
Marcel Spapens 25. Jan. 2021
Cool :D
Suran Basharati 25. Jan. 2021
Wow, that is really cool, good job!
Frode Lillerud 24. Jan. 2021