Chapter 23

(番外編) iOSアプリにチャットボットを組み込む

maKunugi
maKunugi
2022.02.04に更新

このチャプターのゴール

  • iOSアプリにmeboで作成したエージェントをチャットボットとして組み込むことができる
  • iOSアプリと連動させてチャットボットを利用することができる

iOSアプリに組み込む際の概要

iOSアプリに組み込む際はWKWebViewを利用します。WKWebViewでmeboのチャット画面を読み込むだけで、簡単にアプリへチャットボットを導入できます。WKWebViewで表示されるチャット画面はアプリのソースコードと連携ができます。

導入手順

チャットボットを組み込むサンプルアプリを用意しましょう。アプリが準備ができたら、下記の手順でmeboで作成したエージェントのチャットボットを組み込んでいきます。

1. mebo用のWKWebViewを作成する

class ViewController: UIViewController, WKScriptMessageHandler {
    private var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let config = WKWebViewConfiguration()
        let userContentController: WKUserContentController = WKUserContentController()
        userContentController.add(self, name: "meboCallBack")
        config.userContentController = userContentController
        webView = WKWebView(frame: .zero, configuration: config)
        webView.load(URLRequest(url: URL(string: "チャットページURL")!))
        view = webView
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
	    // TODO: Impl
    }
}

webサイトを表示する基本的なWKWebViewの利用方法と大きく違いはありません。
WKWebViewを作る際、チャット画面とアプリのソースコードを連携させるため、WKUserContentControllerを仕込みます。WKUserContentControllerには、「meboCallBack」を追加します。

userContentController.add(self, name: "meboCallBack")

読み込むチャットページのURLは、meboのエージェントの「公開設定」ページで取得できるURLを利用します。

エージェントを一般公開したら、公開設定ページからチャット画面のURLをコピーしましょう。
尚、WKWebViewで利用する場合は、コピーしたURLの最後に下記のクエリーパラメータを追加する必要があります。

<チャットページのURL>&platform=webview

この状態でアプリをビルドし起動すると、アプリ内にチャット画面が表示されるはずです。

この時点でアプリを起動すると、下記のようにエラーが表示されますが、現時点ではこれでOKです。

APIキーを取得する

アプリとmeboのチャット画面を連携させるには、APIキーが必要です。エージェントの公開設定画面でAPI利用を有効化し、APIキーをコピーしましょう。

APIキーをコピーしたら、こちらをソースコード内で利用していきます。

    private let apiKey = "<取得したAPIキー>"

上記のようにapiKeyを利用できるように変数を宣言しておきましょう。

ユーザ識別子を用意する

アプリ内でユーザが会話を行う際は、ユーザを一意に特定する識別子が必要です。作成するアプリにログイン機能等が備わっている場合は、ログイン後のユーザID等を利用すると良いです。今回は、サンプルのためユニークなIDをコード上で生成します。

private let uid = UUID().uuidString

APIキーとユーザ識別子をチャット画面にセットする

上記で用意したAPIキーとユーザ識別子をチャット画面に渡してあげる必要があります。先ほど用意したuserContentControllerメソッドを利用してチャット画面と連携をしながら、セットを行います。

1. チャット画面でエージェントが読み込まれたことを検知する

APIキーとユーザ識別子をセットできるタイミングは、チャット画面でエージェントが読み込まれた後です。そのため、エージェントが読み込まれたことを検知する必要があります。userContentControllerメソッドには、チャット画面からイベントが送信されてくるので、そのイベントをハンドリングします。

イベントの種類

A. エージェントの読み込み完了 (agentLoaded)

{"event":"agentLoaded"}

B. エージェントが応答を返した (agentResponded)

{
    "event": "agentResponded",
    "bestResponse": {
        "utterance": "差し支えなければお名前を教えてください。",
        "options": [
            "匿名",
            "ジョナサン",
            "太郎",
            "ジョン"
        ],
        "is_enabled_free_input": true
    },
    "extensions": null
}

C. エラーが発生した

{
    "error": {
        "code": 400,
        "message": "不適切なリクエストが送信されました。"
    }
}

イベントはJSONで送られてくるため、Codableを適用したStructを用意しましょう。

struct ChatEvent: Codable {
    let event: String
    let bestResponse: BestResponse?
}

struct BestResponse: Codable {
    let utterance: String
    let options: [String]
    let extensions: Extensions?
}

struct Extensions: Codable {
    // 任意の構造体
}

struct ChatError: Codable {
    let error: ChatErrorResponse
}

struct ChatErrorResponse: Codable {
    let code: Int
    let message: String
}

BestResponseとExtensionsに関しては、後ほど利用する際に説明します。エラー用のStructも用意しておきましょう。

エージェントが読み込まれたタイミングを取得する
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard let json = (message.body as? String)?.data(using: .utf8) else {
            return
        }
        let decoder = JSONDecoder()
        guard let chatEvent: ChatEvent = try? decoder.decode(ChatEvent.self, from: json) else {
            if let error: ChatError = try? decoder.decode(ChatError.self, from: json) {
                print(error.error.message)
                return
            }
            fatalError("不明なエラー")
        }
        
        switch chatEvent.event {
        case "agentLoaded":
		// TODO: Impl
        case "agentResponded":
		// TODO: Impl
        default:
            break
        }
    }

