AzureMachine LearningVeranstaltungenVorträge

SQL Saturday #772 MetalBot – erste Schritte im Bot Framework

Veröffentlicht

Bots sind eine spannende Ergänzung zur Arbeit mit Daten in Unternehmen und eröffnen den Mitarbeitern neue Wege, um mit Daten zu interagieren und sich Informationen zu beschaffen. Nachdem ich mich in den letzten Monaten einige Male mit dem Microsoft Bot SDK (link) beschäftigen durfte, habe ich mich auf der Bahnfahrt zum SQL Saturday in München spontan entschieden, für die Session mit Frank Geisler (twitter/www), einen Bot zu programmieren und vorzustellen.

Unser Bot, der MetalBot erfüllt nur eine einzige Aufgabe, nämlich den User nach einem Bild seines Lieblingsgetränks zu fragen und basierend auf einem hochgeladenen Bild eines Getränks eine Empfehlung auszusprechen, welche Band der User als nächstes hören sollte.

Für Eilige: wenn ihr die Session gesehen habt oder nicht an den Erklärungen interessiert seid sondern nur zum Quellcode möchtet, dann begebt euch bitte hier direkt zum Quellcode des Bots.

Gundelegende Konzepte im BOT SDK

Sehen wir uns zunächst einige grundlegende Konzepte des Bot Frameworks an. Grundsätzlich kommunizieren Clients nicht direkt mit eurem Bot sondern mit einem Bot Service Framework, dieses Service Framework „spricht “ wiederum über REST Calls mit eurem Bot. Das heißt einerseits müsst ihr für euren Bot nicht Konnektoren zu verschiedenen Kanälen implementieren, da das Microsoft bereits für euch übernommen hat, andererseits habt ihr klar definierte Schnittstellen über die euer Bot kommuniziert.

Was macht das Framework mit Nachrichten?

Im Bot Framework gibt es nun einen ganzen Stack, der die Nachrichten verarbeitet, bevor sie euren Bot erreichen. Der Stack zur Kommunikation wird in der Dokumentation von Microsoft erklärt.

Abbildung 1: Die Kommunikation im Bot Framework
  1. Der Benutzer schickt mit seinem Client eine Nachricht an den Bot Framework Service.
  2. Der Bot Framework Service macht daraus einen HTTP POST mit der Nachricht und weiteren Informationen zur Benutzeraktivität als JSON Objekt im Payload.
  3. Dieser POST Request wird an einen Webserver weitergereicht. Dort wird er in ein Activity-Objekt des Bot SDKs umgewandelt.
  4. Es wird die „process activity“-Methode eines Adapters in der Middleware aufgerufen, dem das Activity-Objekt übergeben wird.
  5. Der Adapter erzeugt ein Turn Context-Objekt, das an die Middleware übergeben wird, die es an den Bot weiterreicht.
  6. Der Bot prozessiert den Turn Context und sendet eine Antwort an die Middleware.
  7. Die Middleware reicht die Antwort an den Framework Service weiter, der sie wiederum an den User kommuniziert.
  8. Es werden Acknowledgement-Nachrichten vom Framework Service an die Middleware und von dort zum Bot und zurück über Middleware, Webserver zum Franework Service gereicht. Diese dienen dazu, auch auf Ereignisse wie das Zustellen von Nachrichten reagieren zu können, sind hier aber nicht weiter von Bedeutung.

Es gibt zahlreiche Möglichkeiten, in einzelne Schritte dieses Prozesses einzugreifen. Diese sind in der Dokumentation des Bot Frameworks zu finden. Sie ermöglichen es beispielsweise mittels dependency injection in der Middleware den Kontext anzureichern und so weitere Informationen an die Nachrichten anzuhängen.

Im Bot selber greifen folgende Konzepte:

Connector

Die Connector Library im Bot SDK ermöglicht den Zugriff auf den Connector im Bot Framework Service, der die Nachrichten vom Kanal zum Bot und vom Bot zum Kanal weiterreicht.

Activity

Das Activity-Objekt ist das zentrale Objekt der Kommunikation und wird vom Connector verwendet, um Informationen vom und zum User an die Middleware und den Bot weiterzureichen. Die meisten Activity-Objekte sind Nachrichten, es kann aber auch andere Aktivitäten geben.

Dialog

Dialoge sind die Objekte, die den Ablauf einer Konversation steuern. Es können (und werden normalerweise) mehrere Dialoge implementiert werden, die sich auch gegenseitig aufrufen können. Dialoge sind die zentralen Objekte, die ihr in Bot-Projekten üblicherweise anpasst und erzeugt.

Form Flow

