На изображении ниже без промедления показано, что мы создаем:

Чтобы ответить на вопрос в заголовке: «Что нужно для создания чат-бота?» ответ не так уж и много.

Я веб-разработчик. Я давно хотел окунуться в эту волнующую область. К сожалению, я не могу сказать, что обладаю знаниями в области естественного языка (NLU), необходимыми для создания чат-бота без посторонней помощи. Хорошая новость в том, что такая помощь доступна уже сегодня.

Cloud Natural Language API от Google, API-интерфейсы Cognitive Services от Microsoft и Watson Conversation от IBM предоставляют коммерческие услуги NLU с обширными бесплатными уровнями. Есть и совершенно бесплатные, по крайней мере, на данный момент. Сюда входят API.AI, недавно приобретенный Google, и Wit.ai, которым владеет Facebook.

С точки зрения веб-разработчика, это все, что нам нужно - API, который устранит для нас сложности.

Начнем с самого интересного

Если вы хотите увидеть пример вживую, вот демонстрация, доступная на Heroku. Весь код этого примера доступен на GitHub.

Для целей этой статьи мы создадим чат-бота под названием TiBot, чтобы ответить на наши вопросы о дате и времени. Мы будем использовать API API.A I. Для обработки этих вопросов. Я считаю, что API.AP более интуитивно понятен и проще в работе, чем Wit.ai.

На внутренней стороне простой сервер Node.js будет обрабатывать запросы, отправленные из внешнего приложения через WebSockets. Затем мы получим ответ от API языковой обработки. Наконец, мы отправим ответ через WebSockets.

Во внешнем интерфейсе у нас есть приложение, похожее на мессенджер, построенное на единственном угловом компоненте. Angular - это встроенный TypeScript (типизированный надмножество JavaScript). Если вы не знакомы ни с одним из них, вы все равно сможете понять код.

Я выбрал Angular, потому что он изначально использует RxJS (библиотека ReactiveX для JavaScript). RxJS обрабатывает асинхронные потоки данных удивительно мощным, но простым способом.

Настройка API.AI

В API.AI есть аккуратный раздел Документы. Во-первых, нам нужно познакомиться с некоторыми основными терминами и концепциями, относящимися к API, и знать NLU в целом.

После того, как мы создадим учетную запись в API.AI, нам нужно создать агента для запуска нашего проекта. С каждым агентом мы получаем ключи API - токены доступа клиента и разработчика. Мы используем токен клиентского доступа для доступа к API.

Агенты подобны проектам или модулям NLU. Важными частями агента являются намерения, сущности и действия и параметры.

Намерения - это ответы, возвращаемые API, или, согласно API.AI, соответствие между тем, что говорит пользователь, и тем, какие действия должно предпринять ваше программное обеспечение. Например, если пользователь говорит: Я хочу забронировать рейс, полученный результат должен выглядеть следующим образом:

{ ... "action": "book_flight" ... }

Сущности помогают извлекать данные из того, что говорит пользователь. Если пользователь говорит: Я хочу забронировать рейс в Париж, мы хотим получить информацию о Париже. Нам нужны эти данные, переданные в нашу логику, чтобы мы могли забронировать рейс в Париж для нашего пользователя. Результат должен выглядеть так:

{
  ...
  "action": "book_flight", 
  "parameters": {
    "destination": "Paris"
  }
  ...
}

Сущности - это значения параметров, как и типы данных. Платформа API.AI представляет собой системные сущности. Примеры включают @sys.date, @sys.color, @sys.number. Более сложные включают @sys.phone-number, @sys.date-period, @sys.unit-length-name.

Мы также можем определять наши собственные сущности или передавать их на лету с каждым запросом. Хороший пример передачи сущностей на лету - это когда пользователи слушают их списки воспроизведения. Пользователи имеют объект списка воспроизведения в своем запросе или сеанс пользователя со всеми песнями в списке воспроизведения. Мы сможем ответить на "Play Daydreaming", если пользователь в данный момент слушает плейлист Radiohead Пул в форме луны.

Действия и параметры отправляют запросы к API, чтобы они приводили к действию. Но они также могут привести к тому, что наш чат-бот не поймет. В этом случае мы можем вернуться к ответу по умолчанию.

Параметры - спутник действий. Они дополняют и завершают действие. В некоторых случаях нам не нужны параметры. Но есть случаи, когда действия имеют смысл только с параметрами. Примером может служить бронирование рейса, не зная пункта назначения. Это то, о чем нам нужно подумать, прежде чем мы даже начнем создавать намерения.

Наконец, следующий код показывает, как должен выглядеть ответ API для решенного намерения:

Самая важная часть JSON - это объект “result” со свойствами “action” и “parameters”, описанными выше. Достоверность решенного запроса (в диапазоне от 0 до 1) обозначается “score”. Если “score” равно нулю, наш запрос не был понят.

Стоит отметить, что массив “context” содержит информацию о неразрешенных намерениях, которые могут потребовать дополнительного ответа. Например, если пользователь говорит: «Я хочу забронировать рейс», мы обрабатываем book_flight” действие (контекст). Но чтобы получить необходимое “destination”, мы можем ответить: «Хорошо, куда бы вы хотели пойти?» и обработайте “destination” в следующем запросе.

Задняя часть

