blob: e85452194b129241cad79b8cf8e5e5a356031629 [file] [log] [blame]
---
active_crumb: Time <code><sub>ex</sub></code>
layout: documentation
id: time
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 implementation for world time bot. You can say something like
"What time is it now in New York City" or "What's the local time?".
</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/time">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>time_model.yaml</code> - YAML configuration file which contains model description.</li>
<li><code>cities_timezones.txt</code> - Cities timezones database.</li>
<li><code>TimeModel.scala</code> - Model implementation.</li>
<li><code>CitiesDataProvider.scala</code> - Helper service which loads timezones database.</li>
<li><code>GeoManager.scala</code> - Helper service which provides cities timezones information for user request.</li>
<li><code>TimeModelSpec.scala</code> - Test that allows to test your model.</li>
</ul>
<pre class="brush: plain, highlight: [7, 8, 13, 15, 16, 20]">
| build.sbt
+--project
| build.properties
\--src
+--main
| +--resources
| | time_model.yaml
| | cities_timezones.txt
| \--scala
| \--demo
| \--utils
| \--cities
| CitiesDataProvider.scala
| \--keycdn
| GeoManager.scala
| TimeModel.scala
\--test
\--scala
\--demo
TimeModelSpec.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>
We are going to start with declaring the static part of our model using YAML which we will later load using
<a href="../apis/latest/org/apache/nlpcraft/NCModelAdapter.html">NCModelAdapter</a> in our Scala-based model implementation.
Open <code>src/main/resources/<b>time.yaml</b></code>
file and replace its content with the following YAML:
</p>
<pre class="brush: js, highlight: [1, 6]">
macros:
"&lt;OF&gt;": "{of|for|per}"
"&lt;CUR&gt;": "{current|present|now|local}"
"&lt;TIME&gt;": "{time &lt;OF&gt; day|day time|date|time|moment|datetime|hour|o'clock|clock|date time|date and time|time and date}"
elements:
- id: "x:time"
description: "Date and/or time token indicator."
synonyms:
- "{&lt;CUR&gt;|_} &lt;TIME&gt;"
- "what &lt;TIME&gt; {is it now|now|is it|_}"
</pre>
<p>There are number of important points here:</p>
<ul>
<li>
<code>Line 1</code> defines several macros that are used later on throughout the model's elements
to shorten the synonym declarations. Note how macros coupled with option groups
shorten overall synonym declarations 1000:1 vs. manually listing all possible word permutations.
</li>
<li>
<code>Line 6</code> defines <code>x:time</code> model element which
is used in our intent defined in <code>TimeModel</code> class. Note that this model
element is defined mostly through macros we have defined above.
</li>
</ul>
<div class="bq info">
<p><b>YAML vs. API</b></p>
<p>
As usual, this YAML-based static model definition is convenient but totally optional. All elements definitions
can be provided programmatically inside Scala model <code>TimeModel</code> class as well.
</p>
</div>
</section>
<section id="code">
<h2 class="section-title">Model Class <a href="#"><i class="top-link fas fa-fw fa-angle-double-up"></i></a></h2>
<p>
Open <code>src/main/scala/demo/<b>TimeModel.scala</b></code> file and replace its content with the following code:
</p>
<pre class="brush: scala, highlight: [16, 17, 18, 19, 20, 21, 56, 57, 70, 71]">
package demo
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import org.apache.nlpcraft.*
import org.apache.nlpcraft.annotations.*
import demo.utils.cities.*
import demo.utils.keycdn.GeoManager
import org.apache.nlpcraft.internal.util.NCResourceReader
import org.apache.nlpcraft.nlp.parsers.NCOpenNLPEntityParser
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle.MEDIUM
import java.time.*
@NCIntent("fragment=city term(city)~{# == 'opennlp:location'}")
@NCIntent("intent=intent2 term~{# == 'x:time'} fragment(city)")
@NCIntent("intent=intent1 term={# == 'x:time'}")
class TimeModel extends NCModelAdapter(
NCModelConfig("nlpcraft.time.ex", "Time Example Model", "1.0"),
new NCPipelineBuilder().
withSemantic("en", "time_model.yaml").
withEntityParser(NCOpenNLPEntityParser(
NCResourceReader.getPath("opennlp/en-ner-location.bin"))
).
build
):
private val FMT: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(MEDIUM)
private val citiesData: Map[City, CityData] = CitiesDataProvider.get
private def mkResult(
city: String, cntry: String, tmz: String, lat: Double, lon: Double
): NCResult =
val m =
Map(
"city" -> capitalize(city),
"country" -> capitalize(cntry),
"timezone" -> tmz,
"lat" -> lat,
"lon" -> lon,
"localTime" -> ZonedDateTime.now(ZoneId.of(tmz)).format(FMT)
)
try
NCResult(new ObjectMapper(new YAMLFactory).writeValueAsString(m))
catch
case e: JsonProcessingException =>
throw new RuntimeException("YAML conversion error.", e)
private def capitalize(s: String): String =
if s == null || s.isEmpty
then s
else s"${s.substring(0, 1).toUpperCase}${s.substring(1, s.length)}"
@NCIntentRef("intent2")
private def onRemoteMatch(
ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("city") cityEnt: NCEntity
): NCResult =
val cityName: String = cityEnt.mkText
val (city, data) =
citiesData.find(_._1.name.equalsIgnoreCase(cityName)).
getOrElse(
throw new NCRejection(String.format("No timezone mapping for %s.", cityName))
)
mkResult(city.name, city.country, data.timezone, data.latitude, data.longitude)
@NCIntentRef("intent1")
private def onLocalMatch(ctx: NCContext, im: NCIntentMatch): NCResult =
val geo = GeoManager.get(ctx.getRequest).getOrElse(GeoManager.getSiliconValley)
mkResult(geo.city, geo.country_name, geo.timezone, geo.latitude, geo.longitude)
</pre>
<p>
There are two intents, for local and remote locations. Result is represented as JSON value.
Let's review this implementation step by step:
</p>
<ul>
<li>
On <code>line 19</code> our class extends <a href="../apis/latest/org/apache/nlpcraft/NCModelAdapter.html">NCModelAdapter</a> that allows us to pass
prepared configuration and pipeline into model.
</li>
<li>
<code>Line 16</code> creates <code>IDL fragment</code> which is used in <code>intent2</code> definition below.
</li>
<li>
<code>Lines 17 and 18</code> annotate two intents definitions <code>intent1</code> and <code>intent2</code>.
Their callbacks below have references on them by their identifiers.
</li>
<li>
<code>Line 20</code> creates model configuration with most default parameters.
</li>
<li>
<code>Line 21</code> creates pipeline based on built-in components:
<ul>
<li>Built-in English language
<a href="../apis/latest/org/apache/nlpcraft/nlp/parsers/NCSemanticEntityParser.html">NCSemanticEntityParser</a>
configured with <code>time_model.yaml</code>.</li>
<li>Entity parser <a href="../apis/latest/org/apache/nlpcraft/nlp/parsers/NCOpenNLPTokenParser.html">NCOpenNLPTokenParser</a>
configured by <code>opennlp/en-ner-location.bin</code> for GEO locations detection.
</li>
</ul>
Look at these built-in components documentation for more details.
</li>
<li>
<code>Lines 56 and 57</code> annotate intent <code>intent2</code> and its callback method <code>onRemoteMatch()</code>.
This intent requires one mandatory entity - city which is used for getting time for its timezone.
</li>
<li>
<code>Lines 70 and 71</code> annotate intent <code>intent1</code> and its callback method <code>onLocalMatch()</code>.
This intent is triggered by default, tries to detect timezone by request data and return time for this timezone.
Otherwise, it returns Silicon Valley current time.
</li>
</ul>
<p>
Implementations of helper classes <code>GeoManager</code> and <code>CitiesDataProvider</code> are not related
to given example logic.
Just copy these classes and <code>cities_timezones.txt</code> file from project source code into your demo project.
</p>
</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 defined in <code>TimeModelSpec</code> allows to check that all input test sentences are
processed correctly and trigger the expected intents <code>intent2</code> and <code>intent1</code>:
</p>
<pre class="brush: scala, highlight: [9, 11]">
package demo
import org.apache.nlpcraft.*
import org.scalatest.funsuite.AnyFunSuite
import scala.util.Using
class TimeModelSpec extends AnyFunSuite:
test("test") {
Using.resource(new NCModelClient(new TimeModel())) { client =>
def check(txt: String, intentId: String): Unit =
require(client.debugAsk(txt, "userId", true).getIntentId == intentId)
check("What time is it now in New York City?", "intent2")
check("What's the current time in Moscow?", "intent2")
check("Show me time of the day in London.", "intent2")
check("Can you please give me the Tokyo's current date and time.", "intent2")
check("What's the local time?", "intent1")
}
}
</pre>
<ul>
<li>
<code>Line 9</code> creates the client for our model.
</li>
<li>
<code>Line 11</code> calls a special method <code>debugAsk()</code>.
It allows to check the winning intent and its callback parameters without actually
calling the intent.
</li>
<li>
<code>Lines 13-18</code> define all the test input sentences that should all
trigger <code>ls</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 time data 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="#code">Model Class</a></li>
<li><a href="#testing">Testing</a></li>
{% include quick-links.html %}
</ul>
</div>