Обработка естественного языка (NLP)

Чтобы с навыком можно было общаться на естественном языке, он должен уметь корректно обрабатывать реплики пользователей:

  • распознавать задачу, которую хочет решить пользователь (например, заказать такси),
  • извлекать нужные детали (на какое число и по какому адресу).

Для упрощения задач NLP (Natural Language Processing) Диалоги предоставляют специальный инструмент — встроенный язык описания пользовательского запроса. С его помощью в консоли разработчика вы можете описать правила, по которым Диалоги будут классифицировать запросы и извлекать из них нужные данные.

Когда пользователь произносит команду, Диалоги распознают текст и извлекают те фразы, которые описывают намерения пользователя согласно вашим правилам. Распознанные данные Диалоги присылают в навык.

Что такое интент, форма и слоты

Чтобы формализовать разбор реплик, Диалоги используют интенты, формы и слоты.

Интент — это задача, которую пользователь формулирует в конкретной реплике. Например, узнать погоду. Каждому интенту соответствует одна форма.

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

Слот — поле формы. Каждый слот имеет название, тип данных и признак обязательности. Например, из реплики погода на завтра в Питере для заполнения слотов будут извлечены дата (завтра) и место (в Питере).

При обработке реплики Диалоги сначала определяют, к какому интенту она относится. После этого извлекают из реплики необходимые параметры и заполняют ими слоты формы. Распознанные данные Диалоги отправят в навык в поле запроса request.nlu. Если реплика не относится ни к одному интенту, поле request.nlu будет пустым (подробнее).

Синтаксис

Ниже показан пример описания интента:

# Описание интента "turn.on" для включения устройств.
# Эта грамматика позволит распознавать такие фразы как "включи свет на кухне"
# или "включи кондиционер в спальне". 

# Корневой элемент грамматики. Описывает шаблон, по которому будет
# отбираться реплика.
root:
    включи $What $Where

# Описание слотов. Диалоги будут отправлять это описание навыку.
slots:
    what:
        source: $What                   
    where:
        source: $Where
$What:
    свет | кондиционер
$Where:
    в ванной | на кухне | в спальне

Вложенные элементы следует отбивать отступом в 4 пробела.

Описание интента состоит из ключевых слов root, slots и filler, а также нетерминалов — фраз на естественном языке, описывающих, на какие запросы должна срабатывать грамматика. Нетерминалы обозначаются символом $. Они эквиваленты переменным в языках программирования. Нетерминалы можно скрыть, чтобы они были доступны только внутри родительского нетерминала.
Пример
$PlayGame:
    $Play в $Game
    $Play:
        %lemma
        играть
    $Game:
        игру

$Game:
    %lemma
    игра

Внутри нетерминала $PlayGame $Game сработает только на слово «игру», а снаружи — на все падежи слова «игра».

Поддерживаемые ключевые слова:

  • root — обозначение корневого элемента. Описывает шаблон, по которому будет отбираться вся реплика целиком.
    Пример
    root:
        [включи $What $Where (и $Where)*]

    В этом примере используются квантификатор и оператор [].

  • slots — описание слотов запроса. Это поле будет присутствовать в JSON, который Диалоги отправят в навык после обработки запроса. Подробнее см. Какие данные передаются в навык.
    Пример
    slots:
        what:
            source: $What                   
        where:
            source: $Where     
    $What:
        свет | кондиционер
    $Where:
        # Подойдет любая строка, которую введет пользователь.
        .+
  • filler — стоп-слова, которые можно отбросить при разборе запроса. Для исключения незначащих, неинформативных слов используется специальный классификатор, использующий контекст предложения. Например, для разбора из примера выше срабаботает как фраза «включи свет», так и «включи свет, пожалуйста».
    Пример
    filler:
        мне | как всегда | еще раз | нужно

Порядок описания элементов в грамматике не имеет значения.

Типизированные слоты

Слоты могут содержать не только строковое значение, но и именованные сущности:

  • YANDEX.NUMBER — числа;
  • YANDEX.FIO — имена;
  • YANDEX.DATETIME — даты;
  • YANDEX.GEO — геообъекты.

Для указания типизированного слота используются поля type и нетерминал, содержащий этот тип:

slots:
    from:
        source: $From
        type: YANDEX.NUMBER
    to:
        source: $To
        type: YANDEX.NUMBER
root:
    назови число от $From до $To
$From:
    $YANDEX.NUMBER
$To:
    $YANDEX.NUMBER

Пользовательские сущности в слотах

Чтобы задать собственные типы слотов, опишите их в разделе Сущности, например:

entity ChessPiece:
    values:
        queen:
            ферзь
            королева
        pawn:
            пешка

После этого тип станет доступен в качестве нетерминала грамматики и типа слота:

slots:
    piece:
        type: ChessPiece
        source: $Piece
root:
    ход $Piece
$Piece:
    $ChessPiece
При указании lemma: true в описании сущности все ее элементы будут сравниваться без учета формы слова.
Пример
entity ChessPiece:
    lemma: true
    values:
        queen:
            ферзь
            королева
        pawn:
            пешка

Сработает на пешка, пешку, пешкой.

lemma: true распространяется на всю сущность и не отменяется при помощи директивы %exact

Директивы

Директива — это специальная команда, переключающая парсер запросов в определенный режим работы. Директивы всегда начинаются с символа %. Например:

