【WEB】LINEから他サービスAPIを使う(OpenAI/wiki/twitter/youtube)

自動応答Linebotを作ったメモ。流行りのOpenAIも使ってみた。
(機能追加次第追記・修正していく予定です)
```
サーバ：Raspberry Pi4 ModelB + Ubuntu 22.04.2
プログラム：python,flask
外部連携：ngrok,webhook
```
構成はこんな感じで、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/48077cc4-c197-4b11-a4b3-ad41c296e69b.png)


デフォルトではOpenAIで応答し、LINEメッセージの先頭に"twitter XXX" や "wiki XXX"を入れることでそれぞれのサービスからツイートや記事を検索して返してくれます。
「調べモノbot」みたいなもんですね。
※2023/8,"twitter"は"X"になり、searchAPIの使用が有料となってしまいました...


# ①LINEBOT作成
メッセージを送る為のチャットボットを用意します。AI応答の設定に合わせてキャラクターを作ってもいいかも。アカウント作成方法は割愛。

https://developers.line.biz/ja/

LINE Developersの設定画面から
払い出されたアクセストークンとチャネルシークレットを確認しておきます。

### チャネルアクセストークン
LINEDevelopers⇒MessagingAPI設定⇒チャネルアクセストークン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/f6982482-5ddd-176a-1b3d-41e25b9c4832.png)

### チャネルシークレット
LINEDevelopers⇒チャネル基本設定⇒チャネルシークレット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/7cddad87-96a6-da4c-d508-52072ec736d5.png)
webhookの設定は手順の最後に行います。

# ②TwitterAPI登録
TwitterのAPIを使用するためにデベロッパー登録が必要です。

https://developer.twitter.com/ja

キーは払い出し後に確認ができなくなるので注意。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/31259f67-3481-1d3b-817f-0ff00c47e358.png)

# ③OpenAI登録
OpenAIはとてもシンプルです。キー1つだけ。

https://platform.openai.com/account/api-keys

現状OpenAIは従量課金(初回トライアル3ヶ月は$18分までは無料)のため、ここで課金制限をかけられます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/fe021a87-c2a8-5a02-0d62-d1455cf47497.png)

# ④YoutubeAPI登録
Youtube Search APIを使用。こちらもかなりシンプルです。

https://qiita.com/koki_develop/items/4cd7de3898dae2c33f20


# ⑤必要なパッケージのインストール
プログラムを動かすサーバ(ここではローカルのUbuntu)で諸々必要なパッケージをインストールしておきます。
以下コマンド実行
```
apt-get install python-pip   ##pythonパッケージ
pip install line-bot-sdk     ##LINEBOT
pip install flask            ##Flask
pip install openai           ##OpenAIのAPI
pip install tweepy           ##TwitterAPIライブラリ
pip install wikipedia        ##WikipediaAPIライブラリ
pip install google-api-python-client  ##GoogleAPIライブラリ
```
※本記事執筆時(2023/4)のバージョン
```
line-bot-sdk           2.3.0
Flask                  2.2.2
openai                 0.27.2
tweepy                 4.13.0
wikipedia              1.4.0
google-api-python-client  2.102.0
```
`pip list` コマンドで確認できる。

# ⑥環境変数を書いておく
コードの中にAPIキーベタ書きはNGなので、OSの環境変数として定義しておきましょう。
```config
root@host01:~# cat ~/.bash_profile
export YOUR_CHANNEL_ACCESS_TOKEN=XXXXXX    ##①で確認
export YOUR_CHANNEL_SECRET=XXXXXX          ##①で確認
export TWITTER_API_KEY=XXXXXX              ##②で確認
export TWITTER_API_SECRET_KEY=XXXXXX       ##②で確認
export TWITTER_ACCESS_TOKEN=XXXXXX         ##②で確認
export TWITTER_ACCESS_TOKEN_SECRET=XXXXXX  ##②で確認
export OPENAI_ACCESS_TOKEN=XXXXXX          ##③で確認
export YOUTUBE_API_KEY=XXXXXX              ##④で確認
export FLASK_APP=/opt/line-bot/main.py     ##応答プログラム置き場
root@host01:~#
```

# ⑦応答プログラムのコード実装
メインとなる応答プログラムのコードです。
twitter,wikiの文字列で引っ掛けてそれぞれのAPIを実行し、それ以外はすべてAIから応答させるようにします。