Form Flow kann in Bots verwendet werden, um die Informationserhebung zu steuern. Form Flow ist hilfreich, wenn es darum geht, dass – ähnlich einem Formular – mehrere Informationen erfasst werden müssen, um eine Eingabe abzuschließen.

State

Im Bot Builder Framework kann der Status einer Konversation über ein State-Objekt organisiert werden. Im State-Objekt sind beispielsweise vorherige Interaktion mit dem User gespeichert oder auch der Benutzer-Name, wenn ihr User persönlich begrüßen möchtet.

Implementierungsdetails

Abbildung 2: Die gesamte Solution

Um einige dieser Konzepte in Aktion zu erleben, sehen wir uns nun an, wie unser Metal-Bot implementiert ist.

Unser Metal-Bot ist ziemlich einfach gestrickt, er erfüllt nur eine einzige Aufgabe und wir haben auch das Error-Handling und den Kommunikations-Ablauf auf ein Minimum reduziert.

Deshalb gibt es auch nur drei Klassen, die bei der Implementierung von Interesse sind (also für die Funktionalität genau genommen sogar nur 2 Klassen, die dritte Klasse, den MessagesController, fassen wir nur für ein kleines Gimmick an).

Die Klassen, in denen für den ersten Bot Code geschrieben werden muss sind die RootDialog Klasse enthalten in RootDialog.cs  und eine Hilfsklasse namens MessageUtil in der entsprechenden .cs-Datei.

Zusätzlich haben wir noch im MessagesController eine kleine Änderung vorgenommen, um die Konversation mit dem Bot etwas „menschlicher“ zu gestalten.

Der Root Dialog

Wir verwenden in unserer Applikation nur einen Dialog, dieser ist standardmäßig der RootDialog. Wie der Konversations-Fluss für unseren „Bandempfehlungs“-Prozess aussehen sollte, haben wir in Abbildung 3 aufgezeichnet.

 

Abbildung 3: Konversationsfluss mit dem Metal-Bot

Um uns zu merken, wo wir uns gerade in diesem Ablauf befinden (eine Empfehlung können wir schließlich nicht abgeben ohne vorher das Bild bekommen und analysiert zu haben), haben wir in MessageUtil.cs ein Enum eingeführt, das verschiedene Zustände unserer Konversation abbildet:

public enum State { 
  INITIATED, 
  IMAGE_TRIGGERED, 
  AWAITING_IMAGE, 
  GIVING_RECOMMENDATION, 
  FINDING_BANDS }

Die Zustände sind:

  • „Initialisiert“, also dem Zustand wo noch kein „Bandempfehlungs-Prozess“ gestartet wurde,
  • „Image triggered“, den Zustand wo der Bot den Prozess des Bild-Abfragens startet,
  • „Awaiting image“ wo der Bot als nächsten Input vom User ein Bild benötigt,
  • „Giving Recommendation“, wo der Bot ein Bild bekommen und analysiert hat und beginnt dem User eine Empfehlung auszusprechen indem er ihm eine Genre-Auswahl anzeigt.
  • „Finding Bands“ wo der Bot aus dem Genre die geeigneten Bands auswählt.

Den Status initialisieren wir in der Root Dialog Klasse natürlich als „Initiated“.

State myState = State.INITIATED;

Doch bevor wir mit dem User interagieren möchten wir ihn begrüßen. Das können wir entweder in der normalen Routine machen, wenn wir eine Nachricht empfangen oder wir können dafür den „StartAsync“-Task verwenden, der beim Starten des Dialogs ausgeführt wird.

Letztere Variante sieht dann wie folgt aus:

public async Task StartAsync(IDialogContext context)
{
  var activity = context.Activity;
  if (activity.From.Name != null)
  {
    userName = activity.From.Name.Split(' ')[0];
  }
  await context.PostAsync($"{MessageUtil.GetGreeting()}, {userName}. ");
  context.Wait(MessageReceivedAsync);
  await Task.CompletedTask;
}

Die hier aufgerufene Methode aus der MessageUtil-Klsse, GetGreeting(), ist nur ein kleines Gimmick und wählt aus einer Auswahl an Begrüßungen zufällig eine aus:

public static string GetGreeting()
{
  int idx = new Random(Guid.NewGuid().GetHashCode()).Next(-0, Greetings.Count());
  return Greetings[idx];
}

Sehen wir uns MessageReceivedAsync an, die Methode, die die Middleware im OnTurn Handler aufruft. Dort müssen wir unterschiedlich reagieren, je nachdem in welchem Stadium des Dialogs wir uns befinden. Deshalb haben wir dort eine if-Abfrage über den Status des Dialogs:

