| --- |
| active_crumb: Weather Bot <code><sub>ex</sub></code> |
| layout: documentation |
| id: weather_bot |
| --- |
| |
| <!-- |
| 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 example demonstrates relatively complete NLI-based weather service with JSON output and a non-trivial |
| intent matching logic. It uses Apple's <a target="new" href="https://darksky.net">DarkSky</a> |
| REST service for the actual weather information. |
| </p> |
| <p> |
| Complexity: <span class="complexity-two-star"><i class="fas fa-star"></i><i class="fas 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/weather">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]"> |
| <dependencies> |
| <dependency> |
| <groupId>org.apache.nlpcraft</groupId> |
| <artifactId>nlpcraft</artifactId> |
| <version>{{site.latest_version}}</version> |
| </dependency> |
| </dependencies> |
| </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]"> |
| <properties> |
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
| <maven.compiler.source>1.8</maven.compiler.source> |
| <maven.compiler.target>1.8</maven.compiler.target> |
| </properties> |
| </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>weather_model.json</code> |
| file and add the following model declaration into it: |
| </p> |
| <pre class="brush: js, highlight: [10, 42, 50, 59]"> |
| { |
| "id": "nlpcraft.weather.ex", |
| "name": "Weather Example Model", |
| "version": "1.0", |
| "description": "Weather example model.", |
| "examples": [ |
| "What's the local weather forecast?", |
| "What's the weather in Moscow?" |
| ], |
| "macros": [ |
| { |
| "name": "<OF>", |
| "macro": "{of|for|per}" |
| }, |
| { |
| "name": "<CHANCE>", |
| "macro": "{chance|possibility|probability|odds|likelihood|potential|risk|opportunity}" |
| }, |
| { |
| "name": "<PHENOMENON>", |
| "macro": "{high sea|severe weather|hail|heat wave|cold wave|derecho|supercell|avalanche|cyclone|wildfire|landslide|firestorm|dust storm|thunder snow|winter storm|cloudburst|shower|condensation|precipitation|drizzle|rainstorm|rain storm|rainfall|rain|storm|sun|sunshine|cloud|hot|cold|dry|wet|wind||hurricane|typhoon|sand-storm|sand storm|tornado|humid|fog|snow|smog|black ice|haze|thundershower|thundersnow|sleet|drought|wildfire|blizzard|avalanche|mist|thunderstorm}" |
| }, |
| { |
| "name": "<CUR>", |
| "macro": "{current|present|moment|now}" |
| }, |
| { |
| "name": "<WEATHER>", |
| "macro": "{weather {condition|temp|temperature|data|*}|condition|temp|temperature}" |
| }, |
| { |
| "name": "<FORECAST>", |
| "macro": "{forecast|prognosis|prediction}" |
| }, |
| { |
| "name": "<HISTORY>", |
| "macro": "{history|past}" |
| } |
| ], |
| "elements": [ |
| { |
| "id": "wt:hist", |
| "description": "Past weather conditions.", |
| "synonyms": [ |
| "{<WEATHER>|*} <HISTORY>", |
| "<HISTORY> {<OF>|*} {<WEATHER>|<PHENOMENON>}" |
| ] |
| }, |
| { |
| "id": "wt:curr", |
| "description": "Current weather conditions.", |
| "synonyms": [ |
| "{<CUR>|*} {<WEATHER>|<PHENOMENON>}", |
| "<CHANCE> <OF> <PHENOMENON>", |
| "<PHENOMENON> {<CHANCE>|*}" |
| ] |
| }, |
| { |
| "id": "wt:fcast", |
| "description": "Future weather forecast.", |
| "synonyms": [ |
| "{<PHENOMENON>|<WEATHER>|*} <FORECAST>", |
| "<FORECAST> {<OF>|*} {<WEATHER>|<PHENOMENON>}" |
| ] |
| } |
| ] |
| } |
| </pre> |
| <p>There are number of important points here:</p> |
| <ul> |
| <li> |
| <code>Line 10</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>Lines 42, 50, 59</code> define three model elements: the past, present and future (forecast) weather |
| condition. |
| </li> |
| </ul> |
| <p> |
| Now that our model is ready let's create a Java class that would load this model and define intents |
| that use the model elements we have just defined. |
| </p> |
| </section> |
| <section id="code"> |
| <h3 class="section-title">Model Class</h3> |
| <p> |
| Our Java implementation uses one auxiliary class <a target="github" href="https://github.com/apache/incubator-nlpcraft/blob/master/src/main/scala/org/apache/nlpcraft/examples/weather/WeatherResultWrapper.java">WeatherResultWrapper.java</a> |
| that is used for JSON result formatting. Note that the main <a target="github" href="https://github.com/apache/incubator-nlpcraft/blob/master/src/main/scala/org/apache/nlpcraft/examples/weather/WeatherModel.java">WeatherModel</a> class |
| have number of utility methods that we'll skip here (you can see the entire class by following the link above) and |
| we'll concentrate on the intents only: |
| </p> |
| <pre class="brush: java, highlight: [1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 19, 20, 21, 22, 23]"> |
| @NCIntent("intent=fcast term={id == 'wt:fcast'} term(city)={id == 'nlpcraft:city'}? term(date)={id == 'nlpcraft:date'}?") |
| public NCResult onForecastMatch( |
| NCIntentMatch ctx, |
| @NCIntentTerm("city") Optional<NCToken> cityTokOpt, |
| @NCIntentTerm("date") Optional<NCToken> dateTokOpt |
| ) { |
| return onPeriodMatch(ctx, cityTokOpt, dateTokOpt, 5); |
| } |
| |
| @NCIntent("intent=hist term={id == 'wt:hist'} term(city)={id == 'nlpcraft:city'}? term(date)={id == 'nlpcraft:date'}?") |
| public NCResult onHistoryMatch( |
| NCIntentMatch ctx, |
| @NCIntentTerm("city") Optional<NCToken> cityTokOpt, |
| @NCIntentTerm("date") Optional<NCToken> dateTokOpt |
| ) { |
| return onPeriodMatch(ctx, cityTokOpt, dateTokOpt, -5); |
| } |
| |
| @NCIntent("intent=curr term={id == 'wt:curr'} term(city)={id == 'nlpcraft:city'}? term(date)={id == 'nlpcraft:date'}?") |
| public NCResult onCurrentMatch( |
| NCIntentMatch ctx, |
| @NCIntentTerm("city") Optional<NCToken> cityTokOpt, |
| @NCIntentTerm("date") Optional<NCToken> dateTokOpt |
| ) { |
| checkMatch(ctx); |
| |
| try { |
| Coordinate cr = prepGeo(ctx, cityTokOpt); |
| |
| if (dateTokOpt.isPresent()) { |
| DateRange range = extractDate(dateTokOpt.get()); |
| |
| return makeResult(srv.getTimeMachine(cr.latitude, cr.longitude, range.from, range.to), ctx.getIntentId()); |
| } |
| |
| return makeResult(srv.getCurrent(cr.latitude, cr.longitude), ctx.getIntentId()); |
| } |
| catch (DarkSkyException e) { |
| throw new NCRejection(e.getLocalizedMessage()); |
| } |
| catch (NCRejection e) { |
| throw e; |
| } |
| catch (Exception e) { |
| throw new NCRejection("Weather provider error.", e); |
| } |
| } |
| </pre> |
| <p> |
| There three methods define three intents. Each intent is defined "in place", i.e. as an annotation on the |
| method that acts as a callback for that intent: |
| </p> |
| <ul> |
| <li> |
| Line 1 defines intent and the callback for the weather forecast (weather in the future): |
| <ul> |
| <li>Intent is ordered (default), supports conversation (default) and has ID <code>fcast</code></li> |
| <li>Intent has one mandatory term and two optional terms: |
| <ul> |
| <li> |
| Mandatory term is defined as a token with ID <code>wt:fcast</code> as defined |
| in the model. |
| </li> |
| <li> |
| Two optional terms (their IDs are <code>city</code> and <code>date</code>) define |
| optional city and date for the weather report. If not provided, their will be taken |
| from either conversation context or assume the default value (current IP geo location |
| and current time, if available). |
| </li> |
| </ul> |
| </li> |
| </ul> |
| </li> |
| <li> |
| Lines 3-5 define formal parameters for the <code>fcast</code> intent callback method. Note that we use term IDs |
| (<code>city</code> and <code>date</code>) in <code>@NCIntentTerm</code> annotations to automatically |
| assign tokens from the detected terms to their corresponding method formal parameters. |
| </li> |
| <li> |
| Lines 10-14 and lines 19-23 define two other intents and their callbacks methods in the same manner. |
| </li> |
| <li> |
| Implementation of all intent callback method is pretty straightforward and deals mostly with figuring |
| out the default location and time if either one isn't provided. |
| </li> |
| </ul> |
| </section> |
| <section id="tools"> |
| <h3 class="section-title">External Tools</h3> |
| <p> |
| This example uses several external tools to implement its functionality: |
| </p> |
| <ul> |
| <li> |
| <a target=_ href="https://darksky.net">Apple DarkSky</a> - to provide actual weather data service |
| See <code>org.apache.nlpcraft.examples.misc.darksky</code> package for details. |
| </li> |
| <li> |
| <a target="_" href="https://tools.keycdn.com/geo">KeyCDN's IP Location Finder</a> - to provide IP location |
| service. See <code>org.apache.nlpcraft.examples.misc.geo.keycdn</code> package for details. |
| </li> |
| <li> |
| City to timezone mapper. See <code>org.apache.nlpcraft.examples.misc.geo.cities</code> package for details. |
| </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 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.weather.WeatherModel</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 |
| use the default configuration and override 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>"What's the local weather forecast?"</code></li> |
| <li><code>"What's the weather in Moscow?"</code></li> |
| <li><code>"Chance of snow?"</code> (using conversation context)</li> |
| <li><code>"Moscow?"</code> (using conversation context).</li> |
| </ul> |
| <p> |
| Let's create new Java class <code>WeatherTest.java</code> with the following code: |
| </p> |
| <pre class="brush: java, highlight: [47, 43, 55, 59]"> |
| package org.apache.nlpcraft.examples.weather; |
| |
| import com.google.gson.Gson; |
| import com.google.gson.reflect.TypeToken; |
| 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.model.tools.test.NCTestResult; |
| import org.apache.nlpcraft.probe.embedded.NCEmbeddedProbe; |
| import org.junit.jupiter.api.AfterEach; |
| import org.junit.jupiter.api.BeforeEach; |
| import org.junit.jupiter.api.Test; |
| |
| import java.io.IOException; |
| import java.lang.reflect.Type; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import static org.junit.jupiter.api.Assertions.*; |
| |
| class WeatherTest { |
| private static final Gson GSON = new Gson(); |
| private static final Type TYPE_MAP_RESP = new TypeToken<HashMap<String, Object>>() {}.getType(); |
| private NCTestClient cli; |
| |
| private void checkIntent(String txt, String intentId, boolean shouldBeSame) throws NCException, IOException { |
| NCTestResult res = cli.ask(txt); |
| |
| assertTrue(res.isOk(), () -> res.getResultError().get()); |
| |
| assert res.getResult().isPresent(); |
| |
| Map<String, Object> map = GSON.fromJson(res.getResult().get(), TYPE_MAP_RESP); |
| |
| if (shouldBeSame) |
| assertEquals(intentId, map.get("intentId")); |
| else |
| assertNotEquals(intentId, map.get("intentId")); |
| } |
| |
| @BeforeEach |
| void setUp() throws NCException, IOException { |
| NCEmbeddedProbe.start(WeatherModel.class); |
| |
| cli = new NCTestClientBuilder().newBuilder().build(); |
| |
| cli.open("nlpcraft.weather.ex"); // See weather_model.json |
| } |
| |
| @AfterEach |
| void tearDown() throws NCException, IOException { |
| if (cli != null) |
| cli.close(); |
| |
| NCEmbeddedProbe.stop(); |
| } |
| |
| @Test |
| void test() throws NCException, IOException { |
| // Empty parameter. |
| assertTrue(cli.ask("").isFailed()); |
| |
| // Only latin charset is supported. |
| assertTrue(cli.ask("El tiempo en España").isFailed()); |
| |
| // Should be passed. |
| checkIntent("What's the local weather forecast?", "fcast", true); |
| checkIntent("What's the weather in Moscow?", "curr", true); |
| // Can be answered with conversation. |
| checkIntent("Chance of snow?", "curr", true); |
| checkIntent("Moscow", "curr", true); |
| |
| cli.clearConversation(); |
| |
| // Cannot be answered without conversation. |
| assertTrue(cli.ask("Moscow").isFailed()); |
| } |
| } |
| </pre> |
| <p> |
| This test is pretty straight forward: |
| </p> |
| <ul> |
| <li> |
| On line 47 we open the test client with the model ID (see <code>weather_model.yaml</code> |
| file for where we declared it). |
| </li> |
| <li> |
| Test on line 59 is where we issue our test sentences and we should see |
| the confirmation messages in our test console output. |
| </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 43 and 55 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 pretty full featured weather bot 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="#tools">External Tools</a></li> |
| <li><a href="#start_probe">Start Probe <sub>opt</sub></a></li> |
| <li><a href="#start_server">Start Server</a></li> |
| <li><a href="#testing">Testing</a></li> |
| {% include quick-links.html %} |
| </ul> |
| </div> |
| |
| |
| |
| |
| |
| |