ChatGPTのAPIを使用して会話をする(React、PHP)

# はじめに
今回オリジナルプロダクトの機能の一つとして、ChatGPTから五つの質問をしてもらい、その回答をChatGPTが解析することで、現在の感情を数値化してもらう。
といった機能を作成したので、そのコードを見ながら、どのように実装したかを振り返ろうと思います。

**注意**：
自分の復習と、これからAPIを使用したい人にわかりやすく伝えようとコードを載せましたが、このコードは実際に使用できてはいますが、まだデプロイもしていない段階なので、セキュリティの考慮などのリファクタリングがされていません。

そのことを念頭に置いていただければ幸いです。

# 開発環境
- LaravelにViteを使用してフロントエンドはReact
- バックエンドはPHP
- Visual Studio Code使用

# 準備
- [公式サイト](https://platform.openai.com/api-keys)を参考にしてアカウントとAPIキーを取得
- Laravelを作成して、.envファイルにAPIキーを環境変数として記述

# コード
まず最初はReactのフロントエンドから説明します。
実際のコードを見ていきましょう。

# Chat.jsx

## sendInitialMessage
```React:Chat.jsx
import React, { useState, useEffect } from "react";
import Layout from "./Layout";
import styles from "../../css/chat.module.css";

function Chat() {
    const [messageInput, setMessageInput] = useState("");
    const [conversationHistory, setConversationHistory] = useState([]);
    const [chatDisplay, setChatDisplay] = useState([]);

    useEffect(() => {
        sendInitialMessage();
    }, []);

    const sendInitialMessage = async () => {
        try {
            const initialSystemMessage = {
                role: "system",
                content:
                    "あなたがユーザーに対して五つの質問を一つ一つしてください。
                    あなたが五つ質問をしてユーザーが五つ回答したら、それまでのユーザーの回答を振り返り解析して、
                    四つの感情である、興奮、不安、悲しみ、楽しみ、を四つ全部で100としてそれぞれの現在の感情を数値で表してください。
                    侍口調で会話してください",
            };
            const updatedHistory = [
                ...conversationHistory,
                initialSystemMessage,
            ];
            setConversationHistory(updatedHistory);
            await sendMessageToAPI(updatedHistory);
        } catch (error) {
            console.error("Error:", error);
        }
    };
```

まず上記のコードはコンポーネントを切り替えたときに、useEffectにより最初に行われるsendInitialMessageです。

この非同期関数は会話履歴に初期のシステムメッセージを追加し、その更新された履歴をAPIに送信する役割を持っています。

## conversationHistory
作成したstate変数「conversationHistory」は全ての会話を記録する配列です。

**ChatGPTと連続性のある会話をするには、ユーザーの返答だけでなく、過去の会話を送信する必要**があります。

セッションやデータベースによる管理などもできるそうですが、自分は一番簡単だと思った配列による保存を選択しました。

この配列一つ一つはメッセージオブジェクトとして以下のフィールドが入っています。

### role
**roleフィールド**は、**メッセージの発信元を指定**します。主に以下の3つの値を取ります。

- **system**
ChatGPTモデルの振る舞いを制御するために使用されます。例えば、会話のトーンやルールを設定するために使います。
今回は質問形式や感情の数値化、侍口調を指定しています。
- **assistant**
アシスタントからの応答を示します。これはChatGPTが生成した応答を表します。
- **user**
ユーザーからのメッセージを示します。これはユーザーがアシスタントに対して行う質問や指示です。

### content
**contentフィールド**は、**メッセージのテキスト内容を指定**します。これは会話の実際のメッセージそのもので、ユーザーの質問やアシスタントの応答を含みます。

上記の内容をまずsetConversationHistory()で配列に追加し、**sendMessageToAPI**()でAPI通信を行います。



## sendMessageToAPI
```React:Chat.jsx
const sendMessageToAPI = async (messages) => {
        try {
            const response = await fetch("/api/chat", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({ messages }),
            });

            if (!response.ok) {
                console.error("Response Error:", await response.text());
                throw new Error("Network response was not ok");
            }

            const data = await response.json();
            const newMessage = {
                role: "assistant",
                content: data.choices[0].message.content,
            };
            const updatedHistory = [...messages, newMessage];
            setConversationHistory(updatedHistory);
            updateChatDisplay(newMessage.content);
        } catch (error) {
            console.error("Error:", error);
        }
    };
```
この関数は、メッセージの配列をfetchによりコントローラに対してPOSTリクエストします。
コントローラからChatGPTAPIのレスポンスを受け取り、処理をして、会話履歴を更新。
更にchatDisplayを更新することで表示する会話も追加する役割を持っています。

では次はAPI通信を行っているバックエンドの方を見てみましょう。

# ChatController
PHPで作成したコントローラです。
```PHP:ChatController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;


class ChatController extends Controller
{

    public function sendMessage(Request $request)
    {
        $userMessage = ['role' => 'user', 'content' => $request->input('message')];
        $messages[] = $userMessage;

        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . env('OPENAI_API_KEY'),
            'Content-Type' => 'application/json'

        ])->post('https://api.openai.com/v1/chat/completions', [
            'model' => 'gpt-4o',
            'messages' => $request->input('messages')
        ]);

        if ($response->failed()) {
            return response()->json([
                'error' => 'API request failed',
                'details' => $response->json()
            ], 500);
        }

        $apiResponse = $response->json();
        $botMessage = ['role' => 'assistant', 'content' => $apiResponse['choices'][0]['message']['content']];
        $messages[] = $botMessage;

        return response()->json($apiResponse);
    }
}

```
このコントローラでは、まずHTTPリクエストからユーザーのメッセージを取得します。

そのメッセージをChatGPT APIのフォーマットに変換し、$messagesという配列に追加します。

### ChatGPTへのリクエストの送信
```PHP:ChatController
$response = Http::withHeaders([
    'Authorization' => 'Bearer ' . env('OPENAI_API_KEY'),
    'Content-Type' => 'application/json'
])->post('https://api.openai.com/v1/chat/completions', [
    'model' => 'gpt-4',
    'messages' => $request->input('messages')
]);
```
リクエストヘッダーを設定します。ここには、APIキーとコンテンツタイプが含まれます。

最初に設定した.envファイルの環境変数からAPIキーを取得して、Authorizationヘッダーに設定します。

ChatGPT APIのエンドポイントにPOSTリクエストを送信します。リクエストボディには、モデル名とメッセージリストが含まれます。

### APIレスポンスの処理
```PHP:ChatController
$apiResponse = $response->json();
$botMessage = ['role' => 'assistant', 'content' => $apiResponse['choices'][0]['message']['content']];
$messages[] = $botMessage;

return response()->json($apiResponse);
```

レスポンスボディをJSON形式で取得します。
APIからのレスポンスを取得し、ChatGPTのフォーマットに変換した後、レスポンスをメッセージ配列に追加します。
その後、先ほどのフロントエンドへレスポンスをJSON形式で返します。

表示の内容を省略した説明ですが、以上がChatGPTとの会話をするために使用したコードです。

**では実際に会話をしてみましょう**。

## 会話の実践
```React:conversation History:
 0: 
{role: 'system', content: 'あなたがユーザーに対して五つの質問を一つ一つしてください。
あなたが五つ質問をしてユーザーが五つ回答し…しみ、を四つ全部で100としてそれぞれの現在の感情を数値で表してください。
侍口調で会話してください'}
1: 
{role: 'assistant', content: '承知した。ではまず一つ目の質問をいたす。
そなた、最近の趣味や楽しみと感じることは何でござろうか？'}
2: 
{role: 'user', content: 'ランニングです'}
3: 
{role: 'assistant', content: 'なるほど、ランニングとは健やかさを保つ素晴らしき趣味でござるな。
では二つ目の質問をいたす。最近の生活において、何か心配事や不安に感じていることはござるか？'}
4: 
{role: 'user', content: 'プロダクトの作成に間に合わなさそうなことです'}
5: 
{role: 'assistant', content: '成程、プロダクトの作成に対する不安を抱えておるとは、
責任感の強きを感じる。次に三つ目の質問をいたす。そなたが最近、何か悲しみを感じた出来事はござったか？'}
6: 
{role: 'user', content: 'ログイン機能が作れないことです'}
7: 
{role: 'assistant', content: '拙者、その技術の壁に悩むこと、実に辛きことと思う。
努力してもなかなか成果が出ぬことは、誰しも経験する…ござる。では四つ目の質問に参り申す。
最近、心から楽しんだり、嬉しさを感じた出来事は何かござったか？'}
8: 
{role: 'user', content: 'ChatGPTのAPIを利用した会話ができるようになったときは嬉しかったです'}
9: 
{role: 'assistant', content: 'なるほど、それは大いなる達成感と喜びを感じたのじゃな。
進展すること、己の手で何かを成し遂げることは喜…ござる。さて、最後の質問といたす。
そなた、今後の目標や夢はござるか？それが何かあれば教えてたもれ。'}
10: 
{role: 'user', content: 'プロダクトをポートフォリオにし、エンジニアとして就職することです'}
11: 
{role: 'assistant', content:'なるほど、プロダクトを完成させ、エンジニアとして新たな道を開くことが目標とな。
壮大な夢であり、力強き意志を感じる言葉でござる。では、これまでの回答を基にそなたの感情を解析いたす。
\n\n- **興奮**: ランニングやChatGPTのAPIの活用に成功した際の達成感から、20と見受けられる。
\n- **不安**: プロダクト作成に間に合わないことへの不安、ログイン機能の難しさから、35と見受けられる。
\n- **悲しみ**: ログイン機能が思うように作れなかったことから、生じた悔しさから、25と見受けられる。
\n- **楽しみ**: ランニングの楽しさや、ChatGPT APIの利用成功、目標に向かう楽しみから、20と見受けられる。
\n\n以上のように見受けられるが、いかがでござろうか？何か追加のコメントや意見があれば、遠慮なく申すがよい。'}
```
上記が最後まで会話をしたconversationHistory配列の中身です。

気遣いの素晴らしい侍が出てくれました。会話も問題なく繋がっているようです。
# 最後に
今回は現在作成中のプロダクトのコードを使用して、大まかですがChatGPTAPIを使用したコードの書き方をまとめました。

この記事を書く少し前にGPT-4oのモデルが使用できるようになり、APIの方でも使ってみたところ、すぐに分かるほどにレスポンスが早く、正確な会話をしてくれるようになりました。

この機能を作成する際、参考にした記事や公式ドキュメントではPythonを利用したものが多く、ReactやPHPで書いた記事を探しても見つからず苦労しました。
同じ思いをしている人が、この記事を見て手助けになれたら嬉しいです。