if (myState == State.FINDING_BANDS) {…}
else if (myState == State.GIVING_RECOMMENDATION) {…}
else if (myState == State.AWAITING_IMAGE) {…}
else if (myState == State.IMAGE_TRIGGERED)
else {…}

Alle diese Blöcke müssen wir sicher nicht im Detail durchgehen, da hier nur verschiedene Fragen gestellt und verschiedene Status gesetzt werden. Spannend wird es dabei an der Stelle, wo wir dem User nicht einfach nur eine Frage stellen, sondern eine Klickbare Auswahl an Antworten zur Verfügung stellen. Das geschieht über die PromtDialog.Choice-Methode des Bot Frameworks. Wenn man dieser Methode eine Liste von strings übergibt, zeigt sie (wenn der Client des Users das unterstützt, was bei Skype beispielsweise nicht der Fall ist) eine Klickbare Auswahl an Optionen an. Diese Methode bekommt eine Callback-Funktion übergeben, an die die Antwort weitergereicht wird, eine Liste von Strings und einen Nachrichtentext. Wir kapseln sie in einer eigenen Methode:

private void ShowOptions(
  IDialogContext context,
  string messagetext,
  List<string> Options)
{
  PromptDialog.Choice(
    context, 
    this.MessageReceivedAsync, 
    Options, 
    messagetext, 
    "Was soll ich denn DAMIT anfangen?", 
    3);
}

Die 3 ist dabei die Anzahl der erlaubten Fehlversuche bevor die Eingabe abgebrochen wird und der zweite String die Nachricht, die der User bei einer ungültigen Eingabe erhält.

Sehen wir uns nun die MessageUtil-Klasse an. Sie beinhaltet neben einigen Enums und String-Arrays und Methoden, die diese aufeinander mappen auch unsere „Logik“ beispielsweise um eine Band bei vorgegebenem Genre auszuwählen:

public static string GetBandForStyle(Style s)
{
  Random r = new Random(Guid.NewGuid().GetHashCode());
  int idx = 0;
  switch (s)
  {
    case Style.BLACK_METAL:
      idx = r.Next(-0, BlackMetalBands.Count());
      return BlackMetalBands[idx];
    …
  }
}

Nahezu die gesamte Logik verläuft nach diesem Schema. Sehen wir uns nun im nächsten Schritt die Stelle an, die den Bot „intelligent“ macht.

Die Bilderkennung

Um zu erkennen, was für ein Getränk der User fotografiert hat, wollen wir im hochgeladenen Bild nach Text suchen, diesen erkennen und den erkannten Text einem Getränk zuordnen. Zum Glück für uns gibt es fertige Bibliotheken, die Text aus Bildern extrahieren. Noch größeres Glück ist es, dass Microsoft mit den Cognitive Services eine solche Bibliothek nicht nur als Bibliothek zur Verfügung stellt sondern sogar als Service, der uns nach Upload eines Bildes an eine REST-Schnittstelle den Text im Bild zurückliefert.

Um die Cognitive Services zu nutzen, legen wir uns im Azure Portal eine Ressource vom Typ „Computer Vision“ an.

Abbildung 4: Computer Vision Ressource anlegen

In dieser Ressource müssen wir uns den Endpunkt für die Kommunikation mit der Ressource notieren (1 in Abbildung 5) sowie die Keys (findet Ihr unter 2 in Abbildung 5).

Abbildung 5: Die Daten eurer Computer Vision Ressource.

Den Text in einem Bild zu detektieren ist nun ein relativ einfacher Aufruf dieser Ressource.

Zunächst müsst ihr einen HttpClient (im Namespace System.Net.Http) erzeugen, dem ihr euren Key als Default-Header hinzufügt:

HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);

Nun generiert ihr die URL, die für die Bilderkennung aufgerufen werden muss, diese bekommt neben dem Endpunkt eurer Computer Vision Ressource noch die Sprache (in unserem Fall „unknown“) und einen Parameter der besagt, dass der Algorithmus das Bild drehen darf, um den Text zu erkennen übergeben:

string requestParameters = "language=unk&amp;detectOrientation=true";
string uri = uriBase + "?" + requestParameters;

Nun übertragt ihr per POST request das Byte-Array des Bildes an die URL. Da die Bilder im Bot Framework nicht als Rohdaten an den Bot übergeben werden sondern als Link in einem OneDrive-Ordner, laden wir es zunächst mit dem WebClient herunter:

HttpResponseMessage response;
byte[] imageBytes;
using (var webClient = new WebClient())
{
  imageBytes = webClient.DownloadData(contentURL);
}

Dann übergeben wir den Byte-Array an den Computer Vision Dienst und holen uns die Antwort:

using (ByteArrayContent content = new ByteArrayContent(imageBytes))
{
  content.Headers.ContentType =
  new MediaTypeHeaderValue("application/octet-stream");
  response = await client.PostAsync(uri, content);
}
string contentString = await response.Content.ReadAsStringAsync();

Damit haben wir alle Bestandteile zusammen, um Text im Bild zu detektieren. Zurückgegeben wird ein JSON Objekt, das wir nun serialisieren könnten, um uns ein Objekt zu erzeugen, das den Inhalt des Bildes widerspiegelt, in unserem sehr einfachen Fall durchlaufen wir aber nur ein Dictionary das als Key das Getränk und als Value eine Liste mit Keywords enthält und suchen, welche Keywords im JSON-String den die Vision API zurückgibt enthalten sind:

foreach (KeyValuePair<Drink, string[]> kvp in KeywordMappings)
{
  foreach (string keyword in kvp.Value)
  {
    if (LabelText.ToLower().Contains(keyword.ToLower()))
    {
      if (results.ContainsKey(kvp.Key))
      {
        results[kvp.Key] += 1;
      }
      else
      {
        results[kvp.Key] = 1;
      }
    }
  }
}

Dieses Vorgehen ist natürlich sehr oberflächlich und alles andere als elegant, für unseren einfachen Proof of Concept funktioniert es aber gut genug.

Für die Demos, die wir am SQL Saturday in München gezeigt haben, genügt eine Computer Vision API im Free Tier vollkommen, das heißt, die gesamte Entwicklung wie wir sie hier vorstellen, lässt sich komplett kostenlos aufsetzen und testen.

Testen

Was nutzt uns das schönste Bot Framework und die schönste Idee für einen Bot, wenn wir beim Entwickeln nicht schnell testen und debuggen können? Natürlich besteht die Möglichkeit, Eure Bots auch lokal zu testen und die Entwicklung auch ohne teure Cloud-Ressourcen voran zu bringen. Dafür benötigt ihr den Bot Framework Emulator von Microsoft (link, https://github.com/Microsoft/BotFramework-Emulator/releases). Wenn ihr den Emulator startet, habt ihr die Möglichkeit, einen neuen Bot zu erstellen oder euch mit einer vorhandenen Konfiguration zu verbinden. Beim ersten Start müsst ihr natürlich ersteres tun. Auf dem folgenden Screen müsst ihr nur einen URL Endpunkt für euren Bot angeben. Euren lokalen Endpunkt könnt ihr herausfinden, indem ihr euer Bot-Projekt im Visual. Studio (entweder über Drücken von F5 oder über den „Debug-Button“ startet. Es öffnet sich dann ein Browser-Fenster, in dem ihr den Port für euren Bot auf localhost in der Addresszeile finden könnt). Tragt also als URL für euren Bot http://localhost gefolgt von einem Doppelpunkt und eurem Port gefolgt von „/api/messages“ ein, in meinem Fall beispielsweise http://localhost:3978/api/messages

Im Chat-Fenster dass sich nun öffnet, könnt ihr mit eurem Bot kommunizieren (mit 1 in Abbildung 7 markiert). Auf der Rechten Seite des Fensters seht ihr ein Log (2 in Abbildung 7), das den Verlauf der Konversation zusammen mit den HTML Status Codes der einzelnen Nachrichten zeigt. Wenn ihr eine Nachricht hier oder im Chat-Fenster auswählt, seht ihr im oberen rechten Teil des Fensters (3 in Abbildung 7) ein Transkript der Nachrichten, die übermittelt wurden, im Beispiel habe ich die Nachricht mit dem Bild-Upload markiert, ihr seht den content-Type, der übermittelt wurde (image/jpeg) und den Link zum Bild (contenturl).

Abbildung 7: Der Bot Framework Emulator in Aktion

Fazit

Das Microsoft Bot SDK ist ein mächtiges Werkzeug um sehr einfach und sehr schnell einen eigenen Bot an den Start zu bekommen. Wenn ihr daran interessiert seid, könnt ihr hoffentlich mit dieser Anleitung in wenigen Stunden einen eigenen Bot beginnend von einem leeren Projekt programmieren. Wenn ihr euch mit der Technologie beschäftigt, bin ich ziemlich sicher, dass euch viele Einsatzmöglichkeiten für Bots einfallen, ich freue mich über Diskussionen mit euch – entweder hier im Kommentar-Bereich oder per Mail oder Twitter. Wir antworten auch persönlich, ohne Bot. Versprochen.

Happy Bot-ting.

Kommentar verfassen