ChatGPT API で日報から社員のストレスレベルを判別する

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、この間、News API + ChatGPT で1日のニュースを要約してメール送信する という記事を公開しましたが、その際に10ドル課金しました。

そして、せっかく課金したんだから他にも何か作ってみようと思っていたところ、過去にAWSで実装したある機能を思い出しました。

それは・・・・・・

日報からスタッフの「ストレスレベル」を判別する

です。

📝 参考ページ: Laravel + Amazon Comprehend で従業員の感情を日報から判別する

というのも、前回はChatGPTが存在しておらず、AWSの感情分析APIを使いましたが、AIが進化した今となっては「いや、それ ChatGPT でいけるんじゃない!?」と思ったからなんですね。

そこで❗

今回はLaravelを使ってフォームから送信された日報をChatGPT APIに投げてスタッフのストレスレベルを解析し、上司に通知をするという機能を実装してみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😊✨

「私のストレス発散は
遠出して、その土地の
クラフトビールを飲むことです🍺」

開発環境: Laravel 10.x、PHP 8.2

前提として

今回はChatGPT APIを使いますので、(普通のChatGPTとは別に)Open APIへ登録&課金してAPIキーを用意しておく必要があります。

やり方は以下のURLを参考にしてみてください。

📝 参考ページ: OpenAI の API キーを取得する

また、Laravel breezeでログイン機能がインストールされ、ログインできるユーザーがすでに登録されていることが前提です。

では、準備が整ったら実際にやっていきましょう❗

パッケージをインストールする

まずはじめにOpenAPIAPIにアクセスするためのパッケージが用意されているので、以下のページを参考にしてインストールしておいてください。

📝参考ページ: パッケージをインストールする

送信フォームをつくる

次に、日報を送信するフォームをつくります。

ちなみに、Laravel 10.23からはphp artisan make:viewが使えます!
せっかくなので、今回から使っていきます。(というか、なぜ今まで作らなかったのでしょうね…🤔)

以下のコマンドを実行してください。

php artisan make:view daily_report.create

すると、ビューが作成されるので、以下のコードをセットしてください。

resources/views/daily_report/create.blade.php

<html>
<head>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>

    @if (session('status') === 'success')
        <div class="bg-green-500 p-4 rounded-lg mb-6 text-white text-center">
            日報の送信が完了しました。
        </div>
    @endif

    <form method="POST" action="{{ route('daily_report.store') }}" class="p-6 bg-white rounded-lg shadow-lg w-full max-w-md mx-auto">
        @csrf
        <div class="mb-4">
            <label for="name" class="block text-sm font-medium text-gray-600">名前:</label>
            <span id="name" class="text-lg font-semibold">{{ $user->name }}</span>
        </div>

        <div class="mb-4">
            <label for="date" class="block text-sm font-medium text-gray-600">日付:</label>
            <input type="date" name="date" value="{{ old('date') }}" class="p-2 rounded border focus:border-indigo-500 w-full">
            @error('date')
            <div class="text-red-500">{{ $message }}</div>
            @enderror
        </div>

        <div class="mb-4">
            <label for="report_text" class="block text-sm font-medium text-gray-600">日報内容:</label>
            <textarea name="report_text" rows="4" class="p-2 rounded border focus:border-indigo-500 w-full">{{ old('report_text') }}</textarea>
            @error('report_text')
            <div class="text-red-500">{{ $message }}</div>
            @enderror
        </div>

        <div class="flex justify-end">
            <button id="submit_button" type="submit" class="py-2 px-4 bg-indigo-500 text-white rounded hover:bg-indigo-600 focus:outline-none focus:border-indigo-700 focus:ring focus:ring-indigo-200">
                送信
            </button>
        </div>
    </form>

    <script>

        window.onload = () => {

            const submitButton = document.getElementById('submit_button');
            submitButton.addEventListener('click', e => {

                if(! confirm('送信してよろしいですか?')) {

                    e.preventDefault();

                }

            });

        }

    </script>

</body>
</html>

コントローラーをつくる

では、メインのコントローラー部分をつくっていきましょう。
以下のコマンドを実行してください。

php artisan make:controller DailyReportController

すると、ファイルが作成されるので中身を以下のようにします。

app/Http/Controllers/DailyReportController.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\DailyReportRequest;
use App\Mail\DailyReportPosted;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use OpenAI\Laravel\Facades\OpenAI;

class DailyReportController extends Controller
{
    public function create(Request $request)
    {
        $user = $request->user();

        return view('daily_report.create')->with([
            'user' => $user,
        ]);
    }

