In diesem Dokument wird beschrieben wie der Chatbot funktioniert und wo was zu finden ist, damit zukünftige Entwickler ohne Probleme den Chatbot weiterentwickeln können. Dieses Dokument wird in drei Hauptteile aufgeteilt. Einmal allgemein, der Chatbot an sich und den Adminbereich.
In diesem Abschnitt des Dokumentes beschreiben wir das Projekt im Allgemeinen. Wir zählen auf was für Technologien wir verwenden und wie man verschiedene Dinge macht, wie zum Beispiel wie man eine Datenbankverbindung aufbaut. Zudem schreiben wir noch auf was wir machen wollten, aber wegen zu weniger Zeit nicht machen konnten.
In diesem Abschnitt zählen wir die einzelnen Technologien auf damit klar ist was benötigt wird um starten zu können.
Für mehr Informationen zum Abschnitt Datenbank Abfragen gehen.
Wir haben die Java
Klasse com/ubs/backend/util/PrepareString.java
erstellt. In ihr haben wir unterschiedliche Methoden für das vorbereiten von Strings damit sie korrekt und ohne Gefahren
in der Datenbank gespeichert werden können oder an den Benutzer geschickt werden können.
Folgende Methoden hat die Klasse:
Diese Methode bereitet den String vor. Sie akzeptiert folgende Parameter:
String stringint maxStringlengthboolean removeAllSpecialCharsboolean toLowerCaseremoveQuestionMarkDer erste Parameter ist ganz einfach der String, also z.B. die Benutzereingabe beim Chatbot, welcher vorbereitet werden soll.
Der zweite Parameter definiert wie lange (Anzahl Zeichen) der String maximal sein darf. Wenn der gegebene String länger ist als dieser Parameter definiert, wird alles danach weggenommen. Für mehr Informationen gehe zu shortenString.
Der dritte Parameter ist ein boolean und definiert ob spezielle Charaktere entfernt werden sollen. Unter speziellen Charakteren meinen wir besonders HTML code. Dadurch verhindern wir Hackerangriffe.
Der vierte Parameter ist ein boolean und definiert ob der Text in Kleinbuchstaben umgewandelt werden soll. Wenn der boolean false ist bleibt der Text so wie er ist.
Der fünfte Parameter ist ein boolean und definiert ob alle vorkommenden Fragezeichen entfernt werden sollen oder nicht.
Diese Methode verkürzt den angegebenen String auf die gegebene Länge.
Ein Beispiel:
String text="Hallo";
String shortenedText=shortenString(text,3);
System.out.println(preparedText);
Die Ausgabe wäre dann Hal, die Länge des Textes wurde also von 5 auf 3 verkürzt da wir die maximale Länge auf 3 gesetzt haben.
Diese Methode überprüft einfach ob der angegebene String länger ist als die angegebene maximale Länge.
Gibt true zurück wenn der String länger ist und false wenn der String kürzer oder gleich lang ist.
Wir haben eine Java Enum Datei namens com/ubs/backend/classes/enums/DataTypeInfo.java.
In dieser Datei haben wir die unterschiedlichen Datentypen definiert, wie zum Beispiel ANSWER_TEXT.
Alle Enums in dieser Datei besitzen einmal eine maxLength und einen name.
maxLength definiert die maximale grösse des Types und name definiert einfach wie man es im Frontend anzeigen kann.
Als Beispiel kann man da den Charcounter nehmen. Dort wird über einen Service die maximale Anzahl an Zeichen pro Input Feld geladen.
In unserem Projekt verwenden wir Hibernate für die Datenbank verbindung und alle Abfragen. Für das connection pooling und die sicherstellung dass connections wieder geschlossen werden benutzen wir C3PO.
Im pom.xml haben wir Hibernate eingebunden, wir verwenden die Version 5.4.29.
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.29.Final</version>
</dependency>
</dependencies>
Hibernate wird dazu verwendet in den verschiedenen Dialekten, ohne Probleme eine verbindung zur Datenbank zu gewährleisten. Dies ermöglichte es, ohne viel Code zu ändern in verschiedenen Umgebungen zu arbeiten.
So konnten wir zum Beispiel lokal auf unseren Rechner MySQL verwenden während wir auf unserer Testumgebung, Heroku, PostgreSQL und auf unserer Production in der UBS Umgebung Microsoft SQL Server verwenden.
Um die Verbindung zur Datenbank einzustellen, müssen folgende Stellen im Persistence abgeändert werden.
<properties>
<property name="hibernate.dialect" value=""/>
<property name="javax.persistence.jdbc.url" value=""/>
<property name="javax.persistence.jdbc.user" value=""/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.driver" value=""/>
</properties>
In der ersten Linie konfiguriert man den Dialekt. Also welche Datenbank Sprache man verwendet. Eine Liste mit möglichen Dialekten findet man hier.
Ein Beispiel für MySQL 8 wäre folgend.
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect"/>
In der zweiten Linie konfiguriert man die URL. Die URL erklärt Hibernate wo die Datenbank zu finden ist und noch einige weitere optionale Informationen kann man hinzufügen. Eine URL ist immer ein wenig anders, abhängig von eurem gewählten Dialekt. Euer Datenbank Anbieter sollte im Normalfall direkt die korrekte URL anzeigen, welche ihr nur noch kopieren müsst.
Ein Beispiel für eine Verbindung zu einer lokalen MySQL Datenbank.
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/db"/>
In den nächsten zwei Linien konfiguriert man den Benutzer, mit welchem man die Datenbank verwenden will. Zuerst schreibt man den Namen des Users und danach das Passwort. Falls der Benutzer kein Passwort hat, kann man es einfach leer lassen.
Bedenke das Hibernate nur sachen machen darf welche der Benutzer darf. Wenn also der Benutzer nur leserechte hat, kann hibernate auch nur lesen.
Ein Beispiel für einen Benutzer ohne Passwort.
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value=""/>
In der letzten Linie definiert man den Treiber mit welchem Hibernate die Abfragen zur Datenbank macht. Im normalfall gibt es für jede Datenbank einen offiziellen Treiber. Sobald ein geeigneter Treiber gefunden wurde diesen Herunterladen und in das Projekt einbinden, bevorzugt ist es hier das über Maven zu machen. Einfach ein Dependency in das pom.xml tun.
Ein Beispiel für das Einbinden eines MySQL Treibers.
<property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
Das fertige Persistence würde jetzt also wie folgt aussehen.
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/db"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
</properties>
Nach der Konfiguration der Verbindung zur Datenbank haben wir noch zwei weitere Linien
<property name="hibernate.show_sql" value=""/>
<property name="hibernate.hbm2ddl.auto" value=""/>
show_sql kann einen Wert von entweder true oder
false nehmen.
Wenn man diesen auf true setzt, gibt Hibernate alle SQL Befehle in die Konsole aus.
hbm2ddl.auto definiert die Art und Weise wie Hibernate mit Änderungen in der Datenbank umgehen soll.
Folgende Werte können verwendet werden.
validate: Validiert das Schema, macht keine Änderungen an der Datenbankupdate: Fügt neue Tabellen und Spalten hinzu, löscht aber nie etwas.create: Löscht immer das ganze Schema zuerst und erstellt es dann neu.create-drop: Zuerst gleich wie create, danach löscht es das ganze Schema sobald das
SessionFactory Objekt geschlossen wurde. Typischerweise, sobald das Programm beendet wird.none: Macht nichts.Es wird empfohlen in einer Testumgebung update zu verwenden und in der Production none zu
verwenden.
Mehr zu Hibernate allgemein kann man hier finden.
C3PO ist ein Tool, mit welchem man Connection-pooling besser unterstützen kann als mit Hibernate selber.
Mit den Linien 21 - 23 können wir den pool konfigurieren.
<property name="hibernate.c3po.min_size" value="10"/>
<property name="hibernate.c3p0.max_size" value="100"/>
<property name="hibernate.c3p0.acquire_increment" value="5"/>
Die erste Linie definiert wie viele connections es im pool mindestens hat, die zweite wie viele es maximal haben kann. Mit der dritten Linie geben wir an um wie viel sich die aktuelle grösse des pools erhöht.
Beispiel.
Wir haben im Moment 10 verbundene connections und eine weitere möchte sich verbinden, increment ist auf 5
gestellt, das bedeutet die aktuelle grösse des pools wäre jetzt 15.
<property name="hibernate.c3p0.unreturnedConnectionTimeout" value="30"/>
<property name="hibernate.c3po.debugUnreturnedConnectionStackTraces" value="true"/>
Die erste Linie definiert, wie lange eine Connection im Ganzen am Leben sein kann. In der aktuellen Konfiguration heisst
das also das jede Verbindung maximal 30 Sekunden überlebt.
Wenn man also eine Methode oder eine Abfrage ausführt,
die länger braucht, kann es sein das diese nicht vollendet werden kann, in diesem Fall die Zeit höher stellen.
Bevor man etwas mit der Datenbank machen kann brauchen wir eine Verbindung zu ihr. Hier ist wichtig das zuerst die Persistence richtig konfiguriert wurde damit wir auch die korrekte Datenbank ansprechen. Mehr dazu hier.
Danach müssen wir dort wo wir eine Verbindung brauchen (z.B um eine Abfrage zu machen) folgenden Code schreiben:
EntityManager em = Connector.getInstance().open();
em.getTransaction().begin();
Dadurch öffnen wir eine Verbindung zur Datenbank und mit der gerade geholten Instanz (bei uns heisst sie em) können
wir nun Abfragen bei der Datenbank machen.
Sehr wichtig ist das diese Verbindung auch wieder geschlossen wird! Und zwar direkt dann wenn man sie nicht mehr braucht.
Falls die Verbindung nicht geschlossen wird oder einfach zu lange offen ist können wir andere Anfragen blockieren da
irgendwann unser Pool an Connections voll ist.
Um die Verbindung wieder zu schliessen brauchen wir einfach folgende zwei Zeilen code.
em.getTransaction().commit();
em.close();
Wir haben für Interaktionen mit der Datenbank eine generische Java Klasse namens
DAO, zu finden in
src/main/java/com/ubs/backend/classes/database/dao/DAO.java
. Diese Klasse wird für standard Befehle in der Datenbank wie z.B.
select
,
insert
und
remove
.
Für weitere Befehle, welche Klassenspezifisch sind haben wir jeweils ein eigenes DAO für die Datenklasse erstellt.
Wenn man eine Abfrage mit der Tabelle Answers interagieren möchte erstellt man eine Instanz der Klasse
AnswerDao
und verwendet ihre Methoden. Als Beispiel, wenn man alle Antworten aus der Datenbank holen möchte würde das wie folgt
aussehen.
AnswerDAO answerDAO=new AnswerDAO();
List<Answer> answersFromDB=answerDAO.select();
Um das Programm zu Testen möchte man vermutlich eine Datenbank mit Testdaten haben. Für diesen Zweck haben wir die Datei
com/ubs/backend/demo/CreateDB.java für das tatsächliche befüllen der Datenbank und die Datei
com/ubs/backend/demo/DBData.java mit den Daten die wir in die Datenbank laden wollen.
Wir empfehlen vor dem Ausführen der Datei CreateDB die
Persistence konfiguration zu überprüfen und womöglich zu ändern. Auf folgendes sollte geachtet werden.
hbm2ddl.auto auf update oder </code> create? [1][1] Wir empfehlen es auf create zu stellen, da es zuerst alles löscht und danach neu erstellt.
Mit update kriegt man doppelte Daten.
[2] In C3PO haben wir eine Zeit definiert mit welcher wir bestimmen wie lange eine einzelne Verbindung zur Datenbank bestehen kann. Bei Online Datenbanken kann es teilweise länger dauern diese zu befüllen, als was wir der Verbindung Zeit geben. Falls das der Fall ist, müssen wir C3PO kurz anpassen. Gehe dazu zum Abschnitt C3PO konfigurieren - Debug.
Wenn das Persistence entsprechend angepasst wurde, kann man jetzt
CreateDB ausführen und warten bis es fertig ist mit dem befüllen der Datenbank.
In der Datei com/ubs/backend/demo/DBData.java sind alle Testdaten welche nachher in die Datenbank
gespeichert werden. In dieser Datei müssen jetzt nur noch Inhalte gelöscht, bearbeitet oder hinzugefügt werden.
Ein Beispiel, die Adresse zum Chatbot sieht wie folgt aus:
http://localhost:8080/chatbot/ . In diesem Fall ist
chatbot die Adresse des Servers.
Um den Server zu ändern, muss einmal in JavaScript sowie in Java bei jeweils einer Datei etwas geändert werden.
In der Datei src/main/webapp/assets/js/utilities/variables.ts einfach die Variable server
abändern.
Im Beispiel von vorhin würde es so aussehen:
Vorher:
const server = window.location.origin;
Nachher:
const server = window.location.origin + "/chatbot";
In der
Datei com/ubs/backend/util/Variables.java
einfach den
String serverDirectory
anpassen.
Im Beispiel von vorhin würde es wie folgt aussehen:
Vorher:
public class Variables {
public static String serverDirectory = "";
}
Nachher:
public class Variables {
public static String serverDirectory = "/chatbot";
}
Nachdem die Anpassungen vorgenommen wurde, sollte der Chatbot ohne Probleme laufen.
Im Chatbot gibt es verschiedene Antworttypen sowie 2 verschiedene Tagtypen. In diesem Abschnitt erklären wir was der Unterschied zwischen diesen Typen ist und wie sie funktionieren.
Wir haben unterschiedliche Typen von Antworten für unterschiedliche Zwecke.
Einige Typen werden durch Code generiert und sind dadurch dynamisch, während andere von der Datenbank geholt werden.
Einige Typen teilen sich tags pro Antwort während andere Typen pro Antwort andere Tags haben.
Im Chatbot verwenden wir folgende Antworttypen:
Eine kurze Zusammenfassung der Typen:
| Default | Joke | Facts | Statistics | Error | |
|---|---|---|---|---|---|
| Generierte Antworten | Nein | Nein | Nein | Ja | Ja |
| Antworten aus der Datenbank | Ja | Ja | Ja | Nein | Nein |
| Gruppierte Tags | Nein | Ja | Ja | Ja | Nein |
| Nutzer kann bearbeiten | Ja | Ja | Ja | Nein | Nein |
| Ist standard versteckt | Nein | Ja | Ja | Ja | Ja |
| Ist erzwungen versteckt | Nein | Ja | Ja | Ja | Ja |
| Was | Ist so |
|---|---|
| Generierte Antworten | Nein |
| Antworten aus der Datenbank | Ja |
| Gruppierte Tags | Nein |
| Nutzer kann bearbeiten | Ja |
| Ist standard versteckt | Nein |
| Ist erzwungen versteckt | Nein |
Der Default
Typ ist der normale/standard Typ. Alle Antworten mit diesem Typen haben ihre eigenen Tags.
Weitere Antworten können von Administratoren hinzugefügt werden.
Antworten mit diesem Typen sind standardmässig nicht versteckt, können aber versteckt werden.
| Was | Ist so |
|---|---|
| Generierte Antworten | Nein |
| Antworten aus der Datenbank | Ja |
| Gruppierte Tags | Ja |
| Nutzer kann bearbeiten | Ja |
| Ist standard versteckt | Ja |
| Ist erzwungen versteckt | Ja |
Der Joke
Typ ist gedacht für Witze. Alle Antworten mit diesem Typen teilen sich die Tags.
Alle Antworten mit diesem Typen sind standardmässig versteckt und müssen versteckt bleiben.
Weitere Antworten können von Administratoren hinzugefügt werden.
| Was | Ist so |
|---|---|
| Generierte Antworten | Nein |
| Antworten aus der Datenbank | Ja |
| Gruppierte Tags | Ja |
| Nutzer kann bearbeiten | Ja |
| Ist standard versteckt | Ja |
| Ist erzwungen versteckt | Ja |
Der Facts
Typ ist gedacht für Fakten über den Chatbot, kann aber auch für andere Arten von Fakten verwendet werden. Alle Antworten
mit diesem Typen teilen sich die Tags.
Alle Antworten mit diesem Typen sind standardmässig versteckt und müssen versteckt bleiben.
Weitere Antworten können von Administratoren hinzugefügt werden.
| Was | Ist so |
|---|---|
| Generierte Antworten | Ja |
| Antworten aus der Datenbank | Nein |
| Gruppierte Tags | Ja |
| Nutzer kann bearbeiten | Nein |
| Ist standard versteckt | Ja |
| Ist erzwungen versteckt | Ja |
Der Statistics
Typ ist gedacht für Statistiken über den Chatbot, kann aber auch für andere Arten von Statistiken verwendet werden. Alle
Antworten mit diesem Typen teilen sich die Tags.
Alle Antworten mit diesem Typen sind standardmässig versteckt und müssen versteckt bleiben.
Alle Antworten mit diesem Typen werden vom Chatbot selber generiert.
Es können keine weiteren Antworten von Administratoren über das Admintool hinzugefügt werden. Um weitere Antworten
generieren zu können, muss man weitere Antwortmöglichkeiten zum Code hinzufügen. Dazu muss
der ‘handler’ der
Antwort angepasst und bearbeitet werden.
Im Moment generieren wir zwei Arten von Statistiken:
Für jede generierte Antwort holen wir die entsprechenden Daten aus der Datenbank und generieren daraus eine Antwort.
| Was | Ist so |
|---|---|
| Generierte Antworten | Ja |
| Antworten aus der Datenbank | Nein |
| Gruppierte Tags | Nein |
| Nutzer kann bearbeiten | Nein |
| Ist standard versteckt | Ja |
| Ist erzwungen versteckt | Ja |
Der Error
Typ ist gedacht für Fehler beim Versenden oder suchen einer passenden Antwort. Diese Antwort sollte keine Tags besitzen
da diese nie gebraucht werden. Dieser Typ wird nur zum Benutzer zurückgeschickt, wenn der Chatbot keine passende Antwort
findet oder es einen Fehler gibt.
Diese Antwort generiert keine Statistiken
Alle Antworten mit diesem Typen werden vom Chatbot selber generiert.
Es können keine weiteren Antworten von Administratoren über das Admintool hinzugefügt werden. Um weitere Antworten
generieren zu können, muss man weitere Antwortmöglichkeiten zum Code hinzufügen.
Unsere Tags werden in zwei Arten aufgeteilt.
Der Unterschied zwischen den beiden Typen ist einfach: Gehört der Tag zu einer einzelnen Antwort ist er ein
Result, aber gehört der Tag zu einer Gruppe von Antworten so ist er ein TypeTag. Je nach Typ werden
die Tags in einer anderen Tabelle gespeichert.
Result wird in der Tabelle results gespeichert. TypeTag in typetag.
Antworten mit dem Typen Default
haben Tags mit dem Typen Result weil die Antwort nicht gruppierte Tags hat. Allerdings Antworten mit zum
Beispiel dem Typen
Joke haben Tags mit dem Typen TypeTag
Der Unterschied in der Datenbank sieht wie folgt aus.
| Result | TypeTag | |
|---|---|---|
| Upvotes | Ja | Ja |
| Downvotes | Ja | Ja |
| Anzahl Verwendungen | Ja | Ja |
| Referenz zu Tag | Ja | Ja |
| Referenz zu Antwort | Ja | Nein |
| AntwortTyp | Nein | Ja |
Wie man also sehen kann, Result braucht nicht zu wissen was für ein Typ die Antwort ist, da wir eine
direkte referenz zu der Antwort haben und direkt von ihr diese Information beziehen können.
TypeTag hat diese Referenz nicht weswegen wir den Typen abspeichern müssen. Dadurch können wir im Code alle
Antworten welche gruppierte Tags haben nehmen und mit der Tabelle vergleichen. Dann nehmen wir alle Tags mit demselben
Typen und wissen genau welche Antwort welche Tags hat.
Hier werden Ideen aufgelistet, welche wir noch für den Chatbot hatten aber nicht genug Zeit hatten sie zu realisieren.
Statistics, dadurch
ist es recht langweilig und oft werden dieselben Antworten noch einmal geschickt. Mehr unterschiedlich generierte
Antworten währen von Vorteil. (Siehe Statistics).[1] neu = seit dem letzten login des jeweiligen Benutzers neu, davor wurde diese Frage noch nicht vorgeschlagen.
In diesem Abschnitt des Dokumentes beschreiben wir wie der Chatbot funktioniert. Der Chatbot ist die Hauptfunktion dieses Projektes. Es ist die erste Seite die ein Admin sieht und die einzige Seite die ein normaler Benutzer sehen soll.
Folgend wird beschrieben was beim laden der Seite in welcher Reihenfolge passiert.
Wenn der Chatbot geöffnet wird wird als erstes eine Test Abfrage zum Server geschickt und auf seine Antwort gewartet.
Solange auf die Antwort gewartet wird, zeigt der Chatbot eine Nachricht an mit der Information das die “Verbindung zum
Server aufgebaut wird”.
Um den Status zu überprüfen verwenden wir unseren Service
getStatus()
. Diese Methode rufen wir über eine Rest Schnittstelle auf. Der Path zu dieser Schnittstelle ist
services/get/status.
getStatus()
ruft folgende weitere Methoden auf.
Der Aufruf der beiden Methoden ist in einem try catch Block. Sobald wir einen Fehler bekommen, wissen wir das wir ein Problem haben und kriegen einen schlechten Status. Wenn wir kein Fehler haben kriegen wir einen guten Status-
Bei einem guten Status ändern wir die Nachricht des Bots zu einer Begrüssung.
Danach geht es weiter.
Bei einem schlechten Status geben wir einen Fehler aus.
Das Textfeld bleibt deaktiviert und es passiert nichts mehr.
Wenn wir beim überprüfen des Status keinen Fehler bekommen haben laden wir die drei vorschlage Fragen. Diese holen wir über folgenden Fetch Befehl
let response = await fetch(
`${server}/services/get/questionSuggestions?amountQuestions=3`
);
Die Variable server ist unser gesetzter Servername. Mehr dazu im Abschnitt
Chatbot Server Adresse definieren. Dieser Service, den wir hier aufrufen, erwartet
den Parameter
amountQuestions. Dieser Parameter definiert wie viele Fragen wir laden wollen, in diesem Fall sind es 3. Der
Service ist hier zu finden:
com.ubs.backend.services.Get#questionSuggestions
. Im Service werden zuerst die aktuell im Monat best bewerteten Benutzer Fragen geholt.
List<TempAnsweredQuestionTimesResult> answeredQuestions=answeredQuestionTimesResultDAO.selectMonthlyOrderedByUpvotes(new StatistikTimes(new Date()),amountQuestions);
Übergeben wird dabei eine
StatistikTimes
welche dem jetzigen Datum entspricht und die vorab definierte Anzahl an Fragen. Der HQL Befehl für die Abfrage sieht wie
folgt aus:
List<AnsweredQuestionTimesResult> answeredQuestionTimesResults=em.createQuery("select new AnsweredQuestionTimesResult(aqtr.answeredQuestionStatistik, aqtr.answeredQuestionResult, sum(aqtr.upvote), sum(aqtr.downvote)) from AnsweredQuestionTimesResult aqtr "+
"where aqtr.answeredQuestionStatistik.statistikTimes.month.myDate = :month"+
" and aqtr.answeredQuestionStatistik.statistikTimes.year.myDate = :year "+
" and aqtr.answeredQuestionStatistik.answeredQuestion.isHidden = false"+
" group by aqtr.answeredQuestionStatistik.answeredQuestion"+
" order by sum(aqtr.upvote) desc",
AnsweredQuestionTimesResult.class)
.setParameter("month",month)
.setParameter("year",year).setMaxResults(max).getResultList();
Einfach erklärt sagen wir hibernate es soll eine neue Instanz der Klasse
AnsweredQuestionTimesResult
mit den aus der Datenbank geholten Daten erstellen. Die Daten bestehen einmal aus der
AnsweredQuestionStatistik
, der
AnsweredQuestionResult
, der Summe der Upvotes (Daumen hoch) sowie der Summe der Downvotes (Daumen runter) der Frage. Wir selektieren also alle
AnsweredQuestionTimesResult
bei welchen das Jahr und der Monat der selbe ist wie bei der
StatistikTimes
die wir übergeben und gruppieren dann alle gefundene Einträge mit der Frage an sich und sortieren sie nach den Upvotes.
Wir müssen dann nur noch den Monat und das Jahr sowie die maximal zu findenden Einträge setzen und schon fertig.
Es kann sein das es in der Datenbank nicht genügend Fragen hat, bei denen die Kriterien zutreffen. In diesem Fall werden
die fehlenden Plätzen mit den
Standardfragen
gefüllt.
Beispiel: In der Datenbank existiert nur eine passende Frage, wir wollen aber 3. In diesem Fall füllen wir die restlichen 2 Plätze mit zufällig ausgewählten Standardfragen.
Am Ende konstruieren wir noch ein JSON und geben dieses zurück. In Typescript wandeln wir dieses JSON dann um in HTML und zeigen es an.
Benutzer können jetzt auf die vorschlage Fragen drauf drücken und es wird als normale Frage behandelt.
Wenn wir beim überprüfen des Status keinen Fehler bekommen haben laden wir den Char counter. Der Char
counter ist einfach ein kleines Tool welches durch ein Fetch Befehl die maximal erlaubten Charakter für ein Eingabe Feld
lädt. Beim Chatbot sehen wir so wieviele Zeichen der Benutzer als Frage eingeben kann.

