0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel DeepSeek api の実装

Posted at

生成AIの一つであるDeepSeekのapiを用いて、このようにチャンピオン名(lol)のガイドを作ってくれるようにした。

この記事は前回の記事(Laravel Webスクレイピング機能の搭載 ②)
の続きである。

ビュー部分の作成

<!-- 検索フォームをスタイリング -->
    <div class="max-w-7xl mx-auto px-6 mt-4">
        <form id="championSearchForm" class="flex gap-2 mb-6">
            <input type="text" id="championInput" name="query" placeholder="チャンピオン名を入力"
                   class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
            <button type="submit" class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-600">
                検索
            </button>
        </form>
    </div>

まず検索フォームがトリガーとなってくるので再度こちらを見てみる。
ここでは検索フォームがid="championSearchForm"と定義されており、また入力された情報はid="championInput"と定義されていた。

<!-- 既存のテーブルの下に追加 -->
<!-- DeepSeek APIによるチャンピオンガイド -->
<!-- 既存のテーブルの下に追加 -->
<div class="max-w-6xl mx-auto px-14 mt-40 mb-20">
    <div class="bg-white rounded-lg shadow-md p-6">
        <h2 class="text-2xl font-bold text-gray-800 mb-4" id="guideTitle">ガレンの戦い方ガイド</h2>
        <div id="championGuide" class="prose max-w-none">
            <p class="text-gray-500">チャンピオンを検索するとガイドが表示されます</p>
        </div>
    </div>
</div>

こちらがDeepSeek apiを導入する箇所のhtml部分である。デフォルトで「チャンピオンを検索するとガイドが表示されます」と書かれているが、先ほどの検索フォームにsubmitつまり何かが検索されると、以下のJavaScriptの通りに実行されるシステムだ。

<script>
document.addEventListener('DOMContentLoaded', function() {
    const guideElement = document.getElementById('championGuide');
    const searchForm = document.getElementById('championSearchForm');
    const guideTitle = document.getElementById('guideTitle');

    searchForm.addEventListener('submit', async function(e) {
        e.preventDefault();
        const champion = document.getElementById('championInput').value.trim();
        
        if (!champion) return;

        guideElement.innerHTML = '<p class="text-blue-500">読み込み中...</p>';
        guideTitle.textContent = `${champion}の戦い方ガイド`;
        
        try {
            const response = await fetch(`/champion-guide?champion=${encodeURIComponent(champion)}`);
            const guide = await response.text();
            
            // 改行を<br>タグに変換して表示
            guideElement.innerHTML = guide.split('\n')
                .map(line => `<p>${line}</p>`)
                .join('');
        } catch (error) {
            guideElement.innerHTML = `<p class="text-red-500">エラー: ${error.message}</p>`;
        }
    });
});
</script>

async function(e) { e.preventDefault(); }

async function(e)
これは非同期関数 (async function) で、引数 e はイベントオブジェクトを受け取ります。

例えば、click や submit イベントで発生した情報を e に格納します。

e.preventDefault();
e に格納されているイベントのデフォルトの動作を止めます。

フォームの submit イベントでは、デフォルトではページがリロードされますが、これを防ぎます。

さて、

try {
            const response = await fetch(`/champion-guide?champion=${encodeURIComponent(champion)}`);
            const guide = await response.text();
            
            // 改行を<br>タグに変換して表示
            guideElement.innerHTML = guide.split('\n')
                .map(line => `<p>${line}</p>`)
                .join('');
        } 

ここの部分が非常に重要だ。

const response = await fetch("/champion-guide?champion=${encodeURIComponent(champion)}");

ここでルーターで/champion-guideと名前定義されているコントローラーのメソッドの返り値を変数responseに格納する。
このとき、championを変数として送ります。(コントローラーの話はあとで)

const guide = await response.text();

response.text() でレスポンスを プレーンテキスト として取得。
JSON なら response.json() を使うが、ここでは単なるテキストなので text() を使用。

guideElement.innerHTML = guide.split('\n').map(line => "<p>${line}</p>").join('');

guide.split('\n') で、テキストの改行 \n を基準に分割 → 配列にする。
またそれぞれの行を<p>タグで囲む。.joinの部分で配列を結合して、HTML に返す。

さて、コントローラー部分を見ていく。

コントローラーの作成

さて先ほどのfetch関数で述べた部分だが、ルーターに以下が定義されている。

Route::get('/champion-guide', [ChampionController::class, 'getGuide']);

したがってresponseの値はChampionControllerのgetGuideメソッドの返り値だ。
getGuide関数は以下のように定義されている。

public function getGuide(Request $request)
    {
        $champion = $request->query('champion', 'ガレン');
        $apiKey = env('OPENROUTER_API_KEY');
        
        try {
            $response = Http::withHeaders([
                'Authorization' => 'Bearer ' . $apiKey,
                'HTTP-Referer' => env('APP_URL', 'http://localhost'),
                'X-Title' => 'LoL Champion Guide',
                'Content-Type' => 'application/json',
            ])->post('https://openrouter.ai/api/v1/chat/completions', [
                'model' => 'anthropic/claude-3-haiku',
                'messages' => [
                    [
                        'role' => 'user',
                        'content' => "{$champion}のリーグ・オブ・レジェンドのチャンピオンガイドを、初心者向けに詳細かつ具体的に作成してください。"
                    ]
                ]
            ]);

            $guide = $response->json();

            // JSONからガイドの内容を抽出
            $guideContent = $guide['choices'][0]['message']['content'] ?? 'ガイドが見つかりませんでした。';

            // レスポンスを直接返す(HTMLエスケープ)
            return response($guideContent)
                ->header('Content-Type', 'text/plain; charset=UTF-8');

        } catch (\Exception $e) {
            Log::error('API Request Failed', [
                'error' => $e->getMessage()
            ]);
            
            return response('エラーが発生しました: ' . $e->getMessage(), 500);
        }
    }

$apiKey = env('OPENROUTER_API_KEY');ここで$apikeyにdeepseek apiの公式サイトで取得したキーを定義している。無料なので各自調べてほしい。簡単。

.envでキーを貼っておこう。

OPENROUTER_API_KEY=sk-or-v1-長い文字列
'Authorization' => 'Bearer ' . $apiKey,

APIキーを設定(認証に必要)。

'HTTP-Referer' => env('APP_URL', 'http://localhost'),

リファラを設定(APIがどこからのリクエストかを確認するため)。
APP_URL は .env ファイルで設定されている:

'X-Title' => 'LoL Champion Guide',

API のリクエスト目的を明示するためのカスタムヘッダー。

'Content-Type' => 'application/json',

リクエストのデータ形式を JSON に指定。

->post('https://openrouter.ai/api/v1/chat/completions', [

OpenRouter API のエンドポイント に POST リクエストを送信。

$guide = $response->json();

API のレスポンスを JSON として取得 する。

$guideContent = $guide['choices'][0]['message']['content'] ?? 'ガイドが見つかりませんでした。';

急に配列が出てきたと思ったが、json形式が以下のようになっていることが原因だ。

$guide = [
    "choices" => [
        [
            "message" => [
                "content" => "ガレンは耐久力が高く、初心者向けのチャンピオンです..."
            ]
        ]
    ]
];

スクリーンショット 2025-03-30 095430.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?