From zero to “Actions on Google” hero: ваш код

от автора

image

В первой части мы разобрались с основными принципами проектирования и разработки приложений для Google Assistant. Теперь пришло время написать собственного помощника, чтобы пользователи могли наконец выбрать фильм на вечер. Разработчики shipa_o, raenardev и дизайнер ComradeGuest продолжают рассказывать.

Пишем свой код

Давайте попробуем написать что-то посложнее.
Допустим наш агент рекомендует фильмы по жанрам.
Мы просим его «Покажи ужастик», и агент будет парсить жанр, искать фильм в коллекции по жанру и выводить его на экран.

Для начала будем хранить коллекцию фильмов в переменной:

var json = {     "filmsList": [         {             "id": "1",             "title": "Властелин колец: Братство кольца",             "description": "Описание фильма Властелин колец",             "genres": ["фэнтези", "драма", "приключения"],             "imageUrl": "http://t3.gstatic.com/images?q=tbn:ANd9GcQEA5a7K9k9ajHIu4Z5AqZr7Y8P7Fgvd4txmQpDrlQY2047coRk",             "trailer": "https://www.youtube.com/watch?v=RNksw9VU2BQ"         },         {             "id": "2",             "title": "Звёздные войны: Эпизод 2 – Атака клонов",             "description": "Описание фильма Звёздные войны",             "genres": ["фантастика", "фэнтези", "боевик", "приключения"],             "imageUrl": "http://t3.gstatic.com/images?q=tbn:ANd9GcTPPAiysdP0Sra8XcIhska4MOq86IaDS_MnEmm6H7vQCaSRwahQ",             "trailer": "https://www.youtube.com/watch?v=vX_2QRHEl34"         },  {             "id": "3",             "title": "Чужой",             "description": "Описание фильма Чужой",             "genres": ["ужасы", "фантастика", "триллер"],             "imageUrl": "https://www.kinopoisk.ru/images/film_big/386.jpg",             "trailer": "https://www.youtube.com/watch?v=xIe98nyo3xI"         }     ] };

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {     const agent = new WebhookClient({ request, response });      let result = request.body.queryResult;     let parameters = result.parameters;     let outputContexts = result.outputContexts;      let intentMap = new Map();     // Передаем в функцию agent и parameters, для того чтобы достать оттуда жанр     intentMap.set('search-by-genre', searchByGenre.bind(this, agent, parameters));     agent.handleRequest(intentMap); });  function searchByGenre(agent, parameters) {     let filmsList = json.filmsList;      // Фильтруем фильмы по жанру     let filteredFilms = filmsList.filter((film) => {         // Проверяем, что выбранный жанр совпадает хотя бы с одним из указанных для фильма         return film.genres.some((genre) => genre == parameters.genre);     });      // Берем первый фильм в нужном жанре     let firstFlim = filteredFilms[0];      // Выводим название фильма     agent.add(firstFlim.title);      // Выводим карточку с информацией о фильме     agent.add(new Card({  title: firstFlim.title,         imageUrl: firstFlim.imageUrl,         text: firstFlim.description,         buttonText: 'Посмотреть трейлер',         buttonUrl: firstFlim.trailer     }));      // Подталкиваем пользователя к дальнейшим действиям     agent.add([         "Такой фильм тебе по душе?",         new Suggestion("О чем он?"),         new Suggestion("Да"),         new Suggestion("Нет")     ]); }

Теперь ответ стал информативнее.
Мы выводим текст, карточку с информацией и подсказки:

Вывод информации о фильме

Хорошая особенность Dialogflow в том, что он из коробки адаптирован под разные устройства.
Если у устройства будут динамики, то все фразы, которые мы отправляем в метод add, будут озвучены, а если не будет экрана, то объекты Card и Suggestion просто не будут отображаться.

Подключаем базу данных

Давайте усложним задачу и добавим получение данных из базы данных (БД).
Самый простой способ — это использование firebase realtime database.
Для примера будем использовать Admin Database API.

Сначала нужно создать базу данных и заполнить её.
Сделать это можно в том же проекте, который был создан для Cloud Functions:

Заполненая firebase realtime database

После того, как БД заполнена, подключим её к fulfillment:

// Импортируем зависимость firebase-admin const firebaseAdmin = require('firebase-admin');  // Инициализируем firebaseAdmin firebaseAdmin.initializeApp({     credential: firebaseAdmin.credential.applicationDefault(),     databaseURL: 'https://<ID-проекта>.firebaseio.com' });  // Добавим функцию, которая будет обращаться к БД function getFilmsList() {     return firebaseAdmin         .database()         .ref()         .child('filmsList')         .once('value')         .then(snapshot => {             const filmsList = snapshot.val();             console.log('filmsList: ' + JSON.stringify(filmsList));             return filmsList;         })         .catch(error => {             console.log('getFilmsList error: ' + error);             return error;         }); }

Обращение к БД требует многопоточности. API firebase database расчитано на использование Promise. Метод .once('value') возвращает нам Promise. Затем мы получаем наши данные в блоке then() и возвращаем Promise c ними, как результат выполнения функции.
Важно вернуть этот Promise в метод handleRequest(), иначе агент завершит работу с нашим callback без ожидания ответа и обработки результата.

Версия поиска фильма по жанру с использованием БД:

'use strict';  const functions = require('firebase-functions'); const firebaseAdmin = require('firebase-admin'); const { WebhookClient } = require('dialogflow-fulfillment'); const { Card, Suggestion } = require('dialogflow-fulfillment');  firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.applicationDefault(),     databaseURL: 'https://<ID-проекта>.firebaseio.com' });  exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {     const agent = new WebhookClient({ request, response });      let result = request.body.queryResult;     let parameters = result.parameters;     let outputContexts = result.outputContexts;      let intentMap = new Map();     intentMap.set('search-by-genre', searchByGenre.bind(this, agent, parameters));     agent.handleRequest(intentMap); });  function getFilmsList() {     return firebaseAdmin         .database()         .ref()         .child('filmsList')         .once('value')         .then(snapshot => {             const filmsList = snapshot.val();             console.log('filmsList: ' + JSON.stringify(filmsList));             return filmsList;         })         .catch(error => {             console.log('getFilmsList error: ' + error);             return error;         }); }  function searchByGenre(agent, parameters) {     return getFilmsList()         .then(filmsList => {              let filteredFilms = filmsList.filter((film) => {                 return film.genres.some((genre) => genre == parameters.genre);             });              let firstFlim = filteredFilms[0];              agent.add(firstFlim.title);             agent.add(new Card({                 title: firstFlim.title, imageUrl: firstFlim.imageUrl,                 text: firstFlim.description,                 buttonText: 'Посмотреть трейлер',                 buttonUrl: firstFlim.trailer             }));             agent.add([                 "Такой фильм тебе по душе?",                 new Suggestion("О чем он?"),                 new Suggestion("Да"),                 new Suggestion("Нет")             ]);         })         .catch(error => {             console.log('getFilmsList error' + error);         }); }

