ex
ex
This example provides a very simple calculator implementation. It supports restricted set of arithmetic operations for numeric values.
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 Calculator 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:
CalculatorModel.scala
- Model implementationCalculatorModelSpec.scala
- Test that allows to test your model.| build.sbt +--project | build.properties \--src +--main | \--scala | \--demo | CalculatorModel.scala \--test \--scala \--demo CalculatorModelSpec.scala
All element definitions are provided programmatically inside Scala model CalculatorModel
class. Open src/main/scala/demo/CalculatorModel.scala
file and replace its content with the following code:
package demo import edu.stanford.nlp.pipeline.StanfordCoreNLP import org.apache.nlpcraft.* import org.apache.nlpcraft.annotations.* import org.apache.nlpcraft.nlp.parsers.* import org.apache.nlpcraft.nlp.stanford.* import java.util.Properties object CalculatorModel: private val OPS: Map[String, (Int, Int) => Int] = Map("+" -> (_ + _), "-" -> (_ - _), "*" -> (_ * _), "/" -> (_ / _)) private val PIPELINE: NCPipeline = val props = new Properties() props.setProperty("annotators", "tokenize, ssplit, pos, lemma, ner") val stanford = new StanfordCoreNLP(props) new NCPipelineBuilder(). withTokenParser(new NCStanfordNLPTokenParser(stanford)). withEntityParser(new NCNLPEntityParser(t => OPS.contains(t.getText))). withEntityParser(new NCStanfordNLPEntityParser(stanford, Set("number"))). build private def nne(e: NCEntity): Int = java.lang.Double.parseDouble(e[String]("stanford:number:nne")).intValue import CalculatorModel.* class CalculatorModel extends NCModel( NCModelConfig("nlpcraft.calculator.ex", "Calculator Example Model", "1.0"), PIPELINE ) : private var mem: Option[Int] = None private def calc(x: Int, op: String, y: Int): NCResult = mem = Some(OPS.getOrElse(op, throw new IllegalStateException()).apply(x, y)) NCResult(mem.get) @NCIntent( "intent=calc options={ 'ordered': true }" + " term(x)={# == 'stanford:number'}" + " term(op)={has(list('+', '-', '*', '/'), meta_ent('nlp:entity:text')) == true}" + " term(y)={# == 'stanford:number'}" ) def onMatch( ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("x") x: NCEntity, @NCIntentTerm("op") op: NCEntity, @NCIntentTerm("y") y: NCEntity ): NCResult = calc(nne(x), op.mkText, nne(y)) @NCIntent( "intent=calcMem options={ 'ordered': true }" + " term(op)={has(list('+', '-', '*', '/'), meta_ent('nlp:entity:text')) == true}" + " term(y)={# == 'stanford:number'}" ) def onMatchMem( ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("op") op: NCEntity, @NCIntentTerm("y") y: NCEntity ): NCResult = calc(mem.getOrElse(throw new NCRejection("Memory is empty.")), op.mkText, nne(y))
There are two intents with simple logic. First returns arithmetic operation result with two input parameters, second uses last operation result instead of the first argument. Note also that the arithmetic notations (+
, -
, *
, /
) are used as is for without any additional synonyms. Let's review this implementation step by step:
Line 11
declares CalculatorModel
model companion object which contains static content and helper methods.Line 12
defines arithmetic operations map with notations as keys and functions definitions as values.Line 14
defines model pipeline based on three built-in components. Line 21
defines Stanford token parser NCStanfordNLPTokenParser
(we use Stanford NLP components in this example).Line 22
declares entity parser NCNLPEntityParser which allows to find arithmetic operations notations.Line 23
defines entity parser NCStanfordNLPEntityParser
which allows to find numerics in the text input.Line 31
declares CalculatorModel
model class.line 35
declares variable named mem
which act as a holder for the last operation result.Lines 41 and 47
annotate intents calc
and its callback method onMatch()
. Intent calc
requires one arithmetic operation notation and two numerics as its arguments.Lines 56 and 61
annotate intents calcMem
and its callback method onMatchMem()
. Intent calcMem
requires one arithmetic operation notation and one numeric as its second arguments. Note that it attempts to use last operation result as its first argument, if one exists. The test provided in CalculatorModelSpec
allows to check that all input test sentences are processed correctly and trigger the expected intents calc
or calcMem
:
package demo import org.apache.nlpcraft.* import org.scalatest.funsuite.AnyFunSuite import scala.util.Using class CalculatorModelSpec extends AnyFunSuite: test("test") { Using.resource(new NCModelClient(new CalculatorModel())) { client => def check(txt: String, v: Int): Unit = require(v == client.ask(txt, "userId").getBody) check("2 + 2", 4) check("3 * 4", 12) check("/ two", 6) check("+ twenty two", 28) check("7 + 2", 9) } }
Line 9
creates the client for our model.Line 11
calls the method ask()
. Its result is checked with expected value.lines 15, 16
trigger calcMem
intent while other sentences trigger calc
intent. You can run this test via SBT task executeTests
or using IDE.
$ sbt executeTests
You've created calculator model and tested it.