Nasz pierwszy ChatBot

01/10/2018

Data ostatniej aktualizacji: 09.07.2020 06:41

Boty nie są nowym rozwiązaniem. Istnieją na rynku od wielu lat. Obecnie, ich znaczenie i wykorzystanie w komunikacji z klientami istotnie rośnie, między innymi za sprawą coraz większej popularności komunikatorów internetowych. Dzięki połączeniu ChatBota z używanymi na co dzień kanałami komunikacji typu Facebook Messenger, możemy zaproponować nasze usługi za pośrednictwem interfejsu, który użytkownicy już znają i który nie odbiega od sposobu komunikacji “międzyludzkiej”. Te zalety “interfejsów konwersacyjnych” powodują, że coraz więcej firm zaczyna oferować swoje produkty poprzez wirtualnych asystentów. Taki trend widać wyraźnie na przykład w branży bankowej.

Jakie obecnie dostępne technologie wykorzystywane są do budowy botów? Jakie mają możliwości, jaki jest próg wejścia do rozpoczęcia korzystania z tego typu narzędzi, łatwość użycia czy produktywność. Postanowiliśmy przyjrzeć się bliżej temu tematowi.

W tym celu rozpoczęliśmy eksperyment, polegający na stworzeniu ChatBota, który posłuży jako dodatkowy kanał komunikacji dla zbudowanego przez nas wcześniej uproszczonego systemu do sprzedaży ubezpieczeń. Jego zadaniem będzie przeprowadzenie z użytkownikiem konwersacji prowadzącej do sprzedaży ubezpieczenia. W trakcie rozmowy bot powinien pozyskać od klienta informacje niezbędne do przygotowania oferty i zawarcia umowy, a następnie przekazać je do systemu sprzedażowego w celu uzyskania wyceny i założenia polisy.

Linia dialogowa, którą chcemy zaimplementować wygląda następująco:

ChatBot

Logika ChatBota

Jako bazę postanowiliśmy wykorzystać bibliotekę Bot Builder SDK (v3). Pozwala ona na stworzenie logiki bota w dwóch językach: C# (.NET) oraz JavaScript (NodeJS). Na początek wybraliśmy drugą opcję.

Przed rozpoczęciem programowania bota musimy zdefiniować biblioteki, z których będziemy korzystać:

package.json:
...
  "dependencies": {
    "botbuilder": "^3.15.0",
    "dotenv-extended": "^2.0.0",
    "request": "^2.88.0",
    "restify": "^7.2.1"
  },
...

Pracę rozpoczniemy od stworzenia kilku funkcji pomocniczych do komunikacji z API systemu sprzedażowego – w celu określenia pobrania parametrów produktu ubezpieczeniowego, wyceny oferty ubezpieczenia, zakupu polisy i pobrania dokumentu potwierdzającego:

backend.js:
[...]
function getPrice(params, auth) {
    return new Promise(function (resolve, reject) {
        request({
                method: 'POST',
                uri: `http://${backendHost}:8081/api/offers`,
                headers: {
                    'content-type': 'application/json',
                    'Authorization': 'Bearer ' + auth.accessToken
                },
                body:
                    JSON.stringify(params)

            },
            function (error, response, body) {
                if (error) {
                    reject(error);
                } else {
                    console.log(body);
                    resolve(JSON.parse(body));
                }
            }
        );
    });
}
[...]

Aby zainicjować bota musimy zdefiniować connector oraz określić miejsce przechowywania stanu konwersacji. Dla uproszczenia posłużymy się magazynem w pamięci operacyjnej.

bot.js
[...]
const connector = new builder.ChatConnector({
    appId: process.env.MicrosoftAppId,
    appPassword: process.env.MicrosoftAppPassword
});
const inMemoryStorage = new builder.MemoryBotStorage();
const bot = module.exports = new builder.UniversalBot(connector, getStartingSteps()).set('storage', inMemoryStorage);
[...]

Możemy teraz przejść do sedna, czyli programowania możliwości dialogowych bota. Punktem wejścia jest funkcja getStartingSteps wskazana przy inicjalizacji. Zwraca ona tablicę zawierającą dwie funkcje. Pierwsza z nich zawiera definicję powitania oraz pytania kierowanego przez bota do użytkownika. Druga funkcja określa reakcję na otrzymaną odpowiedź.