上記のコードのように、JSONを用意したStruct(ChatEvent)に変換したら、switchでイベントごとの処理を記述できます。今回はエージェントが読み込まれたタイミングで処理を行いたいので、「agentLoaded」に処理を追加していきましょう。

2. APIキーとユーザ識別子をチャット画面に送信する

        case "agentLoaded":
            webView.evaluateJavaScript("setAppInfo(\"\(apiKey)\",\"\(uid)\");")

caseの中では、WKWebViewのevaluateJavaScriptメソッドを利用して、チャット画面内のjavascriptを呼び出します。setAppInfoメソッドにAPIキーとユーザ識別子を渡してあげましょう。

ここまでの流れで、アプリ内でエージェントと会話ができる状態になります。

エージェントの応答をハンドリングする

ただWKWebView内でエージェントと会話できるだけで良ければ、上記の方法で事足ります。しかし、エージェントの応答に応じて、アプリ側で何かしらの処理を行いたい時は、エージェントの応答をハンドリングしてあげる必要があります。その場合は、先ほど登場したChatEventでagentRespondedのイベントを検知してあげます。

        switch chatEvent.event {
        case "agentLoaded":
            webView.evaluateJavaScript("setAppInfo(\"\(apiKey)\",\"\(uid)\");")
        case "agentResponded":
		// TODO: Impl
        default:
            break
        }

agentRespondedは、エージェントがユーザに対して応答を返す際に送信されてきます。
JSONの内容をもう一度おさらいしましょう。

{
    "event": "agentResponded",
    "bestResponse": {
        "utterance": "差し支えなければお名前を教えてください。",
        "options": [
            "匿名",
            "ジョナサン",
            "太郎",
            "ジョン"
        ],
        "is_enabled_free_input": true,
	"extensions": null
    },
}

bestResponseには、エージェントの応答が格納されます。

utterance: エージェントの応答文
options: クイックリプライ
is_enabled_free_input: 自由入力を許可しているか
extensions: エージェント作成者が独自に定義する拡張データ (JSON)

このJSONをアプリ側で検知し、自由な処理をすることができます。

        case "agentResponded":
	    // エージェントの応答を出力する
            print(chatEvent.bestResponse!.utterance)

アプリ内の処理のためのパラメータを受け取る

これまでのチャプターで説明した通り、meboではエージェントの応答に「拡張データ(JSON)」を紐づけることができます。この拡張データを利用することで、アプリ内の処理を行うためのパラメータをエージェントの応答に持たせることができます。

利用用途としては、

  • 特定の発言があったらWebViewを閉じて違う画面に遷移する
  • 特定の発言があったらアプリ内の機能を発動させる
  • 特定の発言があったらアプリ内で何らかの情報をユーザに表示する

といったことが実現できます。

作成したエージェントの応答に何らかの拡張データをJSONで追加してみましょう。

拡張データを追加したら、それに対応するStructを用意してあげましょう。上記のJSONであれば下記のようなStructになります。

struct Extensions: Codable {
    let appAction: String?
}

この状態で、アプリのチャット画面でエージェントに話しかけてみましょう。エージェントに拡張データがセットされた応答が返されるように話しかけると、agentRespondedイベントが送られるタイミングでExtensionsの内容を取得できます。

        case "agentResponded":
	    // 拡張データを確認する
            print(chatEvent.bestResponse?.extensions?.appAction)

前のチャプターでも説明してきた通り、meboではユーザステートというユーザが回答した情報を保持しておく仕組みがあります。( 詳しくはこちら )

拡張データには、ユーザステートの情報も代入できるため、エージェントが会話で得た情報を、拡張データを通じてアプリに渡すことができます。

#{username}のようにステート名を#{}で囲い、JSONに含めます。

struct Extensions: Codable {
    let username: String?
}

このようなStructを作ってあげることで、エージェントがユーザから聞いた名前をアプリに渡すことができます。
拡張データを活用することで、チャット画面とアプリを連動させて何らかの処理を行うことが可能です。

アプリからエージェントに話しかける

最後に、アプリ側のソースコードからエージェントに話しかける方法をご紹介します。基本的にユーザはWKWebView内から文字入力を行い、エージェントに話しかけます。それに加え、アプリ側のソースコードから動的にエージェントへメッセージを送ることができます。この場合は、APIキーセット時と同様に、WKWebViewのevaluateJavaScriptメソッドを用います。

webView.evaluateJavaScript("sendMessage(\"こんにちは。\");")

evaluateJavaScriptメソッドでsendMessageというメソッドを呼び出すことで、メッセージの送信ができます。動的に任意のメッセージを送信させたい場合等にご活用ください。

まとめ

いかがでしたでしょうか。iOSアプリにチャットボットを導入する際、UIから作っているとかなりの手間がかかってしまいます。本機能でない限り、あまり労力をかけたくないケースが多いと思いますので、そういった場合はぜひ上記の方法をご検討ください。