private def logMatch()

in nlpcraft/src/main/scala/org/apache/nlpcraft/internal/intent/matcher/NCIntentSolverManager.scala [523:714]


    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.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, "Term match found:".?)

    /**
     * Solves term.
     *
     * @param term
     * @param idlCtx
     * @param convEnts
     * @param senEnts
     */
    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 => E(s"Runtime error processing IDL term: $term", e)

    /**
     * Solves term's predicate.
     *
     * @param term
     * @param idlCtx
     * @param senEnts
     * @param convEnts
     */
    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)
            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)

            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.
            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

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