ex
ex
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?".
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 Time Example", version := "1.0.0", libraryDependencies += "org.apache.nlpcraft" % "nlpcraft" % "1.0.0", libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.15" % "test" )
NOTE: use the latest versions of Scala and ScalaTest.
Create the following files so that resulting project structure would look like the following:
time_model.yaml
- YAML configuration file which contains model description.cities_timezones.txt
- Cities timezones database.TimeModel.scala
- Model implementation.CitiesDataProvider.scala
- Helper service which loads timezones database.GeoManager.scala
- Helper service which provides cities timezones information for user request.TimeModelSpec.scala
- Test that allows to test your model.| build.sbt +--project | build.properties \--src +--main | +--resources | | time_model.yaml | | cities_timezones.txt | \--scala | \--demo | CitiesDataProvider.scala | GeoManager.scala | TimeModel.scala \--test \--scala \--demo TimeModelSpec.scala
We are going to start with declaring the static part of our model using YAML which we will later load in our Scala-based model implementation. Open src/main/resources/time_model.yaml
file and replace its content with the following YAML:
macros: "<OF>": "{of|for|per}" "<CUR>": "{current|present|now|local}" "<TIME>": "{time <OF> day|day time|date|time|moment|datetime|hour|o'clock|clock|date time|date and time|time and date}" elements: - type: "x:time" description: "Date and/or time token indicator." synonyms: - "{<CUR>|_} <TIME>" - "what <TIME> {is it now|now|is it|_}"
There are number of important points here:
Line 1
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.Line 6
defines x:time
model element which is used in our intent defined in TimeModel
class. Note that this model element is defined mostly through macros we have defined above.YAML vs. API
As usual, this YAML-based static model definition is convenient but totally optional. All elements definitions can be provided programmatically inside Scala model TimeModel
class as well.
Open src/main/scala/demo/TimeModel.scala
file and replace its content with the following code:
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 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 NCModel( 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)
There are two intents, for local and remote locations. Result is represented as JSON value. Let's review this implementation step by step:
line 17
our class extends NCModel with two mandatory parameters.Line 14
creates IDL fragment
which is used in intent2
definition below.Lines 15 and 16
annotate two intents definitions intent1
and intent2
. Their callbacks below have references on them by their identifiers.Line 18
creates model configuration with most default parameters.Line 19
creates pipeline based on built-in components:time_model.yaml
.opennlp/en-ner-location.bin
for GEO locations detection.Lines 54 and 55
annotate intent intent2
and its callback method onRemoteMatch()
. This intent requires one mandatory entity - city which is used for getting time for its timezone.Lines 68 and 69
annotate intent intent1
and its callback method onLocalMatch()
. 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. Implementations of helper classes GeoManager
and CitiesDataProvider
are not related to given example logic. Just copy these classes and cities_timezones.txt
file from project source code into your demo project and don't forget to correct package names to demo
for copied classes. Links are below:
The test defined in TimeModelSpec
allows to check that all input test sentences are processed correctly and trigger the expected intents intent2
and intent1
:
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") } }
Line 9
creates the client for our model.Line 11
calls a special method debugAsk(). It allows to check the winning intent and its callback parameters without actually calling the intent.Lines 13-18
define all the test input sentences that should trigger requited intent. You can run this test via SBT task executeTests
or using IDE.
sbt executeTests
You've created time data model and tested it.