2023年3月23日

Backlog向けChatGPT bot をLarabelで作る

投稿者: システムY

システムソリューション部のYです。
マイクロソフトが「Microsoft 365 Copilot」を発表して話題になったり、ChatGPT 流行してますね。
このようなAIは、企業の生産性にも大きなインパクトを与える可能性が高く、今後、実際の業務で活用する機会が増えてくると思います。

弊社セラフでは、プロジェクト管理にBacklog使うこと多いのですが、
課題担当者に設定すると、ChatGPTでコメントを戻してくれるbotをLarabelで作ってみました。
例えば、「ここまでの話をまとめてください」とコメントすると、要約がコメント投稿されます。
(業務で利用する場合、Azure OpenAI Service 経由 とかでしょうかね)

実装の内容

今回は、WebHookではなく、コマンドなので、Kernel.phpで定期実行させる必要があります。

        // Backlog Bot
        $schedule->command(ReplyBacklogComment::class)
            ->cron('* * * * *')
            ->withoutOverlapping()->runInBackground();

プログラム ReplyBacklogComment.php です。
課題の「件名」「課題の詳細」、投稿コメントを読み込んで、APIに渡し、得た回答をBacklogへコメント投稿してます。
参考になれば。

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use atomita\Backlog;
use atomita\BacklogException;
use Exception;
use App\Models\BacklogNotification;

/**
 * composer require しておく
 * atomita/backlog-v2
 * guzzlehttp/guzzle
 *
 * 利用テーブル
 * Schema::create('backlog_notifications', function (Blueprint $table) {
 *   $table->id();
 *   $table->timestamps();
 *   $table->bigInteger('last_notification_id')->default(0);
 *   $table->collation = 'utf8mb4_bin';
 * });
 */

class ReplyBacklogComment extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:ReplyBacklogComment';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Backlogコメントへ返信';

    private $backlog;

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {


        set_time_limit(0);

        try {

            $minId = 1;
            $res = BacklogNotification::first();
            if (!empty($res)) {
                $minId = $res->last_notification_id + 1;
            }

            // backlog初期化
            $backlogSpaceId = config('const.backlog_space_id');
            $botBacklogApiKey = config('const.bot_backlog_api_key');
            $this->backlog = new Backlog($backlogSpaceId, $botBacklogApiKey);

            // backlogお知らせ取得
            $list = $this->backlog->notifications->get([
                'minId' => $minId,
            ]);

            // 1:課題の担当者に設定
            $list = array_filter($list, fn($x) => in_array($x['reason'], [1]));

            foreach ($list as $item) {
                $this->replyComment($item['issue']);
            }

            // 重複防止
            if (!empty($list)) {
                BacklogNotification::truncate();
                $data = new BacklogNotification();
                $data->last_notification_id = $list[0]['id'];
                $data->save();
            }

            return 0;

        } catch(Exception $e) {
            $this->error($e->getMessage());
            Log::debug($e->getMessage());
            return 1;
        }

        return 0;

    }

    /**
     * openaiから得た回答で、コメント生成
     */
    public function replyComment($issue)
    {

        $botBacklogUserId = config('const.bot_backlog_user_id');
        $chatMessages = [];
        $eventText = "件名: {$issue['summary']}\n\n概要: {$issue['description']}";
        $chatMessages[] = ['role'=>'user', 'content'=>$eventText];

        try {

            $assigneeId = $issue['createdUser']['id'];

            // コメント一覧からメッセージ作成
            $minId = 0;
            for($j=0;$j<10000;$j++) {
                $comments = $this->backlog->issues->param($issue['id'])->comments->get([
                    'minId' => $minId,
                    'order' => 'asc',
                    'count' => 100,
                ]);
                if (empty($comments)){
                    break;
                }

                foreach ($comments as $item){
                    if (empty($item['content'])) {
                        continue;
                    }
                    // アシスタント(ChatGPT)側とユーザー側に振り分けます
                    if ($item['createdUser']['userId'] == $botBacklogUserId) {
                        $chatMessages[] = ['role'=>'assistant', 'content'=>$item['content']];
                    } else {
                        $chatMessages[] = ['role'=>'user', 'content'=>$item['content']];
                        $assigneeId = $item['createdUser']['id'];
                    }
                }
                $minId = $comments[sizeof($comments) - 1]['id'] + 1;
            }

    		// openai
            $client = new \GuzzleHttp\Client();
            $url = 'https://api.openai.com/v1/chat/completions';

            $token = config('const.openai_api_key');
            $headers = [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ];
            $json = [
                "model" => "gpt-3.5-turbo",
                "messages" => $chatMessages,
                "max_tokens" => 1024,
                "temperature" => 1,
            ];

            $data = $client->request('POST', $url, [
                'json' => $json,
                'headers' => $headers,
            ])->getBody()->getContents();
    	    $data = json_decode($data);

            $replyText = "回答できませんでした";
            if (!empty($data->choices) && !empty($data->choices[0])) {
                $replyText = trim($data->choices[0]->message->content);
            }

            // backlogコメントする
            $res = $this->backlog->issues->param($issue['id'])->comments->post([
                'content' => $replyText,
                'notifiedUserId[]' => $assigneeId,
            ]);

            $res = $this->backlog->issues->param($issue['id'])->patch([
                'assigneeId' => $assigneeId,
            ]);

            return;

        } catch(Exception $e) {
            Log::debug($e->getMessage());
            return;
        }
    }
}

The following two tabs change content below.
アバター

システムY

アバター

最新記事 by システムY (全て見る)