ChatGPT APIを使った問答をチャットUIを使って実現する方法

# はじめに

前回はRedmineに埋もれたナレッジをGPTを活用して再利用する実験を行った
今回はそのやり取りをChat形式で行えるUIを作成してみる

注意点：
- 下記のQiitaの記事の内容を前提とする
- 動作することを確認するためのサンプルプログラムなので複数人が同時に問い合わせることは考慮しない

https://qiita.com/rairaii/items/134470e357a1463a53c6

# 結論
スクリーンショットのようなチャットのUIを使って`knowledgebase`の内容について`GPT`に問い合わせできることを確認した。

![スクリーンショット 2023-05-29 9.44.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/7d3168d2-3d03-94f0-d31f-aa1f37939b7a.png)
## 課題
GPTが回答したテキストデータをそのまま出力しているため回答が読み辛い
GPTの回答を待っている時間が長いため辛い

# 用意するもの
本記事ではチャットのUIをローカルのPCで動作させたい。Google Colabを利用せず、M1 Macbook Pro 上の python の環境で動作確認を行った

```terminal
$ /opt/homebrew/bin/python3 --version                                             
Python 3.11.3
```

OpenAI API キー : https://openai.com/blog/openai-api

# 実験に利用したサンプルプログラム
GitHubにサンプルプログラムを保存しておく

https://github.com/aRaikoFunakami/chat_knowledgebase/tree/initial_chat

## VectorStore に格納したデータをファイルに保存

`csv`形式のファイルに保存しておいた`redmine`の`knowledgebase`のデータを`embedding`して`vectorstore`に格納し、再利用しやすいようにファイルに保存する

```python: vectorstore_persist_csv.py
import os
import sys
import json

import openai
import logging

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders.csv_loader import CSVLoader
from langchain.text_splitter import CharacterTextSplitter

# debug config
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s:%(name)s - %(message)s")

# load config
args = sys.argv
for i in args:
    logging.debug(i)
    
config_file = os.path.dirname(__file__) + "/config.json" if len(args) <=1 else args[1]

logging.debug(config_file)
with open(config_file, 'r') as file:
    config = json.load(file)

for key, value in config.items():
    logging.debug(key +":" + value)

# config
openai_api_key = config['openai_api_key']
file_path = os.path.dirname(__file__) + '/'+ config['file']
persist_dir =os.path.dirname(__file__) + '/'+ config['persist_directory']

# load 
loader = CSVLoader(file_path=file_path,  source_column=config.get('source_column'))
data = loader.load()

text_splitter = CharacterTextSplitter(
    separator = "\n",
    chunk_size = 1000,
    chunk_overlap  = 100,
)

docs = text_splitter.split_documents(data)
logging.debug(docs[0])

# Preprocessing for using Open　AI
os.environ["OPENAI_API_KEY"] = openai_api_key
openai.api_key = os.getenv("OPENAI_API_KEY")
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
embeddings = OpenAIEmbeddings()

# Stores information about the split text in a vector store
vectorstore = Chroma.from_documents(docs, embedding=embeddings, persist_directory=persist_dir)
vectorstore.persist()
```

```json: config.json のサンプル (自分の環境に合わせて修正)
{
    "openai_api_key": "sk-...",
    "file": "...csv",
    "persist_directory": "db",
    "source_column": "source"
}
```

## OpenAIへの問い合わせ処理
チャットUIから渡されたテキストをベースにGPTに問い合わせを行いGPTからの回答をチャットUIに返す
- ファイルに保存しておいた`knowledgebase`の内容をロードする
- ロードした内容についてGPTで問い合わせを行う

```python: openai_chat.py 
import os
import sys
import logging
import json

import openai

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain

# load config
def load_config():
    args = sys.argv
    config_file = os.path.dirname(__file__) + "/config.json" if len(args) <=1 else args[1]
    
    logging.info(config_file)
    with open(config_file, 'r') as file:
        config = json.load(file)

    return {
            "openai_api_key": config['openai_api_key'],
            "persist_dir": os.path.dirname(__file__) + '/'+ config['persist_directory']
        }

# Preprocessing for using OpenAI
def get_chain(config):
    os.environ["OPENAI_API_KEY"] = config["openai_api_key"]
    openai.api_key = os.getenv("OPENAI_API_KEY")
    llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
    embeddings = OpenAIEmbeddings()
    # Loading　the persisted database from disk
    vectordb = Chroma(persist_directory=config["persist_dir"], embedding_function=embeddings)
    # IF for asking OpenAI
    return ConversationalRetrievalChain.from_llm(llm, vectordb.as_retriever(), return_source_documents=True)

def openai_qa(query, history=[]):
    csv_qa = get_chain(load_config())
    chat_history = history
    return csv_qa({"question": query, "chat_history": chat_history})

def main():
    # Question
    query = 'ラズパイでスクリーンショットを撮りたい'
    result = openai_qa(query)
    print(result["answer"])
    # Review the original set of texts related to the above responses.
    for doc in result['source_documents']:
        print(doc.page_content.split('\n', 1)[0])
        print(doc.metadata)


if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s:%(name)s - %(message)s")    
    main()
```

