Amazon Echoでスマートに予定の管理をしよう!

目次

概要

今回はAmazon Echoを使ってkintoneで予定管理をスマートにしちゃいます。
Amazon Echoはスマートスピーカーの1つです。
今回kintoneは、Alexaスキルから予定データを出し入れする先として使います。

kintoneカレンダービューを用いて予定管理している方、次のような少しスマートじゃないと思っている部分もあるのではないでしょうか。

  • 予定が2件までしか表示されない。
  • 予定確認のためだけにkintoneにアクセスするのが億劫

今回は、PCやモバイルを用いてkintoneにアクセスすることなく、Amazon Echoによる音声操作で、一日のスケジュールの把握、追加を実現します。

Amazon Echoとは

Google Homeなどと同じ、スマートスピーカーの一種です。
「Amazon Alexa」を搭載しており、音声操作だけで、音楽の再生、天気を調べる、ニュースの読み上げなどさまざまなことをしてくれます。
写真は「Echo Dot」です。

Amazon Alexaとは

Amazonが開発した、音声認識技術の1つです。
Alexa搭載デバイスのEchoだけでなく、他サービスに組み込めるような音声認識サービスとなっています。
詳しくは公式サイト Alexaとは? (External link) を確認してください。

完成イメージ

下準備

Amazon開発者コンソールにログイン

Alexaスキル開発のために、 Amazon開発者コンソール (External link) へログインが必要です。
次の手順にしたがってログインしてください。

  1. Amazon.co.jp (External link) でアカウントを取得
    英語で使用したい方はAmazon開発者コンソールでアカウント取得
  2. 1で取得したアカウントを使ってAmazon開発者コンソールにログイン
caution
警告

Amazon開発者ポータルでアカウントを作成すると、Amazon.comのアカウントになります。

なぜAmazon.co.jpのアカウントで開発者ポータルにログインする必要があるか

Alexaは、一般公開されているAlexaスキルを、スキルストアで追加できます。
詳細は Amazon.co.jp のスキルストアで公開されているスキル (External link) を参照してください。
スキルストアに表示されるAlexaスキルは、Alexaへログインしているアカウントに依存します。
そのため、Amazon.comアカウントでスキルストアにアクセスすると、米国で公開されているスキルが表示され、日本語のスキルが表示されません。
日本語で一般公開されている他のAlexaスキルを利用したい方は、Amazon.co.jpアカウントでAlexaスキルを作成し、そのアカウントでEchoと連携する必要があります。
詳しくは下記URLを参照してください。
Amazon.comアカウントが優先してAlexaアプリに入れない問題の解決法 (External link)

AWSアカウントの取得

AlexaスキルはAWS Lambda関数を呼び出すことが可能です。
今回はLambda関数を用いて、kintoneのレコード取得、登録します。
下記URLを参考にAWSアカウントを取得後、ログインしてください。
AWS アカウント の要件 (External link)

kintoneアプリ準備

アプリ作成

「予定登録アプリ」を作成します。

フィールド名 フィールドコード フィールドタイプ
イベント名 eventName 文字列(1行)
開始日時 startDateTime 日時
終了日時 endDateTime 日時

予定管理なので見やすいようにカレンダー形式の一覧を作成します。

さらに見やすくしたい人は次のイベントカレンダープラグインを使うと、きれいに予定を管理できます。

APIトークン発行

「アプリの設定」>「APIトークン」でAPIトークンを発行します。
今回は、Alexaスキルで予定の確認と追加を実装したいので、閲覧と追加のアクセス権があるAPIトークンを生成します。

Alexa スキルの作成(その 1)

Step1 : Amazon開発者コンソールから、Alexaスキルを作成します。

先ほど作成したアカウントで、 Amazon開発者コンソール (External link) にログインしましょう。
画像とおりに画面を遷移して、Alexaスキルを作成します。

言語は日本語に設定します。
スキル名は任意に設定してください。

モデルは、カスタムを選択し、スキルを作成します。

スキルを作成したら、ビルドのエンドポイントの設定から、「AWS LambdaのARN」を押下し、表示されるスキルIDをメモしておいてください。

Step2 : 対話モデルの設定

インテントスキーマ

