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/24 14:39:27 UTC

[incubator-nlpcraft] branch NLPCRAFT-479 created (now c357f02)

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

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


      at c357f02  RU Lightswitch simplified example.

This branch includes the following new commits:

     new c357f02  RU Lightswitch simplified example.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[incubator-nlpcraft] 01/01: RU Lightswitch simplified example.

Posted by se...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit c357f02c0a4817596c2f9e36928e5a10f35ff0ca
Author: Sergey Kamov <sk...@gmail.com>
AuthorDate: Thu Feb 24 17:38:12 2022 +0300

    RU Lightswitch simplified example.
---
 nlpcraft-examples/lightswitch-ru/README.md         | 50 ++++++++++++
 nlpcraft-examples/lightswitch-ru/pom.xml           | 94 ++++++++++++++++++++++
 .../examples/lightswitch/LightSwitchModelRu.scala  | 89 ++++++++++++++++++++
 .../lightswitch/ru/NCSemanticStemmerRu.scala       | 26 ++++++
 .../ru/NCStopWordsTokenEnricherRu.scala            | 43 ++++++++++
 .../examples/lightswitch/ru/NCTokenParserRu.scala  | 78 ++++++++++++++++++
 .../src/main/resources/lightswitch_model_ru.yaml   | 45 +++++++++++
 .../lightswitch/NCModelValidationSpec.scala        | 32 ++++++++
 .../internal/impl/NCModelPipelineManager.scala     | 26 +++++-
 pom.xml                                            |  1 +
 10 files changed, 480 insertions(+), 4 deletions(-)

diff --git a/nlpcraft-examples/lightswitch-ru/README.md b/nlpcraft-examples/lightswitch-ru/README.md
new file mode 100644
index 0000000..f073eff
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/README.md
@@ -0,0 +1,50 @@
+<!--
+ 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.
+-->
+
+<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)
+[![Build](https://github.com/apache/incubator-nlpcraft/workflows/build/badge.svg)](https://github.com/apache/incubator-nlpcraft/actions)
+[![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)
+
+### 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 [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:
+
+ * View & run other [examples](https://github.com/apache/incubator-nlpcraft/tree/master/nlpcraft-examples)
+ * Read [documentation](https://nlpcraft.apache.org/docs.html), latest [Javadoc](https://nlpcraft.apache.org/apis/latest/index.html) and [REST APIs](https://nlpcraft.apache.org/using-rest.html)
+ * Download & Maven/Grape/Gradle/SBT [instructions](https://nlpcraft.apache.org/download.html)
+ * File a bug or improvement in [JIRA](https://issues.apache.org/jira/projects/NLPCRAFT)
+ * Post a question at [Stack Overflow](https://stackoverflow.com/questions/ask) using <code>nlpcraft</code> tag
+ * Access [GitHub](https://github.com/apache/incubator-nlpcraft) mirror repository.
+ * Join project developers on [dev@nlpcraft.apache.org](mailto:dev-subscribe@nlpcraft.apache.org)
+
+### Copyright
+Copyright (C) 2021 Apache Software Foundation
+
+<img src="https://www.apache.org/img/ASF20thAnniversary.jpg" height="64px" alt="ASF Logo">
+
+
diff --git a/nlpcraft-examples/lightswitch-ru/pom.xml b/nlpcraft-examples/lightswitch-ru/pom.xml
new file mode 100644
index 0000000..67c610f
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/pom.xml
@@ -0,0 +1,94 @@
+<?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 Example Light Switch RU</name>
+    <artifactId>nlpcraft-example-lightswitch-ru</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>
+
+        <dependency>
+            <groupId>org.apache.lucene</groupId>
+            <artifactId>lucene-analyzers-common</artifactId>
+            <version>8.11.1</version>
+        </dependency>
+
+        <dependency>
+            <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>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>nlpcraft</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <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/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..67af24a
--- /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/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCSemanticStemmerRu.scala b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCSemanticStemmerRu.scala
new file mode 100644
index 0000000..e49c72c
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/ru/NCSemanticStemmerRu.scala
@@ -0,0 +1,26 @@
+/*
+ * 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 opennlp.tools.stemmer.snowball.SnowballStemmer
+import org.apache.nlpcraft.nlp.entity.parser.semantic.NCSemanticStemmer
+
+class NCSemanticStemmerRu extends NCSemanticStemmer:
+    private val stemmer = new SnowballStemmer(SnowballStemmer.ALGORITHM.RUSSIAN)
+
+    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/lightswitch-ru/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala b/nlpcraft-examples/lightswitch-ru/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala
new file mode 100644
index 0000000..b6d9d1b
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-ru/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala
@@ -0,0 +1,32 @@
+/*
+ * 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.junit.jupiter.api.*
+
+import scala.util.Using
+
+/**
+  * JUnit models validation.
+  */
+class NCModelValidationSpec:
+    private val MDL = new LightSwitchModelRu
+
+    @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..49c430d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -393,6 +393,7 @@
                 <module>nlpcraft-examples/lightswitch</module>
                 <module>nlpcraft-examples/time</module>
                 <module>nlpcraft-examples/weather</module>
+                <module>nlpcraft-examples/lightswitch-ru</module>
             </modules>
         </profile>
     </profiles>