```python:main.py
# FlaskとLinebotライブラリのインポート
from flask import Flask, request, abort

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage, ImageSendMessage,
)

# OS,正規表現,Twitter,wikipedia,youtube,OpenAIライブラリのインポート
import os
import re
import tweepy
import wikipedia
import openai

from apiclient.discovery import build

app = Flask(__name__)

## APIキーの定義。OSの環境変数に入れておく
## LINEBOTとwebhook用キー定義
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]
line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

## Twitter用キー定義
TWITTER_API_KEY = os.environ["TWITTER_API_KEY"]
TWITTER_API_SECRET_KEY = os.environ["TWITTER_API_SECRET_KEY"]
TWITTER_ACCESS_TOKEN = os.environ["TWITTER_ACCESS_TOKEN"]
TWITTER_ACCESS_TOKEN_SECRET = os.environ["TWITTER_ACCESS_TOKEN_SECRET"]
auth = tweepy.OAuthHandler(TWITTER_API_KEY, TWITTER_API_SECRET_KEY)
auth.set_access_token(TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET)
api = tweepy.API(auth)

## Wiki検索時の言語セット
wikipedia.set_lang("ja")

## Youtube用キー定義
YOUTUBE_API_KEY = os.environ["YOUTUBE_API_KEY"]
youtube = build('youtube', 'v3', developerKey=YOUTUBE_API_KEY)

## OpenAI用キー定義
openai.api_key = os.environ["OPENAI_ACCESS_TOKEN"]

## OpenAI回答用変数定義 #過去の会話を理解させる場合に使用
#assist1 = ""


## Linebotに送られたメッセージ処理
@app.route("/", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)
    return 'OK'


## メッセージが "wikipedia XXX"または "wiki XXX"の場合、wikipedia記事の検索を実行する
## メッセージが "twitter XXX"または "ツイート XXX"の場合、tweet検索を実行して5件表示する
## メッセージが "youtube XXX または" "ユーチューブ XXX"の場合、動画検索を実行して5件表示する
## 上記以外の場合はChatGPTから応答する
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    send_message = event.message.text
    match = re.search("(wiki|wikipedia) (.+)", send_message.lower())
    match1 = re.search("(twitter|ツイート) (.+)", send_message.lower())
    match2 = re.search("(youtube|ユーチューブ) (.+)", send_message.lower())
    if match:
        query = match.group(2)
        try:
            wikipedia_page = wikipedia.page(query)
            # wikipedia.page()の処理で、ページ情報が取得できれば、以下のようにタイトル、リンク、サマリーが取得できる。
            wikipedia_title = wikipedia_page.title
            wikipedia_url = wikipedia_page.url
            wikipedia_summary = wikipedia.summary(query)
            reply_message = '【' + wikipedia_title + '】\n' + wikipedia_summary + '\n\n' + '【詳しくはこちら】\n' + wikipedia_url
        # ページが見つからなかった場合
        except wikipedia.exceptions.PageError:
            reply_message = '【' + query + '】\nについての情報は見つかりませんでした。'
        # 曖昧さ回避に引っかかった場合
        except wikipedia.exceptions.DisambiguationError as e:
            disambiguation_list = e.options
            reply_message = '以下の候補から近いものを再入力してください。\n\n'
            for word in disambiguation_list:
                reply_message += '・' + word + '\n'
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(reply_message)
        )
    elif match1:
        query = match1.group(2)
        tweets = api.search_tweets(q=query, count=5, result_type="mixed", include_entities=True)
        reply_message = ""
        for tweet in tweets:
            reply_message += "=====================\n" + tweet.user.name + "\n@" + tweet.user.screen_name + "\n" + tweet.created_at.strftime('%Y/%m/%d %H:%M:%S') + "\n=====================\n" + tweet.text + "\n" + "https://twitter.com/" + tweet.user.screen_name + "/status/" + tweet.id_str + "\n\n\n\n"
        if reply_message == "":
            reply_message = "該当するツイートは見つかりませんでした。"
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text=reply_message)
        )
    elif match2:
        query = match2.group(2)
        search_response = youtube.search().list(
            q=query,
            type='video',
            part='id,snippet',
            maxResults=5  # 検索結果の最大数を指定
        ).execute()
        reply_message = ""
        for search_result in search_response.get('items', []):
            video_title = search_result['snippet']['title']
            video_url = "https://www.youtube.com/watch?v=" + search_result['id']['videoId']
            reply_message += '【' + video_title + '】\n' + video_url + '\n\n'
        if reply_message == "":
            reply_message = "該当する動画は見つかりませんでした。"

        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text=reply_message)
        )

    else:
#     global assist1  # assist1をグローバル変数にする。#過去の会話を理解させる場合に使用
     response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
#            {"role": "system", "content": "任意の設定を書く。例：占い師になって下さい。"},  # AIに人格を与える場合に使用
#            {"role": "assistant", "content": assist1}, # 過去の会話を理解させる場合に使用
            {"role": "user", "content": send_message},
        ]
     )
#     assist1 = response["choices"][0]["message"]["content"]  # assist1を更新 過去の会話を理解させる場合に使用
     line_bot_api.reply_message(
         event.reply_token,
         TextSendMessage(text=response["choices"][0]["message"]["content"]),
     )

if __name__ == '__main__':
    app.run()
```
`assist1`を有効にすることでAI応答の回答を記憶させ"会話"ができるようにもできますが、
使用トークン数が増大するのと、複数人で使うことを考えると一問一答形式の方がいいと思います。

