Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member
Chatbots is one of the hot "topics de jour" - everyone seems to want one, or work on one, or wish they had one. In this blog, I'll show how we can create a simple chatbot interacting with SAP Netweaver Gateway OData services. I'll mention a couple of chatbot platforms, and show an example created with one of these, but this is not an approval or recommendation of specific tools or platforms to the detriment of others. My aim is just to show the technical orchestration of OData services from SAP (actually, we'll touch on both XSJS services on SAP-CP and classical ABAP-driven OData), and point out a few of the "traps" you might stumble into along the way.

Prerequisites: a rudimentary knowledge of chatbots, Javascript and XSJS, some knowledge of http requests, ABAP Gateway services, OData, and... hey, let's just get started, shall we? I'll try to explain stuff along the way. And if I don't, or only do it in a half-baked manner, feel free to ask!

Disclaimer


This is not a complete, fully working, super-adapted mega-cool chatbot capable of answering any question you may want to throw at it - but more an example of how to stitch some chatbot-building components together. Hopefully, you'll be able to walk away with enough learnings to do something similar yourself.

Right. Let's get started.

Scenario: you walk into the office one fine morning, fire up your PC, and start the process of checking on your purchase orders. After having inquired about the fifth one, and halfway through your second coffee, you stop for a moment. A thought has hit your caffeine-frenzied brain. "Hum. Wouldn't it be great if I could just ask someone instead of doing this tedious task myself".

Someone - or something. This is a typical scenario for a conversational app - a chatbot.

We'll create a chatbot that can retrieve information about purchase orders. We will ask the chatbot questions such as "what are my current purchase orders", or "what is the estimated delivery date of purchase order number X". (Well, actually, to keep it simple, we'll stick to the first question for now; I have a distinct feeling this blog will be long enough as it is - but I'll provide you some clues as to the second question as well).

Here are the components we need:

  1. An OData service

  2. A chatbot framework

  3. SAP Cloud Platform

  4. Some nice developer tools - like Google Postman


Step 1: Creating an OData service


We'll start by creating an OData service for - purchase orders. Of course, we can create a service for anything we want - if your use case is different, fine! Also, if you're already in possession of a service, fine. Use it!

ABAP OData services are created using the Gateway Service Builder, transaction SEGW (or "Segway", as I lovingly call it). Describing how to do this is a bit outside the scope of this blog, so I'll only touch lightly on it. In our case, we'll start by looking at the Business Object Repository, or transaction BAPI. Here, we can search for any business object in our system, and check for existing methods for information retrieval.



We've found our desired object - PurchaseOrder - and selected a suitable method.

(At this point, if you're a real nitpicker, you'll notice that the method - GetDetail - has been replaced by GetDetail1 as of NW 7.02. It's not relevant for our use case, but clearly shows a lack of attention to detail on behalf of the author...)

Anyway, testing our function shows some results (I've used an existing PO number which i conveniently retrieved from table EKKO as input parameter).



Now, all we have to do is fire up our Segway, uhm, SEGW, and generate a new service based on the related function module, BAPI_PO_GETDETAIL.



We use the SEGW wizard to create the necessary Entity Type from the function module:



Then, we select which of the function return parameters we need in our service:



This is clearly overkill, by the way - but, hey, I was trying to create a very generic PO service which will be capable of retrieving, well, everything.

I'll skip the rest of the entity set creation process; suffice to say that you need to remember setting key properties for all the "nodes" in this structure, and also make sure you define relevant fields as "nullable" and search/sortable. The first thing is particularly important, as you will otherwise end up with error messages if some fields are returned empty when calling the service.

But as a seasoned Gateway developer you probably already knew that, right?

In the end, we have our model and data provider classes:



The remaining work here is to redefine the GET_ENTITYSET (and GET_ENTITY) methods of the data provider class - again, this should be well known to Gateway service developers. Honestly, you didn't come here for that, you came here for the chatbot magic!

Finally, we register the new service on our Gateway box - which may well be a separate system.

I'll just show a final Gateway screenshot - that of the test of our new service using the Gateway client:



 

Some more details:



Great - we have our service up and running. Now, on to the fun stuff - the chatbot.


Step 2: creating the chatbot


For this example, we'll be using one of the more popular chatbot frameworks out there: API.ai. There are pros and cons with this chatbot framework, as with most of them - we'll be encountering one of the minor issues in a little while. For now, let's focus on building the chatbot.

API.ai is running in our browser - no need to install anything locally. Just go to API.ai and behold the beauty of the chatbot editor:



This might be a bit to take in for you if you're new to chatbots. Let's stop, take a deep breath, and explain.

Chatbots usually work with "intents" and "entities". The Intents are the things our users want to do, like "order a plane ticket", or "find the nearest restaurant". The Entities are like the parameters - or attributes - of these intents. An Entity for "order a plane ticket" might be "date", or "destination". Usually, the chatbot will try to retrieve all the entities related to an intent, before committing an action, like ordering the plane ticket for you. If built properly, it will ask questions until it has all the information it needs - or get stuck along the way if it fails to understand the user.

For our chatbot, we have defined two intents: GET_PO_INFO and GET_PO_LIST. (In addition, as you can see, we have added a bunch of standard intents from the API.ai libraries - these are the ones in lowercase. This allows our chatbot to cover off-topics, such as when people start talking about other subjects like "you're really funny", or "you must be the most boring chatbot ever".

In API.ai, the intent looks like this:



Here, I have typed a few variations of the way a user could be expressing his/her desire to get a list of purchase orders. For the sake of this example, I've restricted myself to a few utterings - usually, you'll find your users are very creative in their attempts to phrase an intent...

Further down the screen, we can see the remaining attributes of this intent:



There's not much here, really. As mentioned, API.ai gives you the option to add entities to your intent. If this was, for instance, a FIND_ME_A_RESTAURANT intent, we could have added the entities "MEAL_TYPE" and "LOCATION", and set these as "required" above. This would have forced the chatbot to ask questions like "what type of food do you want", and "what is your preferred location". You can make your conversation as complex as you want - but for our purpose, we will not need any entities. We only want to get some purchase orders!

(Note: API.ai as a framework is, as I think I've stated already, quite easy to learn. Please forgive me for not providing an API.ai 101 course here - you should be able to pick up the basics quite easily).

What we want to focus on is that little blue checkbox in the above image: the one that says "Use webhook".

This is where it gets cool!


Now, we have to find a way for our chatbot to make a request to SAP Gateway.

This is done in API.ai using webhooks. A webhook is basically a web service - you call it with some parameters, and get a reply back. Our Gateway service will serve as our webhook. We'll call it with a specific user ID, and get a list of purchase orders back. What could possibly go wrong?

First things first. Notice the word "Fulfillment" just above the blue checkbox? This is API.ai's term for "something that comes back as a response from a webhook", more or less. The Fulfillment tab in our chatbot editor is where we define the actual web service call and handle the result. Let's click on "Fulfillment" on the left menu bar:



This brings us to the Fulfillment screen (logically enough):



Here, we only need to insert the URL for our Gateway service!

 

Step 3: where we get stuck


Ha! You really said "only need to..." ??

Think again.

I spoke about pros and cons, right? This chatbot builder is a charm to work with for setting up the chatbot conversation, but when it comes to using web services... well, let's say, it's a bit "picky".

API.ai web calls must adhere by very strict rules. From the documentation:



Right. This is not exactly what our Purchase Order service returns, in terms of format.

What's worse: it's probably not going to be very easy to cajole our service into returning anything remotely adhering to this format.

Nor is it desirable, really. You'd want your OData services to return data in adherence to the standard format provided by the protocol itself, not tweak it into a chatbot-friendly native format only suitable for one chatbot framework.

Time for reflection.

Here, other frameworks, like Kore.ai, has the flexibility to just execute a service call, then handle the response in any way you want, say, programmatically.

 

Step 4: where we do some serious thinking


Well, we've come a fair way. We cannot just give up, no? Let's have another coffee, take a deep breath again, and think.

We can't format the response internally in API.ai, Nor can we (easily) do it on the Gateway side (and, as mentioned, we shouldn't, really, because that means creating a separate set of services for chatbots in addition to the services for regular consumption by, say, UI5).

What we can do, however, is re-format the service reply somewhere else.

All we have to do is throw SAP Cloud Platform into the mix, and have it do the job for us.

Building an XSJS service on SAP-CP allows us to do the service call to Gateway from SAP-CP, then call the XSJS service from API.ai. The XSJS service will take the "standard" OData response from Gateway, and re-format it for consumption in API.ai.

What a brilliant and somewhat complex idea.

Again, as mentioned, some other chatbot frameworks would allow us to do this internally. But API.ai is easy to use, easy to deploy, and one of the "big ones" out there, so let's stick with it for now.


Step 5: where we try to get a little less stuck


Let's quickly create a middle man in SAP-CP. A negotiator of sorts. A translator. A nice piece of logic that receives calls from our chatbot, passes them on to Gateway, receives a reply, and re-formats it to pass it back to API.ai.

We need the following ingredients:

  1. An xshttpdest file describing our Gateway URL

  2. An XSJS service with some Javascript logic doing the "negotiating" part

  3. The ordinary .xsaccess and .xsapp files needed for XS services

  4. Some work in the XS Admin client, in order to set the authentication parameters


We start by setting up a service called GetPOList.xsjs and a gateway.xshttpdest file in a suitable package on SAP_CP:



The contents of the xshttpdest file is basically a reflection of the connection parameters already present in the "destination" configuration on SAP-CP for our Gateway system:



The reason why we need to create this file is because XSJS services cannot work with existing destinations defined on SAP-CP. We need to re-define the destination manually in each XSJS project - inside the same folder as our service files.as can be seen above. Then, we refer to the xshttpdest file in our XSJS code, when creating the connection.

On to our XSJS service. Here is the code (I've slightly altered the connection parameters):
try {

switch ($.request.method) {
case $.net.http.GET:

$.response.setBody(JSON.stringify("GET OK - but that is not doing anything useful here"));
break;

case $.net.http.POST:
case $.net.http.PUT:

//Reading the destination properties
var destination = $.net.http.readDestination("InsertYourPackageNameHere.PurchaseOrderChatbot.services", "gateway");
//Creating HTTP Client
var client = new $.net.http.Client();
//Creating Request
var request = new $.web.WebRequest($.net.http.GET, "/PURCHASE_ORDERSet?$format=json");
request.headers.set("SAP-Connectivity-SCC-Location_ID", "DEVLANDSCAPE");
request.contentType = "application/json";
client.request(request, destination);


var gwResponse = client.getResponse().body.asString();
var JSONObj = JSON.parse(gwResponse);
var botResponse;

if (JSONObj.d.results.length > 0) {
botResponse = "Your latest Purchase orders are: ";

for (var i = 0; i < JSONObj.d.results.length; i++) {
botResponse += " ";
botResponse += JSONObj.d.results[i].Poheader.PoNumber;
}
} else {
botResponse = "You do not seem to have any active Purchase Orders!";
}

$.response.status = $.net.http.OK;
$.response.contentType = "application/json";
$.response.setBody(JSON.stringify({
"speech": botResponse,
"displayText": botResponse
}));

break;

default:
$.response.status = $.net.http.METHOD_NOT_ALLOWED;
$.response.setBody("Wrong request method");

break;
}

} catch (e) {
$.response.setBody("Failed to execute: " + e.toString());
}

What we basically do here, apart from the usual check on request method (GET vs POST/PUT), is simply do a call to our Gateway-based service. If it succeeds, we parse the results - extracting the PO numbers, and build a response which is sent back to the API.ai chatbot, adhering to the proper format.

Now, all we have to do is call the XSJS service from our API.ai chatbot.

Ah - one more thing: We need to set the authentication level for our service using the XS Admin tool. Invoke the tool, navigate to the package where the xshttpdest file is, select the file, and set "Public" for now - you can add authentication to your heart's content later:



That's it on the SAP_CP side!

OK, at this stage in my exploratory journey, I of course tested the XSJS service properly, but I'll spare you the tedious results of that particular part. Suffice to say the results look more or less like this:



Step 6: Finally, back in chatbot-land...


Now, all we have to do is add the URL to our XSJS "go-between" service in our API.ai webhook:


Step 7: The moment of (ugly) truth


Ok. We're finally there. We've created a Gateway service, a chatbot, a re-formatting XSJS service on SAP-CP, and stitched it all together.

Will it work?

Let's try. API.ai allows for easy testing of the chatbot, out-of-the-box (or, more specifically, inside-the-browser):



Yes!! We did it!

It's alive!

Sort of.

Is it nice? Well, not really. We could have spent some more time on the formatting of the reply from the XSJS service. And we conveniently "forgot" to include the user name in the service call - all we do here, really, is retrieve the 10 first PO's from the data base.Not to mention the fact that we haven't created the intent for retrieving a single PO, nor the Gateway service method to handle such requests.

But, all in good time.

What we have achieved, is creating a simple chatbot that interacts with SAP ERP, using Gateway services. We have also explored using SAP-CP as a "reformatter", since the chatbot tool lacked the necessary functionality to handle "raw" service responses.

It's a start.

 

Wait! Wait! He said he'd show us something about the second intent as well!


Yeah, right, I did. I was actually going to go to bed, but you just reminded me. OK, here we go. I'll show you the way, then it's up to you to finish the job. OK?

The GET_PO_INFO intent basically retrieves information about one specific PO. Here it is, as implemented in API.ai:



 

API.ai automatically resolves the word “first” into a system entity, @sys.ordinal. This is cool!

Let’s try straight away if this works in a test dialog:



Nice! We can even click the JSON button and look at the conversation context:



This is brilliant. It means we have an easy way to deduce which purchase order our user wants more info about.

Note: the "context" is chatbot slang for "everything I've deduced from whatever the user has typed into me from the start of the conversation until now", like entity values, intents, actions and so on. It also holds the result of any web service calls. Here, we see that the "ordinal" is deduced as "2", which kind of gives us an inkling as to which PO the user wants more info about.

Or, to re-phrase it: if our intent is "GET_PO_INFO", and the ordinal is "2", we will need to call the service for specific PO's with the second PO number from the list we already retrieved.

And that, ladies and gentlemen, I leave to you. It should be a walk in the park, really!

 

Thanks for sticking out this far - hope you had a few ideas. Let me know if you run into issues - I'm happy to help!

 

PS: feel free to comment - particularly if you have found cooler or easier solutions!

 

 
31 Comments
Labels in this area