AI

StreamlitとOpenAIで作る! “Hello World”からのチャットボット開発

Streamlitとopenaiによるチャットボット

この数ヶ月(2023/4/1現在)、ChatGPTが話題になっています。

ChatGPTはOpenAI(AIの研究組織)が2022年11月に公開した人工知能チャットボットです。

OpenAIはAPIも公開しており、外部からプログラムを使用してChatGPTのような自然言語処理の他、音声認識、画像生成、翻訳などのさまざまな機能を使用することができます。

本記事では、OpenAIのAPIの”雰囲気”を味わうためにAPIを利用してChatGPTと同じような処理をするチャットボットを作成します。

事前準備

前提条件

本記事で掲載するソースコードは、Python3.11.2で記述されております。

使用している各ライブラリのバージョンは以下の通りです。

  • streamlit(1.20.0)
  • streamlit-chat(0.0.2.2)
  • openai (0.27.2)
  • python-dotenv(1.0.0)

pipなどでインストールしてください。

OpenAIのAPIキー取得

OpenAIのアカウントを作成した後、以下にアクセスして「Create new secret key」によりAPIキーを取得してください。

https://platform.openai.com/account/api-keys

アカウント作成後$18以内かつ3ヶ月以内は無料ですがそれ以上は料金が発生します(2023/4/1現在)。詳細は、OpenAIのページを参照してください。

なお、有料課金した段階で無料利用の$18も支払い対象となるようなので注意が必要です。

Streamlit

画面のUIを作成するためにStreamlitを使用します。

StreamlitはPython用のWebアプリを作成するためのフレームワークでデータ分析のためのダッシュボードなどを作成するのに適しています。

今回はダッシュボードを作成するわけではありませんが、Streamlitはフロントエンド・バックエンドをあまり意識せずに(それが良いかどうかは置いておいて・・)少ないコードでWebアプリを作成することができるため簡単なアプリの作成には最適です。

※ただし簡単な分、デザインなどの融通はきかないです。(特にIDを指定しにくくCSSも当てにくい・・)

“Hello World”からチャットボットへ

“Hello World”

“Hello World”を実現するためのソースコードは以下のようになります。

import streamlit as st

def main():
    st.write("Hello World")

if __name__ == "__main__":
    main()

 

ファイル名をapp.pyとして保存して「streamlit run app.py」をコマンドラインから実行すると以下のような画面が表示されます。(以下、実行方法は全て同様なので省略)

helloworld“Hello World”

“こんにちは○○さん”

次に”Hello World”を発展させて、ブラウザの画面から名前を入力すると、「こんにちは〇〇さん」と表示させるようにします。

ソースコードは以下のようになります。

import streamlit as st

def main():
    # テキストボックスで名前を入力
    name = st.text_input("名前を入力してください")
    #名前が入力されていればこんにちはと表示
    if name:
        st.write(f"こんにちは、{name}さん!")

if __name__ == "__main__":
    main()

 

実行すると以下のような画面が表示され、名前を入力してEnterを押すと「こんにちは〇〇さん」と表示されます。

helloworld-v1-1初期表示
名前を入力すると”こんにちは〇〇さん”と表示

“こんにちは○○さん”を追加して複数表示

次に名前を入力して「追加」ボタンを押すと「こんにちは〇〇さん」を画面に追加して複数表示していくようにします。

ソースコードは以下のようになります。

import streamlit as st

#名前をリストに追加する。
def add_name():
    name = st.session_state.name_input.strip()
    if name:
        st.session_state.names.append(name)
        st.session_state.name_input = ""

# セッションステートに names リストを初期化する
if "names" not in st.session_state:
    st.session_state.names = []

def main():
    # テキストボックスで名前を入力
    st.text_input("名前を入力してください",key="name_input")
    st.button("追加", on_click=add_name)

    # 名前のリストをループして、それぞれの名前に対して「こんにちは〇〇さん」を表示
    for name in st.session_state.names:
        # 名前とメッセージを2列に表示する
        col1, col2 = st.columns(2)
        col1.write(name)
        col2.write(f"こんにちは、{name}さん!")

if __name__ == "__main__":
    main()

 

ポイントは、session_stateに追加した名前のリストを保存して、そのリストをもとにcolumnsを作成して、2列で名前とそれに対応する「こんにちは〇〇さん」を表示することです。

実行すると以下のような画面が表示され、名前を入力して追加ボタンを押すと「こんにちは〇〇さん」が追加されて行きます。

