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 2021/03/26 21:23:12 UTC

[incubator-nlpcraft] branch NLPCRAFT-278 updated: Minor fixes. Tests added.

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

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


The following commit(s) were added to refs/heads/NLPCRAFT-278 by this push:
     new b5bba5e  Minor fixes. Tests added.
b5bba5e is described below

commit b5bba5ebe8183046b61cd7f2b2ddf2a7f813c3a4
Author: Sergey Kamov <sk...@gmail.com>
AuthorDate: Sat Mar 27 00:23:03 2021 +0300

    Minor fixes. Tests added.
---
 .../nlpcraft/common/config/NCConfigurableJava.java |   4 +-
 .../common/extcfg/NCExternalConfigManager.scala    |   4 +-
 .../nlpcraft/common/makro/NCMacroCompiler.scala    |   4 +-
 .../org/apache/nlpcraft/common/util/NCUtils.scala  |  81 +++++++++---
 .../model/intent/compiler/NCIdlCompiler.scala      |  52 +++++---
 .../model/intent/compiler/NCIdlCompilerBase.scala  |   8 +-
 .../model/intent/solver/NCIntentSolverEngine.scala |  38 +++---
 .../probe/mgrs/conn/NCConnectionManager.scala      |  16 +--
 .../server/geo/tools/NCGeoNamesGenerator.scala     |   2 +-
 .../geo/tools/NCGeoStateNamesGenerator.scala       |   2 +-
 .../geo/tools/metro/NCGeoMetroGenerator.scala      |   2 +-
 .../geo/tools/unstats/NCUnsdStatsService.scala     |   2 +-
 .../org/apache/nlpcraft/server/json/NCJson.scala   |   4 +-
 .../nlpcraft/model/dialog/NCDialogSpec.scala       |  71 +++++++++-
 .../nlpcraft/model/dialog/NCDialogSpec2.scala      | 147 +++++++++++++++++++++
 .../idl/compiler/functions/NCIdlFunctions.scala    |  84 +++++++++---
 .../compiler/functions/NCIdlFunctionsCompany.scala |   1 -
 .../compiler/functions/NCIdlFunctionsCustom.scala  |  86 ++++++++++++
 .../compiler/functions/NCIdlFunctionsMath.scala    |  18 +--
 .../compiler/functions/NCIdlFunctionsOther.scala   |  15 ++-
 .../compiler/functions/NCIdlFunctionsRequest.scala |   2 +-
 .../compiler/functions/NCIdlFunctionsStat.scala    |  15 ++-
 .../compiler/functions/NCIdlFunctionsStrings.scala |  15 ++-
 .../compiler/functions/NCIdlFunctionsToken.scala   |   4 +-
 ...nsStat.scala => NCIdlFunctionsTokensUsed.scala} |  48 ++++---
 .../nlpcraft/model/intent/idl/compiler/test_ok.idl |   4 +-
 26 files changed, 564 insertions(+), 165 deletions(-)

diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/config/NCConfigurableJava.java b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/config/NCConfigurableJava.java
index 320eb12..e7cfd45 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/config/NCConfigurableJava.java
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/config/NCConfigurableJava.java
@@ -62,7 +62,7 @@ public class NCConfigurableJava {
             abortWith(String.format("Invalid 'host:port' endpoint format: %s", ep));
 
         try {
-            return new Pair<String, Integer>(ep.substring(0, i), Integer.parseInt(ep.substring(i + 1)));
+            return new Pair<>(ep.substring(0, i), Integer.parseInt(ep.substring(i + 1)));
         }
         catch (NumberFormatException e) {
             abortWith(String.format("Invalid 'host:port' endpoint port: %s", ep));
@@ -80,7 +80,7 @@ public class NCConfigurableJava {
      * @return Pair of host and port from given configuration property or default values.
      */
     public Pair<String, Integer> getHostPortOrElse(String name, String dfltHost, int dfltPort) {
-        return cfg.hasPath(name) ? getHostPort(name) :  new Pair<String, Integer>(dfltHost, dfltPort);
+        return cfg.hasPath(name) ? getHostPort(name) : new Pair<>(dfltHost, dfltPort);
     }
 
     /**
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/extcfg/NCExternalConfigManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/extcfg/NCExternalConfigManager.scala
index c041f7c..d6e0b91 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/extcfg/NCExternalConfigManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/extcfg/NCExternalConfigManager.scala
@@ -231,7 +231,7 @@ object NCExternalConfigManager extends NCService {
     @throws[NCE]
     def getContent(typ: NCExternalConfigType, res: String, parent: Span = null): String =
         startScopedSpan("getContent", parent, "res" → res) { _ ⇒
-            mkString(U.readFile(mkFile(typ, res), "UTF-8"))
+            mkString(U.readFile(mkFile(typ, res)))
         }
 
     /**
@@ -269,7 +269,7 @@ object NCExternalConfigManager extends NCService {
                 d.listFiles(new FileFilter { override def accept(f: File): Boolean = f.isFile && resFilter(f.getName) })
 
             if (files != null)
-                files.toStream.map(f ⇒ NCExternalConfigHolder(typ, f.getName, mkString(U.readFile(f, "UTF-8"))))
+                files.toStream.map(f ⇒ NCExternalConfigHolder(typ, f.getName, mkString(U.readFile(f))))
             else
                 Stream.empty
         }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/makro/NCMacroCompiler.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/makro/NCMacroCompiler.scala
index 16ff67f..561fe23 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/makro/NCMacroCompiler.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/makro/NCMacroCompiler.scala
@@ -87,12 +87,12 @@ object NCMacroCompiler extends LazyLogging {
             // Add harmless empty string.
             buf += ""
 
-            stack.push(StackItem(buf, false))
+            stack.push(StackItem(buf, isGroup = false))
         }
 
         override def enterGroup(ctx: P.GroupContext): Unit = {
             // NOTE: group cannot be empty based on the BNF grammar.
-            stack.push(StackItem(mutable.Buffer.empty[String], true))
+            stack.push(StackItem(mutable.Buffer.empty[String], isGroup = true))
         }
 
         override def exitExpr(ctx: NCMacroDslParser.ExprContext): Unit = {
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala
index df50ab1..42023ae 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/common/util/NCUtils.scala
@@ -860,7 +860,7 @@ object NCUtils extends LazyLogging {
      * @param path File path.
      */
     @throws[NCE]
-    def serializePath(path: String, obj: Any): Unit = {
+    def serializePath(path: String, obj: Any): Unit =
         try {
             manageOutput(new FileOutputStream(path)) acquireAndGet { out ⇒
                 out.writeObject(obj)
@@ -871,7 +871,6 @@ object NCUtils extends LazyLogging {
         catch {
             case e: IOException ⇒ throw new NCE(s"Error writing file: $path", e)
         }
-    }
 
     /**
       * Serializes data from file.
@@ -1945,35 +1944,74 @@ object NCUtils extends LazyLogging {
     }
     
     /**
-      *
+      * Calling a method with one parameter and a non-null return value.
+     *
       * @param objFac
       * @param mtdName
       * @param arg
       * @tparam T
       * @return
       */
-    def callMethod[T: ClassTag, R](objFac: () ⇒ Object, mtdName: String, arg: T): R = {
-        val obj = objFac()
-        val mtd = obj.getClass.getMethod(mtdName, classTag[T].runtimeClass)
-    
-        var flag = mtd.canAccess(obj)
-    
+    @throws[NCE]
+    def callMethod[T: ClassTag, R: ClassTag](objFac: () ⇒ Any, mtdName: String, arg: T): R =
         try {
-            if (!flag) {
-                mtd.setAccessible(true)
-            
-                flag = true
+            val obj: Any = objFac()
+
+            if (obj == null)
+                throw new NCE(s"Invalid 'null' object created attempting to call method: $mtdName")
+
+            val argCls = classTag[T].runtimeClass
+            val retCls = classTag[R].runtimeClass
+
+            def mkErrors = s"[" +
+                s"name=${obj.getClass.getName}#$mtdName(...), " +
+                s"argType=${argCls.getCanonicalName}, " +
+                s"retType=${retCls.getCanonicalName}" +
+            s"]"
+
+            val mtd =
+                try
+                    obj.getClass.getMethod(mtdName, argCls)
+                catch {
+                    case e: NoSuchMethodException ⇒ throw new NCE(s"Method not found $mkErrors", e)
+                }
+
+            var flag = mtd.canAccess(obj)
+
+            try {
+                if (!flag) {
+                    mtd.setAccessible(true)
+
+                    flag = true
+                }
+                else
+                    flag = false
+
+                val res =
+                    try
+                        mtd.invoke(obj, arg.asInstanceOf[Object])
+                    catch {
+                        case e: Throwable ⇒ throw new NCE(s"Failed to execute method $mkErrors", e)
+                    }
+
+                if (res == null)
+                    throw new NCE(s"Unexpected 'null' result for method execution $mkErrors")
+
+                try
+                    res.asInstanceOf[R]
+                catch {
+                    case e: ClassCastException ⇒ throw new NCE(s"Invalid method result type $mkErrors", e)
+                }
+            }
+            finally {
+                if (flag)
+                    mtd.setAccessible(false)
             }
-            else
-                flag = false
-        
-            mtd.invoke(obj, arg.asInstanceOf[Object]).asInstanceOf[R]
         }
-        finally {
-            if (flag)
-                mtd.setAccessible(false)
+        catch {
+            case e: NCE ⇒ throw e
+            case e: Throwable ⇒ throw new NCE(s"Unexpected error calling method: $mtdName(...)", e)
         }
-    }
 
     /**
       *
@@ -1981,6 +2019,7 @@ object NCUtils extends LazyLogging {
       * @tparam T Type of the object to create.
       * @return New instance of the specified type.
       */
+    @throws[NCE]
     def mkObject[T](clsName: String): T = {
         try
             // Try Java reflection first.
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompiler.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompiler.scala
index 7e24d46..3e9f737 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompiler.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompiler.scala
@@ -77,9 +77,11 @@ object NCIdlCompiler extends LazyLogging {
         private var min = 1
         private var max = 1
 
-        // Current method reference.
-        private var refClsName: Option[String] = None
-        private var refMtdName: Option[String] = None
+        // Class & method reference.
+        private var clsName: Option[String] = None
+        private var mtdName: Option[String] = None
+        private var flowClsName: Option[String] = None
+        private var flowMtdName: Option[String] = None
 
         // List of instructions for the current expression.
         private var expr = mutable.Buffer.empty[SI]
@@ -185,10 +187,8 @@ object NCIdlCompiler extends LazyLogging {
         }
 
         override def exitMtdRef(ctx: IDP.MtdRefContext): Unit = {
-            if (ctx.javaFqn() != null)
-                refClsName = Some(ctx.javaFqn().getText)
-
-            refMtdName = Some(ctx.id().getText)
+            clsName = if (ctx.javaFqn() != null) Some(ctx.javaFqn().getText) else None
+            mtdName = Some(ctx.id().getText)
         }
 
         override def exitTermId(ctx: IDP.TermIdContext): Unit = {
@@ -256,6 +256,9 @@ object NCIdlCompiler extends LazyLogging {
 
         override def exitFlowDecl(ctx: IDP.FlowDeclContext): Unit = {
             if (ctx.qstring() != null) {
+                flowClsName = None
+                flowMtdName = None
+                
                 val regex = U.trimQuotes(ctx.qstring().getText)
 
                 if (regex != null && regex.length > 2)
@@ -269,6 +272,13 @@ object NCIdlCompiler extends LazyLogging {
                             newSyntaxError(s"${e.getDescription} in intent flow regex '${e.getPattern}' near index ${e.getIndex}.")(ctx.qstring())
                     }
             }
+            else {
+                flowClsName = clsName
+                flowMtdName = mtdName
+            }
+            
+            clsName = None
+            mtdName = None
         }
 
         override def exitTerm(ctx: IDP.TermContext): Unit = {
@@ -276,11 +286,11 @@ object NCIdlCompiler extends LazyLogging {
                 throw newSyntaxError(s"Invalid intent term min quantifiers: $min (must be min >= 0 && min <= max).")(ctx.minMax())
             if (max < 1)
                 throw newSyntaxError(s"Invalid intent term max quantifiers: $max (must be max >= 1).")(ctx.minMax())
-
-            val pred: NCIdlFunction = if (refMtdName.isDefined) { // User-code defined term.
+                
+            val pred: NCIdlFunction = if (mtdName.isDefined) { // User-code defined term.
                 // Closure copies.
-                val clsName = refClsName.orNull
-                val mtdName = refMtdName.orNull
+                val cls = clsName.orNull
+                val mtd = mtdName.orNull
 
                 (tok: NCToken, termCtx: NCIdlContext) ⇒ {
                     val javaCtx: NCTokenPredicateContext = new NCTokenPredicateContext {
@@ -294,12 +304,12 @@ object NCIdlCompiler extends LazyLogging {
                     }
 
                     val mdl = tok.getModel
-                    val mdlCls = if (clsName == null) mdl.meta[String](MDL_META_MODEL_CLASS_KEY) else clsName
+                    val mdlCls = if (cls == null) mdl.meta[String](MDL_META_MODEL_CLASS_KEY) else cls
 
                     try {
                         val res = U.callMethod[NCTokenPredicateContext, NCTokenPredicateResult](
-                            () ⇒ if (clsName == null) mdl else U.mkObject(clsName),
-                            mtdName,
+                            () ⇒ if (cls == null) mdl else U.mkObject(cls),
+                            mtd,
                             javaCtx
                         )
 
@@ -307,7 +317,7 @@ object NCIdlCompiler extends LazyLogging {
                     }
                     catch {
                         case e: Exception ⇒
-                            throw newRuntimeError(s"Failed to invoke custom intent term: $mdlCls.$mtdName", e)(ctx.mtdDecl())
+                            throw newRuntimeError(s"Failed to invoke custom intent term: $mdlCls.$mtd(...)", e)(ctx.mtdDecl())
                     }
                 }
             }
@@ -330,8 +340,8 @@ object NCIdlCompiler extends LazyLogging {
             termId = null
             expr.clear()
             vars.clear()
-            refClsName = None
-            refMtdName = None
+            clsName = None
+            mtdName = None
         }
 
         /**
@@ -451,14 +461,14 @@ object NCIdlCompiler extends LazyLogging {
                     ordered,
                     if (intentMeta == null) Map.empty else intentMeta,
                     flowRegex,
-                    refClsName,
-                    refMtdName,
+                    flowClsName,
+                    flowMtdName,
                     terms.toList
                 )
             )(ctx.intentId())
 
-            refClsName = None
-            refMtdName = None
+            flowClsName = None
+            flowMtdName = None
             intentMeta = null
             terms.clear()
         }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompilerBase.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompilerBase.scala
index daccbdb..f16ef03 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompilerBase.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/compiler/NCIdlCompilerBase.scala
@@ -386,7 +386,7 @@ trait NCIdlCompilerBase {
     def parseEqNeqExpr(eq: TN, neq: TN)(implicit ctx: PRC): SI = (_, stack: S, _) ⇒ {
         val (x1, x2) = pop2()(stack, ctx)
 
-        def doEq(op: String, v1: Object, v2: Object): Boolean = {
+        def doEq(v1: Object, v2: Object): Boolean = {
             if (v1 eq v2) true
             else if (v1 == null && v2 == null) true
             else if ((v1 == null && v2 != null) || (v1 != null && v2 == null)) false
@@ -405,11 +405,11 @@ trait NCIdlCompilerBase {
 
             val f =
                 if (eq != null)
-                    doEq("==", v1, v2)
+                    doEq(v1, v2)
                 else {
                     assert(neq != null)
 
-                    !doEq("!='", v1, v2)
+                    !doEq(v1, v2)
                 }
 
             Z(f, n)
@@ -742,7 +742,7 @@ trait NCIdlCompilerBase {
                     else {
                         val seq: Seq[Double] = lst.asScala.map(p ⇒ JDouble.valueOf(p.toString).doubleValue())
 
-                        Z(seq.sum / seq.size, n)
+                        Z(seq.sum / seq.length, n)
                     }
                 catch {
                     case e: Exception ⇒ throw rtListTypeError(fun, e)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolverEngine.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolverEngine.scala
index 9cba0d7..7dbdcbf 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolverEngine.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/solver/NCIntentSolverEngine.scala
@@ -18,15 +18,15 @@
 package org.apache.nlpcraft.model.intent.solver
 
 import com.typesafe.scalalogging.LazyLogging
+import org.apache.nlpcraft.common._
 import org.apache.nlpcraft.common.ascii.NCAsciiTable
 import org.apache.nlpcraft.common.debug.{NCLogGroupToken, NCLogHolder}
 import org.apache.nlpcraft.common.opencensus.NCOpenCensusTrace
-import org.apache.nlpcraft.common._
 import org.apache.nlpcraft.model.impl.NCTokenLogger
-import org.apache.nlpcraft.model.{NCContext, NCDialogFlowItem, NCIntentMatch, NCResult, NCToken}
-import org.apache.nlpcraft.probe.mgrs.dialogflow.NCDialogFlowManager
 import org.apache.nlpcraft.model.impl.NCTokenPimp._
 import org.apache.nlpcraft.model.intent.{NCIdlContext, NCIdlFunction, NCIdlIntent, NCIdlTerm, NCIdlStackItem ⇒ Z}
+import org.apache.nlpcraft.model.{NCContext, NCDialogFlowItem, NCIntentMatch, NCResult, NCToken}
+import org.apache.nlpcraft.probe.mgrs.dialogflow.NCDialogFlowManager
 
 import java.util.function.Function
 import scala.collection.JavaConverters._
@@ -435,31 +435,31 @@ object NCIntentSolverEngine extends LazyLogging with NCOpenCensusTrace {
             val str = flow.map(_.getIntentId).mkString(" ")
 
             def x(s: String): Unit = {
-                logger.info(s"Intent '$intentId' ${bo(s)} regex dialog flow $varStr:")
+                logger.info(s"Intent '$intentId' $s regex dialog flow $varStr:")
                 logger.info(s"  |-- ${c("Intent IDs  :")} $str")
                 logger.info(s"  +-- ${c("Match regex :")} ${flowRegex.get.toString}")
             }
 
             if (!flowRegex.get.matcher(str).find(0)) {
-                x("did not match")
+                x(s"${bo(r("did not match"))}")
 
                 flowMatched = false
             }
             else
-                x("matched")
+                x(s"matched")
         }
         else if (intent.flowMtdName.isDefined) {
-            require(intent.flowClsName.isDefined)
-
-            val clsName = intent.flowClsName.get
+            val clsName = intent.flowClsName.orNull
             val mtdName = intent.flowMtdName.get
-            
-            val fqn = s"$clsName.$mtdName(java.util.List[NCDialogFlowItem])"
-            
+
+            val fqn =
+                s"${if (clsName == null) ctx.getModel.getClass.getName else clsName}." +
+                s"$mtdName(java.util.List[NCDialogFlowItem])"
+
             val res =
                 try
                     U.callMethod[java.util.List[NCDialogFlowItem], java.lang.Boolean](
-                        U.mkObject(clsName),
+                        () ⇒ if (clsName == null) ctx.getModel else U.mkObject(clsName),
                         mtdName,
                         flow.toList.asJava
                     )
@@ -469,12 +469,12 @@ object NCIntentSolverEngine extends LazyLogging with NCOpenCensusTrace {
                 }
 
             def x(s: String): Unit = {
-                logger.info(s"Intent '$intentId' ${bo(s)} custom flow callback $varStr:")
+                logger.info(s"Intent '$intentId' $s custom flow callback $varStr:")
                 logger.info(s"  +-- ${c("Custom callback :")} $fqn")
             }
 
             if (!res) {
-                x("did not match")
+                x(s"${bo(r("did not match"))}")
         
                 flowMatched = false
             }
@@ -541,7 +541,7 @@ object NCIntentSolverEngine extends LazyLogging with NCOpenCensusTrace {
 
                     case None ⇒
                         // Term is missing. Stop further processing for this intent. This intent cannot be matched.
-                        logger.info(s"Intent '$intentId' ${r("did not")} match because of unmatched term '$term' $varStr.")
+                        logger.info(s"Intent '$intentId' ${bo(r("did not match"))} because of unmatched term '$term' $varStr.")
 
                         abort = true
                 }
@@ -557,15 +557,15 @@ object NCIntentSolverEngine extends LazyLogging with NCOpenCensusTrace {
                 var res: Option[IntentMatch] = None
 
                 if (usedSenToks.isEmpty && usedConvToks.isEmpty)
-                    logger.info(s"Intent '$intentId' ${r("did not")} match because no tokens were matched $varStr.")
+                    logger.info(s"Intent '$intentId' ${bo(r("did not match"))} because no tokens were matched $varStr.")
                 else if (usedSenToks.isEmpty && usedConvToks.nonEmpty)
-                    logger.info(s"Intent '$intentId' ${r("did not")} match because all its matched tokens came from STM $varStr.")
+                    logger.info(s"Intent '$intentId' ${bo(r("did not match"))} because all its matched tokens came from STM $varStr.")
                 else if (unusedSenToks.exists(_.token.isUserDefined))
                     NCTokenLogger.prepareTable(unusedSenToks.filter(_.token.isUserDefined).map(_.token)).
                         info(
                             logger,
                             Some(
-                                s"Intent '$intentId' ${r("did not")} match because of remaining unused user tokens $varStr." +
+                                s"Intent '$intentId' ${bo(r("did not match"))} because of remaining unused user tokens $varStr." +
                                 s"\nUnused user tokens for intent '$intentId' $varStr:"
                             )
                         )
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
index 8121f88..e19a9d2 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
@@ -17,13 +17,6 @@
 
 package org.apache.nlpcraft.probe.mgrs.conn
 
-import java.io.{EOFException, IOException, InterruptedIOException}
-import java.net.{InetAddress, NetworkInterface}
-import java.util
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.{Properties, TimeZone}
-
 import io.opencensus.trace.Span
 import org.apache.nlpcraft.common._
 import org.apache.nlpcraft.common.config.NCConfigurable
@@ -35,6 +28,11 @@ import org.apache.nlpcraft.probe.mgrs.NCProbeMessage
 import org.apache.nlpcraft.probe.mgrs.cmd.NCCommandManager
 import org.apache.nlpcraft.probe.mgrs.model.NCModelManager
 
+import java.io.{EOFException, IOException, InterruptedIOException}
+import java.net.{InetAddress, NetworkInterface}
+import java.util
+import java.util.concurrent.CountDownLatch
+import java.util.{Properties, TimeZone}
 import scala.collection.mutable
 
 /**
@@ -176,11 +174,11 @@ object NCConnectionManager extends NCService {
                 val ver = NCVersion.getCurrent
                 val tmz = TimeZone.getDefault
     
-                val srvNlpEng =
+                val srvNlpEng: String =
                     hashResp.getOrElse(
                         "NLP_ENGINE",
                         throw new HandshakeError("NLP engine parameter missed in response.")
-                    )
+                    ).asInstanceOf[String]
 
                 val probeNlpEng = NCNlpCoreManager.getEngine
 
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoNamesGenerator.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoNamesGenerator.scala
index 2b64a16..35ab1cc 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoNamesGenerator.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoNamesGenerator.scala
@@ -74,7 +74,7 @@ object NCGeoNamesGenerator extends App {
     // GEO name ID → internal representation mapping.
     private val ids = mutable.Map.empty[String, Location]
 
-    private def read(path: String): Seq[String] = U.readPath(path, "UTF-8").filter(!_.startsWith("#"))
+    private def read(path: String): Seq[String] = U.readPath(path).filter(!_.startsWith("#"))
 
     // Process country and continent information.
     private def processCountries(unsdContinents: Seq[NCUnsdStatsContinent]): Set[Country] = {
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoStateNamesGenerator.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoStateNamesGenerator.scala
index 8c570dc..e775cd9 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoStateNamesGenerator.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/NCGeoStateNamesGenerator.scala
@@ -31,7 +31,7 @@ import org.apache.nlpcraft.common.U
 object NCGeoStateNamesGenerator extends App {
     // Produce a map of regions (countryCode + regCode → region name)).
     private def getStates(txtFile: String): Map[String, String] =
-        U.readPath(txtFile, "UTF-8").filter(!_.startsWith("#")).flatMap(line ⇒ {
+        U.readPath(txtFile).filter(!_.startsWith("#")).flatMap(line ⇒ {
             val seq = line.split("\t").toSeq
 
             if (seq(7) == "ADM1" && seq(8) == "US") {
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/metro/NCGeoMetroGenerator.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/metro/NCGeoMetroGenerator.scala
index 28565ac..a76b823 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/metro/NCGeoMetroGenerator.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/metro/NCGeoMetroGenerator.scala
@@ -43,7 +43,7 @@ object NCGeoMetroGenerator extends App {
         U.normalize(s.replaceAll("\\(", " ").replaceAll("\\)", " "), " ")
 
     private def generate() {
-        val lines = U.readPath(in, "UTF-8").map(_.strip).filter(_.nonEmpty)
+        val lines = U.readPath(in).map(_.strip).filter(_.nonEmpty)
 
        // Skips header.
         val metro = lines.tail.filter(!_.contains("(not set)")).map(line ⇒ Holder(line.takeWhile(_ != ',')))
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/unstats/NCUnsdStatsService.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/unstats/NCUnsdStatsService.scala
index ac4ba82..c7030cf 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/unstats/NCUnsdStatsService.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/geo/tools/unstats/NCUnsdStatsService.scala
@@ -72,7 +72,7 @@ object NCUnsdStatsService {
     private val codesPath = s"$dir/codes.txt"
 
     private def read(path: String): Seq[String] =
-        U.readPath(path, "UTF-8").map(_.strip).filter(_.nonEmpty).filter(_.head != '#')
+        U.readPath(path).map(_.strip).filter(_.nonEmpty).filter(_.head != '#')
 
     def skip(iso: String): Boolean = SKIPPED_COUNTRIES_ISO3.contains(iso)
 
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/json/NCJson.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/json/NCJson.scala
index 9d1f452..a63b1c1 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/json/NCJson.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/json/NCJson.scala
@@ -239,14 +239,14 @@ object NCJson {
      *
      * @param f File to extract from.
      */
-    private def readFile(f: File): String = removeComments(U.readFile(f, "UTF-8").mkString)
+    private def readFile(f: File): String = removeComments(U.readFile(f).mkString)
 
     /**
       * Reads stream.
       *
       * @param in Stream to extract from.
       */
-    private def readStream(in: InputStream): String = removeComments(U.readStream(in, "UTF-8").mkString)
+    private def readStream(in: InputStream): String = removeComments(U.readStream(in).mkString)
 
     /**
      * Extracts type `T` from given JSON `file`.
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/dialog/NCDialogSpec.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/dialog/NCDialogSpec.scala
index 60b234b..3ebd2b5 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/dialog/NCDialogSpec.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/dialog/NCDialogSpec.scala
@@ -17,23 +17,33 @@
 
 package org.apache.nlpcraft.model.dialog
 
-import org.apache.nlpcraft.model.{NCElement, NCIntent, NCModel, NCResult}
+import org.apache.nlpcraft.model.{NCDialogFlowItem, NCElement, NCIntent, NCModel, NCResult}
 import org.apache.nlpcraft.{NCTestContext, NCTestElement, NCTestEnvironment}
 import org.junit.jupiter.api.Assertions.{assertEquals, assertTrue}
 import org.junit.jupiter.api.Test
 
 import java.util
+import scala.collection.JavaConverters._
 
 /**
   * Test model.
   */
+class NCDialogSpecModelFlow {
+    def trueAlways(flow: java.util.List[NCDialogFlowItem]): Boolean = true
+    def falseAlways(flow: java.util.List[NCDialogFlowItem]): Boolean = false
+    def trueAfterOnA7(flow: java.util.List[NCDialogFlowItem]): Boolean = flow.asScala.exists(_.getIntentId == "onA7")
+    def trueAfterOnA7AndA8(flow: java.util.List[NCDialogFlowItem]): Boolean = {
+        val seq = flow.asScala
+        seq.exists(_.getIntentId == "onA7") && seq.exists(_.getIntentId == "onA8")
+    }
+}
+
 class NCDialogSpecModel extends NCModel {
     override def getId: String = this.getClass.getSimpleName
     override def getName: String = this.getClass.getSimpleName
     override def getVersion: String = "1.0.0"
 
-    override def getElements: util.Set[NCElement] =
-        Set((for (ch ← 'a' to 'y'; i ← 1 to 9) yield NCTestElement(s"$ch$i")):_*)
+    override def getElements: util.Set[NCElement] = Set((for (i ← 1 to 100) yield NCTestElement(s"a$i")):_*)
 
     @NCIntent("intent=onA1 term~{tok_id() == 'a1'}")
     def onA1(): NCResult = NCResult.text("ok")
@@ -46,6 +56,31 @@ class NCDialogSpecModel extends NCModel {
 
     @NCIntent("intent=onA4 flow='onA1 onA1' term~{tok_id() == 'a4'}")
     def onA4(): NCResult = NCResult.text("ok")
+
+    @NCIntent("intent=onA5 flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow#trueAlways/ term~{tok_id() == 'a5'}")
+    def onA5(): NCResult = NCResult.text("ok")
+
+    @NCIntent("intent=onA6 flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow#falseAlways/ term~{tok_id() == 'a6'}")
+    def onA6(): NCResult = NCResult.text("ok")
+
+    @NCIntent("intent=onA7 term~{tok_id() == 'a7'}")
+    def onA7(): NCResult = NCResult.text("ok")
+
+    @NCIntent("intent=onA8 flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow#trueAfterOnA7/ term~{tok_id() == 'a8'}")
+    def onA8(): NCResult = NCResult.text("ok")
+
+    @NCIntent("intent=onA9 flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow#trueAfterOnA7AndA8/ term~{tok_id() == 'a9'}")
+    def onA9(): NCResult = NCResult.text("ok")
+
+    def trueAlwaysInternal(flow: java.util.List[NCDialogFlowItem]): Boolean = true
+
+    @NCIntent("intent=onA10 flow=/#trueAlwaysInternal/ term~{tok_id() == 'a10'}")
+    def onA10(): NCResult = NCResult.text("ok")
+
+    def falseAlwaysInternal(flow: java.util.List[NCDialogFlowItem]): Boolean = false
+
+    @NCIntent("intent=onA11 flow=/#falseAlwaysInternal/ term~{tok_id() == 'a11'}")
+    def onA11(): NCResult = NCResult.text("ok")
 }
 
 /**
@@ -116,4 +151,34 @@ class NCDialogSpec extends NCTestContext {
             "a4" → "onA4",
             "a4" → "onA4"
         )
+
+    @Test
+    private[dialog] def test4(): Unit = {
+        // Always true.
+        checkIntent("a5", "onA5")
+        checkIntent("a5", "onA5")
+
+        // Always false.
+        require(getClient.ask("a6").isFailed)
+        require(getClient.ask("a6").isFailed)
+    }
+
+    @Test
+    private[dialog] def test5(): Unit =
+        f(
+            "a8" → null,
+            "a9" → null,
+            "a7" → "onA7",
+            "a9" → null,
+            "a8" → "onA8",
+            "a9" → "onA9"
+        )
+
+    @Test
+    private[dialog] def test6(): Unit = {
+        // Always 'true'.
+        require(getClient.ask("a10").isOk)
+        // Always 'false'.
+        require(getClient.ask("a11").isFailed)
+    }
 }
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/dialog/NCDialogSpec2.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/dialog/NCDialogSpec2.scala
new file mode 100644
index 0000000..6cf9249
--- /dev/null
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/dialog/NCDialogSpec2.scala
@@ -0,0 +1,147 @@
+/*
+ * 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.model.dialog
+
+import org.apache.nlpcraft.model.{NCDialogFlowItem, NCElement, NCIntent, NCModel, NCResult}
+import org.apache.nlpcraft.{NCTestContext, NCTestElement, NCTestEnvironment}
+import org.junit.jupiter.api.Test
+
+import java.util
+
+object NCDialogSpecModelFlow2  {
+    var error = false
+}
+
+import NCDialogSpecModelFlow2._
+
+/**
+  * Test model.
+  */
+class NCDialogSpecModelFlow2  {
+    def invalidDef1(flow: java.util.List[NCDialogFlowItem]): String = "string"
+    def invalidDef2(): Boolean = true
+    def invalidDef3(flow: java.util.List[NCDialogFlowItem]): Boolean = throw new IllegalStateException()
+    def invalidDef4(flow: java.util.List[NCDialogFlowItem]): java.lang.Boolean = null
+    def validDef(flow: java.util.List[NCDialogFlowItem]): Boolean = {
+        if (error)
+            throw new IllegalStateException()
+
+        true
+    }
+}
+
+/**
+  *
+  */
+class NCDialogSpecModel21 extends NCModel {
+    override def getId: String = this.getClass.getSimpleName
+    override def getName: String = this.getClass.getSimpleName
+    override def getVersion: String = "1.0.0"
+
+    override def getElements: util.Set[NCElement] = Set(NCTestElement("a"))
+
+    @NCIntent("intent=onA flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow2#invalidDef1/ term~{tok_id() == 'a'}")
+    def onA(): NCResult = NCResult.text("ok")
+}
+
+/**
+  *
+  */
+@NCTestEnvironment(model = classOf[NCDialogSpecModel21], startClient = true)
+class NCDialogSpec21 extends NCTestContext {
+    @Test
+    def test(): Unit = require(getClient.ask("a").isFailed)
+}
+
+/**
+  *
+  */
+class NCDialogSpecModel22 extends NCDialogSpecModel21 {
+    @NCIntent("intent=onA flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow2#invalidDef2/ term~{tok_id() == 'a'}")
+    override def onA(): NCResult = NCResult.text("ok")
+}
+
+/**
+  *
+  */
+@NCTestEnvironment(model = classOf[NCDialogSpecModel22], startClient = true)
+class NCDialogSpec22 extends NCDialogSpec21
+
+/**
+  *
+  */
+class NCDialogSpecModel23 extends NCDialogSpecModel21 {
+    @NCIntent("intent=onA flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow2#invalidDef3/ term~{tok_id() == 'a'}")
+    override def onA(): NCResult = NCResult.text("ok")
+}
+
+/**
+  *
+  */
+@NCTestEnvironment(model = classOf[NCDialogSpecModel23], startClient = true)
+class NCDialogSpec23 extends NCDialogSpec21
+
+/**
+  *
+  */
+class NCDialogSpecModel24 extends NCDialogSpecModel21 {
+    @NCIntent("intent=onA flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow2#invalidDef4/ term~{tok_id() == 'a'}")
+    override def onA(): NCResult = NCResult.text("ok")
+}
+
+/**
+  *
+  */
+@NCTestEnvironment(model = classOf[NCDialogSpecModel24], startClient = true)
+class NCDialogSpec24 extends NCDialogSpec21
+
+/**
+  *
+  */
+class NCDialogSpecModel25 extends NCDialogSpecModel21 {
+    @NCIntent("intent=onA flow=/org.apache.nlpcraft.model.dialog.NCDialogSpecModelFlow2#validDef/ term~{tok_id() == 'a'}")
+    override def onA(): NCResult = NCResult.text("ok")
+}
+
+/**
+  *
+  */
+@NCTestEnvironment(model = classOf[NCDialogSpecModel25], startClient = true)
+class NCDialogSpec25 extends NCTestContext {
+    @Test
+    def test(): Unit = {
+        def test(txt: String, exp: Boolean): Unit = {
+            NCDialogSpecModelFlow2.error = !exp
+
+            require(getClient.ask("a").isOk == exp)
+        }
+
+        test(txt = "a", exp = true)
+        test(txt = "a", exp = false)
+        test(txt = "a", exp = true)
+    }
+}
+
+
+
+
+
+
+
+
+
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctions.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctions.scala
index 4c8840d..35eb0f7 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctions.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctions.scala
@@ -20,7 +20,7 @@ package org.apache.nlpcraft.model.intent.idl.compiler.functions
 import org.apache.nlpcraft.common.{NCE, ScalaMeta}
 import org.apache.nlpcraft.model.intent.compiler.{NCIdlCompiler, NCIdlCompilerGlobal}
 import org.apache.nlpcraft.model.intent.{NCIdlContext, NCIdlTerm}
-import org.apache.nlpcraft.model.{NCCompany, NCModel, NCModelView, NCRequest, NCToken, NCUser}
+import org.apache.nlpcraft.model.{NCCompany, NCModel, NCModelView, NCRequest, NCToken, NCTokenPredicateContext, NCTokenPredicateResult, NCUser}
 import org.junit.jupiter.api.BeforeEach
 
 import java.util
@@ -34,20 +34,36 @@ import scala.language.implicitConversions
 private[functions] trait NCIdlFunctions {
     private final val MODEL_ID = "test.mdl.id"
 
-    final val MODEL: NCModel = new NCModel {
+    // It shouldn't be anonimous class because we need access to 'trueAlwaysCustomToken' method via reflection.
+    class TestModel extends NCModel {
         override val getId: String = MODEL_ID
         override val getName: String = MODEL_ID
         override val getVersion: String = "1.0.0"
 
         override def getOrigin: String = "test"
+
+        def trueAlwaysCustomToken(ctx: NCTokenPredicateContext): NCTokenPredicateResult =
+            new NCTokenPredicateResult(true, 1)
     }
 
+    final val MODEL: NCModel = new TestModel()
+
     @BeforeEach
     def before(): Unit = NCIdlCompilerGlobal.clearCache(MODEL_ID)
 
-    case class TestDesc(truth: String, token: Option[NCToken] = None, idlCtx: NCIdlContext = ctx()) {
-        val term: NCIdlTerm = {
-            val intents = NCIdlCompiler.compileIntents(s"intent=i term(t)={$truth}", MODEL, MODEL_ID)
+    case class TestDesc(
+        truth: String,
+        token: Option[NCToken] = None,
+        idlCtx: NCIdlContext = ctx(),
+        isCustom: Boolean = false,
+        expectedRes: Boolean = true,
+        tokensUsed: Option[Int] = None
+    ) {
+        // It should be lazy for errors verification methods.
+        lazy val term: NCIdlTerm = {
+            val (s1, s2) = if (isCustom) ('/', '/') else ('{', '}')
+
+            val intents = NCIdlCompiler.compileIntents(s"intent=i term(t)=$s1$truth$s2", MODEL, MODEL_ID)
 
             require(intents.size == 1)
             require(intents.head.terms.size == 1)
@@ -70,7 +86,7 @@ private[functions] trait NCIdlFunctions {
             TestDesc(truth = truth, token = Some(token))
     }
 
-    private def t2s(t: NCToken) = {
+    private def t2s(t: NCToken): String = {
         def nvl(s: String, name: String): String = if (s != null) s else s"$name (not set)"
 
         s"text=${nvl(t.getOriginalText, "text")} [${nvl(t.getId, "id")}]"
@@ -148,42 +164,66 @@ private[functions] trait NCIdlFunctions {
 
     protected def test(funcs: TestDesc*): Unit =
         for (f ← funcs) {
-            val res =
+            val item =
                 try {
                     // Process declarations.
                     f.idlCtx.vars ++= f.term.decls
 
                     // Execute term's predicate.
-                    f.term.pred.apply(f.token.getOrElse(tkn()), f.idlCtx).value
+                    f.term.pred.apply(f.token.getOrElse(tkn()), f.idlCtx)
                 }
                 catch {
-                    case e: NCE ⇒ throw new NCE(s"Execution error processing: $f", e)
+                    case e: NCE ⇒ throw e
                     case e: Exception ⇒ throw new Exception(s"Execution error processing: $f", e)
                 }
 
-            res match {
-                case b: java.lang.Boolean ⇒ require(b, s"Unexpected FALSE result for: $f")
+            item.value match {
+                case b: java.lang.Boolean ⇒ require(if (f.expectedRes) b else !b, s"Unexpected '$b' result for: $f")
                 case _ ⇒
                     require(
                         requirement = false,
                         s"Unexpected result type [" +
-                            s"resType=${if (res == null) "null" else res.getClass.getName}, " +
-                            s"resValue=$res, " +
+                            s"resType=${if (item.value == null) "null" else item.value.getClass.getName}, " +
+                            s"resValue=${item.value}, " +
                             s"function=$f" +
                             s"]"
                     )
             }
-        }
 
-    protected def expectError(f: String): Unit =
-        try {
-            test(f)
+            f.tokensUsed match {
+                case Some(exp) ⇒
+                    require(
+                        exp == item.tokUse,
+                        s"Unexpected tokens used [" +
+                        s"expectedTokensUsed=$exp, " +
+                        s"resultTokensUsed=${item.tokUse}, " +
+                        s"function=$f" +
+                        s"]"
+                    )
 
-            require(false)
-        }
-        catch {
-            case e: Exception ⇒ println(s"Expected error: ${e.getLocalizedMessage}")
+                case None ⇒ // No-op.
+            }
         }
 
+    protected def expectError(funcs: TestDesc*): Unit =
+        for (f ← funcs)
+            try {
+                test(f)
+
+                require(false)
+            }
+            catch {
+                case e: Exception ⇒
+                    println(s"Expected error: ${e.getLocalizedMessage}")
+
+                    var cause = e.getCause
+
+                    while (cause != null) {
+                        println(s"  Cause: ${cause.getLocalizedMessage} (${cause.getClass.getName})")
+
+                        cause = cause.getCause
+                    }
+            }
+
     protected implicit def convert(pred: String): TestDesc = TestDesc(truth = pred)
-}
+}
\ No newline at end of file
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsCompany.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsCompany.scala
index 9a95032..b03a78c 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsCompany.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsCompany.scala
@@ -55,7 +55,6 @@ class NCIdlFunctionsCompany extends NCIdlFunctions {
             override def getMetadata: util.Map[String, AnyRef] =
                 Map("k1" → "v1").map(p ⇒ p._1 → p._2.asInstanceOf[AnyRef]).asJava
         })
-
     }
 
     private def test(comp: NCCompany): Unit = {
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsCustom.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsCustom.scala
new file mode 100644
index 0000000..651ca1e
--- /dev/null
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsCustom.scala
@@ -0,0 +1,86 @@
+/*
+ * 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.model.intent.idl.compiler.functions
+
+import org.apache.nlpcraft.common.U
+import org.apache.nlpcraft.model.{NCTokenPredicateContext, NCTokenPredicateResult}
+import org.junit.jupiter.api.Test
+
+class NCIdlTokensTestWrapper {
+    def trueOn123(ctx: NCTokenPredicateContext): NCTokenPredicateResult =
+        new NCTokenPredicateResult(ctx.getToken.getOriginalText == "123", 1)
+
+    def wrongParams1(): NCTokenPredicateResult = new NCTokenPredicateResult(true, 1)
+    def wrongParams2(s: String): NCTokenPredicateResult = new NCTokenPredicateResult(true, 1)
+    def wrongParams3(s: String, ctx: NCTokenPredicateContext): NCTokenPredicateResult =
+        new NCTokenPredicateResult(true, 1)
+    def wrongRet1(ctx: NCTokenPredicateContext): String = ""
+    def wrongRet2(ctx: NCTokenPredicateContext): Unit = {}
+    def wrongExec1(ctx: NCTokenPredicateContext): NCTokenPredicateResult = null
+    def wrongExec2(ctx: NCTokenPredicateContext): NCTokenPredicateResult = throw new NullPointerException
+}
+
+/**
+  * Tests for 'custom' functions.
+  */
+class NCIdlFunctionsCustom extends NCIdlFunctions {
+    private final val C = U.cleanClassName(classOf[NCIdlTokensTestWrapper], simpleName = false)
+
+    @Test
+    def testErrors(): Unit = {
+        def test(truth: String*): Unit =
+            for (t ← truth)
+                expectError(TestDesc(truth = t, isCustom = true))
+
+        test(
+            "invalid",
+            s"$C#missed",
+            s"$C#wrongParams1",
+            s"$C#wrongParams2",
+            s"$C#wrongParams3",
+            s"$C#wrongRet1",
+            s"$C#wrongRet2",
+            s"$C#wrongExec1",
+            s"$C#wrongExec2"
+        )
+    }
+
+    @Test
+    def test(): Unit =
+        test(
+            TestDesc(
+                truth = s"$C#trueOn123",
+                isCustom = true,
+                token = Some(tkn(txt = "123")),
+                tokensUsed = Some(1)
+            ),
+            TestDesc(
+                truth = s"$C#trueOn123",
+                isCustom = true,
+                token = Some(tkn(txt = "456")),
+                expectedRes = false,
+                tokensUsed = Some(1)
+            ),
+            TestDesc(
+                // Method defined in model.
+                truth = s"#trueAlwaysCustomToken",
+                isCustom = true,
+                token = Some(tkn(txt = "any"))
+            )
+        )
+}
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsMath.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsMath.scala
index 9596dc2..e921ee8 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsMath.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsMath.scala
@@ -25,15 +25,15 @@ import org.junit.jupiter.api.Test
   */
 class NCIdlFunctionsMath extends NCIdlFunctions {
     @Test
-    def testError(): Unit = {
-        // Invalid name.
-        expectError("xxx(1, 1)")
-        expectError("xxx()")
-
-        // Invalid arguments count.
-        expectError("atan(1, 1) == 1")
-        expectError("pi(1)")
-    }
+    def testError(): Unit =
+        expectError(
+            // Invalid name.
+            "xxx(1, 1)",
+            "xxx()",
+            // Invalid arguments count.
+            "atan(1, 1) == 1",
+            "pi(1)"
+        )
 
     @Test
     def test(): Unit =
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsOther.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsOther.scala
index 5d1ebc9..de1c718 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsOther.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsOther.scala
@@ -53,10 +53,19 @@ class NCIdlFunctionsOther extends NCIdlFunctions {
     }
 
     @Test
-    def test3(): Unit = {
+    def test3(): Unit =
         test(
-            s"to_string(list(1, 2, 3)) == list('1', '2', '3')",
+            "to_string(list(1, 2, 3)) == list('1', '2', '3')",
             "to_string(3.123) == '3.123'"
         )
-    }
+
+    @Test
+    def test4(): Unit =
+        test(
+            "true",
+            "true == true",
+            "false == false",
+            "false != true",
+            "true != false"
+        )
 }
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsRequest.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsRequest.scala
index b756389..32daca3 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsRequest.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsRequest.scala
@@ -46,7 +46,7 @@ class NCIdlFunctionsRequest extends NCIdlFunctions {
             mkTestDesc(s"req_normtext() == '$reqNormText'"),
             mkTestDesc(s"req_tstamp() == $reqTstamp"),
             mkTestDesc(s"req_addr() == '$reqAddr'"),
-            mkTestDesc(s"req_agent() == '$reqAgent'"),
+            mkTestDesc(s"req_agent() == '$reqAgent'")
         )
     }
 }
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStat.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStat.scala
index 4e99c1b..b41b596 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStat.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStat.scala
@@ -24,12 +24,13 @@ import org.junit.jupiter.api.Test
   */
 class NCIdlFunctionsStat extends NCIdlFunctions {
     @Test
-    def testError(): Unit = {
-        expectError("avg(list()) == 2")
-        expectError("avg(list('A')) == 2")
-        expectError("stdev(list()) == 2")
-        expectError("stdev(list('A')) == 2")
-    }
+    def testError(): Unit =
+        expectError(
+            "avg(list()) == 2",
+            "avg(list('A')) == 2",
+            "stdev(list()) == 2",
+            "stdev(list('A')) == 2"
+        )
 
     @Test
     def test(): Unit =
@@ -45,6 +46,6 @@ class NCIdlFunctionsStat extends NCIdlFunctions {
             "stdev(list(1, 2.2, 3.1)) > 0",
             "stdev(list(1, 2, 3)) > 0",
             "stdev(list(0.0, 0.0, 0.0)) == 0.0",
-            "stdev(list(0, 0, 0)) == 0.0",
+            "stdev(list(0, 0, 0)) == 0.0"
         )
 }
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStrings.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStrings.scala
index 9a33eaf..12e621b 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStrings.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStrings.scala
@@ -77,11 +77,12 @@ class NCIdlFunctionsStrings extends NCIdlFunctions {
         )
 
     @Test
-    def testError(): Unit = {
-        expectError("substr('abc', 10, 30) == 'bc'")
-        expectError("split('1 A') == true")
-        expectError("split_trim('1 A') == true")
-        expectError("to_double('1, 1') == true")
-        expectError("to_double('A') == true")
-    }
+    def testError(): Unit =
+        expectError(
+            "substr('abc', 10, 30) == 'bc'",
+            "split('1 A') == true",
+            "split_trim('1 A') == true",
+            "to_double('1, 1') == true",
+            "to_double('A') == true"
+        )
 }
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsToken.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsToken.scala
index f985559..cdf2e49 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsToken.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsToken.scala
@@ -105,8 +105,6 @@ class NCIdlFunctionsToken extends NCIdlFunctions {
                 truth = "tok_end_idx() == 123",
                 token = tkn(end = 123)
             ),
-            TestDesc(
-                truth = "tok_this() == tok_this()"
-            )
+            TestDesc(truth = "tok_this() == tok_this()")
         )
 }
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStat.scala b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsTokensUsed.scala
similarity index 50%
copy from nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStat.scala
copy to nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsTokensUsed.scala
index 4e99c1b..8b62bdd 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsStat.scala
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/functions/NCIdlFunctionsTokensUsed.scala
@@ -20,31 +20,35 @@ package org.apache.nlpcraft.model.intent.idl.compiler.functions
 import org.junit.jupiter.api.Test
 
 /**
-  * Tests for 'stat' functions.
+  * Tests for 'tokens used' result.
   */
-class NCIdlFunctionsStat extends NCIdlFunctions {
-    @Test
-    def testError(): Unit = {
-        expectError("avg(list()) == 2")
-        expectError("avg(list('A')) == 2")
-        expectError("stdev(list()) == 2")
-        expectError("stdev(list('A')) == 2")
-    }
-
+class NCIdlFunctionsTokensUsed extends NCIdlFunctions {
     @Test
     def test(): Unit =
         test(
-            "max(list(1, 2, 3)) == 3",
-            "max(list(1.0, 2.0, 3.0)) == 3.0",
-            "min(list(1, 2, 3)) == 1",
-            "min(list(1.0, 2.0, 3.0)) == 1.0",
-            "avg(list(1.0, 2.0, 3.0)) == 2.0",
-            "avg(list(1, 2, 3)) == 2.0",
-            "avg(list(1.2, 2.2, 3.2)) == 2.2",
-            "avg(list(1, 2.2, 3.1)) == 2.1",
-            "stdev(list(1, 2.2, 3.1)) > 0",
-            "stdev(list(1, 2, 3)) > 0",
-            "stdev(list(0.0, 0.0, 0.0)) == 0.0",
-            "stdev(list(0, 0, 0)) == 0.0",
+            TestDesc(
+                truth = "1 == 1",
+                tokensUsed = Some(0)
+            ),
+            TestDesc(
+                truth = "tok_id() == 'a'",
+                token = Some(tkn(id = "a")),
+                tokensUsed = Some(1)
+            ),
+            TestDesc(
+                truth = "tok_id() == 'a' && tok_id() == 'a'",
+                token = Some(tkn(id = "a")),
+                tokensUsed = Some(2)
+            ),
+            TestDesc(
+                truth = "tok_id() == 'a' && tok_parent() == 'b'",
+                token = Some(tkn(id = "a", parentId = "b")),
+                tokensUsed = Some(2)
+            ),
+            TestDesc(
+                truth = "tok_id() == 'a' && tok_id() == 'a' && tok_parent() == 'b'",
+                token = Some(tkn(id = "a", parentId = "b")),
+                tokensUsed = Some(3)
+            )
         )
 }
diff --git a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/test_ok.idl b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/test_ok.idl
index b9ae2df..593a7f5 100644
--- a/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/test_ok.idl
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/model/intent/idl/compiler/test_ok.idl
@@ -55,4 +55,6 @@ intent=i3
             json(meta_req('user_json_payload')),
             list("موسكو\"", 'v1\'v1', "k2", "v2")
         )
-    }
\ No newline at end of file
+    }
+
+intent=i4 flow=/#flowModelMethod/ term(t1)=/#termModelMethod/
\ No newline at end of file