From zero to “Actions on Google” hero: начало

Хакатон Google, и все, что нужно, чтобы начать разрабатывать свои приложения для ассистента.
Google организовал хакатон, посвященный технологии Actions On Google. Это хорошая возможность получить опыт и подумать, как начать делать conversation user interface (CUI) для наших приложений. Поэтому мы собрали команду из двух Android-разработчиков: shipa_o, raenardev и дизайнера comradeguest и отправились участвовать.
Что такое Actions On Google?
Actions On Google (AoG) — это способ добавить свое действие в ассистент.
Сделать это можно с помощью 4 инструментов:
На хакатоне мы делали навык — приложение, расширяющее возможности ассистента, поэтому на нем и остановимся.
После обращения «Окей, гугл. Я хочу поговорить с ${название_приложения}
”, ассистент открывает навык, с которым пользователь и ведет диалог:
Как написать навык?
Вам понадобятся два скилла:
— понимание работы Conversational User Interface (CUI), умение их проектировать;
— умение работать с Natural Language Processing (NLP), например, Dialogflow.
Этап 1: Проектирование
Чтобы у вашего навыка когда-нибудь появился белковый собеседник, лучше подумать о будущем уже сейчас. Востребованными будут те, которые учитывают контекст использования. Диалоговыми интерфейсами будут пользоваться тогда, когда есть возможность говорить вслух, и взаимодействовать с устройствами голосом удобнее и быстрее, чем руками, глазами и прочими частями тела.
Голосовой интерфейс последовательный. Если на графическом можно показать всю форму оформления заказа, а человек сам будет выбирать, на что посмотреть сначала, а на что потом, то в голосовом задавать вопросы можно только один за другим. Чтобы придумать востребованное и удобное приложение, найдите пересечение между потребностями пользователя и возможностью использования голосового интерфейса (или невозможностью использовать другие).
Первое, что приходит в голову — голосовой помощник для слепых, который помогает решить бытовые задачи. Например, оформить заказ в магазине, вызвать такси, позвонить родственникам. Второе — говорящая книга рецептов для домохозяек, у которых руки в муке. Третье — игры, в которых нужно что-то объяснять.
Мы решили начать с простого и разработали робота, который советует людям хорошие фильмы. Мы обыграли несовершенство голосовых синтезаторов: наш помощник даже не притворяется человеком и всячески подчеркивает свою яркую электронную индивидуальность.
Google написали отличные гайдлайны о том, как разрабатывать диалоговые интерфейсы. А мы расскажем о том, как проектировали своего говорящего первенца.
1. Обращение (Invocation)
Для начала помощника надо позвать. Вызов может быть явным (Explicit Invocation) и косвенным (Implicit Invocation). Явное обращение люди будут использовать, когда уже знают приложение. Косвенное нужно, чтобы Google Assistant мог порекомендовать подходящее приложение в определенной ситуации. Правильно подобранные варианты косвенного обращения — как правильные ключевые слова в контекстной рекламе, только более «человеческие».
Тип обращения |
Описание
|
Пример |
Явное (Explicit Invocation) | С упоминанием названия помощника |
Окей, Гугл, я хочу поговорить с Красным страстным кинороботом.
|
Косвенное (Implicit Invocation) |
В контексте, когда нужен помощник |
Окей, Гугл, посоветуй мне какой-нибудь фильм. Хочу посмотреть смешную комедию. Какое кино посмотреть с девушкой? |
Важно, чтобы косвенные обращения не были слишком общими. Как и общие ключевые слова в контекстной рекламе, они только мешают найти нужное приложение и понижают рейтинг приложения в выдаче Ассистента.
Вызовы могут содержать deep link к отдельным функциям голосового помощника. Например, обычно наш киноробот начинает общение с того, что предлагает человеку выбрать какой-нибудь жанр. Но если его вызовут по косвенному обращению «Хочу посмотреть смешную комедию», логично начать диалог с предложения гарантированно хорошего фильма упомянутого жанра.
2. Первое приветствие
Первое приветствие — это то, что говорит человеку приложение сразу после вызова.
Сначала нужно дать пользователю понять, что помощник уже тут:
Привет, белковая форма жизни. Я Красный страстный киноробот. Цель моего существования — советовать биологическим организмам хорошие фильмы.
А потом — подсказать, что делать дальше. Наш робот ищет фильмы по жанрам, поэтому мы подсказываем, с каким запросом человек может обратиться дальше:
Что ты хочешь посмотреть: может, комедию, боевик или ужасы?
Новых и опытных пользователей можно приветствовать по-разному. Если человек в первый раз общается с вашим помощником, можно немного рассказать о себе. Если не в первый — длинное приветствие будет его раздражать. Поэтому можно сразу перейти к делу:
Первый раз |
Повторно
|
Привет, белковая форма жизни. Я Красный страстный киноробот. Цель моего существования — советовать биологическим организмам хорошие фильмы. Что ты хочешь посмотреть: может, комедию, боевик или ужасы? | Приветствую, человек! Какой жанр тебя интересует? |
3. Разговор по-людски
Учите помощника понимать естественную речь и поддерживать беседу. Самый простой способ сделать это — ещё до начала разработки пообщаться с людьми из целевой аудитории. Причем желательно устно, а не письменно, потому что письменная разговорная речь более скудная, чем устная. Сыграйте роль робота, а собеседника попросите представить, что он пользуется вашим будущим приложением. Запишите все диалоги на диктофон, а потом расшифруйте. Это поможет спроектировать схему типовой беседы и найти, где могут появиться ответвления.
Этап 2: Разработка
Разрабатывать свой action для ассистента можно несколькими способами:
- С Dialogflow.
- С Actions on Google SDK.
- Текст можно обрабатывать самостоятельно — например, если у вас есть свое решение для обработки естественного языка (NLP — Natural Language Processing).
Ниже нарисовано взаимодействие ассистента с вашим навыком.
Диалог выглядит примерно так:
-
Ассистент переводит речь в текст и отправляет его в ваш action.
-
Текст обрабатывается одним из указанных выше способов. На этой схеме — через Dialogflow.
-
Dialogflow определяет intent (конкретное намерение пользователя) и получает
из него entities (параметры). -
(Опционально) Dialogflow может вызвать соответствующий webhook, обработать данные на backend и получить ответ.
-
Dialogflow формирует ответ.
-
Ассистент озвучивает ответ, включает микрофон и слушает, что скажет пользователь.
Схема устройства action для ассистента
Dialogflow
Не будем подробно расписывать основы Dialogflow — Google выпустили хорошие обучающие видео.
- Intents — про распознавание intent, как именно Dialogflow понимает что спрашивает пользователь или какое действие он хочет совершить.
- Entities — про распознавание параметров внутри фразы. Например, в случае с рекомендацией фильмов это конкретный жанр.
- Dialog Control — про механизм контекстов (о нем чуть ниже) и fulfillment: о том, как обработать сам запрос пользователя путем обращения к вашему бекенду, и о том, как вернуть что-то более интересное, чем текстовый ответ.
Будем считать, что вы уже посмотрели видео и разобрались с консолью Dialogflow. Давайте разберем вопросы, которые возникали у нас по каждой из частей в процессе реализации, и что интересного можно отметить.
Помните также о правилах построения хорошего диалога, когда будете переходить к реализации — это повлияет на связку intents, набор entities и использование их в ответах, на использование контекстов и все остальное.
Intents
Есть рекомендации — сделать более подробное приветствие нового пользователя, а для остальных делать его более кратким. Как это реализовать?
В консоли Dialogflow определить такую логику не получится. Это можно делать внутри fulfillment для welcome intent. Иначе говоря, сделать это нужно будет руками.
Это касается и обработки ошибок. Например, в первый раз можно просто переспросить, а во второй — рассказать, какого ответа вы ждете от пользователя.
Через responses это не сделать — будет выбран случайный ответ. Можно сделать через fulfillment или чуть хитрей, завязав на контекст (об этом ниже).
Entities
«Allow automated expansion» и sys.Any
Если фраза похожа по структуре, то при включенном «Allow automated expansion» в качестве распознанной сущности может попасть что-то, что все равно обработать с пользой не получится.
Например — ваше приложение распознает фразу «Посоветуй мне что-нибудь из <жанр>» для советов по фильмам. Если сказать «Посоветуй мне что-нибудь из еды», то в качестве параметра «жанр» вы получите не то, что ожидали бы. Это имеет смысл, если набор жанров у вас постоянно динамически обновляется на бекенде, и там же обрабатывается ответ пользователя, но вы не успеваете обновлять entity.
Другое решение — использование sys.Any. В этом случае он передаст все, что было после ожидаемой конструкции, что может быть целым предложением. Это дает более широкие возможности чем automated expansion, но и недостатков больше.
Единственное и множественное число, падежи, род
Давайте придумаем простой пример. Мы будем говорить про любимые фрукты, а приложение будет отвечать какие они классные. В примерах далее П — пользователь, А — наш action.
П: Ананасы самые вкусные.
A: Да, ананасы — самые крутые фрукты!
В этом примере у нас есть entity: Ананасы
Но что если пользователь говорит «Ананас»? Мы можем добавить синоним.
П: Мне понравился ананас.
A: Да, ананасы — самые крутые фрукты!
По умолчанию используется reference form — т.е. именно исходная форма, не ее синонимы. Именно поэтому приложение не говорит “Да, ананас — самые крутые фрукты!”
Стоит это также учитывать при написании ответов. Если бы у нас был ответ “Да, $fruit самый классный!“, то мы бы получили следующее:
П: Мне понравился ананас.
A: Да, ананасы самый классный!
И не забываем про падежи:
П: Я хочу драму
A: Вот классная драма…
Поэтому следует писать как первоначальные значения entity, так и ответы в согласованной форме. Например, все исходные формы entity для фруктов мы можем сделать во множественном числе, а синонимы — в любом.
Жанры на русском будем делать в именительном падеже, но в любом числе (драма, ужасы).
Когда дойдем до ответов — не будем одновременно писать “Да, $fruit — самые крутые фрукты!” и “Да, $fruit самый классный!” — фрукты у нас всегда во множественном числе.
В случае с русским языком стоит пойти еще дальше и подумать как написать согласованный ответ — у нас есть не только падежи (драма/драму), но и род (драма/вестерн).
П: Я хочу вестерн
A: Вот классная вестерн…
П: Я хочу вестерн
A: Вестерн? Хорошо, вот классный фильм…
Но как возвращать именно ту форму entity, которую сказал пользователь?
В случае с жанрами, синонимом «научной фантастики» могло бы быть «инопланетяне». Тогда если бы пользователь говорил «инопланетяне», в качестве параметра возвращался «научная фантастика».
Если мы хотим получить entity в той форме, в которой сказал пользователь то стоит выбрать значение $entity.original
Но тогда возможны проблемы с несогласованностью численности и (особенно) несогласованностью падежей. Действительно ли это нужно? Если да, создавайте entity для единственного, множественного числа и падежей. Ответы также должны быть согласованы с формой entity, которая в них используется.
Contexts
Пожалуй, с этим больше всего проблем.
Input context
Это контекст, к которому привязан конкретный intent. На одну и ту же фразу могут реагировать несколько intent’ов, и скорее всего сработает тот, у которого активен входящий контекст.
Таким образом, можно, например, привязать ответ «да/нет» к конкретному вопросу, что и делается при использовании follow-up intent в Dialogflow
Output context
Это контекст, который активируется при срабатывании intent. Именно так активируются контексты в консоли Dialogflow (в fulfillment это тоже можно делать). Мы указываем число витков диалога, в течении которых он будет активен, а после обнуления счетчика либо по истечению 20 минут он деактивируется. Это значит что данные внутри этого контекста станут больше недоступны и intent’ы, для которых он является входным не будут срабатывать.
На этом же завязан другой трюк: вы можете одним intent активировать контекст, а другим вручную его деактивировать, просто проставив его как output контекст для второго intent с числом ответов 0.
Если не хотите писать код в fulfillment, то таким образом можно реализовать интересную логику, например, используя контекст как счетчик, реализовать обработку ошибок, когда ассистент не понимает пользователя.
Советы по работе в dialogflow
-
Не нужно перезапускать страничку с assistant preview — когда вы внесли изменения в агент dialogflow, можете дождаться завершения его обучения и сразу же повторить нераспознанную фразу в симуляторе. Dialogflow можно рассматривать как backend, к которому обращается ассистент.
-
Пользуйтесь prebuilt agents — там вы сможете посмотреть, как реализовать типовой сценарий.
-
Будьте осторожны с разделом Small talk. Его использование не выключает микрофон в конце беседы, и такие ответы обычно не содержат call-to-action. Вы не направляете пользователя к следующему витку диалога, и ему не совсем понятно, что следует сказать далее. С большой вероятностью из-за этого вы можете не пройти ревью. Лучше сделать отдельные intents для этого, если вы сможете вписать их в диалог.
-
Не стоит редактировать один и тот же intent вдвоем одновременно. Сейчас одновременная работа нескольких человек не поддерживается — неизвестно, чьи изменения перезапишутся.
-
Если необходимо распараллелить работу с intent — ее можно вести в отдельных проектах, а затем просто выбрать нужные и перенести. Также импорт и экспорт entities в json/xml и импорт/экспорт для intent.
-
Сразу стоит учесть, что вы пишите action для конкретного языка. Написание ответов на русском языке имеет дополнительные нюансы. Так что локализация action выглядит более сложной задачей, чем в случае с GUI мобильных приложений.
-
Учитывайте правила дизайна голосовых интерфейсов — они влияют не только на набор реплик, но и на структуру в целом. Вы строите диалог, поэтому каждый ответ должен оставлять call to action, чтобы пользователь понимал, что сказать.
-
После того, как все будет готово, и вы начнете тестирование, не бойтесь отказываться от отдельных ветвей диалога или форм вопросов. Возможно, на этапе тестирования вы поймете, как связать intents и чего не хватает для удобства использования.
Подключение сервера
Для подключения сервера нужно использовать fulfillment. Для этого есть два варианта:
- Webhook client. Поддерживается множество языков.
- Inline Editor на Cloud Functions for Firebase (node.js).
Рассмотрим самый простой — Inline Editor.
На звание экспертов в node.js мы не претендуем, исправление ошибок в комментариях приветствуется.
Важно обращать внимание на версию API Dialogflow.
Последняя версия v2. Все, что написано для версии v1 с ней не работает.
Подробнее про миграцию можно почитать тут.
Полезные ссылки:
- Документация и исходный код библиотеки для работы с dialogflow-fulfillment
- Getting started with Dialogflow fulfillment
- Примеры
- Логи можно смотреть тут: Переход в firebase console рядом с кнопкой Deploy
Разбираем стандартный шаблон
'use strict'; const functions = require('firebase-functions'); const {WebhookClient} = require('dialogflow-fulfillment'); const {Card, Suggestion} = require('dialogflow-fulfillment'); process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => { const agent = new WebhookClient({ request, response }); console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers)); console.log('Dialogflow Request body: ' + JSON.stringify(request.body)); function welcome(agent) { agent.add(`Welcome to my agent!`); } function fallback(agent) { agent.add(`I didn't understand`); agent.add(`I'm sorry, can you try again?`); } // // Uncomment and edit to make your own intent handler // // uncomment `intentMap.set('your intent name here', yourFunctionHandler);` // // below to get this function to be run when a Dialogflow intent is matched // function yourFunctionHandler(agent) { // agent.add(`This message is from Dialogflow's Cloud Functions for Firebase editor!`); // agent.add(new Card({ // title: `Title: this is a card title`, // imageUrl: 'https://developers.google.com/actions/images/badges/XPM_BADGING_GoogleAssistant_VER.png', // text: `This is the body text of a card. You can even use line\n breaks and emoji! `, // buttonText: 'This is a button', // buttonUrl: 'https://assistant.google.com/' // }) // ); // agent.add(new Suggestion(`Quick Reply`)); // agent.add(new Suggestion(`Suggestion`)); // agent.setContext({ name: 'weather', lifespan: 2, parameters: { city: 'Rome' }}); // } // // Uncomment and edit to make your own Google Assistant intent handler // // uncomment `intentMap.set('your intent name here', googleAssistantHandler);` // // below to get this function to be run when a Dialogflow intent is matched // function googleAssistantHandler(agent) { // let conv = agent.conv(); // Get Actions on Google library conv instance // conv.ask('Hello from the Actions on Google client library!') // Use Actions on Google library // agent.add(conv); // Add Actions on Google library responses to your agent's response // } // // See https://github.com/dialogflow/dialogflow-fulfillment-nodejs/tree/master/samples/actions-on-google // // for a complete Dialogflow fulfillment library Actions on Google client library v2 integration sample // Run the proper function handler based on the matched Dialogflow intent name let intentMap = new Map(); intentMap.set('Default Welcome Intent', welcome); intentMap.set('Default Fallback Intent', fallback); // intentMap.set('your intent name here', yourFunctionHandler); // intentMap.set('your intent name here', googleAssistantHandler); agent.handleRequest(intentMap); });
{ "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" } }
Первым делом, обновите зависимости alpha и beta версий, до последних стабильных.
{ "dependencies": { "actions-on-google": "^2.2.0", "firebase-admin": "^5.2.1", "firebase-functions": "^0.6.2", "dialogflow": "^0.6.0", "dialogflow-fulfillment": "^0.5.0" } }
А теперь давайте разберемся подробнее с кодом.
// Cloud Functions для Firebase library const functions = require('firebase-functions'); // Компонент для работы с вашим агентом const {WebhookClient} = require('dialogflow-fulfillment'); // Компоненты для вывода информации на экран const {Card, Suggestion} = require('dialogflow-fulfillment');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => { console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers)); console.log('Dialogflow Request body: ' + JSON.stringify(request.body)); // Создаем инстанс агента. const agent = new WebhookClient({ request, response }); // Полезные данные let result = request.body.queryResult; // Получение action и entities https://dialogflow.com/docs/actions-and-parameters let action = result.action; let parameters = result.parameters; // Работа с контекстом https://dialogflow.com/docs/contexts let outputContexts = result.outputContexts; // Информацию об устройстве можно получить тут let intentRequest = request.body.originalDetectIntentRequest; });
Этот callback будет вызываться для тех intent, у которых Вы активируете fullfilment.
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => { const agent = new WebhookClient({ request, response }); function welcome(agent) { // Вывод фразы agent.add(`Welcome to my agent!`); } function fallback(agent) { agent.add(`I didn't understand`); agent.add(`I'm sorry, can you try again?`); } // Создаём ассоциативный массив, в котором: // key - точное название intent-а. // value - функция с кодом, который надо выполнить. let intentMap = new Map(); intentMap.set('Default Welcome Intent', welcome); intentMap.set('Default Fallback Intent', fallback); agent.handleRequest(intentMap); });
При этом код полностью заменяет ответ intent-а из раздела Responses.
Responses вызовется только если в callback отработает с ошибкой, поэтому там можно сделать обработку ошибок.
Вынесем функции обработки intent-а из callback.
Функции welcome и fallback находятся в замыкании.
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => { const agent = new WebhookClient({ request, response }); let intentMap = new Map(); // Метод set возвращает Map. Поэтому их можно вызывать последовательно intentMap .set('Default Welcome Intent', welcome.bind(this, agent)) .set('Default Fallback Intent', fallback.bind(this, agent)); agent.handleRequest(intentMap); }); function welcome(agent) { agent.add(`Welcome to my agent!`); } function fallback(agent) { // Можно объединить 2 вызова метода add в массив фраз agent.add([ `I didn't understand`, `I'm sorry, can you try again?` ]); }
Итак, теперь вы готовы к тому, чтобы написать свой первый навык для Google Assistant. База есть, а к хардкору перейдем в следующей части.

