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

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


The following commit(s) were added to refs/heads/NLPCRAFT-477 by this push:
     new 3fd1fab  NCIntentSolverEngine initial version added.
3fd1fab is described below

commit 3fd1fabbcd22769dcd8752d1a1998d667f77999c
Author: Sergey Kamov <skhdlem...@gmail.com>
AuthorDate: Tue Feb 15 14:30:40 2022 +0300

    NCIntentSolverEngine initial version added.
---
 .../main/scala/org/apache/nlpcraft/NCContext.java  |   3 +
 .../main/scala/org/apache/nlpcraft/NCToken.java    |   2 +-
 .../intent/matcher/NCIntentSolverEngine.scala      | 567 +++++++++++++++++++++
 .../intent/matcher/NCIntentSolverInput.scala}      |  47 +-
 .../intent/matcher/NCIntentSolverResult.scala}     |  56 +-
 .../intent/matcher/NCIntentSolverVariant.scala     |  10 +-
 .../dialogflow/NCDialogFlowManagerSpec.scala       |   1 +
 7 files changed, 602 insertions(+), 84 deletions(-)

diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCContext.java 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCContext.java
index 1db8097..1cde39f 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCContext.java
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCContext.java
@@ -56,4 +56,7 @@ public interface NCContext {
      * @return
      */
     Collection<NCVariant> getVariants();
+
+    // TODO:
+    List<NCToken> getTokens();
 }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java
index cbc9f75..2d6c87d 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java
@@ -20,7 +20,7 @@ package org.apache.nlpcraft;
 /**
  *
  */
