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 2020/08/09 11:40:10 UTC
[incubator-nlpcraft] branch NLPCRAFT-41 updated: WIP.
This is an automated email from the ASF dual-hosted git repository.
sergeykamov pushed a commit to branch NLPCRAFT-41
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git
The following commit(s) were added to refs/heads/NLPCRAFT-41 by this push:
new fca8a2d WIP.
fca8a2d is described below
commit fca8a2d39705509d1e8d7cde5ec1493615eacb34
Author: Sergey Kamov <se...@apache.org>
AuthorDate: Sun Aug 9 14:39:58 2020 +0300
WIP.
---
nlpcraft/src/main/resources/nlpcraft.conf | 3 +
.../model/intent/impl/NCIntentScanner.scala | 66 ++-
.../tools/suggestions/NCSuggestionsGenerator.scala | 455 ---------------------
.../test/impl/NCTestAutoModelValidatorImpl.scala | 97 +----
.../nlpcraft/probe/mgrs/NCModelDecorator.scala | 2 +
.../probe/mgrs/conn/NCConnectionManager.scala | 16 +-
.../probe/mgrs/deploy/NCDeployManager.scala | 40 +-
.../mgrs/deploy/NCModelHolder.scala} | 25 +-
.../nlpcraft/probe/mgrs/model/NCModelManager.scala | 21 +-
.../org/apache/nlpcraft/server/NCServer.scala | 8 +-
.../nlpcraft/server/mdo/NCProbeModelMdo.scala | 5 +-
.../opencensus/NCOpenCensusServerStats.scala | 1 +
.../nlpcraft/server/probe/NCProbeManager.scala | 31 +-
.../nlpcraft/server/rest/NCBasicRestApi.scala | 135 ++++--
.../NCSuggestion.scala} | 32 +-
.../server/suggestion/NCSuggestionsManager.scala | 315 ++++++++++++++
16 files changed, 602 insertions(+), 650 deletions(-)
diff --git a/nlpcraft/src/main/resources/nlpcraft.conf b/nlpcraft/src/main/resources/nlpcraft.conf
index da972c8..9e181a6 100644
--- a/nlpcraft/src/main/resources/nlpcraft.conf
+++ b/nlpcraft/src/main/resources/nlpcraft.conf
@@ -204,6 +204,9 @@ nlpcraft {
# If Spacy is enabled as a token provider (value 'spacy') - defines Spacy proxy URL.
# spacy.proxy.url=http://localhost:5002
+
+ # If ContextWord enricher is enabled as a token provider - defines ctxword-server URL.
+ ctxword.url="http://localhost:5000"
}
# +---------------------+
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala
index ffd8389..507f7ea 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala
@@ -21,6 +21,7 @@ import java.lang.reflect.{InvocationTargetException, Method, ParameterizedType,
import java.util
import java.util.function.Function
+import com.typesafe.scalalogging.LazyLogging
import org.apache.nlpcraft.common._
import org.apache.nlpcraft.model._
import org.apache.nlpcraft.model.intent.utils.NCDslIntent
@@ -31,7 +32,7 @@ import scala.collection._
/**
* Scanner for `NCIntent`, `NCIntentRef` and `NCIntentTerm` annotations.
*/
-object NCIntentScanner {
+object NCIntentScanner extends LazyLogging {
type Callback = Function[NCIntentMatch, NCResult]
private final val CLS_INTENT = classOf[NCIntent]
@@ -39,6 +40,7 @@ object NCIntentScanner {
private final val CLS_TERM = classOf[NCIntentTerm]
private final val CLS_QRY_RES = classOf[NCResult]
private final val CLS_SLV_CTX = classOf[NCIntentMatch]
+ private final val CLS_SAMPLE = classOf[NCIntentSample]
// Java and scala lists.
private final val CLS_SCALA_SEQ = classOf[Seq[_]]
@@ -415,4 +417,66 @@ object NCIntentScanner {
case (intent, m) ⇒ intent → prepareCallback(m, mdl, intent)
}
.toMap
+
+ /**
+ * TODO:
+ * @param mdl
+ */
+ @throws[NCE]
+ def scanIntentsSamples(mdl: NCModel): Map[String, Seq[String]] = {
+ var annFound = false
+
+ val res =
+ mdl.getClass.getDeclaredMethods.flatMap(method ⇒ {
+ def mkMethodName: String = s"${method.getDeclaringClass.getName}#${method.getName}(...)"
+
+ val smpAnn = method.getAnnotation(CLS_SAMPLE)
+ val intAnn = method.getAnnotation(CLS_INTENT)
+ val refAnn = method.getAnnotation(CLS_INTENT_REF)
+
+ if (smpAnn != null || intAnn != null || refAnn != null) {
+ annFound = true
+
+ def mkIntentId(): String =
+ if (intAnn != null)
+ NCIntentDslCompiler.compile(intAnn.value(), mdl.getId).id
+ else if (refAnn != null)
+ refAnn.value().trim
+ else
+ throw new AssertionError()
+
+ if (smpAnn != null) {
+ if (intAnn == null && refAnn == null) {
+ logger.warn(s"@NCTestSample annotation without corresponding @NCIntent or @NCIntentRef annotations " +
+ s"in method (ignoring): $mkMethodName")
+
+ None
+ }
+ else {
+ val samples = smpAnn.value().toList
+
+ if (samples.isEmpty) {
+ logger.warn(s"@NCTestSample annotation is empty in method (ignoring): $mkMethodName")
+
+ None
+ }
+ else
+ Some(mkIntentId() → samples)
+ }
+ }
+ else {
+ logger.warn(s"@NCTestSample annotation is missing in method (ignoring): $mkMethodName")
+
+ None
+ }
+ }
+ else
+ None
+ }).toMap
+
+ if (!annFound)
+ logger.warn(s"Model '${mdl.getId}' doesn't have any intents.")
+
+ res
+ }
}
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/suggestions/NCSuggestionsGenerator.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/suggestions/NCSuggestionsGenerator.scala
deleted file mode 100644
index 945d130..0000000
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/suggestions/NCSuggestionsGenerator.scala
+++ /dev/null
@@ -1,455 +0,0 @@
-/*
- * 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.model.tools.suggestions
-
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.{CopyOnWriteArrayList, CountDownLatch, TimeUnit}
-
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
-import org.apache.http.HttpResponse
-import org.apache.http.client.ResponseHandler
-import org.apache.http.client.methods.HttpPost
-import org.apache.http.entity.StringEntity
-import org.apache.http.impl.client.HttpClients
-import org.apache.http.util.EntityUtils
-import org.apache.nlpcraft.common.ascii.NCAsciiTable
-import org.apache.nlpcraft.common.makro.NCMacroParser
-import org.apache.nlpcraft.common.nlp.core.NCNlpPorterStemmer
-import org.apache.nlpcraft.common.util.NCUtils
-import org.apache.nlpcraft.common.version.NCVersion
-import org.apache.nlpcraft.model.NCModelFileAdapter
-
-import java.util.{List ⇒ JList}
-
-import scala.collection.JavaConverters._
-import scala.collection._
-
-case class ParametersHolder(modelPath: String, url: String, limit: Int, minScore: Double, debug: Boolean)
-
-object NCSuggestionsGeneratorImpl {
- case class Suggestion(word: String, score: Double)
- case class RequestData(sentence: String, example: String, elementId: String, index: Int)
- case class RestRequestSentence(text: String, indexes: JList[Int])
- case class RestRequest(sentences: JList[RestRequestSentence], limit: Int, min_score: Double)
- case class Word(word: String, stem: String) {
- require(!word.contains(" "), s"Word cannot contains spaces: $word")
- require(
- word.forall(ch ⇒
- ch.isLetterOrDigit ||
- ch == '\'' ||
- SEPARATORS.contains(ch)
- ),
- s"Unsupported symbols: $word"
- )
- }
-
- private final val GSON = new Gson
- private final val TYPE_RESP = new TypeToken[JList[JList[Suggestion]]]() {}.getType
- private final val SEPARATORS = Seq('?', ',', '.', '-', '!')
- private final val BATCH_SIZE = 20
-
- private final val HANDLER: ResponseHandler[Seq[Seq[Suggestion]]] =
- (resp: HttpResponse) ⇒ {
- val code = resp.getStatusLine.getStatusCode
- val e = resp.getEntity
-
- val js = if (e != null) EntityUtils.toString(e) else null
-
- if (js == null)
- throw new RuntimeException(s"Unexpected empty response [code=$code]")
-
- code match {
- case 200 ⇒
- val data: JList[JList[Suggestion]] = GSON.fromJson(js, TYPE_RESP)
-
- data.asScala.map(p ⇒ if (p.isEmpty) Seq.empty else p.asScala.tail)
-
- case 400 ⇒ throw new RuntimeException(js)
- case _ ⇒ throw new RuntimeException(s"Unexpected response [code=$code, response=$js]")
- }
- }
-
- private def split(s: String): Seq[String] = s.split(" ").toSeq.map(_.trim).filter(_.nonEmpty)
- private def toStem(s: String): String = split(s).map(NCNlpPorterStemmer.stem).mkString(" ")
- private def toStemWord(s: String): String = NCNlpPorterStemmer.stem(s)
-
- private def getAllSlices(seq1: Seq[String], seq2: Seq[String]): Seq[Int] = {
- val seq = mutable.Buffer.empty[Int]
-
- var i = seq1.indexOfSlice(seq2)
-
- while (i >= 0) {
- seq += i
-
- i = seq1.indexOfSlice(seq2, i + 1)
- }
-
- seq
- }
-
- def process(data: ParametersHolder): Unit = {
- val now = System.currentTimeMillis()
-
- val mdl = new NCModelFileAdapter(data.modelPath) {}
-
- val parser = new NCMacroParser()
-
- if (mdl.getMacros != null)
- mdl.getMacros.asScala.foreach { case (name, str) ⇒ parser.addMacro(name, str) }
-
- val examples =
- mdl.getExamples.asScala.
- map(ex ⇒ SEPARATORS.foldLeft(ex)((s, ch) ⇒ s.replaceAll(s"\\$ch", s" $ch "))).
- map(ex ⇒ {
- val seq = ex.split(" ")
-
- seq → seq.map(toStemWord)
- }).
- toMap
-
- val elemSyns =
- mdl.getElements.asScala.map(e ⇒ e.getId → e.getSynonyms.asScala.flatMap(parser.expand)).
- map { case (id, seq) ⇒ id → seq.map(txt ⇒ split(txt).map(p ⇒ Word(p, toStemWord(p)))) }.toMap
-
- val allReqs =
- elemSyns.map {
- case (elemId, syns) ⇒
- val normSyns: Seq[Seq[Word]] = syns.filter(_.size == 1)
- val synsStems = normSyns.map(_.map(_.stem))
- val synsWords = normSyns.map(_.map(_.word))
-
- val reqs =
- examples.flatMap { case (exampleWords, exampleStems) ⇒
- val exampleIdxs = synsStems.flatMap(synStems ⇒ getAllSlices(exampleStems, synStems))
-
- def mkRequestData(idx: Int, synStems: Seq[String], synStemsIdx: Int): RequestData = {
- val fromIncl = idx
- val toExcl = idx + synStems.length
-
- RequestData(
- sentence = exampleWords.zipWithIndex.flatMap {
- case (exampleWord, i) ⇒
- i match {
- case x if x == fromIncl ⇒ synsWords(synStemsIdx)
- case x if x > fromIncl && x < toExcl ⇒ Seq.empty
- case _ ⇒ Seq(exampleWord)
- }
- }.mkString(" "),
- example = exampleWords.mkString(" "),
- elementId = elemId,
- index = idx
- )
- }
-
- (for (idx ← exampleIdxs; (synStems, i) ← synsStems.zipWithIndex)
- yield mkRequestData(idx, synStems, i)).distinct
- }
-
- elemId → reqs.toSet
- }.filter(_._2.nonEmpty)
-
- val allReqsCnt = allReqs.map(_._2.size).sum
-
- println(s"Examples count: ${examples.size}")
- println(s"Synonyms count: ${elemSyns.map(_._2.size).sum}")
- println(s"Request prepared: $allReqsCnt")
-
- val allSuggs = new java.util.concurrent.ConcurrentHashMap[String, JList[Suggestion]]()
- val cdl = new CountDownLatch(1)
- val debugs = mutable.HashMap.empty[RequestData, Seq[Suggestion]]
- val cnt = new AtomicInteger(0)
-
- val client = HttpClients.createDefault
-
- for ((elemId, reqs) ← allReqs; batch ← reqs.sliding(BATCH_SIZE, BATCH_SIZE).map(_.toSeq)) {
- NCUtils.asFuture(
- _ ⇒ {
- val post = new HttpPost(data.url)
-
- post.setHeader("Content-Type", "application/json")
- post.setEntity(
- new StringEntity(
- GSON.toJson(
- RestRequest(
- sentences = batch.map(p ⇒ RestRequestSentence(p.sentence, Seq(p.index).asJava)).asJava,
- min_score = data.minScore,
- limit = data.limit
- )
- ),
- "UTF-8"
- )
- )
-
- val resps: Seq[Seq[Suggestion]] =
- try
- client.execute(post, HANDLER)
- finally
- post.releaseConnection()
-
- if (data.debug) {
- require(reqs.size == resps.size)
-
- reqs.zip(resps).foreach { case (req, resp) ⇒ debugs += req → resp}
- }
-
- val i = cnt.addAndGet(batch.size)
-
- println(s"Executed: $i requests.")
-
- allSuggs.
- computeIfAbsent(elemId, (_: String) ⇒ new CopyOnWriteArrayList[Suggestion]()).
- addAll(resps.flatten.asJava)
-
- if (i == allReqsCnt)
- cdl.countDown()
- },
- (e: Throwable) ⇒ {
- e.printStackTrace()
-
- cdl.countDown()
- },
- (_: Unit) ⇒ ()
- )
- }
-
- cdl.await(Long.MaxValue, TimeUnit.MILLISECONDS)
-
- val allSynsStems = elemSyns.flatMap(_._2).flatten.map(_.stem).toSet
-
- val nonEmptySuggs = allSuggs.asScala.map(p ⇒ p._1 → p._2.asScala).filter(_._2.nonEmpty)
-
- val tbl = NCAsciiTable()
-
- tbl #= (
- "Element",
- "Suggestion",
- "ContextWord Server Score",
- "Suggested times",
- "Totla Score",
- )
-
- nonEmptySuggs.
- foreach { case (elemId, elemSuggs) ⇒
- elemSuggs.
- map(sugg ⇒ (sugg, toStem(sugg.word))).
- groupBy { case (_, stem) ⇒ stem }.
- filter { case (stem, _) ⇒ !allSynsStems.contains(stem) }.
- map { case (_, group) ⇒
- val seq = group.map { case (sugg, _) ⇒ sugg }.sortBy(-_.score)
-
- // Drops repeated.
- (seq.head, seq.length)
- }.
- toSeq.
- map { case (sugg, cnt) ⇒ (sugg, cnt, sugg.score * cnt / elemSuggs.size) }.
- sortBy { case (_, _, sumFactor) ⇒ -sumFactor }.
- zipWithIndex.
- foreach { case ((sugg, cnt, sumFactor), sugIdx) ⇒
- def f(d: Double): String = "%1.5f" format d
-
- tbl += (if (sugIdx == 0) elemId else "", sugg.word, f(sugg.score), cnt, f(sumFactor))
- }
- }
-
- if (data.debug) {
- var i = 1
-
- debugs.groupBy(_._1.example).foreach { case (_, m) ⇒
- m.toSeq.sortBy(_._1.sentence).foreach { case (req, suggs) ⇒
- val s =
- split(req.sentence).
- zipWithIndex.map { case (w, i) ⇒ if (i == req.index) s"<<<$w>>>" else w }.
- mkString(" ")
-
- println(
- s"$i. " +
- s"Request=$s, " +
- s"suggestions=[${suggs.map(_.word).mkString(", ")}], " +
- s"element=${req.elementId}"
- )
-
- i = i + 1
- }
- }
- }
-
- println(s"Suggestions calculated (${(System.currentTimeMillis() - now) / 1000} secs)")
-
- tbl.render()
- }
-}
-
-object NCSuggestionsGenerator extends App {
- private lazy val DFLT_URL: String = "http://localhost:5000/suggestions"
- private lazy val DFLT_LIMIT: Int = 20
- private lazy val DFLT_MIN_SCORE: Double = 0
- private lazy val DFLT_DEBUG: Boolean = false
-
- /**
- *
- * @param msg Optional error message.
- */
- private def errorExit(msg: String = null): Unit = {
- if (msg != null)
- System.err.println(s"ERROR: $msg")
- else
- System.err.println(
- s"""
- |NAME:
- | NCSuggestionsGenerator -- NLPCraft synonyms suggestions generator for given model.
- |
- |SYNOPSIS:
- | java -cp apache-nlpcraft-incubating-${NCVersion.getCurrent}-all-deps.jar org.apache.nlpcraft.model.tools.suggestions.NCSuggestionsGenerator [PARAMETERS]
- |
- |DESCRIPTION:
- | This utility generates synonyms suggestions for given NLPCraft model.
- | Note that ContextWord NLP server should be started and accessible parameter URL.
- |
- | This Java class can be run from the command line or from an IDE like any other
- | Java application.""".stripMargin
- )
-
- System.err.println(
- s"""
- |PARAMETERS:
- | [--model|-m] model path
- | Mandatory file model path.
- | It should have one of the following extensions: .js, .json, .yml, or .yaml
- |
- | [--url|-u] url
- | Optional ContextWord NLP server URL.
- | Default is $DFLT_URL.
- |
- | [--limit|-l] limit
- | Optional maximum suggestions per synonyms count value.
- | Default is $DFLT_LIMIT.
- |
- | [--score|-c] score
- | Optional minimal suggestion score value.
- | Default is $DFLT_MIN_SCORE.
- |
- | [--debug|-d] [true|false]
- | Optional flag on whether or not to debug output.
- | Default is $DFLT_DEBUG.
- |
- | [--help|-h|-?]
- | Prints this usage information.
- |
- |EXAMPLES:
- | java -cp apache-nlpcraft-incubating-${NCVersion.getCurrent}-all-deps.jar org.apache.nlpcraft.model.tools.sqlgen.NCSqlModelGenerator
- | -m src/main/scala/org/apache/nlpcraft/examples/weather/weather_model.json
- | -u $DFLT_URL
- """.stripMargin
- )
-
- System.exit(1)
- }
-
- /**
- *
- * @param v
- * @param name
- */
- private def mandatoryParam(v: String, name: String): Unit =
- if (v == null)
- throw new IllegalArgumentException(s"Parameter is mandatory and must be set: $name")
-
- /**
- *
- * @param v
- * @param name
- * @return
- */
- private def parseNum[T](v: String, name: String, extract: String ⇒ T, fromIncl: T, toIncl: T)(implicit e: T ⇒ Number): T = {
- val t =
- try
- extract(v.toLowerCase)
- catch {
- case _: NumberFormatException ⇒ throw new IllegalArgumentException(s"Invalid numeric: $name")
- }
-
- val td = t.doubleValue()
-
- if (td < fromIncl.doubleValue() || td > toIncl.doubleValue())
- throw new IllegalArgumentException(s"Invalid '$name' range. Must be between: $fromIncl and $toIncl")
-
- t
- }
-
- /**
- *
- * @param v
- * @param name
- * @return
- */
- private def parseBoolean(v: String, name: String): Boolean =
- v.toLowerCase match {
- case "true" ⇒ true
- case "false" ⇒ false
-
- case _ ⇒ throw new IllegalArgumentException(s"Invalid boolean value in: $name $v")
- }
-
- /**
- *
- * @param cmdArgs
- * @return
- */
- private def parseCmdParameters(cmdArgs: Array[String]): ParametersHolder = {
- if (cmdArgs.isEmpty || !cmdArgs.intersect(Seq("--help", "-h", "-help", "--?", "-?", "/?", "/help")).isEmpty)
- errorExit()
-
- var mdlPath: String = null
-
- var url = DFLT_URL
- var limit = DFLT_LIMIT
- var minScore = DFLT_MIN_SCORE
- var debug = DFLT_DEBUG
-
- var i = 0
-
- try {
- while (i < cmdArgs.length - 1) {
- val k = cmdArgs(i).toLowerCase
- val v = cmdArgs(i + 1)
-
- k match {
- case "--model" | "-m" ⇒ mdlPath = v
- case "--url" | "-u" ⇒ url = v
- case "--limit" | "-l" ⇒ limit = parseNum(v, k, (s: String) ⇒ s.toInt, 1, Integer.MAX_VALUE)
- case "--score" | "-c" ⇒ minScore = parseNum(v, k, (s: String) ⇒ s.toDouble, 0, Integer.MAX_VALUE)
- case "--debug" | "-d" ⇒ debug = parseBoolean(v, k)
-
- case _ ⇒ throw new IllegalArgumentException(s"Invalid argument: ${cmdArgs(i)}")
- }
-
- i = i + 2
- }
-
- mandatoryParam(mdlPath, "--model")
- }
- catch {
- case e: Exception ⇒ errorExit(e.getMessage)
- }
-
- ParametersHolder(mdlPath, url, limit, minScore, debug)
- }
-
- NCSuggestionsGeneratorImpl.process(parseCmdParameters(args))
-}
\ No newline at end of file
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala
index b71458a..7eae2f4 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala
@@ -17,28 +17,20 @@
package org.apache.nlpcraft.model.tools.test.impl
-import java.lang.reflect.Method
-
import com.typesafe.scalalogging.LazyLogging
import org.apache.nlpcraft.common.ascii.NCAsciiTable
import org.apache.nlpcraft.common.util.NCUtils
-import org.apache.nlpcraft.model.intent.impl.NCIntentDslCompiler
+import org.apache.nlpcraft.model.NCModel
+import org.apache.nlpcraft.model.intent.impl.NCIntentScanner
import org.apache.nlpcraft.model.tools.test.NCTestClientBuilder
-import org.apache.nlpcraft.model.{NCIntent, NCIntentRef, NCIntentSample, NCModel}
import org.apache.nlpcraft.probe.embedded.NCEmbeddedProbe
/**
* Implementation for `NCTestAutoModelValidator` class.
*/
private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
- case class IntentSamples(intentId: String, samples: List[String])
-
private final val PROP_MODELS = "NLPCRAFT_TEST_MODELS"
- private final val CLS_SAMPLE = classOf[NCIntentSample]
- private final val CLS_INTENT = classOf[NCIntent]
- private final val CLS_INTENT_REF = classOf[NCIntentRef]
-
@throws[Exception]
def isValid: Boolean =
NCUtils.sysEnv(PROP_MODELS) match {
@@ -66,7 +58,12 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
@throws[Exception]
private def isValid(classes: Seq[Class[_ <: NCModel]]) = {
- val samples = getSamples(classes.map(cl ⇒ cl → cl.getDeclaredConstructor().newInstance().getId).toMap)
+ val samples =
+ classes.
+ map(_.getDeclaredConstructor().newInstance()).
+ map(mdl ⇒ mdl.getId → NCIntentScanner.scanIntentsSamples(mdl).toMap).
+ toMap.
+ filter(_._2.nonEmpty)
NCEmbeddedProbe.start(classes: _*)
@@ -81,7 +78,7 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
* @param samples
* @return
*/
- private def process(samples: Map[/*Model ID*/String, List[IntentSamples]]): Boolean = {
+ private def process(samples: Map[/*Model ID*/String, Map[String/*Intent ID*/, Seq[String]/*Samples*/]]): Boolean = {
case class Result(
modelId: String,
intentId: String,
@@ -90,7 +87,7 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
error: Option[String]
)
- val results = samples.flatMap { case (mdlId, smpList) ⇒
+ val results = samples.flatMap { case (mdlId, samples) ⇒
val cli = new NCTestClientBuilder().newBuilder.build
cli.open(mdlId)
@@ -100,14 +97,14 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
val res = cli.ask(txt)
if (res.isFailed)
- Result(mdlId, intentId, txt, false, Some(res.getResultError.get()))
+ Result(mdlId, intentId, txt, pass = false, Some(res.getResultError.get()))
else if (intentId != res.getIntentId)
- Result(mdlId, intentId, txt, false, Some(s"Unexpected intent ID '${res.getIntentId}'"))
+ Result(mdlId, intentId, txt, pass = false, Some(s"Unexpected intent ID '${res.getIntentId}'"))
else
- Result(mdlId, intentId, txt, true, None)
+ Result(mdlId, intentId, txt, pass = true, None)
}
- for (smp ← smpList; txt ← smp.samples) yield ask(smp.intentId, txt)
+ for ((intentId, seq) ← samples; txt ← seq) yield ask(intentId, txt)
}
finally
cli.close()
@@ -139,73 +136,7 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
failCnt == 0
}
-
- private def mkMethodName(mtd: Method): String =
- s"${mtd.getDeclaringClass.getName}#${mtd.getName}(...)"
-
- /**
- *
- * @param mdls
- * @return
- */
- private def getSamples(mdls: Map[Class[_ <: NCModel], String]): Map[/*Model ID*/String, List[IntentSamples]] =
- mdls.flatMap { case (claxx, mdlId) ⇒
- var annFound = false
-
- val mdlData = claxx.getDeclaredMethods.flatMap(method ⇒ {
- val smpAnn = method.getAnnotation(CLS_SAMPLE)
- val intAnn = method.getAnnotation(CLS_INTENT)
- val refAnn = method.getAnnotation(CLS_INTENT_REF)
-
- if (smpAnn != null || intAnn != null || refAnn != null) {
- annFound = true
- def mkIntentId(): String =
- if (intAnn != null)
- NCIntentDslCompiler.compile(intAnn.value(), mdlId).id
- else if (refAnn != null)
- refAnn.value().trim
- else
- throw new AssertionError()
-
- if (smpAnn != null) {
- if (intAnn == null && refAnn == null) {
- logger.warn(s"@NCTestSample annotation without corresponding @NCIntent or @NCIntentRef annotations " +
- s"in method (ignoring): ${mkMethodName(method)}")
-
- None
- }
- else {
- val samples = smpAnn.value().toList
-
- if (samples.isEmpty) {
- logger.warn(s"@NCTestSample annotation is empty in method (ignoring): ${mkMethodName(method)}")
-
- None
- }
- else
- Some(IntentSamples(mkIntentId(), samples))
- }
- }
- else {
- logger.warn(s"@NCTestSample annotation is missing in method (ignoring): ${mkMethodName(method)}")
-
- None
- }
- }
- else
- None
- }).toList
-
- if (mdlData.isEmpty) {
- if (!annFound)
- logger.warn(s"Model '$mdlId' doesn't have any intents to test.")
-
- None
- }
- else
- Some(mdlId → mdlData)
- }
/**
*
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala
index 2cfe417..9805a16 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala
@@ -32,6 +32,7 @@ import scala.language.implicitConversions
/**
*
* @param model Decorated model.
+ * @param intentsSamples Model examples.
* @param synonyms Fast-access synonyms map for first phase.
* @param synonymsDsl Fast-access synonyms map for second phase.
* @param additionalStopWordsStems Stemmatized additional stopwords.
@@ -41,6 +42,7 @@ import scala.language.implicitConversions
*/
case class NCModelDecorator(
model: NCModel,
+ intentsSamples: Map[String, Seq[String]],
synonyms: Map[String/*Element ID*/, Map[Int/*Synonym length*/, Seq[NCSynonym]]], // Fast access map.
synonymsDsl: Map[String/*Element ID*/, Map[Int/*Synonym length*/, Seq[NCSynonym]]], // Fast access map.
additionalStopWordsStems: Set[String],
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
index a59eea0..cc6dd1f 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
@@ -35,6 +35,7 @@ import org.apache.nlpcraft.probe.mgrs.NCProbeMessage
import org.apache.nlpcraft.probe.mgrs.cmd.NCCommandManager
import org.apache.nlpcraft.probe.mgrs.model.NCModelManager
+import scala.collection.JavaConverters._
import scala.collection.mutable
/**
@@ -47,7 +48,7 @@ object NCConnectionManager extends NCService {
private final val SO_TIMEOUT = 5 * 1000
// Ping timeout.
private final val PING_TIMEOUT = 5 * 1000
-
+
// Internal probe GUID.
@volatile private var probeGuid: String = _
@@ -234,7 +235,18 @@ object NCConnectionManager extends NCService {
// util.HashSet created to avoid scala collections serialization error.
// Seems to be a Scala bug.
- (mdl.getId, mdl.getName, mdl.getVersion, new util.HashSet[String](mdl.getEnabledBuiltInTokens))
+ (
+ mdl.getId,
+ mdl.getName,
+ mdl.getVersion,
+ new util.HashSet[String](mdl.getEnabledBuiltInTokens),
+ mdl.getMacros,
+ new util.HashMap[String, util.List[String]](
+ mdl.getElements.asScala.map(p ⇒ p.getId → p.getSynonyms).toMap.asJava
+ ),
+ new util.HashMap[String, util.List[String]]
+ (m.intentsSamples.map(p ⇒ p._1 → new util.ArrayList[String](p._2.asJava)).asJava)
+ )
})
), cryptoKey)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
index 66706da..cf61a1a 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
@@ -18,7 +18,7 @@
package org.apache.nlpcraft.probe.mgrs.deploy
import java.io._
-import java.util.jar.{JarInputStream ⇒ JIS}
+import java.util.jar.{JarInputStream => JIS}
import io.opencensus.trace.Span
import org.apache.nlpcraft.common._
@@ -31,7 +31,7 @@ import resource.managed
import scala.collection.JavaConverters._
import scala.collection.convert.DecorateAsScala
-import scala.collection.mutable
+import scala.collection.{Seq, mutable}
import scala.collection.mutable.ArrayBuffer
import scala.util.control.Exception._
@@ -39,11 +39,15 @@ import scala.util.control.Exception._
* Model deployment manager.
*/
object NCDeployManager extends NCService with DecorateAsScala {
- @volatile private var models: ArrayBuffer[NCModel] = _
- @volatile private var modelFactory: NCModelFactory = _
-
+ private final val CLS_SAMPLE = classOf[NCIntentSample]
+ private final val CLS_INTENT = classOf[NCIntent]
+ private final val CLS_INTENT_REF = classOf[NCIntentRef]
+
private final val ID_REGEX = "^[_a-zA-Z]+[a-zA-Z0-9:-_]*$"
+ @volatile private var models: ArrayBuffer[NCModelHolder] = _
+ @volatile private var modelFactory: NCModelFactory = _
+
object Config extends NCConfigurable {
private final val pre = "nlpcraft.probe"
@@ -75,7 +79,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
* @return
*/
@throws[NCE]
- private def wrap(mdl: NCModel): NCModel = {
+ private def wrap(mdl: NCModel): NCModelHolder = {
// Scan for intent annotations in the model class.
val intents = NCIntentScanner.scan(mdl)
@@ -91,16 +95,16 @@ object NCDeployManager extends NCService with DecorateAsScala {
val solver = new NCIntentSolver(
intents.toList.map(x ⇒ (x._1, (z: NCIntentMatch) ⇒ x._2.apply(z)))
)
-
- new NCModelImpl(mdl, solver)
+
+ NCModelHolder(new NCModelImpl(mdl, solver), NCIntentScanner.scanIntentsSamples(mdl).toMap)
}
else {
logger.warn(s"Model has no intents: ${mdl.getId}")
-
- new NCModelImpl(mdl, null)
+
+ NCModelHolder(new NCModelImpl(mdl, null), Map.empty)
}
}
-
+
/**
*
* @param clsName Factory class name.
@@ -122,7 +126,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
* @param clsName Model class name.
*/
@throws[NCE]
- private def makeModel(clsName: String): NCModel =
+ private def makeModel(clsName: String): NCModelHolder =
try
wrap(
makeModelFromSource(
@@ -157,7 +161,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
* @param jarFile JAR file to extract from.
*/
@throws[NCE]
- private def extractModels(jarFile: File): Seq[NCModel] = {
+ private def extractModels(jarFile: File): Seq[NCModelHolder] = {
val clsLdr = Thread.currentThread().getContextClassLoader
val classes = mutable.ArrayBuffer.empty[Class[_ <: NCModel]]
@@ -197,7 +201,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
@throws[NCE]
override def start(parent: Span = null): NCService = startScopedSpan("start", parent) { _ ⇒
modelFactory = new NCBasicModelFactory
- models = ArrayBuffer.empty[NCModel]
+ models = ArrayBuffer.empty[NCModelHolder]
// Initialize model factory (if configured).
Config.modelFactoryType match {
@@ -230,7 +234,9 @@ object NCDeployManager extends NCService with DecorateAsScala {
}
// Verify models' identities.
- models.foreach(mdl ⇒ {
+ models.foreach(h ⇒ {
+ val mdl = h.model
+
val mdlName = mdl.getName
val mdlId = mdl.getId
val mdlVer = mdl.getVersion
@@ -259,7 +265,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
throw new NCE(s"Model element ID '${elm.getId}' does not match '$ID_REGEX' regex in: $mdlId")
})
- if (U.containsDups(models.map(_.getId).toList))
+ if (U.containsDups(models.map(_.model.getId).toList))
throw new NCE("Duplicate model IDs detected.")
super.start()
@@ -280,5 +286,5 @@ object NCDeployManager extends NCService with DecorateAsScala {
*
* @return
*/
- def getModels: Seq[NCModel] = models
+ def getModels: Seq[NCModelHolder] = models
}
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCModelHolder.scala
similarity index 58%
copy from nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
copy to nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCModelHolder.scala
index 1510c4b..1a94481 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCModelHolder.scala
@@ -15,26 +15,13 @@
* limitations under the License.
*/
-package org.apache.nlpcraft.server.mdo
+package org.apache.nlpcraft.probe.mgrs.deploy
-import org.apache.nlpcraft.server.mdo.impl._
+import org.apache.nlpcraft.model.NCModel
/**
- * Probe model MDO.
+ * TODO:
+ * @param model
+ * @param intentSamples
*/
-@NCMdoEntity(sql = false)
-case class NCProbeModelMdo(
- @NCMdoField id: String,
- @NCMdoField name: String,
- @NCMdoField version: String,
- @NCMdoField enabledBuiltInTokens: Set[String]
-) extends NCAnnotatedMdo[NCProbeModelMdo] {
- override def hashCode(): Int = s"$id$name".hashCode()
-
- override def equals(obj: Any): Boolean = {
- obj match {
- case x: NCProbeModelMdo ⇒ x.id == id
- case _ ⇒ false
- }
- }
-}
+case class NCModelHolder(model: NCModel, intentSamples: Map[String, Seq[String]])
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala
index 2875d20..9f67045 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala
@@ -59,24 +59,24 @@ object NCModelManager extends NCService with DecorateAsScala {
)
/**
- * @param mdl Data model.
+ * @param h Data model holder.
*/
- private def addNewModel(mdl: NCModel): Unit = {
+ private def addNewModel(h: NCModelHolder): Unit = {
require(Thread.holdsLock(mux))
- checkModelConfig(mdl)
+ checkModelConfig(h.model)
val parser = new NCMacroParser
- val macros = mdl.getMacros
+ val macros = h.model.getMacros
// Initialize macro parser.
if (macros != null)
macros.asScala.foreach(t ⇒ parser.addMacro(t._1, t._2))
-
- models += mdl.getId → verifyAndDecorate(mdl, parser)
+
+ models += h.model.getId → verifyAndDecorate(h, parser)
// Init callback on the model.
- mdl.onInit()
+ h.model.onInit()
}
@throws[NCE]
@@ -249,12 +249,14 @@ object NCModelManager extends NCService with DecorateAsScala {
/**
* Verifies given model and makes a decorator optimized for model enricher.
*
- * @param mdl Model to verify and decorate.
+ * @param h Model holder to verify and decorate.
* @param parser Initialized macro parser.
* @return Model decorator.
*/
@throws[NCE]
- private def verifyAndDecorate(mdl: NCModel, parser: NCMacroParser): NCModelDecorator = {
+ private def verifyAndDecorate(h: NCModelHolder, parser: NCMacroParser): NCModelDecorator = {
+ val mdl = h.model
+
for (elm ← mdl.getElements)
checkElement(mdl, elm)
@@ -526,6 +528,7 @@ object NCModelManager extends NCService with DecorateAsScala {
NCModelDecorator(
model = mdl,
+ intentsSamples = h.intentSamples,
synonyms = mkFastAccessMap(filter(syns, dsl = false)),
synonymsDsl = mkFastAccessMap(filter(syns, dsl = true)),
additionalStopWordsStems = addStopWords,
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala
index 1418685..7ac8179 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala
@@ -45,6 +45,7 @@ import org.apache.nlpcraft.server.proclog.NCProcessLogManager
import org.apache.nlpcraft.server.query.NCQueryManager
import org.apache.nlpcraft.server.rest.NCRestManager
import org.apache.nlpcraft.server.sql.NCSqlManager
+import org.apache.nlpcraft.server.suggestion.NCSuggestionsManager
import org.apache.nlpcraft.server.tx.NCTxManager
import org.apache.nlpcraft.server.user.NCUserManager
@@ -120,7 +121,10 @@ object NCServer extends App with NCIgniteInstance with LazyLogging with NCOpenCe
NCUserManager.start(span)
NCCompanyManager.start(span)
},
- () ⇒ NCProbeManager.start(span),
+ () ⇒ {
+ NCProbeManager.start(span)
+ NCSuggestionsManager.start(span)
+ },
() ⇒ NCFeedbackManager.start(span)
)
@@ -144,6 +148,8 @@ object NCServer extends App with NCIgniteInstance with LazyLogging with NCOpenCe
NCRestManager,
NCQueryManager,
NCFeedbackManager,
+ NCSuggestionsManager,
+ NCProbeManager,
NCCompanyManager,
NCUserManager,
NCServerEnrichmentManager,
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
index 1510c4b..0598ff6 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
@@ -27,7 +27,10 @@ case class NCProbeModelMdo(
@NCMdoField id: String,
@NCMdoField name: String,
@NCMdoField version: String,
- @NCMdoField enabledBuiltInTokens: Set[String]
+ @NCMdoField enabledBuiltInTokens: Set[String],
+ @NCMdoField macros: Map[String, String],
+ @NCMdoField elementsSynonyms: Map[String, Seq[String]],
+ @NCMdoField intentsSamples: Map[String, Seq[String]]
) extends NCAnnotatedMdo[NCProbeModelMdo] {
override def hashCode(): Int = s"$id$name".hashCode()
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala
index 7c12e16..b50aa66 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala
@@ -31,6 +31,7 @@ import io.opencensus.stats._
trait NCOpenCensusServerStats {
val M_ASK_LATENCY_MS: MeasureLong = MeasureLong.create("ask_latency", "The latency of '/ask' REST call", "ms")
val M_CHECK_LATENCY_MS: MeasureLong = MeasureLong.create("check_latency", "The latency of '/check' REST call", "ms")
+ val M_SUGGESTION_LATENCY_MS: MeasureLong = MeasureLong.create("suggestion_latency", "The latency of '/suggestion' REST call", "ms")
val M_CANCEL_LATENCY_MS: MeasureLong = MeasureLong.create("cancel_latency", "The latency of '/cancel' REST call", "ms")
val M_SIGNIN_LATENCY_MS: MeasureLong = MeasureLong.create("signin_latency", "The latency of '/signin' REST call", "ms")
val M_SIGNOUT_LATENCY_MS: MeasureLong = MeasureLong.create("signout_latency", "The latency of '/signout' REST call", "ms")
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala
index add4ea4..1eea061 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala
@@ -56,8 +56,8 @@ object NCProbeManager extends NCService {
private[probe] object Config extends NCConfigurable {
final private val pre = "nlpcraft.server.probe"
- def getDnHostPort = getHostPort(s"$pre.links.downLink")
- def getUpHostPort = getHostPort(s"$pre.links.upLink")
+ def getDnHostPort: (String, Integer) = getHostPort(s"$pre.links.downLink")
+ def getUpHostPort: (String, Integer) = getHostPort(s"$pre.links.upLink")
def poolSize: Int = getInt(s"$pre.poolSize")
def reconnectTimeoutMs: Long = getLong(s"$pre.reconnectTimeoutMs")
@@ -574,13 +574,34 @@ object NCProbeManager extends NCService {
respond("S2P_PROBE_VERSION_MISMATCH")
else {
val models =
- hsMsg.data[List[(String, String, String, java.util.Set[String])]]("PROBE_MODELS").
- map { case (mdlId, mdlName, mdlVer, enabledBuiltInToks) ⇒
+ hsMsg.data[
+ List[(
+ String,
+ String,
+ String,
+ java.util.Set[String],
+ java.util.Map[String, String],
+ java.util.Map[String, java.util.List[String]],
+ java.util.Map[String, java.util.List[String]]
+ )]]("PROBE_MODELS").
+ map {
+ case (
+ mdlId,
+ mdlName,
+ mdlVer,
+ enabledBuiltInToks,
+ macros,
+ elemSyns,
+ intentsSamples
+ ) ⇒
NCProbeModelMdo(
id = mdlId,
name = mdlName,
version = mdlVer,
- enabledBuiltInTokens = enabledBuiltInToks.asScala.toSet
+ enabledBuiltInTokens = enabledBuiltInToks.asScala.toSet,
+ macros = macros.asScala.toMap,
+ elementsSynonyms = elemSyns.asScala.map(p ⇒ p._1 → p._2.asScala).toMap,
+ intentsSamples.asScala.map(p ⇒ p._1 → p._2.asScala).toMap
)
}.toSet
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala
index 71c0678..b2425df 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala
@@ -37,6 +37,7 @@ import org.apache.nlpcraft.server.mdo.{NCQueryStateMdo, NCUserMdo}
import org.apache.nlpcraft.server.opencensus.NCOpenCensusServerStats
import org.apache.nlpcraft.server.probe.NCProbeManager
import org.apache.nlpcraft.server.query.NCQueryManager
+import org.apache.nlpcraft.server.suggestion.NCSuggestionsManager
import org.apache.nlpcraft.server.user.NCUserManager
import spray.json.DefaultJsonProtocol._
import spray.json.{JsValue, RootJsonFormat}
@@ -257,14 +258,14 @@ class NCBasicRestApi extends NCRestApi with LazyLogging with NCOpenCensusTrace w
* @param acsTkn Access token to check.
*/
@throws[NCE]
- protected def authenticate(acsTkn: String): NCUserMdo = authenticate0(acsTkn, false)
+ protected def authenticate(acsTkn: String): NCUserMdo = authenticate0(acsTkn, shouldBeAdmin = false)
/**
*
* @param acsTkn Access token to check.
*/
@throws[NCE]
- protected def authenticateAsAdmin(acsTkn: String): NCUserMdo = authenticate0(acsTkn, true)
+ protected def authenticateAsAdmin(acsTkn: String): NCUserMdo = authenticate0(acsTkn, shouldBeAdmin = true)
/**
* Checks length of field value.
@@ -618,7 +619,64 @@ class NCBasicRestApi extends NCRestApi with LazyLogging with NCOpenCensusTrace w
}
}
}
-
+
+ /**
+ *
+ * @return
+ */
+ protected def suggestion$(): Route = {
+ case class Req(
+ acsTok: String,
+ mdlId: String,
+ limitOpt: Option[Int],
+ minScoreOpt: Option[Double]
+ )
+
+ case class Suggestion(
+ suggestion: String,
+ score: Double,
+ suggestedCount: Int
+ )
+
+ case class Res(
+ status: String,
+ suggestions: Map[String, Seq[Suggestion]]
+ )
+
+ implicit val reqFmt: RootJsonFormat[Req] = jsonFormat4(Req)
+ implicit val fbFmt: RootJsonFormat[Suggestion] = jsonFormat3(Suggestion)
+ implicit val resFmt: RootJsonFormat[Res] = jsonFormat2(Res)
+
+ entity(as[Req]) { req ⇒
+ startScopedSpan(
+ "check$",
+ "mdlId" → req.mdlId,
+ "limit" → req.limitOpt.getOrElse(() ⇒ null),
+ "minScore" → req.minScoreOpt.getOrElse(() ⇒ null),
+ "acsTok" → req.acsTok
+ ) { span ⇒
+ checkLength("acsTok", req.acsTok, 256)
+ checkLength("mdlId", req.mdlId, 32)
+
+ val admin = authenticateAsAdmin(req.acsTok)
+
+ if (!NCProbeManager.getAllProbes(admin.companyId, span).exists(_.models.exists(_.id == req.mdlId)))
+ throw new NCE(s"Probe not found: ${req.mdlId}")
+
+ val res: Map[String, Seq[Suggestion]] =
+ NCSuggestionsManager.
+ suggest(req.mdlId, req.limitOpt, req.minScoreOpt, span).
+ map { case (elemId, suggs) ⇒
+ elemId → suggs.map(p ⇒ Suggestion(p.suggestion, p.ctxWorldServerScore, p.suggestedCount))
+ }.toMap
+
+ complete {
+ Res(API_OK, res)
+ }
+ }
+ }
+ }
+
/**
*
* @return
@@ -1718,40 +1776,43 @@ class NCBasicRestApi extends NCRestApi with LazyLogging with NCOpenCensusTrace w
}
} ~
post {
- withRequestTimeoutResponse(_ ⇒ timeoutResp) {
- path(API / "signin") { withLatency(M_SIGNIN_LATENCY_MS, signin$) } ~
- path(API / "signout") { withLatency(M_SIGNOUT_LATENCY_MS, signout$) } ~ {
- path(API / "cancel") { withLatency(M_CANCEL_LATENCY_MS, cancel$) } ~
- path(API / "check") { withLatency(M_CHECK_LATENCY_MS, check$) } ~
- path(API / "clear"/ "conversation") { withLatency(M_CLEAR_CONV_LATENCY_MS, clear$Conversation) } ~
- path(API / "clear"/ "dialog") { withLatency(M_CLEAR_DIALOG_LATENCY_MS, clear$Dialog) } ~
- path(API / "company"/ "add") { withLatency(M_COMPANY_ADD_LATENCY_MS, company$Add) } ~
- path(API / "company"/ "get") { withLatency(M_COMPANY_GET_LATENCY_MS, company$Get) } ~
- path(API / "company" / "update") { withLatency(M_COMPANY_UPDATE_LATENCY_MS, company$Update) } ~
- path(API / "company" / "token" / "reset") { withLatency(M_COMPANY_TOKEN_LATENCY_MS, company$Token$Reset) } ~
- path(API / "company" / "delete") { withLatency(M_COMPANY_DELETE_LATENCY_MS, company$Delete) } ~
- path(API / "user" / "get") { withLatency(M_USER_GET_LATENCY_MS, user$Get) } ~
- path(API / "user" / "add") { withLatency(M_USER_ADD_LATENCY_MS, user$Add) } ~
- path(API / "user" / "update") { withLatency(M_USER_UPDATE_LATENCY_MS, user$Update) } ~
- path(API / "user" / "delete") { withLatency(M_USER_DELETE_LATENCY_MS, user$Delete) } ~
- path(API / "user" / "admin") { withLatency(M_USER_ADMIN_LATENCY_MS, user$Admin) } ~
- path(API / "user" / "passwd" / "reset") { withLatency(M_USER_PASSWD_RESET_LATENCY_MS, user$Password$Reset) } ~
- path(API / "user" / "all") { withLatency(M_USER_ALL_LATENCY_MS, user$All) } ~
- path(API / "feedback"/ "add") { withLatency(M_FEEDBACK_ADD_LATENCY_MS, feedback$Add) } ~
- path(API / "feedback"/ "all") { withLatency(M_FEEDBACK_GET_LATENCY_MS, feedback$All) } ~
- path(API / "feedback" / "delete") { withLatency(M_FEEDBACK_DELETE_LATENCY_MS, feedback$Delete) } ~
- path(API / "probe" / "all") { withLatency(M_PROBE_ALL_LATENCY_MS, probe$All) } ~
- path(API / "ask") { withLatency(M_ASK_LATENCY_MS, ask$) } ~
- (path(API / "ask" / "sync") &
- entity(as[JsValue]) &
- optionalHeaderValueByName("User-Agent") &
- extractClientIP
- ) {
- (req, userAgentOpt, rmtAddr) ⇒
- onSuccess(withLatency(M_ASK_SYNC_LATENCY_MS, ask$Sync(req, userAgentOpt, rmtAddr))) {
- js ⇒ complete(HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, js)))
- }
- }}
+ withPrecompressedMediaTypeSupport {
+ withRequestTimeoutResponse(_ ⇒ timeoutResp) {
+ path(API / "signin") { withLatency(M_SIGNIN_LATENCY_MS, signin$) } ~
+ path(API / "signout") { withLatency(M_SIGNOUT_LATENCY_MS, signout$) } ~ {
+ path(API / "cancel") { withLatency(M_CANCEL_LATENCY_MS, cancel$) } ~
+ path(API / "check") { withLatency(M_CHECK_LATENCY_MS, check$) } ~
+ path(API / "suggestion") { withLatency(M_SUGGESTION_LATENCY_MS, suggestion$) } ~
+ path(API / "clear"/ "conversation") { withLatency(M_CLEAR_CONV_LATENCY_MS, clear$Conversation) } ~
+ path(API / "clear"/ "dialog") { withLatency(M_CLEAR_DIALOG_LATENCY_MS, clear$Dialog) } ~
+ path(API / "company"/ "add") { withLatency(M_COMPANY_ADD_LATENCY_MS, company$Add) } ~
+ path(API / "company"/ "get") { withLatency(M_COMPANY_GET_LATENCY_MS, company$Get) } ~
+ path(API / "company" / "update") { withLatency(M_COMPANY_UPDATE_LATENCY_MS, company$Update) } ~
+ path(API / "company" / "token" / "reset") { withLatency(M_COMPANY_TOKEN_LATENCY_MS, company$Token$Reset) } ~
+ path(API / "company" / "delete") { withLatency(M_COMPANY_DELETE_LATENCY_MS, company$Delete) } ~
+ path(API / "user" / "get") { withLatency(M_USER_GET_LATENCY_MS, user$Get) } ~
+ path(API / "user" / "add") { withLatency(M_USER_ADD_LATENCY_MS, user$Add) } ~
+ path(API / "user" / "update") { withLatency(M_USER_UPDATE_LATENCY_MS, user$Update) } ~
+ path(API / "user" / "delete") { withLatency(M_USER_DELETE_LATENCY_MS, user$Delete) } ~
+ path(API / "user" / "admin") { withLatency(M_USER_ADMIN_LATENCY_MS, user$Admin) } ~
+ path(API / "user" / "passwd" / "reset") { withLatency(M_USER_PASSWD_RESET_LATENCY_MS, user$Password$Reset) } ~
+ path(API / "user" / "all") { withLatency(M_USER_ALL_LATENCY_MS, user$All) } ~
+ path(API / "feedback"/ "add") { withLatency(M_FEEDBACK_ADD_LATENCY_MS, feedback$Add) } ~
+ path(API / "feedback"/ "all") { withLatency(M_FEEDBACK_GET_LATENCY_MS, feedback$All) } ~
+ path(API / "feedback" / "delete") { withLatency(M_FEEDBACK_DELETE_LATENCY_MS, feedback$Delete) } ~
+ path(API / "probe" / "all") { withLatency(M_PROBE_ALL_LATENCY_MS, probe$All) } ~
+ path(API / "ask") { withLatency(M_ASK_LATENCY_MS, ask$) } ~
+ (path(API / "ask" / "sync") &
+ entity(as[JsValue]) &
+ optionalHeaderValueByName("User-Agent") &
+ extractClientIP
+ ) {
+ (req, userAgentOpt, rmtAddr) ⇒
+ onSuccess(withLatency(M_ASK_SYNC_LATENCY_MS, ask$Sync(req, userAgentOpt, rmtAddr))) {
+ js ⇒ complete(HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, js)))
+ }
+ }}
+ }
}
}
)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestion.scala
similarity index 58%
copy from nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
copy to nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestion.scala
index 1510c4b..7ea0d28 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestion.scala
@@ -15,26 +15,18 @@
* limitations under the License.
*/
-package org.apache.nlpcraft.server.mdo
-
-import org.apache.nlpcraft.server.mdo.impl._
+package org.apache.nlpcraft.server.suggestion
/**
- * Probe model MDO.
+ * TODO:
+ * @param suggestion
+ * @param ctxWorldServerScore
+ * @param suggestedCount
+ * @param totalScore
*/
-@NCMdoEntity(sql = false)
-case class NCProbeModelMdo(
- @NCMdoField id: String,
- @NCMdoField name: String,
- @NCMdoField version: String,
- @NCMdoField enabledBuiltInTokens: Set[String]
-) extends NCAnnotatedMdo[NCProbeModelMdo] {
- override def hashCode(): Int = s"$id$name".hashCode()
-
- override def equals(obj: Any): Boolean = {
- obj match {
- case x: NCProbeModelMdo ⇒ x.id == id
- case _ ⇒ false
- }
- }
-}
+case class NCSuggestion(
+ suggestion: String,
+ ctxWorldServerScore: Double,
+ suggestedCount: Int,
+ totalScore: Double
+)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestionsManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestionsManager.scala
new file mode 100644
index 0000000..f9d184a
--- /dev/null
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestionsManager.scala
@@ -0,0 +1,315 @@
+/*
+ * 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.server.suggestion
+
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.{CopyOnWriteArrayList, CountDownLatch, TimeUnit}
+import java.util.{List ⇒ JList}
+
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import io.opencensus.trace.Span
+import org.apache.http.HttpResponse
+import org.apache.http.client.ResponseHandler
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.entity.StringEntity
+import org.apache.http.impl.client.HttpClients
+import org.apache.http.util.EntityUtils
+import org.apache.nlpcraft.common.config.NCConfigurable
+import org.apache.nlpcraft.common.makro.NCMacroParser
+import org.apache.nlpcraft.common.nlp.core.NCNlpPorterStemmer
+import org.apache.nlpcraft.common.util.NCUtils
+import org.apache.nlpcraft.common.{NCE, NCService}
+import org.apache.nlpcraft.server.probe.NCProbeManager
+
+import scala.collection.JavaConverters._
+import scala.collection._
+
+/**
+ * TODO:
+ */
+object NCSuggestionsManager extends NCService {
+ private final val DFLT_LIMIT: Int = 20
+ private final val DFLT_MIN_SCORE: Double = 0
+
+ private object Config extends NCConfigurable {
+ val urlOpt: Option[String] = getStringOpt("nlpcraft.server.ctxword.url")
+ }
+
+ case class Suggestion(word: String, score: Double)
+ case class RequestData(sentence: String, example: String, elementId: String, index: Int)
+ case class RestRequestSentence(text: String, indexes: JList[Int])
+ case class RestRequest(sentences: JList[RestRequestSentence], limit: Int, min_score: Double)
+ case class Word(word: String, stem: String) {
+ require(!word.contains(" "), s"Word cannot contains spaces: $word")
+ require(
+ word.forall(ch ⇒
+ ch.isLetterOrDigit ||
+ ch == '\'' ||
+ SEPARATORS.contains(ch)
+ ),
+ s"Unsupported symbols: $word"
+ )
+ }
+
+ private final val GSON = new Gson
+ private final val TYPE_RESP = new TypeToken[JList[JList[Suggestion]]]() {}.getType
+ private final val SEPARATORS = Seq('?', ',', '.', '-', '!')
+ private final val BATCH_SIZE = 20
+
+ private final val HANDLER: ResponseHandler[Seq[Seq[Suggestion]]] =
+ (resp: HttpResponse) ⇒ {
+ val code = resp.getStatusLine.getStatusCode
+ val e = resp.getEntity
+
+ val js = if (e != null) EntityUtils.toString(e) else null
+
+ if (js == null)
+ throw new RuntimeException(s"Unexpected empty response [code=$code]")
+
+ code match {
+ case 200 ⇒
+ val data: JList[JList[Suggestion]] = GSON.fromJson(js, TYPE_RESP)
+
+ data.asScala.map(p ⇒ if (p.isEmpty) Seq.empty else p.asScala.tail)
+
+ case 400 ⇒ throw new RuntimeException(js)
+ case _ ⇒ throw new RuntimeException(s"Unexpected response [code=$code, response=$js]")
+ }
+ }
+
+ private def split(s: String): Seq[String] = s.split(" ").toSeq.map(_.trim).filter(_.nonEmpty)
+ private def toStem(s: String): String = split(s).map(NCNlpPorterStemmer.stem).mkString(" ")
+ private def toStemWord(s: String): String = NCNlpPorterStemmer.stem(s)
+
+ private def getAllSlices(seq1: Seq[String], seq2: Seq[String]): Seq[Int] = {
+ val seq = mutable.Buffer.empty[Int]
+
+ var i = seq1.indexOfSlice(seq2)
+
+ while (i >= 0) {
+ seq += i
+
+ i = seq1.indexOfSlice(seq2, i + 1)
+ }
+
+ seq
+ }
+
+ @throws[NCE]
+ def suggest(
+ mdlId: String, limitOpt: Option[Int], minScoreOpt: Option[Double], parent: Span = null
+ ): Map[String, Seq[NCSuggestion]] =
+ startScopedSpan(
+ "suggest",
+ parent,
+ "modelId" → mdlId,
+ "limit" → limitOpt.getOrElse(() ⇒ null),
+ "minScore" → minScoreOpt.getOrElse(() ⇒ null)
+ ) { _ ⇒
+ var url = Config.urlOpt.getOrElse(throw new NCE("Context word server is not configured"))
+
+ url = s"$url/suggestions"
+
+ val mdl = NCProbeManager.getModel(mdlId)
+
+ val parser = new NCMacroParser()
+
+ if (mdl.macros != null)
+ mdl.macros.foreach { case (name, str) ⇒ parser.addMacro(name, str) }
+
+ val examples =
+ mdl.
+ intentsSamples.
+ flatMap(_._2).
+ map(ex ⇒ SEPARATORS.foldLeft(ex)((s, ch) ⇒ s.replaceAll(s"\\$ch", s" $ch "))).
+ map(ex ⇒ {
+ val seq = ex.split(" ")
+
+ seq → seq.map(toStemWord)
+ }).
+ toMap
+
+ val elemSyns =
+ mdl.elementsSynonyms.map { case (elemId, syns) ⇒ elemId → syns.flatMap(parser.expand) }.
+ map { case (id, seq) ⇒ id → seq.map(txt ⇒ split(txt).map(p ⇒ Word(p, toStemWord(p)))) }
+
+ val allReqs =
+ elemSyns.map {
+ case (elemId, syns) ⇒
+ val normSyns: Seq[Seq[Word]] = syns.filter(_.size == 1)
+ val synsStems = normSyns.map(_.map(_.stem))
+ val synsWords = normSyns.map(_.map(_.word))
+
+ val reqs =
+ examples.flatMap { case (exampleWords, exampleStems) ⇒
+ val exampleIdxs = synsStems.flatMap(synStems ⇒ getAllSlices(exampleStems, synStems))
+
+ def mkRequestData(idx: Int, synStems: Seq[String], synStemsIdx: Int): RequestData = {
+ val fromIncl = idx
+ val toExcl = idx + synStems.length
+
+ RequestData(
+ sentence = exampleWords.zipWithIndex.flatMap {
+ case (exampleWord, i) ⇒
+ i match {
+ case x if x == fromIncl ⇒ synsWords(synStemsIdx)
+ case x if x > fromIncl && x < toExcl ⇒ Seq.empty
+ case _ ⇒ Seq(exampleWord)
+ }
+ }.mkString(" "),
+ example = exampleWords.mkString(" "),
+ elementId = elemId,
+ index = idx
+ )
+ }
+
+ (for (idx ← exampleIdxs; (synStems, i) ← synsStems.zipWithIndex)
+ yield mkRequestData(idx, synStems, i)).distinct
+ }
+
+ elemId → reqs.toSet
+ }.filter(_._2.nonEmpty)
+
+ val allReqsCnt = allReqs.map(_._2.size).sum
+
+ logger.info(s"Examples count: ${examples.size}")
+ logger.info(s"Synonyms count: ${elemSyns.map(_._2.size).sum}")
+ logger.info(s"Requests prepared: $allReqsCnt")
+
+ val allSuggs = new java.util.concurrent.ConcurrentHashMap[String, JList[Suggestion]]()
+ val cdl = new CountDownLatch(1)
+ val debugs = mutable.HashMap.empty[RequestData, Seq[Suggestion]]
+ val cnt = new AtomicInteger(0)
+
+ val client = HttpClients.createDefault
+
+ for ((elemId, reqs) ← allReqs; batch ← reqs.sliding(BATCH_SIZE, BATCH_SIZE).map(_.toSeq)) {
+ NCUtils.asFuture(
+ _ ⇒ {
+ val post = new HttpPost(url)
+
+ post.setHeader("Content-Type", "application/json")
+ post.setEntity(
+ new StringEntity(
+ GSON.toJson(
+ RestRequest(
+ sentences = batch.map(p ⇒ RestRequestSentence(p.sentence, Seq(p.index).asJava)).asJava,
+ min_score = minScoreOpt.getOrElse(DFLT_MIN_SCORE),
+ limit = limitOpt.getOrElse(DFLT_LIMIT)
+ )
+ ),
+ "UTF-8"
+ )
+ )
+
+ val resps: Seq[Seq[Suggestion]] =
+ try
+ client.execute(post, HANDLER)
+ finally
+ post.releaseConnection()
+
+ require(batch.size == resps.size, s"Batch: ${batch.size}, responses: ${resps.size}")
+
+ reqs.zip(resps).foreach { case (req, resp) ⇒ debugs += req → resp}
+
+ val i = cnt.addAndGet(batch.size)
+
+ logger.info(s"Executed: $i requests.")
+
+ allSuggs.
+ computeIfAbsent(elemId, (_: String) ⇒ new CopyOnWriteArrayList[Suggestion]()).
+ addAll(resps.flatten.asJava)
+
+ if (i == allReqsCnt)
+ cdl.countDown()
+ },
+ (e: Throwable) ⇒ {
+ logger.error("Error execution request", e)
+
+ cdl.countDown()
+ },
+ (_: Unit) ⇒ ()
+ )
+ }
+
+ cdl.await(Long.MaxValue, TimeUnit.MILLISECONDS)
+
+ val allSynsStems = elemSyns.flatMap(_._2).flatten.map(_.stem).toSet
+
+ val nonEmptySuggs = allSuggs.asScala.map(p ⇒ p._1 → p._2.asScala).filter(_._2.nonEmpty)
+
+ val res = mutable.HashMap.empty[String, mutable.ArrayBuffer[NCSuggestion]]
+
+ nonEmptySuggs.
+ foreach { case (elemId, elemSuggs) ⇒
+ elemSuggs.
+ map(sugg ⇒ (sugg, toStem(sugg.word))).
+ groupBy { case (_, stem) ⇒ stem }.
+ filter { case (stem, _) ⇒ !allSynsStems.contains(stem) }.
+ map { case (_, group) ⇒
+ val seq = group.map { case (sugg, _) ⇒ sugg }.sortBy(-_.score)
+
+ // Drops repeated.
+ (seq.head, seq.length)
+ }.
+ toSeq.
+ map { case (sugg, cnt) ⇒ (sugg, cnt, sugg.score * cnt / elemSuggs.size) }.
+ sortBy { case (_, _, sumFactor) ⇒ -sumFactor }.
+ zipWithIndex.
+ foreach { case ((sugg, cnt, sumFactor), _) ⇒
+ val seq =
+ res.get(elemId) match {
+ case Some(seq) ⇒ seq
+ case None ⇒
+ val buf = mutable.ArrayBuffer.empty[NCSuggestion]
+
+ res += elemId → buf
+
+ buf
+ }
+
+ seq += NCSuggestion(sugg.word, sugg.score, cnt, sumFactor)
+ }
+ }
+
+ logger.whenInfoEnabled({
+ var i = 1
+
+ debugs.groupBy(_._1.example).foreach { case (_, m) ⇒
+ m.toSeq.sortBy(_._1.sentence).foreach { case (req, suggs) ⇒
+ val s =
+ split(req.sentence).
+ zipWithIndex.map { case (w, i) ⇒ if (i == req.index) s"<<<$w>>>" else w }.
+ mkString(" ")
+
+ logger.info(
+ s"$i. " +
+ s"Request=$s, " +
+ s"suggestions=[${suggs.map(_.word).mkString(", ")}], " +
+ s"element=${req.elementId}"
+ )
+
+ i = i + 1
+ }
+ }
+ })
+
+ res
+ }
+}