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:28 UTC

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

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>