💬

LangChainチャットボットのコードに簡易的なウェブインターフェイスを追加してみる

2023/08/30に公開

LangChainのユースケースの一つであるチャットボットのサンプルコードに、ローカルで試せる簡易的なウェブインターフェイスを追加してみました。

https://python.langchain.com/docs/use_cases/chatbots.html#chat-retrieval

上記のページのChat with document retrievalのサンプルコードは、質問文がハードコードされているのと、コマンドラインでの実行になっているので、以下の画像のようにローカルで試せる簡易的なウェブインターフェイスを追加して、インタラクティブに質問応答ができるように変更してみました。

画面上部のテキストフォームに質問文を入力し、Submitボタンを押すと、画面下部に質問と回答が追記されていく形式です。

ファイル体系は以下の通りです。

/your_directory
|-- app.py
|-- .env
|-- templates/
|   |-- index.html
|-- html_data/
|   |-- Equinox_EN.html
|-- pdf_data/
|   |-- Equinox_JP.pdf

OPENAI_API_KEYは、OpenAIのサイトで発行して、.envファイルに以下のように埋め込みます(以下の文字列はダミーです)。また、私の場合は使えるクレジットがなかったので、課金しました。

.env
OPENAI_API_KEY=D7f9G2uMEK1lvaoA8eB0xjXVNyR5rFQ63LhtSgZWmPc

LangChainのPythonコード(app.js)は、Flaskを使ってWebサーバー化しています。足りないパッケージは適宜pip install bs4などと追加導入してください。

今回は、昨年のJRA年度代表馬である競走馬のイクイノックスについて、Wikipediaのページの情報を読み込ませて回答させるようにしています。Document Loaderは色々なドキュメントに対応していますが、今回は最新である必要もなかったので、サンプルコードのWebBaseLoaderではなく、ローカル保存したHTMLファイルとPDFファイルを読み込ませるようにしました。

なお、これを検証する直前にChatGPT(GPT-4)に質問したところ、イクイノックスについてはよく知らないようでした。2021年9月にはすでにデビューしているのですが、当時から注目している人は熱心な日本競馬ファンぐらいだったでしょう。

app.js
# Flask imports
from flask import Flask, request, jsonify, render_template

# Langchain imports
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain, LLMChain
from langchain.embeddings import OpenAIEmbeddings
from langchain.memory import ConversationSummaryMemory
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Chroma

# Other imports
import os
from dotenv import load_dotenv

app = Flask(__name__)

# Load environment variables
load_dotenv()

# Initialize Langchain model
llm = ChatOpenAI()

data = []

'''
# Load web article
from langchain.document_loaders import WebBaseLoader
urls = [
     "https://en.wikipedia.org/wiki/Equinox_(horse)"
]
for url in urls:
    loader = WebBaseLoader(url)
    partial_data = loader.load()
    data.extend(partial_data)
'''

# get data from HTML files
from langchain.document_loaders import BSHTMLLoader
pages = ["html_data/" + f for f in os.listdir("html_data") if f.endswith('.html')]
for page in pages:
    loader = BSHTMLLoader(page, open_encoding='utf-8')
    partial_data = loader.load()
    data.extend(partial_data)

# get data from PDF files
from langchain.document_loaders import PyPDFLoader
pdfs = ["pdf_data/" + f for f in os.listdir("pdf_data") if f.endswith('.pdf')]
for pdf in pdfs:
    loader = PyPDFLoader(pdf)
    partial_data = loader.load_and_split()
    data.extend(partial_data)

# Split and vectorize text
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0)
all_splits = text_splitter.split_documents(data)

# Create vector store
vectorstore = Chroma.from_documents(documents=all_splits, embedding=OpenAIEmbeddings())

# Initialize memory and retriever
memory = ConversationSummaryMemory(llm=llm, memory_key="chat_history", return_messages=True)
retriever = vectorstore.as_retriever()
qa = ConversationalRetrievalChain.from_llm(llm, retriever=retriever, memory=memory,verbose=True)

# Flask routes
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/ask', methods=['POST'])
def ask():
    question = request.json['question']
    answer = qa(question)['answer']
    return jsonify({"answer": answer})

# Run application
if __name__ == '__main__':
    app.run()

こちらはウェブページのtemplates/index.htmlです。見た目だけはCSSでチャット風にしましたが、色々とUIとしての完成度は低いです。

index.html
<!DOCTYPE html>
<html>
<head>
    <title>LangChain ChatBot</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <style>
        body, html {
            height: 100%;
            margin: 0;
            display: flex;
            flex-direction: column;
        }
        #chat {
            max-width: 100%;
            flex-grow: 1;
            overflow: auto;
        }
        .question,
        .answer {
            max-width: 80%;
            padding: 10px;
            margin: 5px;
            border-radius: 10px;
        }
        .question {
            background-color: #f1f1f1;
            float: right;
            clear: both;
        }
        .answer {
            background-color: #e6ffe6;
            float: left;
            clear: both;
        }
        .error {
            color: red;
            font-weight: bold;
        }
        #input-area {
            text-align: center;
            padding: 10px;
            background-color: #f1f1f1;
        }
    </style>
</head>
<body>
    <div id="chat">
        <!-- Chat history will go here -->
    </div>
    <div id="input-area">
        <input type="text" id="question" placeholder="Type your question here..." minlength="2" maxlength="200" size="100">
        <button id="submit">Submit</button>
    </div>
    <script>
        $(document).ready(function() {
            $("#submit").click(function() {
                const question = $("#question").val();
                $("#chat").append("<p class='question'>Q: " + question + "</p>");
                $('#chat').scrollTop($('#chat')[0].scrollHeight); // Scroll to the bottom of #chat

                // Clear the text input field
                $("#question").val('');
                
                $.ajax({
                    url: '/ask',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify({ "question": question }),
                    success: function(data) {
                        const answer = data.answer;
                        $("#chat").append("<p class='answer'>A: " + answer + "</p>");
                        $('#chat').scrollTop($('#chat')[0].scrollHeight); // Scroll to the bottom of #chat

                        // Re-enable the submit button
                        $("#submit").prop("disabled", false);
                    },
                    error: function() {
                        $("#chat").append("<p class='answer error'>エラーが発生しました。申し訳ありません。もう一度質問してください。</p>");
                        
                    }
                });
            });
        });
    </script>
</body>
</html>

上記ファイルを用意して、python app.jsを実行し、ウェブサーバーがListen状態になったことを確認したら、http://127.0.0.1:5000/をブラウザで開いて操作するだけです。

動作結果は最初の画像の画面にあるとおりです。verbose=Trueにしているので、コマンドライン側ではもう少し細かい挙動が見えるのですが、ベクターストアの検索で関連情報がうまく拾えずに回答出来ないパターンもあるので、このあたりの情報検索精度も考慮ポイントになりそうです。

いずれにせよ、最新情報を組み合わせて回答してくれるのは良いことだと思います。また、従来のボットフレームワークと違って、IntentとEntitiyを定義して、会話フロー設計をしなくても、なんとなくチャットボットとしては動くので、簡単に作るという点ではかなりハードルが低いと感じました。

更新履歴
  • 2023/8/31: Document Loaderの変更。およびウェブページのUIの改善
  • 2023/9/1: 余計なコードの削除。次のエントリーのリンクを追加

Discussion