見出し画像

ローカルPC内で実現!生成AIの為の「超簡単な」バックエンドの仕組み

はじめに

この記事は、ローカルPCで簡易的なバックエンドの仕組みを構築することにより、下記の記事で問題となる部分を補う内容となっております。


Windowsでのアプリケーションのインストール作業って…

私は、非常に慣れた環境でもあるためWindowsのノートPCを好んで使っていますが、どうしても苦手なことがあります。
それが何か?というと…

「アプリケーションをインストールする」

という事です。「そんなの簡単じゃん!」と言われる方も多いと思いますが、インストール後にそれを消したいと思っても…

「アンインストールしてPCを完全に元通りの状態にする自信がない!」

…のです。
OSの機能を利用してスナップショットを取るなどの対策もあるかもしれませんが、スナップショットすら信用できず、保険ではするものの、最悪な事態にならない限り全部戻す決断なんて…私には簡単に出来ません。最悪はOSから入れなおす可能性もあるため、どうしても怖くてアプリケーションのインストールを躊躇してしまいます。
(そういう意味でDockerやHiper-Vを好んで利用。Google Colabは最強)

私がそうであるならば、他の方も同じ気持ちかもしれない…という事で、環境構築を行う際、可能な限り「インストールを行わない」ように考えています。基本はzipファイルを展開して配置するだけで使える!というのが理想ですね!

バックエンドのプログラミング言語について

フロントエンドのプログラミング言語は「JavaScript」でした。それが何故かというと、ブラウザで動かすには最も適した言語だからです。

では、バックエンドはどのようなプログラミング言語で動かすべきでしょうか?

結論から言いましょう。これは「Python」を選択したいと思います!

「フロントエンドがJavaScriptだったのに、バックエンドはPythonなの?」

…って不思議に感じますよね。わざわざ違う言語にして2つ覚えるのは面倒じゃないの?というのもあるかもしれません。

OpenAIのAPIを呼ぶだけならば、正直JavaScript(node.js)でも全く問題ありません。しかし、私たちが扱いたいのはAIです。基本的にAIはPythonで動かすものが殆どです。ここでPythonという選択をしておけば、ゆくゆくはGPUを積んだローカルPC上で、自分専用のAIを動かすことも可能となるかもしれません。楽しみですね!

インストールなしでPython3を動かす

※pytyon3が既にインストール済の方は読み飛ばしてください。

バックエンドのプログラミング言語が決まったのでPythonを動かせるようにしなくてはなりません。当然Pythonをインストールすればすぐ使えるようにはなります。でも、インストールしないで済むならそれに越したことはありません。そんな方法を記述いたします。なお、Windows11を前提にしています。

まず、下記サイトにアクセスしてください。

ここでは基本的には「Stable Releases」「Windows embeddable package (64-bit)」を選択します。ただし、環境によって変わりますので、何をダウンロードしていいかを理解できていない方はGogole検索で「arm64 確認方法」を入力して、何が該当するのかを確認してみて下さい。

赤枠をダウンロードしました

ダウンロードしたら適当なフォルダに解凍してください。
例えば下記のような場所ですね!(C:\python)

上記フォルダ名はバージョンによって変わります

解凍したフォルダ内にある「python.exe」がPythonの本体です。そのファイルの絶対パスをメモ帳などに貼り付けて保存しておいてください。
上記の場合は「C:\python\python-3.12.3-embed-amd64\python.exe」になりますね!
※実行時は”.exe”は省略可能。

そしてコマンドプロンプトを実行して動くか試してみましょう。
下記のような感じで動けばOKです!

上記のように入力してみて下さい

バックエンドの前にフロントエンド側の設定

まずは、下記の記事を読んで環境を用意していますか?用意していないなら、まずは、この記事の環境を整えるところから始めて下さい。特に拡張子の表示はプログラミングする上で非常に重要ですので忘れないように。

さあ、さっそくPythonでバックエンドのプログラムを作成しましょう!
…って、いきたいところなのですが、簡単にはいきません。
なぜなら、フロントエンド側(NGINX)側とバックエンド側(Python)に対してブラウザ側から同時に繋げようとすると…オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS)というセキュリティの問題が発生してエラーとなるからです。
※同じサーバーに対して異なるリソースにアクセスすると拒否される仕組み

この問題の解決方法はいくつか考えられますが、最もスマートな方法は…

「ブラウザ側からアクセスするのはNGINXのみにしよう!」

というやり方ですね。
下記の場所(例)にある「conf\nginx.conf」をメモ帳などで開いて編集しましょう!

下記の赤枠の部分を追加して保存してください!

上記の場所(赤枠)に、下記からコピーしたものを貼り付ける
    location /openai {
        proxy_pass http://localhost:8000;
    }

これで「http://localhost/openai」というURLが呼ばれたら、NGINXがPythonのプログラム(バックエンド)を呼ぶような流れとなります。

バックエンドのプログラムの実装

フロントエンドのみでOpenAIのAPIを呼ぶ問題とは何だったでしょうか?
やはり…

「フロントエンドでOpenAIのAPIキーを扱うのはセキュリティ的に避けたい」

という事でしょう。

あとで解説しますが、このバックエンドの実装は出来る限り独自仕様を入れない方がいいでしょう。たとえば、OpenAIのAPIのパラメータに訳の分からない項目があるとして、それを省略出来る!とかです。

下記のように、OpenAIのAPIキーの部分を空白にしてリクエストの内容はそのまま、エンドポイント(連携先URL)のみ変えれば呼び出せるように考えてみました。

import http.server
import urllib.parse
import json
import http.client
import sys


# アプリケーションサーバーでリクエストが発生したときに動く
class RequestHandler(http.server.BaseHTTPRequestHandler):

    # GETで呼ばれた場合
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.end_headers()
        response_body = "<html><head><title>Welcome</title></head><body><h1>ようこそ!生成AIのAPIの世界へ!POSTでリクエストしてね!</h1></body></html>"
        self.wfile.write(response_body.encode('utf-8'))
            
    # POSTの呼び出し
    def do_POST(self):
        if self.path.startswith("/openai"):
            sub_path = self.path[len("/openai"):]
            if sub_path == "/chat":
                self.handle_chat()
            else:
                self.send_response(404)
                self.end_headers()
                self.wfile.write(b'Not Found')
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b'Not Found')

    # OpenAIのAPIを呼び出す
    def handle_chat(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length)
        parsed_data = json.loads(post_data.decode('utf-8'))

        # APIキーの追加
        headers = {
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {self.server.api_key}'
        }

        # リクエストのコンソール表示
        parsed_data_json = json.dumps(parsed_data)
        print("Request:", parsed_data_json)

        # OpenAI APIにリクエストを送信
        connection = http.client.HTTPSConnection("api.openai.com")
        connection.request("POST", "/v1/chat/completions", parsed_data_json, headers)
        response = connection.getresponse()
        completion_data = response.read().decode()
        connection.close()

        # レスポンスのコンソール表示
        print("Response:", completion_data)

        # レスポンスをクライアントに返す
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        self.wfile.write(completion_data.encode())


# アプリケーションサーバーの本体はこれだけ。最初にOpenAIキーを入力
def run(server_class=http.server.HTTPServer, handler_class=RequestHandler, port=8000):
    class ServerWithKey(server_class):
        def __init__(self, *args, **kwargs):
            self.api_key = input("OpenAI API Key: ")
            super().__init__(*args, **kwargs)
    
    server_address = ('', port)
    httpd = ServerWithKey(server_address, handler_class)
    print(f"Starting server on port {port}")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("Server is shutting down...")
        httpd.server_close()
        sys.exit(0)


if __name__ == "__main__":
    run()

「ai_backend.py」というファイル名で下記のように配置してみました。

コマンドプロンプトを開き、下記のように起動してみましょう。

ブラウザを起動し「 http://localhost:8000/ 」のURLにアクセスして起動したら成功です。

フレームワークとか一切なしで、1ファイルのみでバックエンドの処理が動いてしまうのは嬉しい限りです。停止したいときは・・・ctrl+cで停止できますが、面倒でしたらコマンドプロンプトの画面自体を閉じてしまいましょうw

今回は簡潔な実装なのでこれでいいですが、安定稼働させたり(この方法は結構不安定)バックエンドでもっと複雑なことを行う場合には「Flask」や「Django」を利用しましょう!これらの環境を用意する場合はOS直よりも仮想環境(DockerやHyper-V)がおすすめです。