helloworld-v2-1初期画面
helloworld-v2-2名前を追加すると「こんにちは〇〇さん」が複数表示(追加後、名前はクリアされる)

まずはcURLでAPIを使用する

一旦、こんにちはのアプリは置いておいて、pythonでチャットボットを作る前にcURLでOpenAIのAPIをたたいてみましょう。

cURLとは、インターネットのURLに対して様々なプロトコルを用いてアクセスできるコマンドです。コマンドラインからWEBサイトに対してhttpでリクエストを投げてコンテンツを取得するケースなどでよく使用されます。(mac,linux,windows(10以降)には標準で用意されています。)

例としてコマンドラインから以下のコマンドを打ち込みます。(<APIキー>には取得したAPIキーを設定してください。)

curl https://api.openai.com/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <APIキー>'  \
 -d '{"model": "gpt-3.5-turbo" , 
           "messages": [{"role": "user", "content": "夏目漱石の名作を3つ教えて"}]
         }'

 

実行すると以下のjsonオブジェクトを返します。(回答はchoiceの配列の第一要素のmessageのcontentに格納されています。)

{"id":"chatcmpl-716V3eHakjb1WQkzVQLzYhhndzuhG","object":"chat.completion",
"created":1680497669,"model":"gpt-3.5-turbo-0301","usage":{"prompt_tokens":25,"completion_tokens":36,"total_tokens":61},
"choices":[{"message":{"role":"assistant","content":"1. 『吾輩は猫である』\n2. 『こころ』\n3. 『草枕』"},
"finish_reason":"stop","index":0}]}

 

指定できる引数はいくつかありますが、今回は必須の”model”と”message”のみを使用します。それぞれの引数の意味は以下のとおりです。

  1. model
    AIのモデルを指定します。今回は”gpt-3.5-turbo”を使用します。
  2. messages
    会話をするためのjsonオブジェクトの配列を渡します。(例では1要素のみ)

    [{"role": "user", "content": <質問>}​]

    “role”に”user”を渡して、”content”に質問内容を渡します。
    質問は一つのなのに、何故、配列が想定されているのかは後述します。

その他の引数については、以下のURLを参考にしてください。

https://platform.openai.com/docs/api-reference/completions/create

チャットボット(first step)

名前を追加して「こんにちは〇〇さん」を表示するアプリを流用して、OpenAIに質問をすると回答を返して、それを会話の履歴として表示するアプリを作成します。

「名前」→「質問」、「こんにちは〇〇さん」→「OpenAIからの回答」というようにソースコードを変更すると以下のようになります。

import os
import streamlit as st
import openai
from os.path import join, dirname
from dotenv import load_dotenv


# APIキーの設定
load_dotenv(join(dirname(__file__), '.env'))
openai.api_key = os.environ.get("API_KEY") 

# 「送信」ボタンがクリックされた場合に、OpenAIに問い合わせる
def do_question():
    question = st.session_state.question_input.strip()
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "user", "content": question},
        ],
    )
    answer = response.choices[0]["message"]["content"].strip()
    st.session_state.qa.append({"question":question,"answer":answer})
    st.session_state.question_input = ""

def main():
    # セッションステートに qaリストを初期化する
    if "qa" not in st.session_state:
        st.session_state.qa = []

    # テキストボックスで質問を入力
    st.text_input("質問を入力してください", key="question_input")
    # 送信ボタンがクリックするとOpenAIに問い合わせる
    st.button("送信", on_click=do_question)

    # リストをループして、質問と回答を表示
    for qa in st.session_state.qa:
        col1, col2 = st.columns(2)
        col1.write(qa["question"])
        col2.write(qa["answer"])

if __name__ == "__main__":
    main()        

 

openai.ChatCompletion.createの引数と返り値はcURLでAPIをたたいた場合と同じです。

なお、APIキーは「.env」というファイルに以下の形式で格納されており、それをコードから取得しています。(このようにしておけばgitignoreファイルで.envを指定することでgitの管理対象外にでき、キーの流出を防ぐことができます。)

API_KEY = <APIキー>

 

実行すると以下のような画面が表示されます。
初期表示初期表示

質問を入力して「送信」ボタンを押すと回答が返ってきて、質問とそれに対する回答を表示します。

質問に対して回答を返す質問に対して回答を返す(テキスト欄は送信後にクリアされています。)

以下のように「質問」「回答」を繰り返すことができます。