    public function store(DailyReportRequest $request)
    {
        $user = $request->user();
        $report_text = $request->report_text;
        $date = $request->date;

        // ChatGPT API でストレスレベルを判別
        $stress_level = $this->getStressLevel($report_text);

        // 実際にはここで DB などへ保存(今回は省略)

        $to = 'admin@example.com';
        Mail::to($to)->send(new DailyReportPosted(
            $user,
            $stress_level,
            Carbon::parse($date),
        ));

        return back()->with('status', 'success');
    }

    private function getStressLevel($text)
    {
        $stress_level = -1;
        $prompt = view('daily_report.prompt', [
            'text' => $text
        ])->render();

        try {

            $result = OpenAI::chat()->create([
                'model' => 'gpt-3.5-turbo',
                'messages' => [
                    ['role' => 'user', 'content' => $prompt],
                ],
            ]);
            $json_text = Arr::get($result, 'choices.0.message.content');
            $stress_level = json_decode($json_text, true)['result'];

        } catch (\Throwable $th) {

            $this->error($th->getMessage());
            throw $th;

        }

        return $stress_level;
    }
}

なお、ここで重要なのがgetStressLevel()の中にある、ChatGPT APIからデータが返ってくるところです。

後で紹介しますが、プロンプトに「JSONだけで回答してください」と指定します。つまり、取得できる回答はJSON形式になっているので、json_encode()で配列化しないといけないというわけですね。

バリデーションをつくる

では、先ほど作ったコントローラーの中で使っていたバリデーション(FormRequest)をつくりましょう。

以下のコマンドを実行してください。

php artisan make:request DailyReportRequest

すると、ファイルが作成されるので中身を次のように変更します。

app/Http/Requests/DailyReportRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class DailyReportRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
     */
    public function rules(): array
    {
        return [
            'date' => ['required', 'date'],
            'report_text' => ['required', 'string', 'min:200', 'max:1000'],
        ];
    }
}

プロンプト用のテンプレートをつくる

これもDailyReportControllerでセットしたテンプレート(Blade)です。

※ ちなみに、なぜプロンプトのためにBladeを使っているかと言うと「コードに書くよりスッキリするから」です。後から見てわかりやすいコードはきっと喜ばれると思います👍

以下のコマンドを実行してください。

php artisan make:view daily_report.prompt

するとファイルが作成されるので中身を以下のようにします。

resources/views/daily_report/prompt.blade.php

あなたはとても優秀な心理カウンセラーとして回答してください。
以下の日報からスタッフのストレスレベルを判別してください。
なお、評価は0(最も良い)<->100(最も悪い)で、出力は絶対にJSONのみにしてください。余計な文章は一切不要です!
また、英語で考えてください。

#日報
{{ $text }}

#JSON
{"result":  0 to 100}

ちなみに、なぜ「英語で考えてください」とわざわざ付け加えているかというと、その方が精度が高くなるから(個人的な意見です)です。

やはり、ChatGPTは基本的に英語を使っているので、こう書くことで日本語を一旦英語にしてから考えてくれるんじゃないかと推測しています。

通知用の Mailable をつくる

では、メール通知用のMailableをつくります。
以下のコマンドを実行してください。

php artisan make:mail DailyReportPosted

すると、ファイルが作成されるので中身を以下のようにします。

app/Mail/DailyReportPosted.php

<?php

namespace App\Mail;

use App\Models\User;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class DailyReportPosted extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     */
    public function __construct(private User $user, private int $stress_level, private Carbon $dt)
    {}

    /**
     * Get the message envelope.
     */
    public function envelope(): Envelope
    {
        $subject = $this->user->name .'さんが「'. $this->dt->format('Y年m月d日') .'」の日報を投稿しました。';

        if($this->stress_level > 70) { // ストレスレベルが 70 以上の場合は緊急

            $subject = '【緊急】'. $subject;

        }

        return new Envelope(
            subject: $subject,
        );
    }

    /**
     * Get the message content definition.
     */
    public function content(): Content
    {
        return new Content(
            view: 'emails.daily_report_posted',
            with: [
                'user' => $this->user,
                'stress_level' => $this->stress_level,
                'dt' => $this->dt,
            ],
        );
    }

    /**
     * Get the attachments for the message.
     *
     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
     */
    public function attachments(): array
    {
        return [];
    }
}

そして、ビューです。

php artisan make:view emails.daily_report_posted

resources/views/emails/daily_report_posted.blade.php

