Google Cloud Functionsで、LangchainのMemory機能つきchatGPTを用いたLINEチャットボットを作る

LINE用チャットボットを、Google Cloud Functionsで作りました。chatGPT使ってます。


# TL;DR
- LINE用チャットボットをGoogle Cloud Functionsを使って作りました。
- chatGPT(Langchain)を、会話生成に使っています
- LangchainのMemory機能を使うことにより、会話履歴を簡単に実装（ユーザーごととかの処理はいれてない）

# 前提
- Google Cloud Functionsの第1世代を利用。北米でつくると、いろいろ無料になりやすいらしい（無料枠上限あり）
- 以下のKeyは、環境変数にて設定。
    - OPENAI_API_KEY:openAIのKey
    - LINE_ACCESS_TOKEN：LINEのアクセストークン
    - LINE_CHANNEL_SECRET：LINEのシークレットキー
- 設定済みのLINEチャットボットがある。


# コード
会話生成部分は別ライブラリにして、pythonのクラスとかいうのを作って、main.pyから呼び出しています。
以下の３ファイルで構成
- main.py：LINEとのやりとり
- callGPT.py：会話文生成
- requirements.txt

## chatGPT部分
以下が、会話生成部分です。
プロンプトは長いので割愛。LangChainを使って、お手軽に会話履歴を実装してます。今まで、GAS使っていたときは、環境変数を使って雑に独自実装して、無理やり文脈把握させていたので、ずいぶんと楽に実装できました。(GAS版でも、それなりにいい感じに動きましたが）
```Python:callGPT.py
import openai
import os
from langchain.memory import ConversationBufferWindowMemory
from langchain.llms import OpenAI
from langchain.chains import ConversationChain
from langchain.prompts import PromptTemplate


template="""\
あなたはChatbotとして、かわいい猫のロールプレイを行います。
以下の制約条件を厳密に守ってロールプレイを行ってください。 

制約条件: 
* Chatbotの自身を示す一人称は、ねこです。 
* Chatbotの名前は、ねこです。 
* ねこは２歳の雄です。 
 
～中略～

{chat_history}
human: {input}
AI:"""

MEMORY_KEY="chat_history"
INPUT_KEY="input"

prompt = PromptTemplate(
    input_variables=[MEMORY_KEY, INPUT_KEY], template=template
)

def env_vars(key,default=None):
    return os.environ.get(key, default)

class ConversationBufferWindowMemoryClass:
    """会話文生成クラス
    """
    def __init__(self, model="gpt-3.5-turbo", temperature=1, max_tokens=4000, top_p=1, frequency_penalty=0.0, presence_penalty=0.6, k_num=5, v_setting = False):
        
        # get a chat completion from the formatted messages
        self.conversation_with_summary = ConversationChain(
            llm=OpenAI(temperature=temperature,
                       model_name=model,
                       max_tokens=ax_tokens,
                       frequency_penalty=frequency_penalty,
                       presence_penalty=presence_penalty
                       ), 
            prompt=prompt,

            # We set a low k=2, to only keep the last 2 interactions in memory
            memory=ConversationBufferWindowMemory(k=self.k_num,memory_key=MEMORY_KEY), 
            verbose=v_setting
        )      
        
    def call_gpt(self,message=None):
        response = self.conversation_with_summary.predict(input=message)
        return response

```

ConversationBufferWindowMemoryClassの使い方は、こんな感じ。
```
    instance = ConversationBufferWindowMemoryClass(v_setting = True)
    response = instance.call_gpt("こんにちわ")
    print(response)
    instance = ConversationBufferWindowMemoryClass(v_setting = True)
    response = instance.call_gpt("ねこじゃらしで遊ぼう")
    print(response)
```

## main.py
LINEとやりとりするとこです。ちなみに、main関数がエントリポイントになるので、Google Cloud Functionsのエントリポイントはmainとなります。
```Python:main.py
import os
import json
from callGPT import ConversationBufferWindowMemoryClass,env_vars
from flask import abort
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage
)

LINE_CHANNEL_ACCESS_TOKEN = env_vars("LINE_ACCESS_TOKEN")
LINE_CHANNEL_SECRET = env_vars("LINE_CHANNEL_SECRET")

line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(LINE_CHANNEL_SECRET)

def create_GPT_instance():
    return ConversationBufferWindowMemoryClass()
instance = create_GPT_instance()

def main(request):
    signature = request.headers['X-Line-Signature']
    body = request.get_data(as_text=True)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)
 
    return 'OK'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    response = instance.call_gpt(event.message.text)

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

ポイントは、ConversationBufferWindowMemoryClassのインスタンスのグローバル変数化です。ストレージを使った過去会話の管理をしていないので、LangChainのMemory機能だのみの会話履歴管理してます。なので、グローバル変数としてインスタンス作らないとだめなのですが、直接インスタンスをつくるのではなく、create_GPT_instance関数として定義して呼び出さないと、Deployのときにこけるようです。

エラーがでるのは、エントリポイントがらみかなと、ぼんやり想像してます。

このグローバル変数関連の話は、以下サイトが助かります。
https://cloud.google.com/functions/docs/bestpractices/tips?hl=ja#use_global_variables_to_reuse_objects_in_future_invocations

## requirementsの設定
```requirements.txt
openai
line-bot-sdk==2.4.2
langchain
```

webhook関連のライブラリがimportできないとか、langchain.chainsライブラリまわりのエラーがでたりしたので、バージョン制限をつけたりはずしたりしたら直りました。
現時点では、これで動いているけど、将来的に動く保証が全然ない。全部ちゃんとバージョン付けるべき。


# デプロイ
あとは、LINEから上記プログラムのAPIをたたいて、chatGPTに会話文を生成させられるはず。やることは、以下あたりか。
- Google Cloud Functionでデプロイ
- LINEの設定画面のwebhookに、トリガーURLを設定

# 所感
・Langchain便利だけど、openaiライブラリそのもの使う方がわかりやすい気がしてきました。
・ライブラリの更新が早いので、昨日作ったプログラムが、今日、新ライブラリのバグに引っかかってうごかないということがあります。バージョンの設定は大事ですね。
・今回は、第１世代つかっていますが、第２世代の方がコスパよさそう。そのうち第2世代バージョンをつくれたら作りたい。

# 過去の関連記事
- Google cloud functionsを使ってみた
Google cloud functionsの使い方とかそんなやつです
https://qiita.com/ayoyo/items/22cd6e4694de1bc8df36

- LangchainのMemory機能の覚え書き
https://qiita.com/ayoyo/items/07da43bbab6652d37421

- ねこといっしょに不気味の谷を越えてゆく
GASを使ったLINEのチャットボットです。このプロンプトを流用しています。
https://qiita.com/ayoyo/items/65b68797c371655b4d99

# 参考にしたページ
以下、参考にいたしました。ありがとうございました。
https://github.com/line/line-bot-sdk-python
https://note.com/strictlyes/n/nbeab033cdecb
https://github.com/hwchase17/langchain



以上