Хабрахабр

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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

'use strict'; const GIPHY_API_KEY = 'API_KEY'; const SEARCH_RESULTS = [ 'Хе-хе, сейчас покажу мои любимые.', 'Лови, отличная подборка гифок.', 'Смотри, что я нашел!'
]; // Import the Dialogflow module from the Actions on Google client library.
const = 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 необходимо подключить биллинг, иначе при попытках работы с сетью будет возникать ошибка:

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

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

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

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

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

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

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

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

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. Работа со сторонними сервисами по сети недоступна при бесплатном тарифном плане.

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть