見出し画像

レンタルサーバでWebアプリ作り:ChatGPTでテケテケ表示 Part2

ロリポップレンタルサーバではできることが限られていたので、ホスティングサービスを使ってWebアプリを公開するまでの道のりを記録します。

データ分析を仕事としているのでパソコンやITのことは多少は詳しいですが、インフラまわりは全くの素人です。そんな人が見て参考になる情報をまとめていきたいと思います。


背景

前回、SocketIOを使ってGPTの回答をテケテケ表示させました。
前回記事:https://note.com/cryptoscore/n/n83ef1cef9232

実はもっとシンプルなやり方(Event Source) があるので、それでも実装してみようと思います。


ゴール=EventSourceでテケテケさせる

EventoSourceというものを使うと、SocketIOよりもシンプルにストリーム表示できます。ただ、簡易版のようで表示はカクカクします。いっぺんにドバっと表示され、雑な感じです。

EventSourceによるストリーム表示


フォルダ構成

app.pyとindex.htmlだけの修正になります。それ以外は前回のものと全く同じです。内容は前回の記事をご覧ください。

test_digitalocean/
├ requirements.txt
├ .env
├ Pipfile ※pipが自動作成
├ Pipfile.lock ※pipが自動作成
├ Procfile
├ gunicorn_config.py
├ app.py
├ templates/
│ ┗index.html

app.py

EventSourceの弱点は、HTML側からPython側に値がPOSTできないことだそうです。GPT先生もそこがこんがらがるようで、なかなか良い回答が得られませんでした。

というわけで、いったんHTML側からPOSTしたものをPython側のpost_question()で質問を受け取り、その後で on_start_gptquestion() が回ります。また、HTMLへ情報を流すのに「yield」という関数を使います。それ以外はほぼ前回のと同じです。

from flask import Flask, render_template, request, jsonify, Response
from openai import OpenAI
import threading
from collections import deque

from dotenv import load_dotenv
import time


# 環境変数を読み込む
load_dotenv()
client = OpenAI()
token_queue = deque() #GPTの回答がどしどし入る箱
question = ''


app = Flask(__name__)


def thread_gptquestion(text):
    global token_queue
    token_queue = deque() #初期化

    stream = client.chat.completions.create(
        # model="gpt-3.5-turbo",
        model="gpt-4-1106-preview",        
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": text},
        ],
        stream=True,
    )

    #token_queueにどしどしためていく(非同期で)
    for chunk in stream:
        if chunk.choices[0].delta.content is not None:
            token_queue.append(chunk.choices[0].delta.content)

    token_queue.append('[[end]]') #終了サイン

def start_gptquestion(text):
    global token_queue, question

    thread = threading.Thread(target=thread_gptquestion, args=(text,))
    thread.start()

    msg = ''
    while True :
        if token_queue :
            token = token_queue.popleft()
            token = token.replace('\n', '<BR>')
            if token == '[[end]]' :
                print('-------------end----------------')
                break
            msg += token
            yield f"data:{msg}\n\n"
        else :
            time.sleep(0.1) #キューに何もなくなったところで0.1秒待つ

    time.sleep(0.1) #最後に0.1秒待つ(これがないとブラウザ側で最後の表示ができない)
    yield "data:[[end]]\n\n"
    time.sleep(0.1)


@app.route('/')
def index():
    return render_template('index_eventsource.html')

@app.route('/post_question', methods=['POST'])
def post_question():
    global question
    question = str(request.data)
    return '', 204  # HTTPステータスコード204は「No Content」を意味します

@app.route('/on_start_gptquestion')
def on_start_gptquestion():
    global question
    return Response(start_gptquestion(question), mimetype='text/event-stream')


if __name__ == '__main__':
    app.run(debug=True)


index.html

startButton_es()関数が肝です。「204レスポンスが返ってきたら  on_start_gptquestion を動かす」というような待ち定義 xhr を最初に作っておいて(onreadystatechange)、 post_question につないで質問テキストを 投げ込みます。python側で post_question が204を返すので、それを待って on_start_gptquestion が動き出します。
最初に `on_start_gptquestion` の記述があって、後のほうに `post_question` があるのでなんかこんがらがる。。

<!DOCTYPE html>
<html>
<head>
    <title>Flask App</title>


    <script>
        function startButton_es() {
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4 && xhr.status == 204) {
                    var source = new EventSource('/on_start_gptquestion');
                    source.onmessage = function(event) {
                        if(event.data === '[[end]]'){
                            source.close();
                        } else {
                            var logElement = document.getElementById('gptanswer');
                            logElement.textContent = event.data;
                        }
                    };
                }
            };
            xhr.open("POST", "/post_question", true);
            var text = document.getElementById('textInput').value;
            xhr.send(text);
        }
    </script>
    


</head>
<body>
    <input type="text" id="textInput" placeholder="Enter question to GPT">
    <button onclick="startButton_es()">Submit</button>
    <div id="gptanswer"></div>

</body>
</html>

これらをDigitalOceanに上げて、冒頭の画像のようなカクカクテケテケが表示されたら、成功です。


最後まで見ていただきありがとうございました!

SocketIOだと通信量が増えてしまうらしく、その改善策としてEventSourceでやってみましたが、雑感がちょっと過ぎるか。。私は何かしらの問題が起きるまでSocketIOでやろうと思いました。


DigitalOceanのアカウント登録
(200ドル分チケット付き)

以下は紹介リンクですが、ここから手続きを進めてもらえると200ドル分の無料チケット(有効期間2ヶ月)がもらえるようですので、ぜひご活用ください。
※ 2023/12/29時点

紹介リンク : https://m.do.co/c/a8b31ed34b75

サポート問い合わせ先

DigitalOceanのサポート問い合わせリンクがなかなか見つからないので、リンクを載せておきます。

https://cloudsupport.digitalocean.com/s/

場所は、トップページの右下にある「Ask a question」に行き、そのページの一番下(欄外っぽいところ)にひっそりと「Support」というリンクがあります(Contact内)。そのページの一番最後に「Contact Support」ボタンがあります。

この記事が気に入ったらサポートをしてみませんか?