This is an automated email from the ASF dual-hosted git repository. sergeykamov pushed a commit to branch NLPCRAFT-490 in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git
commit 8f100a4dc26ac41d17e71f49e42d395a9e7fdee9 Author: Sergey Kamov <[email protected]> AuthorDate: Tue Mar 29 13:30:02 2022 +0300 WIP. --- .../scala/org/apache/nlpcraft/NCModelClient.java | 17 +++- .../scala/org/apache/nlpcraft/NCWinnerIntent.java | 29 +++++++ .../nlpcraft/internal/impl/NCModelClientImpl.scala | 11 ++- .../intent/matcher/NCIntentSolverManager.scala | 87 ++++++++++++-------- .../nlpcraft/internal/impl/NCModelClientSpec.scala | 5 +- .../internal/impl/NCModelClientSpec2.scala | 96 ++++++++++++++++++++++ 6 files changed, 200 insertions(+), 45 deletions(-) diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java index 7b98fd9..a0a6ba0 100644 --- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java +++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCModelClient.java @@ -20,7 +20,6 @@ package org.apache.nlpcraft; import org.apache.nlpcraft.internal.impl.NCModelClientImpl; import java.util.Map; -import java.util.List; import java.util.function.Predicate; /** @@ -100,7 +99,19 @@ public class NCModelClient implements AutoCloseable { impl.validateSamples(); } - public List<List<NCEntity>> validateIntentArguments(String txt, Map<String, Object> data, String usrId) { - return impl.validateIntentArguments(txt, data, usrId); + /** + * TODO: + * Gets intent information which contains intent ID and its callback arguments entities. + * Note that + * - Callback is not called in this case. + * - if model `onContext` method overrided - error thrown because we don't find intents in this case. + * + * @param txt + * @param data + * @param usrId + * @return + */ + public NCWinnerIntent getWinnerIntent(String txt, Map<String, Object> data, String usrId) { + return impl.getWinnerIntent(txt, data, usrId); } } diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCWinnerIntent.java b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCWinnerIntent.java new file mode 100644 index 0000000..56baca7 --- /dev/null +++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCWinnerIntent.java @@ -0,0 +1,29 @@ +/* + * 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 java.util.List; +import java.util.Map; + +/** + * TODO: + */ +public interface NCWinnerIntent { + String getIntentId(); + List<List<NCEntity>> getArguments(); +} 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 83cb579..7c605b5 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 @@ -75,7 +75,7 @@ class NCModelClientImpl(mdl: NCModel) extends LazyLogging: dlgMgr.start() plMgr.start() - private def ask0(txt: String, data: JMap[String, AnyRef], usrId: String, isValidation: Boolean): NCResult = + private def ask0(txt: String, data: JMap[String, AnyRef], usrId: String, testRun: Boolean): Either[NCResult, NCWinnerIntent] = val plData = plMgr.prepare(txt, data, usrId) val userId = plData.request.getUserId @@ -99,8 +99,7 @@ class NCModelClientImpl(mdl: NCModel) extends LazyLogging: override val getVariants: util.Collection[NCVariant] = plData.variants.asJava override val getTokens: JList[NCToken] = plData.tokens - intentsMgr.solve(mdl, ctx, isValidation) - + intentsMgr.solve(mdl, ctx, testRun) /* * @param txt @@ -108,7 +107,7 @@ class NCModelClientImpl(mdl: NCModel) extends LazyLogging: * @param usrId * @return */ - def ask(txt: String, data: JMap[String, AnyRef], usrId: String): NCResult = ask0(txt, data, usrId, false) + def ask(txt: String, data: JMap[String, AnyRef], usrId: String): NCResult = ask0(txt, data, usrId, false).swap.toOption.get /** * @@ -191,5 +190,5 @@ class NCModelClientImpl(mdl: NCModel) extends LazyLogging: dlgMgr.close() convMgr.close() - def validateIntentArguments(txt: String, data: JMap[String, AnyRef], usrId: String): JList[JList[NCEntity]] = - ask0(txt, data, usrId, true).getBody.asInstanceOf[JList[JList[NCEntity]]] \ No newline at end of file + def getWinnerIntent(txt: String, data: JMap[String, AnyRef], usrId: String): NCWinnerIntent = + ask0(txt, data, usrId, true).toOption.get \ 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 28fd705..b46f38f 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 @@ -69,6 +69,7 @@ object NCIntentSolverManager: * @param entities */ private case class IntentTermEntities(termId: Option[String], entities: Seq[NCEntity]) + private case class NCWinnerIntentImpl(getIntentId: String, getArguments: JList[JList[NCEntity]]) extends NCWinnerIntent /** * @@ -162,12 +163,14 @@ object NCIntentSolverManager: */ private case class IntentEntity(var used: Boolean, var conv: Boolean, entity: NCEntity) + type ResultData = Either[NCResult, NCWinnerIntent] + /** * * @param result * @param intentMatch */ - private case class IterationResult(result: NCResult, intentMatch: NCIntentMatch) + private case class IterationResult(result: ResultData, intentMatch: NCIntentMatch) /** * @param termId @@ -612,7 +615,7 @@ class NCIntentSolverManager(dialog: NCDialogFlowManager, intents: Map[NCIDLInten Option.when(depth >= 0)(depth + 1) val convDepthsSum = -usedEnts.flatMap(getConversationDepth).sum - + // Mark found entities as used. for (e <- usedEnts) e.used = true @@ -622,10 +625,10 @@ class NCIntentSolverManager(dialog: NCDialogFlowManager, intents: Map[NCIDLInten * * @param mdl * @param ctx - * @param isValidation + * @param testRun * @return */ - private def solveIteration(mdl: NCModel, ctx: NCContext, isValidation: Boolean): Option[IterationResult] = + private def solveIteration(mdl: NCModel, ctx: NCContext, testRun: Boolean): Option[IterationResult] = require(intents.nonEmpty) val req = ctx.getRequest @@ -637,17 +640,18 @@ class NCIntentSolverManager(dialog: NCDialogFlowManager, intents: Map[NCIDLInten if intentResults.isEmpty then throw new NCRejection("No matching intent found.") object Loop: - private var data: Option[Option[IterationResult]] = None - private var stopped: Boolean = false + private var data: Option[IterationResult] = _ - def hasNext: Boolean = !stopped + def hasNext: Boolean = data == null def finish(data: Option[IterationResult] = None): Unit = - Loop.data = Option(data) - Loop.stopped = true - def result: Option[IterationResult] = data.getOrElse(throw new NCRejection("No matching intent found - all intents were skipped.")) + require(data != null) + Loop.data = data + 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) - val intentMatch: NCIntentMatch = + val im: NCIntentMatch = new NCIntentMatch: override val getContext: NCContext = ctx override val getIntentId: String = intentRes.intentId @@ -661,15 +665,18 @@ class NCIntentSolverManager(dialog: NCDialogFlowManager, intents: Map[NCIDLInten new NCVariant: override def getEntities: JList[NCEntity] = intentRes.variant.entities.asJava try - if mdl.onMatchedIntent(intentMatch) then + if mdl.onMatchedIntent(im) then // This can throw NCIntentSkip exception. - val cbRes = if isValidation then new NCResult(intentMatch.getIntentEntities, NCResultType.ASK_RESULT) else intentRes.fn(intentMatch) - // Store won 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|>") - dialog.addMatchedIntent(intentMatch, cbRes, ctx) - Loop.finish(Option(IterationResult(cbRes, intentMatch))) + if testRun then + Loop.finish(Option(IterationResult(Right(NCWinnerIntentImpl(im.getIntentId, im.getIntentEntities)), im))) + else + val cbRes = intentRes.fn(im) + // Store won 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|>") + dialog.addMatchedIntent(im, cbRes, ctx) + Loop.finish(Option(IterationResult(Left(cbRes), im))) else logger.info(s"Model '${ctx.getModelConfig.getId}' triggered rematching of intents by intent '${intentRes.intentId}' on variant #${intentRes.variantIdx + 1}.") Loop.finish() @@ -686,17 +693,17 @@ class NCIntentSolverManager(dialog: NCDialogFlowManager, intents: Map[NCIDLInten * * @param mdl * @param ctx - * @param isValidation + * @param testRun * @return */ - def solve(mdl: NCModel, ctx: NCContext, isValidation: Boolean): NCResult = - var res: NCResult = mdl.onContext(ctx) + def solve(mdl: NCModel, ctx: NCContext, testRun: Boolean): ResultData = + val ctxRes = mdl.onContext(ctx) - if res != null then - // TODO: text. - if intents.nonEmpty then logger.warn("`onContext` method overrides existed intents. They are ignored.") + if ctxRes != null then + if testRun then E("`onContext` method overriden, intents cannot be found.") // TODO: test + if intents.nonEmpty then logger.warn("`onContext` method overrides existed intents. They are ignored.") // TODO: text. - res + Left(ctxRes) else if intents.isEmpty then // TODO: text. @@ -706,17 +713,29 @@ class NCIntentSolverManager(dialog: NCDialogFlowManager, intents: Map[NCIDLInten try while (loopRes == null) - solveIteration(mdl, ctx, isValidation) match + solveIteration(mdl, ctx, testRun) match case Some(iterRes) => loopRes = iterRes case None => // No-op. - res = mdl.onResult(loopRes.intentMatch, loopRes.result) - - if res != null then res else loopRes.result + if testRun then + loopRes.result + else + mdl.onResult(loopRes.intentMatch, loopRes.result.swap.toOption.get) match + case null => loopRes.result + case res => Left(res) catch case e: NCRejection => - val res = mdl.onRejection(if loopRes != null then loopRes.intentMatch else null, e) - if res != null then res else throw e + if testRun then throw e + + mdl.onRejection(if loopRes != null then loopRes.intentMatch else null, e) match + case null => throw e + case res => Left(res) case e: Throwable => - val res = mdl.onError(ctx, e) - if res != null then res else throw e \ No newline at end of file + if testRun then throw e + + mdl.onError(ctx, e) match + case null => throw e + case res => + logger.warn("Error during execution.", e) + + Left(res) 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 9168b8b..f50fb14 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 @@ -40,9 +40,10 @@ class NCModelClientSpec: println(s"Body: ${res.getBody}") client.validateSamples() - val entities = client.validateIntentArguments("Lights on at second floor kitchen", null, "userId") - println("Entities: \n" + entities.asScala.map(p => p.asScala.map(s).mkString(", ")).mkString("\n")) + val winner = client.getWinnerIntent("Lights on at second floor kitchen", null, "userId") + println(s"Winner intent: ${winner.getIntentId}") + println("Entities: \n" + winner.getArguments.asScala.map(p => p.asScala.map(s).mkString(", ")).mkString("\n")) } /** * 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 new file mode 100644 index 0000000..99e5d3b --- /dev/null +++ b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/impl/NCModelClientSpec2.scala @@ -0,0 +1,96 @@ +/* + * 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 NCModelClientSpec2: + @Test + def test(): Unit = + import NCSemanticTestElement as TE + + val mdl = new NCModel: + override def getConfig: NCModelConfig = CFG + override def getPipeline: NCPipeline = new NCPipelineBuilder().withSemantic("en", Seq(TE("e1"), TE("e2")).asJava).build() + + @NCIntent("intent=i1 term(t1)={# == 'e1'} term(t2List)={# == 'e2'}*") + def onMatch(@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.getWinnerIntent(txt, null, "userId") + private val allArgs: JList[JList[NCEntity]] = wi.getArguments + + val intentId: String = wi.getIntentId + val size: Int = allArgs.size() + + lazy val first: Seq[NCEntity] = allArgs.asScala.head.asScala.toSeq + lazy val second: Seq[NCEntity] = allArgs.asScala.last.asScala.toSeq + + // 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.get(0).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.getWinnerIntent("x", null, "userId") + + require(false) + catch + case e: NCRejection => println(s"Expected rejection: ${e.getMessage}") + case e: Throwable => throw e + } + + +
