Skip to Content

The following steps will explain how to quickly get an idea off the ground that requires integrating SAP Leonardo Machine Learning Foundation services found on SAP API Business Hub into an SAP Cloud Platform side-by-side extension using the SAP S/4HANA Cloud SDK.

Note: This post is part of a series. For a complete overview visit the SAP S/4HANA Cloud SDK Overview.

Goal of this Blog Post

This blog post covers the following steps:

  1. Introduce the concept of machine learning services and point out its significance.
  2. Quickly get running with the SAP API Business Hub’s catalog.
  3. Build a microservice integrating a SAP Leonardo Machine Learning service from SAP API Business Hub.

Machine learning services

Machine learning is currently on everybody’s mind, figuring as a driver for efficiency improvements and an enabler of wholly new solutions. Certainly the quality of machine learning services has increased tremendously in the past 10 years. Advances in hardware and data management have led to multiple breakthroughs, fueling a virtuous circle of software improvements and, in turn, further development of hardware and data management. All this was carried by ever increasing interest by the business community. Today we have reached a quality level that make machine learning applications usable for automation purposes: When you reach 75% accuracy on image classification this is a nice trick and good research result, but you still need humans to check the result of the computer and correct every fourth result. When you reach 97% you go beyond human capabilities and can actually automate, and only have an exception in every 30th result. You still need exception handling, but mind the complexity of the task and that humans are even more error-prone doing it.

This quality improvement applies particularly to lots of so called one-second-tasks, like reading text, classifying pictures, seeing similarity, translation and so forth. Some of these tasks are sufficiently generic that they can be offered as ready-to-use services, with improvements being taken care of by the supplier – in this case SAP Leonardo Machine Learning functional services on the SAP Cloud Platform leveraging SAP Leonardo Machine Learning Foundation.

Service catalog

For the consumption of services it is essential to have a good documentation for all stages from prototyping to production. The SAP Business Hub provides the catalog of services offered by SAP products, complete with their API documentation and, for many, a simple prototyping facilitation via example code generation. For SAP Leonardo Machine Learning services the code generation feature is available and will be used in this blog.

You can find the list of services, an initial description, and when clicking the name of the service also the details of the service API, such as message format.

The scenario

Your company is growing, and to support its growth plans to open a subsidiary in a foreign, non-English-speaking country. Therefore, it would greatly help to provide automatic translation capabilities for some user-generated content in your side-by-side extension application. You quickly found the translation service on the SAP API Business Hub, now your goal is to ‘quickly try building a solution’ – to prototype or build a proof-of-concept. For this you only need your SAP ID, and internet access – nothing else!

Your plan is to create a microservice that handles access to the translation API, caching, and some internal monitoring of the translation activity. Therefore, we’ll create a simple service offering the translation functionality with an internal API to the applications that you’ll need to integrate translation services into.