Добавляем непредсказуемости

Наш навык каждый раз будет выдавать фильмы в одной последовательности. Одни и те же ответы сначала будут раздражать пользователя, а потом он перестанет разговаривать с нашим роботом.

Исправим это с помощью библиотеки перемешивания массивов shuffle-array.

Добавим зависимость в файл package.json:

"dependencies": {     // ...     "shuffle-array": "^1.0.1"     // ... }

Добавим перемешивание массива:

// Импортируем библиотеку const shuffle = require('shuffle-array');  function searchByGenre(agent, parameters) {     return getFilmsList() .then(filmsList => {              let filteredFilms = filmsList.filter((film) => {                 return film.genres.some((genre) => genre == parameters.genre);             });              // Перемешиваем массив фильмов             shuffle(filteredFilms);             let firstFlim = filteredFilms[0];              agent.add(firstFlim.title);             agent.add(new Card({                 title: firstFlim.title,                 imageUrl: firstFlim.imageUrl,                 text: firstFlim.description,                 buttonText: 'Посмотреть трейлер',                 buttonUrl: firstFlim.trailer             }));             agent.add([                 "Такой фильм тебе по душе?",                 new Suggestion("О чем он?"),                 new Suggestion("Да"),                 new Suggestion("Нет")             ]);         })         .catch(error => {             console.log('getFilmsList error' + error);         }); }

Теперь каждый раз будет выдаваться новый фильм.
Таким же способом можно добавить вывод разных вариантов фраз: создать массив с фразами, перемешивать его и брать первую из массива.

Работаем с контекстом

Мы просим агента:

Покажи фэнтези

Агент отображает нам фильм «Властелин колец».
Затем мы спрашиваем:

О чем он?

Люди не говорят: «О чем фильм “Властелин Колец”» — это неестественно. Поэтому нам надо сохранить информацию об отображаемом фильме. Сделать это можно в контексте:

// Запоминаем фильм в контексте, чтобы можно было ссылаться на него agent.setContext({     name: 'current-film',     lifespan: 5,     parameters: { id: firstFlim.id } });

Тогда прочитать информацию о фильме мы сможем так:

function genreSearchDescription(agent) {     // Получаем контекст current-film     const context = agent.getContext('current-film');     console.log('context current-film: ' + JSON.stringify(context));     // Получаем id последнего отображаемого фильма     const currentFilmId = context.parameters.id;      // Получаем список фильмов     return getFilmsList()         .then(filmsList => {             // Ищем фильм по id             const currentFilm = filmsList.filter(film => film.id === currentFilmId);              agent.add(currentFilm[0].description);             agent.add([                 'Что скажешь?',                 new Suggestion('Интересно'),                 new Suggestion('Не нравится')             ]);         })         .catch(error => {             console.log('getFilmsList error:' + error);         }); }

Тем же способом мы можем отфильтровать список уже показанных фильмов.

Интеграция с Telegram

Документация и полезные ссылки:

Для интеграции с Telegram практически ничего не требуется, но есть несколько особенностей, которые нужно учитывать.

1) Если в fullfilment сделать отображение Card или Suggestion, то в Telegram они тоже будут работать.
Но есть один баг: для quick replies необходимо указывать title, иначе в Telegram будет отображаться «Choose an item».
Решить проблему с указанием title в fullfilment у нас пока не получилось.

2) Если в intent-е используется Suggestion Chips для google assistant

