LaravelでWatson Assistantを使おう

「もっと簡単に」呼び出せるようにパッケージ化しました。
詳細は[こちら](https://qiita.com/motuo/items/27b7f50fba64c1dd149f)

## Watson Assisantとは
会話系のインターフェースを提供してくれるIBMのサービスです。

## とりあえずデモ
こんな感じで動かしていきます。
![demo.gif](https://qiita-image-store.s3.amazonaws.com/0/154110/a7667d30-54eb-bafb-826d-3335938aad7e.gif)

※デモ内ではWatson AssistantとIBM Cloud Function(ICF)を連携させて、天気予報を取得しています。
  Watson AssistantとICFの連携については別記事で書こうと思います。

### システム概要
![概要.png](https://qiita-image-store.s3.amazonaws.com/0/154110/7a730b17-461f-b01c-36e5-4049453fcf3f.png)


## Watson AssistantをWebアプリケーションでどうやって使うか。
WatsonのサービスはREST APIで簡単に利用することが出来ます。

[以前の記事](https://qiita.com/motuo/items/5d22512fd4d0be110508)では`Node.js`でWatson Assistantとの連携をしましたが、
今回は`Laravel`と`Guzzle`を使ってWebアプリケーションとWatson Assistantを連携する方法について
書きたいと思います。

### Watson AssistantのCall処理

```php
<?php
namespace App\Services;

use GuzzleHttp\Client;

class CallWatsonAssistant{
    /**
     * Watson Assistantを呼び出すモジュール
     *
     * @param string $spokenWord ユーザーが入力した文字列
     * @param array $context watson assistantのcontextデータ
     * @return json Watson AssistantをCallした結果
     */
    public function call(string $spokenWord,array $context)
    {
        if(count($context)>0){
             //以前から継続されている会話の場合
            $requestData  = json_encode(['input'=>['text'=>$spokenWord],'context'=>$context]);
        }else{
            $requestData  = json_encode(['input'=>['text'=>$spokenWord]]);
        }
        $headers = ['Content-Type' => 'application/json','Content-Length' => strlen($requestData)];
        $curlOpts = [
            CURLOPT_USERPWD        => config('watson.user_name').':'.config('watson.password'),
            CURLOPT_POSTFIELDS     => $requestData,
        ];
        $guzzleClient = new Client(['base_uri'=>'https://gateway-fra.watsonplatform.net/assistant/api/v1/workspaces/']);
        $path         = config('watson.workspace_id') . '/message?version=2018-07-10';
        return $guzzleClient->request('POST',$path,['headers'=> $headers,'curl'=>$curlOpts])->getBody()->getContents();
    }
}
```
Guzzleを使う事で、curlを使うよりも簡潔にWatson Assistantを呼び出すことが出来ます。

#### envファイルに定義を追加する
セキュリティの観点より、APIをCallする為のユーザー名やパスワード、workspace_idについては、
envファイルに書くと良いと思います。

```
WATSON_WORKSPACEID=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
WATSON_USER_NAME=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
WATSON_PASSWORD=XXXXXXXXXXXX
```

#### configファイル(watson.php)を作成する。
私は`config`フォルダ直下に`watson.php`というファイルを作っています。
これにより、呼び出すときの処理で`config('watson.workspace_id')`という書き方が出来るようになります。

```php
<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Default Workspace ID
    |--------------------------------------------------------------------------
    | IBM CloudでのWORKSPACE IDを指定します。
    */
    'workspace_id' => env('WATSON_WORKSPACEID'),
    /*
    |--------------------------------------------------------------------------
    | Default UserName/Password
    |--------------------------------------------------------------------------
    | IBM Cloudで取得したサービス資格情報を指定します。
    */
    'user_name'   => env('WATSON_USER_NAME'),
    'password'    => env('WATSON_PASSWORD')];
```

### Controllerに記述する
上記で作成したWatson Assistant呼び出し処理を`インジェクション`して使っています。
Watson Assistantでは、ユーザーと連続した会話を成立させるために、APIのパラメータに
`context`を持たせる必要があるので、これは`session`に格納しています。

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\CallWatsonAssistant;

class WatsonCallController extends Controller
{
    /**
     * セッションを初期化し、初期画面を表示する。

     * @param Request $request
     * @return response 入力初期画面
     */
    public function index(Request $request)
    {
        $request->session()->flush();
        return view("talkWatson");
    }
    /**
     * WatsonAssistantを呼び出してjson形式で返却する。
     * 継続した会話を実現する為、contextはセッションに保管しておく
     *
     * @param Request リクエストデータ
     * @param CallWatsonAssistant WatsonAssistant呼び出しモジュール
     * @return json Watson Assistantからの受信データ
     */
    public function talkToWatson(Request $request,CallWatsonAssistant $CWA)
    {   
        $response      = $CWA->call($request->spokenword,session('context')?session('context'):[]);
        $responseArray = json_decode($response,true);   
        $request->session()->put('context',$responseArray['context']);
        return $response;
    }
}
```

### ルーティングを定義する
#### web.php

```php
Route::get('/', 'WatsonCallController@index')->name('index');
```
#### api.php

```php
Route::post('talk_to_watson','WatsonCallController@talkToWatson')->name('talk_to_watson');
```

上記の処理ではWeb画面の表示とAPIの定義を同じControllerに書いていますが、
本来は分けた方が良いと思います。

#### サンプルView

```php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-6">
            <input type="text" id="message_to_watson" placeholder="何か入れて下さい" class="form-control"/>
        </div>
        <div class="col-md-2">
            <input type="button" id="send_message" value="聞いてみる" class="btn btn-primary"/>        
        </div>
    </div>
    <div class="row">
        <div class="col-md-6">
            <label>ユーザー入力</label>
            <ul id="input"></ul>
        </div>
        <div class="col-md-6">
            <label>Watsonからの返答</label>
            <ul id="responseFromWatson"></ul>
        </div>    
    </div>
</div>
@endsection

@section('script')
<script>
    $("#send_message").click(function(){
        $('#input').append('<li>'+$("#message_to_watson").val()+'</li>')
        $('#responseFromWatson').append('<li>'+'Watson is thinking...'+'</li>')
        $.ajax({
            type: "POST",
            url:  "{{route('talk_to_watson')}}",
            data: {spokenword:$("#message_to_watson").val()},
            dataType: 'json'
        }).done(function(response){
            for (var i = 0, len = response.output.text.length; i < len; i++) {
                if(response.output.text[i] !== ''){ 
                    $('#responseFromWatson li:last-child').text(response.output.text[i]);
                }
            }
        }).fail(function(){
            alert(errorHandler(arguments));
        });
    });
</script>
@endsection
```

これで、上記のデモ画面まで表示する事が出来ると思います。
こちらの方法でも簡単にChat bot等の実装が出来そうですね。