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.') + } + + +
