Docs

Entwickler Dokumentation

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.


Inhaltsverzeichnis

Allgemein

  1. Einleitung
  2. Technologien
    1. Allgemein
    2. Backend
    3. Frontend
  3. Wichtige Klassen
    1. Datenbank Klassen
    2. Texte vorbereiten und reinigen
      1. prepareString
      2. shortenString
      3. stringTooLong
    3. Datentypen Informationen
  4. Allgemein wichtige Informationen
    1. Datenbank
      1. Hibernate konfigurieren
        1. Verbindung zu Datenbank
        2. Weitere Konfigurationen
      2. C3PO konfigurieren
        1. Grösse des Pools
        2. Debug
      3. Datenbank Abfragen
    2. Datenbank mit Testdaten befüllen
      1. Testdaten ändern oder hinzufügen
    3. Chatbot Server Adresse definieren
    4. Antwort- und Tagtypen
      1. Antworttypen
        1. Default
        2. Joke
        3. Facts
        4. Statistics
        5. Error
      2. Tagtypen
  5. Ideen für die Weiterentwicklung

Chatbot

  1. Einleitung
  2. Den Status überprüfen
    1. Guter Status
    2. Schlechter Status
  3. Vorschlage Fragen laden
  4. Char counter laden
  5. Passende Antwort finden
    1. Vorbereiten des Textes
    2. Matches suchen
    3. Suchen nach allen möglichen Antworten
    4. Suchen nach der besten Antwort

Adminbereich

  1. Einleitung

Allgemein

Einleitung

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.

Technologien

In diesem Abschnitt zählen wir die einzelnen Technologien auf damit klar ist was benötigt wird um starten zu können.

Projekt allgemein

Backend

Frontend

Wichtige Klassen

Datenbank Klassen

Für mehr Informationen zum Abschnitt Datenbank Abfragen gehen.

Texte vorbereiten und reinigen

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:

prepareString()

Diese Methode bereitet den String vor. Sie akzeptiert folgende Parameter:

  1. String string
  2. int maxStringlength
  3. boolean removeAllSpecialChars
  4. boolean toLowerCase
  5. removeQuestionMark

Der 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.

shortenString

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.

stringTooLong

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.

Datentypen Informationen

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.

Allgemeine wichtige Informationen

Datenbank

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.

Hibernate konfigurieren

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.

Verbindung zu Datenbank

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>

Weitere Konfigurationen

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.

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 konfigurieren

C3PO ist ein Tool, mit welchem man Connection-pooling besser unterstützen kann als mit Hibernate selber.

Die Grösse des pools definieren

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.

Debug

<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.


Verbindung zur Datenbank aufbauen

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();
Datenbank abfragen

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();

Datenbank mit Testdaten befüllen

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.

[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.


Testdaten ändern oder hinzufügen

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.

Chatbot Server Adresse definieren

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.

JavaScript

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";
Java

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.


Antwort- und Tagtypen

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.

Antworttypen

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
Default
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.

Joke
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.

Facts
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.

Statistics
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.

Error
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.

Tagtypen

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.


Ideen für die Weiterentwicklung

Hier werden Ideen aufgelistet, welche wir noch für den Chatbot hatten aber nicht genug Zeit hatten sie zu realisieren.

[1] neu = seit dem letzten login des jeweiligen Benutzers neu, davor wurde diese Frage noch nicht vorgeschlagen.


Chatbot

Einleitung

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.

  1. Den Status überprüfen
  2. Vorschlage Fragen laden
  3. Char counter laden

Status überprüfen

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”. Checking state of server 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.

  1. questionSuggestions(String amountQuestionsString)
  2. search(String input, boolean affectStatistics)

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-

Guter Status

Bei einem guten Status ändern wir die Nachricht des Bots zu einer Begrüssung. Good State message Danach geht es weiter.

Schlechter Status

Bei einem schlechten Status geben wir einen Fehler aus. Bad State message Das Textfeld bleibt deaktiviert und es passiert nichts mehr.

Vorschlage Fragen laden

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.

Char counter laden

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. char counter userinput

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.


Suchen nach der passenden Antwort

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.


Vorbereiten des Textes

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.


Suchen nach Matches

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.


Suchen nach allen möglichen Antworten

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.

Suchen nach der besten Antwort

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.


Adminbereich

Einleitung

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

Seiten Laden

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.

adminTool Navigation

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:

Mit 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