Laravelを使ってOpenAIのAPIを試す

## 1. はじめに

最近、**ChatGPT**が流行ですよね。
非エンジニアの方でもChatGPTを使っていることを耳にします。

また、ChatGPTだけではなくOpenAIではAPIも公開しています。
APIではGPT4を含む数種類のAIモデルを使用できるということで、
早速試してみようと思い、Webアプリをサクッと作ってみました！

今回作成したアプリはLaravel(breeze)を使っています！
アプリの機能としては下記の簡単なものになります。

1. 画面からテキストを入力
2. 入力されたテキストをもとにOpenAIのAPIを呼び出す
3. 返ってきた結果を画面に出力

下記の画像が今回作成する画面になります。
![GPTページ回答.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/584089/70872a57-d42f-da83-3f49-f53a34782dd4.png)


## 2. 対象読者

- OpenAI のAPI を使いたい方
- Laravel を触ったことがある方
- Docker で環境を作ったことがある方

Laravel や Docker の基本的な内容に関しては省略します。

## 3. 目次
- [1. はじめに](#1-はじめに)
- [2. 対象読者](#2-対象読者)
- [3. 目次](#3-目次)
- [4. 環境](#4-環境)
- [5. 構築](#5-構築)
  - [前提](#前提)
  - [環境構築](#環境構築)
  - [Laravelプロジェクトの作成](#laravelプロジェクトの作成)
  - [breezeのインストール](#breezeのインストール)
  - [プロジェクトのセットアップ](#プロジェクトのセットアップ)
  - [サーバ起動](#サーバ起動)
- [6. 実装](#6-実装)
  - [画面の実装](#画面の実装)
  - [APIの実装](#apiの実装)
- [7. さいごに](#7-さいごに)
- [8. 参考サイト](#8-参考サイト)

## 4. 環境

- ホストPC: CentOS7.9
- 仮想環境: Docker v23.0.2
- サーバサイド: Laravel v9.19
- フロント: Laravel(blade) v9.19
- DB: MySQL v8.0

下記のdocker-compose.yml及びDockerfileで構築しています。
ローカル環境でのみ動作させる想定のため、セキュリティ対策はしておりません。

RDBMS(MySQL)はメモリを食うので、ほかの代替手段(SQLite)を使っていただいてもいいと思います。
今回作成するアプリではログイン認証でのDBを使っています。

```yaml
# docker-compose.yml

version: '3.9'

services:
  app:
    build:
      context: <Dockerfileのディレクトリパス>
    container_name: breeze_app
    volumes:
      - <Laravelプロジェクトのパス>:/var/www/html
    ports:
      - "8000:80"
    environment:
      - TZ=Asia/Tokyo
    depends_on:
      - db

  db:
    image: mysql:8.0
    container_name: breeze_db
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_DATABASE: laravel_breeze
      MYSQL_ROOT_PASSWORD: <パスワード>
      MYSQL_USER: laravel_breeze
      MYSQL_PASSWORD: <パスワード>
      TZ: Asia/Tokyo
    volumes:
      - db_data:/var/lib/mysql
    expose:
      - 3306

volumes:
  db_data:
```

```yml
# Dockerfile

FROM php:8.0-fpm

RUN apt-get update && apt-get upgrade -y && apt-get install -y unzip libzip-dev

RUN docker-php-ext-install pdo_mysql zip
RUN docker-php-ext-enable pdo_mysql zip
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-enable pdo_mysql

RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get install -y nodejs

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

ENV COMPOSER_ALLOW_SUPERUSER=1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin

RUN echo "Asia/Tokyo" > /etc/timezone
RUN dpkg-reconfigure -f noninteractive tzdata

RUN apt-get install -y locales
RUN sed -i -e 's/# ja_JP.UTF-8 UTF-8/ja_JP.UTF-8 UTF-8/' /etc/locale.gen
RUN locale-gen

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8

EXPOSE 80
```

## 5. 構築

##### 前提

- OpenAIにてアカウントが作成済み
- OpenAIにて**SECRET KEY**が発行済み

##### 環境構築

上記のdocker-compose.yml及びDockerfileを作成後、下記を実行します。

```bash
$ docker-compose up -d --build
```

ビルド終了後、下記のコマンドでコンテナが起動していることを確認します。

```bash
$ docker ps
```

##### Laravelプロジェクトの作成

今回はComposerの`create-project`というコマンドを実行してプロジェクトを作成します。

```bash
$ docker exec -it breeze_app bash

# コンテナの中に入る

$ composer global require laravel/installer
$ composer create-project --prefer-dist laravel/laravel .
$ chmod -R a+w storage bootstrap/cache public

```

##### breezeのインストール

上記のコンテナに入った状態で下記を実行します。

```bash
$ composer require laravel/breeze --dev
$ php artisan breeze:install
(Breezeのインストール方法が選択できるので「blade」を選択する)
```

##### プロジェクトのセットアップ

LaravelからDBにアクセスできるように`.env`の下記項目を編集します。

```shell
# 編集
DB_HOST=db
DB_DATABASE=laravel_breeze
DB_USERNAME=laravel_breeze
DB_PASSWORD=<パスワード>

# 追記
OPEN_AI_API_KEY=<OpenAIにて発行したSECRET KEY>
```

その後、下記のコマンドをコンテナに入った状態で実行します。

```bash
$ php artisan migrate
$ npm install
```

##### サーバ起動

下記のコマンドを実行し、ブラウザから`http://<サーバのホスト>:8000`にアクセスするとLaravelの画面が表示されます。
Breezeではログイン機能は用意されています。詳しくは[スターターキット 9.x Laravel](https://readouble.com/laravel/9.x/ja/starter-kits.html#laravel-breeze)を参照してください。

```bash
$ php artisan serve --host 0.0.0.0 --port 80
```

## 6. 実装

##### 画面の実装

画面側のコードにおいて、新規作成及び変更するファイルは下記になります。
コードのアーキテクトは考えずに作成していますのでご了承ください。

```
resources/
  ├── views/
      ├── layouts/
      │   └── navigation.blade.php (変更)
      ├── open_ai/ (以下すべて新規作成)
          ├── partials/
          │   └── chat-gpt-form.blade.php 
          └── gpt.blade.php
```

- navigation.blade.php
```html
<!-- 既存コード -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
        {{ __('Dashboard') }}
    </x-nav-link>
    <!-- 追記 -->
    <x-nav-link :href="route('open_ai.gpt')" :active="request()->routeIs('gpt')">
        {{ __('GPT') }}
    </x-nav-link>
</div>
<!-- 既存コード -->
```

- chat-gpt-form.blade.php
```html
<section>
    <header>
        <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
            {{ __('ChatGPTの入力フォーム') }}
        </h2>

        <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
            {{ __("質問事項を入力して送信ボタンを押してください。") }}
        </p>
    </header>

    <form id="gptForm" method="post" action="{{ route('open_ai.send') }}" class="mt-6 space-y-6">
        @csrf

        <div>
            <x-input-label for="question" :value="__('質問入力欄')" />
            <textarea class="mt-1 block w-full" rows="5" name="question" required>{{old('question', '')}}</textarea>
            <x-input-error class="mt-2" :messages="$errors->get('question')" />
        </div>

        <div>
            <x-input-label for="answer" :value="__('回答出力欄')" />
            @if(session('answer'))
                <textarea id="answerTextArea" class="mt-1 block w-full" rows="5" name="answer" oninput="autoResizeTextarea()" disabled>{{ session('answer') }}</textarea>
            @else
                <textarea id="answerTextArea" class="mt-1 block w-full" rows="5" name="answer" disabled></textarea>
            @endIf
            <x-input-error class="mt-2" :messages="$errors->get('answer')" />
        </div>

        <div class="flex items-center gap-4">
            <x-primary-button>{{ __('送信') }}</x-primary-button>

             @if(session('answer'))
                <p
                    x-data="{ show: true }"
                    x-show="show"
                    class="text-sm text-gray-600 dark:text-gray-400"
                >{{ __('回答欄に出力しました。') }}</p>
            @endif
        </div>
    </form>
</section>

<script>
function autoResizeTextarea() {
    const textarea = document.getElementById('answerTextArea');
    textarea.style.height = 'auto';
    textarea.style.height = (textarea.scrollHeight) + 'px';
}

document.addEventListener('DOMContentLoaded', () => {
    autoResizeTextarea();
});
</script>
```

- gpt.blade.php
```html
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
            {{ __('GPTお試し') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
            <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
                <div class="max-w-xl">
                    @include('open_ai.partials.chat-gpt-form')
                </div>
            </div>
        </div>
    </div>
</x-app-layout>
```

##### APIの実装

API側のコードにおいて、新規作成及び変更するファイルは下記になります。
コードのアーキテクトは考えずに作成していますのでご了承ください。

```
app/
  ├── Http/
      ├── Controllers/
          └── GptController.php (新規作成)

config/
  └── services.php (変更)

routes/
  └── web.php (変更)
```

- web.php

```php
// 下記を追記
Route::middleware('auth')->group(function () {
    Route::get('/gpt', [GptController::class, 'index'])->name('open_ai.gpt');
    Route::post('/gpt', [GptController::class, 'send'])->name('open_ai.send');
});
```

- services.php

```php
// 下記を追記
'open_ai' => [
    'api_key' => env('OPEN_AI_API_KEY', ''),
],
```

- GptController.php

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Http;
use Illuminate\View\View;

class GptController extends Controller
{
    /**
     * ページの表示
     * 
     * @param  Request  $request
     * @return View
     */
    public function index(Request $request): View
    {
        return view('open_ai.gpt', [
            'user' => $request->user(),
        ]);
    }

    /**
     * chatGPTのAPI実行
     * 
     * @param  Request  $request
     * @return View
     */
    public function send(Request $request): RedirectResponse
    {

        $url = "https://api.openai.com/v1/chat/completions";

        $apiKey = config('services.open_ai.api_key');
        $headers = array(
            "Content-Type" => "application/json",
            "Authorization" => "Bearer $apiKey"
        );

        $data = array(
            "model" => "gpt-3.5-turbo",
            "messages" => [
                [
                    "role" => "user",
                    "content" => $request->question,
                ]
            ]
        );

        $response = Http::withHeaders($headers)->timeout(60)->post($url, $data);
        if ($response->json('error')) {
            info('エラーが発生');
        }

        return Redirect::route('open_ai.gpt')
            ->with('answer', $response->json('choices')[0]['message']['content'];)
            ->withInput();
    }
}
```

以上にて、実装は完了です！

ブラウザでログイン後、GPTページにて動作確認することができると思います。
![GPTページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/584089/d29dc545-9390-7176-92a6-6a06852a9d3c.png)


## 7. さいごに

いかがでしたか？
私は今回のアプリを作るためにChatGPTを何度も使いました。

今回は最低限の機能しかないため使いにくい点はたくさんあると思います。
例えば、モデルの選択、リクエストパラメータの調整、ローディング画面など
これをベースに色々と開発してみてください！

また、docker環境を作成しましたが、
Laravelの**Sail**を使えば簡単にDocker環境が構築できることを後日知りました。。

OpenAI のAPIは従量課金制ですが、アカウントを作成すると\\$18までは無料で使えます。(2023/03時点)
これまで何度も使っていますが、まだ\\$1も使えていません。
\\$18の枠内でたくさん使えると思えますので、どんどん遊んでみましょう！

## 8. 参考サイト

- [OpenAI API reference](https://platform.openai.com/docs/api-reference/introduction)
- [スターターキット 9.x Laravel](https://readouble.com/laravel/9.x/ja/starter-kits.html#laravel-breeze)