Skip to content
This repository was archived by the owner on Dec 18, 2021. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For more details about **Functions** take a look at [Functions on SAP Help porta
|[hello-timer](./examples/hello-timer)| Basic example | A function that is triggered according to a CRON expression based schedule |
|[qrcode-producer](./examples/qrcode-producer)| Basic example | A function produces the current timestamp as QR code |
|[s4sdk](./examples/s4sdk)| Advanced example | A function leverages the `SAP Cloud SDK for JavaScript` to interact with the `BusinessPartner` API exposed by an `SAP S/4HANA` system |
|[slack-classify-image](./examples/slack-classify-image)| Advanced example, requires Slack integration | An image post in Slack triggers a function. The function classifies the image via `SAP Leonardo` |
|[weather](./examples/weather)| Advanced example, requires `OpenWeatherMap` account | Two functions representing a simple web page that handles user input and displays the result |
|[kafka-producer](./examples/kafka-producer)| Advanced example, requires Kafka broker instance | A function is triggered by a message and produces a message on a Kafka topic |

Expand Down
42 changes: 42 additions & 0 deletions examples/slack-classify-image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Example: Classify an Image posted in Slack with Leonardo

A function is triggered by an image upload to a Slack channel. The function receives an event about the image upload. It then invokes the __`SAP Leonardo`__ image classification API with the image and waits for the response. The response itself contains a prediction about the possible content of the image. The function then posts the prediction to the channel.

## Requirements

* A Slack application, for example within a test workspace, is required.
The Slack application should define a bot user and be subscribed to the __`file_shared`__ bot event.
Bot events are used, because image classification should be limited to channels where the bot user is invited.


## Deployment
First, create a deployment file to provide credentials.
Run inside the project directory:
```bash
faas-sdk init-values -y values.yaml
```
Update the generated file:
* An api key for the image classification can be obtained at [Leonardo image classification overview](https://api.sap.com/api/image_classification_api/overview).
* Also specify the Slack bot user token.

And finally, deploy the project :
```bash
xfsrt-cli faas project deploy -y ./deploy/values.yaml -v
```

## Configure Slack Endpoint
Go to the Configuration UI of your Slack application, navigate to __`Event Subscriptions`__ and toggle __`Enable Events`__. Configure the HTTP trigger URL as the corresponding slack bot event request endpoint URL.
The HTTP trigger URL can be retrieved from:
```
xfsrt-cli faas project get slack
```
The `artifacts` array contains an object with the URL in its `name` property (and a `reference` to HTTP trigger `slack-handler`)

## Test
Install the Slack application to your workspace. In Slack, create a channel. Invite the bot to your channel.
Click on the attachment upload button inside the channel, select an image of your choice and upload it.
As a response, you should get an `SAP Leonardo` powered probability estimation of what is contained in the image.

## License
Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved.
This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the [LICENSE file](../LICENSE.txt).
1 change: 1 addition & 0 deletions examples/slack-classify-image/data/services/leonardoapikey
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xxx
1 change: 1 addition & 0 deletions examples/slack-classify-image/data/services/slacktoken
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xxx
28 changes: 28 additions & 0 deletions examples/slack-classify-image/faas.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"project": "slack",
"version": "0.0.1",
"runtime": "nodejs10",
"library": "./lib",
"secrets": {
"slack-classify-image": {
"source": "./data/services"
}
},
"functions": {
"slack-handler": {
"module": "slack-event.js"
},
"slack-classify": {
"module": "classify-image.js",
"secrets": [
"slack-classify-image"
]
}
},
"triggers": {
"slack-handler": {
"type": "HTTP",
"function": "slack-handler"
}
}
}
106 changes: 106 additions & 0 deletions examples/slack-classify-image/lib/classify-image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use strict';
/** @typedef {import("@sap/faas").Faas.Event} Faas.Event
* @typedef {import("@sap/faas").Faas.Context} Faas.Context
*/

const request = require('request');
const SlackWebClient = require('@slack/client').WebClient;

module.exports = classifyImage;

/**
* @param {Faas.Event} event
* @param {Faas.Context} context
* @return {Promise<any>}
*/
async function classifyImage(event, context) {

// https://api.sap.com/api/image_classification_api/overview
const LEONARDO_API_KEY = await context.getSecretValueString('slack-classify-image', 'leonardoapikey');
const LEONARDO_ENDPOINT = 'https://sandbox.api.sap.com/mlfs/api/v2/image/classification';
const leonardo = {
endpoint: LEONARDO_ENDPOINT,
apiKey: LEONARDO_API_KEY,
};

// https://api.slack.com/slack-apps#creating_apps
const SLACK_CLIENT_TOKEN = await context.getSecretValueString('slack-classify-image', 'slacktoken');
const slack = {
client: new SlackWebClient(SLACK_CLIENT_TOKEN),
token: SLACK_CLIENT_TOKEN,
};

let file, cType;

switch (event.data.event.type) {
case 'file_shared': // https://api.slack.com/events/file_shared
return slack.client.files.info({ file: event.data.event.file_id, count: 0 })
// check created file
.then((fileDesc) => new Promise((resolve, reject) => {
file = fileDesc.file;
request({
url: file.url_private,
encoding: null,
headers: { 'Authorization': 'Bearer ' + slack.token }
}, (err, response, body) => {
if (err) reject(err);
cType = response.headers['content-type'];
if (!cType.startsWith('image')) reject(new Error(`file mime type ${cType}not supported`));

resolve(body);
});
}))
// post final image to leonardo
.then((imgFinal) => new Promise((resolve, reject) => {
request.post({
url: leonardo.endpoint,
headers: {
'Accept': 'application/json',
'APIKey': leonardo.apiKey,
},
formData: {
files: {
value: imgFinal,
options: {
contentType: cType,
filename: file.name
}
}
},
preambleCRLF: true,
postambleCRLF: true,

}, (err, response, body) => {
if (err) reject(err);
if (response.statusCode === 200) {
resolve(JSON.parse(body));
}
else {
reject();
}
});
}))

// create slack message from leonardo prediction
.then((data) => {
const results = (data.predictions.length === 0) ? [] : data.predictions[0].results.reduce(function (result, current) {
result.push(`${current.label} with score ${current.score}`);
return result;
}, []);
const message = `Leonardo thinks image ${file.name} contains ${results.length ? ':\n' + results.join('\n') : ' nothing'}`;

return slack.client.chat.postMessage({
channel: file.channels[0],
text: message
});
})

.catch((err) => {
console.log('error during image classification ' + err);
});
default:
console.log(`unknown event "${event.data.event.type}" received`);
}

}

22 changes: 22 additions & 0 deletions examples/slack-classify-image/lib/slack-event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';
/**
* @namespace Faas
* @typedef {import("@sap/faas").Faas.Event} Faas.Event
* @typedef {import("@sap/faas").Faas.Context} Faas.Context
*/

/**
* @param {Faas.Event} event
* @param {Faas.Context} context
* @return {Promise<*>|*}
*/
module.exports = function (event, context) {
if (event.data.challenge) {
return { challenge: event.data.challenge };
}
context.callFunction('slack-classify', { // do not await
type: 'application/json',
data: event.data
});
};

19 changes: 19 additions & 0 deletions examples/slack-classify-image/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"dependencies": {
"@slack/client": "5.0.2",
"request": "2.88.2"
},
"devDependencies": {
"@sap/faas": ">=0.7.0"
},
"files": [
"data",
"lib",
"faas.json",
"package.json"
],
"scripts": {
"test": "npm run all-tests",
"all-tests": "node ./node_modules/mocha/bin/_mocha test/**/test*.js --colors -csdlJson --slow 200"
}
}