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 <skhdlem...@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 ""