Alexa の Unhandled を完全に勘違いしていたお話

# <font color="green">はじめに</font>

Alexa のスキル開発をやっていくなかで 

**<font color="red">「なんでこうなるの！？」</font>** 

って感じで Alexa が腑に落ちない挙動をすることがありました。

そして、色々調べて行くうちに、大きな勘違いをしていたことに気がつきました。

本記事ではその大きな勘違いを共有する記事になります。
そして、細やかながら Alexa スキル開発を楽しむ皆様のお力になれたらいいなと思います。

※本記事では Alexa のスキル開発を行う際に登場する概念や用語を躊躇いなく使っていきます。
※また、本記事内ではそれらの用語の説明は行いませんのでご了承ください。

# <font color="green">Unhandled ハンドラに入ってくれない！</font>

まず、以下の対話モデルのスキーマと Lambda Function で定義されるスキルを考えます。

**対話モデルのスキーマ**

```json:
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "あんはんどる",     //呼び出し名
            "intents": [
                {
                    "name": "GreetingIntent",  //intent 名
                    "slots": [],
                    "samples": [ //サンプル発話
                        "こんにちは",
                        "おはよう"
                    ]
                }
            ],
            "types": []
        }
    }
}
```

**Lambda Function**

```js:index.js
// 挨拶をしたら、挨拶を返してくれるスキル
'use strict';

const Alexa = require('alexa-sdk');

const handler = {
    'GreetingIntent' : function(){ 
        this.emit(':tell','おはようございます！');
    },
    'Unhandled': function () {
        this.emit(':tell', '何を仰ってるか分かりません');
    }
};

exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.appId = 'xxxxxxxxxxxxxxxxxx';
    alexa.registerHandlers(handler);
    alexa.execute();
};
```

「おはよう」or「こんにちは」というユーザーの発話に対して
「おはようございます！」と返してくれる、大変シンプルなスキルです。[^1]

## <font color="orange">◆思い描いていた挙動</font>

* 「おはよう」or「こんにちは」 は *<font color="green">GreetingIntent</font>* に対応する発話だから
スキルは「おはようございます！」と返してくれる  

* 上記以外の発話は *<font color="green">GreetingIntent</font>* に対応しない発話なので、 *<font color="green">Unhandled</font>* として判断されて
「何を仰ってるか分かりません」と返してくれる　← <font color="red">大きな間違い！！</font> 

## <font color="orange">◆実際の挙動</font>

* 「こんばんは」or「ありがとう」or「ほげ」or「あいうえお」等々・・・  
 ありとあらゆるユーザーの発話に対しても「おはようございます！」と返す


このような挙動に対して私は
**<font color="red">「Unhandled ハンドラに入ってくれない！」「なんでこうなるの！？」</font>** 
となったわけです。

# <font color="green">どんなときに Unhandled ハンドラに入るのだろう？</font> 
 
今回のサンプルスキルに関して言えば、*<font color="green">Unhandled</font>* ハンドラに入る方法の１つとして

```
Alexa, 「あんはんどる」を開いて
```

とユーザーが発話することが挙げられます。

