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

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

commit eb3b0c53a7d9016a75b720450505c7d75ff05be3
Author: Sergey Kamov <[email protected]>
AuthorDate: Sat Nov 12 17:02:31 2022 +0400

    Intent trait API.
---
 nlpcraft/src/main/resources/time_model.yaml        |  27 ++
 .../main/scala/org/apache/nlpcraft/NCIntent2.scala |  27 ++
 .../org/apache/nlpcraft/NCMatchedCallback2.scala   |  27 ++
 .../main/scala/org/apache/nlpcraft/NCModel.scala   |   4 +-
 .../scala/org/apache/nlpcraft/NCModelClient.scala  |  38 +--
 .../intent/matcher/NCIntentSolverManager.scala     | 327 +++++++++++----------
 .../nlpcraft/internal/impl/NCModelClientSpec.scala |  93 +++---
 .../internal/impl/NCModelClientSpec2.scala         | 129 ++++----
 .../internal/impl/NCModelClientSpec3.scala         |  59 ++--
 .../apache/nlpcraft/nlp/test/TimeTestModel.scala   |  31 ++
 .../nlpcraft/nlp/test/TimeTestModelSpec.scala      |  62 ++++
 11 files changed, 513 insertions(+), 311 deletions(-)

diff --git a/nlpcraft/src/main/resources/time_model.yaml 
b/nlpcraft/src/main/resources/time_model.yaml
new file mode 100644
index 00000000..d3798dfb
--- /dev/null
+++ b/nlpcraft/src/main/resources/time_model.yaml
@@ -0,0 +1,27 @@
+#
+# 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:
+  "<OF>": "{of|for|per}"
+  "<CUR>": "{current|present|now|local}"
+  "<TIME>": "{time <OF> day|day 
time|date|time|moment|datetime|hour|o'clock|clock|date time|date and time|time 
and date}"
+elements:
+  - id: "x:time"
+    description: "Date and/or time token indicator."
+    synonyms:
+      - "{<CUR>|_} <TIME>"
+      - "what <TIME> {is it now|now|is it|_}"
\ No newline at end of file
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCIntent2.scala 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCIntent2.scala
new file mode 100644
index 00000000..c3e56a63
--- /dev/null
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCIntent2.scala
@@ -0,0 +1,27 @@
+/*
+ * 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
+ *
+ *      https://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
+
+import org.apache.nlpcraft.*
+
+trait NCIntent2[T] {
+    // Gets callback argument if matched.
+    def tryMatch(ctx: NCContext, variant: NCVariant): Option[T]
+
+    def mkResult(ctx: NCContext, arg: T): NCResult
+}
diff --git 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCMatchedCallback2.scala 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCMatchedCallback2.scala
new file mode 100644
index 00000000..25342712
--- /dev/null
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCMatchedCallback2.scala
@@ -0,0 +1,27 @@
+/*
+ * 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
+ *
+ *      https://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
+
+/**
+  *
+  * @see [[NCModelClient.debugAsk()]]
+  */
+trait NCMatchedCallback2[T]:
+    def getIntent: NCIntent2[T]
+    def getContext: NCContext
+    def getIntentArgument: T
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModel.scala 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModel.scala
index b45735be..c3cd882d 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModel.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModel.scala
@@ -154,4 +154,6 @@ trait NCModel(cfg: NCModelConfig, pipeline: NCPipeline):
       *         behavior and existing query result or error processing, if 
any. If the method returns `None` - the
       *         default processing flow will continue.
       */
-    def onError(ctx: NCContext, e: Throwable): Option[NCResult] = None
\ No newline at end of file
+    def onError(ctx: NCContext, e: Throwable): Option[NCResult] = None
+
+    def getIntents: List[NCIntent2[_]] = null // TODO: drop definition.
\ No newline at end of file
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.scala 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.scala
index cf007ae8..798c5261 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.scala
@@ -86,7 +86,7 @@ class NCModelClient(mdl: NCModel) extends LazyLogging, 
AutoCloseable:
       * @param usrId
       * @param typ
       */
-    private def ask0(txt: String, data: Map[String, Any], usrId: String, typ: 
NCIntentSolveType): Either[NCResult, NCMatchedCallback] =
+    private def ask0(txt: String, data: Map[String, Any], usrId: String, typ: 
NCIntentSolveType): Either[NCResult, NCMatchedCallback2[Any]] =
         require(txt != null, "Input text cannot be null.")
         require(data != null, "Data cannot be null.")
         require(usrId != null, "User id cannot be null.")
