Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase

В конце прошлого месяца состоялся официальный релиз Google Ассистента на русском языке, так что самое время разобраться, как делать свои приложения (экшены) для Ассистента на стандартном технологическом стеке Google. В этой статье мы рассмотрим создание экшена в Actions on Google, разберём процесс извлечения сущностей и интентов из фраз в Dialogflow, узнаем, как писать обработчики извлеченной информации и работать с сетью в Cloud Functions for Firebase.


Рис. 1. Архитектура приложения для Ассистента.

Разработка под Ассистента начала активно развиваться сравнительно недавно, поэтому в сети пока мало материалов, а количество используемых инструментов и технологий существенно повышает порог вхождения. Эта статья хоть и не решает, но как минимум способствует решению упомянутых проблем. Начнем с архитектуры приложений для Ассистента (рис. 1), реализованных на стандартном технологическом стеке Google:

  • Actions on Google — платформа для создания приложений для Google Ассистента.
  • Dialogflow — NLU-движок (Natural Language Understanding), отвечающий за обработку естественных языков и дизайн диалогов.
  • Cloud Functions for Firebase (для удобства будем использовать сокращение Firebase Functions) — облачные функции для обработки сложной логики взаимодействия с пользователем и для работы со сторонними сервисами. Firebase Functions и Dialogflow взаимодействуют через webhook, поэтому технически можно использовать любое другое серверное решение. Однако Firebase Functions является хорошей альтернативой, а иногда и заменой собственному backend’у. Он позволяет создавать и запускать сервисы на инфраструктуре Google, не заботясь о выделении, масштабировании или управлении серверами. С одной стороны, это позволяет сосредоточится на продуктовой составляющей разработки и функциональности сервиса, не тратя время на инфраструктурные задачи и администрирование. Но с другой стороны, как правило, делегирование влечет за собой ослабление контроля над ситуацией.

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


Рис. 2. Взаимодействие компонентов Google Ассистента (Основано на материале: Google Home and Google Assistant Workshop).

В рамках описанного стека логика работы экшена выглядит так (рис. 2):

  • Пользователь обращается к приложению Google Ассистент и инициирует разговор с определенным экшеном.
  • Google Ассистент через Actions on Google проксирует каждую фразу пользователя в текстовом формате в Dialogflow, дополнительно предоставляя информацию о самом пользователе (при предварительном запросе и с согласия пользователя) и текущей беседе.
  • Dialogflow обрабатывает полученную фразу, извлекает из неё необходимую информацию и на основе ML принимает решения о том, какой ответ будет сформирован.
  • В некоторых случаях Dialogflow может делегировать формирование ответа серверу на Firebase Functions, который, в свою очередь, может задействовать сторонние сервисы для получения необходимой для ответа информации.
  • После того, как ответ сформирован, Dialogflow возвращает его в Actions on Google, откуда он поступает в приложение Google Ассистента.

Идея

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

  1. Настройка и связка Actions on Google, Dialogflow и Firebase Functions.
  2. Извлечение ключевых слов из фраз пользователя (Dialogflow).
  3. Создание сценариев диалога (Dialogflow).
  4. Работа с контекстом диалога (Dialogflow).
  5. Создание и подключение webhook для генерации ответа на фразу пользователя (Dialogflow, Firebase Function).
  6. Отображение карусели из карточек в интерфейсе (Firebase Functions).
  7. Загрузка информации из стороннего сервиса (Firebase Functions).

Первичная настройка


Рис. 3. Создание агента Dialogflow.

Прежде всего нам потребуется Google-аккаунт. Начнем с создания проекта в Dialogflow, для этого в консоли нажмем кнопку «Create Agent» и заполним необходимые поля (рис. 3):

  • Язык по умолчанию: «Russian — ru».
  • Часовой пояс: «(GMT+3:00) Europe/Moscow».
  • Google Cloud Project: новый GCP для вашего Dialogflow-агента создастся автоматически, либо же вы можете выбрать один из существующих GCP-проектов, если таковые у вас имеются.

