Wednesday, February 28, 2018

AWS Lex Bot & Genesys Chat Integration


Summary

This post is the culmination of the posts below on Lex + Genesys chat builds.  In this one, I'll discuss how to build a web client interface that allows integration of the two chat implementations.  The client will start out in a bot session with Lex and then allow for an escalation to a Genesys Agent when the end-user makes the request for an agent.

Genesys Chat build:  http://joeywhelan.blogspot.com/2018/01/genesys-chat-85-installation-notes.html
AWS Lex Chat build: http://joeywhelan.blogspot.com/2018/02/aws-lex-chatbot-programmatic.html
Reverse Proxy to support GMS: http://joeywhelan.blogspot.com/2018/02/nodejs-reverse-proxy.html

Architecture Layer

Below is a diagram depicting the overall architecture.  AWS Lex + Lambda is utilized for chat bot functionality; Genesys for human chat interactions.  A reverse proxy is used to provide access to Genesys Mobility Services (GMS).  GMS is a web app server allowing programmatic access to the Genesys agent routing framework.

Transport Layer

Secure transport is used through out the architecture.  HTTPS is use for SDK calls to Lex.  The web client code itself is served up via a HTTPS server.  Communications between the web client and GMS are proxied and then tunneled through WSS via Cometd to provide support for asynchronous communcations between the web client and Genesys agent. 

Application Layer

I used the 'vanilla demo' included with the Cometd distro to build the web interface.  All the functionality of interest is contained in the chat.js file.  Integration with Lex is via the AWS Lex SDK.  Integration with Genesys is via publish/subscribe across Cometd to the GMS server.  GMS supports Cometd natively for asynchronous communications.


Application Flow

Below are the steps for an example scenario:  User starts out a chat session with a Lex bot, attempts to complete an interaction with Lex, encounters difficulties and asks for a human agent, and finally transfer of the chat session to an agent with the chat transcript.


Step 1 Code Snippets

        // Initialize the Amazon Cognito credentials provider
        AWS.config.region = 'us-east-1'; // Region
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({
            IdentityPoolId: 'us-east-1:yourId',
        });
        var _lexruntime = new AWS.LexRuntime();

        function _lexSend(text) {
         console.log('sending text to lex');
            var fromUser = _firstName + _lastName + ':'; 
            _displayText(fromUser, text);
        
            var params = {
              botAlias: '$LATEST',
        botName: 'OrderFirewoodBot',
        inputText: text,
        userId: _firstName + _lastName,
       };
            _lexruntime.postText(params, _lexReceive);
        }
Lines 1-6:  Javascript. AWS SDK set up.  An AWS Cognito pool must be created with an identity with permissions for the the Lex postText call.

Step 2 Code Snippets

RequestAgent Intent

An intent to capture the request for an agent needs to be added to the Lex Bot.  Below is a JSON-formatted intent object that can be programmatically built in Lex.
{
    "name": "RequestAgent",
    "description": "Intent for transfer to agent",
    "slots": [],
    "sampleUtterances": [
       "Agent",
       "Please transfer me to an agent",
       "Transfer me to an agent",
       "Transfer to agent"
    ],
    "confirmationPrompt": {
        "maxAttempts": 2,
        "messages": [
            {
                "content": "Would you like to be transferred to an agent?",
                "contentType": "PlainText"
            }
        ]
    },
    "rejectionStatement": {
        "messages": [
            {
                "content": "OK, no transfer.",
                "contentType": "PlainText"
            }
        ]
    },
    "fulfillmentActivity": {
        "type": "CodeHook",
        "codeHook": {
         "uri" : "arn:aws:lambda:us-east-1:yourId:function:firewoodLambda",
      "messageVersion" : "1.0"
        }
    }
}

Lambda Codehook

Python code below was added to the codehook described in my previous Lex post.  Lines 3-6 add an attribute/flag that can be interrogated on the client side to determine if a transfer to agent has been requested.
    def __agentTransfer(self):
        if self.source == 'FulfillmentCodeHook':
            if self.sessionAttributes:
                self.sessionAttributes['Agent'] = 'True';
            else:
                self.sessionAttributes = {'Agent' : 'True'}
            msg = 'Transferring you to an agent now.'
            resp = {
                    'sessionAttributes': self.sessionAttributes,
                    'dialogAction': {
                                        'type': 'Close',
                                        'fulfillmentState': 'Fulfilled',
                                        'message': {
                                            'contentType': 'PlainText',
                                            'content': msg
                                        }
                                    }
                    }
            return resp