また、ここでは指定していませんが`"system", "content":`のセクションで人格を与えることができるようです。しかも結構細かいめに↓

https://qiita.com/sakasegawa/items/db2cff79bd14faf2c8e0

作成したLINEBOTのキャラクターに合わせた設定にすると面白いかもしれません。オラ悟空みたいなしゃべり方にしてぇぞ！！

# ⑧ngrokの用意
元々"heroku"というPaaSのサービスを利用するつもりでしたが、有料化してしまったのとラズパイが手に入ったのでローカルで"ngrok"を使うことにしました。
簡単なWebサーバーならサクッと立ち上げることが出来る優れモノですが、ローカルサーバが外部に公開されることになるので注意が必要です。

### ダウンロード、コマンド配置
```console
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip
unzip ngrok-stable-linux-arm.zip
mv ngrok /usr/local/bin/
```

### バージョン確認
```console
root@host01:~$ ngrok version
ngrok version 2.3.40
root@host01:~$
```

### ngrokアカウント認証
ngrokを実行すると公開用のURLが払い出されますが、デフォルトだと2時間で無効になります。
永久に有効にするには以下からアカウント登録し、Authキーを使うことでプロセスが落ちるまではURLが変わりません。
※アカウントを登録したとしても、Freeプランだとngrokプロセスが落ちると再度URLが新規に払い出されます。

https://dashboard.ngrok.com/login?state=

### authキー取得

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/d052edba-fe61-141a-1c18-1b93303eb6af.png)

### 認証の設定
```
ngrok authtoken {取得したAuthToken}
```
`ngrok.yml`という設定ファイルに、コマンドラインのオプションや通信の設定をtunnelsセクションに指定することで実行時のパラメータを省くことができます。
```console
root@host01:~# cat .ngrok2/ngrok.yml
authtoken: {取得したAuthToken}
root@host01:~#
```

# ⑨プログラムをバックグラウンド実行/URL公開
flaskコマンドでプログラムを実行し、ngrokコマンドで外部公開。
バックグラウンドでサーバからログアウトしてもプロセスを実行し続けるようにします。
flaskとngrokのポートは合わせるようにしましょう。
```console
root@host01:~#
root@host01:~# nohup flask run --reload --port 8080 &
[1] 1832
root@host01:~# nohup ngrok http 8080 &
[2] 1835
root@host01:~# curl http://localhost:4040/api/tunnels | grep ngrok
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1028  100  1028    0     0   277k      0 --:--:-- --:--:-- --:--:--  334k
{"tunnels":[{"name":"command_line","uri":"/api/tunnels/command_line","public_url":"https://xxxx.ngrok-free.app","proto":"https","config":{"addr":"http://localhost:8080","inspect":true},"metrics":{"conns":{"count":249,"gauge":0,"rate1":1.(略)
root@host01:~#
```
ngrokで公開されたURLはcurlでトンネル確認して行います。
`"public_url"`の値がwebhookのURLとなり、これをLINE Developersのwebhookの設定に入れます。
ちなみに止めたい時はプロセスをkillすればOKです。

# ⑩Webhook連携
公開したURLをLINE Developersでwebhook設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/4a37639f-b95e-3c62-e2cc-e581bfa718a6.png)
"検証"をクリックすると疎通テストができます。"成功"と出ればOK.

# 完成！

これで完成です。さっそくLINEメッセージを送ってみましょう！

### wikipedia
![IMG_8322.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/956e47e4-0149-bbda-44e1-04ad454b919d.png)


### Twitter
![IMG_8323.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/37b6ac8a-e121-ab70-d899-3462a447dad9.png)

### Youtube
![IMG_0378.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/5ae1c8bd-e1f5-5d8e-7706-ccaa9d7d7cd5.png)

### OpenAI
![IMG_8324.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673840/4a280c43-c8f9-dcc5-18ec-9a1c7a77a7cd.png)

当然自宅ラズパイなのでこいつが落ちると動かなくなります(既読スルーされる)。bot自体はまだまだ色々できそうなのでやってみたいですね。

以上ぅ


## 参考
https://qiita.com/kro/items/67f7510b36945eb9689b

https://zenn.dev/karaage0703/articles/0267c64d5d857d

https://qiita.com/rubytomato@github/items/3ffb51d60fec24863a5b