@@ -194,22 +194,22 @@ class NCModelClient(mdl: NCModel) extends LazyLogging, 
AutoCloseable:
         intentsMgr.close()
 
     /**
-      * Passes given input text to the model's pipeline for processing.
-      *
-      * This method differs from [[NCModelClient.ask()]] method in a way that 
instead of calling a callback
-      * of the winning intent this method returns the descriptor of that 
callback without actually calling it.
-      * This method is well suited for testing the model's intent matching 
logic without automatically
-      * executing the actual intent's callbacks.
-      *
-      * @param txt Text of the request.
-      * @param usrId ID of the user to associate with this request.
-      * @param saveHist Whether or not to store matched intent in the dialog 
history.
-      * @param data Optional data container that will be available to the 
intent matching IDL.
-      * @return Processing result. This method never returns `null`.
-      * @throws NCRejection An exception indicating a rejection of the user 
input. This exception is thrown
-      *     automatically by the processing logic as well as can be thrown by 
the user from the intent callback.
-      * @throws NCException Thrown in case of any internal errors processing 
the user input.
-      */
-    def debugAsk(txt: String, usrId: String, saveHist: Boolean, data: 
Map[String, AnyRef] = Map.empty): NCMatchedCallback =
+     * Passes given input text to the model's pipeline for processing.
+     *
+     * This method differs from [[NCModelClient.ask()]] method in a way that 
instead of calling a callback
+     * of the winning intent this method returns the descriptor of that 
callback without actually calling it.
+     * This method is well suited for testing the model's intent matching 
logic without automatically
+     * executing the actual intent's callbacks.
+     *
+     * @param txt      Text of the request.
+     * @param usrId    ID of the user to associate with this request.
+     * @param saveHist Whether or not to store matched intent in the dialog 
history.
+     * @param data     Optional data container that will be available to the 
intent matching IDL.
+     * @return Processing result. This method never returns `null`.
+     * @throws NCRejection An exception indicating a rejection of the user 
input. This exception is thrown
+     *                     automatically by the processing logic as well as 
can be thrown by the user from the intent callback.
+     * @throws NCException Thrown in case of any internal errors processing 
the user input.
+     */
+    def debugAsk[T](txt: String, usrId: String, saveHist: Boolean, data: 
Map[String, AnyRef] = Map.empty): NCMatchedCallback2[T] =
         import NCIntentSolveType.*
-        ask0(txt, data, usrId, if saveHist then SEARCH else 
SEARCH_NO_HISTORY).toOption.get
\ No newline at end of file
+        ask0(txt, data, usrId, if saveHist then SEARCH else 
SEARCH_NO_HISTORY).toOption.get.asInstanceOf[NCMatchedCallback2[T]]
\ No newline at end of file
diff --git 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverManager.scala
 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverManager.scala
index ed432620..de8cc7e7 100644
--- 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverManager.scala
+++ 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverManager.scala
@@ -178,14 +178,14 @@ object NCIntentSolverManager:
       */
     private case class IntentEntity(var used: Boolean, var conv: Boolean, 
entity: NCEntity)
 
-    type ResultData = Either[NCResult, NCMatchedCallback]
+    type ResultData[T] = Either[NCResult, NCMatchedCallback2[T]]
 
     /**
       *
       * @param result
       * @param intentMatch
       */
