楽天APIと連動したサーバーレスchatbotをTeamsに導入してみた

# はじめに
今回私は、書籍検索ができるサーバーレスなchatbotを作成しTeamsに導入しました。

ユーザーがTeamsにキーワードを入力すると、キーワードに関連する書籍のタイトルと価格が5件表示されるchatbotを作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905863/9e3dc8ba-c770-36be-5edb-49fdac8d0413.png)
本記事では、サーバーレスなchatbotをTeamsに導入する方法を紹介します。

## 背景
私がchatbotを作成するに至った背景を説明します。

弊社は、新卒育成を目的とした新しい取り組みを探していました。
探していく中で、「新卒と先輩社員でペアプログラミングをしてアプリを作ってみるのはどうか？」という案が出ました。

この案の目的は以下の2つです。

- 先輩社員の技術力を使えば短期間でモノを作れる
    - 新卒への学習意欲向上の刺激になる。
- 先輩社員とペアプログラミングをすることでコードを書く際の思考回路など様々な知見が得れる
    - コーディング経験の浅い新卒からすると、先輩社員が何を考えてコーディングしているのか知ることは貴重な経験になる。

この案を一旦、新卒である私と先輩社員で取り組んでみようという流れで今回のchatbot作成に至りました。

ちなみにペアプログラミングは、Live Shareを使用しました。
Live ShareはVS Codeで使用できるペアプログラミング用のツールです。
一緒に作業している人がどこを操作しているのかを視覚的に把握できますし、動作も重くないため、快適にペアプログラミングができます。

