You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nlpcraft.apache.org by se...@apache.org on 2022/04/08 11:03:32 UTC

[incubator-nlpcraft] branch NLPCRAFT-491 updated (a94af435 -> e0a631d3)

This is an automated email from the ASF dual-hosted git repository.

sergeykamov pushed a change to branch NLPCRAFT-491
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git


    from a94af435 WIP.
     new aafd349a WIP.
     new e0a631d3 WIP.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../nlpcraft/examples/pizzeria/PizzeriaModel.scala | 28 ++++---
 .../pizzeria/components/ElementExtender.scala      | 86 ++++++++++++++--------
 .../src/main/resources/pizzeria_model.yaml         |  4 +-
 .../examples/pizzeria/PizzeriaModelSpec.scala      | 36 +++++++--
 .../pizzeria/cli/PizzeriaModelClientCli.scala      | 12 ++-
 .../pizzeria/cli/PizzeriaModelServer.scala         |  9 ++-
 6 files changed, 123 insertions(+), 52 deletions(-)


[incubator-nlpcraft] 01/02: WIP.

Posted by se...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sergeykamov pushed a commit to branch NLPCRAFT-491
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git

commit aafd349afd82fe4aeae7f132a8fad8e9e9ea17fe
Author: Sergey Kamov <sk...@gmail.com>
AuthorDate: Fri Apr 8 11:51:04 2022 +0300

    WIP.
---
 .../pizzeria/components/ElementExtender.scala      | 78 +++++++++++++---------
 1 file changed, 48 insertions(+), 30 deletions(-)

diff --git a/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala b/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala
index 364f1058..69f4a409 100644
--- a/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala
+++ b/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala
@@ -23,6 +23,7 @@ import java.util
 import java.util.List as JList
 import scala.collection.mutable
 import scala.jdk.CollectionConverters.*
+import com.typesafe.scalalogging.LazyLogging
 
 /**
   *
@@ -32,39 +33,56 @@ import scala.jdk.CollectionConverters.*
 case class EntityData(id: String, property: String)
 
 /**
-  * Simple element extender.
-  * For each 'main' element it tries to find extra element. If it cane done instead of this pair new element created.
-  * This new element has 'main' element ID. Extra element configured property copied to this new element.
+  * Element extender.
+  * For each 'main' element it tries to find related extra element and to make new complex element instead of this pair.
+  * This new element has:
+  * 1. Same ID as main element, also all main element properties copied into this new element's properties.
+  * 2. Tokens from both elements.
+  * 3. Configured property from extra element copied into new element's properties.
   *
-  * Note that for simplified implementation, it doesn't care about words between pairs elements and pairs,
-  * also it doesn't check correctness of words order.
+  * Note that it is simple example implementation.
+  * It just tries to unite nearest neighbours and doesn't check intermediate words, order correctness etc.
   */
-case class ElementExtender(mainDataSeq: Seq[EntityData], extraData: EntityData) extends NCEntityMapper:
-    private def getToks(e: NCEntity): mutable.Seq[NCToken] = e.getTokens.asScala
-    override def map(req: NCRequest, cfg: NCModelConfig, entities: util.List[NCEntity]): util.List[NCEntity] =
-        val mainDataMap = mainDataSeq.map(p => p.id -> p).toMap
+object ElementExtender:
+    case class EntityHolder(element: NCEntity):
+        lazy val position: Double =
+            val toks = element.getTokens.asScala
+            (toks.head.getIndex + toks.last.getIndex) / 2.0
 
-        var es = entities.asScala
-        val main = es.filter(e => mainDataMap.contains(e.getId))
-        val extra = es.filter(_.getId == extraData.id)
+    private def toTokens(e: NCEntity): mutable.Seq[NCToken] = e.getTokens.asScala
 
-        if main.nonEmpty && main.size >= extra.size then
-            var ok = true
-            val mapped =
-                for ((e1, e2) <- main.zip(extra) if ok) yield
-                    if e1.getId == e2.getId then
-                        ok = false
-                        null
-                    else
-                        val (mEnt, eEnt) = if mainDataMap.contains(e1.getId) then (e1, e2) else (e2, e1)
-                        new NCPropertyMapAdapter with NCEntity:
-                            mEnt.keysSet().forEach(k => put(k, mEnt.get(k)))
-                            put[String](mainDataMap(mEnt.getId).property, eEnt.get[String](extraData.property).toLowerCase)
-                            override val getTokens: JList[NCToken] = (getToks(mEnt) ++ getToks(eEnt)).sortBy(_.getIndex).asJava
-                            override val getRequestId: String = req.getRequestId
-                            override val getId: String = mEnt.getId
+import ElementExtender.*
 