{{ $user->name }} さんが「<strong>{{ $dt->format('Y年m月d日') }}</strong>」の日報を投稿しました。<br><br>

ストレスレベル: {{ $stress_level }} です。<br>
※ 0(良い)↔ 100(悪い)<br><br>

<a href="{{ url('/') }}">日報の内容を確認する(今回は省略)</a>

ルートをつくる

では、最後にルートです。

routes/web.php

use App\Http\Controllers\DailyReportController;

// 省略

Route::prefix('daily_report')->middleware('auth')->controller(DailyReportController::class)->group(function(){

    Route::get('create', 'create')->name('daily_report.create');
    Route::post('store', 'store')->name('daily_report.store');

});

テストしてみる

では、実際にテストしてみましょう❗

※ メールテストはmailcatcherを使っています。

日報は以下3つ(良い、悪い、可もなく不可もなく)の内容を用意しました。

(良い内容)
今日は非常に生産的な一日でした。午前中にプロジェクトXのフェーズ1が無事に完了し、その後のレビューでも特に大きな問題点は見つかりませんでした。チームメンバー全員が高いモチベーションと集中力で作業を進め、結果として予定よりも早くフェーズ1を終えることができました。午後には早めにフェーズ2の計画を立て、明日に備える時間も確保することができました。また、今週の金曜日に控えているクライアントミーティングの資料もほぼ完成し、余裕をもって最終確認に入れそうです。

(悪い内容)
今日は非常に厳しい一日でした。プロジェクトYにおいて、多数の想定外のバグと技術的な障壁に直面しました。特に、データベースのパフォーマンスが低下し、それが他のタスクにも影響を与えています。チームメンバーはフラストレーションが溜まっており、その影響で予定が大きく遅れています。明日緊急会議を設けて対策を練る必要があります。さらに、来週のクライアントプレゼンテーションの資料作成も停滞している状態です。このままでは納期を守るのが困難になる可能性が高まっています。

(可もなく不可もなくの内容)
今日はプロジェクトZに集中しました。午前中には進捗状況をチームで確認し、各メンバーのタスクリストを更新しました。いくつかの問題はありましたが、特に深刻な障害は確認されませんでした。午後には、新しいフェーズの設計に移りました。設計の方針についてはチーム内で意見が分かれましたが、最終的には中間のアプローチで合意しました。また、今週末に予定されているクライアントミーティングに向けて、プレゼンテーションのアウトラインを作成しました。進捗状況に大きな遅れはなく、順調に進行しています。

では、ブラウザで「https://******/daily_report/create」へアクセスします。

日報のフォームが表示されました。

では、まずは「良い内容」を送信してみます。

すると・・・・・・

はい❗
メールが送信されてきて、ストレスレベルは「0」となりました。

うまく判別できているようですね。
では次に「悪い内容」です。

どうなるでしょうか・・・・・・

はい❗
ストレスレベルは「80」と判別され、さらにメール件名に「緊急」がつきました。

いいですね👍

では、最後に「可もなく不可もなくの内容」を送信してみます。

うまくいくでしょうか・・・・・・

はい❗ストレスレベルは「20」です。
こちらも妥当な感じでストレスレベルを判別できているようです。

成功です😊✨

やっぱりChatGPTはすごいですね(しかもChatGPT 3.5でこの精度です!)

企業様へのご提案

今回のように、ChatGPT APIを使うとそれまで専用のサービスを使わざるを得なかったものを代替できるようになります。

また、もちろん専門家と比べると正確な判別ができるわけではないかもしれませんが、依頼する費用は軽減できる可能性はあります。

※ なお、今回は「gpt-3.5-turbo」版を使っていますが、「gpt-4」も利用可能なので、より精度の高い判別ができるようになるでしょう。(APIの料金は こちら

もしそういった機能をご希望でしたらいつでもお気軽にお問い合わせからご相談ください。

お待ちしております。😊✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravel + ChatGPT APIで「ストレスレベルの判別」を行ってみました。

正直なところ、ChatGPTといえども人の感情を読み取ることは難しいんじゃないかとも思っていたのですが、想像以上に精度が高くてビックリしました❗

しかも、GPT-5が出てくるともウワサされているので今後はより精度が高くなるのでしょうね。

世間的には生成AIの熱みたいなものは冷めてきているようですが、使い方さえ覚えればよりよい結果につながるんだなとよく思います。

ではでは〜❗

「生姜&白だしをプラスした
レトルトカレーうどん
バリウマです😆✨」

このエントリーをはてなブックマークに追加       follow us in feedly