• Docs
  • Community
  • Use Cases
  • Downloads
  • v.1.0.0
  • GitHub
  1. Home
  2. Pizzeria ex

Pizzeria ex

  • Introduction
  • Overview
  • Installation
  • First Example
  • Developer Guide
  • Key Concepts
  • Intent Matching
  • Short-Term Memory
  • Examples
  • Calculator
  • Time
  • Light Switch
  • Light Switch FR
  • Light Switch RU
  • Pizzeria

Overview

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

Create New Project

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
        

Data Model

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.

Model Class

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.
  • On 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 pizza sizes can be specified by the model as it was described above in 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.

Testing

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
        

Done! 👌

You've created pizza model and tested it.

  • On This Page
  • Overview
  • New Project
  • Data Model
  • Model Class
  • Testing
  • Quick Links
  • Examples
  • Scaladoc
  • Download
  • Installation
  • Support
  • JIRA
  • Dev List
  • Stack Overflow
  • GitHub
  • Gitter
  • Twitter
Copyright © 2023 Apache Software Foundation asf Events • Privacy • News • Docs release: 1.0.0 Gitter Built in: