ex
ex
This example provides a simple pizza ordering model. It demonstrates how to work with dialogue systems which require confirmation logic.
Complexity:
Source code: GitHub
Review: All Examples at GitHub
You can create new Scala projects in many ways - we'll use SBT to accomplish this task. Make sure that build.sbt
file has the following content:
ThisBuild / version := "0.1.0-SNAPSHOT" ThisBuild / scalaVersion := "3.2.2" lazy val root = (project in file(".")) .settings( name := "NLPCraft Pizzeria Example", version := "1.0.0", libraryDependencies += "org.apache.nlpcraft" % "nlpcraft" % "1.0.0", libraryDependencies += "org.apache.nlpcraft" % "nlpcraft-stanford" % "1.0.0", libraryDependencies += "edu.stanford.nlp" % "stanford-corenlp" % "4.5.1", libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" % "test" )
NOTE: use the latest versions of Scala, ScalaTest and StanfordNLP library.
Create the following files so that resulting project structure would look like the following:
pizzeria_model.yaml
- YAML configuration file which contains model description.PizzeriaModel.scala
- Model implementation.PizzeriaOrder.scala
- Pizza order state representation.PizzeriaModelPipeline.scala
- Model pipeline definition class.PizzeriaOrderMapper.scala
- NCEntityMapper custom implementation.PizzeriaOrderValidator.scala
- NCEntityValidator custom implementation.PizzeriaModelSpec.scala
- Test that allows to test your model.| build.sbt +--project | build.properties \--src +--main | +--resources | | pizzeria_model.yaml | \--scala | \--demo | \--components | PizzeriaModelPipeline.scala | PizzeriaOrderMapper.scala | PizzeriaOrderValidator.scala | PizzeriaModel.scala | PizzeriaOrder.scala \--test \--scala \--demo PizzeriaModelSpec.scala
We are going to start with declaring the static part of our model using YAML which we will later load in our Scala-based model implementation. Open src/main/resources/pizzeria_model.yaml
file and replace its content with the following YAML:
elements: - type: "ord:pizza" description: "Kinds of pizza." values: "margherita": [ ] "carbonara": [ ] "marinara": [ ] - type: "ord:pizza:size" description: "Size of pizza." values: "small": [ "{small|smallest|min|minimal|tiny} {size|piece|_}" ] "medium": [ "{medium|intermediate|normal|regular} {size|piece|_}" ] "large": [ "{big|biggest|large|max|maximum|huge|enormous} {size|piece|_}" ] - type: "ord:drink" description: "Kinds of drinks." values: "tea": [ ] "coffee": [ ] "cola": [ "{pepsi|sprite|dr. pepper|dr pepper|fanta|soda|cola|coca cola|cocacola|coca-cola}" ] - type: "ord:yes" description: "Confirmation (yes)." synonyms: - "{yes|yeah|right|fine|nice|excellent|good|correct|sure|ok|exact|exactly|agree}" - "{you are|_} {correct|right}" - type: "ord:no" description: "Confirmation (no)." synonyms: - "{no|nope|incorrect|wrong}" - "{you are|_} {not|are not|aren't} {correct|right}" - type: "ord:stop" description: "Stop and cancel all." synonyms: - "{stop|cancel|clear|interrupt|quit|close} {it|all|everything|_}" - type: "ord:status" description: "Order status information." synonyms: - "{present|current|_} {order|_} {status|state|info|information}" - "what {already|_} ordered" - type: "ord:finish" description: "The order is over." synonyms: - "{i|everything|order|_} {be|_} {finish|ready|done|over|confirmed}" - type: "ord:menu" description: "Order menu." synonyms: - "{menu|carte|card}" - "{products|goods|food|item|_} list" - "{hi|help|hallo}"
Lines 2, 9, 16
define order elements which present parts of order.Lines 35, 40, 46, 51
define command elements which are used to control order state.Lines 23, 29
define elements which are used for commands confirmation or cancellation.YAML vs. API
As usual, this YAML-based static model definition is convenient but totally optional. All elements definitions can be provided programmatically inside Scala model PizzeriaModel
class as well.
Open src/main/scala/demo/PizzeriaOrder.scala
file and replace its content with the following code:
package demo import scala.collection.mutable import org.apache.nlpcraft.* enum PizzeriaOrderState: case DIALOG_EMPTY, DIALOG_IS_READY, DIALOG_SHOULD_CANCEL, DIALOG_SPECIFY, DIALOG_CONFIRM private object OrderPosition: val DFLT_QTY = 1 private trait OrderPosition: val name: String var qty: Option[Int] require(name != null && name.nonEmpty) case class Pizza(name: String, var size: Option[String], var qty: Option[Int]) extends OrderPosition: override def toString = s"$name '${size.getOrElse("undefined size")}' ${qty.getOrElse(OrderPosition.DFLT_QTY)} pcs" case class Drink(name: String, var qty: Option[Int]) extends OrderPosition: override def toString = s"$name ${qty.getOrElse(OrderPosition.DFLT_QTY)} pcs" import PizzeriaOrderState.* class PizzeriaOrder: private var state = DIALOG_EMPTY private val pizzas = mutable.ArrayBuffer.empty[Pizza] private val drinks = mutable.ArrayBuffer.empty[Drink] def isEmpty: Boolean = pizzas.isEmpty && drinks.isEmpty def isValid: Boolean = !isEmpty && findPizzaWithoutSize.isEmpty def add(ps: Seq[Pizza], ds: Seq[Drink]): Unit = def setByName[T <: OrderPosition](buf: mutable.ArrayBuffer[T], t: T): Unit = buf.find(_.name == t.name) match case Some(foundT) => if t.qty.nonEmpty then foundT.qty = t.qty case None => buf += t for (p <- ps) def setPizza(pred: Pizza => Boolean, notFound: => () => Unit): Unit = pizzas.find(pred) match case Some(foundPizza) => if p.size.nonEmpty then foundPizza.size = p.size if p.qty.nonEmpty then foundPizza.qty = p.qty case None => notFound() if p.size.nonEmpty then setPizza( x => x.name == p.name && x.size == p.size, () => setPizza(x => x.name == p.name && x.size.isEmpty, () => pizzas += p) ) else setByName(pizzas, p) for (d <- ds) setByName(drinks, d) def findPizzaWithoutSize: Option[Pizza] = pizzas.find(_.size.isEmpty) def fixPizzaWithoutSize(size: String): Boolean = findPizzaWithoutSize match case Some(p) => p.size = Option(size) true case None => false def getState: PizzeriaOrderState = state def setState(state: PizzeriaOrderState): Unit = this.state = state override def toString: String = if !isEmpty then val ps = if pizzas.nonEmpty then s"pizza: ${pizzas.mkString(", ")}" else "" val ds = if drinks.nonEmpty then s"drinks: ${drinks.mkString(", ")}" else "" if ds.isEmpty then ps else if ps.isEmpty then ds else s"$ps, $ds" else "nothing ordered"
Line 6
defines order states enumeration.Lines 17 and 20
define order parts classes definition.Line 25
defines pizza order state representation. Open src/main/scala/demo/PizzeriaModel.scala
file and replace its content with the following code:
package demo import com.typesafe.scalalogging.LazyLogging import org.apache.nlpcraft.* import org.apache.nlpcraft.NCResultType.* import org.apache.nlpcraft.annotations.* import demo.{PizzeriaOrder as Order, PizzeriaOrderState as State} import demo.PizzeriaOrderState.* import demo.components.PizzeriaModelPipeline import org.apache.nlpcraft.nlp.* object PizzeriaExtractors: def extractPizzaSize(e: NCEntity): String = e[String]("ord:pizza:size:value") def extractQty(e: NCEntity, qty: String): Option[Int] = Option.when(e.contains(qty))(e[String](qty).toDouble.toInt) def extractPizza(e: NCEntity): Pizza = Pizza( e[String]("ord:pizza:value"), e.get[String]("ord:pizza:size"), extractQty(e, "ord:pizza:qty") ) def extractDrink(e: NCEntity): Drink = Drink(e[String]("ord:drink:value"), extractQty(e, "ord:drink:qty")) import PizzeriaExtractors.* object PizzeriaModel extends LazyLogging: type Result = (NCResult, State) private val UNEXPECTED_REQUEST = new NCRejection("Unexpected request for current dialog context.") private def getCurrentOrder()(using ctx: NCContext): Order = val sess = ctx.getConversation.getData val usrId = ctx.getRequest.getUserId sess.get[Order](usrId) match case Some(ord) => ord case None => val ord = new Order() sess.put(usrId, ord) ord private def mkResult(msg: String): NCResult = NCResult(msg, ASK_RESULT) private def mkDialog(msg: String): NCResult = NCResult(msg, ASK_DIALOG) private def doRequest(body: Order => Result)(using ctx: NCContext, im: NCIntentMatch): NCResult = val o = getCurrentOrder() logger.info(s"Intent '${im.getIntentId}' activated for text: '${ctx.getRequest.getText}'.") logger.info(s"Before call [desc=${o.getState.toString}, resState: $o.") val (res, resState) = body.apply(o) o.setState(resState) logger.info(s"After call [desc=$o, resState: $resState.") res private def askIsReady(): Result = mkDialog("Is order ready?") -> DIALOG_IS_READY private def askSpecify(o: Order): Result = require(!o.isValid) o.findPizzaWithoutSize match case Some(p) => mkDialog(s"Choose size (large, medium or small) for: '${p.name}'") -> DIALOG_SPECIFY case None => require(o.isEmpty) mkDialog("Please order something. Ask `menu` to look what you can order.") -> DIALOG_SPECIFY private def askShouldStop(): Result = mkDialog("Should current order be canceled?") -> DIALOG_SHOULD_CANCEL private def doShowMenuResult(): NCResult = mkResult( "There are accessible for order: margherita, carbonara and marinara. " + "Sizes: large, medium or small. " + "Also there are tea, coffee and cola." ) private def doShowMenu(state: State): Result = doShowMenuResult() -> state private def doShowStatus(o: Order, state: State): Result = mkResult(s"Current order state: $o.") -> state private def askConfirm(o: Order): Result = require(o.isValid) mkDialog(s"Let's specify your order: $o. Is it correct?") -> DIALOG_CONFIRM private def doResultWithClear(msg: String)(using ctx: NCContext, im: NCIntentMatch): Result = val conv = ctx.getConversation conv.getData.remove(ctx.getRequest.getUserId) conv.clearStm(_ => true) conv.clearDialog(_ => true) mkResult(msg) -> DIALOG_EMPTY private def doStop(o: Order)(using ctx: NCContext, im: NCIntentMatch): Result = doResultWithClear( if !o.isEmpty then "Everything cancelled. Ask `menu` to look what you can order." else "Nothing to cancel. Ask `menu` to look what you can order." ) private def doContinue(): Result = mkResult("OK, please continue.") -> DIALOG_EMPTY private def askConfirmOrAskSpecify(o: Order): Result = if o.isValid then askConfirm(o) else askSpecify(o) private def askIsReadyOrAskSpecify(o: Order): Result = if o.isValid then askIsReady() else askSpecify(o) private def askStopOrDoStop(o: Order)(using ctx: NCContext, im: NCIntentMatch): Result = if o.isValid then askShouldStop() else doStop(o) import PizzeriaModel.* class PizzeriaModel extends NCModel( NCModelConfig("nlpcraft.pizzeria.ex", "Pizzeria Example Model", "1.0"), PizzeriaModelPipeline.PIPELINE ) with LazyLogging: // This method is defined in class scope and has package access level for tests reasons. private[demo] def doExecute(o: Order)(using ctx: NCContext, im: NCIntentMatch): Result = require(o.isValid) doResultWithClear(s"Executed: $o.") private def doExecuteOrAskSpecify(o: Order)(using ctx: NCContext, im: NCIntentMatch): Result = if o.isValid then doExecute(o) else askSpecify(o) @NCIntent("intent=yes term(yes)={# == 'ord:yes'}") def onYes(using ctx: NCContext, im: NCIntentMatch): NCResult = doRequest( o => o.getState match case DIALOG_CONFIRM => doExecute(o) case DIALOG_SHOULD_CANCEL => doStop(o) case DIALOG_IS_READY => askConfirmOrAskSpecify(o) case DIALOG_SPECIFY | DIALOG_EMPTY => throw UNEXPECTED_REQUEST ) @NCIntent("intent=no term(no)={# == 'ord:no'}") def onNo(using ctx: NCContext, im: NCIntentMatch): NCResult = doRequest( o => o.getState match case DIALOG_CONFIRM | DIALOG_IS_READY => doContinue() case DIALOG_SHOULD_CANCEL => askConfirmOrAskSpecify(o) case DIALOG_SPECIFY | DIALOG_EMPTY => throw UNEXPECTED_REQUEST ) @NCIntent("intent=stop term(stop)={# == 'ord:stop'}") // It doesn't depend on order validity and dialog state. def onStop(using ctx: NCContext, im: NCIntentMatch): NCResult = doRequest(askStopOrDoStop) @NCIntent("intent=status term(status)={# == 'ord:status'}") def onStatus(using ctx: NCContext, im: NCIntentMatch): NCResult = doRequest( o => o.getState match // Ignore `status`, confirm again. case DIALOG_CONFIRM => askConfirm(o) // Changes state. case DIALOG_SHOULD_CANCEL => doShowStatus(o, DIALOG_EMPTY) // Keeps same state. case DIALOG_EMPTY | DIALOG_IS_READY | DIALOG_SPECIFY => doShowStatus(o, o.getState) ) @NCIntent("intent=finish term(finish)={# == 'ord:finish'}") def onFinish(using ctx: NCContext, im: NCIntentMatch): NCResult = doRequest( o => o.getState match // Like YES if valid. case DIALOG_CONFIRM => doExecuteOrAskSpecify(o) // Ignore `finish`, specify again. case DIALOG_SPECIFY => askSpecify(o) case DIALOG_EMPTY | DIALOG_IS_READY | DIALOG_SHOULD_CANCEL => askConfirmOrAskSpecify(o) ) @NCIntent("intent=menu term(menu)={# == 'ord:menu'}") // It doesn't depend and doesn't influence on order validity and dialog state. def onMenu(using ctx: NCContext, im: NCIntentMatch): NCResult = doRequest(o => doShowMenu(o.getState)) @NCIntent("intent=order term(ps)={# == 'ord:pizza'}* term(ds)={# == 'ord:drink'}*") def onOrder( using ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("ps") ps: List[NCEntity], @NCIntentTerm("ds") ds: List[NCEntity] ): NCResult = doRequest( o => require(ps.nonEmpty || ds.nonEmpty); // It doesn't depend on order validity and dialog state. o.add(ps.map(extractPizza), ds.map(extractDrink)); askIsReadyOrAskSpecify(o) ) @NCIntent("intent=orderSpecify term(size)={# == 'ord:pizza:size'}") def onOrderSpecify( using ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("size") size: NCEntity ): NCResult = doRequest( // If order in progress and has pizza with unknown size, it doesn't depend on dialog state. o => if !o.isEmpty && o.fixPizzaWithoutSize(extractPizzaSize(size)) then askIsReadyOrAskSpecify(o) else throw UNEXPECTED_REQUEST ) override def onRejection( using ctx: NCContext, im: Option[NCIntentMatch], e: NCRejection ): Option[NCResult] = if im.isEmpty || getCurrentOrder().isEmpty then throw e Option(doShowMenuResult())
There are a number of intents in the given model which allow to prepare, change, confirm and cancel pizza orders. Note that given test model works with only one single user. Let's review this implementation step by step:
Line 12
declares PizzeriaExtractors
helper object which provides conversion methods from NCEntity objects and model data objects.Line 27
defines PizzeriaModel
companion object which contains static content and helper methods.line 114
our class extends NCModel with two mandatory parameters.Line 115
creates model configuration with most default parameters.Line 116
passes pipeline parameter which was prepared in PizzeriaModelPipeline
.Lines 173 and 174
annotate intents order
and its callback method onOrder()
. Intent order
requires lists of pizza and drinks for the order. Note that at least one of these lists shouldn't be empty, otherwise intent is not triggered. In its callback current order state is changed. If processed order is in valid state user receives order confirmation request like "Is order ready?", otherwise user receives request which asks him to specify the order. Both responses have ASK_DIALOG type.order
intent. Lines 187 and 188
annotate intents orderSpecify
and its callback method onOrderSpecify()
. Intent orderSpecify
requires pizza size value parameter. Callback checks that it was called just for suitable order state. Current order state is changed and user receives order confirmation request like "Is order ready?" again.Lines 126, 127 and 135, 136
annotate intents yes
and no
with related callbacks onYes()
and onNo()
. These intents are expected after user answered on received confirmation requests. Callbacks change order state, send another requests to user or reject these intents depending on current order state.Lines 143 and 145, 147 and 148, 158 and 159, 168 and 170
annotate intents stop
, status
, finish
and menu
intents with related callbacks. They are order management commands, their actions are depended on current order state.Line 201
annotates onRejection
method which is called if there aren't any triggered intents. Open src/main/scala/demo/components/PizzeriaOrderValidator.scala
file and replace its content with the following code:
package demo.components import org.apache.nlpcraft.* class PizzeriaOrderValidator extends NCEntityValidator: override def validate(req: NCRequest, cfg: NCModelConfig, ents: List[NCEntity]): Unit = def count(typ: String): Int = ents.count(_.getType == typ) val cntPizza = count("ord:pizza") val cntDrink = count("ord:drink") val cntNums = count("stanford:number") val cntSize = count("ord:pizza:size") // Single size - it is order specification request. if (cntSize != 1 && cntSize > cntPizza) || cntNums > cntPizza + cntDrink then throw new NCRejection("Invalid pizza request.")
PizzeriaOrderValidator
is implementation of NCEntityValidator. It is designed for validation order content that allows to reject invalid orders right away.
Open src/main/scala/demo/components/PizzeriaOrderMapper.scala
file and replace its content with the following code:
package demo.components import org.apache.nlpcraft.* import com.typesafe.scalalogging.LazyLogging import org.apache.nlpcraft.NCResultType.ASK_DIALOG import scala.collection.* case class PizzeriaOrderMapperDesc(elementType: String, propertyName: String) private object PizzeriaOrderMapper: extension(entity: NCEntity) private def position: Double = val toks = entity.getTokens (toks.head.getIndex + toks.last.getIndex) / 2.0 private def tokens: List[NCToken] = entity.getTokens private def str(es: Iterable[NCEntity]): String = es.map(e => s"type=${e.getType}(${e.tokens.map(_.getIndex).mkString("[", ",", "]")})").mkString("{", ", ", "}") def apply(extra: PizzeriaOrderMapperDesc, descr: PizzeriaOrderMapperDesc*): PizzeriaOrderMapper = new PizzeriaOrderMapper(extra, descr) import PizzeriaOrderMapper.* case class PizzeriaOrderMapper( extra: PizzeriaOrderMapperDesc, descr: Seq[PizzeriaOrderMapperDesc] ) extends NCEntityMapper with LazyLogging: override def map(req: NCRequest, cfg: NCModelConfig, ents: List[NCEntity]): List[NCEntity] = def map(destEnt: NCEntity, destProp: String, extraEnt: NCEntity): NCEntity = new NCPropertyMapAdapter with NCEntity: destEnt.keysSet.foreach(k => put(k, destEnt(k))) put[String](destProp, extraEnt[String](extra.propertyName).toLowerCase) override val getTokens: List[NCToken] = (destEnt.tokens ++ extraEnt.tokens).sortBy(_.getIndex) override val getRequestId: String = req.getRequestId override val getType: String = destEnt.getType val descrMap = descr.map(p => p.elementType -> p).toMap val destEnts = mutable.HashSet.empty ++ ents.filter(e => descrMap.contains(e.getType)) val extraEnts = ents.filter(_.getType == extra.elementType) if destEnts.nonEmpty && extraEnts.nonEmpty && destEnts.size >= extraEnts.size then val used = (destEnts ++ extraEnts).toSet val dest2Extra = mutable.HashMap.empty[NCEntity, NCEntity] for (extraEnt <- extraEnts) val destEnt = destEnts.minBy(m => Math.abs(m.position - extraEnt.position)) destEnts -= destEnt dest2Extra += destEnt -> extraEnt val unrelated = ents.filter(e => !used.contains(e)) val artificial = for ((m, e) <- dest2Extra) yield map(m, descrMap(m.getType).propertyName, e) val unused = destEnts val res = (unrelated ++ artificial ++ unused).sortBy(_.tokens.head.getIndex) logger.debug(s"Elements mapped [input=${str(ents)}, output=${str(res)}]") res else ents
PizzeriaOrderMapper
is implementation of NCEntityMapper. It is designed for complex compound entities building based on another entities.
Line 11
defines PizzeriaOrderMapper
model companion object which contains helper methods.Line 26
defines PizzeriaOrderMapper
model which implements NCEntityMapper.Line 30
defines helper method map() w hich clones destEn
entity, extends it by extraEnt
tokens and destProp
property and, as result, returns new entities instances instead of passed into the method.Line 60
defines PizzeriaOrderMapper
result. These entities will be processed further instead of passed into this component method. Open src/main/scala/demo/components/PizzeriaModelPipeline.scala
file and replace its content with the following code:
package demo.components import edu.stanford.nlp.pipeline.StanfordCoreNLP import org.apache.nlpcraft.nlp.parsers.* import org.apache.nlpcraft.* import org.apache.nlpcraft.nlp.stemmer.* import org.apache.nlpcraft.nlp.enrichers.NCEnStopWordsTokenEnricher import org.apache.nlpcraft.nlp.parsers.NCSemanticEntityParser import org.apache.nlpcraft.nlp.stanford.* import java.util.Properties object PizzeriaModelPipeline: 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) import demo.components.PizzeriaOrderMapperDesc as D new NCPipelineBuilder(). withTokenParser(tokParser). withTokenEnricher(new NCEnStopWordsTokenEnricher()). withEntityParser(new NCStanfordNLPEntityParser(stanford, Set("number"))). withEntityParser(new NCSemanticEntityParser(new NCEnStemmer, tokParser, "pizzeria_model.yaml")). withEntityMapper(PizzeriaOrderMapper( extra = D("ord:pizza:size", "ord:pizza:size:value"), descr = D("ord:pizza", "ord:pizza:size")) ). withEntityMapper(PizzeriaOrderMapper( extra = D("stanford:number", "stanford:number:nne"), descr = D("ord:pizza", "ord:pizza:qty"), D("ord:drink", "ord:drink:qty")) ). withEntityValidator(new PizzeriaOrderValidator()). build
There is model pipeline preparing place.
Line 14
defines the pipeline.Line 27
declares NCSemanticEntityParser which is based on YAM model definition pizzeria_model.yaml
.Lines 28 and 32
define entity mappers PizzeriaOrderMapper
instances which map ord:pizza
elements with theirs sizes from ord:pizza:size
and quantities from stanford:number
.Line 36
defines PizzeriaOrderValidator
class described above. The test defined in PizzeriaModelSpec
allows to check that all input test sentences are processed correctly and trigger the expected intents:
package demo import org.apache.nlpcraft.* import org.apache.nlpcraft.NCResultType.* import demo.PizzeriaModel.Result import demo.PizzeriaOrderState.* import org.scalatest.BeforeAndAfter import org.scalatest.funsuite.AnyFunSuite import scala.language.implicitConversions import scala.util.Using import scala.collection.mutable object PizzeriaModelSpec: type Request = (String, NCResultType) private class ModelTestWrapper extends PizzeriaModel: private var o: PizzeriaOrder = _ override def doExecute(o: PizzeriaOrder)(using ctx: NCContext, im: NCIntentMatch): Result = val res = super.doExecute(o) this.o = o res def getLastExecutedOrder: PizzeriaOrder = o def clearLastExecutedOrder(): Unit = o = null private class Builder: private val o = new PizzeriaOrder o.setState(DIALOG_EMPTY) def withPizza(name: String, size: String, qty: Int): Builder = o.add(Seq(Pizza(name, Some(size), Some(qty))), Seq.empty) this def withDrink(name: String, qty: Int): Builder = o.add(Seq.empty, Seq(Drink(name, Some(qty)))) this def build: PizzeriaOrder = o import PizzeriaModelSpec.* class PizzeriaModelSpec extends AnyFunSuite with BeforeAndAfter: private val mdl = new ModelTestWrapper() private val client = new NCModelClient(mdl) private val msgs = mutable.ArrayBuffer.empty[mutable.ArrayBuffer[String]] private val errs = mutable.HashMap.empty[Int, Throwable] private var testNum: Int = 0 after { if client != null then client.close() for ((seq, num) <- msgs.zipWithIndex) println("#" * 150) for (line <- seq) println(line) errs.get(num) match case Some(err) => err.printStackTrace() case None => // No-op. require(errs.isEmpty, s"There are ${errs.size} errors above.") } private def dialog(exp: PizzeriaOrder, reqs: Request*): Unit = val testMsgs = mutable.ArrayBuffer.empty[String] msgs += testMsgs testMsgs += s"Test: $testNum" for (((txt, expType), idx) <- reqs.zipWithIndex) try mdl.clearLastExecutedOrder() val resp = client.ask(txt, "userId") testMsgs += s">> Request: $txt" testMsgs += s">> Response: '${resp.getType}': ${resp.getBody}" if expType != resp.getType then errs += testNum -> new Exception(s"Unexpected result type [num=$testNum, txt=$txt, expected=$expType, type=${resp.getType}]") // Check execution result on last request. if idx == reqs.size - 1 then val lastOrder = mdl.getLastExecutedOrder def s(o: PizzeriaOrder) = if o == null then null else s"Order [state=${o.getState}, desc=$o]" val s1 = s(exp) val s2 = s(lastOrder) if s1 != s2 then errs += testNum -> new Exception( s"Unexpected result [num=$testNum, txt=$txt]" + s"\nExpected: $s1" + s"\nReal : $s2" ) catch case e: Exception => errs += testNum -> new Exception(s"Error during test [num=$testNum]", e) testNum += 1 test("test") { given Conversion[String, Request] with def apply(txt: String): Request = (txt, ASK_DIALOG) dialog( new Builder().withDrink("tea", 2).build, "Two tea", "yes", "yes" -> ASK_RESULT ) dialog( new Builder(). withPizza("carbonara", "large", 1). withPizza("marinara", "small", 1). withDrink("tea", 1). build, "I want to order carbonara, marinara and tea", "large size please", "smallest", "yes", "correct" -> ASK_RESULT ) dialog( new Builder().withPizza("carbonara", "small", 2).build, "carbonara two small", "yes", "yes" -> ASK_RESULT ) dialog( new Builder().withPizza("carbonara", "small", 1).build, "carbonara", "small", "yes", "yes" -> ASK_RESULT ) dialog( null, "marinara", "stop" -> ASK_RESULT ) dialog( new Builder(). withPizza("carbonara", "small", 2). withPizza("marinara", "large", 4). withDrink("cola", 3). withDrink("tea", 1). build, "3 cola", "one tea", "carbonara 2", "small", "4 marinara big size", "menu" -> ASK_RESULT, "done", "yes" -> ASK_RESULT ) dialog( new Builder(). withPizza("margherita", "small", 2). 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 ) }
PizzeriaModelSpec
is complex test which is designed as dialog with pizza ordering bot.
Line 14
declares PizzeriaModelSpec
test companion object which contains static content and helper methods.Line 48
defines after
block. It closes model client and prints test results.Line 61
defines test helper method dialog()
. It sends request to model via client's method ask() and accumulates execution results.Line 96
defines main test block. It contains user request descriptions and their expected results taking into account order state. You can run this test via SBT task executeTests
or using IDE.
PS C:\apache\incubator-nlpcraft-examples\pizzeria> sbt executeTests
You've created pizza model and tested it.