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"