Недавно Cбер запустил Салют — семейство виртуальных ассистентов, которые работают на разных платформах. Мы в SberDevices, кроме самого ассистента, занимаемся разработкой инструментов, позволяющих любому разработчику удобно создавать навыки, которые называются смартапы. Кроме общеизвестных диалоговых сценариев в формате чата — ChatApp, можно создавать смартапы в формате веб-приложения на любых известных веб-технологиях — Canvas App. О том, как создать простейший смартап такого типа, и пойдет сегодня речь.

Canvas App — стандартное веб-приложение в привычном понимании, которое запускается и работает внутри WebView, но есть свои особенности.

Как это работает по шагам:

  1. Пользователь произносит ключевую фразу, например «Салют, какие у меня задачи на сегодня».

  2. Голосовой запрос приходит в NLP-платформу, где разбирается на фонемы, там же определяется его эмоциональный окрас и т.д..

  3. В БД зарегистрированных смартапов находится тот, которому соответствует активационная фраза. Регистрация происходит через SmartApp Studio и доступна всем разработчикам без исключения.

  4. Во время регистрации смартапа в SmartApp Studio разработчик указывает два эндпоинта: один для веб-приложения, второй для сценарного бэкенда. Именно их достанет из БД NLP-платформа, когда найдет соответствующий смартап.

  5. В эндпоинт сценарного бэкенда будет отправлено сообщение с распознанной активационной фразой. Формат сообщений подробно описан в документации SmartApp API.

  6. Эндпоинт веб-приложения будет указан для загрузки в WebView.

  7. Ответ от сценарного бэкенда придёт в веб-приложение в качестве JS-события, подписавшись на которое, можно управлять веб-приложением.

Упрощенная схема для наглядности

Предмет нашего разговора – веб-приложение. Делать будем смартап для ведения тудушек. Поскольку SmartApp Studio предоставляет онлайн-среду разработки сценариев, не будем подробно на этом останавливаться, а воспользуемся форком готового сценария, который в качестве примера доступен на GitHub. В одной из следующих статей расскажем, как написать такой сценарий на NodeJS.

В SmartApp Graph/IDE, той самой онлайн-среде, в качестве источника можно указать git-репозиторий, чем мы и воспользуемся, чтобы получить эндпоинт до сценарного бэкенда. Далее его надо указать при регистрации нашего смартапа в SmartApp Studio. В качестве эндпоинта веб-приложения укажем любой известный веб-ресурс, например, sberdevices.ru. Позже поменяем на URL нашего веб-приложения.

Шаблон проекта

Для примера будем делать веб-приложение на React. К React нет никакой привязки и пример ниже может быть написан на чём угодно. Для нетерпеливых выложили конечный результат на GitHub.

Итак, что мы хотим от приложения:

  • добавлять задачи; 

  • выполнять задачи;

  • удалять задачи;

  • и все это голосом, но не сразу.

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

> npx create-react-app todo-canvas-app

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

Код формы
export const App: FC = memo(() => {
  const [note, setNote] = useState("");

  return (
    <main className="container">
      <form
        onSubmit={(event) => {
          event.preventDefault();
          setNote("");
        }}
      >
        <input
          className="add-note"
          type="text"
          value={note}
          onChange={({ target: { value } }) => setNote(value)}
        />
      </form>
      <ul className="notes">
        {appState.notes.map((note, index) => (
          <li className="note" key={note.id}>
            <span>
              <span style={{ fontWeight: "bold" }}>{index + 1}. </span>
              <span
                style={{
                  textDecorationLine: note.completed ? "line-through" : "none",
                }}
              >
                {note.title}
              </span>
            </span>
            <input
              className="done-note"
              type="checkbox"
              checked={note.completed}
            />
          </li>
        ))}
      </ul>
    </main>
  );
});

Дальше нам надо сделать базовую логику нашего приложения. Пользоваться будем стандартными средствами React, используя useReducer.

Код редьюсера
const reducer = (state, action) => {
  switch (action.type) {
    case "add_note":
      return {
        ...state,
        notes: [
          ...state.notes,
          {
            id: Math.random().toString(36).substring(7),
            title: action.note,
            completed: false,
          },
        ],
      };

    case "done_note":
      return {
        ...state,
        notes: state.notes.map((note) =>
          note.id === action.id ? { ...note, completed: !note.completed } : note
        ),
      };

    case "delete_note":
      return {
        ...state,
        notes: state.notes.filter(({ id }) => id !== action.id),
      };

    default:
      throw new Error();
  }
};

Далее будем диспатчить экшены их обработчиков на форме.