-public interface  NCToken extends NCPropertyMap {
+public interface NCToken extends NCPropertyMap {
     /**
      *
      * @return
diff --git 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverEngine.scala
 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverEngine.scala
new file mode 100644
index 0000000..6053244
--- /dev/null
+++ 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverEngine.scala
@@ -0,0 +1,567 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.internal.intent.matcher
+
+import com.typesafe.scalalogging.LazyLogging
+import org.apache.nlpcraft.*
+import org.apache.nlpcraft.internal.ascii.NCAsciiTable
+import org.apache.nlpcraft.internal.dialogflow.NCDialogFlowManager
+import org.apache.nlpcraft.internal.intent.*
+
+import java.util.List as JList
+import java.util.function.Function
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+import scala.jdk.CollectionConverters.*
+import scala.language.postfixOps
+
+/**
+ * Intent solver that finds the best matching intent given user sentence.
+ */
+case class NCIntentSolverEngine(dialog: NCDialogFlowManager) extends 
LazyLogging:
+    /**
+     * NOTE: not thread-safe.
+     */
+    private class Weight(ws: Int*) extends Ordered[Weight]:
+        private var buf = mutable.ArrayBuffer[Int]() ++ ws
+
+        /**
+         * Adds given weight to this weight.
+         *
+         * @param that Weight to add.
+         * @return
+         */
+        def +=(that: Weight): Weight =
+            val buf2 = mutable.ArrayBuffer[Int]()
+
+            for (i <- 0 until Math.max(buf.size, that.buf.size))
+                buf2.append(norm(i, buf) + norm(i, that.buf))
+
+            buf = buf2
+
+            this
+
+        /**
+         * Appends new weight.
+         *
+         * @param w New weight to append.
+         * @return
+         */
+        def append(w: Int): Weight =
+            buf.append(w)
+
+            this
+
+        /**
+         * Prepends new weight.
+         *
+         * @param w New weight to prepend.
+         * @return
+         */
+        def prepend(w: Int): Weight =
+            buf.prepend(w)
+
+            this
+
+        /**
+         * Sets specific weight at a given index.
+         *
+         * @param idx
+         * @param w
+         */
+        def setWeight(idx: Int, w: Int): Unit =
+            buf(idx) = w
+
+        /**
+         * Gets element at given index or zero if index is out of bounds.
+         *
+         * @param i Index in collection.
+         * @param c Collection.
+         * @return
+         */
+        private def norm(i: Int, c: mutable.ArrayBuffer[Int]): Int = if i < 
c.size then c(i) else 0
+
+        /**
+         *
+         * @param that
+         * @return
+         */
+        override def compare(that: Weight): Int =
+            def compareWeight(idx: Int): Option[Int] =
+                val res = Integer.compare(norm(idx, buf), norm(idx, that.buf))
+                Option.when(res != 0)(res)
+
+            (0 until Math.max(buf.size, 
that.buf.size)).flatMap(compareWeight).to(LazyList).headOption.getOrElse(0)
+
+        def toSeq: Seq[Int] = buf.toSeq
+
+        override def toString: String = buf.mkString("[", ", ", "]")
+
+    /**
+     *
+     * @param used
+     * @param entity
+     */
+    private case class IntentEntity(
+        var used: Boolean,
+        var conv: Boolean,
+        entity: NCEntity
+    )
+
+    /**
+     * @param termId
+     * @param usedEntities
+     * @param weight
+     */
+    private case class TermMatch(termId: Option[String], usedEntities: 
Seq[IntentEntity], weight: Weight):
+        private lazy val maxIndex: Int = 
usedEntities.map(_.entity.getTokens.asScala.map(_.getIndex).max).max
+
+        def after(tm: TermMatch): Boolean = maxIndex > tm.maxIndex
+
+    /**
+      *
+      * @param entities
+      */
+    private case class PredicateMatch(entities: Seq[IntentEntity], weight: 
Weight)
+
+    /**
+     *
+     * @param term
+     * @param usedEntities
+     */
+    private case class TermEntitiesGroup(
+        term: NCIDLTerm,
+        usedEntities: Seq[IntentEntity]
+    )
+
+    /**
+     *
+     * @param entityGroups
+     * @param weight
+     * @param intent
+     */
+    private case class IntentMatch(
+        entityGroups: List[TermEntitiesGroup],
+        weight: Weight,
+        intent: NCIDLIntent
+    )
+
+    /**
+      *
+      * @param intentMatch
+      * @param callback
+      * @param variant
+      * @param variantIdx
+      */
+    private case class MatchHolder(
+        intentMatch: IntentMatch, // Match.
+        callback: NCIntentMatch => NCResult, // Callback function.
+        variant: NCIntentSolverVariant, // Variant used for the match.
+        variantIdx: Int // Variant index.
+    )
+
+    /**
+     * Main entry point for intent engine.
+     *
+     * @param ctx Query context.
+     * @param intents Intents to match for.
+     * @return
+     */
+    def solve(ctx: NCContext, intents: Map[NCIDLIntent, NCIntentMatch => 
NCResult]): List[NCIntentSolverResult] =
+        dialog.ack(ctx.getRequest.getUserId)
+
+        val matches = mutable.ArrayBuffer.empty[MatchHolder]
+
+        // Find all matches across all intents and sentence variants.
+        for (
+            (vrn, vrnIdx) <- ctx.getVariants.asScala.zipWithIndex;
+            ents = vrn.getEntities.asScala;
+            varEnts = ents.map(IntentEntity(false, false, _)).toSeq;
+            varEntsGroups = ents.map(t => if t.getGroups != null then 
t.getGroups.asScala else Set.empty[String]);
+            (intent, callback) <- intents
+        )
+            val convEnts: Seq[IntentEntity] =
+                if intent.terms.exists(_.conv) then
+                    // We do not mix tokens with same group from the 
conversation and given sentence.
+                    ctx.getConversation.getStm.asScala.toSeq.
+                        map(ent => ent -> (if ent.getGroups == null then 
Set.empty[String] else ent.getGroups.asScala)).
+                        filter { (ent, entGroups)  => 
!varEntsGroups.exists(_.subsetOf(entGroups)) }.
+                        map { (e, _) => IntentEntity(used = false, conv = 
true, e) }
+                else
+                    Seq.empty
+
+            // Solve intent in isolation.
+            solveIntent(ctx, intent, varEnts, convEnts, vrnIdx) match
+                case Some(intentMatch) => matches += MatchHolder(intentMatch, 
callback, NCIntentSolverVariant(vrn.getEntities.asScala.toSeq), vrnIdx)
+                case None => // No-op.
+
+        val sorted = matches.sortWith((m1: MatchHolder, m2: MatchHolder) =>
+            // 1. First with maximum weight.
+            m1.intentMatch.weight.compare(m2.intentMatch.weight) match { // Do 
not drop this bracket (IDE confused)
+                case x1 if x1 < 0 => false
+                case x1 if x1 > 0 => true
+                case x1 =>
+                    require(x1 == 0)
+
+                    logEqualMatches(m1, m2)
+
+                    // 2. First with maximum variant.
+                    m1.variant.compareTo(m2.variant) match
+                        case x2 if x2 < 0 => false
+                        case x2 if x2 > 0 => true
+                        case x2 =>
+                            require(x2 == 0)
+
+                            def calcHash(m: MatchHolder): Int =
+                                val variantPart =
+                                    m.variant.
+                                        entities.
+                                        map(t => 
s"${t.getId}${t.getGroups}${t.mkText()}").
+                                        mkString("")
+
+                                val intentPart = m.intentMatch.intent.toString
+
+                                (variantPart, intentPart).##
+
+                            // Order doesn't make sense here.
+                            // It is just to provide deterministic result for 
the matches with the same weights.
+                            calcHash(m1) > calcHash(m2)
+            }
+        )
+
+        logMatches(sorted)
+
+        sorted.map(m =>
+            NCIntentSolverResult(
+                m.intentMatch.intent.id,
+                m.callback,
+                m.intentMatch.entityGroups.map(grp => 
NCIntentEntitiesGroup(grp.term.id, grp.usedEntities.map(_.entity))),
+                m.variant,
+                m.variantIdx
+            )
+        ).toList
+
+    /**
+      *
+      * @param matches
+      */
+    private def logMatches(matches: ArrayBuffer[MatchHolder]): Unit =
+        if matches.nonEmpty then
+            val tbl = NCAsciiTable("Variant", "Intent", "Term Entities", 
"Intent Match Weight")
+
+            for (m <- matches)
+                val im = m.intentMatch
+                val w = im.weight
+                val ents = mutable.ListBuffer.empty[String]
+
+                ents += s"intent=${im.intent.id}"
+                var grpIdx = 0
+
+                for (grp <- im.entityGroups)
+                    ents += s"  ${grp.term.toString}"
+                    grpIdx += 1
+
+                    if grp.usedEntities.nonEmpty then
+                        var entIdx = 0
+
+                        for (e <- grp.usedEntities)
+                            val conv = if e.conv then "(conv) " else ""
+                            ents += s"    #$entIdx: $conv${e.entity}"
+                            entIdx += 1
+                    else
+                        ents += "    <empty>"
+
+                if m == matches.head then
+                    tbl += (
+                        Seq(s"#${m.variantIdx + 1}", "<|best match|>"), 
Seq(im.intent.id, "<|best match|>"), ents, w
+                    )
+                else
+                    tbl += (
+                        s"#${m.variantIdx + 1}", im.intent.id, ents, w
+                    )
+
+            tbl.info(
+                logger,
+                Option(s"Found ${matches.size} matching ${if matches.size > 1 
then "intents"else "intent"} (sorted best to worst):")
+            )
+        else
+            logger.info(s"No matching intent found:")
+            logger.info(s"  +-- Turn on DEBUG log level to see more details.")
+
+    /**
+      *
+      * @param m1
+      * @param m2
+      */
+    private def logEqualMatches(m1: MatchHolder, m2: MatchHolder): Unit =
+        val mw1 = m1.intentMatch.weight
+        val mw2 = m2.intentMatch.weight
+        val v1 = m1.variant
+        val v2 = m2.variant
+
+        val tbl = new NCAsciiTable()
+
+        tbl += (s"${"Intent ID"}", m1.intentMatch.intent.id, 
m2.intentMatch.intent.id)
+        tbl += (s"${"Variant #"}", m1.variantIdx + 1, m2.variantIdx + 1)
+        tbl += (s"${"Intent Match Weight"}", mw1.toString, mw2.toString)
+        tbl += (s"${"Variant Weight"}", v1.toString, v2.toString)
+
+        logger.warn(s"""Two matching intents have the same weight for their 
matches (variants weight will be used further):${tbl.toString}""")
+
+    /**
+     *
+     * @param intent
+     * @param senEnts
+     * @param convEnts
+     * @return
+     */
+    private def solveIntent(
+        ctx: NCContext, intent: NCIDLIntent, senEnts: Seq[IntentEntity], 
convEnts: Seq[IntentEntity], varIdx: Int
+    ): Option[IntentMatch] =
+        val intentId = intent.id
+        val opts = intent.options
+        val flow = dialog.getDialogFlow(ctx.getRequest.getUserId)
+        val varStr = s"(variant #${varIdx + 1})"
+        val flowRegex = intent.flowRegex
+
+        // Check dialog flow regex first, if any.
+        val flowMatched: Boolean =
+            intent.flowRegex match
+                case Some(regex) =>
+                    val flowStr = 
flow.map(_.getIntentMatch.getIntentId).mkString(" ")
+
+                    def process(matched: Boolean): Boolean =
+                        val s = if matched then "matched" else "did not match"
+                        logger.info(s"Intent '$intentId' $s regex dialog flow 
$varStr:")
+                        logger.info(s"  |-- ${"Intent IDs  :"} $flowStr")
+                        logger.info(s"  +-- ${"Match regex :"} 
${regex.toString}")
+
+                        matched
+
+                    process(regex.matcher(flowStr).find(0))
+                case None => true
+
+        if flowMatched then
+            val intentW = new Weight()
+            val intentGrps = mutable.ArrayBuffer.empty[TermEntitiesGroup]
+            var abort = false
+            var lastTermMatch: TermMatch = null
+            val sess = ctx.getConversation.getSession // Conversation metadata 
(shared across all terms). // TODO?
+            val convMeta = sess.keysSet().asScala.map(k => k -> 
sess.get(k).asInstanceOf[Object]).toMap
+            val ents = senEnts.map(_.entity)
+
+            // Check terms.
+            for (term <- intent.terms if !abort)
+                // Fresh context for each term.
+                val idlCtx = NCIDLContext(
+                    ctx.getModelConfig,
+                    ents,
+                    intentMeta = intent.meta,
+                    convMeta = convMeta,
+                    req = ctx.getRequest,
+                    vars = mutable.HashMap.empty[String, NCIDLFunction] ++ 
term.decls
+                )
+
+                solveTerm(term, idlCtx, senEnts, if term.conv then convEnts 
else Seq.empty) match
+                    case Some(termMatch) =>
+                        if opts.ordered && lastTermMatch != null && 
!termMatch.after(lastTermMatch) then
+                            abort = true
+                        else
+                            // Term is found.
+                            // Add its weight and grab its entities.
+                            intentW += termMatch.weight
+                            intentGrps += TermEntitiesGroup(term, 
termMatch.usedEntities)
+                            lastTermMatch = termMatch
+
+                            logMatch(intent, term, termMatch)
+                    case None =>
+                        // Term is missing. Stop further processing for this 
intent. This intent cannot be matched.
+                        logger.debug(s"Intent '$intentId' did not match 
because of unmatched term '$term' $varStr.")
+
+                        abort = true
+
+            if abort then
+                None
+            else
+                val usedSenEnts = senEnts.filter(_.used)
+                val unusedSenEnts = senEnts.filter(!_.used)
+                val usedConvEnts = convEnts.filter(_.used)
+                val usedToks = usedSenEnts.flatMap(_.entity.getTokens.asScala)
+                val unusedToks = ctx.getTokens.asScala.filter(p => 
!usedToks.contains(p))
+
+                if !opts.allowStmEntityOnly && usedSenEnts.isEmpty && 
usedConvEnts.nonEmpty then
+                    logger.info(
+                        s"Intent '$intentId' did not match because all its 
matched tokens came from STM $varStr. See intent 'allowStmEntityOnly' option."
+                    )
+
+                    None
+                else if !opts.ignoreUnusedFreeWords && unusedToks.nonEmpty then
+                    logger.info(
+                        s"Intent '$intentId' did not match because of unused 
free words $varStr. See intent 'ignoreUnusedFreeWords' option. Unused free 
words indexes: ${unusedToks.map(_.getIndex).mkString("{", ",", "}")}"
+                    )
+
+                    None
+                else
+                    if usedSenEnts.isEmpty && usedConvEnts.isEmpty then
+                        logger.warn(s"Intent '$intentId' matched but no 
entities were used $varStr.")
+
+                    // Number of remaining (unused) non-free words in the 
sentence is a measure of exactness of the match.
+                    // The match is exact when all non-free words are used in 
that match.
+                    // Negate to make sure the bigger (smaller negative 
number) is better.
+                    // TODO: check formula.
+                    val nonFreeWordNum = -(ctx.getTokens.size() - 
senEnts.map(_.entity.getTokens.size()).sum)
+
+                    intentW.prepend(nonFreeWordNum)
+
+                    Option(IntentMatch(entityGroups = intentGrps.toList, 
weight = intentW, intent = intent))
+        else
+            None
+
+    /**
+      *
+      * @param intent
+      * @param term
+      * @param termMatch
+      */
+    private def logMatch(intent: NCIDLIntent, term: NCIDLTerm, termMatch: 
TermMatch): Unit =
+        val tbl = NCAsciiTable()
+
+        val w = termMatch.weight.toSeq
+
+        tbl += ("Intent ID", s"${intent.id}")
+        tbl += ("Matched Term", term)
+        tbl += (
+            "Matched Entities",
+            termMatch.usedEntities.map(t =>
+                val txt = t.entity.mkText()
+                val idx = 
t.entity.getTokens.asScala.map(_.getIndex).mkString("{", ",", "}")
+
+                s"$txt${s"[$idx]"}").mkString(" ")
+        )
+        tbl += (
+            s"Term Match Weight", s"${"<"}${w.head}, ${w(1)}, ${w(2)}, 
${w(3)}, ${w(4)}, ${w(5)}${">"}"
+        )
+
+        tbl.debug(logger, Option("Term match found:"))
+
+    /**
+     * Solves term.
+     *
+     * @param term
+     * @param idlCtx
+     * @param convEnts
+     * @param senEnts
+     * @return
+     */
+    private def solveTerm(
+        term: NCIDLTerm,
+        idlCtx: NCIDLContext,
+        senEnts: Seq[IntentEntity],
+        convEnts: Seq[IntentEntity]
+    ): Option[TermMatch] =
+        if senEnts.isEmpty && convEnts.isEmpty then
+            logger.warn(s"No entities available to match on for the term 
'$term'.")
+
+        try
+            solvePredicate(term, idlCtx, senEnts, convEnts) match
+                case Some(pm) =>
+                    Option(
+                        TermMatch(
+                            term.id,
+                            pm.entities,
+                            // If term match is non-empty we add the following 
weights:
+                            //   - min
+                            //   - delta between specified max and normalized 
max (how close the actual quantity was to the specified one).
+                            //   - normalized max
+                            // NOTE: 'usedEntities' can be empty.
+                            pm.weight.
+                                append(term.min).
+                                append(-(term.max - pm.entities.size)).
+                                // Normalize max quantifier in case of unbound 
max.
+                                append(if term.max == Integer.MAX_VALUE then 
pm.entities.size else term.max)
+                        )
+                    )
+                // Term not found at all.
+                case None => None
+        catch case e: Exception => throw new NCException(s"Runtime error 
processing IDL term: $term", e)
+
+    /**
+     * Solves term's predicate.
+     *
+     * @param term
+     * @param idlCtx
+     * @param senEnts
+     * @param convEnts
+     * @return
+     */
+    private def solvePredicate(
+        term: NCIDLTerm,
+        idlCtx: NCIDLContext,
+        senEnts: Seq[IntentEntity],
+        convEnts: Seq[IntentEntity]
+    ): Option[PredicateMatch] =
+        // Algorithm is "hungry", i.e. it will fetch all entities satisfying 
item's predicate
+        // in entire sentence even if these entities are separated by other 
already used entities
+        // and conversation will be used only to get to the 'max' number of 
the item.
+        val usedEnts = mutable.ArrayBuffer.empty[IntentEntity]
+        var usesSum = 0
+        var matchesCnt = 0
+
+        // Collect to the 'max' from sentence & conversation, if possible.
+        for (ents <- Seq(senEnts, convEnts); ent <- ents.filter(!_.used) if 
usedEnts.lengthCompare(term.max) < 0)
+            // TODO: idx == matchesCnt - ok?
+            val NCIDLStackItem(res, uses) = 
term.pred.apply(NCIDLEntity(ent.entity, matchesCnt), idlCtx)
+
+            res match
+                case b: java.lang.Boolean =>
+                    if b then
+                        matchesCnt += 1
+
+                        if uses > 0 then
+                            usesSum += uses
+                            usedEnts += ent
+
+                case _ => throw new NCException(s"Predicate returned 
non-boolean result: $res")
+
+        // We couldn't collect even 'min' matches.
+        if matchesCnt < term.min then
+            None
+        // Term is optional (min == 0) and no matches found (valid result).
+        else if matchesCnt == 0 then
+            require(term.min == 0)
+            require(usedEnts.isEmpty)
+
+            Option(PredicateMatch(List.empty, new Weight(0, 0, 0)))
+        // We've found some matches (and min > 0).
+        else
+            // Number of entities from the current sentence.
+            val senTokNum = usedEnts.count(e => !convEnts.contains(e))
+
+            // Sum of conversation depths for each entities from the 
conversation.
+            // Negated to make sure that bigger (smaller negative number) is 
better.
+            // TODO: check formula.
+            def getConversationDepth(e: IntentEntity): Option[Int] =
+                val depth = convEnts.indexOf(e)
+                Option.when(depth >= 0)(depth + 1)
+
+            val convDepthsSum = -usedEnts.flatMap(getConversationDepth).sum
+            
+            // Mark found entities as used.
+            for (e <- usedEnts) e.used = true
+
+            Option(PredicateMatch(usedEnts.toSeq, new Weight(senTokNum, 
convDepthsSum, usesSum)))
\ No newline at end of file
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverInput.scala
similarity index 64%
copy from nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java
copy to 
nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverInput.scala
index cbc9f75..d64ac4c 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java
+++ 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverInput.scala
@@ -15,45 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft;
+package org.apache.nlpcraft.internal.intent.matcher
+
+import org.apache.nlpcraft.*
 
 /**
- *
+ * Input data for intent solver.
  */
-public interface  NCToken extends NCPropertyMap {
-    /**
-     *
-     * @return
-     */
-    String getText();
-
-    /**
-     *
-     * @return
-     */
-    int getIndex();
-
-    /**
-     *
-     * @return
-     */
-    int getStartCharIndex();
-
-    /**
-     *
-     * @return
-     */
-    int getEndCharIndex();
-
-    /**
-     *
-     * @return
-     */
-    String getLemma();
-
-    /**
-     *
-     * @return
-     */
-    String getPos();
-}
+case class NCIntentSolverInput(
+    context: NCContext,
+    intentMatch: NCIntentMatch = null
+)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverResult.scala
similarity index 63%
copy from nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java
copy to 
nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverResult.scala
index cbc9f75..a5c84d3 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/NCToken.java
+++ 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverResult.scala
@@ -15,45 +15,21 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft;
+package org.apache.nlpcraft.internal.intent.matcher
 
-/**
- *
- */
-public interface  NCToken extends NCPropertyMap {
-    /**
-     *
-     * @return
-     */
-    String getText();
-
-    /**
-     *
-     * @return
-     */
-    int getIndex();
+import org.apache.nlpcraft.*
 
-    /**
-     *
-     * @return
-     */
-    int getStartCharIndex();
-
-    /**
-     *
-     * @return
-     */
-    int getEndCharIndex();
-
-    /**
-     *
-     * @return
-     */
-    String getLemma();
-
-    /**
-     *
-     * @return
-     */
-    String getPos();
-}
+/**
+  * Intent solver engine result. Using basic case class for easier Java 
interop.
+  */
+case class NCIntentEntitiesGroup(
+    termId: Option[String],
+    entities: Seq[NCEntity]
+)
+case class NCIntentSolverResult(
+    intentId: String,
+    fn: NCIntentMatch => NCResult,
+    groups: Seq[NCIntentEntitiesGroup],
+    variant: NCIntentSolverVariant,
+    variantIdx: Int
+)
\ No newline at end of file
diff --git 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverVariant.scala
 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverVariant.scala
index 01a0505..95a292e 100644
--- 
a/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverVariant.scala
+++ 
b/nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverVariant.scala
@@ -51,8 +51,10 @@ case class NCIntentSolverVariant(entities: Seq[NCEntity]) 
extends Ordered[NCInte
 
     override def compare(other: NCIntentSolverVariant): Int =
         def compareWeight(weight1: Int, weight2: Int): Option[Int] =
-            if weight1 > weight2 then Option(1)
-            else if weight2 > weight1 then Option(-1)
-            else None
+            val res = Integer.compare(weight1, weight2)
+            Option.when(res != 0)(res)
 
-        weights.zip(other.weights).flatMap { (w1, w2) => compareWeight(w1, w2) 
}.to(LazyList).headOption.getOrElse(0)
\ No newline at end of file
+        weights.zip(other.weights).flatMap { (w1, w2) => compareWeight(w1, w2) 
}.to(LazyList).headOption.getOrElse(0)
+
+    // TODO:
+    override def toString: String = s"Weights: ${weights.mkString("[", ",", 
"]")}"
\ No newline at end of file
diff --git 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManagerSpec.scala
 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManagerSpec.scala
index 5e26746..55b4816 100644
--- 
a/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManagerSpec.scala
+++ 
b/nlpcraft/src/test/scala/org/apache/nlpcraft/internal/dialogflow/NCDialogFlowManagerSpec.scala
@@ -41,6 +41,7 @@ class NCDialogFlowManagerSpec:
         override def getRequest: NCRequest = NCTestRequest(txt = "Any", userId 
= userId, ts = reqTs)
         override def getConversation: NCConversation = null
         override def getVariants: util.Collection[NCVariant] = null
+        override def getTokens: util.List[NCToken] = null
 
     case class ModelConfigMock(timeout: Long = Long.MaxValue) extends 
NCModelConfig("testId", "test", "1.0", "Test description", "Test origin"):
         override def getConversationTimeout: Long = timeout

Reply via email to