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