Код подключения
export const App: FC = memo(() => {
  const [appState, dispatch] = useReducer(reducer, { notes: [] });
  //...

  return (
    <main className="container">
      <form
        onSubmit={(event) => {
          event.preventDefault();
          dispatch({ type: "add_note", note });
          setNote("");
        }}
      >
        <input
          className="add-note"
          type="text"
          placeholder="Add Note"
          value={note}
          onChange={({ target: { value } }) => setNote(value)}
          required
          autoFocus
        />
      </form>
      <ul className="notes">
        {appState.notes.map((note, index) => (
          <li className="note" key={note.id}>
            <span>
              <span style={{ fontWeight: "bold" }}>{index + 1}. </span>
              <span
                style={{
                  textDecorationLine: note.completed ? "line-through" : "none",
                }}
              >
                {note.title}
              </span>
            </span>
            <input
              className="done-note"
              type="checkbox"
              checked={note.completed}
              onChange={() => dispatch({ type: "done_note", id: note.id })}
            />
          </li>
        ))}
      </ul>
    </main>
  );
});

Запускаем и проверяем.

npm start

Работа с голосом

Когда наше приложение базово работает, можно добавить немного магии голосового управления. Для этого надо установить Assistant Client — библиотеку для взаимодействия с виртуальным ассистентом.

npm i @sberdevices/assistant-client

В момент открытия WebView платформа инжектит JS API для взаимодействия с ассистентом. Это биндиги до нативных методов платформы. Assistant Client — обёртка, которая в дев-режиме позволяет отлаживать взаимодействие с ассистентом в браузере, а в продакшене предоставляет удобный для веб-приложений API.

Идём в app.js и там же, где наш основной редюсер, создаем инстанс Assistant Client.

