Nuxt3 × Vuetify3 × LangChain × Bedrockでチャットボットを作成してみた

## はじめに
Nuxt3とVuetify3、LangChain、Bedrockを使ってチャットボットを作成してみました。
Nuxt3プロジェクトでLangChainを使って、ChatBedrockを実行してみたい方におすすめです。

## 開発環境
- Windows 11
- Nuxt.js 3.8.0
- Vuetify 3.4.0-alpha.1
- npm 10.1.0
- Node.js 20.9.0
- LangChain

## ディレクトリ構成
本記事で作成するチャットボットのディレクトリ構成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/0551783e-c053-c160-8f0a-9bae4e0822ae.png)

## 完成イメージ
チャットボットの完成イメージはこちらです。
Vuetifyを使って、チャットっぽいデザインにしています。
![画像 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/a646484a-83e4-c42e-4997-801b2eb85697.png)


## 実装
### Nuxt3プロジェクトの作成
1. Nuxt3プロジェクトを作成します。

```
npx nuxi@latest init nuxt3-langchain-chatbot
```
　　
2. 作成したNuxt3プロジェクトのディレクトリに移動します。
```
cd nuxt3-langchain-chatbot
```
　　
3. 依存関係をインストールします。
```
npm install
```
　　
4. 試しに開発モードで起動します。
```
npm run dev -- -o
```
　　
以下のような画面が表示されれば、Nuxt3プロジェクトの作成完了です。  
![localhost_3000_ (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/cdc3ef00-c895-cc55-6165-fabda3455272.png)

### Vuetify3の導入
次に、作成したNuxt3プロジェクトにVuetify3を導入します。
　　
1. 以下のコマンドで、Vuetify3の最新版と、スタイルを適用するのに別途必要なmdiとSASSを開発用の依存関係としてインストールします。
```
npm install vuetify@next mdi @mdi/font sass --save-dev
```
　　
2. pluginsディレクトリをルートディレクトリ直下に作成し、その中にvuetify.tsファイルを作成します。vuetify.tsは以下のようにします。
```javascript:vuetify.ts
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";

export default defineNuxtPlugin((nuxtApp) => {
    const vuetify = createVuetify({
        components,
        directives,
    });

    nuxtApp.vueApp.use(vuetify);
});
```
上記のコードでは、Vuetifyを使用するために必要なモジュールをインストールし、defineNuxtPluginの中でVuetifyのインスタンスを作成してマウントしています。これにより、Vuetifyのコンポーネントやディレクティブ(v-bind、v-model、v-ifなどのこと)が利用できます。
　　
3. nuxt.config.tsを以下のように書き換えます。
```javascript:nuxt.config.ts
export default defineNuxtConfig({
    typescript: {
        shim: false,
    },
    ssr: false,
    css: ["vuetify/lib/styles/main.sass", "mdi/css/materialdesignicons.min.css", "@mdi/font/css/materialdesignicons.css"],
    build: {
        transpile: ["vuetify"],
    },
    vite: {
        define: {
            "process.env.DEBUG": false,
        },
    },
});
```
上記の「css:」部分でVuetifyやmdiのスタイルを指定し、「build:」部分でビルドプロセスにVuetifyが含まれるように指定しています。

### Vercel AI SDKとLangChainのインストール
以下のコマンドで、Vercel AI SDKとLangChainをインストールします。
Vercel AI SDKは、[Vercel](https://vercel.com/)が出しているAIチャットボットを簡単に作ることができるライブラリです。
LangChainは、LLMを用いたアプリケーション開発を効率的に行うライブラリです。
```
npm install ai langchain
```

### Bedrockを使うためのパッケージのインストール
Bedrockを使うためにAWSのパッケージをいくつかインストールします。
インストールすることで、AWSとの通信を可能にします。
```
npm install @aws-crypto/sha256-js @aws-sdk/credential-provider-node @smithy/protocol-http @smithy/signature-v4 @smithy/eventstream-codec @smithy/util-utf8 @aws-sdk/types
```

### Amazon Bedrockで使用するモデルの有効化
今回はAmazon Bedrockを使用するので、モデルの有効化を行います。
AWS Amazon Bedrockのページに移動し、「Model Access」を選択、Claudeが「Access granted」になっていない場合は、右上の「edit」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/3971fe54-7043-6287-0e34-eca1208e8623.png)

「Anthropic」にチェックを入れ、「Save changes」を選択します。
![記事投稿用 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/07d177dc-a4f8-ae2c-47dd-aff2fc237653.png)

数分間はステータスが「In Progress」となるので、待機します。
![画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/319ae9c9-6f3c-f7a7-0d33-cee11eb78f7c.png)

数分おきに更新ボタンを押して、ステータスが「Access granted」になれば有効化の完了です。
![画像 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/a4e62dce-6865-5dfc-80ad-5fb757e1141b.png)


### IAMの設定
次に、IAMの設定を行います。
AWS IAMの「ユーザー」タブに移動して、任意のユーザーを選択または作成します。
「セキュリティ認証情報」タブから、「アクセスキーを作成」を選択します。
![記事投稿用 (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/44dedff3-f3f1-5936-7e9f-5c8a51914447.png)

ユースケースは「コマンドラインインターフェイス(CLI)を選択し、「次へ」を選択します。
![記事投稿用 (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/ff30b6d6-dc87-4290-3cab-635ce9c7246a.png)

説明タグ値の入力は任意となるため、入力せずに「アクセスキーを作成」を選択します。
![記事投稿用 (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/3903c5d6-329f-0493-d490-bf29360b93b3.png)

アクセスキーとシークレットキーをメモして「完了」を選択します。
![記事投稿用 (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/a01e1de5-1d4e-20c0-59a2-0c264249f148.png)

続いて、Amazon Bedrock用の許可ポリシーを作成します。
本記事を作成した時点では、Bedrock専用の許可ポリシーは存在しなかったため、インラインポリシーを作成します。
IAMの「ユーザー」のページに移動し、「許可を追加」から「インラインポリシーを作成」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/eb826c5c-b2e7-2f01-67d3-0f60d51efdc0.png)

ポリシーエディタは「JSON」を選択し、中身を書き換え（コードの内容は、以下の画像の直下に記載）、「次へ」を選択します。
![記事投稿用 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/3bdc469d-77c3-a395-9556-6b05c8b6610e.png)

JSONの中身はこちらです。
```json
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "Statement1",
			"Effect": "Allow",
			"Action": "bedrock:*",
			"Resource": "*"
		}
	]
}
```

ポリシー名を「BedrockFullAccess」とし、「ポリシーの作成」を選択します。
![記事投稿用 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/8708712f-1719-0069-5f8a-784bf4791386.png)

　　
続いて、AWS CLIをインストールしていない場合は、インストールします。
以下の記事を参考にして、インストールしてください。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html

今回のチャットボットを実行するディレクトリに移動し、AWS CLIがインストールされたかを以下のコマンドで確認します。
```
aws --version

aws-cli/2.13.29 Python/3.11.6 Windows/10 exe/AMD64 prompt/off
```
結果が表示されれば、インストールされています。

以下のコマンドで、アクセスキーとシークレットキーを設定します。
最初は空の状態なので、ご自身のアクセスキーやシークレットキーを入力し、エンターキーを押してください。region nameは「us-east-1」、output formatは「json」にしてください。
```
aws configure

AWS Access Key ID [****************YK63]:
AWS Secret Access Key [****************2DbP]:
Default region name [us-east-1]:
Default output format [json]:
```

これでIAMの設定は完了です。

### APIエンドポイントの作成
チャットを可能にするためのエンドポイントを作成します。
api/serverパスの中にchat.tsファイルを作成し、以下のようにしてください。
```api/server/chat.ts
import { Message } from 'ai';
import { ChatBedrock } from 'langchain/chat_models/bedrock';
import { AIMessage, HumanMessage } from 'langchain/schema';

export const runtime = 'edge';

export default defineLazyEventHandler(async () => {
    const llm = new ChatBedrock({
        model: "anthropic.claude-v2",
        region: "us-east-1",
        maxTokens: 1000,
    });

    return defineEventHandler(async event => {
        const { messages } = await readBody(event);
        const llmResult = await llm.predictMessages((messages as Message[]).map(message =>
            message.role === 'user'
                ? new HumanMessage(message.content)
                : new AIMessage(message.content),
        ));
        return llmResult.content;
    });
});
```
上記のコードを上から順に解説します。
1. **インポート**: まず、必要なモジュールとクラスをインポートします。Message、ChatBedrock（Bedrockのチャットモデルを操作する）、AIMessage、HumanMessage（人間とAIのメッセージを表現する）が含まれます。
1. **ランタイム設定**: runtime変数が’edge’に設定されています。これは、このコードがエッジコンピューティング環境で実行されることを示します。
1. **イベントハンドラーの定義**: defineLazyEventHandler関数は、イベントが発生したときに実行される関数を定義します。ChatBedrockのインスタンスも作成しています。
1. **イベントハンドラーの実装**: defineEventHandler関数は、特定のイベントが発生したときに実行される具体的な処理を定義します。イベントの本文からメッセージを取得し、各メッセージについて、その役割（ユーザーまたはAI）に応じて適切なメッセージクラスのインスタンスを作成します。作成したメッセージインスタンスの配列をAIモデルに渡します。
1. **レスポンス**: llmResult.contentを返します。これがAIの応答メッセージです。

　　
### UIとAPI呼び出しの仕組みの実装
次にUIとAPI呼び出しの仕組みを実装します。
App.vueを以下のように書き換えてください。
```App.vue
<template>
  <v-app>
    <v-container>
      <v-sheet v-for="message in messages" :key="message.id" class="my-16 d-flex" :style="{
        position: 'relative',
      }">
        <v-avatar :color="message.role === 'user' ? 'primary' : 'secondary'" class="mr-2">
          <v-icon>{{ message.role === 'user' ? 'mdi-account' : 'mdi-robot' }}</v-icon>
        </v-avatar>
        <v-card :class="message.role === 'user' ? 'user-message' : 'ai-message'" class="pa-4 mb-4" :color="message.role === 'user' ? 'primary' : 'secondary'" :style="{
          borderRadius: '10px',
        }">
          {{ message.role === 'user' ? '私: ' : 'AI: ' }}
          {{ message.content }}
        </v-card>
      </v-sheet>
      <v-form @submit="handleSubmit" :style="{ position: 'fixed', bottom: '20px', width: '80%' }">
        <v-text-field v-model="input" hide-details variant="solo" label="メッセージを送信">
          <template #append-inner>
            <v-icon :color="input ? 'primary' : ''" @click="handleSubmit" :disabled="!input">mdi-send</v-icon>
          </template>
        </v-text-field>
      </v-form>
    </v-container>
  </v-app>
</template>

<script setup>
import { useChat } from 'ai/vue'

const { messages, input, handleSubmit } = useChat({
  headers: { 'Content-Type': 'application/json' },
})
</script>
```
コードを解説します。

**templateタグ**
- v-sheet: メッセージを表示します。v-forディレクティブを使用してmessages配列内の各メッセージに対して生成されます。
- v-avatarとv-icon: Vuetifyのアバターとアイコンコンポーネントで、メッセージの送信者（ユーザーまたはAI）を視覚的に表現します。
- v-card: メッセージの内容を表示します。メッセージの送信者によって異なるスタイルが適用されます。
- v-formとv-text-field: Vuetifyのフォームとテキストフィールドコンポーネントで、ユーザーが新しいメッセージを入力し送信するためのものです。送信ボタンはテキストフィールド内に含まれており、テキストが入力されると色が変わってクリックできるようになります。


**scriptタグ**
- useChat: これはカスタムフックで、チャットボットの状態と動作を管理します。このフックはオブジェクトを返し、その中にはメッセージの配列、現在の入力値、および送信ハンドラーが含まれています。
- headers: これはAPIリクエストに含まれるヘッダーを定義します。この例では、'Content-Type’ヘッダーが’application/json’に設定されています。

　　
`npm run dev`を実行し、メッセージを入力してエンターキーもしくは送信アイコンを押すと、以下のようにチャットができます。
AIからの回答がストリーミングではないため、実際のアプリケーションを開発する際には、メッセージを送信してから回答が返されるまでの間に、ローディングを実装するのがおすすめです。
![画像 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/a646484a-83e4-c42e-4997-801b2eb85697.png)

## おわりに
Nuxt3とVuetify3、LangChain、Bedrockを使ってチャットボットを作成してみました。
フロントエンド側でLangChainが呼び出せるのは便利ですね！

最後まで読んでいただき、ありがとうございました！

記事に関する質問等ございましたら、コメントまたは以下のDMにてよろしくお願いします！

https://twitter.com/t___tatsuki

## 参考文献

https://js.langchain.com/docs/modules/model_io/models/chat/integrations/bedrock

https://js.langchain.com/docs/get_started/quickstart

https://github.com/vercel/ai/tree/main/examples/nuxt-langchain

https://sdk.vercel.ai/docs/guides/frameworks/nuxt