JavaScriptで作る!OpenAI APIを活用した簡単なチャットボット[Node.js](初心者)

APINode.js

APIについて学習を進め、いざAPI使って何か作ってみようとしてもよくわからない。
そんな方のために初心者の私が初心者目線で簡単なチャットボットを作ってみました!

※APIキーの取得についてはやりませんが、取得したら使用上限をかけるのがいいかも。最初120$とか上限ありましたが、私は2$にしました!これで十分!

上の動画が完成図です
テキストボックスに入力しSendを押すとチャットが投げられ、ユーザーの入力とAPIからの返答が並んでいってますね

チャットボットが動く仕組み

概要

今回はクライアントサイド(ブラウザ)、サーバーサイド、OpenAI APIの三つが主に登場します

クライアント側で「Send」が押されたら、普通のJavaScriptでテキストが入った要素を上に追加しますこれはまあ簡単ですね
そして入力された内容を”fetch”メソッドを使ってサーバーサイドに投げる
出ましたfetchメソッド

サーバーサイドで、それをキャッチし、それをまたopenAIのAPIに飛ばします。
なんでクライアント側から直接やらないかは、後で説明しますね。
GPTの返答をasync awaitを使って待ちます。
クライアント側からfetchメソッドで通信が来ていましたよね。それには返信する必要があるのですが、実それをasync awaitを使って待ってもらっていました。
GPTからの回答がきたら、fetchメソッドのレスポンスにそれをぶら下げ、クライアント側に返してあげるんです。

それをブラウザ側で解釈し、普通のJavaScriptで上のログに追加する。
これで完成です!

もう少し詳しく見ていきましょう!

プロジェクトのファイル構成はこんなかんじです。

|—-node_modules
|—-public
| |—-index.html
| |—-script.js
| |—-style.css
|—-.env
|—-index.js
|—-package-lock.json
|—-package.json

node_modulesはnode_modulesですね。

publicとは、localhost:3000にアクセスしたときにブラウザに渡されるファイル群です。
これがないと、http://localhost:3000にアクセスしても何も表示されません。
なのでこのpublicの中にブラウザで表示したいindex.htmlやらを入れます。

.env これは、後でOpenAI APIのホームページAPIキーを作成するのですが、それを貼り付けておくファイルです。

index.js これがnode.jsのサーバー用のJavaScriptファイルです。ここにサーバーの記述をしていきます。

package-lock.json, package.json nodeモジュールの情報が入ったファイルです。

このpublic/script.jsでfetchメソッドが実行され、サーバーのindex.jsで受け取る。index.jsからAPIに通信をし、通信がindex.jsに帰ってきて、それをブラウザのscript.jsに返す。にみたいな流れですね。

実際のコード

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>chatGPT chatBot</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="chat-container">
    <div class="chat-box">
    </div>
    <form id="form" class="input-box" action="/chat/send" method="post">
      <textarea name="userInput" id="user-input"></textarea>
      <button id="send" type="submit">Send</button>
    </form>
  </div>

  <script type="module" src="script.js"></script>
</body>
</html>

index.htmlはこの通りで、テキスト書く場所はformになっています。実際にはjavascriptで無効にしますが、このbuttonをクリックすると、.chat-box要素の子要素に、次々と要素が追加されるといった具合ですね。.chat-boxが”ログ”になります。

ブラウザが読み込むJavaScriptファイル

document.getElementById("form").addEventListener("submit", async e => {
  e.preventDefault();

  const userInput = document.getElementById("user-input");
  addMessageToChatBox("user", userInput.value);

  const response = await fetch('/chat/send', {
    method: "post",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ "sendMessage": userInput.value })
  });

  const data = await response.json();
  addMessageToChatBox("Bot", data['receivedMessage']);

  userInput.value = "";  
});  

function addMessageToChatBox(sender, message) {
  const box  = document.getElementsByClassName('chat-box');
  const chatWrapper = document.createElement('div');
  const chatElement1 = document.createElement('p');
  const chatElement2 = document.createElement('p');
  chatWrapper.setAttribute('class', 'chatWrapper')
  chatElement1.innerHTML = sender;
  chatElement2.innerHTML = message;
  chatWrapper.appendChild(chatElement1);
  chatWrapper.appendChild(chatElement2);
  box[0].appendChild(chatWrapper);
}  

e.preventDefault()をすると、イベントをキャンセルすることができます。aタグのページ遷移だったり、ここではformの送信だったり

  const userInput = document.getElementById("user-input");
  addMessageToChatBox("user", userInput.value);

