blob: 251b92283302d1e7d3fb005792b7359eeed6e458 [file] [log] [blame]
---
active_crumb: Calculator <code><sub>ex</sub></code>
layout: documentation
id: calculator
fa_icon: fa-cube
---
<!--
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.
-->
<div class="col-md-8 second-column example">
<section id="overview">
<h2 class="section-title">Overview <a href="#"><i class="top-link fas fa-fw fa-angle-double-up"></i></a></h2>
<p>
This example provides a very simple calculator implementation.
It supports restricted set of arithmetic operations for numeric values.
</p>
<p>
<b>Complexity:</b> <span class="complexity-one-star"><i class="fas fa-star"></i> <i class="far fa-star"></i> <i class="far fa-star"></i></span><br/>
<span class="ex-src">Source code: <a target="github" href="https://github.com/apache/incubator-nlpcraft/tree/master/nlpcraft-examples/calculator">GitHub <i class="fab fa-fw fa-github"></i></a><br/></span>
<span class="ex-review-all">Review: <a target="github" href="https://github.com/apache/incubator-nlpcraft/tree/master/nlpcraft-examples">All Examples at GitHub <i class="fab fa-fw fa-github"></i></a></span>
</p>
</section>
<section id="new_project">
<h2 class="section-title">Create New Project <a href="#"><i class="top-link fas fa-fw fa-angle-double-up"></i></a></h2>
<p>
You can create new Scala projects in many ways - we'll use SBT
to accomplish this task. Make sure that <code>build.sbt</code> file has the following content:
</p>
<pre class="brush: js, highlight: [7]">
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "3.1.3"
lazy val root = (project in file("."))
.settings(
name := "NLPCraft Calculator Example",
version := "{{site.latest_version}}",
libraryDependencies += "org.apache.nlpcraft" % "nlpcraft" % "{{site.latest_version}}",
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.14" % "test"
)
</pre>
<p><b>NOTE: </b>use the latest versions of Scala and ScalaTest.</p>
<p>Create the following files so that resulting project structure would look like the following:</p>
<ul>
<li><code>CalculatorModel.scala</code> - Model implementation</li>
<li><code>CalculatorModelSpec.scala</code> - Test that allows to test your model.</li>
</ul>
<pre class="brush: plain, highlight: [8, 12]">
| build.sbt
+--project
| build.properties
\--src
+--main
| \--scala
| \--demo
| CalculatorModel.scala
\--test
\--scala
\--demo
CalculatorModelSpec.scala
</pre>
</section>
<section id="model">
<h2 class="section-title">Data Model<a href="#"><i class="top-link fas fa-fw fa-angle-double-up"></i></a></h2>
<p>
All element definitions are provided programmatically inside Scala model <code>CalculatorModel</code> class.
Open <code>src/main/scala/demo/<b>CalculatorModel.scala</b></code> file and replace its content with the following code:
</p>
<pre class="brush: scala, highlight: [11, 12, 14, 21, 22, 23, 31, 35, 41, 47, 56, 61]">
package demo
import edu.stanford.nlp.pipeline.StanfordCoreNLP
import org.apache.nlpcraft.*
import org.apache.nlpcraft.annotations.*
import org.apache.nlpcraft.nlp.entity.parser.stanford.*
import org.apache.nlpcraft.nlp.parsers.*
import org.apache.nlpcraft.nlp.token.parser.stanford.NCStanfordNLPTokenParser
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 NCModelAdapter(
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:token: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:token: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))
</pre>
<p>
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
(<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>) are used as is for without any additional synonyms.
Let's review this implementation step by step:
</p>
<ul>
<li>
<code>Line 11</code> declares <code>CalculatorModel</code>, model companion object, which contains
static content and helper methods.
</li>
<li>
<code>Line 12</code> defines arithmetic operations map with notations as keys and functions definitions as values.
</li>
<li>
<code>Line 14</code> defines model pipeline based on three built-in components.
<code>Line 21</code> defines Stanford token parser
<a href="NCStanfordNLPTokenParser">NCStanfordNLPTokenParser</a>
(we use Stanford NLP components in this example).
</li>
<li>
<code>Line 22</code> declares entity parser
<a href="../apis/latest/org/apache/nlpcraft/nlp/parsers/NCNLPEntityParser.html">NCNLPEntityParser</a>
which allows to find arithmetic operations notations.
</li>
<li>
<code>Line 23</code> defines entity parser
<a href="NCStanfordNLPEntityParser">NCStanfordNLPEntityParser</a>,
which allows to find numerics in the text input.
</li>
<li>
<code>Line 31</code> declares <code>CalculatorModel</code> model class.
</li>
<li>
<code>line 35</code> declares variable named <code>mem</code> which act as a holder for the last operation result.
</li>
<li>
<code>Lines 41 and 47</code> annotate intents <code>calc</code> and its callback method <code>onMatch()</code>.
Intent <code>calc</code> requires one arithmetic operation notation and two numerics as its arguments.
</li>
<li>
<code>Lines 56 and 61</code> annotate intents <code>calcMem</code> and its callback method <code>onMatchMem()</code>.
Intent <code>calcMem</code> 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.
</li>
</ul>
</section>
<section id="testing">
<h2 class="section-title">Testing <a href="#"><i class="top-link fas fa-fw fa-angle-double-up"></i></a></h2>
<p>
The test provided in <code>CalculatorModelSpec</code> allows to check that all input test sentences are
processed correctly and trigger the expected intents <code>calc</code> or <code>calcMem</code>:
</p>
<pre class="brush: scala, highlight: [9, 11, 15, 16]">
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)
}
}
</pre>
<ul>
<li>
<code>Line 9</code> creates the client for our model.
</li>
<li>
<code>Line 11</code> calls the method <code>ask()</code>. Its result is checked with expected value.
</li>
<li>
Note that test sentences on <code>lines 15, 16</code> trigger <code>calcMem</code> intent while other sentences
trigger <code>calc</code> intent.
</li>
</ul>
<p>
You can run this test via SBT task <code>executeTests</code> or using IDE.
</p>
<pre class="brush: scala, highlight: []">
$ sbt executeTests
</pre>
</section>
<section>
<h2 class="section-title">Done! 👌 <a href="#"><i class="top-link fas fa-fw fa-angle-double-up"></i></a></h2>
<p>
You've created calculator model and tested it.
</p>
</section>
</div>
<div class="col-md-2 third-column">
<ul class="side-nav">
<li class="side-nav-title">On This Page</li>
<li><a href="#overview">Overview</a></li>
<li><a href="#new_project">New Project</a></li>
<li><a href="#model">Data Model</a></li>
<li><a href="#testing">Testing</a></li>
{% include quick-links.html %}
</ul>
</div>