質問と回答を繰り返すと履歴が表示される質問と回答を繰り返すと履歴が表示されるけど・・

なおChatGPTを使用したことがある方はわかると思いますが、ChatGPTでは前の質問を覚えておいて、連続した会話をすることができます。

今回、2回目の「他には何がある?」の質問も連続した会話を想定した質問だったのですが、前の質問を覚えておらず、会話になっていません。

ChatGPTのように連続した会話をするにはどうすれば良いでしょうか?

チャットボット(連続した会話ができるようにする)

前節では、APIの引数のmessegeに対して以下のjsonを設定していました。

[{"role": "user", "content": <質問>}]

 

この部分を以下のようにして会話の履歴を設定すれば、連続した会話をすることができます。(これが引数が配列になっている理由です。)

[{"role": "user", "content": <質問1>},
{"role": "assistant", "content": <回答1>},
{"role": "user", "content": <質問2>},
{"role": "assistant", "content": <回答2>},
{"role": "user", "content": <質問3>},
{"role": "assistant", "content": <回答3>},
{"role": "user", "content": <質問4(今回の質問)>}]

 

なおroleが”assistant”のjson(以下)がありますが・・

{"role": "assistant", "content": <回答1>},

 

この情報は、APIの返り値のchoice配列の第一要素のmessageの中身そのものなので、返り値をそのまま送信するメッセージに追加していけば良いことになります。

これを踏まえてソースを修正すると以下のようになります。(session_state.messagesに会話の履歴を保存しています。)

import os
import streamlit as st
import openai
from os.path import join, dirname
from dotenv import load_dotenv


# APIキーの設定
load_dotenv(join(dirname(__file__), '.env'))
openai.api_key = os.environ.get("API_KEY") 

# 「送信」ボタンがクリックされた場合に、OpenAIに問い合わせる
def do_question():
    question = st.session_state.question_input.strip()
    if question:
        #メッセージに質問を追加
        st.session_state.messages.append({"role": "user", "content": question})
        st.session_state.question_input = ""

        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=st.session_state.messages
        )
        
        message = response.choices[0]["message"]
        #メッセージに回答を追加
        st.session_state.messages.append(message)
        #回答をQAに追加
        answer = message["content"].strip()
        st.session_state.qa.append({"question":question,"answer":answer})

def main():
    # セッションステートに qa リストを初期化する
    if "qa" not in st.session_state:
        st.session_state.qa = []
        st.session_state.messages = []

    # テキストボックスで質問を入力
    st.text_input("質問を入力してください", key="question_input")
    # 送信ボタンがクリックするとOpenAIに問い合わせる
    st.button("送信", on_click=do_question)

    # リストをループして、質問と回答を表示
    for qa in st.session_state.qa:
        col1, col2 = st.columns(2)
        col1.write(qa["question"])
        col2.write(qa["answer"])

if __name__ == "__main__":
    main()  

 

質問を繰り返していくと以下のようになり、前の質問と回答を踏まえた回答を返すようになります。

連続した会話が可能前の質問を踏まえた連続した会話が可能

チャットボット(キャラ設定できるようにする)

ChatGPTを使い込んでいる人ならばやったことがある方もいると思いますが、ChatGPTに対して、何かしらの”キャラ”を指定すると回答内容の正確性が向上したり、その”キャラ”になりきった回答が得られます。

例えば、「あなたは心理学者です。」「あなたは関西人です。関西弁で答えてください。」「あなたはワンピースの主人公のルフィです。」「質問に対して全て英語で答えてください」などとするとその”設定”をもとにした回答を得られます。

これを実現するためには、APIのmessegeの配列の先頭に以下を追加します。

{"role": "system", "content": <キャラ設定>}

 

キャラ設定を実現するために以下のようにソースコードを変更します。(conditionという変数にキャラ設定を格納します。)

import os
import streamlit as st
import openai
from os.path import join, dirname
from dotenv import load_dotenv


# APIキーの設定
load_dotenv(join(dirname(__file__), '.env'))
openai.api_key = os.environ.get("API_KEY") 

# 「送信」ボタンがクリックされた場合に、OpenAIに問い合わせる
def do_question():
    condition = st.session_state.condition_input.strip()
    if condition and condition != st.session_state.condition:
        st.session_state.condition = condition
        st.session_state.messages.append({"role": "system", "content": condition})

    question = st.session_state.question_input.strip()
    if question:
        #メッセージに質問を追加
        st.session_state.messages.append({"role": "user", "content": question})
        st.session_state.question_input = ""

        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=st.session_state.messages
        )
        
        message = response.choices[0]["message"]
        #回答をメッセージに追加
        st.session_state.messages.append(message)

