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


image

Хакатон Google, и все, что нужно, чтобы начать разрабатывать свои приложения для ассистента.


Google организовал хакатон, посвященный технологии Actions On Google. Это хорошая возможность получить опыт и подумать, как начать делать conversation user interface (CUI) для наших приложений. Поэтому мы собрали команду из двух Android-разработчиков: shipa_o, raenardev и дизайнера comradeguest и отправились участвовать.


Что такое Actions On Google?


Actions On Google (AoG) — это способ добавить свое действие в ассистент.
Сделать это можно с помощью 4 инструментов:



На хакатоне мы делали навык — приложение, расширяющее возможности ассистента, поэтому на нем и остановимся.


После обращения «Окей, гугл. Я хочу поговорить с ${название_приложения}”, ассистент открывает навык, с которым пользователь и ведет диалог:


image

Как написать навык?


Вам понадобятся два скилла:
— понимание работы 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).

Ниже нарисовано взаимодействие ассистента с вашим навыком.
Диалог выглядит примерно так:


  1. Ассистент переводит речь в текст и отправляет его в ваш action.


  2. Текст обрабатывается одним из указанных выше способов. На этой схеме — через Dialogflow.


  3. Dialogflow определяет intent (конкретное намерение пользователя) и получает
    из него entities (параметры).


  4. (Опционально) Dialogflow может вызвать соответствующий webhook, обработать данные на backend и получить ответ.


  5. Dialogflow формирует ответ.


  6. Ассистент озвучивает ответ, включает микрофон и слушает, что скажет пользователь.



image

Схема устройства action для ассистента


Dialogflow


Не будем подробно расписывать основы Dialogflow — Google выпустили хорошие обучающие видео.


  1. Intents — про распознавание intent, как именно Dialogflow понимает что спрашивает пользователь или какое действие он хочет совершить.
  2. Entities — про распознавание параметров внутри фразы. Например, в случае с рекомендацией фильмов это конкретный жанр.
  3. 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


image

Но тогда возможны проблемы с несогласованностью численности и (особенно) несогласованностью падежей. Действительно ли это нужно? Если да, создавайте 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 с ней не работает.
Подробнее про миграцию можно почитать тут.


Полезные ссылки:



Разбираем стандартный шаблон


При открытии раздела Fulfillment, отображается следующий код в файле/вкладке `index.js`:
'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);
});

И такие зависимости в файле/вкладке `package.json`:
{
  "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');

Вся суть fulfillment заключается в переопределении callback-a `dialogflowFirebaseFulfillment`
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.


Теперь переопределим ответ на intent
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 находятся в замыкании.


Чтобы их вынести из callback, придется добавить передачу контекста функции и параметров через `bind`
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. База есть, а к хардкору перейдем в следующей части.




К сожалению, не доступен сервер mySQL