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

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

commit e03027781a40ac0ad85ba6d75770be3be5f084d4
Author: Sergey Kamov <[email protected]>
AuthorDate: Thu Mar 31 12:06:48 2022 +0300

    WIP.
---
 .../scala/org/apache/nlpcraft/NCCallbackData.java  |  7 ++
 .../scala/org/apache/nlpcraft/NCModelClient.java   |  7 ++
 .../internal/dialogflow/NCDialogFlowManager.scala  | 38 +++++++++-
 .../nlpcraft/internal/impl/NCModelClientImpl.scala |  1 +
 .../intent/matcher/NCIntentSolverManager.scala     | 86 +++++++++++++++++-----
 .../internal/impl/NCModelClientSpec3.scala         | 73 ++++++++++++++++++
 6 files changed, 188 insertions(+), 24 deletions(-)

diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCCallbackData.java 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCCallbackData.java
index 96941ff..d6a3db2 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCCallbackData.java
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCCallbackData.java
@@ -18,6 +18,7 @@
 package org.apache.nlpcraft;
 
 import java.util.List;
+import java.util.function.Function;
 
 /**
  *
@@ -34,4 +35,10 @@ public interface NCCallbackData {
      * @return
      */
     List<List<NCEntity>> getCallbackArguments();
+
+    /**
+     *
+     * @return
+     */
+    Function<List<List<NCEntity>>, NCResult> getCallback();
 }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java