## チャットUIを用意

### 利用ライブラリ
チャットUI: [chatux](https://github.com/riversun/chatux)
Webサーバ: [flask](https://flask.palletsprojects.com/en/2.3.x/)

### WebサーバとチャットUI

| ディレクトリ | 説明 |
|:-|:-|
| /  |  チャットUIをロード |  
| /chat | チャットUIとGPTを取り持つ| 
| /icon/botPhoto  | チャットボット側のアイコン ([フリー素材](https://www.flaticon.com/free-icon/grinning_2171967?term=shiba+inu&page=1&position=8&origin=search&related_id=2171967)を利用)  |   

```python: app.py
from flask import Flask
from flask import render_template
from flask import send_file
from flask import request

import os
import io
import json
import logging
from gpt.openai_chat import openai_qa

app = Flask(__name__)
chat_history=[]

# loading chat client
@app.route('/')
def chat_client():
    return render_template('chat_client.html')

# icon botPhoto
@app.route('/icon/botPhoto')
def icon():
    icon = os.path.dirname(__file__) + '/'+ 'templates/botPhoto.png'
    logging.debug("icon:" + icon)
    return send_file(icon, mimetype='image/png')

@app.route('/chat')
def chat():
    # preprocess
    query = request.args.get('text')
    callback = request.args.get('callback')
    # query to openai
    chat_history = []
    result = openai_qa(query, chat_history)
    answer = result["answer"]
    # debug
    logging.debug("answer: " + answer)
    for doc in result['source_documents']:
        logging.debug(doc.page_content.split('\n', 1)[0])
        logging.debug(doc.metadata)
    # answer to client
    res = callback + '(' + json.dumps({
                "output":[  
                    {  
                        "type":"text",
                        "value":answer
                    }
                    ]
            }) + ')'
    logging.debug(res)
    return res

def main():
    logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s:%(name)s - %(message)s")   
    app.debug = True  
    app.run()

if __name__ == '__main__':
    main()
```

```html: chat_client.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>エコーボット</title>
</head>
<body>
<script src="https://riversun.github.io/chatux/chatux.min.js"></script>
<script>
    // original source code
    // license: MIT
    // https://github.com/riversun/chatux-examples-ja
    const chatux = new ChatUx();

    //ChatUXの初期化パラメータ
    const initParam =
        {
            renderMode: 'auto',
            api: {
                //echo chat server
                endpoint: 'http://127.0.0.1:5000/chat',
                method: 'GET',
                dataType: 'jsonp',
                escapeUserInput: false
            },
            bot: {
                // original https://www.flaticon.com/free-icon/grinning_2171967?term=shiba+inu&page=1&position=8&origin=search&related_id=2171967
                // license : Free for personal and commercial use with attribution. More info
                botPhoto: 'http://127.0.0.1:5000/icon/botPhoto',
                humanPhoto: null,
                widget: {
                    sendLabel: '送信',
                    placeHolder: '何か話しかけてみてください'
                }
            },
            window: {
                title: 'エコーボット',
                infoUrl: 'https://github.com/riversun/chatux'
            }
        };
    chatux.init(initParam);
    chatux.start(true);

</script>
</body>
</html>
```


# 実験

`knowledgebase`のデータを`embedding`して`vectorstore`に格納したデータをファイルに保存
```terminal: 
$ python3 ./gpt/vectorstore_persist_csv.py
$ ls gpt/db
chroma-collections.parquet	chroma-embeddings.parquet	index
```

ファイルに保存した`vectorstore`のデータをロードしてGPTに問い合わせるUIを実行
Webブラウザで http://127.0.0.1:5000 にアクセスする

```terminal
$ python3 app.py  
* Serving Flask app 'app'
* Debug mode: on
INFO:werkzeug - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
```