PaLM2 を用いた AI チャット開発 (ハンズオン形式)

この記事は、フラー株式会社 Advent Calendar 2023 の 15 日目の記事となっています。14 日目は@chooblarin さんによる「[Riveでアニメーションやってみた](https://chooblarin.com/post/hello-rive-app)」でした！

# はじめに
初めまして！ 開志専門職大学 3 年(25 卒)のかずや(@bearone236)と申します。現在はフラー株式会社のもとで長期実務実習を行っています。(ブログ内容は実習内容と全く関係ありません。)

今回は AI モデルの 1 つである**PaLM2**を用いて、下記の画像のような簡易的な AI チャットを**ハンズオン形式**で開発していきます！

![開発画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3539543/0cbfa61b-7452-f78b-4572-104e5f1dc959.png)

まだまだ学習中の身であるので、皆さんと一緒に情報共有しながら楽しんで継続して開発していければ嬉しいです ☺️

# PaLM2 とは

[PaLM2](https://ai.google/discover/palm2/) とは、Google が開発した**Google PaLM (Pathways Language Model)の後継モデル**です。2022 年 4 月に PaLM が初めて発表され、翌年 2023 年 5 月 11 日に PaLM2 が発表されました。

PaLM2 は、言語モデルの進化をさらに促進し、多くの場面で高い性能を発揮することが期待されており、現在ではチャットサービスで知られている『**Bard**』や検索サービスへの試験導入である『**SGE**』などにも使われる、同社の基盤技術となっているようです。


:::note
◯ **LLM (Large Language Model)とは：**
非常に巨大なデータセットとディープラーニングの技術を総結集して構築された大規模言語モデルです。
大規模言語モデルは、人間に非常に近い会話を生成することが可能であり、高精度で自然言語処理を行うことから世界から注目を集めているカテゴリーです。
:::


最近のニュースとしては、2023 年 12 月 6 日(米国時間)に Google が新たなる AI モデル『**[Gemini](https://deepmind.google/technologies/gemini/#introduction)**』を発表し世界を大きく賑わせたことで有名です。今後が非常に楽しみになってくる話題ですね...
# 技術スタック

◯ フロントエンド
・ Next.js： v14.0.3

◯ バックエンド
・ Nest.js： v10.0.0

◯ 使用言語
・ TypeScript： v5.1.3

◯ UI ツール
・ Tailwindcss： v3.3.6

# 技術構成図・ディレクトリ構成図

**◯ 技術構成図**
![技術構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3539543/2ed3be60-2854-f1eb-2fc6-6cff87b6603b.png)


**◯ ディレクトリ構成図**

```
PaLM2-Chat/
      ├ nest-backend/  (バックエンド)
      └ next-frontend/ (フロントエンド)
```

# 気になる方に向けて

https://github.com/bearone236/PaLM2-nextjs-frontend

https://github.com/bearone236/PaLM2-nestjs-backend

:::note
上記のプロジェクトでは、『**shadcn/ui**』 と呼ばれる UI スタイリングツール使用して開発しています。気になる方は公式サイトでインストールして学習してみてください！
:::

# ハンズオン開発前の注意点

今回は、PaLM2 の機能の 1 つである「Chat」を使用して開発を行っていきます。

具体的な PaLM2 の扱い方や機能の説明には膨大な時間がかかってしまい書ききれないため、今回は省略してハンズオン開発を進めていきます。

踏み込んで学習を行いたい方は[公式サイト](https://developers.generativeai.google/guide)をご確認ください。

# PaLM2 の API キー取得手順

1. [こちら](https://developers.generativeai.google/tutorials/setup?hl=ja)の公式サイトを開き、下記画像赤枠の「API を取得する」ボタンを押してください

![手順1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3539543/4c81388c-8221-05cb-2d1b-949dceffcc07.png)


2. [下記の画像](https://makersuite.google.com/app/apikey?hl=ja)のサイトに遷移するため、赤枠の「Create API key in new project」ボタンを押してください。これを押すことで API キーが表示されます。 (後ほど使用するためコピーしてください)

![手順2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3539543/b8a4033b-4936-7095-d0c0-823318a395e5.png)


:::note
API のテストを行いたい方は、下記のコマンドをターミナルに打ち込んでみてください。 (YOUR_API_KEY は先ほどの API キーを入力してください)

```
curl \
-H 'Content-Type: application/json' \
-d '{ "prompt": { "text": "Write a story about a magic backpack"} }' \
"https://generativelanguage.googleapis.com/v1beta3/models/text-bison-001:generateText?key=YOUR_API_KEY"
```

:::

# バックエンド開発

### 環境構築

①： グローバルに `nestjs/cli`をインストール

```
npm install -g @nestjs/cli
```

②： Nestjs のプロジェクト作成
   ・事前にフロントエンド/バックエンドをまとめるルートディレクトリを作成、移動しておいてください

```
nest new nest-backend && cd nest-backend
```

③： 今回使用する外部パッケージのインストール

```
npm install express node-fetch@2.7.0 dotenv @nestjs/common
```

### PaLM2 API の実装コード

①： 環境変数の設定
   ・src 直下に`.env` ファイルを作成し、上記で取得した PaLM2 API キーを変数に代入する

```
LANGUAGE_MODEL_API_KEY="YOUR_API_KEY"
```

②： ポート番号変更
   ・src/main.ts ファイルの PORT 番号を 8000 番に変更する (フロントエンド側で 3000 番を使用しているため)

```diff_javascript
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

- await app.listen(3000);
+ await app.listen(8000);
}
bootstrap();

```

③： Nest コントローラの作成/移動
   ・srcディレクトリ直下で下記のコマンドを実行

```
nest generate controller language-model && cd language-model
```

④： 実装コード一覧

<details><summary>src/main.tsファイル</summary>

```rb
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  await app.listen(8000);
}
bootstrap();
```

</details>

<details><summary>src/language-model/language-model.controller.tsファイル</summary>

```rb
import { Controller, Get, Param, Res } from '@nestjs/common';
import { Response } from 'express';
import fetch from 'node-fetch';
import { config } from 'dotenv';
config();

@Controller('prompt')
export class LanguageModelController {
  private readonly LANGUAGE_MODEL_API_KEY = process.env.LANGUAGE_MODEL_API_KEY;
  private readonly LANGUAGE_MODEL_URL = `https://generativelanguage.googleapis.com/v1beta1/models/chat-bison-001:generateMessage?key=${this.LANGUAGE_MODEL_API_KEY}`;

  @Get(':text')
  async getxResponse(@Param('text') text: string, @Res() res: Response) {
    const payload = {
      prompt: {
        messages: [{ content: text }],
      },
      temperature: 0.1,
      candidate_count: 1,
    };

    try {
      const response = await fetch(this.LANGUAGE_MODEL_URL, {
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
        method: 'POST',
      });

      if (!response.ok) {
        throw new Error(`Error: ${response.status}`);
      }

      const data = await response.json();
      res.send(data);
    } catch (error) {
      console.error('Error fetching data:', error);
      res.status(500).send('Internal Server Error');
    }
  }
}
```

</details>

### API テスト

◯ 実行コマンド

```
npm run start
```

・`localhost:8000/prompt/hello(入力したい英語)`を入力すると...

```
{"candidates":[{"author":"1","content":"Hello! How can I help you today?"}],"messages":[{"author":"0","content":"hello"}]}
```

→ **`Hello! How can I help you today?`** が出力されていることが確認！(<font color="Red">**成功**</font>)

# フロントエンド開発

### 環境構築

Next.js 環境を構築する (下記のコマンド・設定に合わせてください)

```
npx create-next-app@latest next-frontend


Need to install the following packages:
create-next-app@14.0.4
Ok to proceed? (y)
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
✔ What import alias would you like configured? … @/*
```

### 実装コード・実行コマンド

◯ 実装コード

<details><summary>src/app/page.tsxファイル</summary>

```rb
"use client";

import { useEffect, useRef, useState } from "react";

export default function Home() {
  const [text, setText] = useState<string>("");
  const [messages, setMessages] = useState<{ author: string; bot: string }[]>(
    []
  );
  const feedRef = useRef<HTMLDivElement>(null);
  const [loading, setLoading] = useState<boolean>(false);

  const getResponse = async () => {
    setText("");
    setLoading(true);

    try {
      const response = await fetch(`http://localhost:8000/prompt/${text}`);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();

      setMessages([
        ...messages,
        { author: data.messages[0].content, bot: data.candidates[0].content },
      ]);
      setLoading(false);
    } catch (error) {
      setLoading(false);

      console.error("Error fetching response:", error);
    }
  };

  useEffect(() => {
    if (feedRef.current) {
      feedRef.current.scrollTop = feedRef.current.scrollHeight;
    }
  }, [messages]);

  return (
    <div className="flex justify-center items-center p-4 my-[7vh]">
      <div className="w-full max-w-4xl">
        <div className="text-center mb-10">
          <h3 className="text-2xl font-semibold ">Chat With</h3>
          <h2 className="text-5xl font-bold mb-4">PaLM 2 Bot</h2>
          <p className="mt-6">Type in English.</p>
        </div>
        <div className=" bg-white rounded-2xl shadow-xl mb-4 overflow-hidden border-2 feed">
          <div ref={feedRef} className="p-6 overflow-y-auto h-96">
            {messages.map((message, index) => (
              <div key={index} className="mb-4 last:mb-0">
                {message.author && (
                  <div className="w-64 md:w-[50%] max-w-full p-5 my-2 mx-6 rounded-lg shadow-md text-gray-700 bg-green-200 ml-auto">
                    {message.author}
                  </div>
                )}
                {message.bot && (
                  <div className="w-64 md:w-[50%] max-w-full p-5 my-2 mx-6 rounded-lg shadow-md text-gray-700 bg-gray-100">
                    {message.bot}
                  </div>
                )}
              </div>
            ))}
          </div>
        </div>

        <div className="gap-2 flex mt-10">
          <textarea
            placeholder="Type here..."
            disabled={loading}
            className={`w-full p-3 rounded-lg border border-gray-400 bg-white text-gray-700  resize-none hover:resize overflow-y-auto focus:outline-gray-500 ${
              loading && "hover:cursor-not-allowed"
            }`}
            value={text}
            onChange={(e) => setText(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                getResponse();
              }
            }}
          />
          <button
            onClick={getResponse}
            disabled={loading}
            className={`md:h-auto w-32 text-white font-bold py-2 px-4  rounded-lg ${
              loading
                ? "bg-gray-400 hover:cursor-not-allowed"
                : "bg-gray-500 hover:bg-gray-600"
            }`}
          >
            {loading ? "Loading..." : "Send"}
          </button>
        </div>
      </div>
    </div>
  );
}
```

</details>



◯ 実行コマンド

```
cd next-frontend && npm run dev
```

# 最後に

初めて PaLM2 と呼ばれる AI 関連 API を用いて開発を行ってみました。

今では ChatGPT API や、speech to text を実現する「**Whisper API**」など、エンジニアが気軽に開発できる環境が整い始めています。使うだけでなく作るからこそ気づくこともあると思われます。

今後も継続して開発をしていきますので一緒にエンジニア界隈を盛り上げましょう 🔥

続いて、フラー株式会社 Advent Calendar15 日目は、@nosse さんの「**フロントエンド未経験者が仕事をするためにやった1ヶ月の勉強**」です！

# 参考文献

https://developers.generativeai.google/guide?hl=ja

https://deepmind.google/technologies/gemini/#introduction

https://www.nri.com/jp/knowledge/glossary/lst/ta/llm

https://www.businessinsider.jp/post-279519