index 6b8ca1f..f625233 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java
@@ -106,6 +106,13 @@ public class NCModelClient implements AutoCloseable {
      *  - Callback is not called in this case.
      *  - if model `onContext` method overrided - error thrown because we 
don't find intents in this case.
      *
+     *  Callback.
+     *   - You can call callback only one time.
+     *   - You can't call callback if it is not last request.
+     *   - if you call callback and 'saveHistory' flag was true - dialog 
overriden by callback result instead of saved before empty result.
+     *   - if you call callback and 'saveHistory' flag was false - history 
data is still ignored.
+     *   - No matter of callback execution time - history data based on 
request timestamp.
+     *
      * @param txt
      * @param data
      * @param usrId
diff --git 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManager.scala
 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManager.scala
index 4c76db8..20b3f9e 100644
--- 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManager.scala
+++ 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManager.scala
@@ -63,6 +63,19 @@ class NCDialogFlowManager(cfg: NCModelConfig) extends 
LazyLogging:
 
     /**
       *
+      * @param intentMatch
+      * @param res
+      * @param ctx
+      * @return
+      */
+    private def mkItem(intentMatch: NCIntentMatch, res: NCResult, ctx: 
NCContext): NCDialogFlowItem =
+        new NCDialogFlowItem:
+            override val getIntentMatch: NCIntentMatch = intentMatch
+            override val getRequest: NCRequest = ctx.getRequest
+            override val getResult: NCResult = res
+
+    /**
+      *
       * @return
       */
     def start(): Unit =
@@ -98,10 +111,7 @@ class NCDialogFlowManager(cfg: NCModelConfig) extends 
LazyLogging:
       * @param ctx Original query context.
       */
     def addMatchedIntent(intentMatch: NCIntentMatch, res: NCResult, ctx: 
NCContext): Unit =
-        val item: NCDialogFlowItem = new NCDialogFlowItem:
-            override val getIntentMatch: NCIntentMatch = intentMatch
-            override val getRequest: NCRequest = ctx.getRequest
-            override val getResult: NCResult = res
+        val item = mkItem(intentMatch, res, ctx)
 
         flow.synchronized {
             flow.getOrElseUpdate(ctx.getRequest.getUserId, 
mutable.ArrayBuffer.empty[NCDialogFlowItem]).append(item)
@@ -109,6 +119,26 @@ class NCDialogFlowManager(cfg: NCModelConfig) extends 
LazyLogging:
         }
 
     /**
+      *
+      * @param intentMatch
+      * @param res
+      * @param ctx
+      */
+    def replaceLastItem(intentMatch: NCIntentMatch, res: NCResult, ctx: 
NCContext): Unit =
+        val item = mkItem(intentMatch, res, ctx)
+
+        flow.synchronized {
+            val buf = flow.getOrElseUpdate(ctx.getRequest.getUserId, 
mutable.ArrayBuffer.empty[NCDialogFlowItem])
+
+            // If buf is empty - it cleared by timer, so there is nothing to 
replace.     
+            if buf.nonEmpty then
+                buf.remove(buf.size - 1)
+                buf.append(item)
+
+            flow.notifyAll()
+        }
+
+    /**
       * Gets sequence of dialog flow items sorted from oldest to newest (i.e. 
dialog flow) for given user ID.
       *
       * @param usrId User ID.
diff --git 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala
 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala
index a417479..4615c0c 100644
--- 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala
+++ 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/impl/NCModelClientImpl.scala
@@ -200,6 +200,7 @@ class NCModelClientImpl(mdl: NCModel) extends LazyLogging:
         plMgr.close()
         dlgMgr.close()
         convMgr.close()
+        intentsMgr.close()
 
     /**
       *
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 95956f7..f1463b9 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
@@ -84,8 +84,13 @@ object NCIntentSolverManager:
       *
       * @param getIntentId
       * @param getCallbackArguments
+      * @param getCallback
       */
-    private case class CallbackDataImpl(getIntentId: String, 
getCallbackArguments: JList[JList[NCEntity]]) extends NCCallbackData
+    private case class CallbackDataImpl(
+        getIntentId: String,
+        getCallbackArguments: JList[JList[NCEntity]],
+        getCallback: Function[JList[JList[NCEntity]], NCResult]
+    ) extends NCCallbackData
 
     /**
       *
@@ -240,6 +245,13 @@ object NCIntentSolverManager:
         variantIdx: Int // Variant index.
     )
 
+    /**
+      *
+      * @param userId
+      * @param mldId
+      */
+    private case class UserModelKey(userId: String, mldId: String)
+
 import org.apache.nlpcraft.internal.intent.matcher.NCIntentSolverManager.*
 
 /**
@@ -250,6 +262,8 @@ class NCIntentSolverManager(
     conv: NCConversationManager,
     intents: Map[NCIDLIntent, NCIntentMatch => NCResult]
 ) extends LazyLogging:
+    private final val reqIds = mutable.HashMap.empty[UserModelKey, String]
+
     /**
      * Main entry point for intent engine.
      *
@@ -644,9 +658,10 @@ class NCIntentSolverManager(
       * @param mdl
       * @param ctx
       * @param typ
+      * @param key
       * @return
       */
-    private def solveIteration(mdl: NCModel, ctx: NCContext, typ: 
NCIntentSolveType): Option[IterationResult] =
+    private def solveIteration(mdl: NCModel, ctx: NCContext, typ: 
NCIntentSolveType, key: UserModelKey): Option[IterationResult] =
         require(intents.nonEmpty)
 
         val req = ctx.getRequest
@@ -668,7 +683,7 @@ class NCIntentSolverManager(
                 data
 
         for (intentRes <- intentResults.filter(_ != null) if Loop.hasNext)
-            val im: NCIntentMatch =
+            def mkIntentMatch(arg: JList[JList[NCEntity]]): NCIntentMatch =
                 new NCIntentMatch:
                     override val getContext: NCContext = ctx
                     override val getIntentId: String = intentRes.intentId
@@ -681,36 +696,59 @@ class NCIntentSolverManager(
                     override val getVariant: NCVariant =
                         new NCVariant:
                             override def getEntities: JList[NCEntity] = 
intentRes.variant.entities.asJava
+
+            val im = 
mkIntentMatch(intentRes.groups.map(_.entities).map(_.asJava).asJava)
             try
                 if mdl.onMatchedIntent(im) then
                     // This can throw NCIntentSkip exception.
                     import NCIntentSolveType.*
 
-                    def saveHistory(res: NCResult): Unit =
+                    def saveHistory(res: NCResult, im: NCIntentMatch): Unit =
                         dialog.addMatchedIntent(im, res, ctx)
                         conv.getConversation(req.getUserId).addEntities(
                             req.getRequestId, 
im.getIntentEntities.asScala.flatMap(_.asScala).toSeq.distinct
                         )
-                    def finishHistory(): Unit =
-                        
Loop.finish(IterationResult(Right(CallbackDataImpl(im.getIntentId, 
im.getIntentEntities)), im))
+                        logger.info(s"Intent '${intentRes.intentId}' for 
variant #${intentRes.variantIdx + 1} selected as the <|best match|>")
+
+                    def execute(im: NCIntentMatch): NCResult =
+                        val cbRes = intentRes.fn(im)
+                        // Store winning intent match in the input.
+                        if cbRes.getIntentId == null then 
cbRes.setIntentId(intentRes.intentId)
+                        cbRes
+
+                    def finishSearch(): Unit =
+                        val cb = new Function[JList[JList[NCEntity]], 
NCResult]:
+                            @volatile private var called = false
+                            override def apply(args: JList[JList[NCEntity]]): 
NCResult =
+                                if called then E("Callback was already 
called.")
+                                called = true
+
+                                val currKey = reqIds.synchronized { 
reqIds.getOrElse(key, null) }
+
+                                // TODO: text.
+                                if currKey != ctx.getRequest.getRequestId then 
E("Callback is out of date.")
+
+                                typ match
+                                    case SEARCH =>
+                                        val imFixed = mkIntentMatch(args)
+                                        val cbRes = execute(imFixed)
+                                        dialog.replaceLastItem(imFixed, cbRes, 
ctx)
+                                        cbRes
+                                    case SEARCH_NO_HISTORY => 
execute(mkIntentMatch(args))
+                                    case _ => throw new 
AssertionError(s"Unexpected state: $typ")
+
+                        
Loop.finish(IterationResult(Right(CallbackDataImpl(im.getIntentId, 
im.getIntentEntities, cb)), im))
 
                     typ match
                         case REGULAR =>
-                            val cbRes = intentRes.fn(im)
-                            // Store winning intent match in the input.
-                            if cbRes.getIntentId == null then
-                                cbRes.setIntentId(intentRes.intentId)
-                            logger.info(s"Intent '${intentRes.intentId}' for 
variant #${intentRes.variantIdx + 1} selected as the <|best match|>")
-                            saveHistory(cbRes)
-
+                            val cbRes = execute(im)
+                            saveHistory(cbRes, im)
                             Loop.finish(IterationResult(Left(cbRes), im))
-
                         case SEARCH =>
-                            saveHistory(new NCResult()) // Added dummy result.
-                            finishHistory()
-
+                            saveHistory(new NCResult(), im) // Added dummy 
result.
+                            finishSearch()
                         case SEARCH_NO_HISTORY =>
-                            finishHistory()
+                            finishSearch()
                 else
                     logger.info(s"Model '${ctx.getModelConfig.getId}' 
triggered rematching of intents by intent '${intentRes.intentId}' on variant 
#${intentRes.variantIdx + 1}.")
                     Loop.finish()
@@ -733,6 +771,9 @@ class NCIntentSolverManager(
     def solve(mdl: NCModel, ctx: NCContext, typ: NCIntentSolveType): 
ResultData =
         import NCIntentSolveType.REGULAR
 
+        val key = UserModelKey(ctx.getRequest.getUserId, mdl.getConfig.getId)
+        reqIds.synchronized { reqIds.put(key, ctx.getRequest.getRequestId)}
+
         val mdlCtxRes = mdl.onContext(ctx)
 
         if mdlCtxRes != null then
@@ -748,7 +789,7 @@ class NCIntentSolverManager(
 
             try
                 while (loopRes == null)
-                    solveIteration(mdl, ctx, typ) match
+                    solveIteration(mdl, ctx, typ, key) match
                         case Some(iterRes) => loopRes = iterRes
                         case None => // No-op.
 
@@ -775,4 +816,9 @@ class NCIntentSolverManager(
                                 case mdlErrRes =>
                                     logger.warn("Error during execution.", e)
                                     Left(mdlErrRes)
-                        case _ => throw e
\ No newline at end of file
+                        case _ => throw e
+
+    /**
+      *
+      */
+    def close(): Unit = reqIds.clear()
\ No newline at end of file
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
new file mode 100644
index 0000000..73bc7f0
--- /dev/null
+++ 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec3.scala
@@ -0,0 +1,73 @@
+/*
+ * 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.internal.impl
+
+import org.apache.nlpcraft.*
+import org.apache.nlpcraft.nlp.entity.parser.*
+import org.apache.nlpcraft.nlp.entity.parser.semantic.*
+import org.apache.nlpcraft.nlp.util.*
+import org.junit.jupiter.api.Test
+
+import java.util
+import java.util.List as JList
+import scala.collection.mutable
+import scala.jdk.CollectionConverters.*
+import scala.util.Using
+
+/**
+  * 
+  */
+class NCModelClientSpec3:
+    @Test
+    def test(): Unit =
+        import NCSemanticTestElement as TE
+        val mdl: NCTestModelAdapter = new NCTestModelAdapter:
+            override val getPipeline: NCPipeline =
+                val pl = mkEnPipeline
+                
pl.getEntityParsers.add(NCTestUtils.mkEnSemanticParser(TE("e1")))
+                pl
+
+            @NCIntent("intent=i1 term(t1)={# == 'e1'}")
+            def onMatch(@NCIntentTerm("t1") t1: NCEntity): NCResult = new 
NCResult("Data", NCResultType.ASK_RESULT)
+
+        Using.resource(new NCModelClient(mdl)) { client =>
+            def ask(): NCCallbackData = client.debugAsk("e1", null, "userId", 
true)
+
+            def execCallbackOk(cb: NCCallbackData): Unit =
+                println(s"Result: 
${cb.getCallback.apply(cb.getCallbackArguments).getBody}")
+
+            def execCallbackFail(cb: NCCallbackData): Unit =
+                try
+                    cb.getCallback.apply(cb.getCallbackArguments)
+                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.')
+        }
+
+
+

Reply via email to