Мы создаем приложение для чата. Связь между клиентом и сервером будет осуществляться через WebSockets. Для этого мы будем использовать на сервере библиотеку Node.js WebSocket. Наш модуль WebSockets выглядит так:

Формат сообщений WebSockets - это строка в кодировке JSON со свойствами “type” и “msg”.

Строка “type” относится к одному из следующих:

“bot”, который отвечает пользователю.

“user”, который задает пользователь боту.

“sessionId”, который выдает уникальный идентификатор сеанса.

Ответ нашего чат-бота содержится в “msg”. Он отправляется обратно пользователю, на вопрос пользователя или sessionId.

processRequest(msg) представляет собой ядро ​​функциональности нашего сервера. Сначала он делает запрос к API:

Затем он выполняет withdoIntent() - конкретное действие для намерения пользователя, основанное на ответе от API:

doIntent() проверяет, есть ли в ответе функция для обработки действия. Затем он вызывает эту функцию с параметрами ответа. Если для действия нет функции или ответ не разрешен, он проверяет наличие отката от API. Или он вызывает handleUnknownAnswer().

Обработчики действий находятся в нашем модуле намерений:

Каждой функции-обработчику мы передаем параметры из ответа API. Мы также передаем часовой пояс пользователя, который получаем со стороны клиента. Поскольку мы имеем дело с датой и временем, оказывается, что часовой пояс играет важную роль в нашей логике. Это не имеет ничего общего с API или NLU в целом, а только с нашей конкретной бизнес-логикой.

Например, если пользователь в Лондоне в пятницу в 20:50 спрашивает нашего бота: «Какой сегодня день?» ответ должен быть: «Сегодня пятница».

Но если тот же пользователь спросит: «Какой сегодня день в Сиднее?» ответ должен быть таким: «В Сиднее суббота».

Местоположение также важно для нашей бизнес-логики. Мы хотим определить, откуда исходит вопрос (в случае Сиднея), чтобы мы могли получить часовой пояс для его местоположения. Для этого мы бы объединили Google Maps API геокодирования и API часовых поясов.

Передняя часть

Наше приложение представляет собой единый компонент Angular. Наиболее важные функции находятся в ngOnInit() методе компонента:

Сначала мы создаем соединение WebSocket (WS) с нашим сервером с помощью WS Observable. Затем мы подписываем на него пару наблюдателей.

Первый наблюдатель получает sessionId при подключении к серверу WebSocket. Сразу отписывается оператор take(1):

Вторая подписка веселая:

this.ws$.takeUntil(this.ngUnsubscribe$)
  .filter(r => r.type === 'bot')
  .retryWhen(err$ =>
    Observable.zip(err$, Observable.range(1, 3), (e, n) => n)
      .mergeMap(retryCount => Observable.timer(1000 * retryCount))
  )
  .delayWhen(inp => Observable.interval(100 + inp.msg.length * 10))
  .subscribe(
    (msg) => this.pushMsg(msg)
  );

Здесь мы хотим забирать сообщения только от бота, отсюда и оператор filter(r => r.type === ‘bot’). Оператор retryWhen(err$ => …) автоматически повторно подключается к WebSocket после его отключения.

Цель оператора delayWhen() - добиться эффекта «бот печатает», который используют мессенджеры. Для этого мы задерживаем данные на 100 + MSG_CHARACTERS_LENGTH * 10 миллисекунды.

Когда сообщение проходит через все операторы, мы помещаем его в наш массив сообщений (msg) => this.pushMsg(msg).

Мы используем закрытый pushMsg() метод компонента, чтобы добавить сообщение и показать его в чате:

Если сообщение отправлено пользователем (флаг clearUserMsg), мы очищаем поле ввода. Мы используем this.botIsTyping для управления эффектом «бот печатает». Итак, здесь мы установили значение false.

Мы обрабатываем вводимые пользователем данные с помощью метода onSubmit(), когда пользователь нажимает Enter:

Вместе с сообщением пользователя мы отправляем его идентификатор сеанса и часовой пояс. Они указаны в this.ws$.next(JSON.stringify(input)). Чтобы показать, что бот набирает эффект, мы также установили this.botIsTyping на true.

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

Это все, что нам нужно для нашего приложения в интерфейсе пользователя.

Удивительно видеть, насколько элегантным и чистым получился этот код. Спасибо RxJS. При использовании WebSockets все становится сложнее. Здесь мы сделали это с помощью одной строчки кода.

А наличие таких функций, как автоматическое переподключение - это отдельная история. Но с RxJS мы справились с этим очень просто.

В заключение, я надеюсь, вы понимаете, почему я сказал: «Это не займет много времени», чтобы ответить на вопрос: «Что нужно для создания чат-бота?»

Это не значит, что создание чат-бота - простая задача. Эти сервисы NLU, какими бы интеллектуальными они ни были, не решат всех наших проблем. Нам все еще нужно позаботиться о собственной бизнес-логике.

Пару лет назад мне было невозможно построить что-то подобное. Но такие сервисы, как API.AI, теперь делают эту возможность доступной для всех.

API.AI также обеспечивает интеграцию с Facebook Messenger, Twitter, Viber и Slack. Но в этой статье я подумал, что было бы лучше использовать их API, чтобы лучше понять, как все работает.

Надеюсь, вам понравилась эта статья, и вы сочли ее полезной при создании собственного чат-бота.