Skip to Content
Technical Articles

Google Hangout Chatbot Integration with S/4HANA

Introduction

I have recently read a couple of blog posts from Sudip Ghosh, and I found them really inspiring. I would like to share my own variant of chatbot integration. I have tried to simplify as much as possible the approach and have considered some simplified assumptions.

I will describe the steps to build a simple chatbot for Google Hangout Chat to lookup for business partners in S/4HANA:

Here is a preview of the expected result:

Architecture

In case of on-premise, “standard” architecture to integrate with SAP Conversational AI is as follows (credits to this tutorial for the picture that I have modified):

  1. SAP Conversational AI -we will call it CAI– is available as a SaaS, somewhere in the outside cloud. From time to time it will need to “request” information/action to your backend. This is done through “webhooks”: SAP CAI will call REST services hosted in a “Fulfillment Server”
  2. The fulfillment server is where some parts of the business intelligence take place. Natural Language Processing (NLP) remains in SAP CAI, but whenever some information is needed from your backend S/4, this is where connexion happens. These functionalities must be exposed as REST services to the outside world, so you may add another layer with API Management to expose, monitor consumptions, manage authorizations, etc.
  3. Connectivity to your backend is enabled through Cloud Connector, acting as a kind of reverse proxy between your private network and SAP Cloud Platform.
  4. Your backend is reached by the fulfillment server, through Cloud Connector. I call it “backend” but it is usually the traditional pair {S/4 + Gateway}, on which OData services are exposed.
  5. Final users have access to backend applications in their company private network, and can discuss with chatbot using a chat app (Whatsapp, Hangout Chat, Messenger, etc.)

This is for the theory. If you go for this architecture, it will take a couple of days/weeks to implement. If you are just a hobbyist like me, this looks pretty complex to implement as it involves some infrastructure steps, registration steps, etc. If you are a mad hobbyist, you may have this infrastructure already set up, and it will not take much time to implement it.

I am one of these mad hobbyists, but I don’t want to deal with all these layers. Let’s see where I can simplify.

Simplification and assumptions

My goal is to use chatbots in Google Hangout Chat, which is the “professional” flavor of Google Hangout. The reason is my current company is using GSuite.

Not all chat apps are equal before chatbots, and chatbot builders do not offer integration to all chat apps in the market. For instance SAP CAI does not provide connectors to Whatsapp nor Google Hangout Chat. Should I write some custom connector using Hangout API? No.

Simplification#1: Let’s not be opinionated about chatbot builder: should we use SAP CAI at all? My answer is biased, as I want Hangout Chat, I want to build quickly a prototype: let’s substitute SAP CAI with DialogFlow, which offers integration with Hangout Chat1.

Simplification#2: We need a “server” to host services called by chatbot’s webhooks. Actually, this is not exact, we need REST services, not a server. I mean, wouldn’t it be faster just to write functions without the hassle of managing a server? DialogFlow offers that option with its inline editor: under the hood, it is provisioning Cloud Functions in Firebase2.

Simplification#3: We need to expose OData services in our backend. To make it simple, we can use any standard OData service delivered with S/4, like API_BUSINESS_PARTNER.

Simplification#4: Do we really need a tunnel through SCP with Cloud Connector? Cloud Connector enables connectivity from our private network to SCP, and from SCP to our private network. Then, we will need something else to relay from SCP to outside world. That something can be API Management or any home-made reverse-proxy in SCP.

But if we do not need any SCP service, we do not really need to pass through SCP. Backend services can be exposed by any other secured method than Cloud connector + SCP. Let’s simplify the requirement to this: we must make URL https://<your_server>/sap/opu/odata/sap/* reachable from internet, hence from our chatbot.

Prerequisites:

  1. Only GSuite users have option to connect DialogFlow with Hangout Chat
  2. Firebase “Spark plan” is free, but you can only make oubound calls to Google Services. Hence, to reach your OData services in your backend, you will need to subscribe to a paid plan

Step 1: Ensure your OData services are exposed (by any secured method)

I assume you know how to expose OData services in SAP Gateway, and that you have made the endpoint reachable.

In our scenario, we want to be able to query our bot for business partners. The aim is not to implement a complex search function, so let’s simplify the query to “Find partners whose names contain <search term>”. Technically, we will implement it as a simple OData request filter with operator “substringof”, that is, a GET request:

https://<server>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner?sap-client=100&$format=json&$filter=substringof('<search term>',BusinessPartnerName)

Make sure you know how to call OData, for example with a tool like Postman, and that you actually get results successfully from your request.

Sample OData result:

