はじめに
前回はRedmineに埋もれたナレッジをGPTを活用して再利用する実験を行った
今回はそのやり取りをChat形式で行えるUIを作成してみる
注意点:
- 下記のQiitaの記事の内容を前提とする
- 動作することを確認するためのサンプルプログラムなので複数人が同時に問い合わせることは考慮しない
結論
スクリーンショットのようなチャットのUIを使ってknowledgebase
の内容についてGPT
に問い合わせできることを確認した。
課題
GPTが回答したテキストデータをそのまま出力しているため回答が読み辛い
GPTの回答を待っている時間が長いため辛い
用意するもの
本記事ではチャットのUIをローカルのPCで動作させたい。Google Colabを利用せず、M1 Macbook Pro 上の python の環境で動作確認を行った
$ /opt/homebrew/bin/python3 --version
Python 3.11.3
OpenAI API キー : https://openai.com/blog/openai-api
実験に利用したサンプルプログラム
GitHubにサンプルプログラムを保存しておく
VectorStore に格納したデータをファイルに保存
csv
形式のファイルに保存しておいたredmine
のknowledgebase
のデータをembedding
してvectorstore
に格納し、再利用しやすいようにファイルに保存する
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()
config.json のサンプル (自分の環境に合わせて修正)
{
"openai_api_key": "sk-...",
"file": "...csv",
"persist_directory": "db",
"source_column": "source"
}
OpenAIへの問い合わせ処理
チャットUIから渡されたテキストをベースにGPTに問い合わせを行いGPTからの回答をチャットUIに返す
- ファイルに保存しておいた
knowledgebase
の内容をロードする - ロードした内容についてGPTで問い合わせを行う
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を用意
利用ライブラリ
WebサーバとチャットUI
ディレクトリ | 説明 |
---|---|
/ | チャットUIをロード |
/chat | チャットUIとGPTを取り持つ |
/icon/botPhoto | チャットボット側のアイコン (フリー素材を利用) |
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()
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
に格納したデータをファイルに保存
$ 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 にアクセスする
$ 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