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 は、Lambda 上で Node.js で 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 で動作を確認しています。