Данная статья является продолжением заметки “Проектируем интенты с Apache NlpCraft” и содержит детальное описание возможностей языка определения интентов NlpCraft IDL, созданного для использования в NLP проектах основанных на системе Apache NlpCraft. Поддержка NlpCraft IDL добавлена в систему начиная с версии 0.7.5.   

Новая версия декларативного языка определения интентов, получившая название NlpCraft IDL (NlpCraft Intents Definition Language), значительно упростила процесс работы с интентами в диалоговых и поисковых системах, построенных на базе проекта Apache NlpCraft и вместе с тем расширила возможности системы.  

NlpCraft IDL - это декларативный язык, позволяющий создавать определения интентов для их последующего использования в моделях Apache NlpCraft.

Начнем с примеров, демонстрирующих общие возможности языка, приведем необходимые пояснения, а далее опишем конструкции языка чуть более формально. 

Напомню, что в точке выбора интента, NLP системы как правило уже имеют на входе обработанный пользовательский запрос, включающий в себя все полученные при разборе запроса комбинации токенов и прочую сопутствующую информацию.

Примеры интентов, определенных с помощью NlpCraft IDL

intent=xa
    flow="^(?:login)(^:logout)*$"
    meta={'enabled': true}
    term(a)={!(tok_id()) != "z"}[1,3]
    term(b)={
        meta_intent('enabled') == true &&
        month() == 1
    } 
    term(c)~{
        @tokId = tok_id()
        @usrTypes = meta_model('user_types')

        (tokId == 'order' || tokId == 'order_cancel') &&   
         has_all(@usrTypes, list(1, 2, 3) &&
         abs(meta_tok('order:size')) > 10
        )
   } 

Пояснение:

  • Имя интента - “xa”. 

  • Интент содержит три terms. Term - это элемент, определяющий правило, каждое из которых должно быть выполнено для срабатывания интента.

    • Правило первое (a) - разобранный запрос должен содержать от одного до трех токенов с идентификаторами отличными от “z”,  без учетах данных из истории диалога (тип term =) . 

    • Правило второе (b) - интент должен быть активен в момент срабатывания -  флаг ”enabled” метаданных модели. Кроме того, такой интент может сработать лишь в январе - функция month().

    • Правило третье(c) - в запросе или в истории диалога (тип term ~) должен быть найден токен с идентификатором “order” или “order_cancel”. Также должны быть учтены ограничения, наложенные на значения метаданных модели и абсолютное значение размера ордера. В определении третьего правила используются переменные, о них мы поговорим чуть позже. 

  • Flow. В данном разделе определено дополнительное правило, согласно которому для срабатывания интента необходимо, чтобы в рамках текущей сессии уже хотя бы один раз сработал интент с идентификатором “login”, и ни разу не срабатывал интент с идентификатором “logout”. Правило определено в виде регулярного выражения, основанного на идентификаторах предыдущих интентов пользовательской сессии. Ниже также будут рассмотрены другие способы создания подобных правил.     

  • Meta. Для описываемого интента определен некий набор данных, в данном случае конфигурация, с помощью которой можно включить или выключить интент.

intent=xb
    flow=/#flowModelMethod/
    ordered=true
    term(a)=/org.mypackage.MyClass#termMethod/?
    fragment(frag)

Пояснение к следующему примеру:  

  • Имя интента - “xb”. 

  • Интент может содержать один term (”a”, опционально, согласно квантификатору “?“, детальные пояснения будут приведены ниже), определенный в коде -  org.mypackage.MyClass#termMethod. 

  • Fragment с идентификатором “frag” расширяет список terms интента,  дополнительными, ранее определенными terms. Элемент “frag” должен быть определен выше в коде или доступен с помощью import.

  • Flow содержит условие, заданное в коде модели в методе flowModelMethod.

Больше примеров - здесь.  

Разберем элементы языка более детально. 

Хочу обратить ваше внимание на то, что данная статья является лишь кратким обзором возможностей NlpCraft IDL и не пытается быть исчерпывающим мануалом. Перед началом полноценной работы с системой рекомендуется изучить детальное описание синтаксиса и всех возможностей языка в соответствующих разделах документации.

Место определения интентов

  • Интенты, определяемые с помощью NlpCraft IDL могут быть объявлены  непосредственно в файлах статического определения модели. Данный подход очень удобен для простых случаев. Пример доступен по ссылке

  • Также интенты могут быть определены непосредственно в коде модели с помощью аннотаций. Пример по ссылке.  

  • Кроме того, интенты могут быть определены в отдельных файлах. Модели при этом будет ссылаться на определение интентов согласно указанному пути к этим файлам или URL ресурсам с помощью элемента import. Данный подход удобен при работе с большими моделями, при редактировании определений которых может оказаться полезной подсветка синтаксиса и прочие возможности предоставляемые IDE (так, например, Intellij Idea обеспечивает подсветку ключевых слов, подсказки и проверку синтаксиса файлов произвольных типов согласно заданной конфигурации). Кроме того данный подход может быть полезен при работе специалистов, не имеющих возможности или желания редактировать код напрямую. Пример доступен по ссылкам: 1, 2.  