{
    "d": {
        "results": [
            {
                "__metadata": {
                    "id": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')",
                    "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')",
                    "type": "API_BUSINESS_PARTNER.A_BusinessPartnerType"
                },
                "BusinessPartner": "17100010",
                "Customer": "17100010",
                "Supplier": "",
                "AcademicTitle": "",
                "AuthorizationGroup": "",
                "BusinessPartnerCategory": "2",
                "BusinessPartnerFullName": "Domestic US Customer 10",
                "BusinessPartnerGrouping": "BP03",
                "BusinessPartnerName": "Domestic US Customer 10",
                "BusinessPartnerUUID": "000c299f-5430-1ee8-b8cf-122f1e1b2de5",
                "CorrespondenceLanguage": "",
                "CreatedByUser": "xxx",
                "CreationDate": "/Date(1541548800000)/",
                "CreationTime": "PT10H34M04S",
                "FirstName": "",
                "FormOfAddress": "0003",
                "Industry": "",
                "InternationalLocationNumber1": "0",
                "InternationalLocationNumber2": "0",
                "IsFemale": false,
                "IsMale": false,
                "IsNaturalPerson": "",
                "IsSexUnknown": false,
                "Language": "",
                "LastChangeDate": null,
                "LastChangeTime": "PT00H00M00S",
                "LastChangedByUser": "",
                "LastName": "",
                "LegalForm": "01",
                "OrganizationBPName1": "Domestic US Customer 10",
                "OrganizationBPName2": "",
                "OrganizationBPName3": "",
                "OrganizationBPName4": "",
                "OrganizationFoundationDate": null,
                "OrganizationLiquidationDate": null,
                "SearchTerm1": "CUST10",
                "AdditionalLastName": "",
                "BirthDate": null,
                "BusinessPartnerIsBlocked": false,
                "BusinessPartnerType": "",
                "ETag": "XYZ20181107103404",
                "GroupBusinessPartnerName1": "",
                "GroupBusinessPartnerName2": "",
                "IndependentAddressID": "",
                "InternationalLocationNumber3": "0",
                "MiddleName": "",
                "NameCountry": "",
                "NameFormat": "",
                "PersonFullName": "",
                "PersonNumber": "",
                "IsMarkedForArchiving": false,
                "BusinessPartnerIDByExtSystem": "",
                "to_BuPaIdentification": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_BuPaIdentification"
                    }
                },
                "to_BusinessPartnerAddress": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_BusinessPartnerAddress"
                    }
                },
                "to_BusinessPartnerBank": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_BusinessPartnerBank"
                    }
                },
                "to_BusinessPartnerContact": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_BusinessPartnerContact"
                    }
                },
                "to_BusinessPartnerRole": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_BusinessPartnerRole"
                    }
                },
                "to_BusinessPartnerTax": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_BusinessPartnerTax"
                    }
                },
                "to_Customer": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_Customer"
                    }
                },
                "to_Supplier": {
                    "__deferred": {
                        "uri": "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner('17100010')/to_Supplier"
                    }
                }
            }
        ]
    }
}

Step 2: Build a chatbot with DialogFlow

DialogFlow is a Google platform for Natural Language Processing (NLP) -say the equivalent of SAP Conversational AI in our landscape-. The official documentation can be found here.

Let’s start by creating a new agent:

Our agent is empty. As we would do in SAP CAI, we need to create at least one intent to match user’s intention to query for business partners from our backend S/4.

Let’s create an intent “Search partner”:

Texts which are highlighted in yellow are actually meant to be parameters for an action. In our scenario, these texts should be detected by the NLP as the query string for Business Partner search.

They are then used in the definition of an action to be triggered: search_partner

Action name is search_partner and takes as unique input parameter named partner of entity (type) @sys.any:partner (any string), which is a list (concatenation of many key words, like [Domestic, US, Supplier, 90] for query string “Domestic US Supplier 90”).

Now, at the bottom of the screen, we will trigger a webhook call for this intent:

We could have created a custom entity for the intent, but we don’t really need it for our small demo.

Fulfillment

Fulfillment section is where we define our webhook. In DialogFlow, we can use only one webhook URL, the same for all intents and actions. Actually, this is up to the “Fulfillment server” to route to the correct processing based on the input intent/action.

We have 2 options here,

  1. Classic way: have our own server setup somewhere and input its URL
  2. Simple way: input our code directly inline, and it will automatically be created as a managed Firebase Cloud Function

I have choosen a mix of the 2 options: use webhook with my own Firebase Cloud Functions. I like the idea of inline editor, but I still prefer to manage and write code in my preferred IDE, and have my source code in github.