Затем нажимаем кнопку «Create» в правом верхнем углу и ждем, пока консоль конфигурирует новый проект.


Рис. 4. Стандартные интенты.

По умолчанию при создании агента Dialogflow создаются два интента (рис. 4):

  • «Default Welcome Intent» — отвечает за приветствие пользователя;
  • «Default Fallback Intent» — обрабатывает неизвестные фразы, которые Dialogflow не может отнести к каким-либо другим интентам.

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


Рис. 5. Ответы для «Default Welcome Intent».

Добавим в «Default Welcome Intent» несколько приветственных ответов, которые помогут пользователю понять, для чего нужен экшн и какие функции он умеет выполнять. В разделе «Responses» выберем вкладку «Google Assistant» и в «Suggestion Ships» пропишем примеры фраз, чтобы подсказать пользователю, как можно общаться с экшеном (рис. 5).

Экшн можно отлаживать в Google Ассистенте как на телефоне, так и в официальном эмуляторе. Чтобы открыть эмулятор, необходимо зайти в раздел «Integrations», в карточке «Google Assistant» нажать на кнопку «Integration Settings» и кликнуть на «Manage Assistant App». И в телефоне и в эмуляторе экшн можно запустить кодовой фразой «Окей Google, я хочу поговорить с моим тестовым приложением».

Базовый сценарий: поиск гифок

Создадим новый интент «Search Intent», который будет извлекать из фразы пользователя ключевые слова и передавать их по webhook серверу на Firebase Functions. Сервер, в свою очередь, с помощью GIPHY API найдет соответствующие гифки и вернет пользователю результат в виде карточек.


Рис. 6. Добавление тренировочных фраз.

Для начала в раздел «Training Phrases» добавим типовые фразы для обучения (рис. 6):

  • «Я хочу посмотреть на танцующих жирафов».
  • «Найди анимашки».
  • «Покажи котиков».
  • «Покажи гифки».
  • «Найди мне анимированных слонов».
  • «Покажи гифки с пандами».
  • «Гифки с енотами-полоскунами».
  • «У тебя есть тюлени».
  • «Найди смешные падения».


Рис. 7. Извлечение параметров из текста.

У добавленных фраз отметим параметр поиска, который Dialogflow должен выделить из текста. В данном случае наиболее подходящим типом параметра будет @sys.any, поскольку в качестве параметра поискового запроса может выступать практически любая языковая конструкция. Назовем этот параметр query и отметим как обязательный (рис. 7).


Рис. 8. Перечень наводящих вопросов.

В подразделе «Prompts» пропишем уточняющие вопросы, которые Dialogflow будет задавать, если не сможет извлечь из фразы ключевые слова (рис. 8).

Далее следует спуститься в раздел «Fulfillment» в самом низу страницы (не путать с одноименным разделом в левом меню). нажать кнопку «Enable Fullfilment», а потом включить настройку «Enable webhook call for this intent». Это позволит Dialogflow при попадании в интент делегировать формирование ответа Firebase Functions.

Теперь перейдем во вкладку «Fulfillment» в левом меню и включим «Inline Editor», где пропишем логику для только что созданного «Search Intent». Для поиска гифок по ключевым словам мы будем использовать запрос https://api.giphy.com/v1/gifs/search, который возвращает список найденных объектов в JSON-формате согласно спецификации. Полученный от GIPHY ответ мы будем выводить в виде Browsing Carousel — карусель из карточек с изображениями, при нажатии на которые открывается веб-страница. В нашем случае при клике на карточку пользователь будет переходить на страницу сервиса GIPHY с этой анимацией и списком похожих.

Код, реализующий описанную выше функциональность, представлен ниже.

