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

Одним из инструментов создания диалоговых помощников является Rasa — сценарная платформа машинного обучения с открытым исходным кодом.

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

Вводная

В рамках задачи нужно было интегрировать помощника Rasa в социальную сеть VK. В этой статье и поговорим о том как это сделать.

Руководство предполагает, что вы уже создали проект Rasa и обучили модель.

Rasa Connector

В Rasa уже имеется модуль для вывода взаимодействия с моделью за пределами командной строки и он называется Rasa Connector.

Коннектор — это модуль, который предоставляет внешним ресурсам делать запросы к Rasa. Он принимает внешний запрос обрабатывает его и приводит к нужному формату, затем отправляя запрос на сервер Rasa. Далее он обрабатывает ответ и отправляет обратно во внешний ресурс.

Архитектура

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

Создание коннектора

Перейдем к написанию кода. Мы рассмотрим создание коннектора на примере VK с использованием Callback API. Но вы также можете использовать это руководство при создании коннекторов для других сервисов.

Шаг 1. Создание структуры файлов

Создаем директорию, например connectors, в корне проекта Rasa и далее в ней создаем файл vk.py в нём мы и будем создавать наш коннектор

Структура файлов

Шаг 2. Добавление учётных данных

В нашем случае с VK понадобятся ключ доступа и строка-подтверждение. Чтобы узнать как их получить обратитесь к документации CallbackAPI.

Далее добавляем их в credentials.yml, но также нужно написать путь до InputChnnel коннектора: <директория>.<файл>.<название_класса>, так Rasa будет знать к чему относить эти данные.

connectors.vk.VkInputChannel:
 access_token: '<YOUR_ACCESS_TOKEN>'  # ключ доступа
 confirmation_token: '<YOUR_CONF_TOKEN>'  # строка подтверждение

Шаг 3. Создаем InputChannel

Первое что нам необходимо сделать, это создать свой класс входного канала VkInputChannel, он будет наследоваться от базового класса InputChannel, который предлагает нам Rasa, в нем объявлены методы для определения префикса URL-адреса веб-хука, создания схемы Sanic, а также получения токенов из credentials.yml.

Есть и другие методы, которые мы не будем использовать в данном коннекторе, их можно посмотреть в документации.

Файл vk.py

Шаг 4. Определяем создание обьекта

Объект класса VkInputChannel будет содержать всю информацию которую мы указали для этого канала в credentials.yml. Это необходимо чтобы мы могли отправлять обратные ответы с помощью API.

   def __init__(
       self,
       access_token: Optional[Text],
       confirmation_token: Optional[Text],
   ) -> None:
       self.access_token = access_token  # ключ доступа
       self.confirmation_token = confirmation_token # строка подтверждение

Шаг 5. Переопределение методов

Так как InputChannel предоставляет нам только описание методов, необходимо переопределить их для конкретного канала.

Метод name определяет префикс URL-адреса веб-хука коннектора, итоговый адрес будет выглядеть http://<host>:<port>/webhooks/vk/webhook , где хост и порт это соответствующие значения работающего сервера Rasa.

   @classmethod
   def name(cls) -> Text:
       return 'vk'

Переопределим еще и метод получения данных из credentials.yml, Rasa сама загружает нужные данные и преобразует их в Dict.

   @classmethod
   def from_credentials(
       cls,
       credentials: Optional[Dict[Text, Any]],
   ) -> InputChannel:
   # Базовый метод вызова ошибки, при отсутствии credentials.
       if not credentials:
           cls.raise_missing_credentials_exception()
       # Возвращаем __init__ с полученными токенами.
       return cls(
           credentials.get('access_token'),
           credentials.get('confirmation_token'),
       )

Шаг 6. Создание OutputChannel

Второй класс который мы создадим это VkOutputChannel, он наследуется от базового класса OutputChannel. Последний реализует методы отправки сообщений разного формата(текст, картинка и тп.) обратно в канал обращения.

class VKOutputChannel(OutputChannel):

Здесь мы тоже определим метод name.

   @classmethod
   def name(cls) -> Text:
       return 'vk'

В объекте VkOutputChannel, нам потребуется использовать VkAPI для отправки ответного сообщения.

   def __init__(
       self,
       access_token: Optional[Text],
   ) -> None:
       self.vk = VkApi(token=access_token)

Переопределяем метод отправки ответа, здесь мы вызываем у обьекта класса VkApi метод отправки текстового сообщения, и задаём user_id и text , а также random_id необходимый для того, чтобы ваш бот не отравлял одно и то же сообщение несколько раз.

   async def send_text_message(
       self,
       recipient_id: Text,
       text: Text,
       **kwargs: Any,
   ) -> None:
       for message_part in text.strip().split('\n\n'):
           self.vk.method(
               'messages.send',
               {
                   'user_id': recipient_id,
                   'message': message_part,
                   'random_id': get_random_id(),
               }
           )