Alexaが対応できるさまざまな会話パターンを設定できます。
詳しくは公式サイトの 音声インターフェースを定義する (External link) を確認してください。
今回はインテントスキーマを次のJSONを使用して定義します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "キントーン",
            "intents": [
                {
                    "name": "askEventIntent",
                    "slots": [
                        {
                            "name": "date",
                            "type": "AMAZON.DATE"
                        }
                    ],
                    "samples": [
                        "{date}",
                        "{date} の予定は",
                        "{date} 予定",
                        "{date} イベント",
                        "{date} のイベントを教えて",
                        "{date} のイベント",
                        "{date} の予定を教えて",
                        "{date} の予定"
                    ]
                },
                {
                    "name": "AMAZON.YesIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NoIntent",
                    "samples": []
                },
                {
                    "name": "addEventIntent",
                    "slots": [
                        {
                            "name": "targetDate",
                            "type": "AMAZON.DATE"
                        },
                        {
                            "name": "eventName",
                            "type": "eventType"
                        },
                        {
                            "name": "startTime",
                            "type": "AMAZON.TIME"
                        },
                        {
                            "name": "endTime",
                            "type": "AMAZON.TIME"
                        }
                    ],
                    "samples": [
                        "{targetDate} に {startTime} から {endTime} まで {eventName} の予定",
                        "{targetDate} の {startTime} から {endTime} まで {eventName} の予定",
                        "{targetDate} {startTime} から {endTime} まで {eventName} の予定",
                        "{targetDate} に {startTime} から {endTime} {eventName}",
                        "{targetDate} の {startTime} から {endTime} {eventName}",
                        "{targetDate} {startTime} から {endTime} {eventName}",
                        "{targetDate} に {startTime} {endTime} まで {eventName}",
                        "{targetDate} の {startTime} {endTime} まで {eventName}",
                        "{targetDate} {startTime} {endTime} まで {eventName}",
                        "{targetDate} に {startTime} {endTime} {eventName} 追加して",
                        "{targetDate} の {startTime} {endTime} {eventName} 追加して",
                        "{targetDate} {startTime} {endTime} {eventName} 追加して",
                        "{targetDate} に {startTime} {endTime} {eventName} 追加",
                        "{targetDate} の {startTime} {endTime} {eventName} 追加",
                        "{targetDate} {startTime} {endTime} {eventName} 追加",
                        "{targetDate} に {startTime} {endTime} {eventName}",
                        "{targetDate} の {startTime} {endTime} {eventName}",
                        "{targetDate} {startTime} {endTime} {eventName}"
                    ]
                }
            ],
            "types": [
                {
                    "name": "eventType",
                    "values": [
                        {
                            "name": {
                                "value": "会議"
                            }
                        },
                        {
                            "name": {
                                "value": "打ち合わせ"
                            }
                        },
                        {
                            "name": {
                                "value": "勉強会"
                            }
                        },
                        {
                            "name": {
                                "value": "ミーティング"
                            }
                        },
                        {
                            "name": {
                                "value": "来訪"
                            }
                        },
                        {
                            "name": {
                                "value": "往訪"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

上記のJSONを対話モデルのJSONエディターにコピペし、モデルを保存してください。

サンプル発話

インテントとインテントを呼び出すためのフレーズをマッピングします。
ビルトインインテントはサンプル発話をマッピングする必要がありません。
ビルトインインテントについては、 ビルトインインテントとは (External link) を参照してください。
Alexaは、サンプル発話のパターンを網羅すればするほど、インテント判別の精度が上がり、スムーズに会話できます。

JSONエディターの22~31行目と、61~80行目の部分でサンプル発話を定義しています。
上の画像のように、サンプル発話が定義されているか確認してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// for askEventIntent
"samples": [
    "{date}",
    "{date} の予定は",
    "{date} 予定",
    "{date} イベント",
    "{date} のイベントを教えて",
    "{date} のイベント",
    "{date} の予定を教えて",
    "{date} の予定"
]

// for addEventIntent
"samples": [
    "{targetDate} に {startTime} から {endTime} まで {eventName} の予定",
    "{targetDate} の {startTime} から {endTime} まで {eventName} の予定",
    "{targetDate} {startTime} から {endTime} まで {eventName} の予定",
    "{targetDate} に {startTime} から {endTime} {eventName}",
    "{targetDate} の {startTime} から {endTime} {eventName}",
    "{targetDate} {startTime} から {endTime} {eventName}",
    "{targetDate} に {startTime} {endTime} まで {eventName}",
    "{targetDate} の {startTime} {endTime} まで {eventName}",
    "{targetDate} {startTime} {endTime} まで {eventName}",
    "{targetDate} に {startTime} {endTime} {eventName} 追加して",
    "{targetDate} の {startTime} {endTime} {eventName} 追加して",
    "{targetDate} {startTime} {endTime} {eventName} 追加して",
    "{targetDate} に {startTime} {endTime} {eventName} 追加",
    "{targetDate} の {startTime} {endTime} {eventName} 追加",
    "{targetDate} {startTime} {endTime} {eventName} 追加",
    "{targetDate} に {startTime} {endTime} {eventName}",
    "{targetDate} の {startTime} {endTime} {eventName}",
    "{targetDate} {startTime} {endTime} {eventName}"
]
カスタムスロットタイプ

スロットには標準スロットタイプとカスタムスロットタイプが存在します。
カスタムスロットタイプは、ユーザー側で値を設定する必要があります。
標準スロットタイプについては スロットタイプリファレンス (External link) を確認してください。
今回は、予定管理アプリの「イベント名」に対応するスロットをカスタムスロットとして用意し、valueにあらかじめイベント名を入れておきます。
先ほどのJSONの83〜119行目で定義しているので、次の画像を参考にカスタムスロットが設定されているか確認してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"types": [
    {
        "name": "eventType",
        "values": [
            {
                "name": {
                    "value": "会議"
                }
            },
            {
                "name": {
                    "value": "打ち合わせ"
                }
            },
            {
                "name": {
                    "value": "勉強会"
                }
            },
            {
                "name": {
                    "value": "ミーティング"
                }
            },
            {
                "name": {
                    "value": "来訪"
                }
            },
            {
                "name": {
                    "value": "往訪"
                }
            }
        ]
    }
]

ここまで来たら一度、Alexaの設定は終了です。
次にLambda関数を設定します。

AWS Lambda関数の設定

Step1 : Lambda関数の実行ファイル作成

Node.jsをインストールした環境で作業します。
以下のサンプルコードをファイル名「index.js」で保存します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*
 * Alexa Skill of sample program
 * Copyright (c) 2021 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

const {KintoneRestAPIClient} = require('@kintone/rest-api-client');
const Alexa = require('ask-sdk');
const luxon = require('luxon');

const ALEXA_APP_ID = '{Alexa Skill Id}'; // your alexa skill id
const DOMAIN = '{domain}.cybozu.com'; // your domain
const APP_ID = '{app id}'; // schedule app id
const API_TOKEN = '{Schedule App Token}'; // API TOKEN

// クライアントの作成
const client = new KintoneRestAPIClient({
  baseUrl: DOMAIN,
  auth: {
    apiToken: API_TOKEN
  }
});

const NO_EVENT_MESSAGE = 'その日は予定がありません。';
const ASK_ADD_EVENT = '予定を追加しますか。';
const ASK_ANOTHER_EVENT = 'ほかに予定を入れますか。';
const ASK_EVENT_DETAIL = '日付と何時から何時まで何の予定か教えてください。';
const COULD_NOT_CATCH = 'うまく聞き取れませんでした。';
let speechText;

const {
  getRequestType,
  getIntentName,
  getSlotValue,
} = require('ask-sdk-core');

// LaunchRequest
const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  handle(handlerInput) {
    speechText = 'キントーンでいつの予定を調べますか?';
    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .getResponse();
  }
};

// askEventIntent
const IntentRequestHandler = {
  canHandle(handlerInput) {
    return getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
    && getIntentName(handlerInput.requestEnvelope) === 'askEventIntent';
  },
  async handle(handlerInput) {
    // intentのslotを受け取る
    const targetDate = getSlotValue(handlerInput.requestEnvelope, 'date');
    // 対象の日付の00:00~24:00を設定する
    const date = luxon.DateTime.fromISO(targetDate);
    const fromDateTime = date.toFormat('yyyy-MM-dd') + 'T' + date.toFormat('HH:mm:ss') + 'Z';
    const toDateTime = date.plus({days: 1}).toFormat('yyyy-MM-dd') + 'T' + date.toFormat('HH:mm:ss') + 'Z';

    // クエリ作成
    const query = 'startDateTime > "' + fromDateTime
                    + '" and endDateTime < "' + toDateTime + '" order by startDateTime asc';
    const params = {
      app: APP_ID,
      query: query
    };

    // レコードの取得(GETリクエスト)
    await client.record
      .getRecords(params)
      .then((resp) => {
        // 取得件数0の場合
        if (resp.records.length === 0) {
          speechText = NO_EVENT_MESSAGE + ASK_ADD_EVENT;
        }
        // 取得成功した場合
        if (resp.records.length === 1) {
          const record = resp.records[0];
          const dateTime = record.startDateTime.value;
          const jpDateTime = luxon.DateTime.fromISO(dateTime).plus({hours: 9}).toFormat('HH:mm');
          const eventDetail = 'その日は' + jpDateTime + 'から' + record.eventName.value + 'です。';
          speechText = eventDetail + ASK_ANOTHER_EVENT;
        } else if (resp.records.length >= 2) {
          let arrayEventDetail = 'その日は';
          resp.records.forEach((rec) => {
            const dateTime1 = rec.startDateTime.value;
            const jpDateTime1 = luxon.DateTime.fromISO(dateTime1).plus({hours: 9}).toFormat('HH:mm');
            arrayEventDetail += jpDateTime1 + 'から' + rec.eventName.value + '、';
          });
          arrayEventDetail = arrayEventDetail.slice(0, -1) + 'です。';
          speechText = arrayEventDetail + ASK_ANOTHER_EVENT;
        }
      }).catch((err) => {
        console.log(err);
      });

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .getResponse();
  }
};

// addEventIntent
const addEventIntentHandler = {
  canHandle(handlerInput) {
    return getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
    && getIntentName(handlerInput.requestEnvelope) === 'addEventIntent';
  },
  async handle(handlerInput) {
    // intentの引数を受け取る
    const intentObj = handlerInput.requestEnvelope.request.intent;
    const targetDate = intentObj.slots.targetDate.value;
    const startTime = intentObj.slots.startTime.value;
    const endTime = intentObj.slots.endTime.value;
    const eventName = intentObj.slots.eventName.value;

    // 開始と終了日時を設定する
    const start = luxon.DateTime.fromISO(targetDate + 'T' + startTime);
    const end = luxon.DateTime.fromISO(targetDate + 'T' + endTime);
    const stDateTime = start.minus({hours: 9}).toFormat('yyyy-MM-dd') + 'T' + start.minus({hours: 9}).toFormat('HH:mm:ss') + 'Z';
    const endDateTime = end.minus({hours: 9}).toFormat('yyyy-MM-dd') + 'T' + end.minus({hours: 9}).toFormat('HH:mm:ss') + 'Z';

    // POSTレコード作成
    const putRecord = {
      app: APP_ID,
      record: {
        startDateTime: {
          value: stDateTime
        },
        endDateTime: {
          value: endDateTime
        },
        eventName: {
          value: eventName
        }
      }
    };

    const add_complete_text = targetDate + 'の' + startTime + 'から' + endTime + 'に' + eventName + 'を登録しました。';

    if (targetDate && startTime && endTime && eventName) {
      // POSTリクエスト送信
      await client.record
        .addRecord(putRecord)
        .then((resp) => {
          speechText = add_complete_text;
        }).catch((err) => {
          speechText = COULD_NOT_CATCH;
          console.log(err);
        });
    } else {
      speechText = COULD_NOT_CATCH + ASK_EVENT_DETAIL;
    }

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .getResponse();
  }
};

const YesRequestHandler = {
  canHandle(handlerInput) {
    return getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
    && getIntentName(handlerInput.requestEnvelope) === 'AMAZON.YesIntent';
  },
  handle(handleInput) {
    return handleInput.responseBuilder
      .speak('追加します。' + ASK_EVENT_DETAIL)
      .reprompt('追加します。' + ASK_EVENT_DETAIL)
      .getResponse();
  }
};

const NoRequestHandler = {
  canHandle(handlerInput) {
    return getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
    && getIntentName(handlerInput.requestEnvelope) === 'AMAZON.NoIntent';
  },
  handle(handleInput) {
    return handleInput.responseBuilder
      .speak('わかりました。終了します。')
      .getResponse();
  }
};

const ErrorHandler = {
  canHandle() {
    return true;
  },
  handle(handlerInput, error) {
    return handlerInput.responseBuilder
      .speak('すみません。理解できませんでした。もう一度お願いします。')
      .reprompt('すみません。理解できませんでした。もう一度お願いします。')
      .getResponse();
  },
};

let skill;
exports.handler = async function(event, context) {
  if (!skill) {
    // eslint-disable-next-line require-atomic-updates
    skill = await Alexa.SkillBuilders.custom()
      .withSkillId(ALEXA_APP_ID)
      .addRequestHandlers(
        LaunchRequestHandler,
        IntentRequestHandler,
        YesRequestHandler,
        NoRequestHandler,
        addEventIntentHandler
      )
      .addErrorHandlers(ErrorHandler)
      .create();
  }

  const response = await skill.invoke(event, context);
  return response;
};

上記コードの13〜16行目を、ご自身の環境に合わせて設定してください。

1
2
3
4
const ALEXA_APP_ID = '{Alexa Skill Id}'; // your alexa skill id
const DOMAIN = '{domain}.cybozu.com'; // your domain
const APP_ID = '{app id}'; // schedule app id
const API_TOKEN = '{Schedule App Token}'; // API TOKEN

以下のコマンドを先ほど保存したコードと同じ階層で実行してpackage.jsonを生成します。

1
npm init

上記のコマンド実行後にpackage.jsonの設定をしますが、namedescriptionのみ任意に設定していただき、他はEnterで進んで問題ありません。
次に以下のコマンドでパッケージをインストールします。

1
npm install alexa-sdk @kintone/rest-api-client luxon

さらに以下のコマンドを実行してZipファイルを作成します。
実行失敗した場合は、「index.js」と「node_modules」が同じ階層に存在しているか確認してください。

1
zip -rq Alexa-to-kintone.zip index.js node_modules
windows OSをお使いの方

windowsコマンドプロンプトを使用している方は、標準ではzipコマンドが入力できません。
そのため、 Cygwin (External link) などのツールが必要です。
また、Cygwinでファイルを作成すると、作成されたファイルに実行権限(パーミッション)を付与される場合があるので注意してください。

Step2 : Lambda関数作成

AWS Lambda関数設定画面から関数を作成します。
作成するリージョンは「アジアパシフィック(東京)」を選択してください。
詳細は AWS Lambda関数に最適なリージョンを選択する (External link) を確認してください。

「一から作成」を選択し、「名前」「ランタイム」「ロール」を設定します。
順番に、「名前」は任意、「ランタイム」は「Node.js 8.10」、「ロール」は「カスタムロールの作成」を設定してください。
「カスタムロールの作成」を選択すると画面が遷移します。

「カスタムロールを作成」を選択すると、下図の画面に遷移します。
「IAMロール」は「新しいIAMロールの作成」を選択し、「ロール名」は任意に設定してください。
設定後、許可を選択するとひとつ前の画面に戻ります。

ロール作成を終えたら先ほどの画面に遷移するので、「既存のロールを選択」を選択し、先ほど作成したロールを設定してください。
関数の作成をクリックするとLambda関数が作成できます。

関数を作成し終えたら、画面右上に表示されるARNをメモしておきましょう。
また、先ほど作成したZIPファイルの実行コードをアップロードします。
「ハンドラー」が先ほど作成したJavascriptファイル「index.js」を指定するindex.handlerになっているか確認してください。

次に、Alexa Skills KitをLambda関数のトリガーに設定します。
トリガーの設定のスキルID欄に、 Alexa スキルの作成(その 1)でメモしたスキルIDを入力してください。

これでLambda関数の設定は終了です。

Alexa スキルの作成(その 2)

Lambda関数の設定が終了したら、もう一度Amazon開発者コンソールに戻ります。
その1で作成したAlexaスキルの編集画面から、エンドポイントを開き、「AWS LambdaのARN(Amazonリソースネーム)」にチェックを入れます。
デフォルトの地域に先ほどメモしたLambda関数のARNをペーストし、エンドポイントを保存してください。

最後に、次の画面から、モデルをビルドしていただくとAlexaスキルとLambda関数を連携できます。

ビルドが失敗する場合は、対話モデル > インテント > インテントを追加 > Alexaのビルトインライブラリから「既存のインテントを使用」より、次の3つを追加して再ビルドを実行してみてください。

  • AMAZON.StopIntent
  • AMAZON.HelpIntent
  • AMAZON.CancelIntent

動作確認

先ほどの設定を終えると、作成したAlexaスキルに対して次の3パターンを動作確認してみましょう。

  • 予定の確認(Alexa開発者コンソール上)
  • 予定の追加(Alexa開発者コンソール上)
  • Echoで音声操作して動作確認

まずはAlexa開発者コンソール上からテストを有効化してください。

予定の確認

テスト画面でAlexaに今日の予定を訪ねます。

Alexaがkintoneの情報を取得できているのがわかります。
隠れている2つ分の予定も確認できていますね。

予定の追加

次に明日の予定を訪ねます。
明日は何も予定が入っていません。

明日は予定がないことを確認できました。
そこで、予定を追加してみます。

しっかりと予定が追加できました!

Echoで動作確認をする場合

Alexaスキルはテスト終了後の設定で作成したスキルの公開設定がありますが、Echoに紐づけたアカウントでスキルを開発し、テストを有効化することで、スキルを公開せず利用できます。

EchoとAmazon開発者アカウントの紐づけは以下のサイトを参考にしてください。

サンプルコード解説

Lambdaに設定したサンプルコード「index.js」について少し解説します。

KintoneRestAPIClient

1
const {KintoneRestAPIClient} = require('@kintone/rest-api-client');

kintoneアプリのレコードを操作する際に、@kintone/rest-api-clientを使用しています。
@kintone/rest-api-clientは、kintone REST APIをJavaScriptで扱う際に必要な処理をまとめたライブラリです。
詳しい導入と使用方法は GitHub (External link) 、または リファレンス (External link) を確認してください。

ここではサンプルコードで使用しているクラスと関数について簡単に説明していきます。

GitHub

https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client (External link)

リファレンス

https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client#references (External link)

まず、サンプルコードの14~24行目についてです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const DOMAIN = '{domain}.cybozu.com'; // your domain
const APP_ID = '{app id}'; // schedule app id
const API_TOKEN = '{Schedule App Token}'; // API TOKEN

// クライアントの作成
const client = new KintoneRestAPIClient({
  baseUrl: DOMAIN,
  auth: {
    apiToken: API_TOKEN
  }
});

上記の6行目でclientを作成します。
clientにはbaseUrlauthプロパティを含める必要があります。
baseUrlプロパティには利用しているkintone環境のドメインを設定し、authプロパティには認証情報を設定します。
今回はAPIトークンを認証する、authプロパティにapiTokenを指定しています。

次にサンプルコードの75行目のkintoneアプリのレコード取得、150行目のkintoneアプリのレコード登録をしている部分についての説明です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// *********************** //
// レコードの取得(GETリクエスト)
// *********************** //
const params = {
  app: APP_ID,
  query: query
};

await client.record
  .getRecords(params)
  .then((resp) => {
    // 成功時の処理
    // 中略
  }).catch((err) => {
    // エラー時の処理
    console.log(err);
  });

// *********************** //
// レコードの登録(POSTリクエスト)
// *********************** //
const putRecord = {
  app: APP_ID,
  record: {
    フィールド名: {
      value: '値'
    }
  }
};

await client.record
  .addRecord(putRecord)
  .then((resp) => {
    // 成功時の処理
    // 中略
  }).catch((err) => {
    // エラー時の処理
    console.log(err);
  });

上記の9行目で「アプリID」と「クエリ条件文」をもとにkintoneアプリからレコードを複数件取得しています。
Promiseオブジェクトで返ってくるので、thenメソッドとcatchメソッドでそれぞれ成功時、エラー時の処理を記述します。

30行目では、「アプリID」と「レコードのJSON」でkintoneアプリにレコードを1件追加しています。
先ほどと同様にPromiseオブジェクトなので、thencatchでそれぞれ対応する処理を記述します。

Alexa ASK SDK for Node.js

Alexa ASK SDK for Node.jsは、Node.jsを使ってLambda上でAlexaスキルを作成する際に非常に便利なSDKです。
サンプルコードで使用しているAlexa ASK SDK for Node.jsはバージョン2(v2)を使用しています。
SDKの導入や利用方法などの詳細は GitHub (External link) をご確認してください。

次のHelloWorldIntentサンプルをもとに軽く解説します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
 * Alexa Skill of sample program
 * Copyright (c) 2021 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

const Alexa = require('ask-sdk');
const ALEXA_APP_ID = '{Alexa Skill Id}'; // your alexa skill id

const HelloWorldIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent';
  },
  handle(handlerInput) {
    const speechText = 'Hello World!';

    return handlerInput.responseBuilder
      .speak(speechText)
      .withSimpleCard('Hello World', speechText)
      .getResponse();
  }
};

let skill;
exports.handler = async function(event, context) {
  if (!skill) {
    skill = Alexa.SkillBuilders.custom()
      .withSkillId(ALEXA_APP_ID)
      .addRequestHandlers(
        HelloWorldIntentHandler
      )
      .create();
  }

  const response = await skill.invoke(event, context);
  return response;
};

サンプルではHelloIntentが呼び出されたとき、「Hello World!」とアレクサが応答するようになっています。

サンプルコードは大きく3つの構成になっています。

  1. 9,10行目でask-sdkの読み込みと変数定義
  2. 12〜25行目でAlexaスキルのハンドラーを定義
  3. 28〜40行目でLambdaハンドラーを作成

まず、AWS Lambda関数が呼び出された時に28行目から40行目のLambdaハンドラーが実行されます。
30行目でSkillBuilders.customビルダーを使用してSDKインスタンスを作成しています。
次に31行目ですが、.withSkillId(ALEXA_APP_ID)でAlexaスキルのスキルIDを判断しています。
10行目で定義したALEXA_APP_IDと一致しないスキルIDのリクエストをすべて拒否します。
さらに32行目のaddRequestHandlersビルダー関数からHelloWorldIntentHandlerを呼び出しているので、12行目が呼び出されます。

12〜25行目はAlexaスキルのハンドラーを定義しています。
今回はHelloWorldIntentHandlerです。
Alexaスキルのハンドラー内にはcanHandle関数とhandle関数の2つの関数が定義されています。
1つ目のcanHandle関数は、リクエストがIntentRequestかを検出し、リクエストのインテント名がHelloWorldIntentの場合にtrueを返します。
trueの場合は、そのままhandle関数が実行されます。
また、通常リクエストを取り出す場合は、handlerInput.requestEnvelope.request.type === 'IntentRequest'と書きます。
ただ、getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'のような、より直接的にスロット値を取得できるユーティリティ関数やヘルパー関数が提供されています。
詳しくは リクエストからスロット値を取得する (External link) を参考にしてみてください。

2つ目のhandle関数は、実行したい処理やアレクサの応答レスポンスを作成して返します。
20行目のhandlerInput.responseBuilderには応答レスポンスに 利用可能なメソッド (External link) がいくつか用意されているため、利用したいメソッドを確認してみてください。

最後に38,39行目でアレクサの応答レスポンスを保持しているskillインスタンスをreturnすることで、リクエストに応答したスキルを作成できます。

更新履歴

  • 2018/05/18
    • Alexa開発者コンソールの新UIに対応
  • 2018/07/12
    • kintone API SDK(β)for Node.jsを使用したコードに修正
  • 2021/05/21
    • kintone RestAPIClientを使用したコードに修正
    • luxonに変更
    • ask-sdkに変更

おわりに

いかがでしょうか。
声だけでkintoneのレコード登録と取得ができました。
kintoneはほかにも豊富にAPIが用意されているので、音声操作でいろいろな操作ができます。
各サービスのガイドです。お困りの際はご活用ください!

information

このTipsは、2021年5月時点のkintoneで動作を確認しています。