ex
ex
This simple example provides an "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 timer is up it will simply print out "BEEP BEEP BEEP" in the data probe console.
Complexity:
Source code: GitHub
Review: All Examples at GitHub
You can create new Java project in many ways - we'll use NLPCraft CLI to accomplish this task:
$ bin/nlpcraft.sh gen-project --baseName=AlarmClock --outputDir=~ --pkgName=demo --mdlType=json
NOTES:
/home/AlarmClock
directory.gen-project
command defaults to Java and Maven as its built tool.bin/nlpcraft.sh help --cmd=gen-project
to get a full help on gen-project
command.nlpcraft.sh
for and nlpcraft.cmd
for . We are going to start with declaring the static part of our model using JSON which we will later load using NCModelFileAdapter
in our Java-based model implementation. Open src/main/resources/alarm_clock.json
file and replace its content with this JSON:
{ "id": "nlpcraft.alarm.ex", "name": "Alarm Example Model", "version": "1.0", "description": "Alarm example model.", "enabledBuiltInTokens": [ "nlpcraft:num" ], "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~{# == 'x:alarm'} term(nums)~{# == 'nlpcraft:num' && meta_token('nlpcraft:num:unittype') == 'datetime' && meta_token('nlpcraft:num:isequalcondition') == true}[0,7]" ] }
There are number of important points here:
Line 7
indicates that we'll be using built-in token nlpcraft:num
(and therefore it needs to be explicitly enabled).Line 11
defines the only custom model element we'll need. It's ID is x:alarm
and it is defined through synonyms. It basically means a verb to set up an alarm clock.line 20
we define an intent alarm
that we are going to be looking for in the user input that consists of two terms: one for x:alarm
element (defined above) and another one for up to 7 numeric values of unit type datetime
(minutes, seconds, hours, etc.). 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 alarm
is detected in the user input.
Open src/main/java/demo/AlarmClock.java
file and replace its content with the following code:
package demo; import org.apache.nlpcraft.model.*; import java.time.*; import java.time.format.*; import java.util.*; import static java.time.temporal.ChronoUnit.*; public class AlarmClock 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 AlarmClock() { // Loading the model from the file in the classpath. super("alarm_model.json"); } @NCIntentRef("alarm") @NCIntentSample({ "Ping me in 3 minutes", "Buzz me in an hour and 15mins", "Set my alarm for 30s" }) private NCResult onMatch( NCIntentMatch ctx, @NCIntentTerm("nums") List<NCToken> 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 assert, because 'datetime' unit can be extended. 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)); } @Override public void onDiscard() { // Clean up when model gets discarded (e.g. during testing). timer.cancel(); } }
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:
line 10
our class extends NCModelFileAdapter
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.Line 27
defines method onMatch
as a callback for intent alarm
when it is detected in the user input. Method parameter numToks
will get up to 7 tokens of type nlpcraft:num
(see intent definition above).line 22
where we use @NCIntentSample annotation to provide samples of the user input that this intent should match. Apart from documentation purpose these samples will be used when we will be testing out model below.onMatch
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.line 82
the intent callback simply returns a confirmation message telling for what actual time the alarm clock was set. Once we have our model ready let's go to ~/AlarmClock
directory and run the Maven build:
$ cd ~/AlarmClock $ mvn clean package
At this stage we have our project built and we are ready to start testing.
Run the following command to start local REST server, if it hasn't been started already, from the NLPCraft installation directory:
$ bin/nlpcraft.sh start-server
NOTES:
bin/nlpcraft.sh help --cmd=start-server
to get a full help on this command.nlpcraft.sh
for and nlpcraft.cmd
for .Remember the @NCIntentSample annotation we have used in our model code next to intent definition?
Part of the test framework, the auto-validator class NCTestAutoModelValidator takes one or more model IDs (or class names) and performs validation. Validation consists of starting an embedded probe with a given model, scanning for @NCIntentSample and @NCIntentSampleRef annotations and their corresponding callback methods, submitting each sample input sentences from these annotations and checking that resulting intent matches the intent the sample was attached to. Note that auto-testing does not require any additional code to be written - the class gathers all required information from the model itself.
As always, you can launch model auto-validator as any other Java class but we'll use NLPCraft CLI to do it more conveniently:
$ bin/nlpcraft.sh test-model --cp=~/AlarmClock/target/classes --mdls=demo.AlarmClock
NOTES:
bin/nlpcraft.sh help --cmd=test-model
to get a full help on this command.retest-model
command in REPL mode to re-run the last model test avoiding the retyping of all required parameters.nlpcraft.sh
for and nlpcraft.cmd
for .Look at the output of this command and you will see the test results for all our sample utterances:
Typical development cycle consists of:
All of these operations can be performed from NLPCraft CLI in REPL mode or from any IDE.
NOTE: you don't need to restart REST server every time - it only needs to be started once.
You've created alarm clock data model, started the REST server and tested this model using the built-in test framework.