# Все последующие нетерминалы будут сравниваться без учета формы слова.
# Сработает как "включи свет", так и "включай свет".
root:
    %lemma
    включи свет
Действие директивы распространяется на все последующие нетерминалы. Директиву можно указать в начале всей грамматики, а также непосредственно перед нетерминалом. В последнем случае директива будет действовать до конца отступа или до отменяющей директивы. Пример:
filler:
    # Директива %lemma действует до %exact. Формы слов не учитываются.
    %lemma
    большое спасибо

    # Отменяем действие %lemma. Сработает только "всегда пожалуйста".
    %exact
    всегда пожалуйста

Ниже перечислены поддерживаемые директивы.

%lemma
Нетерминалы будут сравниваться без учета формы слова. Пример, в котором запросы включи свет и включай свет будут засчитаны как совпадение:
root:
    %lemma
    включи свет
%exact
Нетерминалы будут сравниваться по точному совпадению. Пример, в котором попадет под правило только запрос включи свет:
root:
    %exact
    включи свет
%negative
Внимание.

Отрицательные правила должны быть более конкретными, чем положительные. Например, если в обозначении корневого элемента root указать элемент включи сказку .*, а в %negativeвключи .*, то фраза включи сказку о лисе будет положительной в такой грамматике.

С помощью директивы %negative можно указать отрицательные примеры для элемента. Пример формы, которая сработает для условия включи игру города и не сработает для включи игру престолов:
form start_game:
    root:
        включи игру .*
        %negative
        включи игру $NotAGame
$NotAGame:
    %lemma
    престол

Директива %positive делает все последующие правила положительными.

Оператор []

Позволяет игнорировать порядок слов в грамматике. Пример:
root:
    [включи свет] 

В этом примере положительными срабатываниями будут включи свет и свет включи.

Квантификаторы

В описании элементов грамматики можно использовать квантификаторы:

  • ? — одно или ноль вхождений;
  • * — ноль или больше вхождений;
  • + — хотя бы одно вхождение.
Пример «включи свет»
root: 
    включи (пожалуйста)? свет

Совпадением будет как включи свет, так и включи, пожалуйста, свет.

Пример «включи свет на кухне»
root:
    включи свет $Where (и $Where)*
$Where:
    .+ # Хотя бы одно произвольное слово.

Совпадениями будут включи свет на кухне, включи свет на кухне и во всем доме и т. д.

Пример «включи свет на кухне и в ванной»
root:
    включи свет $Where (и $Where)+
$Where:
    на кухне | в ванной | в коридоре

Совпадениями будут включи свет на кухне и в ванной, включи свет на кухне, и в ванной, и в коридоре и т. д., но не просто «включи свет на кухне».

В слот попадет только первый распознанный нетерминал, если в одном интенте он используется несколько раз (только первый $Where из примеров выше попадет в слот).

Встроенные интенты

Если в навыке есть хотя бы один интент, Яндекс.Диалоги дополнительно отправляют интенты, универсальные для большинства навыков:

  • YANDEX.CONFIRM — согласие;
  • YANDEX.REJECT — отказ;
  • YANDEX.HELP — запрос подсказки;
  • YANDEX.REPEAT — просьба повторить последний ответ навыка.

Какие данные передаются в навык

После того как запрос будет обработан, Диалоги отправят навыку распознанные данные — в поле request.nlu. Это поле содержит идентификатор интента, а также описание заполненных слотов. Например:

"request": {
    "command": "включи свет на кухне, пожалуйста",
    "nlu": {
      "intents": {
        "turn.on": { // Интент.
          "slots": {  // Список слотов.
            "what": {
              "type": "YANDEX.STRING", 
              "value": "свет"          
            },
            "where": {
              "type": "YANDEX.STRING",
              "value": "на кухне"
            }
        }
      }
    },
    ...
}

Если реплика не относится ни к одному интенту, поле request.nlu будет пустым.

В случае типизированных слотов в запросе будет указан тип слота и его каноническое значение.

Пример для чисел
"request": {
    "command": "назови число от одного до шести",
    "nlu": {
        "intents": {
            "random_number": { // Интент.
                "slots": {  // Список слотов.
                    "from": {
                        "type": "YANDEX.NUMBER",
                        "value": 1
                    },
                    "to": {
                        "type": "YANDEX.NUMBER",
                        "value": 6
                    }
                }
            }
        }
    },
    ...
}
Пример «Закажи такси»
"request": {
    "command": "закажи такси на льва толстого 16 на 14:00",
    "nlu": {
      "intents": {
        "taxi": {
          "slots": {
            "where": {
              "type": "YANDEX.GEO",
              "tokens": {
                "start": 2,
                "end": 6
              },
              "value": {
                "street": "льва толстого",
                "house_number": "16"
              }
            },
            "time": {
              "type": "YANDEX.DATETIME",
              "tokens": {
                "start": 6,
                "end": 9
              },
              "value": {
                "hour": 14,
                "minute": 0
              }
            }
          }
        }
      }
    },
    ...
}
Пример для пользовательского типа
{
  "request": {
    "command": "включи свет на кухне, пожалуйста",
    "nlu": {
      "intents": {
        "turn.on": {
          "slots": {
            "what": {
              "type": "YANDEX.STRING",
              "value": "свет"
            },
            "where": {
              "type": "YANDEX.STRING",
              "value": "на кухне"
            }
          }
        }
      }
    }
  },
  ...
}