blob: 7fbec8550908a647b641a568e4326c03699dba71 [file] [log] [blame]
---
active_crumb: Alarm Clock <code><sub>ex</sub></code>
layout: documentation
id: alarm_clock
---
<!--
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">
<section id="overview">
<h2 class="section-title">Overview</h2>
<p>
This simple example provides a "alarm clock" implementation where you can ask to set the timer
for a specific duration from now expressed in hours, minutes and/or seconds. You can say "ping me in 3 minutes",
"buzz me in an hour and 15 minutes", or "set my alarm for 30 secs". When the timers is up it will
simply print out "BEEP BEEP BEEP" in the data probe console.
</p>
<p>
Complexity: <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/>
Source code: <a target="github" href="https://github.com/apache/incubator-nlpcraft/tree/master/src/main/scala/org/apache/nlpcraft/examples/alarm">GitHub</a>
</p>
</section>
<section id="new_project">
<h3 class="section-title">Create New Project</h3>
<p>
You can create new Java project in many different ways - we'll use Maven archetype generation
for that. In your home folder run the following command:
</p>
<pre class="brush: text">
mvn archetype:generate -DgroupId=examples -DartifactId=my-app -DarchetypeVersion=1.4 -DinteractiveMode=false
</pre>
<p>
This will create <code>my-app</code> folder with the following default maven project structure:
</p>
<pre class="console">
├── <b>pom.xml</b>
└── src
├── main
│   └── java
│   └── examples
│   └── App.java
└── test
└── java
└── examples
└── AppTest.java
</pre>
<div class="bq info">
<p>
Note that this setup is same for all examples. Note also that you can use any other tools for
creating and managing Java project with or without Maven.
</p>
</div>
<p>
For our example we'll use JetBrain's <a target=_new href="https://www.jetbrains.com/idea/">IntelliJ IDEA</a>.
Create new IDEA project from this source folder (make sure to pick JDK 8 or later JDK and language support).
Let's also delete auto-generated files <code>App.java</code> and <code>AppTest.java</code> from our
project as we won't be using them.
</p>
</section>
<section id="add_nlpcraft">
<h3 class="section-title">Add NLPCraft</h3>
<p>
Next we need to add NLPCraft dependency to our new project. Open <code>pom.xml</code> file and replace
<code>dependencies</code> section with the following code:
</p>
<pre class="brush: xml, highlight: [3, 4, 5]">
&lt;dependencies&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.apache.nlpcraft&lt;/groupId&gt;
&lt;artifactId&gt;nlpcraft&lt;/artifactId&gt;
&lt;version&gt;{{site.latest_version}}&lt;/version&gt;
&lt;/dependency&gt;
&lt;/dependencies&gt;
</pre>
<p>
Also make sure that you have correct JDK version (1.8 or above) for the maven compiler plugin:
</p>
<pre class="brush: xml, highlight: [3, 4]">
&lt;properties&gt;
&lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
&lt;maven.compiler.source&gt;1.8&lt;/maven.compiler.source&gt;
&lt;maven.compiler.target&gt;1.8&lt;/maven.compiler.target&gt;
&lt;/properties&gt;
</pre>
<p>
IDEA should automatically reload the project with newly updated <code>pom.xml</code> file and
we should be ready now to develop our data model.
</p>
</section>
<section id="model">
<h3 class="section-title">Data Model</h3>
<p>
We are going to start with declaring the static part of our semantic model using JSON which we will later load using
<code>NCModelFileAdapter</code> in our Java-based model implementation. Create new <code>alarm_model.json</code>
file and add the following model declaration into it:
</p>
<pre class="brush: js, highlight: [7, 16, 25]">
{
"id": "nlpcraft.alarm.ex",
"name": "Alarm Example Model",
"version": "1.0",
"description": "Alarm example model.",
"enabledBuiltInTokens": [
"nlpcraft:num"
],
"examples": [
"Ping me in 3 minutes",
"Buzz me in an hour and 15mins",
"Set my alarm for 30s"
],
"elements": [
{
"id": "x:alarm",
"description": "Alarm token indicator.",
"synonyms": [
"{ping|buzz|wake|call|hit} {me|up|me up|*}",
"{set|*} {my|*} {wake|wake up|*} {alarm|timer|clock|buzzer|call} {up|*}"
]
}
],
"intents": [
"intent=alarm term={id=='x:alarm'} term(nums)={id=='nlpcraft:num' && ~nlpcraft:num:unittype=='datetime' && ~nlpcraft:num:isequalcondition==true}[0,7]"
]
}
</pre>
<p>There are number of important points here:</p>
<ul>
<li>
<code>Line 7</code> indicates that we'll be using built-in token <code>nlpcraft:num</code> (and therefore
it needs to be explicitly enabled).
</li>
<li>
<code>Line 16</code> defines the only custom model element we'll need. It's ID is <code>x:alarm</code> and
it is defined through synonyms. It basically means a verb to set up an alarm clock.
</li>
<li>
On <code>line 25</code> we define an intent <code>alarm</code> that we are going to be looking for in the user input that
consists of two terms: one for <code>x:alarm</code> element (defined above) and another one for up to 7 numeric values
of unit type <code>datetime</code> (minutes, seconds, hours, etc.).
</li>
</ul>
<p>
Now that our model is ready let's create a Java class that would load this model and provide the actual
callback for when the intent <code>alarm</code> is detected in the user input.
</p>
</section>
<section id="code">
<h3 class="section-title">Model Class</h3>
<p>
Let's create new Java class in <code>AlarmModel.java</code> with the following code:
</p>
<pre class="brush: java, highlight: [9, 18, 21, 22, 23, 24, 68, 81]">
import org.apache.nlpcraft.model.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import static java.time.temporal.ChronoUnit.MILLIS;
public class AlarmModel extends NCModelFileAdapter {
private static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("HH'h' mm'm' ss's'")
.withZone(ZoneId.systemDefault());
private final Timer timer = new Timer();
public AlarmModel() {
// Loading the model from the file in the classpath.
super("org/apache/nlpcraft/examples/alarm/alarm_model.json");
}
@NCIntentRef("alarm")
private NCResult onMatch(
NCIntentMatch ctx,
@NCIntentTerm("nums") List&lt;NCToken&gt; numToks
) {
if (ctx.isAmbiguous())
throw new NCRejection("Not exact match.");
long unitsCnt = numToks.stream().map(
tok -> (String)tok.meta("num:unit")
).distinct().count();
if (unitsCnt != numToks.size())
throw new NCRejection("Ambiguous time units.");
LocalDateTime now = LocalDateTime.now();
LocalDateTime dt = now;
for (NCToken num : numToks) {
String unit = num.meta("nlpcraft:num:unit");
// Skip possible fractional to simplify.
long v = ((Double)num.meta("nlpcraft:num:from")).longValue();
if (v <= 0)
throw new NCRejection("Value must be positive: " + unit);
switch (unit) {
case "second": { dt = dt.plusSeconds(v); break; }
case "minute": { dt = dt.plusMinutes(v); break; }
case "hour": { dt = dt.plusHours(v); break; }
case "day": { dt = dt.plusDays(v); break; }
case "week": { dt = dt.plusWeeks(v); break; }
case "month": { dt = dt.plusMonths(v); break; }
case "year": { dt = dt.plusYears(v); break; }
default:
// It shouldn't be an assertion, because 'datetime'
// unit type can be extended in the future.
throw new NCRejection("Unsupported time unit: " + unit);
}
}
long ms = now.until(dt, MILLIS);
assert ms >= 0;
timer.schedule(
new TimerTask() {
@Override
public void run() {
System.out.println(
"BEEP BEEP BEEP for: " +
ctx.getContext().getRequest().getNormalizedText() + ""
);
}
},
ms
);
return NCResult.text("Timer set for: " + FMT.format(dt));
}
}
</pre>
<p>
There's a bit of a logic here that deals mostly with taking multiple numeric values and converting them into
a single number of milliseconds that the alarm clock needs to be set up for. Let's review it step by step:
</p>
<ul>
<li>
On line 9 our class extends <code>NCModelFileAdapter</code> that allows us to load most
of the model declaration from the external JSON or YAML file (line 18) and only provide functionality that we
couldn't express in declarative portion in JSON.
</li>
<li>
Lines 21-24 define method <code>onMatch</code> as a callback for intent <code>alarm</code>
when it is detected in the user input. Method parameter <code>numToks</code> will get up to 7 tokens
of type <code>nlpcraft:num</code> (see intent definition above).
</li>
<li>
The rest of the method <code>onMatch</code> implementation is a relatively straight forward Java code
that calculates timer delay from multiple numeric units and their types. In the end (line 68)
it schedules a timer to print "BEEP BEEP BEEP" at calculated time. For simplicity, this message will
be printed right in the data probe console.
</li>
<li>
On the line 81 the intent callback simply returns a confirmation message telling
for what actual time the alarm clock was set.
</li>
</ul>
</section>
<section id="start_probe">
<h3 class="section-title">Start Data Probe <sub>optional</sub></h3>
<div class="bq warn">
<p><b>Embedded Probe</b></p>
<p>
If you are using the <a href="#testing">unit test</a> that comes with this example you <b>do not</b>
need to start the data probe standalone as this unit test uses embedded probe mode. In this mode, the unit
test will be automatically start and stop the data probe from within the test itself.
</p>
<p>
<b>If using <a href="#testing">unit test</a> below - skip this step, you only need to start the server.</b>
</p>
</div>
<p>
NLPCraft data models get deployed into data probe. Let's start a standalone data probe with our newly
created data model. To start data probe we need to configure Run Configuration in IDEA with
the following parameters:
</p>
<ul>
<li>
<b>Main class:</b> <code>org.apache.nlpcraft.NCStart</code>
</li>
<li>
<b>VM arguments:</b> <code>-Dconfig.override_with_env_vars=true</code>
</li>
<li>
<b>Environment variable:</b> <code>CONFIG_FORCE_nlpcraft_probe_models.0=org.apache.nlpcraft.examples.alarm.AlarmModel</code>
</li>
<li>
<b>Program arguments: </b> <code>-probe</code>
</li>
</ul>
<div class="bq info">
<p>
<b>NOTE:</b> instead of supplying a <a href="/server-and-probe.html">full configuration file</a> we just
set one configuration property using configuration override via environment variables.
</p>
</div>
<p>
Start this run configuration and make sure you have positive console output indicating that our model
has been successfully loaded and probe started.
</p>
</section>
<section id="start_server">
<h3 class="section-title">Start REST Server</h3>
<p>
REST server listens for requests from client applications and routes them to the requested data models
via connected data probes. REST server starts the same way as the data probe. Configure new
Run Configuration in IDEA with the following parameters:
</p>
<ul>
<li>
<b>Main class:</b> <code>org.apache.nlpcraft.NCStart</code>
</li>
<li>
<b>Program arguments: </b> <code>-server</code>
</li>
</ul>
<p>
Once started ensure that your REST server console output shows that data probe is connected and the
REST server is listening on the default <code>localhost:8081</code> endpoint.
</p>
<p>
At this point we've developed our data model, deployed it into the data probe, and started the REST server.
To test it, we'll use the built-in <a href="/tools/test_framework.html">test framework</a>
that allows you to write convenient unit tests against your data model.
</p>
</section>
<section id="testing">
<h3 class="section-title">Testing</h3>
<p>
NLPCraft comes with easy to use <a href="/tools/test_framework.html">test framework</a> for
data models that can be used with
any unit testing framework like JUnit or ScalaTest. It is essentially a simplified
version of Java REST client that is custom designed for data model testing.
</p>
<p>
We would like to test with following user requests:
</p>
<ul>
<li><code>"Ping me in 3 minutes"</code></li>
<li><code>"Buzz me in an hour and 15mins"</code></li>
<li><code>"Set my alarm for 30s"</code></li>
</ul>
<p>
Let's create new Java class <code>AlarmTest.java</code> with the following code:
</p>
<pre class="brush: java, highlight: [20, 24, 32, 37, 38, 39]">
package org.apache.nlpcraft.examples.alarm;
import org.apache.nlpcraft.common.NCException;
import org.apache.nlpcraft.model.tools.test.NCTestClient;
import org.apache.nlpcraft.model.tools.test.NCTestClientBuilder;
import org.apache.nlpcraft.probe.embedded.NCEmbeddedProbe;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertTrue;
class AlarmTest {
static private NCTestClient cli;
@BeforeAll
static void setUp() throws NCException, IOException {
NCEmbeddedProbe.start(AlarmModel.class);
cli = new NCTestClientBuilder().newBuilder().build();
cli.open("nlpcraft.alarm.ex"); // See alarm_model.json
}
@AfterAll
static void tearDown() throws NCException, IOException {
if (cli != null)
cli.close();
NCEmbeddedProbe.stop();
}
@Test
void test() throws NCException, IOException {
assertTrue(cli.ask("Ping me in 3 minutes").isOk());
assertTrue(cli.ask("Buzz me in an hour and 15mins").isOk());
assertTrue(cli.ask("Set my alarm for 30s").isOk());
}
}
</pre>
<p>
This test is pretty straight forward:
</p>
<ul>
<li>
On line 24 we open the test client with the model ID (see <code>alarm_model.json</code>
file for where we declared it).
</li>
<li>
Line 37, 38, and 39 is where we issue our test sentences and we should see
the confirmation messages and eventually "BEEP BEEP BEEP" print outs in the data probe
console.
</li>
</ul>
<div class="bq info">
<p><b>Embedded Prove</b></p>
<p>
This test uses <a href="/tools/embedded_probe.html">embedded probe</a> which automatically
start and stops the data probe from within the tests itself. See lines 20 and 32 for details.
</p>
<p>
<b>NOTE:</b> when using test you don't need to start data probe standalone in a previous step.
</p>
</div>
<p>
Right click on this class in the project view and run it. You should be getting standard output in
JUnit panel as well as the output in the data probe console.
</p>
</section>
<section>
<h2 class="section-title">Done! 👌</h2>
<p>
You've created alarm clock data model, deployed it into the data probe, started the
REST server and tested this model using JUnit 5 and the built-in test framework.
</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="#add_nlpcraft">Add NLPCraft</a></li>
<li><a href="#model">Data Model</a></li>
<li><a href="#code">Model Class</a></li>
<li><a href="#start_probe">Start Probe <sub>opt</sub></a></li>
<li><a href="#start_server">Start Server</a></li>
{% include quick-links.html %}
</ul>
</div>