CakePHPにChatGPTのAPIを組み込んで対話型コンソールを作る【CakePHP4・OpenAI】

概要

ChatGPTのAPIを組み込んで、ChatGPTのような対話型のコンソールを実現するとします。

↓のようなフォームを作ります。

こんにちは、と入力して送信すると…

会話が成立します。(回答はChatGPTのAPIが生成したものです。)

事前準備

以下の状態を前提とします。

・OpenAIのアカウントがある

・OpenAIのAPIKeyを発行済み

・OpenAIの支払い設定済み

APIの発行手順・支払い設定の設定方法については、本記事では割愛します。

コード

バックエンド(OpenAIのAIと連携する)

OpenAIのAPIと連携する。こちらからの質問を渡して、回答をフロントエンドに返します。

SamplesController.php

   public function index(){
   }

   public function chatAPI(){
        $this->autoRender = false;
        $response = $this->response;
        $session = $this->getRequest()->getSession();

        $text = h($this->request->getData("text"));
        $user = $this->request->getAttribute('identity');

        //過去の会話の経緯を取得
        $messages = $session->read('Chat') ?? [];

        // 最新のユーザーメッセージを追加
        $messages[] = ['role' => 'user', 'content' => $text];

        // 最低限の設定を先頭に追加を追加
        array_unshift($messages,
             ["role" => "system", "content" => "日本語で話して。"],
        );

        // APIキーとURL(APIキーを仮に直書きしてますが、これはセキュリティ上望ましくありません)
        $apikey = "your-api-key";
        $url = "https://api.openai.com/v1/chat/completions";

        // リクエストヘッダー
        $headers = array(
            'Content-Type: application/json',
            'Authorization: Bearer ' . $apikey
        );

        // リクエストボディ
        $data = array(
            'model' => 'gpt-3.5-turbo',
            'messages' => $messages,
            'max_tokens' => 500,
        );

        // cURLを使用してAPIにリクエストを送信
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        $response_api = curl_exec($ch);
        curl_close($ch);

        // 結果をデコード
        $result = json_decode($response_api, true);
        $result_message = $result["choices"][0]["message"]["content"];

        // ChatGPTの回答を追加
        $messages[] = ['role' => 'assistant', 'content' => $result_message];

        // 過去3セット分の「質問→回答」だけを保持
        if (count($messages) > 6) {
            $messages = array_slice($messages, -6);
        }

        // セッションにメッセージを保存
        $session->write('Chat', $messages);

        // 結果を出力
        $response = $response->withType('application/json')->withStringBody(json_encode(['status' => '200', 'answer' => $result_message]));
        return $response;
    }

このコードは簡略化のためにAPIKeyを直書きしていますが、これはセキュリティの観点から望ましくありません。実際に運用する際は、.envを利用する等しましょう。

フロントエンド

対話コンソールのレイアウトと、バックエンドにフォームの値を送信するシステムを作ります。

index.php

<div class="wrapper">
    <div id="console">
         <div id="chat-container" class="js-chat-container">
              <div class="message assistant">
                  こんにちはAIコンソールです。なんでもお気軽にご質問ください。
              </div>
         </div>
         <div id="input-container">
              <textarea id="user-input" placeholder="AIに送信する"></textarea>
              <button id="send-button">↑</button>
         </div>
         <p id="disclaimer">AIの回答は必ずしも正しいとは限りません。重要な情報は確認するようにしてください。</p>
    </div>
</div>
<input type="hidden" name="_csrfToken" autocomplete="off" value="<?= $this->request->getAttribute('csrfToken') ?>">

<style>
#console {
    width: 100%;
    max-width: 800px;
    height:90%;
    border: 1px solid #e0e0e0;
    padding: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    background-color: #ffffff;
    border-radius: 10px;
    display: flex;
    flex-direction: column;
}

#chat-container {
    height: 100%;
    overflow-y: auto;
    margin-bottom: 10px;
    padding: 10px;
    border: 1px solid #e0e0e0;
    border-radius: 10px;
}