def main():
    # セッションステートに messages リストを初期化する
    if "messages" not in st.session_state:
        st.session_state.messages = []
        st.session_state.condition = ""

    #タイトル
    st.title("教えてGPT先輩!")
    # テキストボックスに前提条件(キャラ)を入力
    st.text_input("(任意)GPT先輩のキャラを決めてください。(例:「あなたは関西人です。関西弁で話してください。」)", key="condition_input")
    # テキストボックスで質問を入力
    st.text_input("(必須)質問を入力してください", key="question_input")
    # 送信ボタンがクリックするとOpenAIに問い合わせる
    st.button("送信", on_click=do_question)

    # messagesをループして、質問と回答を表示
    col1, col2 = st.columns(2)
    for message in st.session_state.messages:
        if message["role"] == "user":
            col1.write(message["content"])
        elif message["role"] == "assistant":   
            col2.write(message["content"])
            col1, col2 = st.columns(2)

if __name__ == "__main__":
    main()  

※ついでにsession_stateのqaとmessageが冗長なのでqaを削除してmessageだけにしました。ただし配列の形式が異なるので履歴表示のループには一工夫必要です。

初期画面は以下のようになります。(味気ないのでタイトルもつけました。)

初期画面初期画面

”キャラ”を設定して質問するとそのキャラになりきった回答が返されます。

キャラ設定して質問キャラ設定して質問
キャラ設定して質問(続き)キャラ設定して質問(続き)

チャットボット(質問・回答をチャット風に表示する)

最後はおまけですが、会話履歴が味気ないので、streamlit-chatというライブラリを使用してチャット風に表示するように変更します。

ソースコードは以下の通りです。

import os
import streamlit as st
from streamlit_chat import message
import openai
from os.path import join, dirname
from dotenv import load_dotenv


# APIキーの設定
load_dotenv(join(dirname(__file__), '.env'))
openai.api_key = os.environ.get("API_KEY") 

# 「送信」ボタンがクリックされた場合に、OpenAIに問い合わせる
def do_question():
    condition = st.session_state.condition_input.strip()
    if condition and condition != st.session_state.condition:
        st.session_state.condition = condition
        st.session_state.messages.append({"role": "system", "content": condition})

    question = st.session_state.question_input.strip()
    if question:
        #メッセージに質問を追加
        st.session_state.messages.append({"role": "user", "content": question})
        st.session_state.question_input = ""

        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=st.session_state.messages
        )
        
        message = response.choices[0]["message"]
        #回答をメッセージに追加
        st.session_state.messages.append(message)

def main():
    # セッションステートに messages リストを初期化する
    if "messages" not in st.session_state:
        st.session_state.messages = []
        st.session_state.condition = ""

    #タイトル
    st.title("教えてGPT先輩!")
    # テキストボックスに役割を入力
    st.text_input("(任意)GPT先輩のキャラを決めてください。(例:「あなたは関西人です。関西弁で話してください。」)", key="condition_input")
    # テキストボックスで質問を入力
    st.text_input("(必須)質問を入力してください", key="question_input")
    # 送信ボタンがクリックするとOpenAIに問い合わせる
    st.button("送信", on_click=do_question)

    # messagesをループして、質問と回答を表示
    for msg in st.session_state.messages:
        if msg["role"] == "system":
            continue
        #右側に表示する回答はisUserをTrueとする。
        message((msg["content"]), is_user = msg["role"] == "assistant") 

if __name__ == "__main__":
    main()  

 

画面は以下のようになり会話がチャット風に表示されます。

会話履歴をチャット風に表示会話履歴をチャット風に表示
会話履歴をチャット風に表示(続き)会話履歴をチャット風に表示(続き)

まとめ

以上、StreamlitとOpenAIを使用して”Hello World”から発展させてチャットボットを作成しました。

ChatGPTがあるのだから、そちらを使えば良いだけなのでこのアプリ自体には実用性はありませんが、OpenAIには音声認識のためのAPIや画像生成のためのAPIも用意されており、それらを利用すると様々なことに活用できそうなので、そのための第一歩としてOpenAPIを使用する練習としては良いのではないでしょうか。

機会があれば他のAPIを使用した記事も書こうと思います。