ここでinputに入力された値を取得して、addMessageToChatBoxなる関数に渡してますね。これ何かというと、コードの最後のほうで定義されてます。

そこは長々と書いてますが、要は

<div class="chatWrapper">
  <p>User</p>
  <p>自己紹介をしてください</p>
</div>

このようなブロックを挿入するコードということです。
これをUser: ユーザーの入力, Bot: GPTの回答, User, Bot, ・・・と繰り返して、どんどん追加していくことで、動画のようなログができあがります!

その次はこのように記述しました

  const response = await fetch('/chat/send', {
    method: "post",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ "sendMessage": userInput.value })
  });

  const data = await response.json();
  addMessageToChatBox("Bot", data['receivedMessage']);

  userInput.value = "";

ここでfetchの登場です。awaitが使われてますので、fetchが完了するまで下のコードは実行されません
fetch先は’/chat/send’で、post通信を行います
headerには「bodyはjson形式ですよ」と伝える”content-type”: “application/json”が添えられています
そしてミソとなるbodyには引数をjsonに変換するJSON.stringify()が使用されています。

javascriptのオブジェクトって微妙にjson形式じゃないんですよね。

{ javascriptオブジェクト: "これはJavaScriptのオブジェクトです" }
{ "json形式": "これはjson形式です" }

みたいに、jsonはキーのところが””で括られます。また、’(シングルクォーテーション)ではなく”(ダブルクォーテーション)でなきゃ駄目みたいです。

それを、上手くjsonに変換してくれるのがJSON.stringifu()です

そして最後にGPTからの回答をresponse.jsonでjson形式に変換し、ログに追加、またinputのテキストを削除しています。

Node.jsのJavaScriptファイル

import express from 'express';
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

app.use(express.json());

app.post('/chat/send', async (req, res) => {
  const message = req.body;

  const completion = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: "you are a helpful assistant." },
      { 
        role: "user",
        content: message["sendMessage"]
      }
    ]
  })

  if (completion.choices[0].message.content) {
    res.send({ 
      "okay": true,
      "sendMessage": message['sendMessage'],
      "receivedMessage": completion.choices[0].message.content 
    });
  }
  else {
    res.send({ "receivedMessage": "error occured."})
  }
})

app.use(express.static('public'));
app.listen(3000, () => { console.log("server running.") });

dotenvについて説明しなくてはなりませんね

先程.envにはAPIキーを入れるといいましたが、まずそれをnode.js側で参照しなければなりません。
でどうやって参照するのかっていうと、まず「環境変数」ってものに.envファイルを登録し、中に記述されたキーをどのファイルでも使用できるようにする作業をします。
それを、環境変数を参照できるprocess.env.(キーの名前)を使用して、OpenAI()の引数に渡してあげます。
これが一般的なやりかたらしいです(自分もあんまわかってない。。)
しかし、プロジェクトを幾つもつくると環境変数がプロジェクトのキーだらけになって大変です。そのために、dotenvを使うと。

これを使うと、「疑似的に」環境変数に登録できるようになります。
本当に登録されたわけではないのに、本来環境変数を参照するprocess.envを.envファイル内のAPIキー参照に使えるようになります。
.envディレクトリが、あたかも実際に環境変数に登録されたかのようになるわけです。

APIのドキュメントを見るとopenai.chat.completions.create()を使ってAPIと通信を行うらしいので、その通りにしました。(詳しくはドキュメントをご覧ください。そこまで難しくなったです)
返ってきたGPTの回答を見るにはcompletion.choices[0].message.contentを使用します。

そして最後に回答があるないの条件分岐をして、fetchメソッドへの返答にres.send()で答えてやればいいと、そういうわけですね。

APIへの送信にnode.jsをはさむ利用

これは単純で、APIキーの入った.envファイルをユーザーのブラウザに渡してしまうとセキュリティ的にやばいからです。

APIキーは慎重に扱わなければなりません。安全な場所に保管するようにOpenAI側も言っているので、サーバー側保持してほくのが妥当なのでしょう。

new OpenAI()の引数にキーを直接書いてブラウザに渡せばいいので、技術的にはnode.jsなくてもできるのかも。

感想

fetchでサーバーにリクエスト送る手があったのに驚きです。

ずっとformのaction属性で’/chat/send’を指定したり、formを使ってサーバーにデータ飛ばそうとしてました。
でもそれだとres.send()で何送っても’/chat/send’にページ遷移しちゃうんですよね。
fetchで’chat/send’にデータ送って、res.send()をfetchでキャッチするなんてことができたんですね。

今回のはすごい勉強になりました!
次はデータベースと連携したやつでも作ってみようと思います。