const initializeAssistant = () => {
  if (process.env.NODE_ENV === "development") {
    return createSmartappDebugger({
      token: process.env.REACT_APP_TOKEN ?? "",
      initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`,
    });
  }

  return createAssistant();
};

Судя по коду выше, нужен некий токен. Токен обеспечивает авторизацию сообщений в NLP-платформе. Токен автоматически приклеивается к сообщениям, когда смартап запускается на устройстве, но в нашем случае это браузер, поэтому токен надо передать вручную. Токен генерируется автоматически для каждого разработчика в SmartApp Studio.

После этого перезапустим наше приложение. Теперь мы видим панельку ассистента с лавашаром и текстовым полем. Лавашар это такое визуальное представление ассистента. По нажатию на лавашар включится микрофон и вы сможете отправить команду ассистенту так же, как вы бы это сделали, запуская смартап на устройстве. Относитесь к этому не как к эмулятору, а как к дев-тулзам, в продакшене всё это за нас будет делать платформа. Те же самые команды вы можете посылать не только голосом, но и текстом, используя текстовое поле рядом с лавашаром, чтобы не будить своих домашних по ночам.

Ассистент присылает структурированные команды в формате JSON. Полное описание формата можно найти в документации Assistant Client на GitHub.

interface AssistantSmartAppCommand {
  // Тип команды
  type: "smart_app_data";
  // Любые данные, которые нужны смартапу
  smart_app_data: Record<string, any>;
  sdkMeta: {
    requestId: string;
  };
}

Теперь подпишем наши экшены на команды от ассистента. Для этого в коде нашего сценария определены специальные интенты — ключевые слова в фразах, которые может говорить пользователь. Разные интенты генерируют разные команды веб-приложению.

export const App: FC = memo(() => {
  const [appState, dispatch] = useReducer(reducer, { notes: [] });

  const [note, setNote] = useState("");

  const assistantRef = useRef();

  useEffect(() => {
    assistantRef.current = initializeAssistant();

    assistantRef.current.on("data", ({ action }) => {
      if (action) {
        dispatch(action);
      }
    });
  }, []);
  
  // ...

Сохраняем, запускаем — ничего не работает. Не волнуйтесь, так и должно быть. Я приоткрою завесу того, как на самом деле работает магия. 

Дело в том, что ваш сценарий сам по себе только лишь по фразе пользователя не может узнать то, что у вас сейчас на экране. Чтобы эта магия работала, к каждому голосовому запросу необходимо клеить стейт веб-приложения. Тут мы приходим к осознанию, что сценарный бэкенд получает на вход не только разобранную фразу, но и данные с экрана — стейт. Задача сценария провести пользователя к следующему шагу по этим двум параметрам, отправив команду веб-приложению на изменение стейта. Можно мыслить себе это как голосовой аналог клика. Разница лишь в том, что элемент управления для такого клика в интерфейсе может и не существовать физически. Например, если бы мы делали интернет-магазин, то кнопку добавления в корзину можно было бы и опустить в пользу голосовой команды «Афина, добавь в корзину красные туфли».

Для того, чтобы это было удобно делать из веб-приложения, в Assistant Client есть API для передачи состояния — getState. В нашем случае стейт – это список тудушек и некоторая мета-информация.

Дополним код инициализации Asisstant Client.

const initializeAssistant = (getState) => {
  if (process.env.NODE_ENV === "development") {
    return createSmartappDebugger({
      token: process.env.REACT_APP_TOKEN ?? "",
      initPhrase: `Запусти ${process.env.REACT_APP_SMARTAPP}`,
      getState,
    });
  }

  return createAssistant({ getState });
};

И передадим стейт в обработку ассистенту. Формат стейта также описан в документации Asisstant Client.

export const App: FC = memo(() => {
  // ...
  const assistantStateRef = useRef<AssistantAppState>();
	// ...

  useEffect(() => {
    assistantRef.current = initializeAssistant(() => assistantStateRef.current);
    // ...
  }, []);

  useEffect(() => {
    assistantStateRef.current = {
      item_selector: {
        items: appState.notes.map(({ id, title }, index) => ({
          number: index + 1,
          id,
          title,
        })),
      },
    };
  }, [appState]);
  // ...

Из кода выше видим появление мета-информации в виде нумерации. Зачем? Согласитесь, тудухи могут быть довольными длинными и иногда удобнее было бы говорить «Джой, я сделал первую задачу» вместо полного заголовка. Но погодите, как это работает? Где единичка превращается в «первую»? Эту магию кастования натуральных фраз, которые мы привыкли использовать в повседневной речи, в машинный формат делает за нас NLP-платформа. То же самое происходит, например, с командами навигации.

Тудух может скопиться достаточное количество, чтобы они не влезли в экран. Само собой, мы хотим уметь скроллить экран, чтобы иметь возможность прочитать всё, что скопилось. На устройствах, где нет тач-интерфейса, например, на SberBox, мы можем скроллить пультом ДУ или голосом. Нажатия кнопок на пульте превращаются в события нажатий на стрелки клавиатуры на window, но что делать с голосом?

Голосовые паттерны навигации встроены в NLP-платформу, и разработчику сценария ничего не надо делать самому. А для разработчика веб-приложения достаточно подписаться на специальный тип команд, приходящих от ассистента через Assistant Client. Все вариации навигационных фраз будут кастится в конечное число навигационных команд. Их всего пять: UP, DOWN, LEFT, RIGHT, BACK.

assistant.on('data', (command) => {
    if (command.navigation) {
        switch(command.navigation.command) {
            case 'UP':
                window.scrollTo(0, 0);
                break;
            case 'DOWN':
                window.scrollTo(0, 1000);
                break;
        }
    }
});

Перезапускаем наше приложение и пробуем после нажатия на лавашар сказать: «Напомни купить коту корм». И вуаля!

Если у вас есть устройство под рукой, то можно проверить работу смартапа на нём. Для этого не обязательно его публиковать или деплоить куда-либо. Достаточно создать тоннель с локального хоста, например, с помощью ngrok.

ngrok http 3000

Полученный URL с https указываем в SmartApp Studio, сохраняем черновик и говорим ассистенту: «Сбер, какие у меня задачи на сегодня?». Это cработает, если вы залогинены под одним и тем же SberID на устройстве и в SmartApp Studio. Черновики по-умолчанию доступны к запуску на устройствах разработчика.

Вместо эпилога

Смысл статьи — в наглядной демонстрации того, как голосовое управление прозрачным образом можно интегрировать не только в специально для этого созданные приложения, но и в уже существующие. Например, если у вас уже есть рабочий веб-сервис, то научить его работать на платформе Салют не составит большого труда.

Этот короткое интро — скорее, обзор возможностей на искусственном примере. Как сделать смартап с компонентами, оплатой, автотестами, обязательно расскажем в следующих статьях. Спасибо за внимание, ещё увидимся!

По всем вопросам разработки смартапов можно обращаться в сообщество разработчиков SmartMarket в телеграмме.

Let’s block ads! (Why?)

Read More

Recent Posts

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

2 дня ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

2 дня ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

2 дня ago

Нейросети будут писать тексты объявления за продавцов на Авито

У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…

2 дня ago

Объявлены победители международной премии Workspace Digital Awards-2024

24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…

3 дня ago

Яндекс проведет гик-фестиваль Young Con

27 июня Яндекс проведет гик-фестиваль Young Con для студентов и молодых специалистов, которые интересуются технологиями и хотят работать в IT.…

3 дня ago