-    private case class IterationResult(result: ResultData, intentMatch: 
NCIntentMatch)
+    private case class IterationResult[T](result: ResultData[T], intentMatch: 
NCIntentMatch)
 
     /**
       * @param termId
@@ -643,110 +643,110 @@ class NCIntentSolverManager(
 
             PredicateMatch(usedEnts.toList, new Weight(senTokNum, 
convDepthsSum, usesSum)).?
 
-    /**
-      *
-      * @param mdl
-      * @param ctx
-      * @param typ
-      * @param key
-      */
-    private def solveIteration(mdl: NCModel, ctx: NCContext, typ: 
NCIntentSolveType, key: UserModelKey): Option[IterationResult] =
-        require(intents.nonEmpty)
-
-        val req = ctx.getRequest
-
-        val intentResults =
-            try solveIntents(mdl, ctx, intents)
-            catch case e: Exception => throw new NCRejection("Processing 
failed due to unexpected error.", e)
-
-        if intentResults.isEmpty then throw new NCRejection("No matching 
intent found.")
-
-        object Loop:
-            private var data: Option[IterationResult] = _
-
-            def hasNext: Boolean = data == null
-            def finish(data: IterationResult): Unit = Loop.data = data.?
-            def finish(): Unit = Loop.data = None
-            def result(): Option[IterationResult] =
-                if data == null then throw new NCRejection("No matching intent 
found - all intents were skipped.")
-                data
-
-        for (intentRes <- intentResults.filter(_ != null) if Loop.hasNext)
-            def mkIntentMatch(arg: List[List[NCEntity]]): NCIntentMatch =
-                new NCIntentMatch:
-                    override val getIntentId: String = intentRes.intentId
-                    override val getIntentEntities: List[List[NCEntity]] = 
intentRes.groups.map(_.entities)
-                    override def getTermEntities(idx: Int): List[NCEntity] = 
intentRes.groups(idx).entities
-                    override def getTermEntities(termId: String): 
List[NCEntity] =
-                        intentRes.groups.find(_.termId === termId) match
-                            case Some(g) => g.entities
-                            case None => List.empty
-                    override val getVariant: NCVariant =
-                        new NCVariant:
-                            override def getEntities: List[NCEntity] = 
intentRes.variant.entities
-
-            val im = mkIntentMatch(intentRes.groups.map(_.entities))
-            try
-                if mdl.onMatchedIntent(ctx, im) then
-                    // This can throw NCIntentSkip exception.
-                    import NCIntentSolveType.*
-
-                    def saveHistory(res: Option[NCResult], im: NCIntentMatch): 
Unit =
-                        dialog.addMatchedIntent(im, res, ctx)
-                        conv.getConversation(req.getUserId).addEntities(
-                            req.getRequestId, 
im.getIntentEntities.flatten.distinct
-                        )
-                        logger.info(s"Intent '${intentRes.intentId}' for 
variant #${intentRes.variantIdx + 1} selected as the <|best match|>")
-
-                    def executeCallback(in: NCCallbackInput): NCResult =
-                        var cbRes = intentRes.fn(in)
-                        // Store winning intent match in the input.
-                        if cbRes.getIntentId.isEmpty then cbRes = 
NCResult(cbRes.getBody, cbRes.getType, intentRes.intentId)
-                        cbRes
-
-                    def finishSearch(): Unit =
-                        @volatile var called = false
-
-                        def f(args: List[List[NCEntity]]): NCResult =
-                            if called then E("Callback was already called.")
-                            called = true
-
-                            val reqId = reqIds.synchronized { 
reqIds.getOrElse(key, null) }
-
-                            if reqId != ctx.getRequest.getRequestId then 
E("Callback is out of date.")
-
-                            typ match
-                                case SEARCH =>
-                                    val imNew = mkIntentMatch(args)
-                                    val cbRes = 
executeCallback(NCCallbackInput(ctx, imNew))
-                                    dialog.replaceLastItem(imNew, cbRes, ctx)
-                                    cbRes
-                                case SEARCH_NO_HISTORY => 
executeCallback(NCCallbackInput(ctx, mkIntentMatch(args)))
-                                case _ => throw new 
AssertionError(s"Unexpected state: $typ")
-
-                        
Loop.finish(IterationResult(Right(CallbackDataImpl(im.getIntentId, 
im.getIntentEntities, f)), im))
-
-                    typ match
-                        case REGULAR =>
-                            val cbRes = executeCallback(NCCallbackInput(ctx, 
im))
-                            saveHistory(cbRes.?, im)
-                            Loop.finish(IterationResult(Left(cbRes), im))
-                        case SEARCH =>
-                            saveHistory(None, im)
-                            finishSearch()
-                        case SEARCH_NO_HISTORY =>
-                            finishSearch()
-                else
-                    logger.info(s"Model '${ctx.getModelConfig.getId}' 
triggered rematching of intents by intent '${intentRes.intentId}' on variant 
#${intentRes.variantIdx + 1}.")
-                    Loop.finish()
-                catch
-                    case e: NCIntentSkip =>
-                        // No-op - just skipping this result.
-                        e.getMessage match
-                            case s if s != null => logger.info(s"Selected 
intent '${intentRes.intentId}' skipped: $s")
-                            case _ => logger.info(s"Selected intent 
'${intentRes.intentId}' skipped.")
-
-        Loop.result()
+//    /**
+//      *
+//      * @param mdl
+//      * @param ctx
+//      * @param typ
+//      * @param key
+//      */
+//    private def solveIteration(mdl: NCModel, ctx: NCContext, typ: 
NCIntentSolveType, key: UserModelKey): Option[IterationResult] =
+//        require(intents.nonEmpty)
+//
+//        val req = ctx.getRequest
+//
+//        val intentResults =
+//            try solveIntents(mdl, ctx, intents)
+//            catch case e: Exception => throw new NCRejection("Processing 
failed due to unexpected error.", e)
+//
+//        if intentResults.isEmpty then throw new NCRejection("No matching 
intent found.")
+//
+//        object Loop:
+//            private var data: Option[IterationResult] = _
+//
+//            def hasNext: Boolean = data == null
+//            def finish(data: IterationResult): Unit = Loop.data = data.?
+//            def finish(): Unit = Loop.data = None
+//            def result(): Option[IterationResult] =
+//                if data == null then throw new NCRejection("No matching 
intent found - all intents were skipped.")
+//                data
+//
+//        for (intentRes <- intentResults.filter(_ != null) if Loop.hasNext)
+//            def mkIntentMatch(arg: List[List[NCEntity]]): NCIntentMatch =
+//                new NCIntentMatch:
+//                    override val getIntentId: String = intentRes.intentId
+//                    override val getIntentEntities: List[List[NCEntity]] = 
intentRes.groups.map(_.entities)
+//                    override def getTermEntities(idx: Int): List[NCEntity] = 
intentRes.groups(idx).entities
+//                    override def getTermEntities(termId: String): 
List[NCEntity] =
+//                        intentRes.groups.find(_.termId === termId) match
+//                            case Some(g) => g.entities
+//                            case None => List.empty
+//                    override val getVariant: NCVariant =
+//                        new NCVariant:
+//                            override def getEntities: List[NCEntity] = 
intentRes.variant.entities
+//
+//            val im = mkIntentMatch(intentRes.groups.map(_.entities))
+//            try
+//                if mdl.onMatchedIntent(ctx, im) then
+//                    // This can throw NCIntentSkip exception.
+//                    import NCIntentSolveType.*
+//
+//                    def saveHistory(res: Option[NCResult], im: 
NCIntentMatch): Unit =
+//                        dialog.addMatchedIntent(im, res, ctx)
+//                        conv.getConversation(req.getUserId).addEntities(
+//                            req.getRequestId, 
im.getIntentEntities.flatten.distinct
+//                        )
+//                        logger.info(s"Intent '${intentRes.intentId}' for 
variant #${intentRes.variantIdx + 1} selected as the <|best match|>")
+//
+//                    def executeCallback(in: NCCallbackInput): NCResult =
+//                        var cbRes = intentRes.fn(in)
+//                        // Store winning intent match in the input.
+//                        if cbRes.getIntentId.isEmpty then cbRes = 
NCResult(cbRes.getBody, cbRes.getType, intentRes.intentId)
+//                        cbRes
+//
+//                    def finishSearch(): Unit =
+//                        @volatile var called = false
+//
+//                        def f(args: List[List[NCEntity]]): NCResult =
+//                            if called then E("Callback was already called.")
+//                            called = true
+//
+//                            val reqId = reqIds.synchronized { 
reqIds.getOrElse(key, null) }
+//
+//                            if reqId != ctx.getRequest.getRequestId then 
E("Callback is out of date.")
+//
+//                            typ match
+//                                case SEARCH =>
+//                                    val imNew = mkIntentMatch(args)
+//                                    val cbRes = 
executeCallback(NCCallbackInput(ctx, imNew))
+//                                    dialog.replaceLastItem(imNew, cbRes, ctx)
+//                                    cbRes
+//                                case SEARCH_NO_HISTORY => 
executeCallback(NCCallbackInput(ctx, mkIntentMatch(args)))
+//                                case _ => throw new 
AssertionError(s"Unexpected state: $typ")
+//
+//                        
Loop.finish(IterationResult(Right(CallbackDataImpl(im.getIntentId, 
im.getIntentEntities, f)), im))
+//
+//                    typ match
+//                        case REGULAR =>
+//                            val cbRes = executeCallback(NCCallbackInput(ctx, 
im))
+//                            saveHistory(cbRes.?, im)
+//                            Loop.finish(IterationResult(Left(cbRes), im))
+//                        case SEARCH =>
+//                            saveHistory(None, im)
+//                            finishSearch()
+//                        case SEARCH_NO_HISTORY =>
+//                            finishSearch()
+//                else
+//                    logger.info(s"Model '${ctx.getModelConfig.getId}' 
triggered rematching of intents by intent '${intentRes.intentId}' on variant 
#${intentRes.variantIdx + 1}.")
+//                    Loop.finish()
+//                catch
+//                    case e: NCIntentSkip =>
+//                        // No-op - just skipping this result.
+//                        e.getMessage match
+//                            case s if s != null => logger.info(s"Selected 
intent '${intentRes.intentId}' skipped: $s")
+//                            case _ => logger.info(s"Selected intent 
'${intentRes.intentId}' skipped.")
+//
+//        Loop.result()
 
     /**
       *
@@ -754,56 +754,79 @@ class NCIntentSolverManager(
       * @param ctx
       * @param typ
       */
-    def solve(mdl: NCModel, ctx: NCContext, typ: NCIntentSolveType): 
ResultData =
+    def solve(mdl: NCModel, ctx: NCContext, typ: NCIntentSolveType): 
ResultData[Any] =
         import NCIntentSolveType.REGULAR
+        for (
+            intent <- mdl.getIntents;
+            v <- ctx.getVariants
+        )
+            val i: NCIntent2[_] = intent
 
-        val key = UserModelKey(ctx.getRequest.getUserId, mdl.getConfig.getId)
-        reqIds.synchronized { reqIds.put(key, ctx.getRequest.getRequestId)}
-
-        mdl.onContext(ctx) match
-            case Some(mdlCtxRes) =>
-                if typ != REGULAR then E("'onContext()' method is overridden, 
intents cannot be found.")
-                if intents.nonEmpty then logger.warn("'onContext()' method 
overrides existing intents - they are ignored.")
-
-                Left(mdlCtxRes)
-            case None =>
-                if intents.isEmpty then
-                    throw NCRejection("There are no registered intents and 
model's 'onContext()' method returns 'null' result.")
-
-                var loopRes: IterationResult = null
+            i.tryMatch(ctx, v) match
+                case Some(data) =>
+                    typ match
+                        case REGULAR => return Left(i.mkResult(ctx, data))
+                        case _ =>  return Right(new NCMatchedCallback2[Any]():
+                            override def getIntent: NCIntent2[Any] = 
i.asInstanceOf[NCIntent2[Any]]
+                            override def getContext: NCContext = ctx
+                            override def getIntentArgument: Any = 
data.asInstanceOf[Any]
+                        )
 
-                try
-                    while (loopRes == null)
-                        solveIteration(mdl, ctx, typ, key) match
-                            case Some(iterRes) => loopRes = iterRes
-                            case None => // No-op.
+                case None => // No-op.
 
-                    typ match
-                        case REGULAR =>
-                            mdl.onResult(ctx, loopRes.intentMatch, 
loopRes.result.swap.toOption.get) match
-                                case Some(mdlRes) => Left(mdlRes)
-                                case None => loopRes.result
-
-                        case _ => loopRes.result
-                catch
-                    case e: NCRejection =>
-                        typ match
-                            case REGULAR =>
-                                mdl.onRejection(ctx, Option.when(loopRes != 
null)(loopRes.intentMatch), e) match
-                                    case Some(mdlRejRes) => Left(mdlRejRes)
-                                    case None => throw e
-
-                            case _ => throw e
-
-                    case e: Throwable =>
-                        typ match
-                            case REGULAR =>
-                                mdl.onError(ctx, e) match
-                                    case Some(mdlErrRes) =>
-                                        logger.warn("Error during execution.", 
e)
-                                        Left(mdlErrRes)
-                                    case None => throw e
-                            case _ => throw e
+        throw new NCRejection("Error")
+
+
+
+//        import NCIntentSolveType.REGULAR
+//
+//        val key = UserModelKey(ctx.getRequest.getUserId, mdl.getConfig.getId)
+//        reqIds.synchronized { reqIds.put(key, ctx.getRequest.getRequestId)}
+//
+//        mdl.onContext(ctx) match
+//            case Some(mdlCtxRes) =>
+//                if typ != REGULAR then E("'onContext()' method is 
overridden, intents cannot be found.")
+//                if intents.nonEmpty then logger.warn("'onContext()' method 
overrides existing intents - they are ignored.")
+//
+//                Left(mdlCtxRes)
+//            case None =>
+//                if intents.isEmpty then
+//                    throw NCRejection("There are no registered intents and 
model's 'onContext()' method returns 'null' result.")
+//
+//                var loopRes: IterationResult = null
+//
+//                try
+//                    while (loopRes == null)
+//                        solveIteration(mdl, ctx, typ, key) match
+//                            case Some(iterRes) => loopRes = iterRes
+//                            case None => // No-op.
+//
+//                    typ match
+//                        case REGULAR =>
+//                            mdl.onResult(ctx, loopRes.intentMatch, 
loopRes.result.swap.toOption.get) match
+//                                case Some(mdlRes) => Left(mdlRes)
+//                                case None => loopRes.result
+//
+//                        case _ => loopRes.result
+//                catch
+//                    case e: NCRejection =>
+//                        typ match
+//                            case REGULAR =>
+//                                mdl.onRejection(ctx, Option.when(loopRes != 
null)(loopRes.intentMatch), e) match
+//                                    case Some(mdlRejRes) => Left(mdlRejRes)
+//                                    case None => throw e
+//
+//                            case _ => throw e
+//
+//                    case e: Throwable =>
+//                        typ match
+//                            case REGULAR =>
+//                                mdl.onError(ctx, e) match
+//                                    case Some(mdlErrRes) =>
+//                                        logger.warn("Error during 
execution.", e)
+//                                        Left(mdlErrRes)
+//                                    case None => throw e
+//                            case _ => throw e
     /**
       *
       */
diff --git 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec.scala
 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec.scala
index ddbb5ebf..d540faf0 100644
--- 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec.scala
+++ 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec.scala
@@ -29,49 +29,50 @@ import scala.util.Using
   *
   */
 class NCModelClientSpec extends AnyFunSuite:
-    /**
-      *
-      * @param e
-      */
-    private def s(e: NCEntity): String =
-        s"Entity [id=${e.getId}, text=${e.mkText}, 
properties={${e.keysSet.map(k => s"$k=${e(k)}")}}]"
-
-    /**
-      *
-      * @param mdl
-      */
-    private def test0(mdl: NCTestModelAdapter): Unit =
-        mdl.pipeline.entParsers += 
NCTestUtils.mkEnSemanticParser("models/lightswitch_model.yaml")
-
-        Using.resource(new NCModelClient(mdl)) { client =>
-            val res = client.ask("Lights on at second floor kitchen", "userId")
-
-            println(s"Intent: ${res.getIntentId}")
-            println(s"Body: ${res.getBody}")
-
-            val winner = client.debugAsk("Lights on at second floor kitchen", 
"userId", true)
-            println(s"Winner intent: ${winner.getIntentId}")
-            println("Entities: \n" + winner.getCallbackArguments.map(p => 
p.map(s).mkString(", ")).mkString("\n"))
-        }
-
-    /**
-      *
-      */
-    test("test 1") {
-        test0(
-            new NCTestModelAdapter():
-                @NCIntent("intent=ls term(act)={# == 'ls:on'} term(loc)={# == 
'ls:loc'}*")
-                def onMatch(ctx: NCContext, im: NCIntentMatch, 
@NCIntentTerm("act") act: NCEntity, @NCIntentTerm("loc") locs: List[NCEntity]): 
NCResult = TEST_RESULT
-        )
-    }
-
-    /**
-      * 
-      */
-    test("test 2") {
-        test0(
-            new NCTestModelAdapter():
-                @NCIntent("intent=ls term(act)={has(ent_groups, 'act')} 
term(loc)={# == 'ls:loc'}*")
-                def onMatch(ctx: NCContext, im: NCIntentMatch, 
@NCIntentTerm("act") act: NCEntity, @NCIntentTerm("loc") locs: List[NCEntity]): 
NCResult = TEST_RESULT
-        )
-    }
+    {}  // TODO:
+//    /**
+//      *
+//      * @param e
+//      */
+//    private def s(e: NCEntity): String =
+//        s"Entity [id=${e.getId}, text=${e.mkText}, 
properties={${e.keysSet.map(k => s"$k=${e(k)}")}}]"
+//
+//    /**
+//      *
+//      * @param mdl
+//      */
+//    private def test0(mdl: NCTestModelAdapter): Unit =
+//        mdl.pipeline.entParsers += 
NCTestUtils.mkEnSemanticParser("models/lightswitch_model.yaml")
+//
+//        Using.resource(new NCModelClient(mdl)) { client =>
+//            val res = client.ask("Lights on at second floor kitchen", 
"userId")
+//
+//            println(s"Intent: ${res.getIntentId}")
+//            println(s"Body: ${res.getBody}")
+//
+//            val winner = client.debugAsk("Lights on at second floor 
kitchen", "userId", true)
+//            println(s"Winner intent: ${winner.getIntentId}")
+//            println("Entities: \n" + winner.getCallbackArguments.map(p => 
p.map(s).mkString(", ")).mkString("\n"))
+//        }
+//
+//    /**
+//      *
+//      */
+//    test("test 1") {
+//        test0(
+//            new NCTestModelAdapter():
+//                @NCIntent("intent=ls term(act)={# == 'ls:on'} term(loc)={# 
== 'ls:loc'}*")
+//                def onMatch(ctx: NCContext, im: NCIntentMatch, 
@NCIntentTerm("act") act: NCEntity, @NCIntentTerm("loc") locs: List[NCEntity]): 
NCResult = TEST_RESULT
+//        )
+//    }
+//
+//    /**
+//      *
+//      */
+//    test("test 2") {
+//        test0(
+//            new NCTestModelAdapter():
+//                @NCIntent("intent=ls term(act)={has(ent_groups, 'act')} 
term(loc)={# == 'ls:loc'}*")
+//                def onMatch(ctx: NCContext, im: NCIntentMatch, 
@NCIntentTerm("act") act: NCEntity, @NCIntentTerm("loc") locs: List[NCEntity]): 
NCResult = TEST_RESULT
+//        )
+//    }
diff --git 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec2.scala
 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec2.scala
