祝 Azure OpenAI Service GA! ⑤幼稚園生の疑問に答えてくれる "しま〇ろう BOT" を ChatGPT で作ってみる

1/16 に GA (General Availability: 一般提供) になった [Azure OpenAI Service](https://azure.microsoft.com/ja-jp/products/cognitive-services/openai-service) は、Azure 上で OpenAI が稼働するサービスです。Cognitive Services の一員として & Web API として利用できる、Studio と呼ばれる GUI が用意されており、テストや調整などが可能です。

今回は、Azure OpenAI Service にも用意されている ChatGPT を使って、幼稚園生の疑問に優しく答えてくれる(⁈) しま〇ろう BOT を Web アプリ (Node.JS) として実装してみます。

![スクリーンショット 2023-04-10 222559.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/fa46d3da-ad98-4420-c025-80ae1b7509cb.png)

> 3/10 現在、GA したとはいえ、Azure OpenAI Service を利用するには [Web から利用申請](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUOFA5Qk1UWDRBMjg0WFhPMkIzTzhKQ1dWNyQlQCN0PWcu&culture=ja-jp&country=jp)が必要です。**Text and Code Model** (GPT-3) と **Dall-E 2** の申し込みができますが、とりあえず **Text and Code Model** だけ申請するのがおススメ！ChatGPT は Text Model の権限があれば利用できます。また、申請時には有効な Azure サブスクリプション (のID) が必要です。

> Azure OpenAI Service 自体は GA ですが、3/10~ 利用可能になった ChatGPT は現在プレビュー中です。

# 準備

- [Azure OpenAI Studio からお試し](https://qiita.com/annie/items/ce7214c6d683b9442382) の記事を参考に、
    - Azure OpenAI Service のプレビュー申込 & Azure Portal からのサービス作成を行い、キーとエンドポイントをローカルに保存しておきます。
    - ChatGPT モデルの **gpt-35-turbo** をデプロイしておきます。

- Rest API を POST 送信できるツール (こちらでは [Postman](https://www.postman.com/) を使います)

- 開発環境
    - 今回は Node.JS (express) を使って Web アプリを作成します。お好みですが、以下の開発ツール＆環境で動作確認をしています;
        - Windows 11 (x64 10.0.22621)
        - Visual Studio Code (v1.77.1)
        - Node.JS (v18.13.0)
- サンプルコード
    - [GitHub/ayako/AzureOpenAIAppSamples/ChatGPTWeb/ChatGPTWeb.zip](https://github.com/ayako/AzureOpenAIAppSamples/blob/main/ChatGPTWeb/) にアクセスし、ローカル環境にダウンロード＆展開しておきます。

# 手順

## Azure OpenAI Studio からチャットボットの挙動を調整する

[Azure OpenAI Studio](https://oai.azure.com) を開き、Azure OpenAI Service を作成した (or 利用するためのロールを持っている) アカウントでサインインします。

ChatGPT モデルの **gpt-35-turbo** がデプロイ済みであれば、**[ChatGPT プレイグラウンド]** が表示されるので、こちらをクリックします。

![スクリーンショット 2023-04-10 211615.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/85d396eb-b899-cbfe-90df-d3dddbcf25c4.png)

今回は、**アシスタントのセットアップ** の機能を使って、予め回答の方法を指示します。
**システムメッセージ** の欄に指示を入力します。
ここでは、

- 4-6歳児に分かるように回答する
- 親切、かつ親しみやすい口調で回答する

ということを指示しています。
入力したら、**[変更の保存]** をクリックして保存します。

![スクリーンショット 2023-04-10 211722.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/d802a061-600d-b8ba-a7ce-a75184a1d8ac.png)

> **システムメッセージを更新しますか？** というダイアログが表示されたら、**[続行]** をクリックして保存します。
![スクリーンショット 2023-04-10 212112.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/447af48e-121e-ba46-ebfd-3bf09ada53cf.png)

システムメッセージの通りに返答するか、**[チャットセッション]** で入力して確認します。
今回、挨拶や自己紹介は要らないので、その辺りをシステムメッセージで調整します。

![スクリーンショット 2023-04-10 212200.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/c9386460-0293-82d6-24a9-572128d80da2.png)

システムメッセージに「挨拶や自己紹介はしない」という指示を盛り込んで更新、チャットセッションで、チャットをクリアしてから再度質問を入力すると、その通りになりました。(この辺りはいろいろとお好みで調整してください。)

![スクリーンショット 2023-04-10 212754.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/5761ac48-c459-e8be-a6bc-c5a8f58be27f.png)

> 幼稚園児向けを想定＆ひらがな入力しているのに、漢字交じりの回答になっているところはご愛敬、フリガナを付けたり、音声読み上げしたりする、などで別途対応できる範囲ということでご容赦ください。

## Rest API で Azure OpenAI Service (ChatGPT) にアクセスしてみる

### Azure OpenAI Studio から Rest API POST に必要な方法を確認する 

Azure OpenAI Studio の ChatGPT プレイグラウンドで、一旦チャットをクリアしてから、1回質問を入力 → 回答された状態で **コードの表示** をクリックします。

![スクリーンショット 2023-04-10 213357.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/4764239f-f4f1-3d55-9ca4-15588902af27.png)

サンプルコードが表示されます。一番上部に書かれている URL がアクセスする Rest API のURL となります。こちらをコピーしてローカルに保存しておきます。

![スクリーンショット 2023-04-10 213421.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/f6261d7a-be10-e063-9f82-0e8cbeb90cca.png)

サンプルコードの種類を **curl** に変更して、Header に必要な情報を確認します。
Rest API の種類は **POST**、Headerには、**ContentType** (application/json) と **api-key** (Azure Portal で取得してローカルに保存しておいたキー) が必要であることが分かります。

![スクリーンショット 2023-04-10 213444.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/b33093bf-7be8-a604-c235-065fff2beccc.png)

さらに、サンプルコードの種類を **json** に変更して、Rest API のリクエスト本体 (Body) となる JSON を確認します。**[コピー]** をクリックして、ローカルに保存しておきます。

![スクリーンショット 2023-04-10 213516.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/6d1503ad-e241-412a-c73e-0e19b3c1c370.png)

JSON は以下のフォーマットになっていることが確認できます。
ChatGPT からの回答は **assistant** として表示されています。ChatGPT に回答を生成することを指示したい場合は、この **assistant** の回答部分をブランクにして Rest API にリクエストします。(後で再度説明します)

```json
{
  "messages": [
    {
      "role": "system",
      "content": ### システムメッセージ ###
    },
    {
      "role": "user",
      "content": ### ユーザーの質問 ###
    },
    {
      "role": "assistant",
      "content": ### ChatGPTからの返答 ### 
    }
  ],
  "temperature": 0.7,
  "top_p": 0.95,
  "frequency_penalty": 0,
  "presence_penalty": 0,
  "max_tokens": 800,
  "stop": null
}
```

> 他に注意したいポイントは **max_tokens** です。Rest API に POST する Request Body と、返答である Response Body をトークンに変換したときの最大値 (ここでは 800 トークン) の設定となります。この値を大きくすると、複数のやり取り (質問＆回答) を含めて、回答を生成することが可能ですが、今回は一問一答なので、この程度にしておきます。

### Rest API ツール から Azure OpenAI Service にリクエスト送信してみる

Rest API ツール (ここでは Postman) を開き、情報をセットします。

- Type: **POST**
- URL: **これまでの手順でローカルに保存しておいた URL** (https://XXXXXXX.openai.azure.com/openai/....)
- Header: 
    - Content-Type: **application/json**
    - api-key: **Azure Portal からコピーしておいた Azure OpenAI サービスのキー**

![スクリーンショット 2023-04-10 215242.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/d3ae5592-9fe7-e8a1-927a-bb2fc10c6c12.png)

Request Body の部分は、これまでの手順でローカルに保存しておいた JSON をひとまずコピーし、**assistant** の回答部分を "" (ブランク) にします。

例えば、このような内容の JSON になります。

```json
{
  "messages": [
    {
      "role": "system",
      "content": "あなたは「しま〇ろう」というキャラクターです。4-6歳が分かるように話してください。また、口調は親切で親しみやすくしてください。"
    },
    {
      "role": "user",
      "content": "なぜ、よるになるとねむくなるの？"
    },
    {
      "role": "assistant",
      "content": ""
    }
  ],
  "temperature": 0.7,
  "top_p": 0.95,
  "frequency_penalty": 0,
  "presence_penalty": 0,
  "max_tokens": 800,
  "stop": null
}
```

![スクリーンショット 2023-04-10 215303.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/86d9609e-d121-1884-d410-0a79119b4027.png)

情報が入力できたら、**POST** してみます。
Response Body は JSON で表記されて、**Choices** > **message** に返答が書かれています。**"role" : "assistant"** になっているのを確認、**"content"** が実際の返答となります。思ったような返答が返されていればOKです。

![スクリーンショット 2023-04-10 215349.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/52354b5d-8128-b470-644b-6ac0240013f8.png)

> 返答は、POST のたびに毎回生成されるため、同じ質問をしても同じ回答が返されるとは限りません。

## Node.JS アプリに Azure OpenAI Service (ChatGPT) を組み込む

### サンプルコードでひとまず動かしてみる

いよいよ Azure OpenAI Service をアプリに組み込みます。
ローカル環境にダウンロードしておいた [サンプルコード](https://github.com/ayako/AzureOpenAIAppSamples/tree/main/ChatGPTWeb) を開発ツール (今回は Visual Studio Code) で開きます。

![スクリーンショット 2023-04-10 221310.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/305c81cb-d23b-4ddb-f00e-dca3ee03013b.png)

**public** > **index.js** を開き、1~2 行目にある **aoai_url** および **aoai_key** をそれぞれローカルに保存しておいた **RestAPI URL** と **Azure OpenAI Service キー** に置き換えます。

![スクリーンショット 2023-04-10 221331.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/4f6ad751-f0e1-24eb-56aa-2c3bc9832bd1.png)

> 本来なら環境変数として設定すべきですが、今回はお試し(テスト|ハンズオン) ということで、直接情報をソースコードに書き込んでいます。ご注意ください。

ターミナルを開き、```npm i``` コマンドで、必要なライブラリーをインストールします。

![スクリーンショット 2023-04-11 113222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/19329de8-5dd9-7619-6e8f-fdbfadc99602.png)

最後に ```node server.js``` コマンドで Node.js サーバーを起動し、ブラウザーから　[http://localhost:3000](http://localhost:3000) にアクセスします。

![スクリーンショット 2023-04-11 113255.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/064b4bab-6f49-f62a-159c-0c738900cf52.png)

チャット画面が表示されたら、質問を入力して [送信] をクリック (or Shift+Enter で送信できます) します。回答が表示されれば完了です。

![スクリーンショット 2023-04-10 222559.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/fa46d3da-ad98-4420-c025-80ae1b7509cb.png)

> Node.js サーバーを止めるには、ターミナルから Ctrl+C を入力します。

### コード解説

UI 部分は index.html で設定しています。
会話は **section** で表示し、リストで順番にやり取りを表示します。
チャットボット (ChatGPT) からの回答は左寄せ (```div class ="ballon"```)、ユーザーの入力は右寄せ (下記コード内にはありませんが、```div class ="ballon-r"```) です。

```index.html
(前省略)
：
<body>
    <header class="header">
      <h1>しま〇ろうに聞いてみよう！</h1>
    </header>

    <section class="content">
      <ul id="chat-area">
        <li>
          <div class="balloon">
            <img class="img-circle" src="tiger.jpg" alt="image" />
            <p class="talk">こんにちは！</p>
          </div>
        </li>
        <li>
          <div class="balloon">
            <img class="img-circle" src="tiger.jpg" alt="image" />
            <p class="talk">きいてみたいこと、わからないこと、きいてくれたらこたえるよ！</p>
          </div>
        </li>
      </ul>
    </section>
：
(以下省略)
```

> 各要素の設定は **public/index.css** で行っています。

> 今回のトラのアイコン(tiger.jpg)は Bing Image Creator を使って作成してみました。こちらお好みのものを設定してご利用ください。
![スクリーンショット 2023-04-10 141340.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117583/47224019-e246-35e8-969d-270bda8ff822.png)


Azure OpenAI Service への RestAPI リクエストなどは **public/index.js** で行っています。RestAPI へのリクエスト～取得～表示は **postToAOAI** で行っています。
POST する内容は、これまでの手順で Postman からリクエストしたものと同じです。
システムメッセージや各パラメーターはご自身で適時調整してご利用ください。

```index.js
function postToAOAI(req_message) {
  const req_body = {
    "messages": [
      {
        "role": "system",
        "content": "あなたは「しま〇ろう」というキャラクターです。0-6歳の子供が分かるように話してください。また、口調は親切で親しみやすくしてください。"
      },
      {
        "role": "user",
        "content": req_message
      },
      {
        "role": "assistant",
        "content": ""
      }
    ],
    "temperature": 0.7,
    "top_p": 0.95,
    "frequency_penalty": 0,
    "presence_penalty": 0,
    "max_tokens": 800,
    "stop": null
  }

  fetch(aoai_url,{
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'api-key': aoai_key
    },
    body: JSON.stringify(req_body)
  })
  .then((res) => res.json())
  .then((json) => { 
    // 取得内容を表示
    console.log(json);
    showBotMessage(json.choices[0].message.content);
  })
  .catch((error) => {
    console.error('Error:', error.code + ': ' + error.message)
  })
}
```