bot.js:
[...]
function getStartingSteps() {
    return  [
        function (session) {
            session.send('Hello in ASC LAB Insurance Agent Bot!');
            builder.Prompts.choice(
                session,
                'What do you want? (currently you can only buy insurance)',
                [IntentType.BUY_POLICY],
                {
                    maxRetries: 3,
                    retryPrompt: 'Not a valid option'
                });
        },
        function (session, result) {
[...]
            const selection = result.response.entity;
            switch (selection) {
                case IntentType.BUY_POLICY:
                    return session.beginDialog('policy-sell');
            }
        }
    ];
}
[...]

Kluczowym elementem drugiej funkcji jest linia zawierająca wywołanie:

return session.beginDialog('policy-sell');

Powoduje ona rozpoczęcie nowego dialogu ‘policy-sell’ zdefiniowanego za pomocą wywołania:

bot.dialog('policy-sell', getPolicySellSteps());

oraz funkcji getCreatePolicySteps zwracającej tablicę funkcji określających kolejne kroki dialogu – analogicznie jak w poprzednim przykładzie.

Przesyłane przez użytkownika odpowiedzi możemy również zapisać w sesji:

function getCreatePolicySteps() {
    return [
        function (session) {
            session.send('OK, lets sign papers!');
            builder.Prompts.text(session, 'What is your first name?');
        },
        function (session, results, next) {
            session.userData.firstName = results.response;
            next();
        },
[...]

Dzięki temu możemy do nich sięgnąć w dalszej części konwersacji, aby przykładowo na ich podstawie skonstruować i wycenić ofertę:

function _addCalculatePriceSteps(product, steps) {
    steps.push(function (session) {
        session.send("Calculating price. Please wait...");

        const params = {
            "productCode": product.code,
            "policyFrom": session.dialogData.policyStart,
            "policyTo": session.dialogData.policyEnd,
            "selectedCovers": product.covers.map(c => c.code),
            "answers": session.dialogData.answers
        };

        console.log(params);
        backend.calculatePrice(params).then(function (offer) {
            session.send("Your insurance will cost %s EUR. Offer ID: %s", offer.totalPrice, offer.offerNumber);
            session.conversationData.offer = offer;
            builder.Prompts.choice(
                session,
                'Are you interested?',
                ['Yes', 'No']
            );
        });

SDK posiada również funkcjonalność pozwalającą na przesyłanie plików do użytkownika. Za jej pomocą, na koniec rozmowy bot może wysłać klientowi certyfikat potwierdzający zawarcie polisy w postaci pliku PDF:

backend.createPolicy(params).then(function (policy) {
        console.log(policy);
        session.send('Your policy has been created: %s', policy.policyNumber);
        waitForDocumentCreation()
            .then(function () {
                return backend.getPolicyAttachments(policy.policyNumber)
            })
            .then(function (response) {
                console.log("processing attachments ");
                if (response != null && response.documents != null) {
                    console.log(response.documents);
                    response.documents.forEach(function (doc) {
                        sendInline(session, {
                                content: doc.content,
                                contentType: 'application/pdf',
                                name: 'policy.pdf'
                            }
                        );
                    });
                }
                session.endDialog();
            });
    });


[...]

function sendInline(session, document) {
    const msg = new builder.Message(session)
        .addAttachment({
            contentUrl: util.format('data:%s;base64,%s', document.contentType, document.content),
            contentType: document.contentType,
            name: document.name
        });

    session.send(msg);
}

Na koniec musimy jeszcze utworzyć interfejs HTTP, za pomocą którego będzie można komunikować się z naszym botem:

app.js:
const restify = require('restify');
const bot = require('./bot.js');

const server = restify.createServer();
server.post('/api/messages', bot.connector('*').listen());
server.listen(process.env.PORT || 3978, () => {
        console.log(`${server.name} listening to ${server.url}`);
});

Pełny kod bota można znaleźć w repozytorium.

W celu uruchomienia bota lokalnie trzeba utworzyć plik .env i zdefiniować w nim zmienną BACKEND_HOST (zawierającą adres IP hosta na którym uruchomiony jest API Gateway naszego uproszczonego systemu do sprzedaży ubezpieczeń) a następnie wykonać polecenie:

node app.js

Działanie naszego bota możemy przetestować lokalnie za pomocą stworzonego przez Microsoft Emulatora.

 

Instalacja na Azure

Aby uruchomić naszego Bota na Azure wystarczy kilka prostych kroków.

W pierwszym kroku tworzymy instancję bota:

ChatBot

W tym celu musimy określić kilka parametrów. Najważniejsze z nich to:

ChatBot

Po kilku (2-3) minutach bot zostanie utworzony:

ChatBot

W drugim kroku dodajemy adres API Gateway’a systemu ubezpieczeniowego w zmiennej środowiskowej. W tym celu otwieramy ekran konfiguracji usługi web (trzecia od góry na ekranie listy zasobów):

ChatBot

Przechodzimy na zakładkę “Ustawienia aplikacji” i dodajemy zmienną BACKEND_HOST zawierającą adres IP hosta, na którym uruchomiony jest API Gateway systemu ubezpieczeniowego, a następnie wciskamy przycisk “Zapisz”:

ChatBot

Ostatnim krokiem jest skonfigurowanie ciągłego wdrażania (continuous deployment). Zaczynamy od otwarcia  ekranu do konfiguracji usługi bota (druga od góry na ekranie listy zasobów):

ChatBot

Przechodzimy na zakładkę “Kompiluj” i wybieramy opcję “Skonfiguruj ciągłe wdrażanie”:

ChatBot

Naciskamy przycisk “Instalacja” -> “Skonfiguruj wymagane ustawienia” i podajemy dane dostępowe do repozytorium kodu:

ChatBot

Po zatwierdzeniu ustawień za pomocą przycisku “OK” zostanie automatycznie uruchomiony proces instalacji, który powinien zakończyć się w ciągu kilkudziesięciu sekund:

ChatBot

Na koniec możemy sprawdzić poprawność wdrożenia. Zamykamy okno konfiguracji ciągłego wdrażania i otwieramy zakładkę “Testuj w funkcji rozmowy w sieci Web”. Z tego miejsca możemy przeprowadzić próbną konwersację z naszym botem:

ChatBot

Próbowaliśmy również wykonać instalację bota na Azure z poziomu linii poleceń. Niestety obecna wersja CLI (azure-cli: 2.0.45, botservice extension: 0.4.0) nie pozwala na poprawne wykonanie opisanych powyżej operacji.

 

Integracja z zewnętrznymi kanałami komunikacji

WebChat

Najprostszym do skonfigurowania zewnętrznym kanałem komunikacji jest czat. Aby go uruchomić otwieramy zakładkę kanały:

ChatBot

a następnie w pozycji “Web Chat” wybieramy akcję “Edytuj”:

ChatBot

Wyświetlony kod osadzania (wraz z kluczem tajnym), możemy umieścić na dowolnej stronie www. Na potrzeby demonstracji posłużymy się tu pierwszym z brzegu edytorem HTML online:

ChatBot

 

Slack

Szczegółowy opis sposobu integracji bota ze Slackiem znajduje się w dokumentacji Microsoftu.

Po wykonaniu kilku kroków opisanych w powyższej instrukcji możemy porozmawiać z naszym botem za pośrednictwem slacka:

ChatBot

 

Facebook Messenger

W podobny sposób możemy zintegrować naszego bota z Facebook Messengerem.

 

Podsumowanie

Obserwacje, jakie poczyniliśmy w trakcie niniejszego eksperymentu:

Mamy świadomość, że testowany przez nas framework jest technologią stosunkowo nową i liczymy, że niedociągnięcia, na które natrafiliśmy, zostaną niedługo usunięte.

Następnym krokiem w rozwoju naszego bota będzie próba rozszerzenia jego funkcji dialogowych o zdolność rozumienia języka naturalnego, ale o tym – w kolejnym artykule!

Autor: Robert Kuśmierek, Lead Software Engineer, ASC LAB