'use strict';  const GIPHY_API_KEY = 'API_KEY';  const SEARCH_RESULTS = [     'Хе-хе, сейчас покажу мои любимые.',     'Лови, отличная подборка гифок.',     'Смотри, что я нашел!' ];  // Import the Dialogflow module from the Actions on Google client library. const { dialogflow, BrowseCarouselItem, BrowseCarousel, Suggestions, Image } = require('actions-on-google'); // Import the firebase-functions package for deployment. const functions = require('firebase-functions'); // Import the request-promise package for network requests. const request = require('request-promise');  // Instantiate the Dialogflow client. const app = dialogflow({ debug: true });  function getCarouselItems(data) {     var carouselItems = [];     data.slice(0, 10).forEach(function (gif) {         carouselItems.push(new BrowseCarouselItem({             title: gif.title || gif.id,             url: gif.url,             image: new Image({                 url: gif.images.downsized_medium.url,                 alt: gif.title || gif.id             }),         }));     });     return carouselItems; }  function search(conv, query) {     // Send the GET request to GIPHY API.     return request({         method: 'GET',         uri: 'https://api.giphy.com/v1/gifs/search',         qs: {             'api_key': GIPHY_API_KEY,             'q': query,             'limit': 10,             'offset': 0,             'lang': 'ru'         },         json: true,         resolveWithFullResponse: true,     }).then(function (responce) {         // Handle the API call success.          console.log(responce.statusCode + ': ' + responce.statusMessage);         console.log(JSON.stringify(responce.body));         // Obtain carousel items from the API call response.         var carouselItems = getCarouselItems(responce.body.data);         // Validate items count.         if (carouselItems.length <= 10 && carouselItems.length >= 2) {             conv.data.query = query;             conv.data.searchCount = conv.data.searchCount || 0;             conv.ask(SEARCH_RESULTS[conv.data.searchCount % SEARCH_RESULTS.length]);             conv.data.searchCount++;             conv.ask(new BrowseCarousel({ items: carouselItems }));         } else {             // Show alternative response.             conv.ask('Ничего не смог найти по такому запросу, может поищем что-то другое?)');         }     }).catch(function (error) {         // Handle the API call failure.          console.log(error);         conv.ask('Извини, кажется альбом с гифками потерялся.');     }); } // Handle the Dialogflow intent named 'Search Intent'. // The intent collects a parameter named 'query'. app.intent('Search Intent', (conv, { query }) => {     return search(conv, query); });  // Set the DialogflowApp object to handle the HTTPS POST request. exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

Зависимости

{   "name": "dialogflowFirebaseFulfillment",   "description": "This is the default fulfillment for a Dialogflow agents using Cloud Functions for Firebase",   "version": "0.0.1",   "private": true,   "license": "Apache Version 2.0",   "author": "Google Inc.",   "engines": {     "node": "~6.0"   },   "scripts": {     "start": "firebase serve --only functions:dialogflowFirebaseFulfillment",     "deploy": "firebase deploy --only functions:dialogflowFirebaseFulfillment"   },   "dependencies": {     "actions-on-google": "2.0.0-alpha.4",     "firebase-admin": "^4.2.1",     "firebase-functions": "^0.5.7",     "dialogflow": "^0.1.0",     "dialogflow-fulfillment": "0.3.0-beta.3",     "request": "^2.81.0",     "request-promise": "^4.2.1"   } }

Поскольку пользователь может обращаться несколько раз к одному и тому же интенту, рекомендуется возвращать ему разнообразные ответы. Для этого был использован JSON-объект Conversation.data, сохраняющий свое значение как при повторном обращении к интенту, так и при обращении к другим сценариям разговора.


Рис. 9. Инициализация беседы (слева), уточнение параметров поиска и дальнейшее отображение результатов (по центру), отображение поисковой выдачи для нового запроса (справа)

Примечание: для работы с API сторонних сервисов через Firebase Functions необходимо подключить биллинг, иначе при попытках работы с сетью будет возникать ошибка:

«Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions».

Для этого в левом меню следует кликнуть на «Платный аккаунт» и среди предложенных тарифных планов выбрать Flame ($25 в месяц) либо Blaze (оплата по мере использования). Я выбрал последний вариант, поскольку в рамках разработки тестового приложения он показался мне более выгодным.

Продвинутый сценарий: пагинация

В большинстве случаев по поисковому запросу GIPHY найдет значительно больше десяти гифок, поэтому правильно будет позволить пользователю увидеть всю поисковую выдачу, т.е. добавить пагинацию.

