Первоначально опубликовано в блоге Что я узнал »

В прошлый раз мы научились захватывать ввод от пользователя. Используя захваченные входные данные, мы можем управлять общением с пользователями с помощью наших навыков.

Но навык не всегда имеет линейный разговор. Мы можем захотеть задать вопрос и вести себя по-разному в зависимости от полученного ответа. Для этого нам нужно сохранить некоторую информацию в «памяти» навыка Alexa.

Для этого Alexa Skill SDK предоставляет нам возможность использовать атрибуты сеанса.

Давайте посмотрим, как мы можем это использовать.

Чтобы продемонстрировать это на примере, давайте реализуем «Симулятор игрушечного робота» как навык Alexa.

Симулятор игрушечного робота

Если вы не знакомы с проблемой игрушечного робота, вот она.

У нас есть столешница размером 5 и 5 квадратов. Мы можем поставить игрушечного робота на стол. Тогда у робота будут свои координаты и положение лица. Направление взгляда может быть «север», «восток», «юг» или «запад». Как только робот окажется на столе, мы сможем переместить его с помощью команды «Переместить». Он должен переместиться на один шаг в сторону лица. Мы также можем вращать его «влево» и «вправо», меняя направление взгляда. Мы можем использовать команду «Отчет», чтобы запросить текущую позицию робота. Также робот не должен упасть со стола. Чтобы выполнить это требование, мы не должны двигать робота, если он находится на краю.

Реализация навыков

Сначала я создам шаблонный навык для работы. Я создам его с помощью ASK CLI так же, как я описал в статье Использование ASK CLI для создания и развертывания Alexa Skills. Если вы не знакомы с этим, вы можете следовать этим инструкциям.

В конце этого шага у нас будет навык с приветственным сообщением и возможностью разместить робота. Как только мы поместим его в исходное положение, Alexa сообщит нам об этом и отключит навык.

PlaceIntent отвечает за запуск симуляции. Вот как выглядит реализация:

const PlaceIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'PlaceIntent';
  },
  handle(handlerInput) {
    const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
    const { position } = sessionAttributes;
    const speechText = 'The robot is in the initial position.';
    return handlerInput.responseBuilder
      .speak(speechText)
      .withSimpleCard(CARD_TITLE, speechText)
      .getResponse();
  },
};

Этого было бы достаточно, чтобы начать отсюда.

Размещение робота

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

Также, если робот уже находится в позиции, мы хотим уведомить об этом пользователя.

Чтобы сохранить положение робота, мы можем использовать следующую структуру:

position = {
  'direction': 'north',
  'x': 0,
  'y': 0
};

Если мы определим положение робота как обычную константу, это значение будет потеряно между намерениями. Таким образом, нам нужно правильно сохранить его в атрибутах сеанса.

Как мы это делаем?

Объект handlerInput содержит файл attributesManager. Что, в свою очередь, дает нам функции getSessionAttributes и setSessionAttributes.

  • getSessionAttributes - как вы уже поняли, предоставляет нам список существующих атрибутов сеанса
  • setSessionAttributes - позволяет нам их обновить.

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

const PlaceIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'PlaceIntent';
  },
  handle(handlerInput) {
    const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
    const { position } = sessionAttributes;
    let speechText = 'The robot is already in the position.';
    if (typeof position === 'undefined') {
      speechText = 'The robot is in the initial position.';
      sessionAttributes.position = {
        'direction': 'north',
        'x': 0,
        'y': 0
      };
    }
    handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withSimpleCard(CARD_TITLE, speechText)
      .getResponse();
  },
};

Теперь у нас есть робот в позиции, и позиция хранится как атрибут сеанса.

Давайте продолжим и реализуем ReportIntent, чтобы получить текущую позицию робота.

Получить отчеты

Во-первых, нам нужно создать намерение. Мы можем обновить файл models/en-US.json, включив в него:

{
  "name": "ReportIntent",
  "slots": [],
  "samples": [
    "Report",
    "Where is the robot",
    "Where am I"
  ]
}

Затем нам нужно добавить ReportIntentHandler к addRequestHandlers:

addRequestHandlers(
  // ...
  ReportIntentHandler,
  // ...
)

Затем нам нужно определить сам ReportIntentHandler:

const ReportIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'ReportIntent';
  },
  handle(handlerInput) {
    const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
    const { position } = sessionAttributes;
    let speechText = '';
    if (typeof position === 'undefined') {
      speechText = 'The robot is not in the position yet. You need to place it first.';
    } else {
      const { direction, x, y } = position;
      speechText = `The robot is in position ${x} ${y} facing ${direction}.`;
    }
    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withSimpleCard(CARD_TITLE, speechText)
      .getResponse();
  },
};

Как видите, на этот раз мы используем только getSessionAttributes для чтения текущей позиции. Нам не нужно его обновлять.

Как только мы читаем позицию и она существует, мы сообщаем пользователю текущие координаты робота. Если позиция пуста, мы предполагаем, что робот еще не был размещен.

Вот пример диалога пользователя:

Поверните робота

Как обычно, начнем с определения намерения.

{
  "name": "TurnRightIntent",
  "slots": [],
  "samples": [
    "turn right",
    "right",
    "rotate right"
  ]
}

Затем нам нужно зарегистрировать обработчик запроса:

.addRequestHandlers(
  // ...
  TurnRightIntentHandler,
  // ...
)

И реализовать само намерение:

const TurnRightIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'TurnRightIntent';
  },
  handle(handlerInput) {
    const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
    const { position } = sessionAttributes;
    const rotateDirections = {
      north: 'east',
      east: 'south',
      south: 'west',
      west: 'north'
    };
    let speechText = '';
    if (typeof position === 'undefined') {
      speechText = 'The robot is not in the position yet. You need to place it first.';
    } else {
      position.direction = rotateDirections[position.direction];
      speechText = 'Beep-Boop.';
    }
    handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withSimpleCard(CARD_TITLE, speechText)
      .getResponse();
  },
};

Сначала читаем текущую позицию. Таким же образом, как и в ReportIntentHandler, мы проверяем, был ли вообще размещен робот. Затем определяем направления вращения. Если мы повернем направо, нам нужно пересесть с севера на восток, с востока на юг и так далее. Затем мы фактически вращаем робота и обновляем атрибуты сеанса. Потому что мы хотим, чтобы они сохранялись во время нашего сеанса.

Поворот робота влево практически идентичен. Я пропущу это здесь. Отличие только в направлении вращения. С севера вращаемся на запад, с запада — на юг и так далее.

Вот пример снова.

Теперь давайте перейдем к реализации последнего шага. Движение.

Движение

Намерение идет первым.

{
  "name": "MoveIntent",
  "slots": [],
  "samples": [
    "move",
    "go forward"
  ]
}

Затем регистрируется обработчик.

.addRequestHandlers(
  // ...
  MoveIntentHandler,
  // ...
)

Затем сама реализация.

const MoveIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'MoveIntent';
  },
  handle(handlerInput) {
    const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
    let { position } = sessionAttributes;
    let speechText = '';
    if (typeof position === 'undefined') {
      speechText = 'The robot is not in the position yet. You need to place it first.';
    } else {
      position = calculateNewPosition(position);
      speechText = 'Beep-Boop.';
    }
    handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .withSimpleCard(CARD_TITLE, speechText)
      .getResponse();
  },
};

Как вы видете. Это снова очень похоже на намерения, которые мы реализовали ранее. Читаем атрибуты сессии, меняем их и снова обновляем.

Разница здесь в расчете новой позиции:

position = calculateNewPosition(position);

Вот реализация этой функции:

const calculateNewPosition = (position) => {
  switch (position.direction) {
    case 'north':
      position.x = Math.min(position.x + 1, 4);
      return;
    case 'east':
      position.y = Math.min(position.y + 1, 4);
      return;
    case 'south':
      position.x = Math.max(position.x - 1, 0);
      return;
    case 'west':
      position.x = Math.max(position.y - 1, 0);
      return;
    default:
  }
  return position;
};

Мы перемещаем робота в зависимости от того, куда он смотрит. Кроме того, мы предотвращаем падение робота со стола. Именно поэтому не идут выше 4 и ниже 0 по координатам.

Есть пример после того, как я сделал пару ходов.

Подведение итогов

Вот и все. Вы можете видеть, что использование атрибутов сеанса — довольно тривиальная задача. Хотя я пытался привести больше примеров и сделать статью более полной.

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

Вы можете найти полный исходный код на странице GitHub.

Увидимся в следующий раз.

Первоначально опубликовано в блоге Что я узнал.