Step 3 Code Snippets

Receiving the Lex response with the agent request

Lines 11-13 interogate the session attributes returned by Lex and then set up the agent transfer, if necessary.
        function _lexReceive(err, data) {
         console.log('receiving lex message')
         if (err) {
    console.log(err, err.stack);
   }
         
   if (data) {
    console.log('message: ' + data.message);
    var sessionAttributes = data.sessionAttributes;
    _displayText('Bot:', data.message);
    if (data.sessionAttributes && 'Agent' in data.sessionAttributes){
     _mode = 'genesys';
     _genesysConnect(_getTranscript());
    }
   } 
        }

Genesys connection.

Genesys side configuration is necessary to set up the hook between the GMS API calls and the Genesys routing framework. 'Enable-notification-mode' must be set to True to allow Cometd connections to GMS.  A service/endpoint must be created that corresponds to an endpoint definition in the Genesys Chat Server configuration.  That chat end point is a pointer to a Genesys routing strategy.


If a Cometd connection doesn't already exist, create one and perform the handshake to determine connection type.  Websocket is the preferred method, but if that fails - Cometd will fall back to a polling-type async connection.  The request to connect to Genesys is then sent across that Cometd(websocket) connection via the publish command.


        
        var _genesysChannel = '/service/chatV2/v2Test';

        function _metaHandshake(message) {
         console.log('cometd handshake msg: ' + JSON.stringify(message, null, 4));         
         if (message.successful === true) {
          _genesysReqChat();
         }
        }

        function _genesysReqChat() {
         var reqChat = {
           'operation' : 'requestChat',
        'nickname' : _firstName + _lastName
      };
         _cometd.batch(function() { 
       _genesysSubscription = _cometd.subscribe(_genesysChannel, _genesysReceive); 
       _cometd.publish(_genesysChannel, reqChat);
      });
        }
        
        function _genesysConnect() {
         console.log('connecting to genesys');
         if (!_connected) { 
          _cometd.configure({
           url: 'https://' + location.host + '/genesys/cometd',
           logLevel: 'debug'
          });
          _cometd.addListener('/meta/handshake', _metaHandshake);
          _cometd.addListener('/meta/connect', _metaConnect);
          _cometd.addListener('/meta/disconnect', _metaDisconnect);
          _cometd.handshake();
         }
         else {
          _genesysReqChat();
         }
        }

Step 4 Code Snippets

In the previous step, the web client subscribed to a Cometd channel corresponding to a Genesys chat end point.  When the message arrives that this client is 'joined', publish the existing chat transcript (between the user and Lex) to that Cometd channel.
    function _getTranscript(){
     var chat = _id('chat');
     var text;
     if (chat.hasChildNodes()) {
      text = '***Transcript Start***' + '\n';
      var nodes = chat.childNodes;
      for (var i=0; i < nodes.length; i++){
       text += nodes[i].textContent + '\n';
      }
      text += '***Transcript End***';
     }
     return text;
    }
        function _genesysReceive(res) {
         console.log('receiving genesys message: ' + JSON.stringify(res, null, 4));
      if (res && res.data && res.data.messages) {
       res.data.messages.forEach(function(message) {
        if (message.index > _genesysIndex) {
         _genesysIndex = message.index;
         switch (message.type) {
          case 'ParticipantJoined':
           var nickname = _firstName + _lastName;
           if (!_genesysSecureKey && message.from.nickname === nickname){
            _genesysSecureKey = res.data.secureKey;
            console.log('genesys secure key reset to: ' + _genesysSecureKey);
            var transcript = _getTranscript();
            if (transcript){
             _genesysSend(transcript, true);
            }
           }
           break;

Step 5 Screen Shots


Agent Desktop (Genesys Workspace)



Source: https://github.com/joeywhelan/lexgenesys

Copyright ©1993-2024 Joey E Whelan, All rights reserved.