В консоли Dialogflow наведем курсор на ячейку «Search Intent». Справа появятся несколько кнопок, нажмем на «Add follow-up intent». Это позволит нам создать ветвь разговора, следующую после «Search Intent». Среди элементов выпадающего списка выберем «more» — стандартный игнтент для инициирования отображения дополнительной информации.


Рис. 10. Контекст интента «Search Intent — more».

Перейдем в только что созданный интент и внесем изменения в раздел «Context». Поскольку пользователь может несколько раз подряд просить показать ещё гифок, этот интент должен уметь вызываться рекурсивно. Для этого в исходящем контексте необходимо прописать ту же строку, что указана во входящем (рис. 10). В разделе «Fullfilment» также следует включить настройку «Enable webhook call for this intent».

Теперь вернемся в «Fillfulment» из бокового меню, где инициализируем обработчик для «Search Intent — more». Также добавим в функцию search параметр offset, который будет использоваться при пагинации в GIPHY API.

const SEARCH_RESULTS_MORE = [     'Вот ещё пара гифок!',     'Надеюсь, эти тебе тоже понравятся.',     'На, лови еще парочку. Если что, у меня ещё есть.' ];   function search(conv, query, offset) {     // Send the GET request to GIPHY API.     return request({         method: 'GET',         uri: 'https://api.giphy.com/v1/gifs/search',         qs: {             'api_key': GIPHY_API_KEY,             'q': query,             'limit': 10,             'offset': offset,             'lang': 'ru'         },         json: true,         resolveWithFullResponse: true,     }).then(function (responce) {         // Handle the API call success.          console.log(responce.statusCode + ': ' + responce.statusMessage);         console.log(JSON.stringify(responce.body));         // Obtain carousel items from the API call response.         var carouselItems = getCarouselItems(responce.body.data);         // Validate items count.         if (carouselItems.length <= 10 && carouselItems.length >= 2) {             conv.data.query = query;             conv.data.offset = responce.body.pagination.count + responce.body.pagination.offset;             conv.data.paginationCount = conv.data.paginationCount || 0;             conv.data.searchCount = conv.data.searchCount || 0;             // Show successful response.             if (offset == 0) {                 conv.ask(SEARCH_RESULTS[conv.data.searchCount % SEARCH_RESULTS.length]);                 conv.data.searchCount++;             } else {                 conv.ask(SEARCH_RESULTS_MORE[conv.data.paginationCount % SEARCH_RESULTS_MORE.length]);                 conv.data.paginationCount++;             }             conv.ask(new BrowseCarousel({ items: carouselItems }));             conv.ask(new Suggestions('Ещё'));         } else {             // Show alternative response.             conv.ask('Ничего не смог найти по такому запросу, может поищем что-то другое?)');         }     }).catch(function (error) {         // Handle the API call failure.          console.log(error);         conv.ask('Извини, кажется альбом с гифками потерялся.');     }); }   // Handle the Dialogflow intent named 'Search Intent - more'. app.intent('Search Intent - more', (conv) => {     // Load more gifs from the privious search query     return search(conv, conv.data.query, conv.data.offset); });


Рис. 11. Пагинация при поиске гифок.

Результат

Видео работы экшена представлено ниже.

Код проекта и дамп ассистента доступен на Github.

Инструкция по установке проекта и импорту дампа

  1. Перейдите в консоль Dialogflow и создайте нового агента или выберите существующего.
  2. Кликните на иконке настроек, перейдите в раздел «Export and Import» и нажмите кнопку «Restore from ZIP». Выберите ZIP-файл из корневой директории репозитория.
  3. Выберите «Fulfillment» из левого навигационного меню.
  4. Включите настройку «Inline Editor».
  5. Скопируйте содержимое файлов из директории functions в соответствующие вкладки в «Fulfillment».
  6. Укажите ваш ключ доступа к GIPHY API во вкладке index.js.
  7. Перейдите в консоль Firebase и смените ваш тарифный план на Flame или Blaze. Работа со сторонними сервисами по сети недоступна при бесплатном тарифном плане.

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *