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/03/14 21:54:03 UTC

[incubator-nlpcraft] branch master updated: FR examples added (initial version)

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 dd8f232  FR examples added (initial version)
dd8f232 is described below

commit dd8f232d2f226ec73f159d72df5b62169e24dd9b
Author: Sergey Kamov <sk...@gmail.com>
AuthorDate: Tue Mar 15 00:52:36 2022 +0300

    FR examples added (initial version)
---
 nlpcraft-examples/lightswitch-fr/README.md         | 50 ++++++++++++++
 nlpcraft-examples/lightswitch-fr/pom.xml           | 80 ++++++++++++++++++++++
 .../examples/lightswitch/LightSwitchFrModel.scala} | 52 +++++++-------
 .../entity/parser/NCFrSemanticEntityParser.scala   | 35 ++++++++++
 .../enricher/NCFrLemmaPosTokenEnricher.scala}      |  7 +-
 .../enricher/NCFrStopWordsTokenEnricher.scala      | 48 +++++++++++++
 .../nlp/token/parser/NCFrTokenParser.scala}        | 26 +++----
 .../src/main/resources/lightswitch_model_fr.yaml   | 45 ++++++++++++
 .../lightswitch/NCModelValidationSpec.scala        | 30 ++++++++
 .../examples/lightswitch/LightSwitchRuModel.scala  | 10 +--
 .../token/enricher/NCRuLemmaPosTokenEnricher.scala |  1 -
 .../nlp/token/parser/NCRuTokenParser.scala         | 27 ++++----
 .../nlpcraft/internal/impl/NCModelClientImpl.scala |  2 -
 pom.xml                                            |  1 +
 14 files changed, 351 insertions(+), 63 deletions(-)

diff --git a/nlpcraft-examples/lightswitch-fr/README.md b/nlpcraft-examples/lightswitch-fr/README.md
new file mode 100644
index 0000000..f073eff
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-fr/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-fr/pom.xml b/nlpcraft-examples/lightswitch-fr/pom.xml
new file mode 100644
index 0000000..d596ea2
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-fr/pom.xml
@@ -0,0 +1,80 @@
+<?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 FR</name>
+    <artifactId>nlpcraft-example-lightswitch-fr</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-fr</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>
+    </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/LightSwitchRuModel.scala b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchFrModel.scala
similarity index 62%
copy from nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchRuModel.scala
copy to nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchFrModel.scala
index 5350aee..fb7734f 100644
--- a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchRuModel.scala
+++ b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchFrModel.scala
@@ -17,10 +17,11 @@
 
 package org.apache.nlpcraft.examples.lightswitch
 
+import com.google.gson.Gson
 import org.apache.nlpcraft.*
-import org.apache.nlpcraft.examples.lightswitch.nlp.entity.parser.NCRuSemanticEntityParser
-import org.apache.nlpcraft.examples.lightswitch.nlp.token.enricher.{NCRuLemmaPosTokenEnricher, NCRuStopWordsTokenEnricher}
-import org.apache.nlpcraft.examples.lightswitch.nlp.token.parser.NCRuTokenParser
+import org.apache.nlpcraft.examples.lightswitch.nlp.entity.parser.NCFrSemanticEntityParser
+import org.apache.nlpcraft.examples.lightswitch.nlp.token.enricher.{NCFrLemmaPosTokenEnricher, NCFrStopWordsTokenEnricher}
+import org.apache.nlpcraft.examples.lightswitch.nlp.token.parser.NCFrTokenParser
 import org.apache.nlpcraft.nlp.entity.parser.{NCNLPEntityParser, NCSemanticEntityParser}
 import org.apache.nlpcraft.nlp.token.enricher.NCENStopWordsTokenEnricher
 import org.apache.nlpcraft.nlp.token.parser.NCOpenNLPTokenParser
@@ -40,13 +41,13 @@ import scala.jdk.CollectionConverters.*
   * <p>
   * See 'README.md' file in the same folder for running and testing instructions.
   */
-class LightSwitchRuModel extends NCModelAdapter(
-    new NCModelConfig("nlpcraft.lightswitch.ru.ex", "LightSwitch Example Model RU", "1.0"),
+class LightSwitchFrModel extends NCModelAdapter(
+    new NCModelConfig("nlpcraft.lightswitch.fr.ex", "LightSwitch Example Model FR", "1.0"),
     new NCModelPipelineBuilder().
-        withTokenParser(new NCRuTokenParser()).
-        withTokenEnricher(new NCRuLemmaPosTokenEnricher()).
-        withTokenEnricher(new NCRuStopWordsTokenEnricher()).
-        withEntityParser(new NCRuSemanticEntityParser("lightswitch_model_ru.yaml")).
+        withTokenParser(new NCFrTokenParser()).
+        withTokenEnricher(new NCFrLemmaPosTokenEnricher()).
+        withTokenEnricher(new NCFrStopWordsTokenEnricher()).
+        withEntityParser(new NCFrSemanticEntityParser("lightswitch_model_fr.yaml")).
         build()
 ):
     /**
@@ -58,31 +59,32 @@ class LightSwitchRuModel extends NCModelAdapter(
       */
     @NCIntent("intent=ls term(act)={has(ent_groups, 'act')} term(loc)={# == 'ls:loc'}*")
     @NCIntentSample(Array(
-        "Выключи свет по всем доме",
-        "Выруби электричество!",
-        "Включи свет в детской",
-        "Включай повсюду освещение",
-        "Включайте лампы в детской комнате",
-        "Свет на кухне, пожалуйста, приглуши",
-        "Нельзя ли повсюду выключить свет?",
-        "Пожалуйста без света",
-        "Отключи электричество в ванной",
-        "Выключи, пожалуйста, тут всюду свет",
-        "Выключай все!",
-        "Свет пожалуйста везде включи",
-        "Зажги лампу на кухне"
+        "Éteins l“allumier dans toute la maison",
+        "Coupez l'électricité!",
+        "Allume la lumière dans la chambre des enfants",
+        "Allume l'éclairage partout",
+        "Allumer les lampes dans la chambre des enfants",
+        "Éteins la lumière dans la cuisine, s'il te plaît",
+        "Est-il possible d'éteindre les lumières partout?",
+        "S'il vous plaît sans lumière",
+        "Couper l'électricité dans la salle de bain",
+        "Éteins la lumière, s'il te plaît.",
+        "Éteignez tout!",
+        "Allume la lumière partout s'il te plaît ",
+        "Allume la lampe dans la cuisine"
     ))
     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(", ")
+        val action = if actEnt.getId == "ls:on" then "allumer" else "éteindre"
+        val locations = if locEnts.isEmpty then "toute la maison" else locEnts.map(_.mkText()).mkString(", ")
 
         // Add HomeKit, Arduino or other integration here.
 
         // By default - just return a descriptive action string.
+
         new NCResult(
-            s"Lights are [$status] in [${locations.toLowerCase}].",
+            new Gson().toJson(Map("locations" -> locations, "action" -> action).asJava),
             NCResultType.ASK_RESULT
         )
\ No newline at end of file
diff --git a/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/entity/parser/NCFrSemanticEntityParser.scala b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/entity/parser/NCFrSemanticEntityParser.scala
new file mode 100644
index 0000000..6a70a75
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/entity/parser/NCFrSemanticEntityParser.scala
@@ -0,0 +1,35 @@
+/*
+ * 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.nlp.entity.parser
+
+import opennlp.tools.stemmer.snowball.SnowballStemmer
+import org.apache.nlpcraft.examples.lightswitch.nlp.token.parser.NCFrTokenParser
+import org.apache.nlpcraft.nlp.entity.parser.*
+
+/**
+  *
+  * @param src
+  */
+class NCFrSemanticEntityParser(src: String) extends NCSemanticEntityParser(
+    new NCSemanticStemmer:
+        private val stemmer = new SnowballStemmer(SnowballStemmer.ALGORITHM.FRENCH)
+        override def stem(txt: String): String = stemmer.synchronized { stemmer.stem(txt.toLowerCase).toString }
+    ,
+    new NCFrTokenParser(),
+    src
+)
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCRuLemmaPosTokenEnricher.scala b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCFrLemmaPosTokenEnricher.scala
similarity index 88%
copy from nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCRuLemmaPosTokenEnricher.scala
copy to nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCFrLemmaPosTokenEnricher.scala
index 1e6d15c..92db11f 100644
--- a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCRuLemmaPosTokenEnricher.scala
+++ b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCFrLemmaPosTokenEnricher.scala
@@ -17,10 +17,9 @@
 
 package org.apache.nlpcraft.examples.lightswitch.nlp.token.enricher
 
-import org.apache.lucene.analysis.ru.RussianAnalyzer
 import org.apache.nlpcraft.*
 import org.languagetool.AnalyzedToken
-import org.languagetool.tagging.ru.RussianTagger
+import org.languagetool.tagging.fr.FrenchTagger
 
 import java.util
 import java.util.stream.Collectors
@@ -29,12 +28,12 @@ import scala.jdk.CollectionConverters.*
 /**
   *
   */
-class NCRuLemmaPosTokenEnricher extends NCTokenEnricher:
+class NCFrLemmaPosTokenEnricher extends NCTokenEnricher:
     private def nvl(v: String, dflt : => String): String = if v != null then v else dflt
 
     override def enrich(req: NCRequest, cfg: NCModelConfig, toksList: util.List[NCToken]): Unit =
         val toks = toksList.asScala
-        val tags = RussianTagger.INSTANCE.tag(toks.map(_.getText).asJava).asScala
+        val tags = FrenchTagger.INSTANCE.tag(toks.map(_.getText).asJava).asScala
 
         require(toks.size == tags.size)
 
diff --git a/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCFrStopWordsTokenEnricher.scala b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCFrStopWordsTokenEnricher.scala
new file mode 100644
index 0000000..335d00f
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCFrStopWordsTokenEnricher.scala
@@ -0,0 +1,48 @@
+/*
+ * 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.nlp.token.enricher
+
+import org.apache.lucene.analysis.fr.FrenchAnalyzer
+import org.apache.nlpcraft.*
+
+import java.util
+import scala.jdk.CollectionConverters.*
+
+/**
+  *
+  */
+class NCFrStopWordsTokenEnricher extends NCTokenEnricher:
+    private final val stops = FrenchAnalyzer.getDefaultStopSet
+
+    private def getPos(t: NCToken): String = t.getOpt("pos").orElseThrow(() => throw new NCException("POS not found in token."))
+    private def getLemma(t: NCToken): String = t.getOpt("lemma").orElseThrow(() => throw new NCException("Lemma not found in token."))
+
+    override def enrich(req: NCRequest, cfg: NCModelConfig, toks: util.List[NCToken]): Unit =
+        for (t <- toks.asScala)
+            val lemma = getLemma(t)
+            lazy val pos = getPos(t)
+
+            t.put(
+                "stopword",
+                lemma.length == 1 && !Character.isLetter(lemma.head) && !Character.isDigit(lemma.head) ||
+                stops.contains(lemma.toLowerCase) ||
+                pos.startsWith("I") ||
+                pos.startsWith("O") ||
+                pos.startsWith("P") ||
+                pos.startsWith("D")
+            )
\ No newline at end of file
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCRuTokenParser.scala b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCFrTokenParser.scala
similarity index 66%
copy from nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCRuTokenParser.scala
copy to nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCFrTokenParser.scala
index 8dda52d..45eac3b 100644
--- a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCRuTokenParser.scala
+++ b/nlpcraft-examples/lightswitch-fr/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCFrTokenParser.scala
@@ -17,13 +17,9 @@
 
 package org.apache.nlpcraft.examples.lightswitch.nlp.token.parser
 
-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 org.languagetool.tokenizers.fr.FrenchWordTokenizer
 
 import java.util
 import scala.jdk.CollectionConverters.*
@@ -31,20 +27,24 @@ import scala.jdk.CollectionConverters.*
 /**
   *
   */
-class NCRuTokenParser extends NCTokenParser:
-    private val tokenizer = new WordTokenizer
+class NCFrTokenParser extends NCTokenParser:
+    private val tokenizer = new FrenchWordTokenizer
 
     override def tokenize(text: String): util.List[NCToken] =
         val toks = collection.mutable.ArrayBuffer.empty[NCToken]
         var sumLen = 0
 
         for (((word, len), idx) <- tokenizer.tokenize(text).asScala.map(p => p -> p.length).zipWithIndex)
-            if word.strip.nonEmpty then toks += new NCPropertyMapAdapter with NCToken:
-                override def getText: String = word
-                override def getIndex: Int = idx
-                override def getStartCharIndex: Int = sumLen
-                override def getEndCharIndex: Int = sumLen + word.length
+            val start = sumLen
+            val end = sumLen + word.length
 
-            sumLen += word.length
+            if word.strip.nonEmpty then
+                toks += new NCPropertyMapAdapter with NCToken:
+                    override def getText: String = word
+                    override def getIndex: Int = idx
+                    override def getStartCharIndex: Int = start
+                    override def getEndCharIndex: Int = end
+
+            sumLen = end
 
         toks.asJava
diff --git a/nlpcraft-examples/lightswitch-fr/src/main/resources/lightswitch_model_fr.yaml b/nlpcraft-examples/lightswitch-fr/src/main/resources/lightswitch_model_fr.yaml
new file mode 100644
index 0000000..fc7bae9
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-fr/src/main/resources/lightswitch_model_fr.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>" : "{allumer|déclencher|lancer|illuminer}"
+  "<TURN_OFF>" : "{éteindre|couper|désactiver|déconnecter|éliminer|baisser}"
+  "<ENTIRE_OPT>" : "{tout|partout|autour|totalement|_}"
+  "<LIGHT_OPT>" : "{ce|lampe|bras|lustre|luminaire|ampoule|éclairage|lumière|électricité|électricien|_}"
+
+elements:
+  - id: "ls:loc"
+    description: "Location of lights."
+    synonyms:
+      - "<ENTIRE_OPT> {bâtiment|salle|maison|cuisine|chambre des enfants|bureau|salon|chambre à coucher|salle de bain|toilette|grande salle à manger|salle de bain|chambre des enfants}"
+
+  - 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>"
+      - "sans <ENTIRE_OPT> <LIGHT_OPT>"
diff --git a/nlpcraft-examples/lightswitch-fr/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala b/nlpcraft-examples/lightswitch-fr/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala
new file mode 100644
index 0000000..2871ec5
--- /dev/null
+++ b/nlpcraft-examples/lightswitch-fr/src/test/java/org/apache/nlpcraft/examples/lightswitch/NCModelValidationSpec.scala
@@ -0,0 +1,30 @@
+/*
+ * 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:
+    @Test
+    def test(): Unit = Using.resource(new NCModelClient(new LightSwitchFrModel)) { _.validateSamples() }
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchRuModel.scala b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchRuModel.scala
index 5350aee..cd99c2c 100644
--- a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchRuModel.scala
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/LightSwitchRuModel.scala
@@ -17,6 +17,7 @@
 
 package org.apache.nlpcraft.examples.lightswitch
 
+import com.google.gson.Gson
 import org.apache.nlpcraft.*
 import org.apache.nlpcraft.examples.lightswitch.nlp.entity.parser.NCRuSemanticEntityParser
 import org.apache.nlpcraft.examples.lightswitch.nlp.token.enricher.{NCRuLemmaPosTokenEnricher, NCRuStopWordsTokenEnricher}
@@ -76,13 +77,14 @@ class LightSwitchRuModel extends NCModelAdapter(
         @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(", ")
+        val action = if actEnt.getId == "ls:on" then "включить" else "выключить"
+        val locations = if locEnts.isEmpty then "весь дом" else locEnts.map(_.mkText()).mkString(", ")
 
         // Add HomeKit, Arduino or other integration here.
 
         // By default - just return a descriptive action string.
+
         new NCResult(
-            s"Lights are [$status] in [${locations.toLowerCase}].",
-            NCResultType.ASK_RESULT
+           new Gson().toJson(Map("locations" -> locations, "action" -> action).asJava),
+           NCResultType.ASK_RESULT
         )
\ No newline at end of file
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCRuLemmaPosTokenEnricher.scala b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCRuLemmaPosTokenEnricher.scala
index 1e6d15c..2a9bfd3 100644
--- a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCRuLemmaPosTokenEnricher.scala
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/enricher/NCRuLemmaPosTokenEnricher.scala
@@ -17,7 +17,6 @@
 
 package org.apache.nlpcraft.examples.lightswitch.nlp.token.enricher
 
-import org.apache.lucene.analysis.ru.RussianAnalyzer
 import org.apache.nlpcraft.*
 import org.languagetool.AnalyzedToken
 import org.languagetool.tagging.ru.RussianTagger
diff --git a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCRuTokenParser.scala b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCRuTokenParser.scala
index 8dda52d..bf0440e 100644
--- a/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCRuTokenParser.scala
+++ b/nlpcraft-examples/lightswitch-ru/src/main/java/org/apache/nlpcraft/examples/lightswitch/nlp/token/parser/NCRuTokenParser.scala
@@ -17,12 +17,7 @@
 
 package org.apache.nlpcraft.examples.lightswitch.nlp.token.parser
 
-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
@@ -39,12 +34,16 @@ class NCRuTokenParser extends NCTokenParser:
         var sumLen = 0
 
         for (((word, len), idx) <- tokenizer.tokenize(text).asScala.map(p => p -> p.length).zipWithIndex)
-            if word.strip.nonEmpty then toks += new NCPropertyMapAdapter with NCToken:
-                override def getText: String = word
-                override def getIndex: Int = idx
-                override def getStartCharIndex: Int = sumLen
-                override def getEndCharIndex: Int = sumLen + word.length
-
-            sumLen += word.length
-
-        toks.asJava
+            val start = sumLen
+            val end = sumLen + word.length
+            
+            if word.strip.nonEmpty then
+                toks += new NCPropertyMapAdapter with NCToken:
+                    override def getText: String = word
+                    override def getIndex: Int = idx
+                    override def getStartCharIndex: Int = start
+                    override def getEndCharIndex: Int = end
+
+            sumLen = end
+
+        toks.asJava
\ No newline at end of file
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala
index e23b21f..87b8983 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala
@@ -54,8 +54,6 @@ class NCModelClientImpl(mdl: NCModel) extends LazyLogging:
 
     /**
       *
-      * @param cfg
-      * @param pipeline
       */
     private def verify(): Unit =
         Objects.requireNonNull(mdl, "Model cannot be null.")
diff --git a/pom.xml b/pom.xml
index 3362ba3..c64e1d0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -392,6 +392,7 @@
                 <module>nlpcraft-examples/echo</module>
                 <module>nlpcraft-examples/lightswitch</module>
                 <module>nlpcraft-examples/lightswitch-ru</module>
+                <module>nlpcraft-examples/lightswitch-fr</module>
             </modules>
         </profile>
     </profiles>