-            es = es --= main.take(mapped.size)
-            es = es --= extra.take(mapped.size)
-            (es ++ mapped).sortBy(getToks(_).head.getIndex).asJava
+/**
+  *
+  * @param mainSeq
+  * @param extra
+  */
+case class ElementExtender(mainSeq: Seq[EntityData], extra: EntityData) extends NCEntityMapper with LazyLogging:
+    override def map(req: NCRequest, cfg: NCModelConfig, entities: JList[NCEntity]): JList[NCEntity] =
+        def combine(m: NCEntity, mProp: String, e: NCEntity): NCEntity =
+            new NCPropertyMapAdapter with NCEntity:
+                m.keysSet().forEach(k => put(k, m.get(k)))
+                put[String](mProp, e.get[String](extra.property).toLowerCase)
+                override val getTokens: JList[NCToken] = (toTokens(m) ++ toTokens(e)).sortBy(_.getIndex).asJava
+                override val getRequestId: String = req.getRequestId
+                override val getId: String = m.getId
+
+        val es = entities.asScala
+        val mainById = mainSeq.map(p => p.id -> p).toMap
+        val mainHs = mutable.HashSet.empty ++ es.filter(e => mainById.contains(e.getId)).map(p => EntityHolder(p))
+        val extraHs = es.filter(_.getId == extra.id).map(p => EntityHolder(p))
+
+        if mainHs.nonEmpty && mainHs.size >= extraHs.size then
+            val main2Extra = mutable.HashMap.empty[NCEntity, NCEntity]
+
+            for (e <- extraHs)
+                val m = mainHs.minBy(m => Math.abs(m.position - e.position))
+                mainHs -= m
+                main2Extra += m.element -> e.element
+
+            val newEs = for ((m, e) <- main2Extra) yield combine(m, mainById(m.getId).property, e)
+            val used = (mainHs.map(_.element) ++ extraHs.map(_.element)).toSet
+
+            (es.filter(e => !used.contains(e)) ++ mainHs.map(_.element) ++ newEs).sortBy(toTokens(_).head.getIndex).asJava
         else entities
\ No newline at end of file


[incubator-nlpcraft] 02/02: WIP.

Posted by se...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sergeykamov pushed a commit to branch NLPCRAFT-491
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git

commit e0a631d3d3a0ad72376aac9527f297bcc0829a5e
Author: Sergey Kamov <sk...@gmail.com>
AuthorDate: Fri Apr 8 14:03:25 2022 +0300

    WIP.
---
 .../nlpcraft/examples/pizzeria/PizzeriaModel.scala | 28 ++++++----
 .../pizzeria/components/ElementExtender.scala      | 64 ++++++++++++----------
 .../src/main/resources/pizzeria_model.yaml         |  4 +-
 .../examples/pizzeria/PizzeriaModelSpec.scala      | 36 ++++++++++--
 .../pizzeria/cli/PizzeriaModelClientCli.scala      | 12 +++-
 .../pizzeria/cli/PizzeriaModelServer.scala         |  9 ++-
 6 files changed, 103 insertions(+), 50 deletions(-)

diff --git a/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModel.scala b/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModel.scala
index e9498fd7..9d8020d4 100644
--- a/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModel.scala
+++ b/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModel.scala
@@ -46,16 +46,20 @@ import org.apache.nlpcraft.examples.pizzeria.PizzeriaModel.*
 /**
   *
   */
-class PizzeriaModel extends NCModelAdapter (new NCModelConfig("nlpcraft.pizzeria.ex", "Pizzeria Example Model", "1.0"), PizzeriaModelPipeline.PIPELINE) with LazyLogging:
-    private def withLog(im: NCIntentMatch, body: PizzeriaOrder => NCResult): NCResult =
-        val usrId = im.getContext.getRequest.getUserId
-        val data = im.getContext.getConversation.getData
-        var o: PizzeriaOrder = data.get(usrId)
-
-        if o == null then
-            o = new PizzeriaOrder()
+class PizzeriaModel extends NCModelAdapter(new NCModelConfig("nlpcraft.pizzeria.ex", "Pizzeria Example Model", "1.0"), PizzeriaModelPipeline.PIPELINE) with LazyLogging:
+    private def getOrder(ctx: NCContext): PizzeriaOrder =
+        val data = ctx.getConversation.getData
+        val usrId = ctx.getRequest.getUserId
+        val o: PizzeriaOrder = data.get(usrId)
+
+        if o != null then o
+        else
+            val o = new PizzeriaOrder()
             data.put(usrId, o)
