You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nlpcraft.apache.org by se...@apache.org on 2022/03/31 09:06:54 UTC

[incubator-nlpcraft] branch NLPCRAFT-490-1 created (now e030277)

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

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


      at e030277  WIP.

This branch includes the following new commits:

     new e030277  WIP.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[incubator-nlpcraft] 01/01: WIP.

Posted by se...@apache.org.
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 <sk...@gmail.com>
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.')
+        }
+
+
+