index bb24a3ef..6238fb93 100644
--- 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec2.scala
+++ 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec2.scala
@@ -31,67 +31,68 @@ import scala.util.Using
   * 
   */
 class NCModelClientSpec2 extends AnyFunSuite:
-    test("test") {
-        val mdl = new NCTestModelAdapter:
-            import NCSemanticTestElement as TE
-            override val getPipeline =
-                val pl = mkEnPipeline(TE("e1"), TE("e2"))
-                pl.tokEnrichers += EN_TOK_LEMMA_POS_ENRICHER
-                pl
-
-            @NCIntent("intent=i1 term(t1)={# == 'e1'} term(t2List)={# == 
'e2'}*")
-            def onMatch(ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("t1") 
act: NCEntity, @NCIntentTerm("t2List") locs: List[NCEntity]): NCResult =
-                E("Shouldn't be called.")
-
-        Using.resource(new NCModelClient(mdl)) { client =>
-            case class Result(txt: String):
-                private val wi = client.debugAsk(txt, "userId", true)
-                private val allArgs: List[List[NCEntity]] = 
wi.getCallbackArguments
-
-                val intentId: String = wi.getIntentId
-                val size: Int = allArgs.size
-
-                lazy val first: Seq[NCEntity] = allArgs.head
-                lazy val second: Seq[NCEntity] = allArgs.last
-
-                // 1. One argument.
-            var res = Result("e1")
-
-            require(res.intentId == "i1")
-            require(res.size == 2)
-
-            def check(e: NCEntity, txt: String): Unit =
-                require(e.mkText == txt)
-                // All data aren't lost.
-                require(e.getTokens.head.keysSet.contains("lemma"))
-
-            require(res.first.size == 1)
-            check(res.first.head, "e1")
-
-            require(res.second.isEmpty)
-
-            // 2. One argument.
-            res = Result("e1 e2 e2")
-
-            require(res.intentId == "i1")
-            require(res.size == 2)
-
-            require(res.first.size == 1)
-            check(res.first.head, "e1")
-
-            require(res.second.size == 2)
-            check(res.second.head, "e2")
-            check(res.second.last, "e2")
-
-            // 3. No winners.
-            try
-                client.debugAsk("x", "userId", false)
-
-                require(false)
-            catch
-                case e: NCRejection => println(s"Expected rejection: 
${e.getMessage}")
-                case e: Throwable => throw e
-        }
-    }
-
-
+    {}  // TODO:
+//    test("test") {
+//        val mdl = new NCTestModelAdapter:
+//            import NCSemanticTestElement as TE
+//            override val getPipeline =
+//                val pl = mkEnPipeline(TE("e1"), TE("e2"))
+//                pl.tokEnrichers += EN_TOK_LEMMA_POS_ENRICHER
+//                pl
+//
+//            @NCIntent("intent=i1 term(t1)={# == 'e1'} term(t2List)={# == 
'e2'}*")
+//            def onMatch(ctx: NCContext, im: NCIntentMatch, 
@NCIntentTerm("t1") act: NCEntity, @NCIntentTerm("t2List") locs: 
List[NCEntity]): NCResult =
+//                E("Shouldn't be called.")
+//
+//        Using.resource(new NCModelClient(mdl)) { client =>
+//            case class Result(txt: String):
+//                private val wi = client.debugAsk(txt, "userId", true)
+//                private val allArgs: List[List[NCEntity]] = 
wi.getCallbackArguments
+//
+//                val intentId: String = wi.getIntentId
+//                val size: Int = allArgs.size
+//
+//                lazy val first: Seq[NCEntity] = allArgs.head
+//                lazy val second: Seq[NCEntity] = allArgs.last
+//
+//                // 1. One argument.
+//            var res = Result("e1")
+//
+//            require(res.intentId == "i1")
+//            require(res.size == 2)
+//
+//            def check(e: NCEntity, txt: String): Unit =
+//                require(e.mkText == txt)
+//                // All data aren't lost.
+//                require(e.getTokens.head.keysSet.contains("lemma"))
+//
+//            require(res.first.size == 1)
+//            check(res.first.head, "e1")
+//
+//            require(res.second.isEmpty)
+//
+//            // 2. One argument.
+//            res = Result("e1 e2 e2")
+//
+//            require(res.intentId == "i1")
+//            require(res.size == 2)
+//
+//            require(res.first.size == 1)
+//            check(res.first.head, "e1")
+//
+//            require(res.second.size == 2)
+//            check(res.second.head, "e2")
+//            check(res.second.last, "e2")
+//
+//            // 3. No winners.
+//            try
+//                client.debugAsk("x", "userId", false)
+//
+//                require(false)
+//            catch
+//                case e: NCRejection => println(s"Expected rejection: 
${e.getMessage}")
+//                case e: Throwable => throw e
+//        }
+//    }
+//
+//
diff --git 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec3.scala
 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec3.scala