The starting point is the generation of the hello-world application template with maven (see Step 3 https://blogs.sap.com/2017/05/19/step-3-with-sap-s4hana-cloud-sdk-helloworld-on-scp-cloudfoundry/ for a detailed introduction):

mvn archetype:generate -DarchetypeGroupId=com.sap.cloud.s4hana.archetypes \
  -DarchetypeArtifactId=scp-cf-tomee -DarchetypeVersion=LATEST

Open the template with your favorite IDE, and browse to the HelloWorldServlet. You can generate project files with maven, too:

mvn eclipse:eclipse

mvn idea:idea

To start quickly we’ll use the result of the code generation on the SAP API Business Hub as a starter, and only minimally adapt it.

Go to SAP API Business Hub, click on APIs and find the translation service via the search tool (search for ‘translation’). Log in – create an ID if you don’t have one yet, it’s free! And then click the ‘Code Snippet’ button as shown below, and select ‘Java’ code.

Paste the resulting code into the get method and change the line printing the result to print it into the servlet response stream:

//printing response
//old: System.out.println(response.toString());
httpServletResponse.getWriter().println(response.toString());

If you logged in to the SAP API Hub before generating the code, then your API key is already present in the code.

At this point let me quickly point out that this kind of access to the SAP Leonardo Machine Learning services is only intended for prototyping and not for production. For production please use the service provisioning via SAP Cloud Platform service binding or the service keys, both leveraging the monitoring and security infrastructure of the platform.

Now you can compile and run the code for a quick initial test (in application subdirectory):

mvn package

mvn tomee:run  # or: mvn tomee:debug

The service should be accessible at http://localhost:8080/hello . You can even deploy it to SAP Cloud Platform (in main directory, where manifest.yml is located):

cf push

The application host name will be provided by the output on the command line (mine was named: ml-services-prototyping-blog-tomee-aidless-solver.cfapps.sap.hana.ondemand.com, making it accessible at http://ml-services-prototyping-blog-tomee-aidless-solver.cfapps.sap.hana.ondemand.com/hello ).

That was a quick start. Of course the code is still lacking a lot of functionality required even for minimal prototyping. Let’s at least fix a few gigantic WTFs:

  • No usable API provided: Input cannot be provided by the caller.
  • Error handling is not at all adapted to the environment.
  • It’s not a cloud-ready application following 12-factor principles. For example the API URI and the API key are hard-coded and not read from the environment.

Let’s fix the biggest problems. First let’s add a simple way to provide input. For the tutorial’s sake we’ll use get-parameters as input. The one thing I cannot stand in programming is sloppy encoding, so to avoid any pitfalls outright we’ll employ the GSON library for creating and parsing JSON. We’ll implement the encoding in the createRequestJson method. See listing below for the changes to add a get-parameter API and use GSON for JSON-encoding:

String targetLanguage = request.getParameter("targetLanguage");
String source = request.getParameter("source");
if (Strings.isNullOrEmpty(targetLanguage) || Strings.isNullOrEmpty(source)) {
    throw new IllegalArgumentException("Missing targetlanguage or source get parameters.");
}

final JsonObject requestContent = new JsonObject();
requestContent.addProperty("sourceLanguage", "en");
requestContent.add("targetLanguages", new Gson().toJsonTree(new String[]{ targetLanguage }));
requestContent.add("units", new Gson().toJsonTree(
  new Object[]{Collections.singletonMap("value", source)}));

String requestJson = new Gson().toJson(requestContent);

The next thing to improve is the handling of the authentication key and the service URL prefix. This information should be configurable via the cloud or development environment and not be hard-coded. Even when creating a prototype this is very beneficial because it allows adaptions during testing testing, such as quickly targeting a new environment for example to test against a new mocked endpoint. On SAP Cloud Platform the recommended way for the configuration of service endpoint parameters is the destination service, as you can read in other tutorials. Here we are going to set the destination environment variable. This works, too, but requires a restart of the services and doesn’t work as well for implementing tenant-specific destinations as it would with the destination service.

Mind that for the application to be configurable via cf you need to deploy it. To even show up in the Cockpit or when listing apps with the command line tool cf via cf apps, it has to have been ‘pushed’ before with cf push, or created in the cockpit. The recommended way to do this is using

The shell-command for the creation of the destination environment variable in the Cloud Foundry environment of the ml_prototyping_with_services app (please replace XXX with your API key):
cf push (see above).

cf set-env ml_prototyping_with_services  destinations "[{name: 'sap_api_business_hub_ml', url: 'https://sandbox.api.sap.com/ml/', properties: [{key: 'API_KEY', value: ‘XXX’}] }]"

With that we can start introducing the http client implementation of the sdk, too. It provides additional performance and security with for example a managed connection cache and a simplified interface. The key point here is that the client is provided via the SDK and can be configured via environment variable or destination service. The correct source is determined on the fly – you only need to place the configuration in the environment. Besides the basic connection parameters, the destination concept allows providing a proxy and additional parameters. The additional parameters are in this case used to provide the API key required by the SAP Leonardo Machine Learning Services on the SAP API Business Hub.

The destination is used to retrieve URI, API key, and a HTTP client is being requested via the destination and request headers are set:
cf push (see above).

final Destination mlDestination = DestinationAccessor.getDestination("sap_api_business_hub_ml");
// Be aware that resolve requires the uri in the destination to end with / character
HttpPost postRequest = new HttpPost(mlDestination.getUri().resolve("translation/translate"));

postRequest.setHeader("Content-Type", "application/json");
postRequest.setHeader("Accept", "application/json;charset=UTF-8");

final String apiKey = mlDestination.getPropertiesByName().get("API_KEY");
if (Strings.isNullOrEmpty(apiKey)) {
    throw new IllegalStateException("Missing API_KEY destination property");
}
postRequest.setHeader("APIKey", apiKey);

HttpEntity body = new StringEntity(requestJson, ContentType.APPLICATION_JSON);
postRequest.setEntity(body);

try {
    // Getting cached http client for base URL, reuse of connection - and send request
    final HttpResponse mlResponse = 
      HttpClientAccessor.getHttpClient(mlDestination).execute(postRequest);
    if (HttpStatus.SC_OK != mlResponse.getStatusLine().getStatusCode()) {
        throw new Exception("Request failed: " + mlResponse.getStatusLine());
    }

    // retrieve entity content (requested json with Accept header, so should be text) and close request
    final String responseBody = HttpEntityUtil.getResponseBody(mlResponse);
    final Map responseMap = new Gson().fromJson(responseBody, Map.class);
    logger.debug("Response: {}", responseMap);
    response.getWriter().write(new Gson().toJson(responseBody));
} finally {
    postRequest.releaseConnection();
}

Then the body created with the afore mentioned createRequestJson method is being used as payload body, the request executed and the result code verified. Finally the resulting JSON is simply passed through to the requestor. You may of course modify this behavior and reformat the result, but for the sake of brevity of this prototyping example it was left as-is.

Add a little exception handler and the code is complete – we can deploy the modified version locally and on SAP Cloud Platform Cloud Foundry:

export ALLOW_MOCKED_AUTH_HEADER=true # or windows: set ALLOW_MOCKED_AUTH_HEADER=true

mvn package && mvn tomee:run -pl application

and then:

cf push

Be aware that also here, if you don’t set up the security environment required for production that provides tenant information (see Secure your application on SAP Cloud Platform Cloud Foundry ) you will need to mock auth headers:

cf set-env <enter your app name> ALLOW_MOCKED_AUTH_HEADER true

Now you should be able to provide input for your new microservice sketch with get parameters: http://localhost:8080/hello?targetLanguage=de&source=Hello and as easily on SAP Cloud Platform: http://enter-your-app-host-name-here.cfapps.sap.hana.ondemand.com/hello?targetLanguage=de&source=Hello. The host name for the deployment on SAP Cloud Platform should be available in the command line output of cf push.

 

For you reference: The code of the adapted example servlet:

@WebServlet("/ml")
public class MlServlet extends HttpServlet
{
    private static final long serialVersionUID = 1L;
    private static final Logger logger = CloudLoggerFactory.getLogger(MlServlet.class);

    @Override
    protected void doGet( final HttpServletRequest request, final HttpServletResponse response )
        throws ServletException, IOException
    {

        try {
            String targetLanguage = request.getParameter("targetLanguage");
            String source = request.getParameter("source");
            if (Strings.isNullOrEmpty(targetLanguage) || Strings.isNullOrEmpty(source)) {
                throw new IllegalArgumentException("Missing targetlanguage or source get parameters.");
            }

            String requestJson = createRequestJson("en", targetLanguage, source);

            // API endpoint for API sandbox - created by combining info from destination and subpath.
            final Destination mlDestination = DestinationAccessor.getDestination("sap_api_business_hub_ml");
            // Be aware that resolve requires the uri in the destination to end with / character
            HttpPost postRequest = new HttpPost(mlDestination.getUri().resolve("translation/translate"));

            postRequest.setHeader("Content-Type", "application/json");
            postRequest.setHeader("Accept", "application/json;charset=UTF-8");

            final String apiKey = mlDestination.getPropertiesByName().get("API_KEY");
            if (Strings.isNullOrEmpty(apiKey)) {
                throw new IllegalStateException("Missing API_KEY destination property");
            }
            postRequest.setHeader("APIKey", apiKey);

            HttpEntity body = new StringEntity(requestJson, ContentType.APPLICATION_JSON);
            postRequest.setEntity(body);

            try {
                // Getting cached http client for base URL, reuse of connection - and send request
                final HttpResponse mlResponse = HttpClientAccessor.getHttpClient(mlDestination).execute(postRequest);
                if (HttpStatus.SC_OK != mlResponse.getStatusLine().getStatusCode()) {
                    throw new Exception("Request failed: " + mlResponse.getStatusLine());
                }

                // retrieve entity content (requested json with Accept header, so should be text) and close request
                final String responseBody = HttpEntityUtil.getResponseBody(mlResponse);
                final Map responseMap = new Gson().fromJson(responseBody, Map.class);
                logger.debug("Response: {}", responseMap);
                response.getWriter().write(new Gson().toJson(responseBody));
            } finally {
                postRequest.releaseConnection();
            }

        } catch (Exception e) {
            logger.error("Failure: " + e.getMessage(), e);

            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().println("Error: " + e.getMessage());
        }

    }

    static String createRequestJson( String sourceLanguage, String targetLanguage, String source )
    {
        final JsonObject requestContent = new JsonObject();
        requestContent.addProperty("sourceLanguage", sourceLanguage);
        requestContent.add("targetLanguages", new Gson().toJsonTree(new String[]{targetLanguage}));
        requestContent.add("units", new Gson().toJsonTree(new Object[]{Collections.singletonMap("value", source)}));

        return new Gson().toJson(requestContent);
    }
}
To report this post you need to login first.

Be the first to leave a comment

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

Leave a Reply