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

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

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

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

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

## 完成イメージ
チャットボットの完成イメージはこちらです。
Vuetifyを使って、チャットっぽいデザインにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/676ebe91-86f5-8777-4ed5-80cfc72e7345.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
```

### OpenAIのAPIキーの設定
ルートディレクトリ直下に.envファイルを作成します。
OpenAIのAPIキーはご自身のものに合わせてください。
```.env
NUXT_OPENAI_API_KEY="[OpenAIのAPIキー]"
```

nuxt.config.tsファイルに、APIキー設定の記述を追加します。

```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,
    },
  },

  //追加
  runtimeConfig: {
    openaiApiKey: process.env.NUXT_OPENAI_API_KEY,
  },
});
```

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

export const runtime = 'edge';

export default defineLazyEventHandler(() => {
    const apiKey = useRuntimeConfig().openaiApiKey;
    if (!apiKey) {
        throw createError('Missing OpenAI API key');
    }
    const llm = new ChatOpenAI({
        openAIApiKey: apiKey,
        streaming: true,
    });

    return defineEventHandler(async event => {
        const { messages } = await readBody(event);

        const { stream, handlers } = LangChainStream();
        llm
            .call(
                (messages as Message[]).map(message =>
                    message.role === 'user'
                        ? new HumanMessage(message.content)
                        : new AIMessage(message.content),
                ),
                {},
                [handlers],
            )
            .catch(console.error);

        return new StreamingTextResponse(stream);
    });
});
```
上記のコードを上から順に解説します。
1. **インポート**: まず、必要なモジュールとクラスをインポートします。LangChainStream、Message、StreamingTextResponse（AIからのメッセージをストリーミングする）、ChatOpenAI（OpenAIのチャットモデルを操作する）、AIMessage、HumanMessage（人間とAIのメッセージを表現する）が含まれます。
1. **ランタイム設定**: runtime変数が’edge’に設定されています。これは、このコードがエッジコンピューティング環境で実行されることを示します。
1. **イベントハンドラーの定義**: defineLazyEventHandler関数は、イベントが発生したときに実行される関数を定義します。この関数内で、OpenAI APIキーが存在するかどうかを確認し、存在しない場合はエラーを返します。その後、新しいChatOpenAIインスタンスを作成します。
1. **イベントハンドラーの実装**: defineEventHandler関数は、特定のイベントが発生したときに実行される具体的な処理を定義します。この関数内で、リクエストからメッセージを抽出し、それらを適切な形式（人間またはAI）に変換してから、ChatOpenAIインスタンスのcallメソッドに渡します。
1. **ストリーミングレスポンス**: 新しいStreamingTextResponseインスタンスが作成されます。これは、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`を実行し、メッセージを入力してエンターキーもしくは送信アイコンを押すと、以下のようにチャットができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/676ebe91-86f5-8777-4ed5-80cfc72e7345.png)


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

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

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

https://twitter.com/t___tatsuki

## 参考文献

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

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