You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nlpcraft.apache.org by se...@apache.org on 2022/02/25 11:55:04 UTC

[incubator-nlpcraft] branch master updated: Examples refactoring.

This is an automated email from the ASF dual-hosted git repository.

sergeykamov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git


The following commit(s) were added to refs/heads/master by this push:
     new 32a411c  Examples refactoring.
32a411c is described below

commit 32a411c6d44754532129233bcc889c15720149c8
Author: Sergey Kamov <sk...@gmail.com>
AuthorDate: Fri Feb 25 14:54:57 2022 +0300

    Examples refactoring.
---
 .../{weather => lightswitch-ru}/README.md          |  15 +-
 .../{weather => lightswitch-ru}/pom.xml            |  32 ++-
 .../examples/lightswitch/LightSwitchModelRu.scala  |  89 +++++++
 .../lightswitch/ru/NCSemanticStemmerRu.scala}      |  29 +--
 .../ru/NCStopWordsTokenEnricherRu.scala            |  43 ++++
 .../examples/lightswitch/ru/NCTokenParserRu.scala  |  78 ++++++
 .../src/main/resources/lightswitch_model_ru.yaml   |  45 ++++
 .../lightswitch/NCModelValidationSpec.scala}       |  34 +--
 nlpcraft-examples/time/pom.xml                     |   6 -
 .../apache/nlpcraft/examples/time/TimeModel.java   |   7 +-
 .../time}/utils/cities/CitiesDataProvider.java     |   2 +-
 .../nlpcraft/examples/time}/utils/cities/City.java |   2 +-
 .../examples/time}/utils/cities/CityData.java      |   2 +-
 .../examples/time}/utils/keycdn/GeoData.java       |   2 +-
 .../examples/time}/utils/keycdn/GeoManager.java    |   2 +-
 .../examples/time}/utils/keycdn/Response.java      |   2 +-
 .../examples/time}/utils/keycdn/ResponseData.java  |   2 +-
 .../src/main/resources/cities_timezones.txt        |   0
 nlpcraft-examples/utils/pom.xml                    |  70 -----
 .../nlpcraft/examples/weather/WeatherModel.java    | 281 ---------------------
 .../openweathermap/OpenWeatherMapService.java      | 214 ----------------
 .../weather/src/main/resources/weather_model.json  |  59 -----
 .../examples/weather/NCModelValidationSpec.scala   |  46 ----
 .../internal/impl/NCModelPipelineManager.scala     |  26 +-
 pom.xml                                            |   8 +-
 25 files changed, 335 insertions(+), 761 deletions(-)

diff --git a/nlpcraft-examples/weather/README.md b/nlpcraft-examples/lightswitch-ru/README.md
similarity index 79%
rename from nlpcraft-examples/weather/README.md
rename to nlpcraft-examples/lightswitch-ru/README.md
index 589cdc3..f073eff 100644
--- a/nlpcraft-examples/weather/README.md
+++ b/nlpcraft-examples/lightswitch-ru/README.md
@@ -15,7 +15,7 @@
  limitations under the License.
 -->
 
