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 — т.е. именно исходная форма, не синонимы. Именно поэтому приложение не говорит «Ананасы огонь!»
Стоит это учитывать при написании ответов. Если бы у нас был ответ «Да, $фрукт самые вкусные», то мы бы получили следующее:
П: Мне понравился ананас.
A: Да, ананасы самые вкусные.
И не забываем про падежи:
П: Я хочу драму
A: Вот классная драма…
Поэтому следует писать как первоначальные значения entity, так и ответы в согласованной форме. Например, все исходные формы entity для фруктов мы можем сделать во множественном числе, а синонимы — в любом.
Жанры на русском будем делать в именительном падеже, но в любом числе (драма, ужасы).
Когда дойдем до ответов — не будем писать «Да, $фрукт огонь!» и «Да, $фрукт самый вкусный» — фрукты у нас всегда во множественном числе.
В случае с русским языком стоит пойти еще дальше и подумать как написать согласованный ответ — у нас есть не только падежи (драма/драму), но и род (драма/вестерн).
П: Я хочу вестерн
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 с ней не работает.
Подробнее про миграцию можно почитать тут.
Полезные ссылки:
Разбираем стандартный шаблон
'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. База есть, а к хардкору перейдем в следующей части.