Lex 組み込みスロットタイプのAMAZON.TIMEのあいまいな時間の対処法

# はじめに
Lex 組み込みスロットタイプのAMAZON.TIMEを使用した際、
`21時`と伝えると、聞き取ってくれますが、`9時`だと、`9:00　もしくは　21:00`の2通りあるため、Lexがどっちか分からず、エラーになります。

下記のドキュメントに記載されていました。

https://docs.aws.amazon.com/ja_jp/lexv2/latest/dg/built-in-slot-time.html


ただし、`時間の発話の範囲が、9:00〜19:00の場合`、`9時`と答えると、`21時`ではないため、9時と認識してほしいものです。
その方法をまとめます。

# 事前構築

下記の記事を参考にしました。

https://qiita.com/holdout0521/items/58f524be7cc5d9ca2093

Lexは、下記の通りにしました。

![スクリーンショット 2022-02-28 17.29.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/9615cea7-07f2-4aca-0061-a3bc4d776181.png)



# 実際の発話
### 21時と答えた場合
![スクリーンショット 2022-02-28 17.32.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/1c59eebe-ce1a-8649-541b-50bc40c485ea.png)

LambdaのCloudWatchLogsからログを確認してみると、
`interpretedValue（解釈した値）`は、`21:00`となっていました。

```json
{
    "what_time": {
        "shape": "Scalar",
        "value": {
            "originalValue": "21",
            "resolvedValues": [
                "21:00"
            ],
            "interpretedValue": "21:00"
        }
    }
}
```

- `interpretedValue`
  >Amazon Lex V2 がスロットについて決定する値。実際の値は、ボットの値を選択する設定によって異なります。
ユーザーが入力した値を使用できるか、Amazon Lex V2 に resolvedValues のリスト中の値を選ばせるかが行えます。

- `originalValue`
  >スロットに対して入力されたユーザーからの発話のテキスト。
- `resolvedValues`
  >スロットで認識された追加の値のリスト。


https://docs.aws.amazon.com/ja_jp/lexv2/latest/dg/API_runtime_Value.html


### 9時と答えた場合

9時と答えるとエラーになりました。

![スクリーンショット 2022-02-28 17.33.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/bf7deab7-2888-a748-f05e-39eef446087b.png)


LambdaのCloudWatchLogsからログを確認してみると、
`resolvedValues`内に、`"09:00","21:00"`の2つがあることがわかります。


```json
{
    "what_time": {
        "shape": "Scalar",
        "value": {
            "originalValue": "9",
            "resolvedValues": [
                "09:00",
                "21:00"
            ]
        }
    }
}
```
`interpretedValue（解釈した値）`は、ないことも分かりました。

# 対処方法


LexのLambdaは、以下のようにしました