Шаг 7. Добавление метода blueprint в InputChannel

Этот метод будет создает схему Sanic, и закрепляет ее за сервером Sanic, который будет обрабатывать входящие маршруты. В методе мы также создадим объект класса VKOutputChannel .

Sanic — быстрый асинхронный веб‑сервер и веб‑фреймворк, использующий синтаксис async/await.

   def blueprint(
       self,
       on_new_message: Callable[[UserMessage], Awaitable[Any]]
   ) -> Blueprint:
       # webhooks/vk
       webhook = Blueprint('webhook', __name__)
       output_channel = VKOutputChannel(self.access_token)

Нам необходимо создать минимум два маршрута / и /webhook (подробнее в документации).

       # webhooks/vk/
       @webhook.route('/', methods=['GET'])
       async def health(request: Request) -> HTTPResponse:
           return response.json({'status': 'ok'})

На этот маршрут будут прилетать запросы от VK.

	   # webhooks/vk/webhook
       @webhook.route('/webhook', methods=['POST'])
       async def recieve(request: Request) -> HTTPResponse:
           # Берем из запроса json
           request = request.json
           # Если запрос в виде строки, то преобразуем
           if isinstance(request, Text):
               request = json.loads(request)

Но, если запрос прилетает некорректный или вообще не от VK, мы тоже это должны обработать, для этого обратимся к структуре входящего JSON.

           # Если пришел некорректный запрос
           if 'type' not in request.keys():
               return response.text('not vk')

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

          # Обработка запроса строки-подтверждения
          if request['type'] == 'confirmation':
              return response.text(self.confirmation_token)
          
          # Обработка нового сообщения от пользователя
          if request['type'] == 'message_new':
              sender_id = request['object']['message']['from_id']
              text = request['object']['message']['text']

И наконец мы говорим Rasa обработать запрос, и формируем UserMessage. Важно отметить, в случае с VK, для каждого события сервер должен отвечать 'ok' в случае успешного запроса, подробнее тут.

             metadata = self.get_metadata(request)
             # Говорим Rasa обработать сообщение пользователя
             await on_new_message(
                 UserMessage(
                     text,
                     output_channel,
                     sender_id,
                     input_channel=self.name(),
                     metadata=metadata,
                 )
             )


         return response.text('ok')
     return webhook

Шаг 8. Запуск сервера Rasa

Далее запустите сервер раса c вашей обученой моделью командой:

rasa run

Шаг 9. Настройка Ngrok

Так как мы разворачиваем сервер Rasa на локально, то нужно создать публичный адрес на который будут приходить запросы и перебрасывать их на наш локальный адрес и заданный порт. Для этого будем использовать ngrok, но подойдет и любая другая альтернатива, к примеру localtunnel или Pagekite.

Установка ngrok

1. Установим и добавим токен ngrok по инструкции.

2. Создаем статический домен ngrok здесь.

3. Далее идём в терминал и открываем туннель с портом 5005

ngrok http --domain=master-positively-flounder.ngrok-free.app 5005

Шаг 10. Настройка сервиса CallbackAPI

1. Для начала создадим сообщество в VK, и в его настройках включим "Сообщения сообщества".

2. Далее переходим в раздел "Работа с API", создаем ключ доступа, его и записываем в credentials.yml.

3. Идём в "CallbackAPI" в разделе "Типы событий" выберем "Входящие сообщения", так бот не будет реагировать на другие типы событий.

4. В "Настройках сервера" нужно скопировать строку которую будет возвращать сервер и записать в credentials.

5. В адрес указываем созданный ранее публичный адрес и подтверждаем.

https://master-positively-flounder.ngrok-free.app/webhooks/vk/webhook

Результат работы

Переходим в диалог с сообществом и теперь мы можем вести общение с нашим помощником.

Как происходит взаимодействие?

На диаграмме ниже я представил взаимодействие всех компонентов.Пользователь обращается к помощнику с приветствием, далее запрос отправляется на публичный адрес ngrok, который перенаправляет его на наш сервер. Запрос обрабатывается классом InputChannel и потом отправляет запрос в Rasa для получения ответа от модели. Rasa возвращает ответ и OutputChannel отправляет его в качестве ответа помощнику, а тот пользователю.

Итог

Надеюсь это руководство было вам полезно! Теперь вы сможете самостоятельно интегрировать чат-бота Rasa, и улучшить user experience во взаимодействии в с диалоговым искусственным интеллектом. Если возникнут проблемы, приглашаю обсудить их в комментариях.

Полезные ссылки