index c108693c..8214e5bf 100644
--- 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec3.scala
+++ 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec3.scala
@@ -31,34 +31,35 @@ import scala.util.Using
   * 
   */
 class NCModelClientSpec3 extends AnyFunSuite:
-    test("test") {
-        import NCSemanticTestElement as TE
-
-        val mdl: NCTestModelAdapter = new NCTestModelAdapter :
-            override val getPipeline: NCPipeline = mkEnPipeline(TE("e1"))
-
-            @NCIntent("intent=i1 term(t1)={# == 'e1'}")
-            def onMatch(ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("t1") 
t1: NCEntity): NCResult = TEST_RESULT
-
-        Using.resource(new NCModelClient(mdl)) { client =>
-            def ask(): NCMatchedCallback = client.debugAsk("e1", "userId", 
true)
-            def execCallback(cb: NCMatchedCallback): NCResult = 
cb.getCallback.apply(cb.getCallbackArguments)
-            def execCallbackOk(cb: NCMatchedCallback): Unit = 
println(s"Result: ${execCallback(cb).getBody}")
-            def execCallbackFail(cb: NCMatchedCallback): Unit =
-                try execCallback(cb)
-                catch case e: NCException => println(s"Expected error: 
${e.getMessage}")
-
-            var cbData = ask()
-            execCallbackOk(cbData)
-            execCallbackFail(cbData) // It cannot be called again (Error is 
'Callback was already called.')
-
-            cbData = ask()
-            execCallbackOk(cbData)
-
-            cbData = ask()
-            ask()
-            execCallbackFail(cbData) // Cannot be called, because there are 
new requests  (Error is 'Callback is out of date.')
-        }
-    }
+    {}  // TODO:
+//    test("test") {
+//        import NCSemanticTestElement as TE
+//
+//        val mdl: NCTestModelAdapter = new NCTestModelAdapter :
+//            override val getPipeline: NCPipeline = mkEnPipeline(TE("e1"))
+//
+//            @NCIntent("intent=i1 term(t1)={# == 'e1'}")
+//            def onMatch(ctx: NCContext, im: NCIntentMatch, 
@NCIntentTerm("t1") t1: NCEntity): NCResult = TEST_RESULT
+//
+//        Using.resource(new NCModelClient(mdl)) { client =>
+//            def ask(): NCMatchedCallback = client.debugAsk("e1", "userId", 
true)
+//            def execCallback(cb: NCMatchedCallback): NCResult = 
cb.getCallback.apply(cb.getCallbackArguments)
+//            def execCallbackOk(cb: NCMatchedCallback): Unit = 
println(s"Result: ${execCallback(cb).getBody}")
+//            def execCallbackFail(cb: NCMatchedCallback): Unit =
+//                try execCallback(cb)
+//                catch case e: NCException => println(s"Expected error: 
${e.getMessage}")
+//
+//            var cbData = ask()
+//            execCallbackOk(cbData)
+//            execCallbackFail(cbData) // It cannot be called again (Error is 
'Callback was already called.')
+//
+//            cbData = ask()
+//            execCallbackOk(cbData)
+//
+//            cbData = ask()
+//            ask()
+//            execCallbackFail(cbData) // Cannot be called, because there are 
new requests  (Error is 'Callback is out of date.')
+//        }
+//    }
 
 
diff --git 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/nlp/test/TimeTestModel.scala 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/nlp/test/TimeTestModel.scala
new file mode 100644
index 00000000..1d68127e
--- /dev/null
+++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/nlp/test/TimeTestModel.scala
@@ -0,0 +1,31 @@
+package org.apache.nlpcraft.nlp.test
+
+import org.apache.nlpcraft.*
+import org.apache.nlpcraft.nlp.parsers.NCOpenNLPEntityParser
+import org.apache.nlpcraft.internal.util.NCResourceReader
+
+
+class TimeTestModel extends NCModel(
+    NCModelConfig("nlpcraft.test.ex", "Test", "1.0"),
+    pipeline = new NCPipelineBuilder().
+        withSemantic("en", "time_model.yaml").
+        
withEntityParser(NCOpenNLPEntityParser(NCResourceReader.getPath("opennlp/en-ner-location.bin"))).
+        build
+):
+    case class TimeData()
+    case class CityTimeData(city: String)
+
+    // Add this method to API.
+    override def getIntents: List[NCIntent2[_]] = List(
+        new NCIntent2[TimeData] :
+            override def tryMatch(ctx: NCContext, v: NCVariant): 
Option[TimeData] =
+                Option.when(v.getEntities.length == 1 && 
v.getEntities.count(_.getId == "x:time") == 1)(TimeData())
+            override def mkResult(ctx: NCContext, data: TimeData): NCResult = 
NCResult("Asked for local") // TBI.
+        ,
+        new NCIntent2[CityTimeData] :
+            override def tryMatch(ctx: NCContext, v: NCVariant): 
Option[CityTimeData] =
+                val cities = v.getEntities.filter(_.getId == 
"opennlp:location")
+                val times = v.getEntities.filter(_.getId == "x:time")
+                Option.when(v.getEntities.length == 2 && cities.length == 1 && 
times.length == 1)(CityTimeData(cities.head.mkText))
+            override def mkResult(ctx: NCContext, data: CityTimeData): 
NCResult = NCResult(s"Asked for ${data.city}") // TBI.
+    )
diff --git 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/nlp/test/TimeTestModelSpec.scala 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/nlp/test/TimeTestModelSpec.scala
new file mode 100644
index 00000000..af54adfb
--- /dev/null
+++ 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/nlp/test/TimeTestModelSpec.scala
@@ -0,0 +1,62 @@
+/*
+ * 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.nlp.test
+
+import org.apache.nlpcraft.*
+import org.scalatest.funsuite.AnyFunSuite
+
+import scala.util.Using
+
+/**
+  * Model validation.
+  */
+class TimeTestModelSpec extends AnyFunSuite:
+    test("test") {
+        Using.resource(new NCModelClient(new TimeTestModel())) { client =>
+            def check(txt: String, intentId: String): Unit =
+                System.out.println(s"Body: ${client.ask(txt, 
"userId").getBody}")
+
+            check("What time is it now in New York City?", "intent2")
+            check("What's the current time in Moscow?", "intent2")
+            check("Show me time of the day in London.", "intent2")
+            check("Can you please give me the Tokyo's current date and time.", 
"intent2")
+
+            check("What's the local time?", "intent1")
+        }
+    }
+
+    test("test2") {
+        Using.resource(new NCModelClient(new TimeTestModel())) { client =>
+            def check(txt: String, intentId: String): Unit =
+                val intent = client.debugAsk(txt, "userId", true)
+
+                System.out.println(s"Argument: ${intent.getIntentArgument}")
+
+                val res = intent.getIntent.mkResult(intent.getContext, 
intent.getIntentArgument)
+
+                System.out.println(s"Body: ${res.getBody}")
+
+
+            check("What time is it now in New York City?", "intent2")
+            check("What's the current time in Moscow?", "intent2")
+            check("Show me time of the day in London.", "intent2")
+            check("Can you please give me the Tokyo's current date and time.", 
"intent2")
+
+            check("What's the local time?", "intent1")
+        }
+    }


Reply via email to