private def logMatches()

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


    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.getType}(${e.entity.mkText})"
                            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,
                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)

        tbl.warn(logger, "Two matching intents have the same weight for their matches (variants weight will be used further):".?)

    /**
     *
     * @param intent
     * @param senEnts
     * @param convEnts
     */
    private def solveIntent(
        ctx: NCContext, intent: NCIDLIntent, senEnts: Seq[IntentEntity], convEnts: Seq[IntentEntity], varIdx: Int
    ): Option[IntentMatchHolder] =
        val intentId = intent.id
        val opts = intent.options
        val flow = dialog.getDialogFlow(ctx.getRequest.getUserId)
        val varStr = s"(variant #${varIdx + 1})"

        // 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.getData // Conversation metadata (shared across all terms).
            val convMeta = sess.keysSet.map(k => k -> sess(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 usedConvEnts = convEnts.filter(_.used)
                val usedToks = usedSenEnts.flatMap(_.entity.getTokens)
                val unusedToks = ctx.getTokens.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.
                           |""".stripMargin
                    )
                    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("{", ",", "}")}