🕶️

LangChainによるChatAPI

2023/04/21に公開

複数のチャットサービスに対応しつつ、複数ユーザとの会話記憶を共有するAPI。
LangChainはとにかく書き方が多くて迷うので、備忘録的に。

前提

  • Python 3.10
  • FastAPI 0.95.0
  • LangChain 0.0.132

コード

import os
import json
import datetime

import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel, Field

from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage

from dotenv import load_dotenv
load_dotenv()
print(os.environ["OPENAI_API_KEY"])

class Payload(BaseModel):
    service_name: str = Field(..., example="slack", description="service name")
    timestamp: str = Field(..., example="2023-04-20 16:47:22", description="timestamp")
    sender: str = Field(..., example="MyUser", description="sender name")
    reciever: str = Field(..., example="MyBot", description="reciever name")
    content: str = Field(..., example="Hello, World!", description="message content")

class ChatApi:
    def __init__(self) -> None:
        self.app = FastAPI()
        self.ai = LangchainBot()

        @self.app.post("/question", response_model=Payload)
        async def question(payload: Payload):
            response = self.ai.chat(payload)  
            return vars(response)

class LangchainBot:
    def __init__(self):
        self.log_dir = "logs"
        self.llm = ChatOpenAI(model_name="gpt-4", temperature=0.9)
        self.chat_history = []

    def chat(self, payload: Payload):
        self.load_history(payload)
        bot_name = payload.reciever
        local_chat_history = ""
        for item in self.chat_history[-100:]:
            local_chat_history += f"{item.service_name} | {item.timestamp} | {item.sender} to {item.reciever} | {item.content}\n"
        local_chat_history = local_chat_history[-8000:]

        system_template = f"""
        現在の日時を認識してください: {payload.timestamp}

        あなたの名前は{bot_name}です。
        {bot_name}は日本語を話す親切なアシスタントです。
        {bot_name}は同時に複数の相手と会話することができます。
        今回の発言者は{payload.sender}です、
        {bot_name}は過去の記憶を以下のフォーマットで保持しています。
        "[Conversation Interface] | [Timestamp] | [Sender] to [Reciever] | [Content]"

        これまでの会話の履歴:
        {local_chat_history}

        今回の{payload.sender}の発言:
        {{user_input}}

        では、返答を考えてください。日本語で返答するのを忘れずに。
        """

        self.save_history(payload)

        chat = self.llm([SystemMessage(content=system_template), HumanMessage(content=payload.content)])
       
        now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).strftime("%Y-%m-%d %H:%M:%S")
        response = Payload(service_name=payload.service_name, timestamp=now, sender=bot_name, reciever=payload.sender, content=chat.content)
        self.save_history(response)

        return response

    def load_history(self, payload: Payload):
        chat_log_file_name = f"{self.log_dir}/chat_history_{payload.reciever}.json"
        if os.path.exists(chat_log_file_name):
            with open(chat_log_file_name, "r", encoding="utf-8") as f:
                json_payload = json.load(f)
            for item in json_payload["history"]:
                self.chat_history.append(Payload(**item))

    def save_history(self, payload: Payload):
        chat_log_file_name = f"{self.log_dir}/chat_history_{payload.sender}.json"
        self.chat_history.append(payload)
        _json_payload = []
        for item in self.chat_history:
            _json_payload.append(item.dict())
        json_payload = {"history": _json_payload}
        with open(chat_log_file_name, "w", encoding="utf-8") as f:
            json.dump(json_payload, f, ensure_ascii=False, indent=4)

if __name__ == "__main__":
    api = ChatApi()
    app = api.app
    uvicorn.run("__main__:app", host="0.0.0.0", port=9301, reload=False)

# 以下に差し替えると、CUIでテストできる。
'''
if __name__ == "__main__":
    bot = LangchainBot()
    service_name = "CUI
    now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).strftime("%Y-%m-%d %H:%M:%S")
    user_name = "MyUser"
    bot_name = "MyBot"

    while True:
        user_input = input(f"{user_name}: ")
        payload = Payload(service_name=service_name, timestamp=now, sender=user_name, reciever=bot_name, content=user_input)
        result = bot.chat(payload)
        print(f"bot: {result}")
'''

補足

LangChainのmemoryを使わない理由

どうしてもタイムスタンプが欲しかったのだが、LangChain側で保持させる手段が見つからず...
力づくの対応をしてしまった。長期記憶の保存もjsonファイルの読み書きだけの手抜き仕様。
長期記憶に関しては、SQLやPineConeに差し替え可能と思う。

タイムスタンプの効果

  • 現在時刻と、過去の会話の時刻を認識するようになった!
  • 昨日の何時ごろのアレ、で話が通じるようになった!
  • 複数の話者の区別がつきやすくなった(たぶん)

Payloadについて

ChatBot側でPayloadのスキーマに沿ったjsonをPOSTする想定。レスポンスも同一スキーマで返す。
長期記憶も同じスキーマのjsonなので、後々流用しやすいかな、と。

chat_historyのファイル名

ボット名別に記憶を振り分けている。ChatBot側でBot名をpayload(通常recieverになるはず)に入れてくれれば、複数のボット(ペルソナ)に対応した共通APIになれるかも。

レスポンス返す時は、senderがボット名になる。loadとsaveで、recieverとsenderが入れ替わってるのはこの為。

今気づいたけど、複数のボットを運用するならsystem_templateも分けなきゃダメだわ。system_templeteまでpayloadに含めてもいいけど...どうしようかな。

Discussion