--- active_crumb: Intent Matching layout: documentation id: intent_matching ---

Overview

{% scaladoc NCModel NCModel %} processing logic is defined as a pipeline and the collection of one or more intents to be matched on. The sections below explain what intent is, how to define it in your model, and how it works.

Intent

The goal of the data model implementation is to take the user input text, pass it through processing pipeline and match the resulting variants to a specific user-defined code that will execute for that input. The mechanism that provides this matching is called an intent.

The intent generally refers to the goal that the end-user had in mind when speaking or typing the input utterance. The intent has a declarative part or template written in IDL - Intent Definition Language that strictly defines a particular form the user input. Intent is also bound to a callback method that will be executed when that intent, i.e. its template, is detected as the best match for a given input. A typical data model will have multiple intents defined for each form of the expected user input that model wants to react to.

For example, a data model for banking chatbot or analytics application can have multiple intents for each domain-specific group of input such as opening an account, closing an account, transferring money, getting statements, etc.

Intents can be specific or generic in terms of what input they match. Multiple intents can overlap and NLPCraft will disambiguate such cases to select the intent with the overall best match. In general, the most specific intent match wins.

IDL Syntax

NLPCraft intents are written in Intent Definition Language (IDL). IDL is a relatively straightforward declarative language. For example, here's a simple intent x with two terms a and b:

            /* Intent 'x' definition. */
            intent=x
                term(a)~{# == 'my_elm'} // Term 'a'.
                term(b)={has(ent_groups, "my_group")} // Term 'b'.
        

IDL intent defines a match between the parsed user input represented as the collection of {% scaladoc2 NCEntity entities %}, and the user-define callback method. IDL intents are bound to their callbacks via Java annotation and can be located in the same Java annotations or in external *.idl files.

You can review the formal ANTLR4 grammar for IDL, but here are the general properties of IDL:

IDL program consists of intent, fragment, or import statements in any order or combination:

Intent Lifecycle

During {% scaladoc NCModelClient NCModelClient %} initialization it scans the provided model class for the intents. All found intents are compiled into an internal representation.

Note that not all intent-related problems can be detected at the compilation phase, and {% scaladoc NCModelClient NCModelClient %} can be initialized with intents not being completely validated. For example, each term in the intent must evaluate to a boolean result. This can only be checked at runtime. Another example is the number and the types of parameters passed into IDL function which is only checked at runtime as well.

Intents are compiled only once during the {% scaladoc NCModelClient NCModelClient %} initialization and cannot be re-compiled. Model logic, however, can affect the intent behavior through {% scaladoc NCModel NCModel %} callback methods and metadata all of which can change at runtime and are accessible through IDL functions.

Intent Examples

Here's few of intent examples with explanations:

Example 1:

        intent=a
            term~{# == 'x:type'}
            term(nums)~{# == 'num' && lowercase(meta_ent('num:unittype')) == 'datetime'}[0,2]
        

NOTES:


Example 2:

        intent=id2
            flow='id1 id2'
            term={# == 'myent' && signum(get(meta_ent('score'), 'best')) != -1}
            term={has_any(ent_groups, list('actors', 'owners'))}
        

NOTES:

IDL Functions

IDL provides over 100 built-in functions that can be used in IDL intent definitions. IDL function call takes on traditional fun_name(p1, p2, ... pk) syntax form. If function has no parameters, the brackets are optional. IDL function operates on stack - its parameters are taken from the stack and its result is put back onto stack which in turn can become a parameter for the next function call and so on. IDL functions can have zero or more parameters and always have one result value. Some IDL functions support variable number of parameters.

Special Shorthand #

The frequently used IDL function ent_type() has a special shorthand #. For example, the following expressions are all equal:

                ent_type() == 'type'
                ent_type == 'type' // Remember - empty parens are optional.
                # == 'type'
            

When chaining the function calls IDL uses mathematical notation (a-la Python) rather than object-oriented one: IDL length(trim(" text ")) vs. OOP-style " text ".trim().length().

IDL functions operate with the following types:

JVM Type IDL Name Notes
java.lang.StringString
java.lang.Long
java.lang.Integer
java.lang.Short
java.lang.Byte
Long Smaller numerical types will be converted to java.lang.Long.
java.lang.Double
java.lang.Float
Double java.lang.Float will be converted to java.lang.Double.
java.lang.BooleanBooleanYou can use true or false literals.
java.util.List<T>List[T]Use list(...) IDL function to create new list.
java.util.Map<K,V>Map[K,V]
{% scaladoc NCEntity NCEntity %}Entity
java.lang.ObjectAnyAny of the supported types above. Use null literal for null value.

Some IDL functions are polymorphic, i.e. they can accept arguments and return result of multiple types. Encountering unsupported types will result in a runtime error during intent matching. It is especially important to watch out for the types when adding objects to various metadata containers and using that metadata in the IDL expressions.

Unsupported Types

Detection of the unsupported types by IDL functions cannot be done during IDL compilation and can only be done during runtime execution. This means that even though the model compiles IDL intents and {% scaladoc NCModelClient NCModelClient %} starts successfully - it does not guarantee that intents will operate correctly.

All IDL functions are organized into the following groups:

{% for fn in site.data.idl-fns.fn-ent %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}
{% for fn in site.data.idl-fns.fn-text %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}
{% for fn in site.data.idl-fns.fn-math %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}
{% for fn in site.data.idl-fns.fn-collections %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}
{% for fn in site.data.idl-fns.fn-metadata %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}
{% for fn in site.data.idl-fns.fn-datetime %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}
{% for fn in site.data.idl-fns.fn-req %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}
{% for fn in site.data.idl-fns.fn-other %}

Description:
{{fn.desc}}

Usage:

{{fn.usage}}
{% endfor %}

IDL Location

IDL declarations can be placed in different locations based on user preferences:

Binding Intent

IDL intents must be bound to their callback methods. This binding is accomplished using the following Java annotations:

Annotation Target Description
@NCIntent Callback method or model class

When applied to a method this annotation allows to define IDL intent in-place on the method serving as its callback. This annotation can also be applied to a model's class in which case it will just declare the intent without binding it and the callback method will need to use @NCIntentRef annotation to actually bind it to the declared intent above. Note that multiple intents can be bound to the same callback method, but only one callback method can be bound with a given intent.

This method is ideal for simple intents and quick declaration right in the source code and has all the benefits of having IDL to be part of the source code. However, multi-line IDL declaration can be awkward to add and maintain depending on Scala language, i.e. multi-line string literal support. In such cases it is advisable to move IDL declarations into separate *.idl file or files and import them at the model class level.

@NCIntentRef Callback method This annotation allows to reference an intent defined elsewhere like an external *.idl file, or other @NCIntent annotations. In real applications, this is a most common way to bound an externally defined intent to its callback method.
@NCIntentObject Model class field Marker annotation that can be applied to class member of main model. The fields objects annotated with this annotation are scanned the same way as main model.
@NCIntentTerm Callback method parameter This annotation marks a formal callback method parameter to receive term's entities when the intent to which this term belongs is selected as the best match.

Here's a couple of examples of intent declarations to illustrate the basics of intent declaration and usage.

An intent from Light Switch example:

            @NCIntent("intent=ls term(act)={has(ent_groups, 'act')} term(loc)={# == 'ls:loc'}*")
            def onMatch(
                @ctx: NCContext,
                @im: NCIntentMatch,
                @NCIntentTerm("act") actEnt: NCEntity,
                @NCIntentTerm("loc") locEnts: List[NCEntity]
            ): NCResult = {
                ...
            }
        

NOTES:

In the following Time example the intent is defined model class and referenced in code using @NCIntentRef annotation:

            @NCIntent("fragment=city term(city)~{# == 'opennlp:location'}")
            @NCIntent("intent=intent2 term~{# == 'x:time'} fragment(city)")
            class TimeModel extends NCModel(
                ...
                @NCIntentRef("intent2")
                private def onRemoteMatch(
                    ctx: NCContext, im: NCIntentMatch, @NCIntentTerm("city") cityEnt: NCEntity
                ): NCResult =
                    ...
        

NOTES:

Intent Matching Logic

{% scaladoc NCPipeline NCPipeline %} processing result is collection of {% scaladoc NCVariant NCVariant %} instances. {% scaladoc nlp/parsers/NCSemanticEntityParser NCSemanticEntityParser %} is used for following example configured via JSON file. Let's consider the input text 'A B C D' and the following elements defined in our model:

            "elements": [
                {
                    "id": "elm1",
                    "synonyms": ["A B"]
                },
                {
                    "id": "elm2",
                    "synonyms": ["B C"]
                },
                {
                    "id": "elm3",
                    "synonyms": ["D"]
                }
            ],
            

All of these elements will be detected but since two of them are overlapping (elm1 and elm2) there should be two parsing variants at the output of this step:

  1. elm1('A', 'B') freeword('C') elm3('D')
  2. freeword('A') elm2('B', 'C') elm3('D')

Note that initially the system cannot determine which of these variants is the best one for matching - there's simply not enough information at this stage. It can only be determined when each variant is matched against model's intents. So, each parsing variant is matched against each intent. Each matching pair of a variant and an intent produce a match with a certain weight. If there are no matches at all - an error is returned. If matches were found, the match with the biggest weight is selected as a winning match. If multiple matches have the same weight, their respective variants' weights will be used to further sort them out. Finally, the intent's callback from the winning match is called.

Although details on exact algorithm on weight calculation are too complex, here's the general guidelines on what determines the weight of the match between a parsing variant and the intent. Note that these rules coalesce around the principle idea that the more specific match always wins:

Intent Callback

Whether the intent is defined directly in @NCIntent annotation or indirectly via @NCIntentRef annotation - it is always bound to a callback method:

@NCIntentTerm annotation marks callback parameter to receive term's entities. This annotations can only be used for the parameters of the callbacks, i.e. methods that are annotated with @NCIntnet or @NCIntentRef. @NCIntentTerm takes a term ID as its only mandatory parameter and should be applied to callback method parameters to get the entities associated with that term (if and when the intent was matched and that callback was invoked).

Depending on the term quantifier the method parameter type can only be one of the following types:

Quantifier Scala Type
[1,1] NCEntity
[0,1] Option[NCEntity]
[1,∞] or [0,∞] List[NCEntity]

For example:

            NCIntent("intent=id term(termId)~{# == 'my_ent'}?")
            private def onMatch(
                ctx: NCContext,
                im: NCIntentMatch,
                @NCIntentTerm("termId") myEnt: Option[NCEntity]
            ): NCResult = {
               ...
            }
        

NOTES:

NCRejection and NCIntentSkip Exceptions

There are two exceptions that can be used by intent callback logic to control intent matching process.

When {% scaladoc NCRejection NCRejection %} exception is thrown by the callback it indicates that user input cannot be processed as is. This exception typically indicates that user has not provided enough information in the input string to have it processed automatically. In most cases this means that the user's input is either too short or too simple, too long or too complex, missing required context, or is unrelated to the requested data model.

{% scaladoc NCIntentSkip NCIntentSkip %} is a control flow exception to skip current intent. This exception can be thrown by the intent callback to indicate that current intent should be skipped (even though it was matched and its callback was called). If there's more than one intent matched the next best matching intent will be selected and its callback will be called.

This exception becomes useful when it is hard or impossible to encode the entire matching logic using only declarative IDL. In these cases the intent definition can be relaxed and the "last mile" of intent matching can happen inside of the intent callback's user logic. If it is determined that intent in fact does not match then throwing this exception allows to try next best matching intent, if any.

Note that there's a significant difference between {% scaladoc NCIntentSkip NCIntentSkip %} exception and model's {% scaladoc NCModel#onMatchedIntent-946 NCModel#onMatchedIntent %} callback. Unlike this callback, the exception does not force re-matching of all intents, it simply picks the next best intent from the list of already matched ones. The model's callback can force a full reevaluation of all intents against the user input.

IDL Expressiveness

Note that usage of NCIntentSkip exception (as well as model's life-cycle callbacks) is a required technique when you cannot express the desired matching logic with only IDL alone. IDL is a high-level declarative language and it does not support a complex programmable logic or other types of sophisticated matching algorithms. In such cases, you can define a broad intent that would broadly match and then define the rest of the more complex matching logic in the callback using NCIntentSkip exception to effectively indicate when intent doesn't match and other intents, if any, have to be tried.

There are many use cases where IDL is not expressive enough. For example, if your intent matching depends on financial market conditions, weather, state from external systems or details of the current user geographical location or social network status - you will need to use NCIntentSkip-based logic or model's callbacks to support that type of matching.

NCContext Trait

{% scaladoc NCContext NCContext %} trait passed into intent callback as its first parameter. This trait provide runtime information about the model configuration, request, extracted tokens and all entities variants, conversation control trait {% scaladoc NCConversation NCConversation %}.

NCIntentMatch Trait

{% scaladoc NCIntentMatch NCIntentMatch %} trait passed into intent callback as its second parameter. This trait provide runtime information about the intent that was matched (i.e. the intent with which this callback was annotated with).