Live Shareの詳細は、[Visual Studio Live Shareでペアプロしてみた](https://dev.classmethod.jp/articles/visual-studio-live-share-pairprograming/)(Developers.IOより引用)を参照ください。

## 作成したchatbotの構成図
作成したchatbotの処理内容は以下です。

1.Teamsでキーワードを送信する。
2.キーワードの送信をトリガーに送信Webhook[^1]が起動する。
3.送信WebhookがキーワードをリクエストBodyに詰めてAPIGatewayにPOSTリクエストする。
4.APIGatewayへのリクエストをトリガーに、Lambda関数が起動する。
5.Lambda関数内で楽天APIにGETリクエストする。
6.楽天APIが書籍を検索する。
7.検索結果をTeamsにレスポンスする。

[^1]: 送信WebhookとはTeamsと外部アプリに対してPOST要求をするTeamsの機能です。<br>詳細は[送信 Webhook を使用して Microsoft Teams にカスタム ボットを追加する](https://docs.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/add-outgoing-webhook)(Microsoft Officeデベロッパーセンターより引用)を参照ください。

楽天APIは楽天ブックス総合検索APIを使用しています。
楽天ブックス総合検索APIの詳細は
[楽天ブックス総合検索API (version:2017-04-04)](https://webservice.rakuten.co.jp/api/bookstotalsearch/)(Rakuten Developersより引用)を参照ください。

以下構成図です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905863/0b1083d1-b585-9859-8ac9-97df5533ea2a.png)

# 作成手順
## 1.AWS環境の準備
今回は主に2つのAWSサービスを使用します。

- AWS Lambda
    - 特定のイベントをトリガーにLambda関数が実行されます。<br>サーバーを設置しなくても関数の実行が可能なサーバーレスアプリケーションを実現します。
- Amazon API Gateway
    - LambdaのトリガーにはAPI Gatewayを使用します。<br>API Gatewayは簡単にAPIを作成できるAWSのフルマネージドサービスです。

Lambda関数はpythonで実装します。実装内容は[3.スクリプトの記述(pythonを使用)](#3スクリプトの記述pythonを使用)で紹介します。

API GatewayのURLにPOSTリクエストすると、Lambda関数が実行される設定をします。
設定方法は[API GatewayとLambdaでAPI作成のチュートリアル](https://qiita.com/vankobe/items/ab5bc6487c7e07cb3aba)([@vankobe](https://qiita.com/vankobe)さんより引用)を参照ください。

## 2.楽天APIの準備
楽天APIを使用するためには、楽天の会員登録とアプリIDの発行が必要です。
アプリIDの発行方法は[楽天商品検索APIの使い方－最安値の商品を見つけよう（PHP）－](https://haniwaman.com/rakuten-api/)(HPcodeより引用)を参照ください。

アプリIDの発行や楽天APIの使用は無料です。

## 3.スクリプトの記述(pythonを使用)
処理を実装するにあたって、以下の3つの関数を作成しました。

1.ユーザーが入力したキーワードで書籍を検索する関数
2.1の関数の処理結果からタイトルと価格のパラメータのみ抽出する関数
3.1,2の関数を呼び出し、chatbotの一連の処理を記載する関数

### 3-1.ユーザーが入力したキーワードで書籍を検索する関数
この関数では、ユーザーが入力したキーワードで書籍を検索しています。

関数が呼び出された際に、ユーザーが入力したキーワードを引数で受け取ります。

また、書籍の検索結果をjsonで楽天APIから受け取り、dict型でreturnします。

以下、pythonスクリプトです。

```python
REQUEST_URL = "https://app.rakuten.co.jp/services/api/BooksTotal/Search/20170404"
APP_ID="ここに発行したアプリIDを記載"
def search_books(keyword):
    serch_params={
    "applicationId" : [APP_ID],
    "formatVersion": 2,
    "keyword" : keyword,
    "hits": 5,
    "availability": 0,
    "sort": "reviewAverage"
    "booksGenreId": "001"
    }
    response = requests.get(REQUEST_URL, serch_params)
    result = response.json()
    return result
```

`search_params`に楽天APIの入力パラメータを格納します。今回は以下のパラメータを指定しました。

- formatVersion
    - 出力フォーマットのバージョンを指定。
- hits
    - 1ページあたりの取得件数を指定。
- availability
    - 検索結果の書籍の在庫状況を指定。
- sort
    - 検索結果をどの順で並び替えるかを指定。
- booksGenreId
    - 楽天ブックス内のジャンルを指定。

その他の入力パラメータは[楽天ブックス総合検索API 入力パラメーター version:2017-04-04](https://webservice.rakuten.co.jp/api/bookstotalsearch/#inputParameter)(Rakuten Developersより引用)を参照ください。

### 3-2.1の関数の処理結果からタイトルと価格のパラメータのみ抽出する関数
この関数では、1の関数で処理した検索結果からタイトルと価格のパラメータのみを抽出しています。

関数が呼び出された際に、1の関数の処理結果を引数で受け取ります。

また、タイトルと価格のパラメータで抽出した結果をlist型でreturnします。

以下、pythonスクリプトです。

```python
def extract_books(result):
    item_list = [{'title': item['title'], 'price': item['price']} for item in result['Items']]
    return item_list
```

1の関数で取得した書籍でループを回し、タイトルと価格のパラメータで抽出した結果を`item_list`にlist型で格納します。
また上記の処理は、内包表記を使用しています。

### 3-3.1,2の関数を呼び出し、chatbotの一連の処理を記載する関数
この関数では、1,2の関数を呼び出してchatbotに必要な一連の処理を実行し、APIGatewayにレスポンスします。

以下、pythonスクリプトです。

```python
app = Chalice(app_name='ここにAPI名を記載')
def search():
    json_text = app.current_request.json_body['text']
    keyword = json_text[json_text.find('</at>')+5:]
    keyword = html.unescape(keyword)
    result = search_books(keyword)
    text = ''
    for item in extract_books(result):
        text += f'**タイトル**： {item["title"]}。\n\n**価格**： {item["price"]}円\n\n------------------------------------------\n\n'
    return {'text':text}
```

`json_text`にTeamsで入力されたキーワードを格納します。
TeamsからAPIGatewayへのリクエスト内容はCloudWatch Logsを参考にしながら記載しました。
(`app`の格納に使用しているChaliceは[4.pythonスクリプトをLambdaにデプロイ](#4pythonスクリプトをlambdaにデプロイ)で説明しています。)

TeamsからAPIGatewayにキーワードを送る際、特定の文字列をhtml文字列でエスケープしてしまうため、
unescape関数を使用してエスケープしないよう設定した上で`keyword`にキーワードを格納しています。

`result`にlist型で欲しい情報を格納した後に、list型をString型に変換する処理をします。

最後にkeyをtextで設定したdict型オブジェクト[^2]をreturnすれば、pythonスクリプトの処理は終了です。

[^2]: Teamsにレスポンスする際は、keyにtextを含める必要があります。<br>keyを含めなかった場合、テキストとして扱われずエラーとなってしまいます。

### 3-4.全てのpythonスクリプト
今回のchatbotに必要な全てのスクリプトを改めて以下に記載します。

```python
import requests
import html
from chalice import Chalice

app = Chalice(app_name='ここにAPI名を記載')

REQUEST_URL = "https://app.rakuten.co.jp/services/api/BooksTotal/Search/20170404"
APP_ID="ここに発行したアプリIDを記載"

@app.route('/bookSearch', methods = ['POST'])
def search():
    json_text = app.current_request.json_body['text']
    keyword = json_text[json_text.find('</at>')+5:]
    keyword = html.unescape(keyword)
    result = search_books(keyword)
    text = ''
    for item in extract_books(result):
        text += f'**タイトル**： {item["title"]}。\n\n**価格**： {item["price"]}円\n\n------------------------------------------\n\n'
    return {'text':text}

def search_books(keyword):
    serch_params={
    "applicationId" : [APP_ID],
    "formatVersion": 2,
    "keyword" : keyword,
    "hits": 5,
    "availability": 0,
    "sort": "reviewAverage"
    "booksGenreId": "001"
    }
    response = requests.get(REQUEST_URL, serch_params)
    result = response.json()
    return result

def extract_books(result):
    item_list = [{'title': item['title'], 'price': item['price']} for item in result['Items']]
    return item_list
```

## 4.pythonスクリプトをLambdaにデプロイ
AWS Chaliceを使用してLambdaにデプロイしました。

chaliceは、コマンド一つでLambdaにコードをデプロイできるAWSのアプリケーションフレームワークです。
AWS Chaliceの詳細は[AWS Chaliceとは](https://aws.amazon.com/jp/builders-flash/202003/chalice-api/)(builders.flashより引用)を参照ください。

デプロイするスクリプトとLambdaを関連付けるために以下を記載します。

```Python
from chalice import Chalice
app = Chalice(app_name='ここにAPI名を記載')
@app.route('/bookSearch', methods = ['POST'])
# 以下処理コードを記載
```

上記のスクリプトを記載した後にターミナルで

```Bash
chalice deploy
```

とコマンドを打つとLambdaにデプロイできます。

AWS ChaliceはAWS credentialファイルが必要なのでご注意を。

## 5.動作確認
chatbotの作成が終了したらTeamsに導入します。

chatbotをTeamsに導入する方法は以下を参照ください。
[Microsoft TeamsのOutgoing Webhooksを使ってAWS Lambda(Python), Amazon API Gatewayとbot](https://www.yamamanx.com/microsoft-teams-outgoing-webhooks-aws-lambdapython-amazon-apigateway-bot/)(ヤマムギより引用)

Teamsのチームにアプリを導入すると、チームに所属している全てのチャネルからchatbotの起動が可能になります。

実際にチャネルの中で、chatbotアプリ名にメンションを付けて、キーワードを入力すると書籍の検索結果が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905863/34e93c46-7d2c-bbb8-3577-f1b7b185f8c8.png)

また、CloudWatch Logsに表示されているログを見ると、入力したキーワードで検索していることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905863/fd21312b-b96b-f6cc-0bcf-d401be2b35e9.png)

# 最後に
自分は今までAWSサービスは、EC2を立てたり、ELBを使用して負荷分散したり、という目的でしか使用したことがありませんでした。
また、言語もJavaしか触ったことのないようなエンジニア初心者でした。

そんな自分が、今回初めてAPIGatewayやLambda、Python、外部APIを使用して一つのアプリを作れたことは、とても貴重な経験でした。

先輩社員とペアプログラミングすることで自分では気づかない冗長なコーディングであったり、色んなパターンのコーディング方法を知ることができました。
また、先輩社員が助言をくれるので、長時間悩んだり調べたりして全くコーディングが進まないという新卒含め若手が陥りがちな現象を回避できます。

ペアプログラミングは、自分含め新卒社員の教育にとても有意義な教育方法だと実感しました。是非皆さんもペアプログラミングをやってみることをお勧めします。

pythonの環境構築(pipenvを用いるなど)やAWS Chaliceなど座学すらしたことのないようなサービスをいきなり使うことは苦戦しましたが、今回の弊社の取り組みによって多くのことを学べました。
また、AWSや外部APIを使うことで簡単にアプリを作成できるんだと実感できました。次は先輩社員がいなくても自分で何かアプリを作成したいと思います。

是非皆さんも本記事を参考に、様々なchatbotを作成して欲しいと思います。