Ein Projekt von Ronja Brauchle, Julia Cypra und Lara Winter
Einleitung
Gab es bereits drei Tage hintereinander Pasta mit Pesto? Ist das einzig abwechslungsreiche in deinem Speiseplan die Entscheidung ob die klassische Tiefkühlpizza mit Salami oder die extravagantere Wahl des Flammenkuchens nach Bauernart in den Ofen geschoben wird? Unser MealBot hilft genau bei diesen monotonen und inspirationslosen Phasen: Er schlägt Rezepte basierend auf Lebensmitteln vor, die man gerade zuhause liegen hat; er kann nach Länderküchen und Essensvorlieben Mahlzeiten heraussuchen oder einfach zu einer dir bereits bekannten Mahlzeit das Rezept ausgeben.
Wir haben für das Projekt Dialogflow von Google verwendet, das eine Engine basierend auf NLU (Natural Language Understanding) ist und mit der man intelligente Chatbots erstellen kann. Durch das Anbinden an die MealDB (https://www.themealdb.com/) mithilfe der MealAPI kann unser Chatbot dem User hunderte von Rezepten ausgeben. Der MealBot wurde in Telegram integriert und ist somit dort für jeglichen Chitchat anzutreffen.
Entwurf und Architektur
Die obige Abbildung visualisiert die Auftragsausführung nach einer Benutzereingabe. Wie bereits erwähnt, ist unser Chatbot in den Messengerdienst Telegram integriert, von dort wird die Benutzereingabe weiter an Dialogflow geleitet. Dank der in Dialogflow integrierten KI kann die Benutzereingabe einem der passenden individuell erstellten Intents zugeordnet werden. Dabei werden außerdem auch bestimmte Parameter aus der Eingabe extrahiert. Ein Intent kann eine vorgefertigte statische Antwort beinhalten, es kann aber auch unter dem Punkt „Fulfillment“ ein Webhook Aufruf aktiviert und somit passender Code getriggert werden. Im Falle eines Webhook Aufrufes werden die extrahierten Parameter, sowie weitere Informationen zu dem zugeordneten Intent in einer JSON HTTPS POST Anfrage, dem Fulfillment Request, an den Webhhokdienst geschickt. Der Webhook kann selbst gehostet werden, dieser muss dann HTTPS POST Anfragen in Form von JSON verarbeiten. In unserem Fall haben wir den Code zur Auftragsausführung im Inline-Editor von Dialogflow erstellt und somit in einer Cloud Function bereitgestellt. Die Cloud Function wird dann durch den Dialogflow getriggert und erhält die Daten aus der JSON Anfrage. Die Cloud Function besteht lediglich aus einer index.js, die Funktionen für die jeweiligen Intents enthält. Durch das passende Intent Matching wird dann auch die jeweilige Funktion ausgeführt. In dieser Funktion wird unsere externe API, die MealDB, abgefragt. Dabei wird je nach Intent ein zugehöriger Endpoint mit den passenden Parametern angesprochen. Die Antwort der MealDB Abfrage wird dann zusammen mit anderen Daten in der Webhook Antwort wieder an Dialogflow zurückgeschickt. Dialogflow extrahiert aus der Antwort die benötigten Daten und sendet diese letztendlich an Telegram, sodass der Endbenutzer die Antwort erhält.
Die untenstehende Abbildung verdeutlicht nochmal den Ablauf mit den von uns benutzten Technologien.
Der Vorteil von Cloud Functions ist, dass diese serverless, günstig und skalierbar sind. Das heißt die Cloud Function ist ereignisgesteuert und wird nur bei einem Event getriggert. Dadurch und wegen der Skalierbarkeit halten sich die Kosten gering.
Basics aka die Bausteine von Dialogflow
Intents
Intents ermöglichen es dem Agenten die Motivation hinter einer bestimmten Benutzereingabe zu verstehen. Jeder Intent stellt eine Aufgabe oder Handlung dar, die der Benutzer ausführen möchte. Im folgenden Bild sieht man die von uns für den MealBot erstellten Intents inklusive der Erklärung dieser.
Ausdrücke, die dasselbe bedeuten, aber auf unterschiedliche Weise konstruiert werden, werden demselben Intent zugeordnet. Somit kommen wir direkt zum ersten Bestandteil (von dreien) eines Intents, und zwar zu den Trainingsformulierungen:
1.Trainingsformulierungen
Trainingsformulierungen sind Beispielsätze für das, was der Endnutzer sagen könnte. Wenn der Endnutzerausdruck einer dieser Sätze ähnelt, stimmt Dialogflow mit dem Intent überein. Jedoch muss man nicht jede denkbare Formulierung angeben, da das integrierte maschinelle Lernen die Liste automatisch um ähnliche Äußerungen erweitert.
2. Parameter
Parameter sind die aus dem Benutzerausruck extrahierten Werte (in der ersten Trainingsformulierung wären es “garlic” und “salt”). Jeder Parameter hat einen Entitätstyp (siehe Abschnitt Entities; hier “Ingredients”). Der bzw. die Parameter werden je nach Intent an den passenden Endpoint geschickt. Aus dem von der Meal API zurückgeschickten Datensatz formt man dann eine Antwort, die dem User ausgegeben wird.
3. Antwort
Bei den meisten Intents konstruieren und formatieren wir die Antwort im Inline-Editor, wo wir mit dem von der API zurückgeschickten Datensatz direkt weiterarbeiten können. Bei Smalltalk bezogenen Intents, die keine API-Anfrage benötigen, wird die Antwort per GUI angegeben:
Man unterscheidet bei Antworten zwischen zwei Arten:
1.Standard-Antwort: Bot antwortet auf die Frage und ist anschließend bereit weitere unspezifische Fragen zu beantworten.
Bsp: Intent “random-meal” : Der User fragt, ob der Bot irgendein Rezept vorschlagen könnte -> Der Bot gibt direkt eins aus und ist bereit, andere Fragen zu beantworten.
2. follow-up-intents: Bot antwortet auf die erste Frage, erwartet dann aber vom User weitere Eingaben, die sachlich auf die erste Frage aufbauen.
Bsp: alle “filter-by” Intents: Der User fragt beispielsweise nach chinesischem Essen, woraufhin der Bot ihm 3 Rezepte vorschlägt und er vom User erwartet, eins auszuwählen.
Entities
Entities werden verwendet, um bestimmte Informationen herauszufiltern, die der Benutzer erwähnt. Sie nehmen nützliche Daten auf und stellen sie zur weiteren Verarbeitung bereit. Man unterscheidet zwischen zwei Arten:
1.System entities, die bereits in Dialogflow enthalten sind und mit denen der Agent ohne zusätzliche Konfiguration Informationen zu einer Vielzahl von Konzepten extrahieren kann.
Bsp: Dialogflow erkennt direkt eine Zeitangabe, ein Datum oder eine Zahl (sys.time, sys.date, sys.number)
2. Developer entities definiert der Entwickler anhand seines individuellen Datensatzes selbst. Bezüglich der Parameter, die wir an die Endpoints der Meal API schicken, haben wir folgende Entities definiert:
Entity Dilemma
Um eine Benutzereingabe richtig zu verstehen und die passenden Parameter herauszufiltern, muss Dialogflow die zugehörigen Entitys kennen. Denn ist ein Entity vorher nicht bekannt, kann Dialogflow dies auch nicht erkennen und somit eine Eingabe auch nicht dem passenden Intent zuweisen. Sollte der Benutzer zum Beispiel eingeben: „I want to cook pancakes.“, Dialogflow aber nicht weiß was pancakes sind, kann er damit nichts anfangen. Aus diesem Grund mussten wir alle möglichen Gerichte, Länder, Kategorien und Zutaten als Entitys abspeichern. Bei den Kategorien und Ländern ging dies auch relativ schnell. Bei dem Entity „AreaAdjective“ mussten wir ein Adjektiv herausfiltern, da der API Endpoint nur diese akzeptiert hat. So funktioniert zum Beispiel eine Abfrage mit „italian“, wohingeben „Italy“ keine Ergebnisse liefert. Durch das Hinzufügen von Synonymen konnten wir das Problem aber schnell lösen, denn so wird „Italy“ automatisch zu „italian“ umgewandelt. Bei den Gerichten und Zutaten hat es sich aber ein bisschen schwieriger gestaltet. Denn es gibt keinen Endpoint bei der MealDB, mit welchem man alle vorhandenen Rezepte abfragen kann, man kann lediglich alle Rezepte mit einem bestimmten Anfangsbuchstaben abfragen. Deshalb haben wir uns eine kleine Hilfsfunktion erstellt, die in einer Schleife Abfragen mit allen möglichen Buchstaben macht. Die einzelnen Zwischenergebnisse haben wir dann in ein Array gepushed, sodass wir zum Schluss alle möglichen Rezepte zusammen hatten. Diese Rezepte haben wir uns dann ausgeben lassen über die Konsole und als CSV Format in dem Entity „Ingredients“ hochgeladen. Dazu der passende Code, der vielleicht nicht der schönste ist, aber funktioniert hat.
Die Zutaten konnten wir zwar alle über einen Endpoint abfragen, da es aber viel zu vielen waren, um sie von Hand als Enitiy einzugeben, haben wir uns dafür auch eine Hilfsfunktion erstellt. Mit dieser Funktion haben wir die Abfrage formatiert und wieder ausgegeben, sodass wir alle Zutaten auf einmal im CSV Format hochladen konnten. Der Code dazu funktioniert ähnlich wie der obere.
Kontext
Schon mal an einer regen Diskussion von Fremden vorbeigelaufen und Phrasen aufgeschnappt, die für einen als Außenstehender überhaupt keinen Sinn ergeben haben?
So wie Menschen einen Kontext brauchen, um herauszufinden, was ein bestimmtes Wort oder bestimmter Satz bedeutet, benötigen auch Chatbots sie. Ohne Kontext könnte das Wort bzw. der Satz absolut keine Bedeutung haben oder etwas völlig anderes bedeuten als die ursprüngliche Absicht des Benutzers
Unser persönliches (philosophisches) Kontext-Dilemma
Wie bei den Intents oben bereits kurz skizziert, ist unser Rezept-Vorschlag-Konzept wie folgt aufgebaut: Der Benutzer fragt idealerweise nach einem Rezept basierend auf einer Länderküche, Essenskategorie etc. Darauffolgend schlägt der Agent ihm bis zu 3 Rezepte vor, von denen sich der Benutzer für eins entscheiden kann. Wie man bereits schon ahnen könnte, ist hier ein Kontext erforderlich, damit der Chatbot Aussagen wie “Option 2”,“Ich will das erste” auch zuordnen kann.
Mit voller Motivation haben wir für jeden Intent einen follow-up intent zusammengeklickt, der genau diese Auswahl anhand der Nummerierung der vorgeschlagenen Rezepte erkennen sollte. Somit wurde logischerweise für jeden Intent ein eigener Output-Kontext erstellt, der bei dem jeweiligen follow-up Intent als Input-Kontext wieder eingespeist wurde. Schnell wurde uns beim Code-Schreiben bewusst, dass eigentlich jeder follow-up intent die selbe Logik implementiert, und zwar zu erkennen welches Rezept ausgewählt wurde und dieses formatiert an den User zurückzugeben. Die Idee schlich sich herein, alle individuellen Kontexte der Intents in einen zusammenzufassen und diesen einen bequem in einer Funktion zu verwalten und für alle follow-up intents aufzurufen. Wir gaben der Idee nach, da alles nach unseren Erwartungen richtig ausgegeben wurde. Trotzdem fühlt sich die Lösung nicht ganz “clean” an, da sprachwissenschaftlich gesehen eigentlich jeder Intent vom Inhalt her einen eigenen Kontext verdienen sollte: Ob man gerade nach einer Essenskategorie oder Länderküche gefragt hat, sollte in einer echten Konversation schon einen Unterschied machen. Programmatisch hat es uns die Sache jedoch wesentlich erleichtert.
API Routen
axios.get(“https://www.themealdb.com/api/json/v1/1/categories.php “)
Auflisten aller meal categories. Wird genutzt, um die Beschreibung einer vom Endnutzer eingegebenen Kategorie herauszufiltern und auszugeben.
axios.get(“https://www.themealdb.com/api/json/v1/1/random.php“)
Ausgabe des Rezeptes eines random meals.
axios.get(“https://www.themealdb.com/api/json/v1/1/filter.php”, { params: { a: inputArea } })
Filtern der Gerichte nach einem vom Endnutzer eingegeben Land. Wird genutzt um drei random Gerichte dieses Landes als Optionen dem Nutzer zur Verfügung zu stellen
axios.get(“https://www.themealdb.com/api/json/v2/9973533/filter.php”, { params: { i: paramsIngredients } })
Filtern der Gerichte nach einer oder mehreren vom Endnutzer eingegeben Zutaten. Wird genutzt um drei random Gerichte mit diesen Zutaten als Optionen dem Nutzer zur Verfügung zu stellen.
axios.get(“https://www.themealdb.com/api/json/v1/1/search.php”, { params: { s: inputMeal } })
Filtern der Gerichte nach einem vom Endnutzer eingegeben Gericht. Wird genutzt um bis zu drei Arten dieses Gerichtes als Optionen dem Nutzer zur Verfügung zu stellen.
axios.get(“https://www.themealdb.com/api/json/v1/1/filter.php”, { params: { c: inputCategory } })
Filtern der Gerichte nach einer vom Endnutzer eingegeben Kategorie. Wird genutzt um bis zu drei Optionen aller Gerichte aus dieser Kategorie als Optionen dem Nutzer zur Verfügung zu stellen.
axios.get(“https://www.themealdb.com/api/json/v2/9973533/lookup.php”, { params: { i: mealId } })
Suche nach einem bestimmten Gericht durch den Parameter id und Ausgabe des Rezeptes.
Beispiel Cloud Function for Firebase – FilterByArea:
In der Funktion wird als Parameter der Webhook Client agent übergeben. Über diesen bekommt die Variable InputArea als Wert den Parameter des Entitätstyps “AreaAdjective”, der vom Nutzer eingeben und von Dialgogflow erkannt und extrahiert wurde. Mit Hilfe der Bibliothek axios wird auf die MealDB zugegriffen. Dabei wird die URL mit inputArea übergeben. Als Antwort kommen bei res nun die Daten aus der MealDB zurück. Diese werden an die Funktion handleResult() übergeben, welche die Daten filtert. Letztendlich werden die gefilterten Daten über agent.add() zur Antwort, die dem Endnutzer dann angezeigt wird.
Chatbot in Action
1. Intents und Entitäten: Die Nutzereingabe wird mit Hilfe der Trainingssätze und dem Parameter mit dem Entitätstyp dem passenden Intent zugeordnet, dem “filter-by-ingredients” Intent. Somit kommt die passende Antwort zurück. Die Nutzereingabe carrot wird als Parameter “Carrots” des Entitätstypen “ingredients” erkannt. Dialogflow kann dies zuordnen, auch mit anderer Groß – und Kleinschreibung bzw. Ein – oder Mehrzahl.
2. FollowUpIntent: Option 0: Dialogflow erkennt die 0 als passend zu den Trainingssätzen vom Intent “filter-by-ingredients – zero”. Dieser ist ein FollowUp Intent auf “filter-by-ingredients”.
3. Kontext: Die Nutzereingabe wurde dem “filter-by-area” Intent mit dem Parameter “areaAdjective” = thai zugeordnet. Außerdem wurde der Kontext “user-data” hergestellt. Der Nutzer gibt nun die Option 2 an und der Chatbot befindet im FollowUpIntent “filter-by-area – options” mit dem Parameter value = 2. Das passende Rezept wird ausgegeben. Gibt der Nutzer nun die Option 3 ein, so wird wieder das passende Rezept ausgegeben. Der Chatbot kann die 3 deswegen immer noch als number Parameter des FollowUpIntent “filter-by-area – options” erkennen, da er sich noch im Kontext “user-data” befindet. Für solche Zusammenhänge ist also der Kontext nötig.
4. Problem: Lasagna Problem: Unter der Option Lasagna kann der Chatbot nichts finden. Unter der Option Lasagna Sandwich jedoch schon. Die Erwartung des Endnutzers könnte allerdings sein, bei der Suche nach Lasagna alle Rezepte die Lasagna im Namen haben zu finden, u.a. Lasagna Sandwich. Die Meal Optionen im Chatbot sind allerdings an die in der MealDB gebunden und in der MealDB gibt es kein Gericht Lasagna. Demnach gibt es in der “Meal” Entitäten Liste auch keine Lasagna. Dialogflow sucht die Entitäten nach Lasagna ab, kann aber keinen genauen Treffer finden und den Wortteil “Lasagna” aus “Lasagna Sandwich” nicht extrahieren und als richtig erkennen. Der Nutzer muss also den gesamten Namen des Gerichts angeben.
Testing
Zum Testing haben wir leider nur wenig gefunden, vor allem zu automatischen Tests, um den Dialogflow als Ganzes zu testen. Zwar gibt es in der Dialogflow GUI die Möglichkeit einzelne Sätze zu testen, wobei auch der zugehörige Intent, Context sowie die extrahierten Parameter angezeigt werden, was auch sehr hilfreich zum Debuggen war. Der nebenstehende Screenshot zeigt, wie dies dann aussieht. So werden zum Beispiel unter dem Punkt „Contexts“ die passenden Contexte angezeigt, da es sich um einen follow-up Intent handelt. Außerdem wurde der passende Intent „filter-by-area“ zugeordnet, sowie der Parameter „italian“. Unter dem Punkt „Diagnostic Info“ kann man noch zusätzlich die JSON Anfrage und Antwort vom Webhook anschauen.
Jedoch kann man in der Google Cloud Console die Cloud Function testen, wie der untenstehende Screenshot zeigt.
Dazu kopiert man die JSON Webhook Anfrage aus der obigen „Diagnostic Info“ und startet den Test. Wie man sehen kann, funktioniert die Anfrage und die passende fulfillmentMessage mit der MealDB Antwort kommt zurück. Außerdem werden darunter auch die Logs angezeigt.
Unser Fazit mit Dialogflow
Vorteile
- Dialogflow basiert auf einem sehr intuitiven Konzept; Intents, Entities etc. versuchen die Charakteristiken der natürlichen Sprache nachzustellen, sodass es einem leicht fällt die Funktionsweise eines Chatbots nachzuvollziehen.
- Die GUI ist sehr einfach und simpel gehalten; man konnte sich auch ohne Hilfe der Dokumentation schnell zurechtfinden.
- Da die KI bereits zur Verfügung gestellt wird, sind keine tieferen Kenntnisse in dem Gebiet notwendig.
- Der 90 Tage Testzeitraum war mit circa 300$ Guthaben mehr als ausreichend für einen Chatbot in unserem Rahmen
Nachteile
- Der größte Nachteil von Dialogflow war das sehr lange Deployen der cloud functions; ein Durchgang hat circa drei Minuten gedauert. Von daher war es ziemlich nervig, wenn auf Anhieb was nicht funktioniert hat und man bei jedem Rumprobieren, selbst wenn man nur eine Kleinigkeit geändert hat, drei Minuten warten musste. Bei einem tiefergehenden/umfangreicheren Problem, dass von mehreren Codestellen verursacht wurde, kann man sich die Frustration bei dem Fixen mit dieser Wartezeit wahrscheinlich gut vorstellen.
- Alle Funktionen mussten in einer einzigen file geschrieben werden. Es kam nicht selten vor, dass paar von uns parallel an der Datei gearbeitet haben, sodass wenn einer das Deployen gestartet hat, die Änderung des anderen verworfen wurden. Wenn eine Person das Deployen gestartet hat, konnten die anderen währenddessen auch nichts abspeichern, da es sofort gecancelled wurde.
- Der Inline-Editor ist eher rudimentär aufgebaut und weit von einer vollwertigen IDE entfernt. Es fängt bei banalen Kleinigkeiten an, wie dass man nicht erkennt, welche öffnende Klammer zu welcher schließenden gehört. Aber auch, dass keine Liste an verfügbaren Funktionen vorgeschlagen wird und dass viele Syntaxfehler toleriert werden, die beim Deployen aber zu Errors führen.
- Die Dokumentation von Dialogflow ist unvollständig und bleibt sehr oberflächlich. Wir haben viel Stackoverflow benutzt aber mussten letztendlich auch viel Herumprobieren.
Ausblick
Rezepte
Unser Zeitplan ist gut aufgegangen. Da unser Zeitraum – u.a. wegen der limitierten Zeit der Testversion – jedoch begrenzt war, konnten wir nicht alle erdenklichen Intents in den Chatbot einbauen. Eine Möglichkeit den Chatbot zu erweitern wäre beispielsweise das Erstellen eines dritten FollowUpIntents auf die FilterBy Intents. Dieser würde daraus bestehen, dass der Endnutzer die Option wählt, drei weitere random Meals heraus zu filtern, sofern es genug weitere Gerichte gibt. Im Code müsste ein Teil eingebaut werden, der garantiert, dass die ersten drei Gerichte sich nicht wiederholen und nicht noch einmal mit ausgegeben werden. Der Intent würde außerdem voraussetzen, dass vorher mindestens drei Gerichte ausgegeben wurden.
Webhook
Um Code nach der Zuordnung zu einem passenden Intent auszuführen, haben wir in Dialogflow unter dem Punkt „Fulfillment“ den Inline-Edior benutzt. Dort gibt es eine index.js und eine index.json, in welchen der gesamte benötigte Code erstellt wird. Der Code wird dann automatisch in Form von einer Cloud Function bereitgestellt und wenn nötig getriggert. Wie aber bereits erwähnt, hat sich das Arbeiten an einer einzigen Datei schwer schwierig gestaltet und das lange Deployen die Arbeit zusätzlich erschwert. Deshalb wäre es bei größeren Projekten mit mehr Intents durchaus eine Überlegung wert, den Webhook selbst zu hosten und die JSON Anfragen zu verarbeiten. So hätte man mehr Kontrolle und kann außerdem besser zusammenarbeiten und debuggen, da das lange Deployen entfällt. Um selbst den Webhook zu hosten, muss dieser ein paar Anforderungen erfüllen:
· Der Webhook muss HTTPS POST Anfragen verarbeiten, HTTP wird nicht unterstützt
· Die URL für Anfragen muss öffentlich zugänglich sein
· JSON Anfragen müssen verarbeitet werden und eine JSON Antwort muss zurückkommen
Leave a Reply
You must be logged in to post a comment.