Ключевые слова NlpCraft IDL

flow, fragment, import, intent, meta, ordered, term, true, false, null.

  • intent, flow, fragment, meta, ordered, term - составные части определения интента. 

  • Ключевое слово fragment также используется для создания именованных списков terms, включаемых в определения интентов. 

  • import - необходим при подключении внешних файлов для возможности использования определенных в них элементов fragment, intent или других import.

  • true, false, null - используются при работе со встроенными функциями, о которых мы поговорим чуть ниже.

Структура программы NlpCraft IDL

Программа содержит набор следующих необязательных элементов, расположенных в произвольном порядке:

  • import

  • fragment

  • intent

Отладка и запуск

Интент компилируется при запуске модели и может быть отлажен лишь в процессе отладки модели. Для задания сложных интентов рекомендуется создавать их в отдельных файлах и использовать возможности редактирования, предоставляемые IDE для обеспечения начальной валидации сложных конструкций. Элементы раздела import и fragment могут быть задействованы и проверены только совместно с использующим эти элементы интентом.  

Структура определения импорта

Содержит ключевое слово “import” и имя файла или URL ресурса в круглых скобках.

Пример: import('http://mysite.com/nlp/idls/external.idl)

Структура определения фрагмента

Содержит ключевое слово “fragment” с именем и список terms. Terms в данном случае могут быть параметризованными. 

Пример простого fragment:

fragment=buzz term~{tok_id() == 'x:alarm'}

Пример параметризованного fragment c аргументами ‘a’ и ‘b’:

fragment=p1
    term={
        meta_frag('a') &&
        has_any(get(meta_frag('b'), 'Москва'), list(1, 2))
    }

Ниже приведен пример использования данного fragment в интенте. Аргументам ‘a’ и ‘b’ ставятся в соответствие значения параметров. 

intent=i1
    fragment(p1, {'a': true, 'b': {'Москва': [1, 2, 3]}})

Структура определения интента

Имя интента

Обязательный элемент. Имя - это уникальный в рамках модели идентификатор, необходимый для создания в колбеках ссылок на интент. Ниже приведен пример использования ссылки на интент "timeIntent". 

@NCIntentRef("timeIntent")
fun onTimeMatch(
    ctx: NCIntentMatch, 
    @NCIntentTerm("t1") tok: NCToken
): NCResult { ... }

Набор terms

Обязательный элемент. Term - это основной элемент определения интента. В каждом term задается правило срабатывания интента на основе сконфигурированного условия, тела term. Составные части правил могут относиться к токену или опираться на какие-то иные факторы. 

Как определятеся term:

  • Ключевое слово term. Обязательный элемент.

  • Имя в круглых скобках. Опционально. Служит для создания ссылок на найденный токен в аргументах колбека, смотри пример выше, токен “t1”.

  • Тип term. Обязательный элемент. Поддерживается два типа term: 

    • “~“ - токен может быть получен из истории диалога или из текущего запроса.

    • “=“ - токен должен быть получен только из текущего запроса.  

    Пример: term(nums)~{tok_id() == 'nlpcraft:num'}

    Выбор типа term имеет смысл только для terms относящихся к токенам, в противном случае значение выставленного типа игнорируется. 

  • Тело term. Обязательный элемент. Существуют два способа задания тела term: с помощью встроенных функций или с помощью программного кода. 

    Примеры:

    • term(nums)={tok_id() == 'nlpcraft:num'}

    • term(nums)~{true}

    • term~/org.mypackage.MyClass#termMethod/?

    Обратите внимание на специальный синтаксис последнего term. 

  • Квантификатор. Опционально. Поддерживаются следующие типы квантификаторов: 

    • [M, N] - условие должно сработать от N до M раз.

    • * - условие должно сработать хотя бы один раз, эквивалентно [0, ?]

    • + - условие должно сработать более одного раза, эквивалентно [1, ?]

    • ? - условие должно сработать 0 или 1 раз, эквивалентно [0, 1]

    Примеры:

    • term(nums)={tok_id() == 'nlpcraft:num'}[1,2] - запрос должен содержать один или два токена с идентификатором “nlpcraft:num”.

    • term(nums)={tok_id() == 'nlpcraft:num'}* - запрос должен содержать один или более токенов с идентификатором “nlpcraft:num”.

Подробнее о встроенных функциях NlpCraft IDL. 

Поддерживаемые в языке функции условно подразделяются на следующие типы:

  • Основанные на базовых свойствах токенов - идентификаторах, группах. Примеры: tok_id(), tok_groups(), tok_parent().

  • Основанные на NLP свойствах токенов - стеммах, леммах, частях речи, признаках стоп-слов. Примеры: tok_lemma(), tok_is_wordnet(), tok_swear().

  • Основанные на информации о том, как токен был обнаружен в пользовательском запросе - значениях синонимов и т.д.  Примеры: tok_value(), tok_is_permutated(), tok_is_direct().

  • Основанные на данных о пользовательском запросе - времени запроса, типе user agent. Примеры: req_tstamp(), req_addr(), req_agent().

  • Основанные на различных метаданных - токенов, модели, запроса и т.д.  Примеры: meta_model('my:prop'), meta_tok('nlpcraft:num:unit'), meta_user('my:prop').

  • Основанные на данных, предоставляемых NER провайдерами токенов. Пример, для “geo:city“ это может быть количество жителей или координаты,  полученные из метаданных. 

  • Основанные на данных пользователя системы и его компании - статусы, время регистрации. Примеры: user_admin(), comp_name(), user_signup_tstamp().

  • Основанные на системных данных - значениях переменных окружения, системном времени и т.д. Примеры: meta_sys('java.home'), now(), day_of_week().

  • Математические, текстовые функции, функции работы с коллекциями и т.д. Примеры: lowercase("TeXt"), abs(-1.5), distinct(list(1, 2, 2, 3, 1)).

Более детальная информация и описание каждой функции - по ссылке

Тело term - это предикат, построенный на основе комбинации этих функций. 

Для предотвращения необходимости повторных вычислений при работе с функциями, могут быть использованы локальные переменные, пример ниже:

term(t2)={
    @a = meta_model('a')
    @list = list(1, 2, 3, 4)

    (@a == 42 || @a == 44) && has_all(@list, list(3, 2))
}

Локальные переменные определяются с помощью специального префикса @. Их использование помогает избежать повторных вычислений и сокращает запись. 

Подразумевается, что существующего набора встроенных функций и средств NlpCraft IDL достаточно для определения большинства возможных term любого интента. Но при необходимости пользователь может создать свои собственные предикаты на java, scala, kotlin, groovy или другом java based языке и прописать их в теле term. То есть NlpCraft IDL может содержать ссылки на фрагменты кода, полностью написанного на других языках.    

Пример: term(a)=/MyClass#myMethod/

На входе пользовательская функция получает аргумент, содержащий все имеющиеся данные о пользовательском запросе, а на выходе должна вернуть значение определенного типа.

Fragment

Fragment это поименованный набор terms, создаваемый для возможности повторного использования в разных интентах. Пример по ссылке

Flow

Здесь мы определяем дополнительное правило срабатывания интента, опирающееся на данные по предыдущим срабатываниям в рамках сессии. 

Данное правило может быть определено на основе regex, составленного на основе идентификаторов предыдущих сработавших интентов.  

Пример такого определения: flow="^(?:login)(^:logout)*$" 

Данное правило означает, что для срабатывания интента необходимо, чтобы в рамках текущей сессии уже было срабатывание интента с идентификатором “login”, и не было с идентификатором “logout”. 

В случае необходимости задания более сложной логики, она также может быть вынесена в пользовательский код, написанный на Java-based языке, как и тело term. 

Пример определения в интенте: 

@NCIntent("intent=x 
    flow=/com.company.dialog.Flow#customFlow/ 
    term~{tok_id() == 'some_id'}"
)
def onX(): NCResult = { .. }

Предикат, определенный в методе customFlow(), получает на входе список с информацией по всем интентам, ранее вызванным в рамках текущей сессии, и возвращает значение типа boolean. 

Meta

Опциональный элемент. Набор данных для задания дополнительных условий срабатывания интента представленный в формате JSON.

Ordered flag

Опциональный элемент. По умолчанию false. Определяет правило - должны ли токены сработавших terms быть упорядочены в запросе.  

Зачем вообще нужен отдельный язык

Еще раз обратите внимание на то, что вся логика создания интентов, определенная с помощью NlpCraft IDL, может быть написана на любом java based языке. Зачем тогда вообще нужен этот новый язык? Даже если его синтаксис краток, прост и понятен, все равно придется потратить какое-то время на его изучение. 

Ниже приведем ряд аргументов в пользу использования NlpCraft IDL.

  • Краткость записи. Запись на специализированном DSL всегда короче записи той же логики на базовом языке. Для интентов с небанальными правилами это может быть важно. 

  • В случае если NlpCraft IDL код был вынесен в отдельный файл, то редактирование IDL программы, например для изменения логики срабатывания интента, не влечет за собой необходимости пересобирать код модели и ее колбеков.

  • Разнесение логики написания колбеков и логики срабатывания интентов. Данными вопросами могут заниматься разные специалисты. В силу осознанной ограниченности языковых средств, DSL проще для изучения.  

  • В настоящее время модели могут быть созданы на любом java based языке. В ближайших планах Apache NlpCraft расширение списка поддерживаемых языков. Использование NlpCraft IDL позволит сохранить один общий язык определения интентов для разных типов моделей в рамках одного проекта.

Заключение

Надеюсь вы смогли получить первое представление о возможностях языка определения интентов NlpCraft IDL и типах задач, которые можно решить с его помощью. Здесь вы найдете детальное описание языка и его возможностей. Дополнительные примеры моделей созданных на java, kotlin, groovy и scala, использующих для определения интентов NlpCraft IDL, доступны в гитхабе проекта.