- PVSM.RU - https://www.pvsm.ru -
В конце прошлого месяца состоялся официальный релиз [1] Google Ассистента на русском языке, так что самое время разобраться, как делать свои приложения (экшены) для Ассистента на стандартном технологическом стеке Google. В этой статье мы рассмотрим создание экшена в Actions on Google, разберём процесс извлечения сущностей и интентов из фраз в Dialogflow, узнаем, как писать обработчики извлеченной информации и работать с сетью в Cloud Functions for Firebase.
Рис. 1. Архитектура приложения для Ассистента.
Разработка под Ассистента начала активно развиваться сравнительно недавно, поэтому в сети пока мало материалов, а количество используемых инструментов и технологий существенно повышает порог вхождения. Эта статья хоть и не решает, но как минимум способствует решению упомянутых проблем. Начнем с архитектуры приложений для Ассистента (рис. 1), реализованных на стандартном технологическом стеке Google:
В статье делается акцент на технический аспект разработки, стоимость использования перечисленных сервисов разобрана не будет.
Рис. 2. Взаимодействие компонентов Google Ассистента (Основано на материале: Google Home and Google Assistant Workshop [2]).
В рамках описанного стека логика работы экшена выглядит так (рис. 2):
Наш экшн будет по фразе определять, какие гифки хочет увидеть пользователь, а затем будет искать их через GIPHY API и возвращать пользователю в виде карточек. При реализации экшена мы разберем решение следующих задач:
Рис. 3. Создание агента Dialogflow.
Прежде всего нам потребуется Google-аккаунт. Начнем с создания проекта в Dialogflow, для этого в консоли [5] нажмем кнопку «Create Agent» и заполним необходимые поля (рис. 3):
Затем нажимаем кнопку «Create» в правом верхнем углу и ждем, пока консоль конфигурирует новый проект.
Рис. 4. Стандартные интенты.
По умолчанию при создании агента Dialogflow создаются два интента (рис. 4):
Создание диалогов в Dialogflow уже было подробно описано в статьях тут [6], тут [7] и тут [8], поэтому я не буду акцентировать внимание на его принципе работы.
Рис. 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 [9], который возвращает список найденных объектов в JSON-формате согласно спецификации [10]. Полученный от GIPHY ответ мы будем выводить в виде Browsing Carousel [11] — карусель из карточек с изображениями, при нажатии на которые открывается веб-страница. В нашем случае при клике на карточку пользователь будет переходить на страницу сервиса 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 [12].
functions
в соответствующие вкладки в «Fulfillment».
Автор: sismetanin
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/node-js/289530
Ссылки в тексте:
[1] официальный релиз: https://habr.com/company/google/blog/347824/
[2] Google Home and Google Assistant Workshop: https://www.slideshare.net/bretmc/google-home-and-google-assistant-workshop-build-your-own-serverless-action-on-google-app
[3] информацию о самом пользователе: https://developers.google.com/actions/assistant/helpers
[4] текущей беседе: https://developers.google.com/actions/assistant/save-data
[5] консоли: https://console.dialogflow.com/
[6] тут: https://habr.com/company/redmadrobot/blog/419773/
[7] тут: https://habr.com/company/redmadrobot/blog/420111/
[8] тут: https://habr.com/post/342762/
[9] https://api.giphy.com/v1/gifs/search: https://api.giphy.com/v1/gifs/search
[10] спецификации: https://developers.giphy.com/docs/
[11] Browsing Carousel: https://developers.google.com/actions/assistant/responses
[12] Github: https://github.com/sismetanin/dialogflow-giphy-nodejs
[13] Источник: https://habr.com/post/419261/?utm_source=habrahabr&utm_medium=rss&utm_campaign=419261
Нажмите здесь для печати.