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


The following commit(s) were added to refs/heads/NLPCRAFT-491 by this push:
     new 81eab344 Minor fixes.
81eab344 is described below

commit 81eab344df26f44b0432b20e529d53e158187a4c
Author: Sergey Kamov <skhdlem...@gmail.com>
AuthorDate: Tue Apr 5 18:52:08 2022 +0300

    Minor fixes.
---
 .../nlpcraft/examples/order/OrderModel.scala       | 329 ++++++++++-----------
 .../nlpcraft/examples/order/OrderState.scala       | 105 ++-----
 .../order/components/DrinkQtyExtender.scala        |  32 ++
 .../examples/order/components/OrderValidator.scala |  34 +++
 ...SimpleCombiner.scala => PizzaQtyExtender.scala} |  39 ++-
 ...impleCombiner.scala => PizzaSizeExtender.scala} |  39 ++-
 .../order/src/main/resources/order_model.yaml      |  38 +--
 7 files changed, 303 insertions(+), 313 deletions(-)

diff --git 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderModel.scala
 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderModel.scala
index 6962b5a3..ec706567 100644
--- 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderModel.scala
+++ 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderModel.scala
@@ -28,195 +28,172 @@ import org.apache.nlpcraft.nlp.entity.parser.*
 
 import scala.collection.mutable
 import org.apache.nlpcraft.NCResultType.*
-import org.apache.nlpcraft.examples.order.components.SimpleCombiner
-import org.apache.nlpcraft.nlp.entity.parser.semantic.{NCSemanticEntityParser, 
NCSemanticStemmer}
-import org.apache.nlpcraft.nlp.entity.parser.stanford.NCStanfordNLPEntityParser
-import org.apache.nlpcraft.nlp.token.parser.stanford.NCStanfordNLPTokenParser
+import org.apache.nlpcraft.examples.order.components.*
+import org.apache.nlpcraft.nlp.entity.parser.semantic.*
+import org.apache.nlpcraft.nlp.entity.parser.stanford.*
+import org.apache.nlpcraft.nlp.token.parser.stanford.*
 
-import java.util.Properties
 import scala.jdk.CollectionConverters.*
+import java.util.Properties
+import scala.jdk.OptionConverters._
+
+object StanfordEn:
+    val PIPELINE: NCPipeline =
+        val stanford =
+            val props = new Properties()
+            props.setProperty("annotators", "tokenize, ssplit, pos, lemma, 
ner")
+            new StanfordCoreNLP(props)
+        val tokParser = new NCStanfordNLPTokenParser(stanford)
+        val stemmer = new NCSemanticStemmer():
+            private val ps = new PorterStemmer
+            override def stem(txt: String): String = ps.synchronized { 
ps.stem(txt) }
+
+        new NCPipelineBuilder().
+            withTokenParser(tokParser).
+            withEntityParser(new NCStanfordNLPEntityParser(stanford, 
"number")).
+            withEntityParser(new NCSemanticEntityParser(stemmer, tokParser, 
"order_model.yaml")).
+            withEntityMappers(Seq(new PizzaSizeExtender, new PizzaQtyExtender, 
new DrinkQtyExtender).asJava).
+            withEntityValidator(new OrderValidator).
+            build()
 
 object OrderModel extends LazyLogging:
-    private val STANFORD =
-        val props = new Properties()
-        props.setProperty("annotators", "tokenize, ssplit, pos, lemma, ner")
-        new StanfordCoreNLP(props)
-    private val TOK_PARSER = new NCStanfordNLPTokenParser(STANFORD)
-
-    private def extractPizzaKind(e: NCEntity): String = 
e.get[String]("ord:pizza:kind:value")
-    private def extractPizzaSize(e: NCEntity): PizzaSize = 
PizzaSize.valueOf(e.get[String]("ord:pizza:size:value").toUpperCase)
-    private def extractDrink(e: NCEntity): String = 
e.get[String]("ord:drink:value")
-    private def isStopWord(t: NCToken): Boolean = t.get[Boolean]("stopword")
-    private def confirmOrSpecify(ord: OrderState): NCResult =
-        NCResult(if ord.isValid() then ord.ask2Confirm() else 
ord.ask2Specify(), ASK_DIALOG)
-    private def continue(ord: OrderState): NCResult =
-        NCResult(if ord.isValid() then s"OK, please continue your order: $ord" 
else ord.ask2Specify(), ASK_DIALOG)
-
-    private def log(o: OrderState): Unit = logger.info(o.getState())
-    private def onStart(im: NCIntentMatch, o: OrderState): Unit =
-        logger.info(s"Initial state before request: 
${im.getContext.getRequest.getText}")
-    private def onFinish(o: OrderState): Unit =
-        logger.info(s"Result state")
-        logger.info(o.getState())
-
-    private def withRelations(
-        ents: Seq[NCEntity], relations: Seq[NCEntity], skip: Seq[NCEntity], 
allToks: Seq[NCToken]
-    ): Map[NCEntity, NCEntity] =
-        case class IdxHolder(from: Int, to: Int)
-
-        def getIndexes(e: NCEntity): IdxHolder =
-            val toks = e.getTokens.asScala
-            IdxHolder(toks.head.getIndex, toks.last.getIndex)
-
-        val skipIdxs = skip.flatMap(_.getTokens.asScala.map(_.getIndex)).toSet
-        val used = mutable.ArrayBuffer.empty[NCEntity]
-
-        def areNeighbours(i1: Int, i2: Int): Boolean =
-            if i2 == i1 + 1 then true else Range(i1 + 1, i2).forall(i => 
isStopWord(allToks(i)) || skipIdxs.contains(i))
-
-        ents.map(e => {
-            val eIdxs = getIndexes(e)
-            val r = relations.filter(r => !used.contains(r)).find(r => {
-                val rIdxs = getIndexes(r)
-                areNeighbours(rIdxs.to, eIdxs.from) || areNeighbours(eIdxs.to, 
rIdxs.from)
-            }).orNull
-
-            if r != null then used += r
-            e -> r
-        }).toMap
+    private def norm(s: String) = s.trim.replaceAll("(?m)^[ \t]*\r?\n", "")
+    private def withComma[T](iter: Iterable[T]): String = iter.mkString(", ")
+    private def seq2Str[T](name: String, seq: Iterable[T]): String = if 
seq.nonEmpty then s"$name: ${withComma(seq)}." else ""
+
+    private def extractPizzaSize(e: NCEntity): String = 
e.get[String]("ord:pizza:size:value")
+    private def extractQty(e: NCEntity, qty: String): Option[Int] = 
Option.when(e.contains(qty))(e.get[String](qty).toInt)
+    private def extractPizza(e: NCEntity): Pizza =
+        Pizza(e.get[String]("ord:pizza:value"), 
e.getOpt[String]("ord:pizza:size").toScala, extractQty(e, "ord:pizza:qty"))
+    private def extractDrink(e: NCEntity): Drink = 
Drink(e.get[String]("ord:drink:value"), extractQty(e, "ord:drink:qty"))
+
+    private def getContent(o: OrderState): String =
+        s"""
+       |${seq2Str("Pizza", o.getPizzas.values.map(p => s"${p.name} 
${p.size.getOrElse("undefined")} ${p.qty.getOrElse(1)}"))}
+       |${seq2Str("Drinks", o.getDrinks.values.map(p => s"${p.name} 
${p.qty.getOrElse(1)}"))}
+        """.stripMargin
+
+
+    private def toString(o: OrderState): String =
+        norm(
+            s"""
+           |Order
+           |${getContent(o)}
+            """.stripMargin
+        )
 
 import org.apache.nlpcraft.examples.order.OrderModel.*
 /**
   *
   */
 class OrderModel extends NCModelAdapter (
-    new NCModelConfig("nlpcraft.order.ex", "Order Example Model", "1.0"),
-    new NCPipelineBuilder().
-        withTokenParser(TOK_PARSER).
-        withEntityParser(new NCStanfordNLPEntityParser(STANFORD, "number")).
-        withEntityParser(new NCSemanticEntityParser(
-            new NCSemanticStemmer():
-                final private val ps = new PorterStemmer
-                override def stem(txt: String): String = ps.synchronized { 
ps.stem(txt) }
-            ,
-            TOK_PARSER,
-            "order_model.yaml"
-        )).
-//        withEntityMappers(
-//            Seq(
-//                SimpleCombiner("ord:pizza:size", "ord:pizza:kind", "x"),
-//                SimpleCombiner("x", "stanford:number", "")
-//            ).asJava
-//        ).
-        build()
+    new NCModelConfig("nlpcraft.order.ex", "Order Example Model", "1.0"), 
StanfordEn.PIPELINE
 ) with LazyLogging:
     private val ords = mutable.HashMap.empty[String, OrderState]
 
     private def getOrder(im: NCIntentMatch): OrderState = 
ords.getOrElseUpdate(im.getContext.getRequest.getUserId, new OrderState)
-
-    @NCIntent("intent=confirm term(confirm)={has(ent_groups, 'confirm')}")
-    def onConfirm(im: NCIntentMatch, @NCIntentTerm("confirm") confirm: 
NCEntity): NCResult =
-        val ord = getOrder(im)
-        onStart(im, ord)
-        val dlg = im.getContext.getConversation.getDialogFlow
-
-        def cancelAll(): NCResult =
-            ord.clear()
-            im.getContext.getConversation.clearStm(_ => true)
-            im.getContext.getConversation.clearDialog(_ => true)
-            NCResult("Order canceled. We are ready for new orders.", 
ASK_RESULT)
-
-        val res =
-            // 'stop' command.
-            if !dlg.isEmpty && dlg.get(dlg.size() - 
1).getIntentMatch.getIntentId == "stop" then
-                confirm.getId match
-                    case "ord:confirm:yes" => cancelAll()
-                    case "ord:confirm:no" => confirmOrSpecify(ord)
-                    case "ord:confirm:continue" => continue(ord)
-                    case _ => throw new AssertionError()
-            // 'confirm' command.
-            else
-                if !ord.inProgress() then throw new NCRejection("No orders in 
progress.")
-
-                confirm.getId match
-                    case "ord:confirm:yes" =>
-                        if ord.isValid() then
-                            logger.info(s"ORDER EXECUTED: $ord")
-                            ord.clear()
-                            NCResult("Congratulations. Your order executed. 
You can start make new orders.", ASK_RESULT)
-                        else
-                            NCResult(ord.ask2Specify(), ASK_DIALOG)
-                    case "ord:confirm:no" => cancelAll()
-                    case "ord:confirm:continue" => continue(ord)
-                    case _ => throw new AssertionError()
-
-        onFinish(ord)
-        res
+    private def getLastIntentId(im: NCIntentMatch): Option[String] =
+        im.getContext.getConversation.getDialogFlow.asScala.lastOption match
+            case Some(e) => Some(e.getIntentMatch.getIntentId)
+            case None => None
+
+    private def mkOrderFinishDialog(o: OrderState): NCResult =
+        if o.isValid then new NCResult("Is order ready?", ASK_DIALOG)
+        else NCResult(s"What is size size (large, medium or small) for: 
${o.getPizzaNoSize.name}", ASK_DIALOG)
+
+    private def mkOrderContinueDialog(o: OrderState): NCResult =
+        require(o.inProgress)
+        NCResult("OK. Please continue", ASK_DIALOG)
+
+    private def mkOrderReadyDialog(o: OrderState): NCResult =
+        require(o.isValid)
+        NCResult(
+            norm(
+                s"""
+                   |Let me specify your order.
+                   |${getContent(o)}
+                   |Is it correct?
+                """.stripMargin
+            ),
+            ASK_DIALOG
+        )
+
+    private def mkClearResult(im: NCIntentMatch, o: OrderState): NCResult =
+        o.clear()
+        val conv = im.getContext.getConversation
+        conv.clearStm(_ => true)
+        conv.clearDialog(_ => true)
+        NCResult("Order canceled. We are ready for new orders.", ASK_RESULT)
+
+    private def mkExecuteResult(o: OrderState): NCResult =
+        println(s"EXECUTED:")
+        println(OrderModel.toString(o))
+        o.clear()
+        NCResult("Congratulations. Your order executed. You can start make new 
orders.", ASK_RESULT)
+
+    @NCIntent("intent=yes term(yes)={# == 'ord:yes'}")
+    def onYes(im: NCIntentMatch, @NCIntentTerm("yes") yes: NCEntity): NCResult 
=
+        val o = getOrder(im)
+        val lastIntentId = getLastIntentId(im).orNull
+
+        if lastIntentId == "stop" then mkOrderContinueDialog(o)
+        else if o.isWait4Approve then mkClearResult(im, o)
+        else mkOrderFinishDialog(o)
+
+    @NCIntent("intent=no term(no)={# == 'ord:no'}")
+    def onNo(im: NCIntentMatch, @NCIntentTerm("no") no: NCEntity): NCResult =
+        val o = getOrder(im)
+        val lastIntentId = getLastIntentId(im).orNull
+
+        if lastIntentId == "stop" then mkOrderContinueDialog(o)
+        else if o.isWait4Approve then mkClearResult(im, o)
+        else mkOrderFinishDialog(o)
 
     @NCIntent("intent=stop term(stop)={# == 'ord:stop'}")
-        def onStop(im: NCIntentMatch, @NCIntentTerm("stop") stop: NCEntity): 
NCResult =
-            val ord = getOrder(im)
-            onStart(im, ord)
-            val res =
-                if ord.inProgress() then NCResult("Are you sure that you want 
to cancel current order?", ASK_DIALOG)
-                else NCResult("Nothing to cancel", ASK_RESULT)
-            onFinish(ord)
-            res
-
-    @NCIntent(
-        "intent=order " +
-        "  term(common)={# == 'ord:common'}* " +
-        "  term(pizzaList)={# == 'ord:pizza:kind'}*" +
-        "  term(pizzaSizesList)={# == 'ord:pizza:size'}* " +
-        "  term(drinkList)={# == 'ord:drink'}*"
-    )
-    def onCommonOrder(
-        im: NCIntentMatch,
-        @NCIntentTerm("common") common: List[NCEntity],
-        @NCIntentTerm("pizzaList") pizzaKinds: List[NCEntity],
-        @NCIntentTerm("pizzaSizesList") pizzaSizes: List[NCEntity],
-        @NCIntentTerm("drinkList") drinks: List[NCEntity]
-    ): NCResult =
-        if pizzaKinds.isEmpty && drinks.isEmpty then throw new 
NCRejection("Please order some pizza or drinks")
-        if pizzaSizes.size > pizzaKinds.size then throw new NCRejection("Pizza 
and their sizes cannot be recognized")
-
-        val ord = getOrder(im)
-        onStart(im, ord)
-        val m = withRelations(pizzaKinds, pizzaSizes, Seq.empty, 
im.getContext.getTokens.asScala.toSeq)
-
-        for ((p, sz) <- m)
-            val pz = extractPizzaKind(p)
-            if sz != null then ord.addPizza(pz, extractPizzaSize(sz)) else 
ord.addPizza(pz)
-
-        for (p <- drinks.map(extractDrink)) ord.addDrink(p)
-
-        val res = confirmOrSpecify(ord)
-        onFinish(ord)
-        res
-
-    @NCIntent(
-        "intent=specifyPizzaSize " +
-        "  term(common)={# == 'ord:common'}* " +
-        "  term(size)={# == 'ord:pizza:size'} " +
-        "  term(pizza)={# == 'ord:pizza:kind'}?"
-    )
-    def onSpecifyPizzaSize(
-        im: NCIntentMatch,
-        @NCIntentTerm("common") common: List[NCEntity],
-        @NCIntentTerm("size") size: NCEntity,
-        @NCIntentTerm("pizza") pizzaOpt: Option[NCEntity]
-    ): NCResult =
-        val ord = getOrder(im)
-        require(!ord.isValid())
-
-        onStart(im, ord)
-
-        val sz = extractPizzaSize(size)
-
-        pizzaOpt match
-            case Some(pizza) => ord.addPizza(extractPizzaKind(pizza), sz)
-            case None => if !ord.specifyPizzaSize(sz) then throw new 
NCRejection("What specified?")
-
-        val res = confirmOrSpecify(ord)
-        onFinish(ord)
-        res
\ No newline at end of file
+    def onStop(im: NCIntentMatch, @NCIntentTerm("stop") stop: NCEntity): 
NCResult =
+        val o = getOrder(im)
+
+        if o.inProgress then NCResult("Are you sure that you want to cancel 
current order?", ASK_DIALOG)
+        else NCResult("Nothing to cancel.", ASK_RESULT)
+
+    @NCIntent("intent=order term(ps)={# == 'ord:pizza'}* term(ds)={# == 
'ord:drink'}*")
+    def onOrder(im: NCIntentMatch, @NCIntentTerm("ps") ps: List[NCEntity], 
@NCIntentTerm("ds") ds: List[NCEntity]): NCResult =
+        if ps.isEmpty && ds.isEmpty then throw new NCRejection("Please order 
some pizza or drinks")
+
+        val o = getOrder(im)
+
+        for (p <- ps) o.addPizza(extractPizza(p))
+        for (d <- ds) o.addDrink(extractDrink(d))
+
+        mkOrderFinishDialog(o)
+
+    @NCIntent("intent=orderPizzaSize term(size)={# == 'ord:pizza:size'}")
+    def onOrderPizzaSize(im: NCIntentMatch, @NCIntentTerm("size") size: 
NCEntity): NCResult =
+        val o = getOrder(im)
+
+        if !o.inProgress then throw NCRejection("") // TODO
+        o.setPizzaNoSize(extractPizzaSize(size))
+
+        mkOrderFinishDialog(o)
+
+    @NCIntent("intent=status term(status)={# == 'ord:status'}")
+    def onStatus(im: NCIntentMatch, @NCIntentTerm("status") s: NCEntity): 
NCResult =
+        val o = getOrder(im)
+
+        if o.inProgress then NCResult(OrderModel.toString(o), ASK_RESULT)
+        else NCResult("Nothing ordered.", ASK_RESULT)
+
+    @NCIntent("intent=finish term(finish)={# == 'ord:finish'}")
+    def onFinish(im: NCIntentMatch, @NCIntentTerm("finish") f: NCEntity): 
NCResult =
+        val o = getOrder(im)
+
+        if o.inProgress then mkOrderReadyDialog(o)
+        else NCResult("Nothing to finish.", ASK_RESULT)
+
+    @NCIntent("intent=menu term(menu)={# == 'ord:menu'}")
+    def onMenu(im: NCIntentMatch, @NCIntentTerm("menu") m: NCEntity): NCResult 
=
+        NCResult(
+            "There are margherita, marbonara and marinara. Sizes: large, 
medium or small. " +
+            "Also there are tea, gren tea, coffee and cola.",
+            ASK_RESULT
+        )
\ No newline at end of file
diff --git 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderState.scala
 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderState.scala
index b1c128e2..ba99ced2 100644
--- 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderState.scala
+++ 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/OrderState.scala
@@ -19,83 +19,36 @@ package org.apache.nlpcraft.examples.order
 
 import scala.collection.mutable
 
-private enum PizzaSize:
-    case SMALL, MEDIUM, LARGE
+case class Pizza(name: String, var size: Option[String], qty: Option[Int])
+case class Drink(name: String, qty: Option[Int])
 
-object OrderState:
-    private val allPizzaSizeKinds: String = 
withComma(PizzaSize.values.map(_.toString.toLowerCase))
-
-    private def withComma(iter: Iterable[String]): String = iter.mkString(", ")
-    private def pizza2Str(name: String, size: PizzaSize): String =
-        if size != null then s"$name ${size.toString.toLowerCase} size" else 
name
-    private def seq2Str[T](name: String, seq: Seq[T], toStr: T => String = (t: 
T) => t.toString): String =
-        if seq.nonEmpty then s"$name: ${withComma(seq.map(toStr))}." else ""
-    private def norm(s: String) = s.trim.replaceAll("(?m)^[ \t]*\r?\n", "")
-
-import OrderState.*
-
-/**
-  * Contract.
-  * 1. 'mkSpecifyRequest' scans ordered data, finds first invalid element and 
asks to specify it.
-  * 2. 'specify' methods (specifyPizzaSize) should specify elements in the 
same order.
-  * So, we don't need to save which concrete element we treying to specify.
-  */
 class OrderState:
-    private val pizza = mutable.LinkedHashMap.empty[String, PizzaSize]
-    private val drinks = mutable.LinkedHashSet.empty[String]
-
-    def addDrink(name: String): Unit = drinks += name
-    def addPizza(name: String): Unit = pizza += name -> null
-    def addPizza(name: String, size: PizzaSize): Unit = pizza += name -> size
-
-    def inProgress(): Boolean = pizza.nonEmpty || drinks.nonEmpty
-
-    def isValid(): Boolean =
-        (pizza.nonEmpty || drinks.nonEmpty) &&
-        (pizza.isEmpty || pizza.forall{ (_, size) => size != null } )
-
-    def specifyPizzaSize(sz: PizzaSize): Boolean = pizza.find { (_, size) => 
size == null } match
-        case Some((name, _)) => pizza += name -> sz; true
-        case None => false
-
-    def ask2Specify(): String =
-        require(!isValid())
-
-        if pizza.isEmpty && drinks.isEmpty then
-            "Order is empty. Please order some pizza or drinks."
-        else
-            pizza.find { (_, size) => size == null } match
-                case Some((name, _)) => s"Please specify $name size? It can be 
$allPizzaSizeKinds"
-                case None => throw new AssertionError("Invalid state")
-
-    def ask2Confirm(): String =
-        require(isValid())
-        norm(
-            s"""
-           |Let me specify your order.
-           |${seq2Str("Pizza", pizza.toSeq, pizza2Str)}
-           |${seq2Str("Drinks", drinks.toSeq)}
-           |Is it correct?
-           """.stripMargin
-        )
-
-    def getState(): String =
-        norm(
-            s"""
-               |Current order state: '${if inProgress() then "in progress" 
else "empty"}'.
-               |${seq2Str("Pizza", pizza.toSeq, pizza2Str)}
-               |${seq2Str("Drinks", drinks.toSeq)}
-           """.stripMargin
-        )
-    
-    override def toString(): String =
-        norm(
-            s"""
-           |${seq2Str("Pizza", pizza.toSeq, pizza2Str)}
-           |${seq2Str("Drinks", drinks.toSeq)}
-           """.stripMargin
-        ).replaceAll("\n", " ")
-
+    private val pizzas = mutable.LinkedHashMap.empty[String, Pizza]
+    private val drinks = mutable.LinkedHashMap.empty[String, Drink]
+
+    private var wait4Appr = false
+
+    private def findPizzaNoSize: Option[Pizza] = 
pizzas.values.find(_.size.isEmpty)
+
+    def addPizza(p: Pizza): Unit = pizzas += p.name -> p
+    def addDrink(d: Drink): Unit = drinks += d.name -> d
+
+    def getPizzas: Map[String, Pizza] = pizzas.toMap
+    def getDrinks: Map[String, Drink] = drinks.toMap
+
+    def inProgress: Boolean = pizzas.nonEmpty || drinks.nonEmpty
+    def isValid: Boolean = (pizzas.nonEmpty || drinks.nonEmpty) && 
pizzas.forall(_._2.size.nonEmpty)
+    def isWait4Approve: Boolean = wait4Appr
+    def wait4Approve(): Unit = wait4Appr = true
+    def getPizzaNoSize: Pizza =
+        require(!isValid)
+        findPizzaNoSize.get
+    def setPizzaNoSize(size: String): Unit =
+        require(!isValid)
+        require(size != null)
+        findPizzaNoSize.get.size = Option(size)
     def clear(): Unit =
-        pizza.clear()
+        pizzas.clear()
         drinks.clear()
+
+
diff --git 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/DrinkQtyExtender.scala
 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/DrinkQtyExtender.scala
new file mode 100644
index 00000000..e3158153
--- /dev/null
+++ 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/DrinkQtyExtender.scala
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.examples.order.components
+
+import org.apache.nlpcraft.*
+
+import java.util
+import java.util.List as JList
+import scala.collection.mutable
+import scala.jdk.CollectionConverters.*
+
+/**
+  *
+  */
+class DrinkQtyExtender extends NCEntityMapper:
+    private def extract(e: NCEntity): mutable.Seq[NCToken] = 
e.getTokens.asScala
+    override def map(req: NCRequest, cfg: NCModelConfig, entities: 
util.List[NCEntity]): util.List[NCEntity] = entities
diff --git 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/OrderValidator.scala
 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/OrderValidator.scala
new file mode 100644
index 00000000..dc51c804
--- /dev/null
+++ 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/OrderValidator.scala
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.examples.order.components
+
+import org.apache.nlpcraft.*
+
+import java.util
+import scala.jdk.CollectionConverters.*
+
+/**
+  *
+  */
+class OrderValidator extends NCEntityValidator:
+    override def validate(req: NCRequest, cfg: NCModelConfig, ents: 
util.List[NCEntity]): Unit =
+        val es = ents.asScala
+
+        if !es.exists(_.getId == "ord:pizza") then
+            if es.count(_.getId == "stanford:number") > 1 then throw new 
NCRejection("Error1") // TODO:
+            if es.count(_.getId == "ord:pizza:size") > 1 then throw new 
NCRejection("Error2") // TODO:
\ No newline at end of file
diff --git 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/SimpleCombiner.scala
 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/PizzaQtyExtender.scala
similarity index 60%
copy from 
nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/SimpleCombiner.scala
copy to 
nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/PizzaQtyExtender.scala
index 28f49626..5499f6d6 100644
--- 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/SimpleCombiner.scala
+++ 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/PizzaQtyExtender.scala
@@ -26,38 +26,35 @@ import scala.jdk.CollectionConverters.*
 
 /**
   *
-  * @param id1
-  * @param id2
-  * @param newId
   */
-case class SimpleCombiner(id1: String, id2: String, newId: String) extends 
NCEntityMapper:
+class PizzaQtyExtender extends NCEntityMapper:
     private def extract(e: NCEntity): mutable.Seq[NCToken] = 
e.getTokens.asScala
     override def map(req: NCRequest, cfg: NCModelConfig, entities: 
util.List[NCEntity]): util.List[NCEntity] =
         var es = entities.asScala
-        val es1 = es.filter(_.getId == id1)
-        val es2 = es.filter(_.getId == id2)
+        val pizzas = es.filter(_.getId == "ord:pizza")
+        val nums = es.filter(_.getId == "stanford:number")
 
-        if es1.nonEmpty && es2.size == es1.size then
+        if pizzas.nonEmpty && nums.nonEmpty then
+            if pizzas.size != nums.size then throw new NCRejection("Pizza and 
their nums should be defined together1")
             var ok = true
-
             val mapped =
-                for ((e1, e2) <- es1.zip(es2) if ok) yield
+                for ((e1, e2) <- pizzas.zip(nums) if ok) yield
                     if e1.getId == e2.getId then
                         ok = false
                         null
                     else
+                        val (pizza, num) = if e1.getId == "ord:pizza" then 
(e1, e2) else (e2, e1)
                         new NCPropertyMapAdapter with NCEntity:
-                            override val getTokens: JList[NCToken] = 
(extract(e1) ++ extract(e2)).sortBy(_.getIndex).asJava
-                            override val getRequestId: String = 
req.getRequestId
-                            override val getId: String = newId
-
-            if ok then
-                es = es --= es1
-                es = es --= es2
-                (es ++ mapped).sortBy(extract(_).head.getIndex).asJava
-            else
-                entities
-        else
-            entities
+                            // Copy from pizza.
+                            pizza.keysSet().forEach(k => put(k, pizza.get(k)))
+                            // New value from size.
+                            put[String]("ord:pizza:qty", 
num.get[String]("stanford:number:nne").toLowerCase)
 
+                            override val getTokens: JList[NCToken] = 
(extract(pizza) ++ extract(num)).sortBy(_.getIndex).asJava
+                            override val getRequestId: String = 
req.getRequestId
+                            override val getId: String = pizza.getId
 
+            es = es --= pizzas
+            es = es --= nums
+            (es ++ mapped).sortBy(extract(_).head.getIndex).asJava
+        else entities
\ No newline at end of file
diff --git 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/SimpleCombiner.scala
 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/PizzaSizeExtender.scala
similarity index 60%
rename from 
nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/SimpleCombiner.scala
rename to 
nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/PizzaSizeExtender.scala
index 28f49626..52106f25 100644
--- 
a/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/SimpleCombiner.scala
+++ 
b/nlpcraft-examples/order/src/main/java/org/apache/nlpcraft/examples/order/components/PizzaSizeExtender.scala
@@ -26,38 +26,35 @@ import scala.jdk.CollectionConverters.*
 
 /**
   *
-  * @param id1
-  * @param id2
-  * @param newId
   */
-case class SimpleCombiner(id1: String, id2: String, newId: String) extends 
NCEntityMapper:
+class PizzaSizeExtender extends NCEntityMapper:
     private def extract(e: NCEntity): mutable.Seq[NCToken] = 
e.getTokens.asScala
     override def map(req: NCRequest, cfg: NCModelConfig, entities: 
util.List[NCEntity]): util.List[NCEntity] =
         var es = entities.asScala
-        val es1 = es.filter(_.getId == id1)
-        val es2 = es.filter(_.getId == id2)
+        val pizzas = es.filter(_.getId == "ord:pizza")
+        val sizes = es.filter(_.getId == "ord:pizza:size")
 
-        if es1.nonEmpty && es2.size == es1.size then
+        if pizzas.nonEmpty && sizes.nonEmpty then
+            if pizzas.size != sizes.size then throw new NCRejection("Pizza and 
their sizes should be defined together1")
             var ok = true
-
             val mapped =
-                for ((e1, e2) <- es1.zip(es2) if ok) yield
+                for ((e1, e2) <- pizzas.zip(sizes) if ok) yield
                     if e1.getId == e2.getId then
                         ok = false
                         null
                     else
+                        val (pizza, size) = if e1.getId == "ord:pizza" then 
(e1, e2) else (e2, e1)
                         new NCPropertyMapAdapter with NCEntity:
-                            override val getTokens: JList[NCToken] = 
(extract(e1) ++ extract(e2)).sortBy(_.getIndex).asJava
-                            override val getRequestId: String = 
req.getRequestId
-                            override val getId: String = newId
-
-            if ok then
-                es = es --= es1
-                es = es --= es2
-                (es ++ mapped).sortBy(extract(_).head.getIndex).asJava
-            else
-                entities
-        else
-            entities
+                            // Copy from pizza.
+                            size.keysSet().forEach(k => put(k, size.get(k)))
+                            // New value from size.
+                            put[String]("ord:pizza:size", 
size.get[String]("ord:pizza:size:value").toLowerCase)
 
+                            override val getTokens: JList[NCToken] = 
(extract(pizza) ++ extract(size)).sortBy(_.getIndex).asJava
+                            override val getRequestId: String = 
req.getRequestId
+                            override val getId: String = pizza.getId
 
+            es = es --= pizzas
+            es = es --= sizes
+            (es ++ mapped).sortBy(extract(_).head.getIndex).asJava
+        else entities
\ No newline at end of file
diff --git a/nlpcraft-examples/order/src/main/resources/order_model.yaml 
b/nlpcraft-examples/order/src/main/resources/order_model.yaml
index 798b1f29..400b8e5c 100644
--- a/nlpcraft-examples/order/src/main/resources/order_model.yaml
+++ b/nlpcraft-examples/order/src/main/resources/order_model.yaml
@@ -16,14 +16,7 @@
 #
 
 elements:
-  - id: "ord:common"
-    description: "Common words for order."
-    synonyms:
-      - "{pizza|food}"
-      - "{drink|lemonade}"
-      - "{buy|order|bring|delivery}"
-
-  - id: "ord:pizza:kind"
+  - id: "ord:pizza"
     description: "Kinds of pizza."
     values:
       "margherita": [ ]
@@ -45,27 +38,34 @@ elements:
       "coffee": [ ]
       "cola": ["{coca|cola|coca cola|cocacola|coca-cola}"]
 
-  - id: "ord:confirm:yes"
+  - id: "ord:yes"
     description: "Conformation (yes)."
-    groups: ["confirm"]
     synonyms:
       - "{yes|yeah|right|fine|nice|excellent|good}"
       - "{you are|_} {correct|right}"
 
-  - id: "ord:confirm:no"
+  - id: "ord:no"
     description: "Conformation (no)."
-    groups: ["confirm"]
     synonyms:
       - "{no|nope|incorrect|wrong}"
       - "{you are|_} {not|are not|aren't} {correct|right}"
 
-  - id: "ord:confirm:continue"
-    description: "Confirmation (continue)."
-    groups: ["confirm"]
-    synonyms:
-      - "continue"
-
   - id: "ord:stop"
     description: "Stop and cancel all."
     synonyms:
-      - "{stop|cancel} {it|all|everything|_}"
\ No newline at end of file
+      - "{stop|cancel} {it|all|everything|_}"
+
+  - id: "ord:status"
+    description: "Order status information."
+    synonyms:
+      - "{order|_} {status|state}"
+
+  - id: "ord:finish"
+    description: "Order finish."
+    synonyms:
+      - "{order|I|_} {is|are|_} {ready|done|finish}"
+
+  - id: "ord:menu"
+    description: "Order menu."
+    synonyms:
+      - "menu"

Reply via email to