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

в 8:47, , рубрики: cloud functions for firebase, dialogflow, google assistant, node.js, Блог компании Mail.Ru Group, Разработка для интернета вещей, Разработка под android

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

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 1
Рис. 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, не заботясь о выделении, масштабировании или управлении серверами. С одной стороны, это позволяет сосредоточится на продуктовой составляющей разработки и функциональности сервиса, не тратя время на инфраструктурные задачи и администрирование. Но с другой стороны, как правило, делегирование влечет за собой ослабление контроля над ситуацией.

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

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 2
Рис. 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).

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

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 3
Рис. 3. Создание агента Dialogflow.

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

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

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

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

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

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

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

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 5
Рис. 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 найдет соответствующие гифки и вернет пользователю результат в виде карточек.

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 6
Рис. 6. Добавление тренировочных фраз.

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

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

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 7
Рис. 7. Извлечение параметров из текста.

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

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 8
Рис. 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, сохраняющий свое значение как при повторном обращении к интенту, так и при обращении к другим сценариям разговора.

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 9
Рис. 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» — стандартный игнтент для инициирования отображения дополнительной информации.

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 10
Рис. 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);
});

Actions on Google: пишем простое приложение для Google Ассистента на Dialogflow и Cloud Functions for Firebase - 11
Рис. 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. Работа со сторонними сервисами по сети недоступна при бесплатном тарифном плане.

Автор: sismetanin

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js