.message {
    padding: 10px;
    margin: 10px 0;
    border-radius: 10px;
    max-width: 95%;
    word-wrap: break-word;
}

.message.user {
    background-color: #007bff;
    color: #ffffff;
    align-self: flex-end;
    margin-left: auto; 
}

.message.assistant {
    background-color: #e9ecef;
    color: #343a40;
    align-self: flex-start;
    text-align: left;
}

#input-container {
    display: flex;
    align-items: center;
    border: 1px solid #e0e0e0;
    border-radius: 30px;
    padding: 5px;
    background-color: #ffffff;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

#user-input {
    flex: 1;
    border: none;
    padding: 10px;
    border-radius: 30px;
    outline: none;
    resize: none;
    font-size: 16px;
    background-color: #f1f1f1;
    margin-right: 10px;
}

#send-button {
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 50%;
    background-color: gray;
    color: #ffffff;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
}

#disclaimer {
    font-size: 12px;
    color: #6c757d;
    margin-top: 10px;
    text-align: center;
}
</style>

JavaScript

let answerMessageElement = '<div class="message assistant js-assistant"></div>';
let questionMessageElement = '<div class="message user js-user"></div>';

$(document).on("click", "#send-button", function () {
    let question = $("#user-input").val();

    var fd = new FormData();
    fd.append("text", question);

    $("#chat-container").append(questionMessageElement);
    $(".js-user:last").text(question);

    $.ajax("/samples/chatAPI", {
        headers: { "X-XSRF-TOKEN": $('input[name="_csrfToken"]').val() },
        beforeSend: function (xhr) {
            xhr.setRequestHeader(
                "X-CSRF-Token",
                $('input[name="_csrfToken"]').val()
            );
        },
        type: "post",
        data: fd,
        processData: false,
        contentType: false,
    })
        .then(function (data) {
            console.log(data);
            $("#chat-container").append(answerMessageElement);
            $("#user-input").val("");

            let answer = data.answer;
            $(".js-assistant:last").text(answer);
        })
        .fail(function (error) {
            console.log(error);
        });
});

コードのポイント解説:会話の経緯も含めてAPIに送る必要がある。

SamplesControllerのChatAPI()関数についてです。

12〜16行目:過去の会話の経緯の後に、最新のメッセージを追加する

//過去の会話の経緯を取得
$messages = $session->read('Chat') ?? [];

// 最新のユーザーメッセージを追加
$messages[] = ['role' => 'user', 'content' => $text];

なぜこんなことをしているの?

過去の会話の経緯をsessionから取得して追加→最新のユーザメッセージ(フォームから送られたのもの)を追加、と言う流れです。

なぜわざわざこんなことをするかというと、ChatGPTのAPIでは過去の会話の経緯を保持してくれないからです。

これをやっておかないと、AIがこれまでの経緯を含めた回答をしてくれません。

会話の経緯を保持しておかないと、会話がつながらない

会話の経緯をもとに「それ」に1を足してほしいと言ってるのですが、「それ」が何かAIに伝わっていません。

過去の会話の経緯を保持すると、会話がつながる

最新のメッセージだけではなくて、過去の会話も送ってあげることで↓のように適切に会話がつながるようになります。

55~64行目:ChatGPTの回答も会話の経緯に加える

// ChatGPTの回答を追加
$messages[] = ['role' => 'assistant', 'content' => $result_message];
 
// 過去3セット分の「質問→回答」だけを保持
if (count($messages) > 6) {
   $messages = array_slice($messages, -6);
}
 
// セッションにメッセージを保存
$session->write('Chat', $messages);

こちらからの質問だけでなく、ChatGPTのAPIからの回答もsessionに保存しています。

これも、会話の経緯をChatGPTに理解させるためです。

過去3セット分の質問・回答だけに絞っているのは、料金を抑えるためです。
※文章量に対して従量課金されます。料金については公式サイトをご参照ください。

より精密さを求めるのであれば保存する会話セット数を増やすとよいでしょう。

参考

OpenAI公式:API Reference – OpenAI API

コメント

タイトルとURLをコピーしました