Enable integration with Google Hangout Chat

We can now enable integration with Google Hangout Chat as simply as clicking:

Our chatbot is almost ready, and we can even try it in the right panel directly.

Step3: Implement chatbot business logic in a Firebase Cloud Function

We use Typescript to implement the webhook. To initialize a project with Firebase is pretty easy, just follow the steps described here.

To write typescript code to call OData service API_BUSINESS_PARTNER as we did with Postman is straightforward. We will use axios for our HTTP requests, but any other library should work the same.

We declare a webhook function (the one called by DialogFlow chatbot)

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
    const agent = new WebhookClient({ request, response });
    const intentMap = new Map();
    intentMap.set('Search partner', searchPartner);

    agent.handleRequest(intentMap);
  });

agent is a helper object which will take care of formatting response when we request chatbot to add an answer with agent.add

agent.handleRequest( …) sets up a middleware router. In our case, when intent is “Search partner”, the request is routed to function searchPartner

request contains the HTTP request body as sent from chatbot, for example when requesting with sentence “search partner domestic US”, request payload is:

{
  "responseId": "dba564d6-67da-4b56-8b5e-28d7430c4c1a-b81332aa",
  "queryResult": {
    "queryText": "search partner domestic us",
    "action": "search_partner",
    "parameters": {
      "partner": [
        "domestic us"
      ]
    },
    "allRequiredParamsPresent": true,
    "fulfillmentMessages": [
      {
        "text": {
          "text": [
            ""
          ]
        }
      }
    ],
    "intent": {
      "name": "projects/<your-bot>/agent/intents/63a6b488-11bc-44ea-86b6-b959a06bcfd3",
      "displayName": "Search partner"
    },
    "intentDetectionConfidence": 0.7615776,
    "languageCode": "en"
  },
  "originalDetectIntentRequest": {
    "payload": {}
  },
  "session": "projects/<your-bot>/agent/sessions/24f4ef36-5ed4-ecb7-6d58-bc04357c8aa2"
}

Actually we don’t have to use request directly, but agent instead which now holds these values.

My function searchPartner is as follows

function searchPartner(agent:any) {

    // OData endpoint
    const url = "https://<odata-endpoint>/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartner";

    // Build filter parameter for OData request
    let stringName = "";
    agent.parameters.partner.forEach((element:string) => {
        stringName += element + " ";
    });
    const filter = "substringof('" + stringName.trim() + "', BusinessPartnerName )"

    // Axios request options
    const options = {
        headers: {
            "Authorization": "Basic xxxxxxxxxxxxx"
        },
        params: {
            '$filter': filter,
            'sap-client': '100',
            '$format': 'json'
        }
    };

    // Function must return
    return axios.get(url, options)
        .then((axios_resp: any) => {

            // Actual results are under path response.data.d.results, which is an array
            if(axios_resp.data && axios_resp.data.d.results.length) {
                // If results actually contains records
                if(axios_resp.data.d.results.length > 0) {

                    // Chatbot will first provide records count
                    agent.add(`I have found ${axios_resp.data.d.results.length} matching partners:`)

                    // Then each partner is displayed as a card
                    for(let i=0;i<axios_resp.data.d.results.length;i++) {
                        console.log(axios_resp.data.d.results[i].BusinessPartner);

                        agent.add(new Card({
                                title: axios_resp.data.d.results[i].BusinessPartner,
                                text: axios_resp.data.d.results[i].BusinessPartnerName,
                                buttonText: 'Details',
                                buttonUrl: 'https://assistant.google.com/'
                            })
                        );
                    }

                }
                else {
                    // No record found in OData response
                    agent.add("I cannot find any matching partner, sorry");
                }
            }
            //response.json(axios_resp.data);
        })
        .catch((error: any) => {
            // handle error
            agent.add(error);
        });
}

All it does is to query OData `business partner to find partners with names containing <keyword>. Then responses will be displayed as cards.

We are done, let’s deploy it to firebase (command firebase deploy).

Go and test it!

There are many ways to test our chatbot:

  • Directly in DialogFlow builder, in the right panel
  • In Hangout Chat Web
  • In Hangout Chat Mobile

As proposed by many bot builders, you can plug with many other apps (Messenger, Slack, Skype, Twilio, etc.) with near zero effort.

 

To integrate S/4HANA with a chatbot is not very difficult, it is an interface development like any other. But it opens the door to new opportunities to bridge end users to their backend in an even more user friendly way.

 

Enjoy!

2 Comments
You must be Logged on to comment or reply to a post.