【Amazon Lex】Lambdaをフックして国内電話番号を正しく判定するチャットボットを構築してみた！

## はじめに

Amazon Lexを使用して、ユーザーの電話番号を聞き取るチャットボットを構築する際には、組み込みスロットタイプ「[AMAZON.PhoneNumber](https://docs.aws.amazon.com/lexv2/latest/dg/built-in-slot-phone.html)」を利用できます。
しかし、このスロットタイプは、「1234-5678」などの**市外局番を除いた番号**や、「123」などの**短い数字列まで電話番号として解釈してしまう**ため、そのままでは意図しない入力を許してしまうことがあります。

本記事ではチャットボットにLambda関数をフックすることで、国内の携帯電話（070/080/090など※)や固定電話（03/06など）のみを受け付けるチャットボットを構築します。

※[携帯番号への060番号の追加](https://www.soumu.go.jp/main_sosiki/joho_tsusin/top/tel_number/060keitai.html)については対応ができていません。


:::note info
本記事の情報は2025年3月時点のものです。
最新情報については公式ドキュメントをご確認ください。
:::

## 目次

[1.動作環境](#1-動作環境)
[2.前提条件](#2-前提条件)
[3.Lambda Layerの準備](#3-lambda-layerの準備)
[4.Lambda関数の作成](#4-lambda関数の作成)
[5.Lexチャットボットの作成](#5-lexチャットボットの作成)
[6.Lambda関数のフック前後での動作比較](#6-lambda関数のフック前後での動作比較)
[まとめ](#まとめ)

## 1. 動作環境 

本記事で使用する環境は以下のとおりです。
- AWS リージョン：東京
- Amazon Lex：Lex V2
- AWS Lambda：Node.js 20.x
- google-libphonenumber (電話番号の解析ライブラリ)
- npmを利用

## 2. 前提条件
本記事では以下を前提とします。
- AWSアカウントが作成されていること
- AWS Lambda 関数の作成権限やAmazon Lexのボット作成権限、またそれに係る権限を持つユーザーで操作を行うこと



## 3. Lambda Layerの準備
AWS Lambdaで[google-libphonenumber](https://github.com/ruimarinho/google-libphonenumber)を使用するために、Lambda Layerを準備します。
[google-libphonenumber](https://github.com/ruimarinho/google-libphonenumber)はGoogleが提供している[libphonenumber](https://github.com/google/libphonenumber)をNode.js向けにラップしたnpmパッケージで、以下の機能を提供しています。
- 電話番号の解析（国コードの判定など）
- 電話番号のフォーマット（国際形式、国内形式など）
- 電話番号のバリデーション（正しい番号かどうかの確認）

https://www.npmjs.com/package/libphonenumber-js

### 3.1 必要なパッケージのインストール

以下のコマンドより、作成したnodejsフォルダに`google-libphonenumber`をインストールします。

```:ターミナル
$ mkdir nodejs
$ cd nodejs
$ mkdir node_modules
$ cd node_modules 
npm install google-libphonenumber

```

- 上記操作後、以下のようなディレクトリ構造になっていることを確認

```
nodejs
├── node_modules
│   └── google-libphonenumber
│       ├── LICENSE
│       ├── README.md
│       ├── dist
│       │   └── libphonenumber.js
│       └── package.json
├── package-lock.json
└── package.json
```

### 3.2 パッケージのzip化

- 用意した`nodejs`フォルダをzip化します。(任意のzip名でzip化してください。)
    - 今回は`test_custom_layer`としています。
```ターミナル

$ zip -r test_custom_layer nodejs

```
### 3.3 Lambda Layerの作成
AWS Lambdaコンソール画面の「レイヤー」より、以下の設定でLambda Layerを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/7f8ffedc-92cb-4abf-96d2-372d5f19bc73.png)
- 名前：**test_custom_layer**
- .zipファイルをアップロード： [パッケージのzip化](#パッケージのzip化)で作成したzipファイルを選択
- 互換性のあるアーキテクチャ：**x86_64**
- 互換性のあるランタイム：**Node.js 20.x**

## 4. Lambda関数の作成
AWS Lambdaコンソール画面の「関数の作成」より、以下の設定でLambda関数を作成します。

https://docs.aws.amazon.com/ja_jp/lexv2/latest/dg/lambda-attach.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/7c690557-5f94-4b41-b0db-355170e73bbe.png)

- オプション：**一から作成**
- 関数名：**test_lex_phone_number_validation**
- ランタイム：**Node.js 20.x**
- アーキテクチャ：**x86_64**
- アクセス権限 , その他の構成：デフォルト

### 4.1 Lambda関数 コード
作成した**test_lex_phone_number_validation**の`index.mjs`を以下のコードで上書きします。
このコードには、Lambdaコードフック元のLexチャットボットに入力された値が、**日本国内の電話番号**かを判定する処理が含まれています。(詳細はコメントアウトをご参照ください)

```javascript
import libphonenumber from 'google-libphonenumber';

export const handler = async (event) => {

  // google-libphonenumber のユーティリティを取得
  const phoneNumberUtil = libphonenumber.PhoneNumberUtil.getInstance();
  const phoneNumberFormat = libphonenumber.PhoneNumberFormat;

  // Amazon Lex から受け取ったイベントオブジェクトからセッション情報を取得
  const sessionState = event.sessionState || {}; // セッション情報を取得（初回呼び出し時は空オブジェクト）
  const intentInfo = sessionState.intent || {}; // インテント情報を取得
  const slots = intentInfo.slots || {}; // インテント内のスロット情報を取得

  // スロット 'PhoneNumber' の値を取得（ユーザーが入力した電話番号）
  let phoneNumber = "";
  if (slots.PhoneNumber?.value?.interpretedValue) {
    phoneNumber = slots.PhoneNumber.value.interpretedValue;
  }

  // 初期状態のバリデーションフラグとメッセージを設定
  let validationFlag = false; // バリデーション結果（成功なら true）
  let validationMessage = ""; // ユーザーへ返すメッセージ

  // 電話番号が入力されている場合のみバリデーションを実施
  if (phoneNumber) {

      // 入力された電話番号を "JP"（日本）として解析
      const parsedPhone = phoneNumberUtil.parseAndKeepRawInput(phoneNumber, 'JP');

      // 解析した電話番号から国コードを取得（例: "JP"）
      const regionCode = phoneNumberUtil.getRegionCodeForNumber(parsedPhone);

      // 取得した国コードが "JP"（日本）で、かつ有効な電話番号かどうかを確認
      if (regionCode === "JP" && phoneNumberUtil.isValidNumberForRegion(parsedPhone, "JP")) {
        validationFlag = true; // バリデーション成功
        validationMessage = "電話番号が正しく入力されました";

        // スロットの値をフォーマット済みの電話番号に更新
        slots.PhoneNumber = {
          value: {
            interpretedValue: phoneNumber, // もともとの入力値を格納
            originalValue: phoneNumber // 変更なしで格納
          }
        };
      } else {
        // 日本国内の有効な電話番号でない場合の処理
        validationMessage = "日本国内の電話番号のみ入力してください。再度入力をお願いします。";
        delete slots.PhoneNumber; // 不正な値を削除（スロットクリア）
      }
  }

  // Amazon Lex へのレスポンスを作成
  const response = {
    sessionState: {
      // バリデーション結果に応じて対話アクションを決定
      dialogAction: validationFlag
        ? { type: "Close", fulfillmentState: "Fulfilled" } // 成功時は会話を終了
        : { type: "ElicitSlot", slotToElicit: "PhoneNumber" }, // 失敗時はスロットの再入力を要求
      intent: {
        name: intentInfo.name || "", // インテント名を設定
        slots: slots, // スロット情報を更新
        state: validationFlag ? "Fulfilled" : "InProgress" // 成功時は完了、失敗時は進行中
      },
    },
    // ユーザーへの応答メッセージ
    messages: [{
      contentType: "PlainText",
      content: validationMessage
    }]
  };

  return response;
};
```

### 4.2 Lambda Layerの追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/00fa04e0-f582-48c5-ad5c-0e1e905a6f3e.png)
Lambda 関数の設定画面から「**レイヤーの追加**」をクリックし、以下の設定でLambda LayerをLambda関数に追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/7396d1df-e36d-4876-8f3c-92cb0cf43c0c.png)
- レイヤーを選択：**カスタムレイヤー**
- カスタムレイヤー：**test_custom_layer**
- バージョン：**1**


## 5. Lexチャットボットの作成
Amazon Lex チャットボットを作成し、電話番号の取得とバリデーションを行うための設定を行います。

https://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/connect-conversational-ai-bots.html

### 5.1 ボットの設定

Amazon Lexコンソール画面の「**ボットを作成**」より、以下の設定でチャットボットを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/e47ec4b5-5a8d-4ba5-ac87-e904aff9905a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/2c9929d1-de29-4a84-8aba-9710b8ec8a8d.png)

- 作成方法：**空のボットを作成します。**
- ボット名：**test_phone_number_bot**
- IAMアクセス許可：**基本的な Amazon Lex 権限を持つロールを作成します。**
- 児童オンラインプライバシー保護法 (COPPA)：**いいえ**
- アーキテクチャ：**x86_64**
- アイドルセッションタイムアウト：**5分**(デフォルト)

また、言語設定は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/f4ab0088-528f-4520-99c7-9f1028b04875.png)

- 言語を選択：**Japanese(JP)**
- 音声による対話：**Kazuha**(デフォルト)
- インテント分類信頼スコアのしきい値：**0.40**(デフォルト)

### 5.2 スロットの追加
Amazon Lex ボット構築画面の「**スロット**」より、「**スロットを追加**」をクリックし、以下の設定でスロットを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/c1ee5e73-9a68-4716-a0db-03bcfe5b48ed.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/b10c27ac-78d7-4f56-ab58-1d1716c58276.png)

- 名前：**PhoneNumber**
- スロットタイプ：**AMAZON.PhoneNumber**
- プロンプト：**任意の文字列**

::: note warn
今回は、LexボットにLambda関数をフックし処理を委任するため、スロット内のプロンプトは再生されません。
:::




### 5.3 サンプル発話の追加

[スロットの追加](#スロットの追加)で追加したスロット名を基にサンプル発話を追加します。
スロット名を中括弧`{}`で囲むことで、インテントに定義したスロットをインテントのトリガーとして使用することができます。

https://docs.aws.amazon.com/ja_jp/lexv2/latest/dg/sample-utterances.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/076a3c37-5fcf-46ed-be05-bb4912fc5ccd.png)

### 5.4 コードフックの有効化
**コードフック-オプション**より、**「初期化と検証に Lambda 関数を使用」** をクリックし、有効化する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/743cb153-7966-4079-a388-2c07f1d22311.png)

### 5.5 コードフック用のLamnbda関数の設定
**test_phone_number_bot** > **エイリアス** >**エイリアス名(TestBotAlias)** >**言語**をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/987a6734-6884-4164-af7f-950effb9ab9b.png)

[Lambda関数の作成](#lambda関数の作成)で作成したLambda関数を選択して「**保存**」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/90d8e7bb-87f3-41f0-a6c6-209f61f1a535.png)

以上で準備完了です！

## 6. Lambda関数のフック前後での動作比較

Lexコンソールのテスト画面からメッセージを入力し、送信した際の動作を比較します。

### 6.1 Lambda関数をフックしていないLexチャットボットの場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/a72841ea-b012-47c4-b7c3-499b1a73776d.png)

- 実行結果
    - 国内の電話番号（070/080/090 などの携帯番号や 03/06 などの固定電話）を入力
    → 入力値を受け付け、チャットボットの処理を終了する。
    - **市外局番を除いた電話番号や、不適切な入力(短い数字列など)を入力**
    → **入力値を受け付け、チャットボットの処理を終了する。**

### 6.2 Lambda関数をフックしている Lex チャットボットでの実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843906/81f38574-b1b2-4c4a-a4a3-2613dbc347f1.png)

- 実行結果
    - 国内の電話番号（070/080/090 などの携帯番号や 03/06 などの固定電話）を入力
    → 入力値を受け付け、チャットボットの処理を終了する。
    - **市外局番を除いた電話番号や、不適切な入力(短い数字列など)を入力**
    → **「日本国内の電話番号のみ入力してください」などのエラーメッセージを返し、再入力を促す。**

### その他の入力値での比較

| 入力値               | Lambda フックなしの動作                         | Lambda フックありの動作                                      |
|----------------------|---------------------------------|--------------------------------------------------|
| 090-1234-5678         | 受け付けて処理を終了(09012345678)           | 受け付けて処理を終了 (09012345678) |
| 03-1234-5678        | 受け付けて処理を終了(0312345678)           | 受け付けて処理を終了(0312345678)                         |
| 12345678901234567890           | 受け付けて処理を終了(12345678901234567890)       | **エラー：「日本国内の電話番号のみ入力してください」** |
| +1 310-555-6789     | 受け付けて処理を終了(+13105556789)   | **エラー：「日本国内の電話番号のみ入力してください」** |
| +81 90-1234-5678    | 受け付けて処理を終了(+819012345678)          | 受け付けて処理を終了 |


## まとめ

本記事ではチャットボットにLambdaをフックし、国内の携帯電話（070/080/090など）や固定電話（03/06 など）のみを受け付けるチャットボットの構築方法を紹介しました。
[AMAZON.PhoneNumber](https://docs.aws.amazon.com/lexv2/latest/dg/built-in-slot-phone.html)のみ設定した場合と比較して、Lambdaをフックすることでより正確なバリデーションが可能になり、不適切な入力を防ぐことができます。
また、今回利用した[google-libphonenumber](https://github.com/ruimarinho/google-libphonenumber)は、国コードの判定の他、国内形式・国際形式・E.164形式などの電話番号のフォーマットも行える便利なパッケージです。
そのため、コードフック用のLambda関数をカスタマイズすることで、より柔軟なチャットボットの作成が可能です。

次回の記事では、Lambdaコードフックによる、より柔軟なチャットボットの開発方法を紹介します。