Рис. 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 и возвращать пользователю в виде карточек. При реализации экшена мы разберем решение следующих задач:
- Настройка и связка Actions on Google, Dialogflow и Firebase Functions.
- Извлечение ключевых слов из фраз пользователя (Dialogflow).
- Создание сценариев диалога (Dialogflow).
- Работа с контекстом диалога (Dialogflow).
- Создание и подключение webhook для генерации ответа на фразу пользователя (Dialogflow, Firebase Function).
- Отображение карусели из карточек в интерфейсе (Firebase Functions).
- Загрузка информации из стороннего сервиса (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.
- Перейдите в консоль Dialogflow и создайте нового агента или выберите существующего.
- Кликните на иконке настроек, перейдите в раздел «Export and Import» и нажмите кнопку «Restore from ZIP». Выберите ZIP-файл из корневой директории репозитория.
- Выберите «Fulfillment» из левого навигационного меню.
- Включите настройку «Inline Editor».
- Скопируйте содержимое файлов из директории
functions
в соответствующие вкладки в «Fulfillment». - Укажите ваш ключ доступа к GIPHY API во вкладке index.js.
- Перейдите в консоль Firebase и смените ваш тарифный план на Flame или Blaze. Работа со сторонними сервисами по сети недоступна при бесплатном тарифном плане.
ссылка на оригинал статьи https://habr.com/post/419261/
Добавить комментарий