Using Content and Experience Cloud with your Oracle Intelligent Bots chatbot

In this blog I will show how you can use the content items from Content and Experience Cloud in your Oracle Intelligent Bots chatbot.

Why

By using Content and Experience Cloud you can have a dedicated team working on the content with the appropriate business tools to manage the content lifecycle, next to the group that manages the bot.

Overview of the integration

The sample explained in this blog is a variant of a FAQ service. The user of the blog service can ask the chatbot various FAQ type questions and the bot answers with the FAQ content. Questions by the user can be in the form of “I would like to know something more on bots” or “Can you tell me something on content”.

The FAQ content is managed in Content and Experience Cloud and the bot is handling the Intent and Entity resolution, as well as the conversation dialog. A custom component that queries Content and Experience Cloud for FAQ articles based on a keyword is deployed in Intelligent Bots.

The high level architecture is visualised in this image.

 

2017 CEC and chatbot how is it build

Breakdown of the parts

In this blog I will not go into details on how to create a content type and content items on Content and Experience Cloud, or how to configure Intelligent Bots and deploy the custom component. This is described in the public documentation. I will describe what you need to implement and how I have implemented this. This blog expects that you understand most of the concepts in Content and Experience Cloud, like Content-Type and Content-Item, and also Intent, Entity, DialogFlow and Custom Component in Oracle Intelligent Bots.

The channel used in this sample is the Test Channel. The same code should work with Messenger if you configure Facebook Messenger as a channel.

ContentType

On Content and Experience Cloud a Content Type is defined. It is named Knowledge and has two fields: body and keyword. Body holds the actual Knowledge content, and keyword is an array of keywords. A business user is adding content and publishing that content.

content type

Intent

On Oracle Intelligent Bots we have more parts.

We have one Intent called Knowledge with the following utterances:

utterances

Entity

The Intent is linked to the keyword Entity. The entity is a regex entity, that extracts the keyword from the typed in phrase by the chatbot user.

Image 09-11-17 at 08.37

DialogFlow

The dialog flow is simple, as we have one intent and one entity.

#metadata: information about the flow
#  platformVersion: the version of the bots platform that this flow was written to work with 
metadata:
  platformVersion: 1.0
main: true
name: MrKnowItAll
#context: Define the variables which will used throughout the dialog flow here.
context:
  variables:
#The syntax for defining the variables is variablename: "variableType".
# The "variableType" can be defined as a primitive type ("int", "string", "boolean"), "list", or an entity name. A variable can also hold the results returned by the Intent Engine. For these variables, the "variableType" must be "nlpresult" (for example, iResult: "nlpresult").
    iResult: "nlpresult"
    keyword: "string"
#states is where you can define the various states within your flow.
# The syntax for defining a state is
# statename:
#   component:Specify the component you want to use. This can be either a Built-In or custom component.
#   properties:
#     property1: "value" (These are the properties to the specified component
#   transitions: You can specify one of the following four
#     next: Specify the state you want to execute next after this state. By default it will execute the state specified after this
#     error: Specify the state you want to execute in case the component encounters any error in execution.
#     actions: You can handle various actions returned by the components here the syntax is actionName: "statename"
#        action1: state1
#     return: "done" You can exit the flow using the return statement

states:
  getInput:
    component: "System.Intent"
    properties:
      variable: "iResult"
      confidenceThreshold: 0.4
    transitions:
      actions:
        Knowledge: "startSearch"
        unresolvedIntent: "unresolved"
  startSearch:
    component: "System.SetVariable"
    properties:
      variable: "keyword"
      value: "${iResult.value.entityMatches['keyword'][0]}"
    transitions: {}
  start:
    component: "KnowItAll"
    properties:
      nlpVariable: "${keyword.value}"
    transitions:
      return: "done"
  unresolved:
    component: "System.Output"
    properties:
      text: "No good, dunno"
    transitions:
      return: "done"

Custom Component

The DialogFlow is connecting to a custom component called KnowItAll. This component is implemented in JavaScript and runs on nodejs. It gets the keyword and returns with the body from the Content and Experience Knowledge item.

The custom component is broken up in 3 subparts: the REST end point, the business logic that deals with the keyword extraction and  sending the response, as wel as interacting with the third subpart, the REST client.

The REST endpoint is straight forward and is the same for all custom component endpoints: it calls the ‘shell’ SDK module, for the custom component names (the / route) and invokes the custom component by name (the POST /:componentName route). The snippet below shows the important code lines of the endpoint implementation.

    var shell = require('./shell')(config);

    var router = express.Router();
    router.use(auth.connect(basic));

    // Return component metadata
    router.route('/').get(function(req, res) {
        res.set('Content-Type', 'application/json')
            .status(200)
            .json(shell.getAllComponentMetadata());
    });

    // Invoke component by name
    router.route('/:componentName').post(function(req, res) {
        const componentName = req.params.componentName;
        shell.invokeComponentByName(req.params.componentName, req.body, {}, function(err, data) {
            if (!err) {
                res.status(200).json(data);
            } else {
                switch (err.name) {
                    case 'unknownComponent':
                        res.status(404).send(err.message);
                        break;
                    case 'badRequest':
                        res.status(400).json(err.message);
                        break;
                    default:
                        res.status(500).json(err.message);
                        break;
                }
            }
        });
    });

The shell module looks up CustomComponent implementations via a registry file:

module.exports =
    {
        components: {
            'KnowItAll': require('./knowitall/knowitall')
        }
    };

This registry module provides access to the CustomComponent implementation

const log4js = require('log4js');
const logger = log4js.getLogger();
const CecService = require('./CecService');

