Commit a58a665d authored by Christof Walther's avatar Christof Walther Committed by Patrick Schlindwein
Browse files

#92 refactor ktor api

parent f146398d
package de.h_da.fbi.smebt.intentfinder.server
import com.squareup.moshi.JsonDataException
import de.h_da.fbi.smebt.intentfinder.server.nlp.PythonBridge
import de.h_da.fbi.smebt.intentfinder.server.sources.*
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import kotlinx.serialization.json.Json
import java.io.File
import java.lang.RuntimeException
import de.h_da.fbi.smebt.intentfinder.server.upload.registerApiRoutes
......@@ -20,9 +16,7 @@ fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
fun Application.module() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
})
json()
}
install(StatusPages) {
......@@ -36,18 +30,16 @@ fun Application.module() {
}
routing {
get("/summary") {
val response = PythonBridge().getSummary("test bridge")
post("/summarize") {
val req = call.receive<SummaryRequest>()
val response = PythonBridge().getSummary(req.text, req.maxLength)
call.respond(response)
}
post("/{name}") {
}
// get faq with Json Configuration
get("/faqResource") {
// Rückgabe Json Object (vgl. #38)
......
package de.h_da.fbi.smebt.intentfinder.server;
import kotlinx.serialization.Serializable
@Serializable
data class SummaryRequest(val text: String, val maxLength: Int)
\ No newline at end of file
......@@ -2,6 +2,7 @@ package de.h_da.fbi.smebt.intentfinder.server.nlp
import de.h_da.fbi.smebt.intentfinder.server.nlp.client.RetrofitClient
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Summary
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.SummaryBody
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorCompletionService
......@@ -11,34 +12,36 @@ import kotlin.collections.ArrayList
class PythonBridge(private val client: RetrofitClient = RetrofitClient()) {
private fun getAllSummaries(text: String): List<Summary> {
private fun getAllSummaries(text: String, maxLength: Int): List<SummaryBody> {
val strategies = client.getStrategies()
val executor = Executors.newFixedThreadPool(strategies.size)
val executor = Executors.newFixedThreadPool(strategies.body.size)
val completionService = ExecutorCompletionService<Summary>(executor)
strategies.forEach {
completionService.submit { client.getSummary(it.name, text) }
strategies.body.forEach {
completionService.submit { client.getSummary(it, text, maxLength) }
}
val summaries = ArrayList<Summary>()
repeat(strategies.size) {
val summaries = ArrayList<SummaryBody>()
repeat(strategies.body.size) {
try {
val result = completionService.take()
summaries.add(result.get())
val res = result.get()
if(res.statusCode == 200)
summaries.add(res.body)
} catch (ex: ExecutionException) {
// TODO handle ExecutionException
print(ex)
}
}
return summaries
}
fun getSummary(text: String): List<Summary> {
return getAllSummaries(text).sortedWith(compareByDescending { it.quality })
fun getSummary(text: String, maxLength: Int): List<SummaryBody> {
return getAllSummaries(text, maxLength).sortedWith(compareByDescending { it.quality })
}
fun getIntentId(text: String): String {
return client.getIntentId(text)
}
}
\ No newline at end of file
}
package de.h_da.fbi.smebt.intentfinder.server.nlp.client
import com.google.gson.JsonObject
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Strategy
import de.h_da.fbi.smebt.intentfinder.server.SummaryRequest
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Strategies
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Summary
import okhttp3.OkHttpClient
import retrofit2.Retrofit
......@@ -28,12 +29,12 @@ class RetrofitClient {
return RetrofitExecutor().execute(client.root())
}
fun getStrategies() : List<Strategy> {
fun getStrategies() : Strategies {
return RetrofitExecutor().execute(client.getStrategies())
}
fun getSummary(strategy: String, text: String) : Summary {
return RetrofitExecutor().execute(client.getSummary(strategy, text))
fun getSummary(strategy: String, text: String, maxLength: Int) : Summary {
return RetrofitExecutor().execute(client.getSummary(strategy, SummaryRequest(text, maxLength)))
}
fun getIntentId(text: String) : String {
......
package de.h_da.fbi.smebt.intentfinder.server.nlp.client
import com.google.gson.JsonObject
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Strategy
import de.h_da.fbi.smebt.intentfinder.server.SummaryRequest
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Strategies
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Summary
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.*
interface SummarizationAPI {
......@@ -15,10 +13,10 @@ interface SummarizationAPI {
fun root() : Call<JsonObject>
@GET("/strategies")
fun getStrategies(): Call<List<Strategy>>
fun getStrategies(): Call<Strategies>
@POST("/summarize/{strategy}")
fun getSummary(@Path("strategy") strategy: String, @Body text: String): Call<Summary>
fun getSummary(@Path("strategy") strategy: String, @Body summaryRequest: SummaryRequest): Call<Summary>
@POST("/intentid")
fun getIntentId(@Body text: String) : Call<String>
......
......@@ -2,5 +2,6 @@ package de.h_da.fbi.smebt.intentfinder.server.nlp.dto
import kotlinx.serialization.Serializable
@Serializable
data class Strategy(val name: String)
data class Strategies(val statusCode: Int, val message: String, val body: List<String>)
......@@ -3,8 +3,15 @@ package de.h_da.fbi.smebt.intentfinder.server.nlp.dto
import kotlinx.serialization.Serializable
@Serializable
data class Summary(
data class SummaryBody(
val strategy: String,
val quality: Double,
val result: String,
val summary: String,
)
@Serializable
data class Summary(
val statusCode: Int,
val message: String,
val body: SummaryBody
)
......@@ -2,12 +2,11 @@ package de.h_da.fbi.smebt.intentfinder.server.sources
import de.h_da.fbi.smebt.intentfinder.server.nlp.PythonBridge
import de.h_da.fbi.smebt.intentfinder.server.nlp.client.RetrofitClient
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Strategy
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Strategies
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.Summary
import de.h_da.fbi.smebt.intentfinder.server.nlp.dto.SummaryBody
import org.hamcrest.CoreMatchers
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.collection.IsIterableContainingInAnyOrder
import org.hamcrest.collection.IsIterableContainingInOrder
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
......@@ -17,8 +16,8 @@ import kotlin.test.assertEquals
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PythonBridgeTest {
lateinit var retrofitClient: RetrofitClient
lateinit var bridge: PythonBridge
private lateinit var retrofitClient: RetrofitClient
private lateinit var bridge: PythonBridge
@BeforeAll
fun setup() {
......@@ -30,24 +29,40 @@ class PythonBridgeTest {
fun `returns strategy with the highest quality`() {
val strategyNameOne = "first"
val strategyNameTwo = "second"
val strategies = listOf(
Strategy(strategyNameOne),
Strategy(strategyNameTwo)
val strategies = Strategies(
200,
"ok",
listOf(strategyNameOne, strategyNameTwo)
)
val summaryOne = Summary(strategyNameOne, 0.7, "")
val summaryTwo = Summary(strategyNameTwo, 0.8, "")
val summaryOne = Summary(
200,
"",
SummaryBody(
strategyNameOne,
0.7, ""
)
)
val summaryTwo = Summary(
200,
"",
SummaryBody(
strategyNameTwo,
0.8,
""
)
)
`when`(retrofitClient.getStrategies()).thenReturn(strategies)
`when`(retrofitClient.getSummary(strategyNameOne, "")).thenReturn(summaryOne)
`when`(retrofitClient.getSummary(strategyNameTwo, "")).thenReturn(summaryTwo)
`when`(retrofitClient.getSummary(strategyNameOne, "", Integer.MAX_VALUE)).thenReturn(summaryOne)
`when`(retrofitClient.getSummary(strategyNameTwo, "", Integer.MAX_VALUE)).thenReturn(summaryTwo)
val result = bridge.getSummary("")
val result = bridge.getSummary("", Integer.MAX_VALUE)
verify(retrofitClient, times(1)).getStrategies()
verify(retrofitClient, times(2)).getSummary(anyString(), anyString())
verify(retrofitClient, times(2)).getSummary(anyString(), anyString(), anyInt())
assertEquals(2, result.size)
assertThat(result, CoreMatchers.`is`(listOf(summaryTwo, summaryOne)))
assertThat(result, CoreMatchers.`is`(listOf(summaryTwo.body, summaryOne.body)))
}
}
\ No newline at end of file
......@@ -7,8 +7,14 @@ from app.summary.summarization_with_strategy_TFIDF import SummaryTFIDF
from app.summary.summary_bert import BertSummary
from app.summary.summary_sentence_embedding import SentenceEmbeddingSummarizer
from app.summary.summary_word_embedding import WordEmbeddingSummarizer
from app.utilities.success_response import SuccessResponse
from app.utilities.client_error_response import ClientErrorResponse
from app.utilities.server_error_response import ServerErrorResponse
from app.utilities import generator
from app.utilities.models import nlp, bert_model, bert_tokenizer
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
class Item(BaseModel):
......@@ -39,6 +45,25 @@ strategies = [
WordEmbeddingSummarizer(_nlp=nlp)]
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc: RequestValidationError):
"""
Handle all raised request exceptions and return them to the client as JSON
"""
return JSONResponse(content=jsonable_encoder({'statusCode': 400,
'message': exc._error_cache,
'body': None}))
@app.exception_handler(Exception)
async def exception_handler(request: Request, exception: Exception):
"""
Handle all raised exceptions and return them to the client as JSON
"""
res = ServerErrorResponse(exception)
return JSONResponse(content=jsonable_encoder(res))
@app.post("/questionGenerator")
async def api_questionGenerator(item: Item):
qg = QuestionGenerator()
......@@ -61,9 +86,11 @@ async def api_strategies():
:return: The list of all summarization-strategy-ids in JSON format
"""
res = []
strategy_ids = list()
for strategy in strategies:
res.append(strategy.id)
strategy_ids.append(strategy.id)
res = SuccessResponse(strategy_ids)
return res
......@@ -81,10 +108,13 @@ async def call_strategy(strategy_id: str, text: str, num_sentences: int):
if strategy.id == strategy_id:
quality = 0.5
summary = strategy.summarize(text, num_sentences)
return {"strategy": strategy_id, "quality": quality,
"summary": summary}
response = SuccessResponse({"strategy": strategy_id,
"quality": quality,
"summary": summary})
return response
return {"error": "invalid id"}
response = ClientErrorResponse("error: unknown strategy")
return response
@app.get("/summarize/{strategy_id}", summary="Generate a summary of the given"
......@@ -117,6 +147,11 @@ async def post_summarize(strategy_id: str, req: Request):
req_json["maxLength"])
@app.get("/{bot}/similarIntents", summary="Get similar Intents.")
async def get_similar_intents(bot, t=0.8):
pass
@app.post("/intentid", summary="Generate an intent id from a given intent"
" text")
async def generate_intent_id(intent: str, max_tokens: int):
......
import numpy as np
from app.quality.vectorizer_interface import IVectorizer
from app.quality.spacy_vectorizer import SpacyVectorizer
def find_similar_strings(vectorizer: IVectorizer,
strings: list[str], t: float) -> list[list[int]]:
"""
This method will find similar strings the the given list.It uses
cosine similarity to represent relations.
:param vectorizer: The vectorizer to transform the strings into a matrix
:param strings: The list of Strings
:param t: The threshold when a string is considered similar
:returns: A List of indexes of similar strings
"""
matrix = vectorizer.vectorize(strings)
similarity_matrix = matrix.dot(np.transpose(matrix))
similarity_matrix = (similarity_matrix - np.tril(similarity_matrix))
return np.argwhere(similarity_matrix > t)
def spacy_cosine_similarity(strings: list[str], t: float) -> list[list[int]]:
"""
This method will find similar strings the the given list.It uses
cosine similarity to represent relations and Spacy to transform the strings
into a matrix.
:param strings: The list of Strings
:param t: The threshold when a string is considered similar
:returns: A List of indexes of similar strings
"""
return find_similar_strings(SpacyVectorizer(), strings, t)
from app.quality.vectorizer_interface import IVectorizer
from app.utilities.models import nlp
import numpy as np
class SpacyVectorizer(IVectorizer):
def vectorize(self, strings: list[str]) -> list:
matrix = list()
for sent in strings:
doc = nlp(sent)
matrix.append(doc.vector)
return matrix / np.linalg.norm(matrix, axis=1, keepdims=True)
from abc import ABC, abstractmethod
class IVectorizer(ABC):
@abstractmethod
def vectorize(self, strings: list[str]) -> list:
"""
Turns a set of strings into a n x n Matrix
:param strings: List of strings that will be vectorized
:returns: A n x n Matrix representing the input strings as vectors
"""
pass
......@@ -18,6 +18,7 @@ class BertSummary(ISummaryStrategy):
def summarize(self, text: str, num_sentences: int) -> str:
"""
This method returns a summary for the given text
:param num_sentences: maximum number of characters
:param text: str: text to create summary from
:returns: summary: str: generated summary
......
......@@ -93,6 +93,7 @@ test_text2 = '''Sowohl vollständig durchgeführte Impfungen als auch eine
https://www.bundesregierung.de/breg-de/themen/coronavirus/erleichterunge
-geimpfte-1910886. Ein alleiniger Antikörpernachweis ist nicht ausreichend,
unabhängig vom gemessenen Antikörperwert. '''
test_text3 = '''Für eine vollständige Immunisierung sind mit den
mRNA-Impfstoffen von BioNTech/Pfizer (Comirnaty) und Moderna (COVID-19
Vaccine Moderna) und dem Vektor-basierten Impfstoff von AstraZeneca (
......@@ -247,4 +248,105 @@ test_text8 = '''mRNA-Impfstoffe enthalten Genabschnitte des SARS-CoV-2-Virus
Wirksamkeit geprüft. Weitere Informationen zu COVID-19-Impfstoffen finden
sich hier. Stand: 02.11.2020 '''
rki_faq = [
"Was ist über den Erreger und die Krankheit bekannt?",
"Wie erfasst das RKI die Situation in Deutschland, wie schätzt das RKI"
" die Lage ein und welche Empfehlungen gibt es für die "
"Fachöffentlichkeit?",
"Was versteht man unter der Reproduktionszahl R, und wie wichtig ist"
" sie für die Bewertung der Lage?",
"Welchen Zusammenhang gibt es generell zwischen erhöhten Testzahlen "
"und erhöhten Fallzahlen?",
"Gibt es eine Saisonalität bei SARS-CoV-2?",
"Welche Rolle spielen die neuen, besorgniserregenden Varianten?",
"Wo gibt es die aktuellen Fallzahlen und Inzidenzen?",
"Was ist alles meldepflichtig?",
"Wie funktioniert der Meldeweg und welche Informationen zu den "
"Erkrankten werden ans RKI übermittelt?",
"Werden die Meldedaten durch die wachsende Anzahl an Schnelltests "
"verzerrt?",
"Wie entsteht die Diskrepanz zwischen Inzidenzen der Landkreise und "
"den Angaben des RKI-Dashboards?",
"Warum sind die Fallzahlen am/nach dem Wochenende geringer als an "
"Arbeitstagen?",
"Wie erfassen Gesundheitsämter Fälle, Ausbrüche und "
"Infektionsumstände?",
"Wie werden Todesfälle erfasst?",
"Weiß man, wie viele COVID-19-Patienten im Krankenhaus und auf "
"Intensivstationen behandelt werden und wie viele"
" die akute Infektion überstanden haben?",
"Was ist der Unterschied zwischen den COVID-19-Meldedaten nach "
"<abbr>IfSG </abbr>und SARS-CoV-2-Nachweisen aus"
" dem Sentinel der Arbeitsgemeinschaft Influenza?",
"Warum bilden sich die COVID-19-Wellen bisher nicht bei GrippeWeb ab?",
"Wieso unterscheidet sich die Anzahl der COVID-19 Fälle aus dem "
"ICOSARI-Krankenhaus-Sentinel von der Anzahl"
" Intensivpatienten mit COVID-19 aus dem DIVI-Intensivregister?",
"Welche Gruppen sind besonders häufig von einem schweren Verlauf "
"betroffen?",
"Ist man nach einer durchgemachten SARS-CoV-2-Infektion immun?",
"Was ist über COVID-19 bei Kindern und Jugendlichen bekannt?",
"Was ist über COVID-19 bei Schwangeren bekannt?",
"Ist die Blutgruppe ein Risikofaktor für COVID-19?",
"Welche Behandlungsmöglichkeiten stehen für eine COVID-19-Erkrankung"
" zur Verfügung?",
"Warum sind bei SARS-CoV-2/COVID-19 solche weitreichenden Maßnahmen"
" erforderlich?",
"Wie kann man sich bzw. seine Mitmenschen vor einer Ansteckung"
" schützen?",
"Welche Rolle spielen die Impfungen gegen COVID-19, Impfungen im"
" Rahmen der Pandemie und was gilt es beim"
" Impfen zu beachten?",
"Welchen Vorteil bringt Abstand halten die Beschränkung sozialer"
" Kontakte?",
"Was ist beim Tragen einer Mund-Nasen-Bedeckung bzw. eines"
" Mund-Nasen-Schutzes (\"OP-Maske\") in der "
"Öffentlichkeit zu beachten?",
"Welche Funktion bzw. Einsatzbereiche haben FFP2-Masken außerhalb des"
" Arbeitsschutzes?",
"Ist der Einsatz von Visieren anstatt einer Mund-Nasen-Bedeckung im"
" öffentlichen Raum sinnvoll?",
"Ist der Einsatz von Visieren im öffentlichen Raum sinnvoll?",
"Welche Rolle spielen Aerosole bei der Übertragung von SARS-CoV-2?",
"Was ist beim Lüften zu beachten?",
"Können Luftreinigungsgeräte bzw. mobile Luftdesinfektionsgeräte"
" andere Hygienemaßnahmen ersetzen?",
"Was ist aus Sicht des Infektionsschutzes im Schulumfeld zu beachten?",
"Was ist beim Umgang mit an COVID-19-Verstorbenen zu beachten?",
"Was müssen Arbeitnehmerinnen und Arbeitnehmer während der"
" COVID-19-Pandemie beachten, welche Verpflichtungen"
" haben Arbeitgeber?",
"Was ist bei Reisen zu beachten?",
"Besteht die Gefahr, sich über Lebensmittel, Oberflächen,"
" Gegenstände oder in der Umwelt mit SARS-CoV-2"
" anzustecken?",
"Welche Empfehlungen gibt es für den Umgang mit Haustieren?",
"Was versteht man unter Isolierung, was unter Quarantäne und welchen"
" Zweck haben diese?",
"Wann und wie lange muss man in Quarantäne?",
"Was wird empfohlen bei Personen, die als genesen gelten?",
"Was versteht man unter Kontaktpersonennachverfolgung, was müssen"
" Kontaktpersonen beachten?",
"Wie funktioniert die Corona Warn-App?",
"Wie funktioniert die Corona App?",
"Was ist ein Containment Scout?",
"Wie geht man bei Ausbruchsuntersuchungen vor?",
"Wie wird eine Infektion mit SARS-CoV-2 labordiagnostisch nachgewiesen"
", welche Tests gibt es?",
"Welche Anforderungen werden an Antigen-Tests gestellt?",
"Was ist bei Antigentests zur Eigenanwendung (Selbsttests) zu"
" beachten?",
"Wann sollte ein Arzt eine Laboruntersuchung auf SARS-CoV-2"
" veranlassen?",
"Was sollen Betroffene mit Symptomen tun?",
"Was sollen Symptomen mit Betroffene tun?",
"Was sollen Betroffene tun?",
"Wie wird die Anzahl an Labortests in Deutschland erfasst?",
"Was bedeutet die Positivenquote?",
"Welche Rolle spielen falsch-positive Testergebnisse?",
"Ist ein Test bei Personen ohne jedes Krankheitszeichen sinnvoll? ",
"Warum sind Genomsequenzierungen wichtig?",
"Wo gibt es weitere Informationen?"
]
num_sentences = 3
from unittest import TestCase
from app.utilities.success_response import SuccessResponse
from fastapi.testclient import TestClient
......@@ -57,7 +58,7 @@ class TestNlpApi(TestCase):
# assert results
assert response.status_code == 200
assert response.json() == ["test1", "test2"]
assert response.json()['body'] == ["test1", "test2"]
def test_get_summarize(self):
"""
......@@ -76,12 +77,15 @@ class TestNlpApi(TestCase):
"/summarize/test2?num_sentences=1&text=test%20text")
# assert result
expected1 = SuccessResponse({"strategy": "test1",
"quality": 0.5, "summary": "test text"})
expected2 = SuccessResponse({"strategy": "test2",
"quality": 0.5, "summary": "result text"})
assert response1.status_code == 200
assert response1.json() == {"strategy": "test1",
"quality": 0.5, "summary": "test text"}
assert response1.json()['body'] == expected1.body
assert response2.status_code == 200
assert response2.json() == {"strategy": "test2",
"quality": 0.5, "summary": "result text"}
assert response2.json()['body'] == expected2.body
def test_post_summarize(self):
"""
......@@ -100,9 +104,12 @@ class TestNlpApi(TestCase):