```py:Lambda
import json
from decimal import Decimal

# json形式でログを出力するため、Decimalがある場合取り除く
def decimal_to_int(obj):
    if isinstance(obj, Decimal):
        return int(obj)

def elicit_slot(slot_to_elicit, intent_name, slots):
    return {
        'sessionState':{
            'dialogAction': {
                'type': 'ElicitSlot',
                'slotToElicit': slot_to_elicit,
            },
            'intent':{
                'name': intent_name,
                'slots': slots,
                'state': 'InProgress'
            }
        }
    }

def validation_slot(slot_to_elicit, message_content, intent_name, slots):
    return {
        'messages': [{'contentType': 'PlainText', 'content': message_content}],
        'sessionState':{
            'dialogAction': {
                'type': 'ElicitSlot',
                'slotToElicit': slot_to_elicit,
            },
            'intent':{
                'name': intent_name,
                'slots': slots,
            }
        }
    }

def confirm_intent(message_content, intent_name, slots):
    return {
        'messages': [{'contentType': 'PlainText', 'content': message_content}],
        'sessionState':{
            'dialogAction': {
                'type': 'ConfirmIntent',
            },
            'intent':{
                'name': intent_name,
                'slots': slots,
                'state': 'Fulfilled'
            }
        }
    }
def input_slots_value(value):
    return {
        "shape": "Scalar",
        "value": {
            "originalValue": value,
            "resolvedValues": [value],
            "interpretedValue": value
        }
    }

def close(fulfillment_state, message_content, intent_name, slots):
    return {
        'messages': [{'contentType': 'PlainText', 'content': message_content}],
        "sessionState": {
            'dialogAction': {
                'type': 'Close',
            },
            'intent':{
                'name': intent_name,
                'slots': slots,
                'state': fulfillment_state
            }
        }
    }

# "time"インテント
def time_reserve(intent_request):
    print("Received event:" + json.dumps(intent_request, default=decimal_to_int, ensure_ascii=False))

    intent_name = intent_request['sessionState']['intent']['name']
    slots = intent_request['sessionState']['intent']['slots']

    print('Received intent_name:' +  json.dumps(intent_name, default=decimal_to_int, ensure_ascii=False))
    print('Received slots:' +  json.dumps(slots, default=decimal_to_int, ensure_ascii=False))

    if slots['what_time'] is None:
        return elicit_slot('what_time',intent_name,slots)

    what_time_resolved = slots['what_time']['value']['resolvedValues']

    counts = len(what_time_resolved)

    # あいまいな時間を言った場合
    for i in range(counts):
        if "09:00" <= what_time_resolved[i] <= "19:00":
            what_time = what_time_resolved[i]
            slots['what_time']['value']['interpretedValue'] = what_time
            break

    try:
        if "09:00" <= slots['what_time']['value']['interpretedValue'] <= "19:00":
            what_time = slots['what_time']['value']['interpretedValue']
        # 20時の場合
        else:
            return validation_slot('what_time',f"{what_time}ですか？時間は、9:00から19:00時の間でお伝え下さい。",intent_name,slots)

    # ８時の場合
    except KeyError:
        return validation_slot('what_time',f"{what_time_resolved[0]}ですか？時間は、9:00から19:00時の間でお伝え下さい。",intent_name,slots)

    confirmation_status = intent_request['sessionState']['intent']['confirmationState']
    print(confirmation_status)

    if confirmation_status == "Confirmed":

        return close( "Fulfilled", '承知しました。', intent_name, slots)

    elif confirmation_status == "Denied":

        return close( "Failed", 'キャンセルしました。', intent_name, slots)

    elif confirmation_status == "None":

        return confirm_intent( 
            f"{what_time}ですね？よろしければ、はいと、そうでない場合、いいえとお伝え下さい。",
            intent_name, slots)

# インテントのルーティング
def dispatch(intent_request):
    # 他にインテントを使用する場合
    intent_name = intent_request['sessionState']['intent']['name']

    # "book"インテント
    if intent_name == 'book':
        return book_reserve(intent_request)

    # "time"インテント
    if intent_name == 'time':

        return time_reserve(intent_request)

def lambda_handler(event, context):

    return dispatch(event)
```

注意点ですが、時間を判定する際、時間は、`"9:00"`ではなく、`"09:00"`と0が必要です。


```py
if what_time < "9:00" :  # ✕
```

```py
if what_time < "09:00" :  # ◯
```

# テスト

### 9時と答えた場合

9時と答えた場合、`21:00`ではなく、`9:00`と判断されました。

![スクリーンショット 2022-03-14 21.33.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/6cc7d7a4-67ce-2761-c61c-fe073f538f62.png)

### 8時と答えた場合

`9:00〜19:00`を答えた場合、拒否されます。

![スクリーンショット 2022-03-14 21.34.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/b9a1ee65-6dc6-702d-49b2-9fb3ccb598aa.png)

# 別の方法

今回時間範囲が`9:00〜19:00`でしたが、時間範囲がない場合の対処法は、時間の前に`午前・午後`をつけると聞き取ってくれます。

![スクリーンショット 2022-02-28 18.27.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/988747/e86d2766-e04f-a8c5-29fe-3e8e6c64a92f.png)



# AMAZON.TIMEの発話リスト
他にも以下の伝え方だと、聞き取ってくれます。

|発話  |判定|
|---|---|
| 3時 | ✕（聞き取れない） |
| 15時 | 15:00 |
| 15時半 | 15:30 |
| 午後3時 | 15:00 |
| 午前12時 | 00:00 |
| 午後0時 |  00:00|
| 午後12時 | 12:00 |
| 午後0時 | 12:00 |
| 今 | 現在の時刻 |