module.exports = {

    metadata: () => ({
        "name": "KnowItAll",
        "properties": {
            "nlpVariable": {
                "type": "string",
                "required": true
            }
        },
        "supportedActions": []
    }),

    invoke: (conversation, done) => {
        var nlpVariable = conversation.properties().nlpVariable;
        var nlpResult = conversation.variable(nlpVariable);
        if(nlpVariable && nlpVariable.length > 0 ){
               let keyword = nlpVariable.replace(/^(on|about) /gi,'')

               CecService.gimmeSomeKnowledge(keyword).then(items => {
                    if (items && items.length > 0) {
                        conversation.reply(items[0].body);
                    } else {
                        conversation.reply('MrKnowItAll doesn\'t know about \'' + keyword + '\'.')
                    }
                    conversation.transition();
                    done();
                }).catch(err => {
                    conversation.reply('An error occured. Err ' + err);
                    conversation.transition();
                    done();
                });
        } else {
            conversation.reply('I\'m not sure what your question was, can you ask it a little differently?');
            conversation.transition();
            done();
        }
    }
};

This is the heart of the implementation. Here if the keyword extracted from the provided conversation input, that is passed to the CecService module to get some Knowledge ( CecService.gimmeSomeKnowledge(keyword)), the return value is evaluated and the reply is sent to the bot user.

Service module to construct query

The CeCService module is an intermediate layer between the customer component and the REST API client. The implementation of CeCservice is below. The fetchContent method does two things: structure the query that is sent to Content and Experience Cloud to search the content items and transform the response into a structure that is easier to work with in the CustomComponent.

const headless = require('./headless');
const log4js = require('log4js');
const logger = log4js.getLogger();

const contentType = 'Knowledge';

const fetchContent = (tag) =>
    headless.contentItems({
        'field:type:equals': contentType,
        'field:knowledge_keyword:contains': tag.toLowerCase(),
        'fields': 'name,description,knowledge_keyword,knowledge_body',
        'orderBy': 'name'
    }).then(values => {
        logger.debug(JSON.stringify(values, null, 2));
        return values.map(item => ({
            id: item.id,
            description: item.description,
            name: item.name,
            body: item.data[contentType.toLowerCase() + '_body'],
            keywords: item.data[contentType.toLowerCase() + '_keyword']
        }));
    });

module.exports = {
    gimmeSomeKnowledge: fetchContent
};

 

This is query that will be send to the REST API. We want the API to return the fields name, description, knowledge_keyword, knowledge_body. The knowledge_keyword field is search for the keyword. Since the field knowledge_keyword is a multiple value field (an array), we need to use to use the contains operator.

{
        'field:type:equals': contentType,
        'field:knowledge_keyword:contains': tag.toLowerCase(),
        'fields': 'name,description,knowledge_keyword,knowledge_body',
        'orderBy': 'name'
 }

The response from the API is transformed. The returned items are flattened with the map function

{
            id: item.id,
            description: item.description,
            name: item.name,
            body: item.data[contentType.toLowerCase() + '_body'],
            keywords: item.data[contentType.toLowerCase() + '_keyword']
}

REST call to the Delivery API

The CeCService delegates the REST call to the headless module:

const base = process.env.CEC_URL;
const rp = require('request-promise-native');
const _ = require('lodash');
const access_token = process.env.CEC_TOKEN;
const log4js = require('log4js');
const logger = log4js.getLogger();

const transform = function (body, response, resolveWithFullResponse) {
    return {
        data: body,
        headers: response.headers,
        statusCode: response.statusCode
    };
};

function get(url, args) {
    let options = {
        uri: url,
        json: true,
        qs: {
            'access-token': access_token
        },
        transform: transform
    };
    _.merge(options, args);
    logger.debug(options, args);
    return rp(options);
}


module.exports = {

    contentItems: function (qs) {
        return get(base + '/content/published/api/v1/items', {
            qs: qs
        }).then(response => {
            logger.debug(JSON.stringify(response, null, 2));
            if (!response.data || !response.data.items) {
                return [];
            }

            return response.data.items;
        });
    }

};

The headless module is responsible for making the HTTP call via he request-promise module. It reads the two environment variables CEC_URL and CEC_TOKEN for respectively the hostname for Content and Experience Cloud and the access-token for the publish destination. The remainder of the code is mostly on setting up the calls with the request-promise module.

The completes the interaction with Content and Experience Cloud. If we return to the custom component, you can see that the body field of the first content item is sent in reply, and that the conversation is transitioned to the next state in the DialogFlow. After that the done() callback is invoked to mark the completion of the custom component call.

 CecService.gimmeSomeKnowledge(keyword).then(items => {
   if (items && items.length > 0) {
       conversation.reply(items[0].body);
   } else {
       conversation.reply('MrKnowItAll doesn\'t know about \'' + keyword + '\'.')
   }
   conversation.transition();
   done();
}).catch(err => {
   conversation.reply('An error occured. Err ' + err);
   conversation.transition();
   done();
});

Re-using this sample

The code and configuration snippets above give you an overview of how you can integrate Content and Experience Cloud and Intelligent Bots.

If you want to use this sample as a starting point for your own integration, there are a couple of modules you can use without changing and some that you probably will need to change. The CeCService handles the binding of the specific Content-Type to the REST calls. If you use other content types wit different fields and names, you will need to change this module.

If you add additional channels, you will need to change your conversation.reply() invocation. For instance with the Facebook channel you probably want to write a Facebook specify JSON response, instead of the plain text that we have implemented here. Also the entities might be different, so you may need to cater for that in your custom component.

 

As you can see, it just takes a couple of lines of code to retrieve content from Content and Experience Cloud and use that in Intelligent Bots.

 

Add Your Comment