Главный разработчик в компании Azimo Мирек Станек рассказал о процессе создания простого приложения для голосовых интерфейсов Google.
Умные колонки и ассистенты для мобильных устройств предлагают решения для самых разных задач – вы можете поговорить со своими умными часами, а увидеть результат на экране телевизора или на дисплее в автомобиле. Персональные ассистенты и голосовые интерфейсы медленно распространяются вокруг нас и делают наши жизни проще.
Так как я верю, что естественный язык станет следующим интерфейсом для взаимодействия человека и машины, я решил написать новую серию статей, где я покажу создание нового типа приложений: ориентированных на голос, независимых от устройства, которые дадут нам свободу от платформ и оборудования. И они будут поддерживать самый естественный для человека интерфейс – голос.
Приложение WaterLog
Мы начнем с создания простой программы. WaterLog – это приложение, которое помогает нам отследить ежедневное потребление воды на основе разговора или переписки с Google Assistant. Первая версия приложения сможет отследить, сколько литров или миллилитров воды мы выпили за день. Для простоты мы пропустим теорию дизайна голосовых интерфейсов и сфокусируемся на технической стороне создания работающего приложения.
Вот возможные сценарии бесед:
Новый пользователь
Пользователь: Окей, Google, поговорить с WaterLog.
WaterLog: Добро пожаловать в WaterLog. Вы знаете, что вам следует пить около 3 литров воды в день, чтобы оставаться здоровыми? Сколько вы уже выпили?
Пользователь: Я выпил 500 миллилитров воды.
WaterLog: Ок, я добавил 500 миллилитров воды в ваш дневной журнал. В сумме вы выпили за сегодня 500 миллилитров воды. Дайте мне знать, когда вы будете пить воду в следующий раз! Увидимся позже.
Вернувшийся пользователь
Пользователь: Окей, Google, поговорить с WaterLog.
WaterLog: Добрый день! Сегодня вы выпили 500 мл воды. Какое количество воды мне добавить?
Пользователь: 100 мл.
WaterLog: Ок, я добавил 100 мл воды в ваш дневной журнал. В сумме вы выпили за сегодня 600 мл воды. Дайте мне знать, когда вы будете пить воду в следующий раз! Увидимся позже.
Вернувшийся пользователь спрашивает о записанной воде
Пользователь: Окей, Google, спросить WaterLog, сколько воды я сегодня выпил.
WaterLog: В сумме вы выпили за сегодня 600 мл воды. Дайте мне знать, когда вы будете пить воду в следующий раз! Увидимся позже.
Вы можете протестировать это приложение на своем устройстве.
Начало
Это приложение очень простое, но даже такой проект требует связи некоторых частей, чтобы он заработал. Хотя у нас много свободы в выборе платформы (мы можем создать приложение на разных языках и поместить его в разные облачные хранилища, такие как Google Cloud или AWS), для начала мы возьмем самый распространенный стек:
- Cloud Functions и база данных Firebase для бэкенда;
- Dialogflow для понимания естественного языка и диалогов;
- JavaScript/Node.js для кода приложения (на данный момент Firebase Cloud Functions поддерживает только этот язык);
- Google Actions SDK для интеграции с Google Assistant (в будущем мы попробуем использовать платформы вроде Amazon Alexa или Facebook Messenger).
Я не буду подробно писать о том, как связать все это вместе. На сайте Google Actions есть хороший пошаговое руководство.
Если вкратце:
- Начните новый проект в консоли Google.
- Затем вас попросят выбрать инструмент или платформу для создания приложения для Assistant. Как я и сказал, это будет Dialogflow. Если вы сделаете все верно, ваши приложения Actions и Dialogflow будут соединены. Вы можете проверить это в настройках агента Dialogflow:
Агент Dialogflow
Первой большой частью вашего приложения станет агент для разговоров, в нашем случае он будет создан на платформе Dialogflow. Его самая важная роль – понять, что пользователь говорит приложению, и определить свойства, которыми может управлять наш код. Именно это делают Dialogflow Intents.
Согласно документации:
Intent (намерение) представляет собой связь между тем, что сказал пользователь, и действием, которое должно выполнить приложение.
Давайте определим наши намерения. Вот список предложений, которые мы хотим учесть:
Стандартный запасной вариант
Его мы пока не будем трогать. Как и предполагает название, этот intent вступает в силу, если входные данные от пользователя не соответствуют обычным (документация). Стоит упомянуть, что этот intent даже не указывается в коде нашего приложения, он полностью управляется платформой Dialogflow.
welcome_user
Событие для приветствия нашего пользователя. Оно включается всегда, когда пользователь вызывает приложение, без дополнительных условий.
— Config —
Action name: input.welcome
Events: WELCOME
, GOOGLE_ASSISTANT_WELCOME
— events are additional mappings which allow to invoke intents by an event name instead of a user query.
Fulfillment: ✅ Use webhook
— Intent welcome_user will be passed to our backend.
log_water
Событие используется, чтобы сохранить, какое количество воды хочет записать пользователь во время диалога. Будет несколько вариантов, которыми мы хотели бы управлять одинаково. Вот некоторые из них:
- Окей, Google, сказать WaterLog записать 1 литр воды – событие происходит сразу же, как только пользователь вызывает действие. В этом случае приветствие пропускается. Подробнее о вызове ассистента вы можете найти в документации.
- Записать 500 мл воды – может быть сказано в середине диалога, когда приложение ждет ввода данных.
- 500 мл – как ответ на вопрос ассистента.
Чтобы справиться с подобными случаями, нам нужно привести примеры высказываний, которые могут быть сказаны пользователями. Примеры затем используются машинным обучением Dialogflow, чтобы научить нашего агента понимать пользовательский ввод. Чем больше примеров мы используем, тем умнее становится наш агент.
Кроме того, нам необходимо комментировать фрагменты наших примеров, которые нужно обрабатывать особым образом, так наше приложение узнает нужное высказывание. Например: “Я выпила 500 миллилитров воды”. Высказывание содержит количество и единицы объема выпитой воды. Всё, что нам нужно сделать, это выделить фрагмент и выбрать нужный объект (смотрите документацию).
— Config —
Action name: log_water
User says (should be much more examples, esp. in more complex apps):
Fulfillment: ✅ Use webhook
Google assistant:
✅ End conversation
— pick this to let Google Assistant know that conversation should be finished here.
get_logged_water
Событие, которое пользователь использует, чтобы узнать сколько воды он или она выпили в текущий день. Как и в случае log_water, есть несколько способов вызвать это событие:
- Окей, Google, спросить WaterLog, сколько воды я выпил сегодня? – вызывать вместо приветственного события, когда действие известно;
- Сколько я воды я выпила? – задается в середине беседы с приложением.
— Config —
Action name: get_logged_water
User says:
Fulfillment: ✅ Use webhook
Google assistant:
✅ End conversation
Это все для конфигурации Dialogflow. Если вы хотите увидеть полный файл конфигурации, вы можете скачать его и импортировать в агент из репозитория (WaterLog.zip).
Код
Если вы следовали гиду Google (Build fulfillment), у вас уже должна быть базовая структура кода, развернутые облачные функции Firebase и связь с агентом Dialogflow через конфигурацию выполнения.
Теперь давайте создадим код для приложения WaterLog. Репозиторий с финальным решением доступен на GitHub.
По сути нам нужно определить все Intents в приложении Dialogflow. Мы определим их в файле functions/assistant-actions.js:
1 2 3 4 5 |
module.exports = { ACTION_WELCOME: 'input.welcome', ACTION_LOG_WATER: 'log_water', ACTION_GET_LOGGED_WATER: 'get_logged_water' }; |
Ядро нашего приложения – файл index.js, который также вызывает HTTP для Firebase Cloud Function (конечная точка):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//... firebase.initializeApp(functions.config().firebase); exports.waterLog = functions.https.onRequest((request, response) => { //Initialise app dependencies const dialogflowApp = new DialogflowApp({request, response}); const userManager = new UserManager(firebase); const waterLog = new WaterLog(firebase, userManager); const conversation = new Conversation(dialogflowApp, userManager, waterLog); //Define map of Dialogflow agent Intents let actionMap = new Map(); actionMap.set(Actions.ACTION_WELCOME, () => conversation.actionWelcomeUser()); actionMap.set(Actions.ACTION_LOG_WATER, () => conversation.actionLogWater()); actionMap.set(Actions.ACTION_GET_LOGGED_WATER, () => conversation.actionGetLoggedWater()); //Handle request from Dialogflow (will be dispatched into appropriate action defined above) dialogflowApp.handleRequest(actionMap); }); |
В нашей облачной функции мы определим связи Intents с функциями, которые должны быть вызваны для окончания беседы. Как пример рассмотрим conversation.actionLogWater()(выполнение намерения log_water).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
const ARG_WATER_VOLUME = 'water_volume'; //... class Conversation { //... actionLogWater() { //Get argument extracted by Dialogflow let waterToLog = this.dialogflowApp.getArgument(ARG_WATER_VOLUME); //Save logged water into Firebase Realtime Database this.waterLog.saveLoggedWater(this._getCurrentUserId(), waterToLog); //Load sum of logged water for current user and reply user //with how much water he or she logged so far. //End the conversation. return this.waterLog.getLoggedWaterForUser(this._getCurrentUserId()) .then(loggedWater => { this.dialogflowApp.tell( util.format(Str.WATER_LOGGED_NOW, waterToLog.amount, waterToLog.unit, loggedWater ) ); }); } } |
Что происходит:
- Приложение получает аргумент, выделенный из высказывания Dialogflow. Для входных данных Записать 500 мл воды мы получим объект {“amount”:500,”unit”:”ml”} .
- Приложение сохраняет эти данные в базу данных Firebase.
- В конце приложение получает сумму записанной воды и отправляет её как ответ в объект dialogflowApp. Функция tell() отвечает пользователю и закрывает беседу (документация).
Полный код класса Conversation: conversation.js.
Остальное код не делает ничего интересного. Класс conversation несет ответственность за управление вводными данными пользователя. WaterLog сохраняет данные и извлекает их из базы данных Firebase. UserManager добавляет несколько хелперов для (анонимного) управления пользователями.
Юнит-тестирование
Хотя этот абзац не связан напрямую с приложениями Assistant или голосовыми интерфейсами, я думаю, что он очень важен в каждом типе приложений. Просто представьте, что при каждом изменении кода вам нужно разворачивать функцию и начинать диалог с приложением. В WaterLog это было довольно просто (хотя все равно требует десятки развертываний). В более крупных приложениях юнит-тесты необходимы. Это на порядки ускорит разработку.
Все юнит-тесты для наших классов можно найти в директории functions/test/. В этом проекте тесты не были сложными (они используют библиотеки sinon.js и chai без расширений), но они во многом помогли сделать приложение за короткий срок.
Вот результат $ npm test :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Conversation actionWelcomeUser ✓ Should create new anonymous user ✓ Should greet new user ✓ Should greet existing user actionGetLoggedWater ✓ Should tell about logged water for given user actionLogWater ✓ Should save given amount of water and response with saved value Cloud Functions waterLog UserManager isFirstUsage ✓ Should return first usage when user doesnt exist in DB ✓ Shouldnt return first usage when user exists in DB saveAssistantUser ✓ Should save assistant user into DB ensureAuthUser ✓ Should authenticate anonymous user when user isnt authenticated ✓ Should return authenticated user WaterLog saveLoggedWater ✓ Should save logged mililiters of water ✓ Should save logged liters of water getLoggedWaterForUser ✓ Should load logged water for given user and present data from recent day 14 passing (44ms) |
Полный исходный код приложения с Firebase Cloud Functions, конфигурацией агента Dialogflow и необходимыми для распространения приложениями активами можно найти на GitHub.