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") + } + }