Der Fetch Befehl sieht wie folgt aus:
const response = await fetch(`${server}/services/get/maxInputLength`);
Der Service ist hier zu finden:
com.ubs.backend.services.Get#getMaxLength
.
Hier ist wie der Service funktioniert. Wir haben die Java Klasse
com/ubs/backend/classes/enums/DataTypeInfo.java
definiert. Diese Java Klasse ist ein Enum in welchem wir die verschiedenen Typen an Daten definiert haben. Zum Beispiel
haben wir
USER_QUESTION_INPUT
welcher wir für das Eingabe Feld des Chatbots verwenden. Jedes Enum hat zwei Werte. Einmal
maxLength
, welcher die maximale länge in Charakteren definiert und einmal
name
welches in TypeScript verwendet wird um herauszufinden wo diese Information gebraucht wird.
Im Service konstruieren wir jetzt noch ein JSON und geben dieses zurück. In TypeScript wird das JSON ausgelesen und in HTML umgewandelt.
Wenn ein Nutzer eine Nachricht abschickt, rufen wir einen Service auf welcher nach der am besten passenden Antwort
sucht.
Der Service findet man
hier: com.ubs.backend.services.IntentFinderNew#findAnswer
.
Der Service gibt ein Objekt des
Types Response zurück.
Im Service bereiten wir zuerst den Text den wir vom Benutzer bekommen vor. Dadurch verwenden wir unsere Methode
prepareString()
bei welcher wir den Text übergeben. Das sieht dann wie folgt aus:
input = prepareString(input, DataTypeInfo.USER_QUESTION_INPUT.getMaxLength(), true, false, false);
Beim Input des Benutzers entfernen wir also alle speziellen Zeichen, er wird nicht to lowercase gemacht und das
Fragezeichen wird auch nicht entfernt. Zudem wurde die Maximalgrösse auf den Wert des Datentypes
DataTypeInfo.USER_QUESTION_INPUT
gesetzt.
Danach nehmen wir alle Wörter aus dem Input und speichern diese in ein Array namens words.
String[] words = input.split("\\s");
Das Problem jetzt ist, in dem Text könnten doppelte Wörter vorkommen. Diese sind für uns unnötig und würden nur die Zeit erhöhen die wir brauchen würden, um eine passende Antwort zu finden. Deswegen nehmen wir diese durch die folgende Zeile Code raus.
words = new HashSet<>(Arrays.asList(words)).toArray(new String[0]);
Durch diese Schwarze Magie werden doppelte Wörter aus dem Array entfernt. Nachdem wir den Text vorbereitet, in einzelne Wörter aufgeteilt und doppelte Wörter entfernt haben machen wir weiter mit dem Entfernen von Wörtern auf der Blacklist.
Zuerst öffnen wir eine Verbindung zur Datenbank. Hier wird erklärt wie das geht.
Um jetzt die Wörter zu finden, die vollständig ignoriert werden sollen, gehen wir mit einer for loop durch alle Wörter
durch und vergleichen diese mit der Datenbank.
Falls wir in der Datenbank ein gleiches Wort finden entfernen wir dieses aus dem Text des Benutzers.
Falls es in der Datenbank nicht dasselbe Wort gibt fügen wir dieses Wort zu einer neuen ArrayList hinzu und entfernen
dort das Fragezeichen, falls es eines hat. In dieser Liste werden am Ende alle zu benutzenden Wörter sein.
Der Grund dafür das wir die Fragezeichen hier entfernen und nicht vorhin schon ist relativ einfach:
Wir wollen die Eingabe des Benutzers so normal wie nur möglich behalten da diese Frage später ein Vorschlag werden
könnte. Da soll diese Frage einigermassen Sinn ergeben. Allerdings müssen wir das Fragezeichen aus dem Wort entfernen da
es sonst die Suche verfälschen könnte da wir keine Tags haben mit Fragezeichen.
Das ganze sieht wie folgt aus:
ArrayList<String> filteredWords = new ArrayList<>();
for (String word : words) {
if (isBlacklisted(word, blacklistEntryDAO, em)) {
BlacklistEntry blacklistEntryFromDB = blacklistEntryDAO.selectByWord(word, em);
input = input.replaceAll(blacklistEntryFromDB.getWord() + " ", ""); // replace the blacklist entry and its following space
input = input.replaceAll(blacklistEntryFromDB.getWord(), ""); // if the blacklist entry doesn't have a space afterwards, replace just the blacklist entry
} else {
filteredWords.add(word.replace("?", ""));
}
}
Die Methode isBlacklisted
nimmt ein String als Parameter und überprüft ob dieser String bereits in der Datenbank Tabelle mit den Blacklists
existiert, wenn ja erhöhen wir die Verwendung dieses Eintrages und geben den Wert true zurück. Wenn dieser String
nicht bereits in der Datenbank existiert geben wir den Wert false zurück.
Nach dem Rausfiltern der zu ignorierenden Wörter holen wir uns alle Tags aus der Datenbank.
Zudem erstellen wir eine
ArrayList< AnswerType >,
foundAnswerTypes, in welcher wir alle verschiedenen Antworttypen speichern, mehr dazu später.
Als drittes brauchen wir eine ArrayList< WordLevenshteinDistance >,
wordLevenshteinDistances.
List<Tag> tags = tagDAO.select(em);
ArrayList<AnswerType> foundAnswerTypes = new ArrayList<>();
ArrayList<WordLevenshteinDistance> wordLevenshteinDistances = new ArrayList<>();
Jetzt kommen wir dazu mögliche Antworten zu finden.
Wir gehen durch alle Wörter, welche wir am schluss noch haben, durch und dann durch alle gefundenen Tags.
for (String word : filteredWords){
for(Tag tag : tags){
}
}
Wir berechnen jetzt immer die Distanz (dist), also der Unterschied in der Länge und der Schreibweise zwischen dem
aktuellen Wort und dem aktuellen Tag.
Dazu verwenden wir in unserer Klasse
com/ubs/backend/util/CalculateRating.java
die Methode
getLevenshteinDistance().
Diese Methode erwartet zwei Werte, einmal distanceRaw und stringLengthDifference.
Der erste Parameter berechnen wir durch die Methode
calculate
in der Klasse Levenshtein.
Um den Levenshtein Algorithmus zu verstehen sieh hier
Der zweite Parameter ist ganz einfach die länge des längeren Wortes.
Wir vergleichen ja das aktuelle Wort mit dem aktuellen Tag, der zweite Parameter ist einfach die Länge des längeren.
Math.max(tag.getTag().length(), word.length());
getLevenshteinDistance()
gibt dann einen Wert zwischen 0 und 1 zurück. Wir wissen jetzt also die Distanz zwischen Wort und Tag.
Als Nächstes überprüfen wir, ob diese Distanz innerhalb unserer Werte liegen.
Falls die Distanz zu weit weg von unseren Werten ist, passt der aktuelle Tag nicht zu diesem Wort und wir gehen weiter
zum nächsten Tag.
Falls die Distanz innerhalb unserer Werte ist holen wir alle
Results mit diesem Tag.
Wir gehen durch alle gefundenen Results und erstellen ein neues
WordLevenshteinDistance
mit dem aktuellen Wort, dem aktuellen Result, der Distanz und
levenshteinCertain.
Zu letzteres kommen wir gleich. Zudem übergeben wir noch den aktuellen EntityManager da wir im Konstruktor noch eine Datenbank Abfrage machen müssen.
Im Konstruktor des gerade erstellten WordLevenshteinDistance überprüfen wir, ob die Distanz die wir übergeben kleiner
ist als levenshteinCertain. levenshteinCertain definiert einfach wie gross die Distanz maximal sein darf damit das
Wort als ‘absolut korrekt’ gilt.
Falls die Distanz kleiner ist als levenshteinCertain definieren wir das aktuelle Wort als
Match.
Wenn es ein Match ist, müssen wir einen neuen Match erstellen und wenn nötig in der Datenbank speichern.
Dafür rufen wir
newMatch
auf. In dieser Methode überprüfen wir, ob es bereits dieser Match in der Datenbank gibt, wenn ja geben wir diesen zurück, wenn nein erstellen wir einen neuen in der Datenbank und geben diesen zurück.
Nachdem wir durch alle Results dieses Tags durch sind, müssen wir noch kurz alle
TypeTags überprüfen.
Wir suchen also in der Tabelle mit den TypeTags nach demselben Tag bei dem wir gerade sind.
Jeder Tag kann nur einmal pro TypeTag vorkommen, aber da wir verschiedene Typen haben könnte der Tag öfters in der Tabelle vorkommen, weswegen wir eine Liste zurückbekommen.
Wir gehen nun durch diese Liste durch und machen eigentlich dasselbe wie vorhin. Wir erstellen ein
WordLevenshteinDistance
und übergeben die verschiedenen Parameter.
Jetzt überprüfen wir aber noch zusätzlich, ob der aktuelle Typ schon in unserer am Anfang erstellten ArrayList wordLevenshteinDistances existiert. Falls nicht fügen wir den Typen hinzu.
Das Ganze wird jetzt wiederholt bis wir durch alle Wörter durch sind.
Nachdem wir alle möglichkeiten gefunden haben, geht es darum sich jetzt für die Beste zu entscheiden.
Im Moment haben wir aber noch das Problem, das wir nicht wirklich wissen welche Antwort wir von den gruppierten Typen schicken sollen.
Alle Antworten mit einem
AntwortTypen wie zum Beispiel
den Typen
JOKE haben alle
dieselben Tags. Dadurch ist es für uns in diesem Bereich unmöglich herauszufinden welche jetzt die beste Antwort wär.
Deswegen nehmen wir einfach eine zufällige Antwort.
Dazu gehen wir durch alle gefundenen Antworttypen und lassen eine zufällige Antwort finden und speichern diese zusammen mit dem Typen der Antwort in einer Hashmap< AnswerType, Answer >.
HashMap<AnswerType, Answer> answerTypeAnswerHashMap = new HashMap<>();
for (AnswerType answerType : foundAnswerTypes) {
answerTypeAnswerHashMap.put(answerType, answerType.handle(null));
}
Jetzt wissen wir welche Antwort wir pro Typ schicken würden.
Als Nächstes geht es darum alle möglichen Antworten zu finden. Denn bis jetzt haben wir zwar die unterschiedlichen Results mit den Tags und den Bewertungen, aber diese sind nicht pro Antwort kombiniert. Also fügen wir jetzt alle zusammen.
Dafür erstellen wir zuerst eine neue ArrayList in welcher wir Instanzen der Klasse
PossibleAnswer speichern.
Danach gehen wir durch alle vorhin gefundenen WordLevenshteinDistance in unserer ArrayList wordLevenshteinDistances.
Wir holen die aktuellen Up- sowie Downvotes von der aktuellen WordLevenshteinDistance Instanz und schauen welche Art
das Result ist.
Wenn es vom Typ Result ist nehmen wir einfach die aktuelle Antwort dieses Results und erstellen eine neue Instanz
von PossibleAnswer.
Wenn es vom Typ TypeTag ist nehmen wir zwar die aktuellen up- und downvotes, aber wir können nicht die Antwort davon
nehmen da TypeTag keine Referenz zu dieser hat. Aber genau aus diesem Grund haben wir vorhin zu den einzelnen
Antworttypen eine zufällige Antwort gesucht. Jetzt nehmen wir aus der Hashmap diese Antwort und erstellen eine neue
Instanz von PossibleAnswer.
Als Nächstes wollen wir diese Instanz in unsere ArrayList speichern.
Dafür überprüfen wir zuerst, ob die Antwort schon in der Liste existiert. Falls ja erhöhen wir die up- und downvotes
der bereits existierenden Antwort mit den Up- und Downvotes der aktuellen Antwort.
Falls nein fügen wir die aktuelle Antwort zur Liste hinzu.
Jetzt gehen wir durch alle gefundenen möglichen Antworten und berechnen deren einzelnen Bewertungen.
for (PossibleAnswer possibleAnswer : possibleAnswers) {
possibleAnswer.setRating();
}
Die Methode
setRating()
wird dafür verwendet.
In dieser Methode gehen wir durch alle WorldLevenshteinDistance durch, welche die aktuelle Antwort besitzt. Dabei
addieren wir immer die aktuelle Distanz zu einer einzelnen Variable.
Dadurch haben wir am Ende die gesamte Distanz. Mit diesem Wert berechnen wir nun die Bewertung in dem wir
com.ubs.backend.util.CalculateRating#getRating(int, int, float)
aufrufen.
Nachdem wir bei allen möglichen Antworten die Bewertung gesetzt haben gehen wir weiter und suchen die beste Antwort.
Dafür erstellen wir ein float namens maxRating und setzen den Wert auf -1f.
dann gehen wir durch alle möglichen Antworten und schauen, ob deren Bewertung besser ist als das aktuelle maxRating.
Falls dies der Fall wäre, setzen wir das aktuelle maxRating auf diese Bewertung und die aktuelle bestAnswersetzen
wir auf die momentane Antwort.
Das machen wir dann so lange bis wir durch alle Antworten durch sind.
Falls wir am Ende keine Antwort gefunden haben, erstellen wir selber eine Antwort mit dem Typen
ERROR und
konstruieren daraus ein JSON welches wir zurückgeben.
Gleichzeitig fügen wir eine neue
UnansweredQuestion
in die Datenbank ein.
Falls wir am Ende eine Antwort gefunden haben überprüfen wir, ob diese versteckt ist, falls dies der Fall ist wollen wir
keine Statistiken generieren.
Falls wir Statistiken generieren, erhöhen wir die Aufrufe der gefundenen Antwort um 1.
Zusätzlich, unabhängig davon, ob wir Statistiken generieren oder nicht, erhöhen wir die Aufrufe jedes in der Antwort vorkommenden Results.
Danach, falls wir Statistiken generieren wollen, fügen wir eine neue
AnsweredQuestion
zur Datenbank hinzu mit all den nötigen Informationen.
Am Ende schliessen wir die Verbindung zur Datenbank und erstellen ein JSON mit unserer gefundenen Antwort, welche wir dann an den Benutzer zurückschicken. Der Browser bekommt dann diese Antwort und zeigt sie dem Benutzer an.
In diesem Abschnitt des Dokumentes beschreiben wir wie der Adminbereich funktioniert.
Das Admintool ist dazu da, alles mögliche am Chatbot zu verwalten z.B. dem Hinzufügen von Antworten
Unser Admintool ist als eine Seite aufgebaut. Das bedeutet das wir ein Hauptfile, src/main/webapp/pages/adminTool/adminTool.jsp,
haben und bei diesem laden wir alle Seiten einfach rein.
Der Vorteil davon ist es das wir bei Javascript unterschiedliche Variablen und “states” zwischen den verschiedenen Seiten
behalten können.
So haben wir z.B. ein HTML file, src/main/webapp/pages/adminTool/adminToolNavigation.html, in welchem wir ganz einfach
die linke Leiste, die Navigation, beschrieben haben.

Per Javascript legen wir dann fest welche dieser Knöpfe aktiv ist und welche Seite wir laden sollen.
Um Seiten zu Laden haben wir die Methode loadPage(), sie hat folgende Parameter:
specificDataNeeded = true istMit dem folgenden Code Beispiel kann man zum Beispiel die Antwort Details zur Antwort mit der ID 1 in der Datenbank anzeigen:
await loadPage("answersDetails.html", "answersButton", true, 1);
Dabei wird auch der Knop mit der ID “answersButton” als aktiv gesetzt.
Autor: Tim Irmler
Zuletzt bearbeitet: 08.10.2021