この発話は [Alexa の標準リクエストタイプ](https://developer.amazon.com/ja/docs/custom-skills/request-types-reference.html#launchrequest) で定義されている *<font color="green">LaunchRequest</font>* にあたる発話で、この発話に対しては、スキルは 「何を仰ってるか分かりません」と返してくれます。

## <font color="orange">◆LaunchRequest は Unhandled ハンドラに入る？</font>

そんなことありません。

例えば、ここで、 Lambda Function の実装を以下のように変更してみましょう。

```js:index.js
// 挨拶をしたら、挨拶を返してくれるスキル
'use strict';

const Alexa = require('alexa-sdk');

const handler = {
    // LaunchRequest に対するハンドラを定義した！
    'LaunchRequest' : function(){ 
        this.emit(':ask','何か話してみてください');
    },
    'GreetingIntent' : function(){ 
        this.emit(':tell','おはようございます！');
    },
    'Unhandled': function () {
        this.emit(':tell', '何を仰ってるか分かりません');
    }
};

exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.appId = 'xxxxxxxxxxxxxxxxxx';
    alexa.registerHandlers(handler);
    alexa.execute();
};
```

実装を変更した後、再度

```
Alexa, 「あんはんどる」を開いて
```

と発話すると、スキルは「何か話してみてください」と返し、次のユーザーの発話を待ちます。

当然ですよね笑
そのように定義しましたから。

大事なのは **Lambda Function でハンドラが定義されているかどうか** です。

## <font color="orange">◆Unhandled ハンドラに入る本当の条件</font>

* ユーザーの発話から Alexa がスキルの呼び出し名を認識できる
  * Alexa がスキルを認識してくれなければ Lambda Function まで到達してくれません 
* ユーザーの発話から Alexa が *<font color="green">LaunchRequest</font>* と対応する発話だと判断して  
なおかつ    
Lambda Function 内に *<font color="green">LaunchRequest</font>* のハンドラが定義されていない
* ユーザーの発話から Alexa が特定の Intent を持つ *<font color="green">IntentRequest</font>* と対応する発話だと判断して  
なおかつ  
Lambda Function 内に対応する Intent のハンドラが定義されていない  

２個目と3個目の条件はいずれかが成り立つときと考えてください。

これで、*<font color="green">Unhandled</font>* ハンドラに入る**基本的な**条件を整理することができました。
実は、これらの条件は厳密には前提が足りていなかったりします。それは後ほど説明します。

## <font color="orange">◆Alexa は必ずいずれかの Intent に当てはめてしまう</font>

ところで、上記の条件の中に敢えて

* ユーザーの発話から Alexa が特定の Intent を持つ *<font color="green">IntentRequest</font>* と対応する発話だと判断できること

を入れていません。 

それは、**この条件は必ずクリアされるということ**を明示的に表現したかったためです。

私がこの勘違いに気付くきっかけになった alexa-skills-kit-sdk-for-nodejs の Github の [The unhandled intent is never called #43](https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/issues/43) という Issue 上で多くの方が、Alexa の intent の判断に関して、下記のようにコメントしています。


> Same here, I put in some **gibberish** and it still called one of my customized intent  

　 とか

> Same issue. I think it has nothing to do with this SDK however, as the utterance tester in the dev console consistently resolves **gibberish** test utterances to valid defined intents (somewhat randomly).

　 とか

> So from my testing 'Unhandled' is only called when some intent is received that is not handled by your handler or state handler. This IS useful, as it means that you can have a really large intent-schema with a bunch of different intents, and you don't need to have every intent covered in every state handler. The problem lies in the NLP itself, which (at least in the skills test environment online) always returns one of your intents, even if you write complete **gibberish**. This is pretty much resolvable through @pheintzelman solution above, but that is kinda sad...

詳しい内容や議論の様子は実際に Issue を見ていただいた方が良いのですが、
どうやら、Alexa はユーザーの **gibbereish(ちんぷんかんぷんな［訳の分からない］話［おしゃべり・文章］、でたらめ)** に対してもいずれかの intent を判断して Lambda Function で処理をハンドルするようなのです。

これは NLP(自然言語処理)上の問題で、スキルの実装とは直接関係のない問題で、
「スキルの設計や実装上の工夫で上手く回避する方法知ってる方いませんかー？」な状態のようです。

つまり、ユーザーがどんな無茶苦茶な発話をしても Alexa は intent schema の中から最も近い（と Alexa が判断した） intent に当てはめて判断してしまうのです。

この仕様を正しく理解していなかったため、元々の *<font color="green">Unhandled</font>* に関する勘違いも生まれてしまったのです。

# <font color="green">Unhandled ハンドラの正しい使い道</font>

この記事を書くことになった発端は *<font color="green">Unhandled</font>* ハンドラに入る条件を完全に勘違いしていたことでした。
そして、この勘違いは**その正しい使い道の勘違いにも繋がっていました。**

今や *<font color="green">Unhandled</font>* ハンドラに入る正しい条件を理解できたので、その使い道も違って見えてくるはずです。

*<font color="green">Unhandled</font>* ハンドラに入る条件をざっくり言うと、「intent schema に intent を定義したけど、ハンドラを実装しなかった場合」です。

スキルの実装がこのような状態になるのはどういったパターンが考えられるでしょうか。

### <font color="orange">パターン A : うっかり！ハンドラの実装忘れ</font>

単純にハンドラの実装漏れです。
intent schema に不要なインテントが定義されていないかを確認して、不要なものは削除していきましょう。

* *<font color="green">LaunchRequest</font>* に対するハンドラは基本的に必須ですね [^2]
* 使わないカスタムインテントやビルトインインテントは対話モデルに定義しない！

### <font color="orange">パターン B : スキルがステートを持つ場合</font> 

スキルの対話モデルの設計等をちゃんとやった上で、*<font color="green">Unhandled</font>* がちゃんと必要になるのはほとんどこのパターンだと思います。

例として、以下の対話モデルのスキーマと Lambda Function で定義されるスキルを考えます。

**対話モデルのスキーマ**

```json
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "あんはんどる",
            "intents": [
                {
                    "name": "GreetingIntent",
                    "slots": [],
                    "samples": [
                        "こんにちは",
                        "おはよう"
                    ]
                },
                {
                    "name": "GoodbyeIntent",
                    "slots": [],
                    "samples": [
                        "またね",
                        "ばいばい"
                    ]
                }
            ],
            "types": []
        }
    }
}
```

**Lambda Function**

```js:index.js
// 挨拶をしたら、挨拶を返してくれるスキル（ステートを導入したバージョン）
'use strict';

const Alexa = require('alexa-sdk');

const STATUS = {
    NEXTMODE: '_NEXTMODE'
};

// 始まりの挨拶を受け付けるステート
const startHandler = {
   'LaunchRequest' : function(){ 
        this.emit(':ask','何か話してみてください');
    },
    'GreetingIntent' : function(){
        this.handler.state = STATUS.NEXTMODE;
        this.emit(':ask','おはようございます！');
    },
    'Unhandled': function () {
        this.emit(':ask', 'まだ始まりの挨拶もしてませんよね');
    }
};

// 終わりの挨拶を受け付けるステート
const nextHandler = Alexa.CreateStateHandler(STATUS.NEXTMODE, {
    'GoodbyeIntent' : function(){ 
        this.emit(':tell','さようなら、またお話ししましょうね');
    },
    'Unhandled': function () {
        this.emit(':ask', 'おはようございます！');
    }
});

exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.appId = 'xxxxxxxxxxxxxxxxxx';
    alexa.registerHandlers(startHandler, nextHandler);
    alexa.execute();
};
```

今回のスキルでは「おはよう」or「こんにちは」みたいな始まりの挨拶を受け付けるステートと
「またね」or「ばいばい」みたいな終わりの挨拶を受け付けるステートを持っています。

実は、この例では

* 始まりの挨拶を受け付けるステートで「またね」or「ばいばい」とユーザーが発話する
* 終わりの挨拶を受け付けるステートで「おはよう」or「こんにちは」とユーザーが発話する

の２パターンの対話で *<font color="green">Unhandled</font>* ハンドラに入ってくれます。

### <font color="orange">◆Unhandled ハンドラに入る本当の本当の条件</font>

先ほど *<font color="green">Unhandled</font>* ハンドラに入る本当の条件を定義した際に
「厳密には前提が足りていない」と書きました。

ここで前提を追記します（赤字部分）。

* ユーザーの発話から Alexa がスキルの呼び出し名を認識できる
  * Alexa がスキルを認識してくれなければ Lambda Function まで到達してくれません 
* ユーザーの発話から Alexa が *<font color="green">LaunchRequest</font>* と対応する発話だと判断して  
なおかつ    
Lambda Function 内の<font color="red">現在のステートに</font> *<font color="green">LaunchRequest</font>* のハンドラが定義されていない
* ユーザーの発話から Alexa が特定の Intent を持つ *<font color="green">IntentRequest</font>* と対応する発話だと判断して  
なおかつ  
Lambda Function 内の<font color="red">現在のステートに</font>対応する Intent のハンドラが定義されていない

このように複数のステートを定義して、各ステートごとに提供してくれる機能や受け付けるユーザーの発話が区別されているようなスキルでは *<font color="green">Unhandled</font>* ハンドラはバリバリと効力を発揮するのです。

### <font color="orange">◆Unhandled ハンドラの正しい使い道の話に戻すと</font>

ここで、ようやく *<font color="green">Unhandled</font>* ハンドラの正しい使い道に話を戻します。

* ステート管理が不要なシンプルなスキルでは、不要なインテントは intent schema で定義しなくていいはず
* どんなスキルでも基本的に必要なインテントのハンドラは Lambda Function で実装すべき
* ステート管理が必要なスキルではステートによっては intent schema に定義したインテントに対応するハンドラを実装しない場合がある

これらの前提とスキルにおけるステートがユーザーと Alexa の対話における文脈を表すと考えると
*<font color="green">Unhandled</font>* ハンドラは**文脈に合わないユーザーの発話を拾う機能**
として使うのが正しいのだと思います。
これは Alexa が**「ユーザーが何を言ってるのか、どういう意図で言ってるのかは理解できるけど、今その文脈じゃないんだよなー」**と感じたというような感覚でしょうか。

そして、現状では*<font color="green">Unhandled</font>* ハンドラは**ユーザーの理解できない発話を拾う機能**
として使うのは難しいようです。
これは Alexa が**「ユーザーが何を言ってるのか、どういう意図で言ってるのかは分からない」**と感じたというような感覚でしょうか。

この勘違いが *<font color="green">Unhandled</font>* ハンドラに対する最大の勘違いでした。 

# <font color="green">おわりに</font>

*<font color="green">Unhandled</font>* ハンドラにまつわる勘違いに関して
いろいろ調べて整理してみたので、記事にして共有しようと思いました。

**<font color="green">Unhandled</font> ハンドラにまつわる新たな認識**

* *<font color="green">Unhandled</font>* ハンドラは訳の分からないユーザー発話を拾う機能じゃない
* *<font color="green">Unhandled</font>* ハンドラは文脈に合わないユーザーの発話を拾う機能
* Alexa は訳の分からないユーザーの発話に対しては必ず intent schema 内の intent の中で最も近いものを判断してしまう

*<font color="green">Unhandled</font>* ハンドラを使って、正しいエラーハンドリングや場合によっては複数のインテントのハンドラをまとめるみたいな使い方もできそうに思えてきました。

最後に、この記事が私と同様の勘違いをしていた人の助けになれば幸いです。

[^1]: 実際は「あんはんどるでこんにちは」のように、ややトリッキーな発話をしなければ挨拶を返してくれない対話モデルのスキーマになっていますが、説明の都合上ご容赦ください
[^2]: スキルが提供する機能が単一で、あらゆるリクエストに対して同一のレスポンスを返したい場合などは Unhandled ハンドラだけを実装するという手もあるなと思います