-<img src="https://nlpcraft.apache.org/images/nlpcraft_logo_black.gif" height="80px">
+<img alt="" src="https://nlpcraft.apache.org/images/nlpcraft_logo_black.gif" height="80px">
 <br>
 
 [![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/apache/opennlp/master/LICENSE)
@@ -23,13 +23,14 @@
 [![Documentation Status](https://img.shields.io/:docs-latest-green.svg)](https://nlpcraft.apache.org/docs.html)
 [![Gitter](https://badges.gitter.im/apache-nlpcraft/community.svg)](https://gitter.im/apache-nlpcraft/community)
 
-### Weather Service Example
-This example demonstrates relatively complete NLI-based weather service with JSON output and a non-trivial
-intent matching logic. It uses Apple's [Dark Sky](https://darksky.net) API weather provider REST service for the actual 
-weather information.
+### Light Switch Example
+This example provides very simple implementation for NLI-powered light switch. You can say something like `turn the lights off in
+the entire house` or `switch on the illumination in the master bedroom closet`. 
+You can easily modify intent callbacks to perform the actual light switching using HomeKit or Arduino-based
+controllers.
 
-### Documentation  
-See [WeatherBot](https://nlpcraft.apache.org/examples/weather_bot.html) guide for more instructions on how to run this example.
+### Documentation
+See [Light Switch](https://nlpcraft.apache.org/examples/light_switch.html) guide for more instructions on how to run this example.
 
 For any questions, feedback or suggestions:
 
diff --git a/nlpcraft-examples/weather/pom.xml b/nlpcraft-examples/lightswitch-ru/pom.xml
similarity index 76%
rename from nlpcraft-examples/weather/pom.xml
rename to nlpcraft-examples/lightswitch-ru/pom.xml
index 4e26bc3..67c610f 100644
--- a/nlpcraft-examples/weather/pom.xml
+++ b/nlpcraft-examples/lightswitch-ru/pom.xml
@@ -21,8 +21,8 @@
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
-    <name>NLPCraft Example Weather</name>
-    <artifactId>nlpcraft-example-weather</artifactId>
+    <name>NLPCraft Example Light Switch RU</name>
+    <artifactId>nlpcraft-example-lightswitch-ru</artifactId>
 
     <parent>
         <artifactId>nlpcraft-parent</artifactId>
@@ -31,10 +31,6 @@
         <relativePath>../../pom.xml</relativePath>
     </parent>
 
-    <properties>
-        <apache.httpcomponents.ver>4.5.13</apache.httpcomponents.ver>
-    </properties>
-
     <dependencies>
         <dependency>
             <groupId>${project.groupId}</groupId>
@@ -43,16 +39,28 @@
         </dependency>
 
         <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>nlpcraft-stanford</artifactId>
-            <version>${project.version}</version>
+            <groupId>org.apache.lucene</groupId>
+            <artifactId>lucene-analyzers-common</artifactId>
+            <version>8.11.1</version>
         </dependency>
 
         <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>nlpcraft-examples-utils</artifactId>
-            <version>${project.version}</version>
+            <groupId>org.languagetool</groupId>
+            <artifactId>language-de</artifactId>
+            <version>5.6</version>
         </dependency>
+        <dependency>
+            <groupId>org.languagetool</groupId>
+            <artifactId>language-ru</artifactId>
+            <version>5.6</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.languagetool</groupId>
+            <artifactId>languagetool-core</artifactId>
+            <version>5.6</version>
+        </dependency>
+
 
         <!-- Test dependencies. -->
         <dependency>
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchModelRu.scala b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchModelRu.scala
new file mode 100644
index 0000000..b36b1b2
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchModelRu.scala
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+package org.apache.nlpcraft.examples.lightswitch
+
+import org.apache.nlpcraft.*
+import org.apache.nlpcraft.examples.lightswitch.ru.*
+import org.apache.nlpcraft.nlp.entity.parser.nlp.NCNLPEntityParser
+import org.apache.nlpcraft.nlp.entity.parser.semantic.NCSemanticEntityParser
+import org.apache.nlpcraft.nlp.entity.parser.semantic.impl.en.NCEnSemanticPorterStemmer
+import org.apache.nlpcraft.nlp.token.parser.opennlp.NCOpenNLPTokenParser
+import org.apache.nlpcraft.nlp.token.enricher.en.NCStopWordsTokenEnricher
+
+/**
+  * This example provides very simple implementation for NLI-powered light switch.
+  * You can say something like this:
+  * <ul>
+  *     <li>"Turn the lights off in the entire house."</li>
+  *     <li>"Switch on the illumination in the master bedroom closet."</li>
+  * </ul>
+  * You can easily modify intent callbacks to perform the actual light switching using
+  * HomeKit or Arduino-based controllers.
+  * <p>
+  * See 'README.md' file in the same folder for running and testing instructions.
+  */
+
+class LightSwitchModelRu extends NCModel:
+    override val getConfig: NCModelConfig = new NCModelConfig("nlpcraft.lightswitch.ru.ex", "LightSwitch Example Model RU", "1.0")
+    override val getPipeline: NCModelPipeline =
+        val tp = new NCTokenParserRu
+        new NCModelPipelineBuilder(
+            tp,
+            new NCSemanticEntityParser(new NCSemanticStemmerRu(), tp, "lightswitch_model_ru.yaml")
+        ).
+            withTokenEnricher(new NCStopWordsTokenEnricherRu()).
+            build()
+
+    /**
+      * Intent and its on-match callback.
+      *
+      * @param actEnt Token from `act` term (guaranteed to be one).
+      * @param locEnts Tokens from `loc` term (zero or more).
+      * @return Query result to be sent to the REST caller.
+      */
+    @NCIntent("intent=ls term(act)={has(ent_groups, 'act')} term(loc)={# == 'ls:loc'}*")
+    @NCIntentSample(Array(
+        "Выключи свет по всем доме",
+        "Выруби электричество!",
+        "Включи свет в детской",
+        "Включай повсюду освещение",
+        "Включайте лампы в детской комнате",
+        "Свет на кухне пожалуйста приглуши",
+        "Нельзя ли повсюду выключить свет",
+        "Пожалуйста без света",
+        "Отключи электричесвто в ванной",
+        "Выключи, пожалуйста, тут всюду свет",
+        "Выключай все!",
+        "Свет пожалуйсте везде включи"
+    ))
+    def onMatch(
+        @NCIntentTerm("act") actEnt: NCEntity,
+        @NCIntentTerm("loc") locEnts: List[NCEntity]
+    ): NCResult =
+        val status = if actEnt.getId == "ls:on" then "on" else "off"
+        val locations = if locEnts.isEmpty then "entire house" else locEnts.map(_.mkText()).mkString(", ")
+
+        // Add HomeKit, Arduino or other integration here.
+
+        // By default - just return a descriptive action string.
+        val res = new NCResult()
+
+        res.setType(NCResultType.ASK_RESULT)
+        res.setBody(s"Lights are [$status] in [${locations.toLowerCase}].")
+
+        res
\ No newline at end of file
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/ResponseData.java b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCSemanticStemmerRu.scala
similarity index 63%
copy from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/ResponseData.java
copy to nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCSemanticStemmerRu.scala
index 20a9524..e49c72c 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/ResponseData.java
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCSemanticStemmerRu.scala
@@ -15,29 +15,12 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.keycdn;
+package org.apache.nlpcraft.examples.lightswitch.ru
 
-/**
- * Service https://tools.keycdn.com/geo response bean.
- */
-class ResponseData {
-    private GeoData geo;
+import opennlp.tools.stemmer.snowball.SnowballStemmer
+import org.apache.nlpcraft.nlp.entity.parser.semantic.NCSemanticStemmer
 
-    /**
-     * Gets geo data holder.
-     *
-     * @return Geo data holder.
-     */
-    public GeoData getGeo() {
-        return geo;
-    }
+class NCSemanticStemmerRu extends NCSemanticStemmer:
+    private val stemmer = new SnowballStemmer(SnowballStemmer.ALGORITHM.RUSSIAN)
 
-    /**
-     * Sets get data holder.
-     *
-     * @param geo Geo data holder to set.
-     */
-    public void setGeo(GeoData geo) {
-        this.geo = geo;
-    }
-}
+    override def stem(txt: String): String = stemmer.synchronized { stemmer.stem(txt.toLowerCase).toString }
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCStopWordsTokenEnricherRu.scala b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCStopWordsTokenEnricherRu.scala
new file mode 100644
index 0000000..0e9c064
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCStopWordsTokenEnricherRu.scala
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+package org.apache.nlpcraft.examples.lightswitch.ru
+
+import org.apache.lucene.analysis.ru.RussianAnalyzer
+import org.apache.nlpcraft.*
+
+import java.util
+import scala.jdk.CollectionConverters.*
+
+/**
+  *
+  */
+class NCStopWordsTokenEnricherRu extends NCTokenEnricher:
+    private final val stops = RussianAnalyzer.getDefaultStopSet
+
+    override def enrich(req: NCRequest, cfg: NCModelConfig, toks: util.List[NCToken]): Unit =
+        toks.asScala.foreach(t =>
+            t.put(
+                "stopword",
+                t.getLemma.length == 1 && !Character.isLetter(t.getLemma.head) ||
+                t.getPos.startsWith("PARTICLE") ||
+                t.getPos.startsWith("INTERJECTION") ||
+                t.getPos.startsWith("PREP") ||
+                stops.contains(t.getLemma) ||
+                stops.contains(t.getText.toLowerCase)
+            )
+        )
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCTokenParserRu.scala b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCTokenParserRu.scala
new file mode 100644
index 0000000..5bda243
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCTokenParserRu.scala
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+package org.apache.nlpcraft.examples.lightswitch.ru
+
+import org.apache.lucene.analysis.ru.RussianAnalyzer
+import org.apache.nlpcraft.*
+import org.languagetool.AnalyzedToken
+import org.languagetool.language.Russian
+import org.languagetool.rules.ngrams.*
+import org.languagetool.tagging.ru.*
+import org.languagetool.tokenizers.WordTokenizer
+
+import java.util
+import scala.jdk.CollectionConverters.*
+
+object NCTokenParserRu:
+    private val tokenizer = new WordTokenizer
+
+    private case class Span(word: String, start: Int, end: Int)
+
+    private def nvl(v: String, dflt : => String): String = if v != null then v else dflt
+
+    private def split(text: String): Seq[Span] =
+        val spans = collection.mutable.ArrayBuffer.empty[Span]
+        var sumLen = 0
+
+        for (((word, len), idx) <- tokenizer.tokenize(text).asScala.map(p => p -> p.length).zipWithIndex)
+            if word.strip.nonEmpty then spans += Span(word, sumLen, sumLen + word.length)
+            sumLen += word.length
+
+        spans.toSeq
+
+import org.apache.nlpcraft.examples.lightswitch.ru.NCTokenParserRu.*
+
+class NCTokenParserRu extends NCTokenParser:
+    override def tokenize(text: String): util.List[NCToken] =
+        val spans = split(text)
+        val tags = RussianTagger.INSTANCE.tag(spans.map(_.word).asJava).asScala
+
+        require(spans.size == tags.size)
+
+        spans.zip(tags).zipWithIndex.map { case ((span, tag), idx) =>
+            val readings = tag.getReadings.asScala
+
+            val (lemma, pos) =
+                readings.size match
+                    // No data. Lemma is word as is, POS is undefined.
+                    case 0 => (span.word, "")
+                    // Takes first. Other variants ignored.
+                    case _ =>
+                        val aTok: AnalyzedToken = readings.head
+                        (nvl(aTok.getLemma, span.word), nvl(aTok.getPOSTag, ""))
+
+            val tok: NCToken =
+                new NCPropertyMapAdapter with NCToken:
+                    override val getText: String = span.word
+                    override val getIndex: Int = idx
+                    override val getStartCharIndex: Int = span.start
+                    override val getEndCharIndex: Int = span.end
+                    override val getLemma: String = lemma.toLowerCase // TODO: discuss
+                    override val getPos: String = pos
+            tok
+        }.asJava
\ No newline at end of file
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/resources/lightswitch_model_ru.yaml b/nlpcraft-examples/lightswitch-ru/src/main/resources/lightswitch_model_ru.yaml
new file mode 100644
index 0000000..f8294ac
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/src/main/resources/lightswitch_model_ru.yaml
@@ -0,0 +1,45 @@
+#
+# 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.
+#
+
+macros:
+  "<TURN_ON>" : "{включить|включать|врубить|врубать|запустить|запускать|зажигать|зажечь}"
+  "<TURN_OFF>" : "{погасить|загасить|гасить|выключить|выключать|вырубить|вырубать|отключить|отключать|убрать|убирать|приглушить|приглушать|стоп}"
+  "<ENTIRE_OPT>" : "{весь|все|всё|повсюду|вокруг|полностью|везде|_}"
+  "<LIGHT_OPT>" : "{это|лампа|бра|люстра|светильник|лампочка|лампа|освещение|свет|электричество|электрика|_}"
+
+elements:
+  - id: "ls:loc"
+    description: "Location of lights."
+    synonyms:
+      - "<ENTIRE_OPT> {здание|помещение|дом|кухня|детская|кабинет|гостиная|спальня|ванная|туалет|{большая|обеденная|ванная|детская|туалетная} комната}"
+
+  - id: "ls:on"
+    groups:
+      - "act"
+    description: "Light switch ON action."
+    synonyms:
+      - "<LIGHT_OPT> <ENTIRE_OPT> <TURN_ON>"
+      - "<TURN_ON> <ENTIRE_OPT> <LIGHT_OPT>"
+
+  - id: "ls:off"
+    groups:
+      - "act"
+    description: "Light switch OFF action."
+    synonyms:
+      - "<LIGHT_OPT> <ENTIRE_OPT> <TURN_OFF>"
+      - "<TURN_OFF> <ENTIRE_OPT> <LIGHT_OPT>"
+      - "без <ENTIRE_OPT> <LIGHT_OPT>"
diff --git a/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/openweathermap/OpenWeatherMapException.java b/nlpcraft-examples/lightswitch-ru/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala
similarity index 59%
rename from nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/openweathermap/OpenWeatherMapException.java
rename to nlpcraft-examples/lightswitch-ru/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala
index 5f64269..b6d9d1b 100644
--- a/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/openweathermap/OpenWeatherMapException.java
+++ b/nlpcraft-examples/lightswitch-ru/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala
@@ -15,28 +15,18 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.weather.openweathermap;
+package org.apache.nlpcraft.examples.lightswitch
+
+import org.apache.nlpcraft.*
+import org.junit.jupiter.api.*
+
+import scala.util.Using
 
 /**
- * Open Weather Map exception.
- */
-public class OpenWeatherMapException extends RuntimeException {
-    /**
-     * Creates new exception.
-     *
-     * @param msg Error message.
-     */
-    public OpenWeatherMapException(String msg) {
-        super(msg);
-    }
+  * JUnit models validation.
+  */
+class NCModelValidationSpec:
+    private val MDL = new LightSwitchModelRu
 
-    /**
-     * Creates new exceptions.
-     *
-     * @param msg Error message.
-     * @param cause Optional cause.
-     */
-    public OpenWeatherMapException(String msg, Throwable cause) {
-        super(msg, cause);
-    }
-}
\ No newline at end of file
+    @Test
+    def test(): Unit = Using.resource(new NCModelClient(MDL)) { client => client.validateSamples() }
diff --git a/nlpcraft-examples/time/pom.xml b/nlpcraft-examples/time/pom.xml
index 7988f1c..a2e5e3b 100644
--- a/nlpcraft-examples/time/pom.xml
+++ b/nlpcraft-examples/time/pom.xml
@@ -38,12 +38,6 @@
             <version>${project.version}</version>
         </dependency>
 
-        <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>nlpcraft-examples-utils</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-
         <!-- Test dependencies. -->
         <dependency>
             <groupId>org.junit.jupiter</groupId>
diff --git a/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/TimeModel.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/TimeModel.java
index 6d8d29e..1d63e6a 100644
--- a/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/TimeModel.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/TimeModel.java
@@ -21,11 +21,8 @@ import com.fasterxml.jackson.core.*;
 import com.fasterxml.jackson.databind.*;
 import com.fasterxml.jackson.dataformat.yaml.*;
 import org.apache.nlpcraft.*;
-import org.apache.nlpcraft.examples.utils.cities.CitiesDataProvider;
-import org.apache.nlpcraft.examples.utils.cities.City;
-import org.apache.nlpcraft.examples.utils.cities.CityData;
-import org.apache.nlpcraft.examples.utils.keycdn.GeoManager;
-import org.apache.nlpcraft.examples.utils.keycdn.GeoData;
+import org.apache.nlpcraft.examples.time.utils.cities.*;
+import org.apache.nlpcraft.examples.time.utils.keycdn.*;
 import org.apache.nlpcraft.nlp.entity.parser.opennlp.NCOpenNLPEntityParser;
 import org.apache.nlpcraft.nlp.entity.parser.semantic.NCSemanticEntityParser;
 import org.apache.nlpcraft.nlp.entity.parser.semantic.impl.en.NCEnSemanticPorterStemmer;
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/CitiesDataProvider.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/CitiesDataProvider.java
similarity index 98%
rename from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/CitiesDataProvider.java
rename to nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/CitiesDataProvider.java
index 2b14e7a..3e85565 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/CitiesDataProvider.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/CitiesDataProvider.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.cities;
+package org.apache.nlpcraft.examples.time.utils.cities;
 
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.nlpcraft.NCException;
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/City.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/City.java
similarity index 97%
rename from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/City.java
rename to nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/City.java
index 72cf102..718b308 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/City.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/City.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.cities;
+package org.apache.nlpcraft.examples.time.utils.cities;
 
 import java.util.Objects;
 
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/CityData.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/CityData.java
similarity index 96%
rename from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/CityData.java
rename to nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/CityData.java
index 4ba42e9..3fa40c8 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/cities/CityData.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/cities/CityData.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.cities;
+package org.apache.nlpcraft.examples.time.utils.cities;
 
 /**
  * City data holder.
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/GeoData.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/GeoData.java
similarity index 98%
rename from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/GeoData.java
rename to nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/GeoData.java
index 85c7238..2a3e940 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/GeoData.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/GeoData.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.keycdn;
+package org.apache.nlpcraft.examples.time.utils.keycdn;
 
 import com.google.gson.annotations.SerializedName;
 
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/GeoManager.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/GeoManager.java
similarity index 98%
rename from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/GeoManager.java
rename to nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/GeoManager.java
index 756fc5a..9de10ec 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/GeoManager.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/GeoManager.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.keycdn;
+package org.apache.nlpcraft.examples.time.utils.keycdn;
 
 import com.google.gson.Gson;
 import org.apache.nlpcraft.NCRequest;
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/Response.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/Response.java
similarity index 97%
rename from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/Response.java
rename to nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/Response.java
index eca6deb..147b55a 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/Response.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/Response.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.keycdn;
+package org.apache.nlpcraft.examples.time.utils.keycdn;
 
 /**
  * Service https://tools.keycdn.com/geo response part bean.
diff --git a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/ResponseData.java b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/ResponseData.java
similarity index 95%
rename from nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/ResponseData.java
rename to nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/ResponseData.java
index 20a9524..a130d8a 100644
--- a/nlpcraft-examples/utils/src/main/java/org/apache/nlpcraft/examples/utils/keycdn/ResponseData.java
+++ b/nlpcraft-examples/time/src/main/java/org/apache/nlpcraft/examples/time/utils/keycdn/ResponseData.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.examples.utils.keycdn;
+package org.apache.nlpcraft.examples.time.utils.keycdn;
 
 /**
  * Service https://tools.keycdn.com/geo response bean.
diff --git a/nlpcraft-examples/utils/src/main/resources/cities_timezones.txt b/nlpcraft-examples/time/src/main/resources/cities_timezones.txt
similarity index 100%
rename from nlpcraft-examples/utils/src/main/resources/cities_timezones.txt
rename to nlpcraft-examples/time/src/main/resources/cities_timezones.txt
diff --git a/nlpcraft-examples/utils/pom.xml b/nlpcraft-examples/utils/pom.xml
deleted file mode 100644
index 587e0c4..0000000
--- a/nlpcraft-examples/utils/pom.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!--
- 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.
--->
-
-<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-
-    <name>NLPCraft Examples Utils</name>
-    <artifactId>nlpcraft-examples-utils</artifactId>
-
-    <parent>
-        <artifactId>nlpcraft-parent</artifactId>
-        <groupId>org.apache.nlpcraft</groupId>
-        <version>1.0.0</version>
-        <relativePath>../../pom.xml</relativePath>
-    </parent>
-
-    <dependencies>
-        <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>nlpcraft</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-
-        <!-- Test dependencies. -->
-        <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>nlpcraft</artifactId>
-            <version>${project.version}</version>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-engine</artifactId>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>${maven.compiler.plugin.ver}</version>
-                <configuration>
-                    <source>${java.ver}</source>
-                    <target>${java.ver}</target>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
-</project>
\ No newline at end of file
diff --git a/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/WeatherModel.java b/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/WeatherModel.java
deleted file mode 100644
index 1a37d7a..0000000
--- a/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/WeatherModel.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.nlpcraft.examples.weather;
-
-import com.google.gson.Gson;
-import edu.stanford.nlp.pipeline.StanfordCoreNLP;
-import org.apache.nlpcraft.NCEntity;
-import org.apache.nlpcraft.NCIntent;
-import org.apache.nlpcraft.NCIntentMatch;
-import org.apache.nlpcraft.NCIntentSample;
-import org.apache.nlpcraft.NCIntentTerm;
-import org.apache.nlpcraft.NCModel;
-import org.apache.nlpcraft.NCModelConfig;
-import org.apache.nlpcraft.NCModelPipeline;
-import org.apache.nlpcraft.NCModelPipelineBuilder;
-import org.apache.nlpcraft.NCRejection;
-import org.apache.nlpcraft.NCResult;
-import org.apache.nlpcraft.NCResultType;
-import org.apache.nlpcraft.examples.utils.cities.CitiesDataProvider;
-import org.apache.nlpcraft.examples.utils.cities.City;
-import org.apache.nlpcraft.examples.utils.cities.CityData;
-import org.apache.nlpcraft.examples.utils.keycdn.GeoManager;
-import org.apache.nlpcraft.examples.utils.keycdn.GeoData;
-import org.apache.nlpcraft.examples.weather.openweathermap.OpenWeatherMapException;
-import org.apache.nlpcraft.examples.weather.openweathermap.OpenWeatherMapService;
-import org.apache.nlpcraft.nlp.entity.parser.semantic.NCSemanticEntityParser;
-import org.apache.nlpcraft.nlp.entity.parser.semantic.impl.en.NCEnSemanticPorterStemmer;
-import org.apache.nlpcraft.nlp.entity.parser.stanford.NCStanfordNLPEntityParser;
-import org.apache.nlpcraft.nlp.token.parser.stanford.NCStanfordNLPTokenParser;
-
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Properties;
-import java.util.Set;
-
-import static java.time.temporal.ChronoUnit.DAYS;
-
-/**
- * Weather example data model.
- * <p>
- * This is a relatively complete weather service with JSON output and a non-trivial
- * intent matching logic. It uses OpenWeather API weather provider REST service for the actual
- * weather information (https://openweathermap.org/api/one-call-api).
- * <p>
- * NOTE: you must provide OpenWeather API key in 'OWM_API_KEY' system property.
- * See  https://openweathermap.org/api for more information.
- * <p>
- * See 'README.md' file in the same folder for running and testing instructions.
- */
-@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
-public class WeatherModel implements NCModel {
-    // System property for OpenWeatherMap API key.
-    public final String OWM_API_KEY = "OWM_API_KEY";
-
-    // Please register your own account at https://openweathermap.org/api and
-    // replace this demo token with your own.
-    // We are using the One Call API (https://openweathermap.org/api/one-call-api) in this example
-    private final OpenWeatherMapService openWeather;
-
-    // Geo manager.
-    private final GeoManager geoMrg = new GeoManager();
-
-    // Default shift in days for history and forecast.
-    private static final int DAYS_SHIFT_BACK = 5;
-    private static final int DAYS_SHIFT_FORWARD = 7;
-
-    // GSON instance.
-    private static final Gson GSON = new Gson();
-
-    static private final Map<City, CityData> citiesData = CitiesDataProvider.get();
-
-    // Keywords for 'local' weather.
-    private static final Set<String> LOCAL_WORDS = new HashSet<>(Arrays.asList("my", "local", "hometown"));
-
-    private final NCModelConfig cfg;
-    private final NCModelPipeline pipeline;
-
-    /**
-     * Extracts geolocation (city) from given solver context that is suitable for Dark Sky API weather service.
-     *
-     * @param ctx Intent solver context.
-     * @param geoEntOpt Optional geo entity.
-     * @return Geo location.
-     */
-    private CityData prepGeo(NCIntentMatch ctx, Optional<NCEntity> geoEntOpt) throws NCRejection {
-        if (geoEntOpt.isPresent()) {
-            String cityName = geoEntOpt.get().mkText();
-
-            Optional<Map.Entry<City, CityData>> dataOpt =
-                citiesData.entrySet().stream().filter(p -> p.getKey().getName().equalsIgnoreCase(cityName)).findAny();
-
-            if (!dataOpt.isPresent()) {
-                throw new NCRejection(String.format("Latitude and longitude not found for: %s", cityName));
-            }
-
-            return  dataOpt.get().getValue();
-        }
-
-        Optional<GeoData> geoOpt = geoMrg.get(ctx.getContext().getRequest());
-
-        if (geoOpt.isEmpty())
-            throw new NCRejection("City cannot be determined.");
-
-        // Manually process request for local weather. We need to separate between 'local Moscow weather'
-        // and 'local weather' which are different. Basically, if there is word 'local/my/hometown' in the user
-        // input and there is no city in the current sentence - this is a request for the weather at user's
-        // current location, i.e. we should implicitly assume user's location and clear conversion context.
-        // In all other cases - we take location from either current sentence or conversation STM.
-
-        // NOTE: we don't do this separation on intent level as it is easier to do it here instead of
-        // creating more intents with almost identical callbacks.
-
-        @SuppressWarnings("SuspiciousMethodCalls")
-        boolean hasLocalWord =
-            ctx.getVariant().getEntities().stream().anyMatch(t -> LOCAL_WORDS.contains(t.mkText().toLowerCase()));
-
-        if (hasLocalWord)
-            // Because we implicitly assume user's current city at this point we need to clear
-            // 'nlpcraft:city' tokens from conversation since they would no longer be valid.
-            ctx.getContext().getConversation().clearStm(t -> t.getId().equals("nlpcraft:city"));
-
-        // Try current user location.
-        GeoData geo = geoOpt.get();
-
-        return new CityData(geo.getTimezoneName(), geo.getLatitude(), geo.getLongitude());
-    }
-
-    /**
-     * A callback for the intent match.
-     *
-     * @param ctx Intent match context.
-     * @param indEntsOpt List of optional indicator elements.
-     * @param cityEntOpt Optional GEO token for city.
-     * @param dateEntOpt Optional date token.
-     * @return Callback result.
-     */
-    @NCIntent(
-        "intent=req " +
-        "term~{# == 'wt:phen'}* " + // Zero or more weather phenomenon.
-        "term(ind)~{" +
-            "@isIndicator = has(ent_groups, 'indicator') " + // Just to demo term variable usage.
-            "@isIndicator" +
-        "}* " + // Optional indicator words (zero or more).
-        "term(city)~{# == 'nlpcraft:city'}? " + // Optional city.
-        "term(date)~{# == 'nlpcraft:date'}?" // Optional date (overrides indicator words).
-    )
-    // NOTE: each samples group will reset conversation STM during auto-testing.
-    @NCIntentSample({
-        "Current forecast?",
-        "Chance of rain in Berlin now?"
-    })
-    // NOTE: each samples group will reset conversation STM during auto-testing.
-    @NCIntentSample({
-        "Moscow forecast?",
-        "Chicago history"
-    })
-    // NOTE: each samples group will reset conversation STM during auto-testing.
-    @NCIntentSample({
-        "What's the local weather forecast?",
-        "What's the weather in Moscow?",
-        "What's the current forecast for LA?",
-        "What is the weather like outside?",
-        "How's the weather?",
-        "What's the weather forecast for the rest of the week?",
-        "What's the weather forecast this week?",
-        "What's the weather out there?",
-        "Is it cold outside?",
-        "Is it hot outside?",
-        "Will it rain today?",
-        "When it will rain in Delhi?",
-        "Is there any possibility of rain in Delhi?",
-        "Is it raining now?",
-        "Is there any chance of rain today?",
-        "Was it raining in Beirut three days ago?",
-        "How about yesterday?"
-    })
-    public NCResult onMatch(
-        NCIntentMatch ctx,
-        @NCIntentTerm("ind") List<NCEntity> indEntsOpt,
-        @NCIntentTerm("city") Optional<NCEntity> cityEntOpt,
-        @NCIntentTerm("date") Optional<NCEntity> dateEntOpt
-    ) {
-        try {
-            Instant now = Instant.now();
-
-            Instant from = now;
-            Instant to = now;
-
-            if (indEntsOpt.stream().anyMatch(tok -> tok.getId().equals("wt:hist")))
-                from = from.minus(DAYS_SHIFT_BACK, DAYS);
-            else if (indEntsOpt.stream().anyMatch(tok -> tok.getId().equals("wt:fcast")))
-                to = from.plus(DAYS_SHIFT_FORWARD, DAYS);
-
-            if (dateEntOpt.isPresent()) { // Date token overrides any indicators.
-                NCEntity dateEnt = dateEntOpt.get();
-
-                // TODO: from NNE ?
-                from = Instant.now();
-                to = Instant.now();
-            }
-
-            CityData cd = prepGeo(ctx, cityEntOpt); // Handles optional city too.
-
-            double lat = cd.getLatitude();
-            double lon = cd.getLongitude();
-
-            NCResult res = new NCResult();
-
-            res.setType(NCResultType.ASK_RESULT);
-            res.setBody(GSON.toJson(from == to ? openWeather.getCurrent(lat, lon) : openWeather.getTimeMachine(lat, lon, from, to)));
-
-            return res;
-        }
-        catch (OpenWeatherMapException e) {
-            throw new NCRejection(e.getLocalizedMessage());
-        }
-        catch (NCRejection e) {
-            throw e;
-        }
-        catch (Exception e) {
-            throw new NCRejection("Weather provider error.", e);
-        }
-    }
-
-    /**
-     *
-     * @param apiKey OpenWeatherMap API key.
-     */
-    public WeatherModel(String apiKey) {
-        Properties props = new Properties();
-        props.setProperty("annotators", "tokenize, ssplit, pos, lemma, ner");
-        StanfordCoreNLP stanford = new StanfordCoreNLP(props);
-
-        NCStanfordNLPTokenParser tp = new NCStanfordNLPTokenParser(stanford);
-
-        this.cfg = new NCModelConfig("nlpcraft.weather.ex", "Weather Example Model", "1.0");
-        this.pipeline = new NCModelPipelineBuilder(
-            tp,
-            new NCStanfordNLPEntityParser(stanford, "date", "city"),
-            new NCSemanticEntityParser(new NCEnSemanticPorterStemmer(), tp, "weather_model.json")
-        ).build();
-
-        openWeather = new OpenWeatherMapService(apiKey, 5, 7);
-    }
-
-    /**
-     *
-     */
-    public void close() {
-        openWeather.stop();
-    }
-
-    @Override
-    public NCModelConfig getConfig() {
-        return cfg;
-    }
-
-    @Override
-    public NCModelPipeline getPipeline() {
-        return pipeline;
-    }
-}
diff --git a/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/openweathermap/OpenWeatherMapService.java b/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/openweathermap/OpenWeatherMapService.java
deleted file mode 100644
index 2ae8a65..0000000
--- a/nlpcraft-examples/weather/src/main/java/org/apache/nlpcraft/examples/weather/openweathermap/OpenWeatherMapService.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.nlpcraft.examples.weather.openweathermap;
-
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.reflect.Type;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-import static java.time.temporal.ChronoUnit.DAYS;
-import static java.time.temporal.ChronoUnit.SECONDS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
-/**
- * OpenWeather API weather provider. See https://openweathermap.org/api for details.
- */
-public class OpenWeatherMapService {
-    // GSON response type.
-    private static final Type TYPE_RESP = new TypeToken<HashMap<String, Object>>() { }.getType();
-
-    // Access key.
-    private final String key;
-
-    // Maximum days (looking backwards) in seconds.
-    private final int maxDaysBackSecs;
-
-    // Maximum days (looking forwards) in seconds.
-    private final int maxDaysForwardSecs;
-
-    // HTTP client instance.
-    private final HttpClient httpClient;
-
-    // GSON instance.
-    private static final Gson GSON = new Gson();
-
-    // Can be configured.
-    private final ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
-    /**
-     *
-     */
-    private static final Logger log = LoggerFactory.getLogger(OpenWeatherMapService.class);
-
-    /**
-     * Constructor.
-     *
-     * @param key Service key.
-     * @param maxDaysBack Max days (looking back) configuration value.
-     * @param maxDaysForward Max days (looking forward) configuration value.
-     */
-    public OpenWeatherMapService(String key, int maxDaysBack, int maxDaysForward) {
-        this.key = key;
-        this.maxDaysBackSecs = maxDaysBack * 24 * 60 * 60;
-        this.maxDaysForwardSecs = maxDaysForward * 24 * 60 * 60;
-        this.httpClient = HttpClient.newBuilder().build();
-    }
-
-    /**
-     * Stop method.
-     */
-    public void stop() {
-        pool.shutdown();
-
-        try {
-            //noinspection ResultOfMethodCallIgnored
-            pool.awaitTermination(Long.MAX_VALUE, MILLISECONDS);
-        }
-        catch (InterruptedException e) {
-            log.error("Error stopping pool.", e);
-        }
-    }
-
-    /**
-     * @param lat Latitude.
-     * @param lon Longitude.
-     * @param d Date.
-     * @return REST call result.
-     */
-    private Map<String, Object> get(double lat, double lon, long d) {
-        return get("https://api.openweathermap.org/data/2.5/onecall?" +
-            "lat=" + lat +
-            "&lon=" + lon +
-            "&dt=" + d +
-            "&exclude=current,minutely,hourly,daily,alerts&appid=" + key
-        );
-    }
-
-    /**
-     * @param url REST endpoint URL.
-     * @return REST call result.
-     */
-    private Map<String, Object> get(String url) {
-        // Ack.
-        System.out.println("REST URL prepared: " + url);
-
-        HttpRequest req = HttpRequest.newBuilder().GET().uri(URI.create(url)).build();
-
-        try {
-            HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
-
-            return GSON.fromJson(resp.body(), TYPE_RESP);
-        }
-        catch (Exception e) {
-            e.printStackTrace(System.err);
-
-            throw new OpenWeatherMapException("Unable to answer due to weather data provider error.");
-        }
-    }
-
-    /**
-     * See https://openweathermap.org/api/one-call-api#hist_parameter to extract fields.
-     *
-     * @param lat Latitude.
-     * @param lon Longitude.
-     * @param from From date.
-     * @param to  To date.
-     * @return List of REST call results.
-     * @throws OpenWeatherMapException Thrown in case of any provider errors.
-     */
-    public List<Map<String, Object>> getTimeMachine(double lat, double lon, Instant from, Instant to) throws OpenWeatherMapException {
-        assert from != null;
-        assert to != null;
-
-        log.debug("OpenWeather time machine API call [lat={}, lon={}, from={}, to={}]", lat, lon, from, to);
-
-        Instant now = Instant.now();
-        long forwardSeconds = to.getEpochSecond() - now.getEpochSecond();
-        long backSeconds = now.getEpochSecond() - from.getEpochSecond();
-
-        if (Duration.between(from, to).get(SECONDS) > maxDaysForwardSecs && forwardSeconds > 0)
-            throw new OpenWeatherMapException(String.format("Forward Request period is too long [from=%s, to=%s]", from, to));
-
-        if (Duration.between(from, to).get(SECONDS) > maxDaysBackSecs && backSeconds > 0 && to.getEpochSecond() <= now.getEpochSecond())
-            throw new OpenWeatherMapException(String.format("Backward Request period is too long [from=%s, to=%s]", from, to));
-
-        long durMs = to.toEpochMilli() - from.toEpochMilli();
-
-        int n = (int) (durMs / 86400000 + (durMs % 86400000 == 0 ? 0 : 1));
-
-        class Pair {
-            private final int shift;
-            private final Map<String, Object> reguest;
-
-            Pair(int left, Map<String, Object> reguest) {
-                this.shift = left;
-                this.reguest = reguest;
-            }
-
-            int getShift() {
-                return this.shift;
-            }
-            Map<String, Object> getReguest() {
-                return this.reguest;
-            }
-        }
-
-        return IntStream.range(0, n).
-            mapToObj(shift -> pool.submit(() -> new Pair(shift, get(lat, lon, from.plus(shift, DAYS).getEpochSecond())))).
-            map(p -> {
-                try {
-                    return p.get();
-                }
-                catch (ExecutionException | InterruptedException e) {
-                    throw new OpenWeatherMapException("Error executing weather request.", e);
-                }
-            }).
-            sorted(Comparator.comparing(Pair::getShift)).
-            map(Pair::getReguest).
-            collect(Collectors.toList());
-    }
-
-    /**
-     * See https://openweathermap.org/api/one-call-api#hist_parameter to extract fields.
-     *
-     * @param lat Latitude.
-     * @param lon Longitude.
-     * @return REST call result.
-     * @throws OpenWeatherMapException Thrown in case of any provider errors.
-     */
-    public Map<String, Object> getCurrent(double lat, double lon) throws OpenWeatherMapException {
-        return get("https://api.openweathermap.org/data/2.5/forecast?lat=" + lat + "&lon=" + lon + "&appid=" + key);
-    }
-}
\ No newline at end of file
diff --git a/nlpcraft-examples/weather/src/main/resources/weather_model.json b/nlpcraft-examples/weather/src/main/resources/weather_model.json
deleted file mode 100644
index e1059b3..0000000
--- a/nlpcraft-examples/weather/src/main/resources/weather_model.json
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.
- */
-
-{
-  "elements": [
-    {
-      "id": "wt:phen",
-      "description": "Weather phenomenon.",
-      "synonyms": [
-        "{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}",
-        "{weather {condition|temp|temperature|data|_}|condition|temp|temperature}"
-      ]
-    },
-    {
-      "id": "wt:hist",
-      "description": "History (past) indicator.",
-      "groups": [
-        "indicator"
-      ],
-      "synonyms": [
-        "{history|past|previous}"
-      ]
-    },
-    {
-      "id": "wt:curr",
-      "description": "Current indicator.",
-      "groups": [
-        "indicator"
-      ],
-      "synonyms": [
-        "{current|today|now|right now}"
-      ]
-    },
-    {
-      "id": "wt:fcast",
-      "description": "Forecast (future) indicator.",
-      "groups": [
-        "indicator"
-      ],
-      "synonyms": [
-        "{future|forecast|prognosis|prediction}"
-      ]
-    }
-  ]
-}
\ No newline at end of file
diff --git a/nlpcraft-examples/weather/src/test/java/org/apache/nlpcraft/examples/weather/NCModelValidationSpec.scala b/nlpcraft-examples/weather/src/test/java/org/apache/nlpcraft/examples/weather/NCModelValidationSpec.scala
deleted file mode 100644
index 4febb07..0000000
--- a/nlpcraft-examples/weather/src/test/java/org/apache/nlpcraft/examples/weather/NCModelValidationSpec.scala
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.
- */
-
-package org.apache.nlpcraft.examples.weather
-
-import org.apache.nlpcraft.NCModelClient
-import org.junit.jupiter.api.*
-
-import scala.util.Using
-
-/**
-  * JUnit model validation.
-  */
-class NCModelValidationSpec:
-    private final val propName = "OWM_API_KEY"
-
-    private var mdl: WeatherModel = _
-
-    @BeforeEach
-    def setUp(): Unit =
-        // Set your own API key here.
-        var apiKey: String = System.getProperty(propName)
-        if apiKey == null then apiKey = System.getenv(propName)
-        // Default value, used for tests.
-        if apiKey == null then apiKey = "8a51a2eb343bf87dc55ffd352f5641ea"
-        mdl = new WeatherModel(apiKey)
-
-    @AfterEach
-    def tearDown(): Unit = if mdl != null then mdl.close()
-
-    @Test
-    def test(): Unit = Using.resource(new NCModelClient(mdl)) { client => client.validateSamples() }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelPipelineManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelPipelineManager.scala
index e1f56cb..afcd63a 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelPipelineManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelPipelineManager.scala
@@ -86,6 +86,15 @@ class NCModelPipelineManager(cfg: NCModelConfig, pipeline: NCModelPipeline) exte
 
     /**
       *
+      * @param m
+      * @return
+      */
+    private def mkProps(m: NCPropertyMap): String =
+        if m.keysSet().isEmpty then ""
+        else m.keysSet().asScala.toSeq.sorted.map(p => s"$p=${m.get[Any](p)}").mkString("{", ", ", "}")
+
+    /**
+      *
       * @param txt
       * @param data
       * @param usrId
@@ -119,6 +128,19 @@ class NCModelPipelineManager(cfg: NCModelConfig, pipeline: NCModelPipeline) exte
                 check()
                 e.enrich(req, cfg, toks)
 
+        val tbl = NCAsciiTable("Text", "Lemma", "POS", "Start index", "End index", "Properties")
+
+        for (t <- toks.asScala)
+            tbl += (
+                t.getText,
+                t.getLemma,
+                t.getPos,
+                t.getStartCharIndex,
+                t.getEndCharIndex,
+                mkProps(t)
+            )
+        tbl.info(logger, Option(s"Tokens for: ${req.getText}"))
+
         // NOTE: we run validators regardless of whether token list is empty.
         for (v <- tokVals)
             check()
@@ -169,10 +191,6 @@ class NCModelPipelineManager(cfg: NCModelConfig, pipeline: NCModelPipeline) exte
         for ((v, i) <- vrnts.zipWithIndex)
             val tbl = NCAsciiTable("EntityId", "Tokens", "Tokens Position", "Properties")
 
-            def mkProps(m: NCPropertyMap): String =
-                if m.keysSet().isEmpty then ""
-                else m.keysSet().asScala.toSeq.sorted.map(p => s"$p=${m.get[Any](p)}").mkString("{", ", ", "}")
-
             for (e <- v.getEntities.asScala)
                 val toks = e.getTokens.asScala
                 tbl += (
diff --git a/pom.xml b/pom.xml
index fc180eb..867af16 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,8 +11,7 @@
       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.
+ 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.
 -->
@@ -389,10 +388,9 @@
         <profile>
             <id>examples</id>
             <modules>
-                <module>nlpcraft-examples/utils</module>
-                <module>nlpcraft-examples/lightswitch</module>
                 <module>nlpcraft-examples/time</module>
-                <module>nlpcraft-examples/weather</module>
+                <module>nlpcraft-examples/lightswitch</module>
+                <module>nlpcraft-examples/lightswitch-ru</module>
             </modules>
         </profile>
     </profiles>