Пример

то такой же функционал для Telegram можно реализовать двумя способами:

Quick replies

Настройка Quick replies

Custom payload
Тут можно реализовать быстрые ответы c помощью основной клавиатуры:

Скриншот основной клавиатуры

    {         "telegram": {             "text": "Выбери из списка или введи свой вариант:",             "reply_markup": {                 "keyboard": [                     [                         "фантастика",                         "приключения",                         "комедия",                         "драма",                         "ужасы"                     ]                 ],                 "one_time_keyboard": true,                 "resize_keyboard": true        }         }     }

и встроенной клавиатуры:

Скриншот встроенной клавиатуры

    {         "telegram": {             "text": "Выбери из списка или введи свой вариант:",             "reply_markup": {                 "inline_keyboard": [                     [{                         "text": "фантастика",                         "callback_data": "фантастика"                     }],                     [{                         "text": "приключения",                         "callback_data": "приключения"                     }],                     [{                         "text": "комедия",                         "callback_data": "комедия"                     }],                     [{                         "text": "драма",                         "callback_data": "драма"                     }],                     [{                         "text": "ужасы",                         "callback_data": "ужасы"                     }]                 ]             }         }     }

Основная клавиатура будет отправлять сообщение, которое сохранится в истории, в то время как встроенная клавиатура этого не делает.

Важно помнить, что основная клавиатура не пропадает со временем. Для этого в Telegram API есть специальный запрос. Поэтому нужно слидить за тем, чтобы у пользователя всегда были актуальные подсказки.

3) Если нужна разная логика для Telegram и Google ассистента, сделать это можно так:

let intentRequest = request.body.originalDetectIntentRequest;  if(intentRequest.source == 'google'){     let conv = agent.conv();     conv.ask('Такой фильм тебе по душе?');     agent.add(conv); } else {     agent.add('Такой фильм тебе по душе?'); }

4) Отправку аудиофайла можно реализовать так:

{   "telegram": {     "text": "https://s0.vocaroo.com/media/download_temp/Vocaroo_s0bXjLT1pSXK.mp3"   } }

5) Контекст в Dialogflow будет храниться 20 минут. Нужно учитывать это при проектировании Telegram-бота. Если пользователь отвлекся на 20 минут, то он не сможет продолжить с того же места.

Примеры

Мы опубликуем исходный код навыка в ближайшее время. Сразу после его релиза.

PS. Что было на хакатоне.

image

Это были насыщенные 2 дня.
Сначала были обучающие лекции, а во второй половине дня мы приступили к реализации своих проектов.
Следующий день велась активная доработка проектов и подготовка презентаций.

Ребята из Google все это время помогали нам и отвечали на кучу вопросов, которые неизбежно возникают в работе. Это была отличная возможность узнать много нового и оставить фидбек, пока железо еще горячо.

Спасибо всем участникам, организаторам из Google а также экспертам, которые вели лекции и помогали нам на протяжении хакатона!

Мы, кстати, заняли второе место.

Если появятся вопросы, можно написать:
shipa_o
raenardev
comradeguest

А также есть Telegram-чат посвященный обсуждению голосовых интерфейсов, заходите:
https://t.me/conversational_interfaces_ru


ссылка на оригинал статьи https://habr.com/company/redmadrobot/blog/420111/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *