Laravel DeepSeek api の実装

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

この記事は[前回の記事（Laravel Webスクレイピング機能の搭載 ②）](https://qiita.com/mana-tech/items/9e85158f27ee20b48c20)
の続きである。

## ビュー部分の作成

```php
<!-- 検索フォームをスタイリング -->
    <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"`と定義されていた。

```html
<!-- 既存のテーブルの下に追加 -->
<!-- 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の通りに実行されるシステムだ。

```js
<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 イベントでは、デフォルトではページがリロードされますが、これを防ぎます。

さて、
```js
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関数で述べた部分だが、ルーターに以下が定義されている。
```php
Route::get('/champion-guide', [ChampionController::class, 'getGuide']);
```
したがってresponseの値はChampionControllerのgetGuideメソッドの返り値だ。
getGuide関数は以下のように定義されている。
```php
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-長い文字列
```

```php
'Authorization' => 'Bearer ' . $apiKey,
```
APIキーを設定（認証に必要）。
```php
'HTTP-Referer' => env('APP_URL', 'http://localhost'),
```
リファラを設定（APIがどこからのリクエストかを確認するため）。
APP_URL は .env ファイルで設定されている：

```php
'X-Title' => 'LoL Champion Guide',
```
API のリクエスト目的を明示するためのカスタムヘッダー。
```php
'Content-Type' => 'application/json',
```
リクエストのデータ形式を JSON に指定。
```php
->post('https://openrouter.ai/api/v1/chat/completions', [
```
OpenRouter API のエンドポイント に POST リクエストを送信。
```php
$guide = $response->json();
```
API のレスポンスを JSON として取得 する。

```php
$guideContent = $guide['choices'][0]['message']['content'] ?? 'ガイドが見つかりませんでした。';
```
急に配列が出てきたと思ったが、json形式が以下のようになっていることが原因だ。
```json
$guide = [
    "choices" => [
        [
            "message" => [
                "content" => "ガレンは耐久力が高く、初心者向けのチャンピオンです..."
            ]
        ]
    ]
];

```

![スクリーンショット 2025-03-30 095430.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4034432/d8611038-989b-4bf2-888c-1182bc8c692b.png)