+            o
 
+    private def withLog(im: NCIntentMatch, body: PizzeriaOrder => NCResult): NCResult =
+        val o = getOrder(im.getContext)
         val initState = o.getState.toString
         val initDesc = o.getDesc
 
@@ -88,7 +92,7 @@ class PizzeriaModel extends NCModelAdapter (new NCModelConfig("nlpcraft.pizzeria
     private def doShowMenu() =
         NCResult(
             "There are accessible for order: margherita, carbonara and marinara. Sizes: large, medium or small. " +
-            "Also there are tea, green tea, coffee and cola.",
+            "Also there are tea, coffee and cola.",
             ASK_RESULT
         )
 
@@ -236,4 +240,8 @@ class PizzeriaModel extends NCModelAdapter (new NCModelConfig("nlpcraft.pizzeria
             // If order in progress and has pizza with unknown size, it doesn't depend on dialog state.
             if !o.isEmpty && o.setPizzaNoSize(extractPizzaSize(size)) then askIsReadyOrAskSpecify(o)
             else throw UNEXPECTED_REQUEST
-    )
\ No newline at end of file
+    )
+
+    override def onRejection(im: NCIntentMatch, e: NCRejection): NCResult =
+        // TODO: improve logic after https://issues.apache.org/jira/browse/NLPCRAFT-495 ticket resolving.
+        if im == null || getOrder(im.getContext).isEmpty then doShowMenu() else throw e
\ No newline at end of file
diff --git a/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala b/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala
index 69f4a409..6bc1089f 100644
--- a/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala
+++ b/nlpcraft-examples/pizzeria/src/main/java/org/apache/nlpcraft/examples/pizzeria/components/ElementExtender.scala
@@ -24,6 +24,7 @@ import java.util.List as JList
 import scala.collection.mutable
 import scala.jdk.CollectionConverters.*
 import com.typesafe.scalalogging.LazyLogging
+import org.apache.nlpcraft.NCResultType.ASK_DIALOG
 
 /**
   *
@@ -34,55 +35,62 @@ case class EntityData(id: String, property: String)
 
 /**
   * Element extender.
-  * For each 'main' element it tries to find related extra element and to make new complex element instead of this pair.
-  * This new element has:
-  * 1. Same ID as main element, also all main element properties copied into this new element's properties.
-  * 2. Tokens from both elements.
-  * 3. Configured property from extra element copied into new element's properties.
+  * For each 'main' element it tries to find related extra element and convert this pair to new complex element.
+  * New element:
+  * 1. Gets same ID as main element, also all main element properties copied into this new one.
+  * 2. Gets tokens from both elements.
+  * 3. Configured extra element property copied into new element's properties.
   *
   * Note that it is simple example implementation.
   * It just tries to unite nearest neighbours and doesn't check intermediate words, order correctness etc.
   */
 object ElementExtender:
-    case class EntityHolder(element: NCEntity):
+    case class EntityHolder(entity: NCEntity):
         lazy val position: Double =
-            val toks = element.getTokens.asScala
+            val toks = entity.getTokens.asScala
             (toks.head.getIndex + toks.last.getIndex) / 2.0
-
-    private def toTokens(e: NCEntity): mutable.Seq[NCToken] = e.getTokens.asScala
+    private def extract(e: NCEntity): mutable.Seq[NCToken] = e.getTokens.asScala
 
 import ElementExtender.*
 
 /**
   *
-  * @param mainSeq
-  * @param extra
+  * @param mainDataSeq
+  * @param extraData
   */
-case class ElementExtender(mainSeq: Seq[EntityData], extra: EntityData) extends NCEntityMapper with LazyLogging:
+case class ElementExtender(mainDataSeq: Seq[EntityData], extraData: EntityData) extends NCEntityMapper with LazyLogging:
     override def map(req: NCRequest, cfg: NCModelConfig, entities: JList[NCEntity]): JList[NCEntity] =
-        def combine(m: NCEntity, mProp: String, e: NCEntity): NCEntity =
+        def combine(mainEnt: NCEntity, mainProp: String, extraEnt: NCEntity): NCEntity =
             new NCPropertyMapAdapter with NCEntity:
-                m.keysSet().forEach(k => put(k, m.get(k)))
-                put[String](mProp, e.get[String](extra.property).toLowerCase)
-                override val getTokens: JList[NCToken] = (toTokens(m) ++ toTokens(e)).sortBy(_.getIndex).asJava
+                mainEnt.keysSet().forEach(k => put(k, mainEnt.get(k)))
+                put[String](mainProp, extraEnt.get[String](extraData.property).toLowerCase)
+                override val getTokens: JList[NCToken] = (extract(mainEnt) ++ extract(extraEnt)).sortBy(_.getIndex).asJava
                 override val getRequestId: String = req.getRequestId
-                override val getId: String = m.getId
+                override val getId: String = mainEnt.getId
 
         val es = entities.asScala
-        val mainById = mainSeq.map(p => p.id -> p).toMap
-        val mainHs = mutable.HashSet.empty ++ es.filter(e => mainById.contains(e.getId)).map(p => EntityHolder(p))
-        val extraHs = es.filter(_.getId == extra.id).map(p => EntityHolder(p))
+        val mainById = mainDataSeq.map(p => p.id -> p).toMap
+        val main = mutable.HashSet.empty ++ es.filter(e => mainById.contains(e.getId)).map(p => EntityHolder(p))
+        val extra = es.filter(_.getId == extraData.id).map(p => EntityHolder(p))
 
-        if mainHs.nonEmpty && mainHs.size >= extraHs.size then
+        if main.nonEmpty && extra.nonEmpty && main.size >= extra.size then
+            val used = (main.map(_.entity) ++ extra.map(_.entity)).toSet
             val main2Extra = mutable.HashMap.empty[NCEntity, NCEntity]
 
-            for (e <- extraHs)
-                val m = mainHs.minBy(m => Math.abs(m.position - e.position))
-                mainHs -= m
-                main2Extra += m.element -> e.element
+            for (e <- extra)
+                val m = main.minBy(m => Math.abs(m.position - e.position))
+                main -= m
+                main2Extra += m.entity -> e.entity
+
+            val unrelatedEs = es.filter(e => !used.contains(e))
+            val artificialEs = for ((m, e) <- main2Extra) yield combine(m, mainById(m.getId).property, e)
+            val unused = main.map(_.entity)
+
+            val res = (unrelatedEs ++ artificialEs ++ unused).sortBy(extract(_).head.getIndex)
 
-            val newEs = for ((m, e) <- main2Extra) yield combine(m, mainById(m.getId).property, e)
-            val used = (mainHs.map(_.element) ++ extraHs.map(_.element)).toSet
+            def str(es: mutable.Buffer[NCEntity]) =
+                es.map(e => s"id=${e.getId}(${extract(e).map(_.getIndex).mkString("[", ",", "]")})").mkString("{", ", ", "}")
+            logger.debug(s"Elements mapped [input=${str(es)}, output=${str(res)}]")
 
-            (es.filter(e => !used.contains(e)) ++ mainHs.map(_.element) ++ newEs).sortBy(toTokens(_).head.getIndex).asJava
+            res.asJava
         else entities
\ No newline at end of file
diff --git a/nlpcraft-examples/pizzeria/src/main/resources/pizzeria_model.yaml b/nlpcraft-examples/pizzeria/src/main/resources/pizzeria_model.yaml
index 25676fb0..589adab2 100644
--- a/nlpcraft-examples/pizzeria/src/main/resources/pizzeria_model.yaml
+++ b/nlpcraft-examples/pizzeria/src/main/resources/pizzeria_model.yaml
@@ -34,14 +34,13 @@ elements:
     description: "Kinds of drinks."
     values:
       "tea": [ ]
-      "green tea": [ ]
       "coffee": [ ]
       "cola": ["{coca|cola|coca cola|cocacola|coca-cola}"]
 
   - id: "ord:yes"
     description: "Conformation (yes)."
     synonyms:
-      - "{yes|yeah|right|fine|nice|excellent|good|correct|sure}"
+      - "{yes|yeah|right|fine|nice|excellent|good|correct|sure|ok}"
       - "{you are|_} {correct|right}"
 
   - id: "ord:no"
@@ -70,3 +69,4 @@ elements:
     synonyms:
       - "{menu|carte|card}"
       - "{products|goods|food|_} list"
+      - "{hi|help|hallo}"
\ No newline at end of file
diff --git a/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModelSpec.scala b/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModelSpec.scala
index 1478a693..109f2069 100644
--- a/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModelSpec.scala
+++ b/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/PizzeriaModelSpec.scala
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.*
 import scala.language.implicitConversions
 import scala.util.Using
 import scala.collection.mutable
+
 /**
   *
   */
@@ -67,7 +68,7 @@ class PizzeriaModelSpec:
                 case None => // No-op.
             println()
 
-        require(errs.isEmpty)
+        require(errs.isEmpty, s"There are ${errs.size} errors above.")
 
     private def dialog(exp: PizzeriaOrder, reqs: (String, NCResultType)*): Unit =
         val testMsgs = mutable.ArrayBuffer.empty[String]
@@ -114,10 +115,10 @@ class PizzeriaModelSpec:
     def test(): Unit =
         given Conversion[String, (String, NCResultType)] with
             def apply(txt: String): (String, NCResultType) = (txt, ASK_DIALOG)
-            
+
         dialog(
-            new Builder().withDrink("tea", 1).build,
-            "One tea",
+            new Builder().withDrink("tea", 2).build,
+            "Two tea",
             "yes",
             "yes" -> ASK_RESULT
         )
@@ -176,12 +177,35 @@ class PizzeriaModelSpec:
         dialog(
             new Builder().
                 withPizza("margherita", "small", 2).
-                withPizza("marinara", "small", 3).
-                withDrink("tea", 1).
+                withPizza("marinara", "small", 1).
+                withDrink("tea", 3).
                 build,
             "margherita two, marinara and three tea",
             "small",
             "small",
             "yes",
             "yes" -> ASK_RESULT
+        )
+
+        dialog(
+            new Builder().
+                withPizza("margherita", "small", 2).
+                withPizza("marinara", "large", 1).
+                withDrink("cola", 3).
+                build,
+            "small margherita two, marinara big one and three cola",
+            "yes",
+            "yes" -> ASK_RESULT
+        )
+
+        dialog(
+            new Builder().
+                withPizza("margherita", "small", 1).
+                withPizza("marinara", "large", 2).
+                withDrink("coffee", 2).
+                build,
+            "small margherita, 2 marinara and 2 coffee",
+            "large",
+            "yes",
+            "yes" -> ASK_RESULT
         )
\ No newline at end of file
diff --git a/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelClientCli.scala b/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelClientCli.scala
index 91c31f96..3690a84e 100644
--- a/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelClientCli.scala
+++ b/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelClientCli.scala
@@ -28,6 +28,14 @@ import java.net.http.HttpRequest.*
 import java.net.http.HttpResponse.*
 import scala.util.Using
 
+/**
+  * Use it for Pizzeria Model test.
+  *  - Run model server (PizzeriaModelServer) as application.
+  *  - Run client CLI (PizzeriaModelClientCli) as application.
+  *  Order pizza via CLI client.
+  *
+  *  Note that it supports only one default test user and only one user session at the same time.
+  */
 object PizzeriaModelClientCli extends LazyLogging :
     private val client = HttpClient.newHttpClient()
 
@@ -51,7 +59,8 @@ object PizzeriaModelClientCli extends LazyLogging :
     def main(args: Array[String]): Unit =
         println("Application started.")
 
-        // Clears possible saved sessions.tea
+        // Clears possible saved sessions.
+        // it is necessary because this test client/server implementation supports only one session for same test user.
         ask("stop")
 
         var applStarted = true
@@ -66,7 +75,6 @@ object PizzeriaModelClientCli extends LazyLogging :
 
             try
                 var in = scala.io.StdIn.readLine()
-
                 if in != null then
                     in = in.trim
                     if in.nonEmpty then println(ask(in))
diff --git a/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelServer.scala b/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelServer.scala
index ba9ad12c..aa8fd12a 100644
--- a/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelServer.scala
+++ b/nlpcraft-examples/pizzeria/src/test/java/org/apache/nlpcraft/examples/pizzeria/cli/PizzeriaModelServer.scala
@@ -27,7 +27,12 @@ import java.net.InetSocketAddress
 import scala.util.Using
 
 /**
-  *
+  * Use it for Pizzeria Model test.
+  *  - Run model server (PizzeriaModelServer) as application.
+  *  - Run client CLI (PizzeriaModelClientCli) as application.
+  *  Order pizza via CLI client.
+  *  
+  *  Note that it supports only one default test user and only one user session at the same time.  
   */
 object PizzeriaModelServer:
     private val host = "localhost"
@@ -56,7 +61,7 @@ object PizzeriaModelServer:
                             case "POST" => Using.resource(new BufferedReader(new InputStreamReader(e.getRequestBody))) { _.readLine }
                             case _ => throw new Exception(s"Unsupported request method: ${e.getRequestMethod}")
 
-                    if req == null || req.isEmpty then Exception(s"Empty request")
+                    if req == null || req.isEmpty then Exception(s"Empty request.")
 
                     val resp = nlpClient.ask(req, null, "userId")
                     val prompt = if resp.getType == ASK_DIALOG then "(Your should answer on the model's question below)\n" else ""