とりあえずAIを使ったサイトを作りたい時に見る記事

# はじめに
こんにちは
[abc.osaka](https://abc.osaka) とかで色々書いたりしてるS高生(3期生)です

普段ウェブサイトを作っていて皆さんもこう思ったことがあるはずです
> 最近AIが流行ってるのか..。 なんか入れてみたいな。

こうなったとき、どうすればいいのかをコードを交えて書こうと思います

<!--:::note info
メインとなるサンプルコードは全てライブラリ無しのバニラjsで書かれています
そのため[about:blank](about:blank)などで全てのコードは試すことができます
:::-->


<!--:::note warn
<details>
<summary>
Click Me

**サクッとした記事を書きたいので詳しいところは本文から割愛しています。
本文中に書ききれないことはノートとして折りたたんであります、気になる場合は開いて読んでみてください**

</summary>

こんな感じで開く
```js
console.log("hello, world!")
```

</details>


:::

-->

:::note info
### はじめに
**この記事ではやること**
- [x] LLMの得意な事やユーケースの説明
- [x] AIとの会話とその準備
- [x] 実際にアプリの作成

**必要かもしれない予備知**

- [ ] **基礎的なHTML**
- [ ] **基礎的なJavascript**

ぶっちゃけ分からなくてもコピペでも動きます


<details><summary>この記事のnoteとdiff</summary>

- ノートは緑は参考程度、黄色は見といた方がいいかも、赤は絶対見といた方がいい、と言う感じです
- コードのハイライト ( diffの緑 )は見といた方がいいという感じで使ってます

</details>


:::




# 軽くAIについて分析
何事も既にあるものを知るところからがスタートです

## AIは何が得意で何が苦手なのか
ここではある用途にフィットするように作られているAIは考えませんが
一般的には大まかに分けると次のようになります

| 得意なこと | 苦手なこと |
|:-:|:-:|
| 文章の要約 | 数学 |
| 与えられた文章から回答 | 推論 |
| 日常的な会話 | 学術的な会話 |

つまり、AIは考えることが苦手なのです。　数学の問題について話しても頓珍漢なことを言いますが、作者の気持ちを答えるのには長けているのです

:::note info

<details><summary>数学や論理的な推論ができるAI</summary>

最近は**一部**ですが、数学の問題を解けたり、学術的な会話をしたり与えられた文章から推論することもできます

代表的なものに、数学の幾何学を得意とするGoogleの[Alpha Geometory](https://deepmind.google/discover/blog/alphageometry-an-olympiad-level-ai-system-for-geometry/)などがあります。

汎用性の高いものだとOpen AIの[o1 Preview](https://openai.com/index/introducing-openai-o1-preview/)が[高い性能](https://openai.com/index/learning-to-reason-with-llms/)を出しています

その他コーディングに特化した[Star Coder](https://huggingface.co/bigcode/starcoder2-15b)があります。　[Github Copilot](https://github.com/features/copilot)も其の一種です。

</details>


:::


# どんなところで使われているのか
AIが使われている場所はどんどん増えていますが、それに追いつこうと、世間が言うほど焦る必要はありません。　ざっと並べますから、さらっと目を通すくらいでいいでしょう。　これからAIを使ったアプリケーションを作ろうとしているのであればじっくり見てみてください。

| すること | 詳細 | 実用例 |
|:-:|:-:|:-:|
| カスタマーサポート | 質問に答える | [Chat Plus](https://chatplus.jp/), [ZenDesk](https://www.zendesk.co.jp/), [Deca](https://deca.marketing/service/cloud/cs/) |
| 要約 | 文章の要点を抜き出す | [NoteBookLM](https://notebooklm.google/), [YOMEL](https://aismiley.co.jp/product/ernie_yomel/) |
| 文章生成 |  | [Catchy](https://lp.ai-copywriter.jp/), [3秒敬語](https://3keigo.com/) | 
| カテゴリ化 | コメントの分析など | [Youtube Comment Topic](https://support.google.com/youtube/answer/14239105) |
| アイデア出し | | [Miro Ai](https://miro.com/ja/ai/mind-map-ai/), [ひらめきAIメーカー](https://hirameki.app/) |
| ウェブ開発 | APIやサイト作成 | [Hanabi](https://hanabi.rest/), [v0](https://v0.dev/) |
| 検索 | 検索してその結果を要約 | [Arc Search](https://arc.net/search), [ChatGPT search](https://openai.com/index/introducing-chatgpt-search/) |
| 総合 | アイデア出し、要約、 その他 | [Notion Ai](https://www.notion.so/ja/product/ai), [Figma Ai](https://www.figma.com/ja-jp/ai/) |

後はこれらを参考に実際に手を動かして作ってみましょう!

:::note info

<details><summary>AIの可能性</summary>

この表を見るとAIを使ったサービスが数えきれないくらいあるので無限のことができそうですが実際にはやっていることは概ね同じことです。
ちなみに、僕の作った[Ai32](https://email.i32.jp)だってやっていることは要約と文章生成だけです。

つまりAIは、特に一般人に至ってはアイデア勝負です。
ぜひあなたも新しいサービスを作ってみてください!
AI占い師、なんてどうですか?

</details>

<!--

<details><summary>AIとの付き合い方</summary>

AIはあくまでも生活を豊かにするための道具です。
が悲しい事にXでは今日も大量のLLMがポストし、インターネットの世界ではLLMによって生成された大量の誤情報や質の低い自動生成記事で溢れています。 その上AI推進派と慎重派が日々対立しています。　善か悪かは僕には判断できませんが、こう言った弊害を知った上でAIを使うことが我々に求められているのではないかと最近思っています。

</details>

-->

:::

# とりあえず使ってみる
とりあえず入れてみるとは言ったものの、何を使うか迷うと思います。
世はまさにLLM戦国時代、ChatGPT? Gemini? Claude?数々のLLMのAPIが乱立しています
この記事を読んでいる人はシンプルなものを求めていると思うので、今回はGroqを使おうと思います

https://groq.com/

:::note info

<details><summary>なぜGroq?</summary>

凝ったことをしたいなら既にある程度手法が確立している`Azure OpenAI Service`か、少なくとも`Open AI`系列を使うことになりますが、今回はあくまでも5分でできることを目標に書いてるので`Groq`を使います

LLMを初めて使うのであったり、簡単なことをするには良い選択だと思います

</details>

<details><summary>API KEYの発行方法</summary>

### 1. とりあえずログインする
<!--
![スクリーンショット 2024-11-28 2.36.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626751/5c2bc5eb-ecdc-1c5f-3238-50da95591776.png)-->

https://console.groq.com/login

上のリンクに飛べばログインページにいけます

### 2. api keyを発行する
![スクリーンショット 2024-11-28 2.37.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626751/a033a8ed-855c-7fc3-fa99-60abb79419fe.png)

https://console.groq.com/keys

上の緑で囲われているボタン`Create Key`を押して適当に名前をつければKeyが貰えます

</details>

<details><summary>リミットについて</summary>

`Groq`の無料枠は多いです。 ( そもそも従量課金のプランが`Coming Soon`でまだありません )。ですので、個人で普通に常識の範囲で使う分にはあまり困ることはないでしょう。

|  | リクエスト数 | トークン数 |
|:-:|:-:|:-:|
| 1分 | 30 req |  7,000 - 15,000 token |
| 1日 | 7,000 - 14,400 req  | 500,000 token |

</details>

<details><summary>token数について</summary>

tokenは端的に言えば文章の量です、文章が増えればtoken数が増えますし、逆もしかりです。
英語の場合は単語数とほぼ同一ですが、日本語の場合は少し複雑ですが1文字1tokenとすればよいでしょう。

- 普通に会話するだけなら1回100token程度 ( 設定付きなら300token )
- しっかりと作り込んで何らかのタスクをこなさせたいなら2,000-4,000token程度
- 普通の記事程度の分量を書かせたいなら数万tokenは必要でしょう

ただし、1回の会話が100程度と言っても、それはやり取りの中で最初の1回だけです。 理由はLLMにこれまでの会話をすべて渡す必要があるからです。 途切れ途切れでいいなら毎回100tokenでいいですが、多くの人は明日遊ぶ予定を考えてるときに、急に仕事の話をされるのは嫌でしょう。その場合は25回のやり取りで12000token程です。

</details>


:::

# Hello, world!
さてとりあえずhello, world!を実行しましょう
```diff_javascript:grpq/groq.js
/**
 * @param {string} content
 * @returns string
 */
const getGroqChatCompletion = async (body) => {
    const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
        method: "POST",
        headers: {
            /**
             * API_TOKENに発行したapi keyを入れる
             * ```
             * const API_KEY = "gsk_XXXXXXXXXXXXXXXXXXXXXXXXX"
             * ```
             */
            Authorization: `Bearer ${API_KEY}`,
        },
        ContentType: "application/json",
        body: JSON.stringify({
            /**
             * モデル一覧
             * @see https://console.groq.com/docs/models
             */
            model: "gemma2-9b-it",
            max_tokens: 4096,

            ...body,
        })
    })
    return res
}

/**
 * @param {Response | Promise<Response>} res
 * @returns string
 */
const getGroqChatCompletionTextWrapper = async (res) => {
    const json = await (await res).json()
    /**
     * 一番最初の回答を取り出して返す
     */
    return json.choices[0].message.content
}
```
```js:console
console.log(
    await getGroqChatCompletionTextWrapper(
        getGroqChatCompletion({
            messages: [
                { role: "user", content: "こんにちは!、初めまして!" }
            ]
        })
    )
)
```

<!--

:::note info

<details><summary>curl version</summary>

</details>

<details><summary>typescript version</summary>

</details>

<details><summary>python version</summary>

</details>

:::

-->

非常にシンプルですね?


:::note warn

<details><summary>リアルタイムで返してもらう</summary>
さて、ここで普段ChatGPTなどを使ってる人は疑問に思ったかもしれません。
ChatGPTなどは返答がリアルタイム、言い方を変えると徐々に回答ができていきます。ところがこれは一度に結果が帰ってきています。 理由は、一度に帰ってきたほうが処理処理がしやすいからですが、もちろん徐々に返してもらうこともできます

```javascript:stream.js
/**
  * chunk format:
  * data: {"id":"XXXXXXXXXXXXXX","object":"chat.completion.chunk","created":1700000000,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"\n\n"},"finish_reason":null}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
  * 
  * 上記の様に帰ってくるので、それをテキストにして返す。良い方法が見つからなかったので適当に
  */
/**
  * 
  * @param {string} chunk 
  * @returns 
  */
const parseChunk = (chunk) => 
    chunk
        .split("data: ")
        .map((text) => text.replaceAll("\n", ""))
        .filter((text) => text.startsWith("{\"id\":"))
        .filter((text) => text.endsWith("}"))
        .map((text) => JSON.parse(text))
        .map((json) => json.choices[0].delta.content)
        .join("")

/**
 * 
 * @param {Response | Promise<Response>} res 
 * @param {(parsedText: string)=> void} event 
 * @returns {Promise<string>}
 */
const getGroqChatCompletionStreamWrapper = async (res, event) => {
    const reader = (await res).body.getReader();
    const decoder = new TextDecoder("utf-8");

    /**
     * @type {string}
     */
    let content;

    while (true) {
        const { done, value } = await reader.read();
        const chunk = decoder.decode(value, { stream: true });

        const parsed = parseChunk(chunk);
        event(parsed)

        content += parsed;
        if (done) break;
    }
    return content;
}
```
```javascript:console
getGroqChatCompletionStreamWrapper(
    getGroqChatCompletion({
        stream: true,
        messages: [
            { role: "user", content: "こんにちは!、初めまして!"}
        ]
    }),
    (res) => {
        console.log(res)
    }
);
```


</details>

<details><summary>会話の履歴と会話に参加する人物</summary>

勘のいい人ならお気づきかと思いますが、AIにプロンプトを渡すとき、以下のような`messages`という配列で渡します。

```javascript:messages
[
    {role: "system", content: "あなたはアシスタントです。"},
    {role: "user", content: "こんにちは"}
]
```
みたいな感じです。これが所謂会話の履歴です。

## 会話の履歴

更に`getGroqChatCompletion`函数の返す値をよく見てみると、以下の様に帰ってくる事がわかります
```javascript:response.choices[0]
{role: "assistant", content: "はじめまして。なにかお手伝いできることはありますか?"}
```
つまり、これを次の会話でAI渡してあげると、AIが文脈を把握し、より適切な会話ができるようになります。

ところで`role`とはなんでしょうか?

## 会話に参加する人物
`role`は直訳すると`役割`です。それはそのままの意味で、特に説明することは無いのですが、面白い事に`user`と`assistant`以外にも様々な役割があります。

| 役割 | 機能 |
|:-:|:-:|
| user | ユーザー |
| assustant | AI |
| system | AIに指示を出したりするときに使う |
| tool | AIを補助する |

ざっとこんな感じです。
キャラ設定をつけるときに`system`を使ってAIに設定通り動くよう命令するっと言った使い方です。

`tool`に関しては`Tool Use`と呼ばれる機能で使うのですが、残念ながらここに書くには余白が少なすぎるので、詳しくは次のセクション、[天気を教えて!]()を御覧ください

</details>

<details><summary>画像を与えてみる</summary>

上の例だとテキストだけですが、画像をLLMに与える事もできます

```javascript:console
getGroqChatCompletionStreamWrapper(
    getGroqChatCompletion({
        stream: true,
        messages: [{
            role: "user",
            content: [
                { type: "text", text: "この画像みてどう思う?" },
                { type: "image_url", image_url: { url: "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg" } }
            ]
        }],
        model: "llama-3.2-90b-vision-preview"
    }),
    (event)=> {
        console.log(event)
    }
)
```

使い方は簡単ですが、少し制約があります
1. `model`は`llama-3.2-90b-vision-preview`、または`llama-3.2-11b-vision-preview`のみ
2. `{ role: "system" }`は使えない
3. 使える画像は1枚のみ
4. 画像は最初に入れなければならない

少し面倒です、特に`{ role: "system" }`が使えないというのは命令するときに少し厄介ですが、まぁ問題はありません。


**コード**: [/app/chat-vision/page.tsx](https://github.com/applemango/qiita-simple-llm-app/blob/main/app/chat-vision/page.tsx)
**デモ**: [https://qsla.i32.jp/chat-vision](https://qsla.i32.jp/chat-vision)


</details>

<details><summary>画像をユーザーに入力させる場合</summary>
LLMは全く関係ないですが、JSで画像をURLに変換する方法を少し

LLMは画像をurlでしか受け付けないですが、そのurlは別にbase64でいいのです。なので別にサーバーを立てて、そこに画像を保存するとかする必要はありません
```javascript
/**
 * @param {File} file 
 */
const convertFileToBase64Url = (file) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    return new Promise((resolve, reject) => {
        reader.onload = function () {
            const url = reader.result;
            resolve(url)
        };
    })
}
```

別にreactじゃなくてもいいですが、reactの場合はこんな感じ
```react
<Input type="file" onChange={async (e) => {
    if (e.target.files && e.target.files.length > 0) {
        const file = e.target.files[0];
        const url = await convertFileToBase64Url(file)
    }
}}/>
```
</details>

:::


# プロンプトエンジニアリングってなーに?
少し話が脱線しますが、これも重要なので。
みなさんプロンプトエンジニアリングについてご存知でしょうか
<!--
[wikipedia](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%83%AA%E3%83%B3%E3%82%B0)を要約すると、プロンプトエンジニアリングとは**AIにやってほしいこと指示するための文章**です
-->
端的に言うと**AIにやってほしいこと指示するための文章**です


人に頼む時とやることはほとんど同じでですが、人に頼む時と違って、AIに頼むときは明らかに結果が変わってきます。 ただ単に命令するのでは無く、どんな感じにすれば良いのかサンプルを渡すなどの工夫ですが、まずはあまり深く考えずに、早速やってみましょう

:::note info

<!--

<details><summary>引用したwikipediaによる定義</summary>

> プロンプトエンジニアリング（英: prompt engineering）は、主に「テキストからテキスト」型言語モデルとのコミュニケーションで使用され、生成的人工知能（生成AI）モデルが解釈し、理解できるように指示文章を構造化する過程である。プロンプト・エンジニアリングは、「プロンプトから一時的に学習するモデルの能力」として定義されるコンテキスト内学習（in-context learning）によって可能となる。コンテキスト内学習の能力は、大規模言語モデル（LLM）の創発的能力である。

[wikipedia](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%83%AA%E3%83%B3%E3%82%B0) 2024年11月11日観覧

</details>

-->

<details><summary>効率の良いプロンプトの試し方</summary>
プロンプトを試すときに、普通にいちいちコードを変更していては手間がかかります
そこで簡単に試せる環境が欲しいのですが、Groqの場合は専用のコンソールがあります。

[https://console.groq.com/playground](https://console.groq.com/playground)

Open AIにも同様のコンソールがあります
[https://platform.openai.com/playground](https://platform.openai.com/playground)

</details>

<details><summary>コンソールの使い方</summary>

必要ないかもしれませんが、一応説明しておきます

ログインした状態で[コンソール](https://console.groq.com/playground)を開いている前提です

![スクリーンショット 2024-11-11 23.17.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626751/acfe7c5e-34aa-ed07-dfcd-8e1cf4403e9e.png)

# 使い方
### **0. chatとstudio**
緑の枠で囲まれているところの上に`Chat`と`Studio`を切り替えるところがあります
`chat`のところは普通にチャットするだけなので、特に説明するところもなく、うん。って感じなので`Studio`を選びます

### **1. モデルを変える**
モデルは黄色のところから選べます。

一応モデルについて簡単に説明しておくと、モデルは一言で言うと**誰に頼むか**です。お金が多くかかる代わりに性能の高いものを使うか、あまり能力は必要としない単純作業だから、一番安いものを使う、とか選べます。

おすすめは`gemma2-9b-it`です
性能的には`llama3-groq-70b-8192-tool-use-preview`が一番高いと思います

### **2. プロンプトを入力する**
入力したいプロンプトを画像でいう緑のふちのところに書きます

### **3. 必要に応じてパラメーターを調整する**
パラメーターを調整することはあまりないですが、よく使う項目は`Stream`と`JSON Mode`です。
それ以外はほぼ使わなくて良いと思います。
| パラメーター | 効果 |
|:-:|:-:|
| Temperature | どの程度ランダムな結果を返すか。0にすると同じプロンプトには同じことしか返ってこなくなる |
| Max Tokens | AIが返せる最大の文章量、日本語の場合は大体1文字1トークンなので1024にすると、最大1024文字で返す |
| Stream | 文章を細々と返すかどうか |
| JSON Mode | 結果をテキストでは無くJSONで返すかどうか |

### **4. 実行する**
画像の青緑色で囲まれたボタンを押します

### **5. コードとして書き出す**
画像の紫色で囲まれたボタンを押すと、良い感じにコードを表示してくれます。
`Curl`, `Python`, `Javascript`, `JSON`に対応してます。

</details>


:::



## プロンプトを試してみる

色々と説明しても良いのですが、百聞は一見に如かず、まずは見てみましょう


```yaml:system
Persona:
    Description: あなたは与えられた単語を基に短い小論文を書きます。
Instructions:
    - 400字程度で書いてください
    - それに関連するいくつかのアイデアを出してください
Output_format: 400字程度の文章で、最後にいくつかの関連する分野や、創造的なアイデアを提示する
    
Examples:
    - Word: 大規模言語モデル
    - Solution:
        LLM（大規模言語モデル）は、膨大なデータを学習し、自然言語の理解や生成を行う人工知能の一形態です。例えば文章の生成、翻訳、要約など、多岐にわたるタスクをこなすことができます。
        しかし、LLMにはいくつかの課題も存在します。まず、膨大な計算資源とデータを必要とするため、環境への影響が懸念されています。さらにハルシネーションなど、誤った情報を生成する可能性があります。
        それでも、LLMは今後も進化を続け、さまざまな分野で新たな価値を生み出すことが期待されます。

        RAG (検索拡張生成): 事前に用意した資料を基にLLMに回答させてハルシネーションを低下させる技術
        プロンプトエンジニアリング: LLMへの命令方法を変えて、よりLLMのパフォーマンスを向上させる技術

        レビューの要約: ユーザーが投稿した大量のレビューをジャンル毎に要約してユーザーの声を把握しやすくします
        アシスタント: 天気を教えたり、TODO Listにやることを追加したり、ユーザーの興味がある分野のニュースを要約して教えたり、ユーザーの生活をサポートします
```
```yaml:user
Task:
    - Word: {INPUT}
```

こんな感じです。

:::note info

<details><summary>YAMLを使っている理由</summary>

最近発表された論文[Does Prompt Formatting Have Any Impact on LLM Performance?](https://arxiv.org/abs/2411.10541)によると

普通のテキストやマークダウンで渡すよりも`YAML`で渡したほうが、高いスコアが出ているためです。`JSON`を採用しなかったのはモデルによってばらつきが大きいので、中間を取った形です。
もちろん言語によっても変わるので一概には言えませんが。

僕はプロンプトエンジニアリングの分野は詳しくないので、そこら辺は自分で調べてみることをおすすめします
( じゃあ何でこの章を作ったんだって話だけど、重要だと思ったんだもん )




</details>

:::


# 作ってみる
さて、閑話休題いよいよ実際に作ってみようと思います。　使用する言語やフレームワークは好きなものを使用していただければよいのですが、この記事では簡単のためNext.jsを使用します。

<!--
:::note info
TypeScriptで書くとJavaScript環境つまりバニラJSで直接動かせなくなるので、UI以外のLLMを直接操作している根幹部分はバニラJSにJSDocで型付けする様に書いています
:::
-->

:::note alert
此処から先に出てくるコードは、大体の流れがつかめるように書いているので、**なんの説明もなく突然コンポーネントやフック、関数ができてます**。　触ってみたい場合は**デプロイされているリンクから、コード全体を見たい場合はgithubのリンクに飛んでください**

なぜかって言うと、流石に長くなりすぎるから
:::

:::note info
**リポジトリ**: https://github.com/applemango/qiita-simple-llm-app
:::



## チャットしたい
まずは王道のチャットボットから作ってみたいと思います

とは言っても、さっきのを繋ぎ合わせるだけです
```diff_tsx:chat/page.tsx
export default function() {
    
    const [response, setResponse] = useState("どうしたの? ごしゅじんさまぁ")
    const { push } = useChatMessages([
        { role: "system", content: "あなたは...、何でしょうね。少なくとも哲学は好きなのかも" }
    ])

    const send = (input: string)=> {

        const history = push([
            /**
             * 前回の回答を送信する。初回は事前に用意した回答
             */
            { role: "assistant", content: response },
            { role: "user", content: input }
        ])

        setResponse("")

        getGroqChatCompletionStreamWrapper(
            getGroqChatCompletion({
                stream: true,
                messages: history,
            }),
            (value) => setResponse((r) => r.concat(value))
        )

    }
    return <ChatContainer>
        <Alert className="mb-2">
            <Info className="h-4 w-4" />
            <AlertTitle className="text-lg">せつめい</AlertTitle>
            <AlertDescription>
                これ以上ないほどシンプルなLLMを使ったアプリ
            </AlertDescription>
        </Alert>
        <Chat response={response} onSend={send} />
    </ChatContainer>
}
```
:::note info
**コード**: [/app/chat-simple/page.tsx](https://github.com/applemango/qiita-simple-llm-app/blob/main/app/chat-simple/page.tsx)
**デモ**: [https://qsla.i32.jp/chat-simple](https://qsla.i32.jp/chat-simple)
:::

簡単ですね?

<!--

:::note info

<details><summary>音声で操作できるようにしてみる</summary>

ここまで文字で操作する前提でしたが、中にはAIと音声で会話したい人もいるでしょう。
実はAIと音声で会話するのは割と楽です

## 音声で操作できるようにしてみる

音声ファイルをAIに渡して実現する方法もありますが、今回は[Web SpeechRecognition API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition)を使ってクライアント側で文字起こしを行いたいと思います。　今回は更に、それのReact用ライブラリ[React Speech Recognition](https://www.npmjs.com/package/react-speech-recognition)を使います

まずは音声入力に使うライブラリを入れます
```:terminal
npm i react-speech-recognition
```
そして、エラー回避のためにnext.jsの場合は`layout.tsx`を作成します
```react:layout.tsx
"use client"
import 'regenerator-runtime/runtime'


export default function RootLayout({
    children,
}: Readonly<{
    children: React.ReactNode;
}>) {
    return (
        <html lang="en">
            <body>
                {children}
            </body>
        </html>
    );
}
```
簡単に使い方を説明すると

このライブラリは、下のような`hook`を追加してくれて、これが便利なのです
```react
const {
    transcript,
    listening,
    resetTranscript,
    browserSupportsSpeechRecognition,
    finalTranscript,
    isMicrophoneAvailable
} = useSpeechRecognition();
```
余計なことを考えなくても、`SpeechRecognition.startListening()`を実行すれば、勝手に`transcript`や`finalRTranscript`に文字列として出てきてくれます

あとはこれをいい感じに調理すれば、できました
```react
import { Input } from './ui/input';
import { Info } from 'lucide-react';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
import { useEffect, useState } from 'react';
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';

export const ChatVoice = ({ response, onSend }: {
    response: string,
    onSend: (text: string) => void
}) => {
    const [isSupport, setIsSupport] = useState(true)
    const {
        transcript,
        listening,
        resetTranscript,
        browserSupportsSpeechRecognition,
        finalTranscript,
        isMicrophoneAvailable
    } = useSpeechRecognition();

    useEffect(() => {
        onSend(finalTranscript)
    }, [finalTranscript])

    useEffect(() => {
        if (!browserSupportsSpeechRecognition || !isMicrophoneAvailable) {
            setIsSupport(false)
        }
    }, [browserSupportsSpeechRecognition, isMicrophoneAvailable])

    if (!isSupport) {
        return <Alert className="mb-2">
            <Info className="h-4 w-4" />
            <AlertTitle className="text-lg">ぶらうざーがたいおうしてないよ!</AlertTitle>
            <AlertDescription>
                ブラウザが音声入力に対応してないから使えないよ!
            </AlertDescription>
        </Alert>
    }

    return <>
        <Textarea readOnly value={response} style={{
            resize: "none",
            height: 300
        }}></Textarea>
        <div className="flex mt-2 gap-2">
            <Input readOnly value={transcript} placeholder="なにか喋ってください..."></Input>
            <Button onClick={()=> {
                if (listening) {
                    SpeechRecognition.stopListening()
                } else {
                    resetTranscript()
                    SpeechRecognition.startListening({ language: 'ja-JP', continuous: true })
                }
            }}>
                {listening ? "Stop" : "Start"}
            </Button>
        </div>
    </>
}
```
あとは`chat/page.tsx`の`Chat`コンポーネントをこの`ChatVoice`に変えるだけです

</details>

:::

-->

### 天気予報を教えて!
さて、ここまで会話するだけでしたが。少しつまらないので天気を教えてもらおうと思います。 とは言ったものの、今までの延長線で天気を教えてもらおうとすると、プロンプトに天気の情報を渡す必要が出てきます。しかし、いちいち世界中の天気を渡すことは不可能に近いですし、会話の中で必要になるかすらわかりません。なのでもし会話の中で天気の情報が必要になったら、AIからシステムに尋ねて貰えば良いのです。

そのための機能が`Tool Use`です

試しにやってみましょう
```javascript:groq/tool.js
/**
 * @typedef Tool
 * @property {string} type ほぼ`"function"`固定
 * @property {object} function
 * @property {function} function.function 実行したい関数
 * @property {string} function.name 関数の名前
 * @property {string} function.description 関数の説明
 * @property {object} function.parameters 関数の引数
 * @property {string} function.parameters.type `object`とか、他は知らないけどあるかも
 * @property {object} function.parameters.properties
 * @property {string[]} function.parameters.required 絶対に必要なやつ
 */

import { getGroqChatCompletion } from "./groq"

/**
 * @param {Array<Tool>} tools
 * @param {*} messages
 */
const getGroqChatCompletionWithTools = async (tools, messages) => {
    /**
     * ツール(関数)の実行結果や、LLMが要求したツールを保存するところ。
     * 最後にその会話履歴をLLMに渡すと結果が生成される
     */
    const resultMessage = messages.concat()

    /**
     * LLMにツールを渡して、何を使うのか聞く。
     */
    const toolUseRes = await getGroqChatCompletion({
        messages,
        tools: tools
            .concat()
            /**
             * .map(tool) => {
             *     delete tool.function.function
             *     return tool
             * })
             * と同じ ( 参照元も一緒に消されたので仕方なく下みたいな感じにした )
             */
            .map((tool)=> ({
                type: tool.type,
                function: {
                    name: tool.function.name,
                    description: tool.function.description,
                    parameters: tool.function.parameters
                }
            })),
        tool_choice: "auto",
    })
    const toolUseResJson = await toolUseRes.json()

    /**
     * LLMから帰ってきた使うツール一覧をとりあえず会話履歴にいれる
     */
    const toolUseResJsonMessage = toolUseResJson.choices[0].message
    resultMessage.push(toolUseResJsonMessage)

    /**
     * 使うツールのリスト
     */
    const toolCalls = toolUseResJsonMessage.tool_calls || []

    for(const tool of toolCalls) {
        const toolName = tool.function.name // 使う関数の名前
        const toolArgs = JSON.parse(tool.function.arguments) // 関数の引数

        const useTool = tools.find((tool) => tool.function.name === toolName) // 名前から関数を取り出す
        const toolFunction = useTool.function.function

        /**
         * 使うツールを管理するためにGroqから渡されたid
         */
        const toolId = tool.id

        /**
         * ツールを実行する
         */
        const toolResult = await toolFunction(toolArgs)

        /**
         * 会話履歴に実行結果を入れる
         */
        resultMessage.push({
            tool_call_id: toolId,
            role: "tool",
            name: toolName,
            content: JSON.stringify(toolResult)
        })
    }

    /**
     * 必要なツールの一覧と、そのツールの実行結果が含まれている会話履歴を返す
     */
    return resultMessage
}

export {
    getGroqChatCompletionWithTools
}
```
```javascript:console
console.log(
    await getGroqChatCompletionTextWrapper(
        getGroqChatCompletion({
            stream: true,
            messages: await getGroqChatCompletionWithTools(
                [
                    {
                        type: "function",
                        function: {
                            name: "get_weather_forecast",
                            description: "都市IDから天気予報を取得する",
                            parameters: {
                                type: "object",
                                properties: {
                                    locationId: {
                                        type: "string",
                                        description: "都市のID, 大阪府: 270000, 東京都: 130010, その他: 230010",
                                    },
                                    dateLabel: {
                                        type: "string",
                                        description: "today or tomorrow"
                                    }
                                },
                                required: ["locationId", "dateLabel"],
                            },
                            function: async (args: { locationId: string, dateLabel: string }) => {
                                const forecast = await getForecastSimple(args.locationId, args.dateLabel as any)
                                return JSON.stringify(forecast)
                            }
                        },
                    }
                ],
                [
                    { role: "system", content:"あなたは天気を伝えることしかできません。あなたは天気のことを話さずにはいられません。"},
                    { role: "user", content: "大阪の天気教えて" }
                ]
            ),
        }),
    )
)
```

:::note info

**コード**: [/app/chat-with-tool/page.tsx](https://github.com/applemango/qiita-simple-llm-app/blob/main/app/chat-with-tool/page.tsx)
**デモ**: [/chat-with-tool](https://qsla.i32.jp/chat-with-tool)


<details><summary>天気予報を取得する関数</summary>

今回は天気予報を取得するのに

```javascript
export const getForecast = async (locationId: string) => {
    const res = await fetch(`https://weather.tsukumijima.net/api/forecast/city/${locationId}`)
    const json = await res.json()
    return json
}

export const getForecastSimple = async (locationId: string, date: "today" | "tomorrow") => {
    const json = await getForecast(locationId)
    const index = date == "today" ? 0 : 1
    return {
        text: json.description.text,
        forecast: {
            detail: json.forecasts[index].detail,
            temperature: json.forecasts[index].temperature,
            telop: json.forecasts[index].telop,
        },
    }
}
```

</details>

:::

えぇ、なんか、思ってたよりも仰々しくなりましたが、やってることはシンプルです ( 多分 )
図にすると

```plantuml
@startuml
skinparam backgroundColor #EEEBDC
skinparam handwritten true

skinparam sequence {
ArrowColor DodgerBlue
ActorBorderColor DeepSkyBlue
LifeLineBorderColor DodgerBlue
LifeLineBackgroundColor #A9DCDF

ParticipantBorderColor #FFFFFF
ParticipantBackgroundColor DodgerBlue
ParticipantFontSize 16
ParticipantFontColor #FFFFFF

FontCOlor red
}

group 関数のやり取り
    くらいあんと -> Groq: 関数の情報とプロンプトを渡す
    Groq -> くらいあんと: 必要な関数の名前を返す
    note right
渡された関数の情報と
プロンプトから
実行したい関数と
その引数を渡す
    end note

end

くらいあんと -> くらいあんと: 関数を実行する
note left
渡された引数を
基に関数を実行する
end note

group いつものやりとり
    くらいあんと -> Groq: 関数の結果とプロンプトを渡す
    Groq -> くらいあんと: 関数の結果を基に生成して返す
end

```
とてもシンプルですね!
ですがこの機能はとても強力です、普通ならLLMはただチャットすることしかできませんが、この機能があれば、タイマーを設定したり、ニュースを関連付けて話したりなど何らかの機能を提供することができるようになります。

<!--

## RAG ( 拡張検索 ) をする!
さて、ここから少し毛色を変えますが、ここまでは割とライトな話をしてきましたが、ここでAIに正しい情報を言わせる方法を紹介しようと思います。

AIに正しい情報を出させる方法はいくつかありますが、ここではRAGをしてみようと思います。
仕組みは単純?で、以下のような感じです
```plantuml
@startuml
skinparam backgroundColor #EEEBDC
skinparam handwritten true

skinparam sequence {
ArrowColor DodgerBlue
ActorBorderColor DeepSkyBlue
LifeLineBorderColor DodgerBlue
LifeLineBackgroundColor #A9DCDF

ParticipantBorderColor #FFFFFF
ParticipantBackgroundColor DodgerBlue
ParticipantFontSize 16
ParticipantFontColor #FFFFFF

FontCOlor red
}

くらいあんと -> DB: プロンプトを渡す
note left
DBを参照して
関連するドキュメントを探す
end note
DB -> Groq: ドキュメントと一緒に渡す

Groq -> くらいあんと: 結果を返す

```
さも当たり前の様に1行で`DBを参照して関連するドキュメントを探す`と書いていますが、それが一番難しいところで、あとは何の目新しさもありません。

-->

# 終わりに
いかがでしたか?
もしこの記事が役に立ったなら、あー役立つ記事があったなーとでも思っていてください。

ぜひ、みなさんも何か作ってみてください。 すでに同じものがあったとしても、同じものを作り始めても、同じものはできませんから。


ではまた、何時かのアドベントカレンダーでお会いしましょう
<!--
:::note info

プロジェクトとかデモのユーザーインターフェースに貼ってね!って書いてたけど、めんどくさいからここに貼っとく

<a href="https://groq.com" target="_blank" rel="noopener noreferrer">
  <img
    src="https://groq.com/wp-content/uploads/2024/03/PBG-mark1-color.svg"
    alt="Powered by Groq for fast inference."
    width="300"
  />
</a>

:::
-->