独自仕様を入れない方がいい理由

出来る限りPython側のプログラムに独自仕様を入れない方がいいと書きました。

「なぜ?簡単に使えるように変更した方がいいんじゃないの?」

と思われた方もいるかと思います。その理由をご説明いたします。
プログラマーの性として…

僕が考えた「簡単に実装できる」最強の共通処理!!

というのを組み込みたくなるというのがあります。これは利用者にとって有用ではありません。

「授人以魚 不如授人以漁」という言葉があるように、人に魚を与えれば一日で食べてしまうが、釣り方を教えれば一生食べていける…ような考えが必要です。

つまり、可能な限り、「多くの人が使っている仕様を踏襲」しましょう。今回の話でいえばJavaScript側からOpenAIのAPIに対して何を渡しているかを意識させるような作りにして、応用でできる作りを心がけることが優しさです。「簡単に使える僕が考えた最強の仕様」に依存させないよう考えましょう。他人の考えた独自の仕様を覚えるのは時間の無駄ですし、生成AIが学習していないので利用も限定されてしまいます。

フロントエンドのプログラムを修正

さて、フロントエンド側の修正も必要です。プログラムは下記の記事内のプログラムを利用します。

修正内容は下記の2点です。

  • OpenAIのAPIキーを入力するところを削除

  • エンドポイントを「 http://localhost/openai 」に変更する

いままでの解説を読んでいれば、「なぜこの2点か?」を理解できるかと思います。ですので説明しないことにします。まだ理解できていない方は、再度記事を読み直してみて下さい。

細かいことは説明しません。
動くプログラムをサクッと下記に書きます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OpenAI GPT-4o API Test</title>
</head>
<body>
    <h1>OpenAI GPT-4o API Test</h1>
    <div>
        <textarea id="promptInput" placeholder="Enter your prompt here..." rows="4" cols="50"></textarea>
        <button onclick="requestChatCompletion()">Send Request</button>
    </div>
    <div id="response"></div>

    <script>
        let chatGptApiKey = "";

        const modelName_Battle = "gpt-4o";
        const endPoint = "http://localhost/openai/chat";

        async function requestChatCompletion() {
            let prompt = document.getElementById('promptInput').value;
            if (!prompt) {
                alert("Please enter a prompt.");
                return;
            }

            const requestOptions = {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    Authorization: `Bearer ${chatGptApiKey}`
                },
                body: JSON.stringify({
                    model: modelName_Battle,
                    messages: [{role: "user", content: prompt}],
                    max_tokens: 300
                })
            };

            try {
                const response = await fetch(endPoint, requestOptions);
                if (!response.ok) {
                    throw new Error('Network response was not ok.');
                }
                const data = await response.json();
                // Assuming the data returned is an array of messages, displaying the last message's content
                const lastMessage = data.choices[0].message.content;
                document.getElementById('response').textContent = lastMessage;
            } catch (error) {
                console.error('Error:', error);
                document.getElementById('response').textContent = 'Failed to fetch data.';
            }
        }
    </script>
</body>
</html>

せっかくなので「GPT-4o」呼び出すように変更してみました。何が変わったかは前回のプログラムと比較してみてご確認ください。

先ほどのバックエンドのプログラムとフロントエンジンであるNGINXを実行して動かしてみましょう!!
下記のように動けばOKです。GPT-4oは自分の事をGPT-4と思っているようですね。

おわりに

みなさん、動きましたか?

本当はバックエンド側はDockerで実装するのが理想なのですが、その内容だと…ついてこれない方が多く出てくると思い、この内容としました。
(Docker Desktopは大企業だと有料、Podman Desktopも分かりやすいとは言い難いので)

まずはリスクなく簡単に動かせる環境が作れる…という事が重要だと考えています。

フロントエンドから呼びたい生成AIはまだまだ沢山のあると思います。例えば…OpenAIのAPIにはchat以外もありますし、他社のAPIなど他にも多くあります。機能拡張は必須になりますね。

これからも、フロントエンドだけ意識すれば実装出来るように、いろいろ試していきたいと思います!


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