Web開発の新境地: OpenAIとFirebaseを活用したNext.jsでのAPIレスポンスのリアルタイムレンダリング

:::note info 
この記事はシスコの有志による Cisco Systems Japan Advent Calendar 2023 の 13日目として投稿しています。
:::
2017年版: https://qiita.com/advent-calendar/2017/cisco
2018年版: https://qiita.com/advent-calendar/2018/cisco
2019年版: https://qiita.com/advent-calendar/2019/cisco
2020年版 1枚目: https://qiita.com/advent-calendar/2020/cisco
2020年版 2枚目: https://qiita.com/advent-calendar/2020/cisco2
2021年版 1枚目https://qiita.com/advent-calendar/2021/cisco
2021年版 2枚目https://qiita.com/advent-calendar/2021/cisco2
2022年版(1,2): https://qiita.com/advent-calendar/2022/cisco
2023年版: https://qiita.com/advent-calendar/2023/cisco<---こちら

# プロダクト
![Screenshot 2023-12-12 at 19.46.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/e0008ac5-ecb1-bb66-3946-d3e8d350b7f5.png)

タイトルは **API-TRANSLATOR** です。
サイトURLは、 **[こちら](https://auto-api-translator.firebaseapp.com/)** です。
ソースコードは **[こちら](https://github.com/uzumal/auto-translate-api-into-UI)** です。

#### 注意
このプロダクトは表、グラフ生成にすごい時間がかかるため（OpenAIの処理に時間がかかるため）、ご了承ください。

# 開発背景
業務の中で、Cisco製品である[ThousandEyes](https://www.thousandeyes.com/ja-jp/)のAPIのレスポンス結果を一目で可視化できるものがあればいいのに…という一言から始まりました。

では、ThousandEyesに限らず、どのようなAPIのレスポンスでも表やグラフで表示できるモノがあれば、後々便利なんではないかと思い、開発に至りました。

しかし、APIのレスポンスは、使用するAPIによって多種多様であり、常に最適に表示するのは難しいな…と。

そこで、今話題のOpenAIを使用すれば、ある程度、APIレスポンスに応じて最適な可視化を行なってくれるのでは！と考え、APIレスポンスをOpenAIに渡し、表・グラフ表示してくれる仕組みを作成しました。

# 環境構成
![Screenshot 2023-12-12 at 19.13.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/3a385a8f-9d87-3d29-b281-7eb4112b2172.png)
#### 操作手順
1. Webサイトにアクセス
1. Googleアカウントによる認証
1. 使用したいAPI（必要に応じてAPI KEY）を入力し、実行
1. バックエンドのFirebase FunctionsがAPIを実行
1. 返ってきたAPIレスポンスをOpenAI APIに渡す
1. OpenAI APIからのレスポンスを動的にDOMに挿入し、レンダリング

```jsx:OpenAI APIへ渡す Prompt
export const PRE_TEXT =
  "Build an HTML page using the latest CSS framework like Bulma and jQuery and pass the code. In doing so, focus on keeping everything within a single HTML page and ensuring that all JSON data is accurately and properly represented. You should focus on accurately displaying all JSON data, use semantic HTML, be user-friendly, beautifully designed and colourful. Placement should also be clean; for CSS, jQuery, search and use the latest frame work, do not use custom CSS; for JavaScript, use the latest jQuery to parse the JSON data and display it properly. Each JSON key and value should be organised into a separate card and displayed with attention to specific data types (text, numbers, dates). Display graphs only according to the nature and relevance of the data. Instruct users to search for the most up-to-date information each time and to seek out the best use of the information.JSON is here: \n";
```
実際の使用の流れは後々説明します。

# 分かっておくと理解しやすい技術群
### Next.js
Reactアプリケーションをより高度に構築するためのフレームワークです。
簡潔に言うと、Webページを構築するためのツールです。
### TypeScript
JavaScriptを拡張して作られたプログラミング言語です。
2014年頃にMicrosoftによって開発・発表されました。

TypeScriptで書かれたコードをコンパイルするとJavaScriptのコードに変換されるので、JavaScriptファイルが実行できる環境ならすぐに使えて、JavaScriptライブラリもTypeScriptから使用できるなど、互換性の高さが特徴です。

### Firebase
Google が提供するモバイルおよびウェブ開発プラットフォームです。
簡潔に言うと、静的ホスティングを行なってくれるツールです。
他にも、Firebase Authenticationという機能を使用して、認証を行えたり、Firebase Functionsという機能を使用して、特定の関数をバックエンドで実行してくれる、便利なプラットフォームです。
### OpenAI API
近年話題である、ChatGPTをAPIベースで使用できるサービスです。
現在は、GPT-4もAPIベースで使用可能であり、今回の開発環境でもこちらを利用しています。

# ディレクトリ階層
まず、大前提としてこの開発環境はかなり汚いです！
あまりまとまった時間が取れなかったので、<font color="red">__ディレクトリ階層、コード、諸々が汚いままデプロイしています。__</font>
そのため、このプロダクトも挙動としては完璧でない部分がたくさんあるので、優しく見ていただけると幸いです。
なので、あ〜こんな汚い環境で開発を行なっていたんだな程度で流し見してください。

```:大まかなディレクトリ階層
.
├── .github
│   └──CI/CD用のworkflow
├── app
│   ├── page.tsx
│   └── auth_dir (認証用)
│       └── page.tsx
├── components_dir（各コンポーネント格納）
├── constants_dir（OpenAI APIに渡すPrompt記載）
├── firebase_dir（FirebaseのAPI Keyなど格納：.env）
└── functions_dir（Firebase Functions用の関数記載）
    ├── index.ts
    └── .env（OpenAI APIKeyを記載）
```

# 環境構築の流れ/注意点
ここでは、フロントエンド側の具体的な手順などは省くので、随時、ドキュメントをご覧ください。
- [OpenAI](https://platform.openai.com/docs/api-reference)
- [Firebase](https://firebase.google.com/docs?hl=ja)
- [Next.js](https://nextjs.org/docs)
## OpenAI API-Keyの取得
#### 1. *[OpenAI](https://platform.openai.com/docs/overview)にアクセスし、アカウント作成/ログインを行います。*
#### 2. *API Keysを選択します*
![Screenshot 2023-12-12 at 11.51.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/a423c4ad-f861-2345-ec92-3f8322d4405f.png)
#### 3. *Create new secret keyを選択すると、API Keyが一度だけ表示されるので、そちらをコピーしておきます。*
:::note warn
ここでは、既にアカウントにAPI Keyにかかる費用分の金額を振り込み済みとします。
:::

![Picture1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/0dfe1950-4c2a-7ef2-890c-6b555cf14035.png)

## Firebaseの利用
#### 1. *プロジェクトを作成します。*
こちらもドキュメントを見ることをお勧めします。ドキュメントに沿ってプロジェクトを作成すれば、基本は失敗しません。
#### 2. *Authenticationの設定*
今回は、Googleアカウントによる認証を行なっているので、Googleでのログインを可能にしておきます。
<font color="Green">__先ほどのAPI KeyやFirebase Functionsは従量課金制なので、本環境では、過度なリクエストを防ぐために、アカウントごとに実行回数制限を付けています。__</font>
![Screenshot 2023-12-12 at 11.58.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/882e163a-c5c2-8a08-ef89-2f77d8f6be2b.png)
#### 3. *Functionsに関数を設定*
開発環境にて、functionsディレクトリ配下のindex.tsにて関数を記載しています。
本環境では二つの関数を作成しています。
どちらも、Functionsを利用する際に、`functions.https.onCall`を利用しています。
`functions.https.onRequest`メソッドと異なり、APIを呼び出す際に、FirebaseのSDKを使用しています。
簡単にいうと、**従来のFunctionsは誰でもcurlを打ててしまったのですが、こちらは認証をこっそりと行なっているイメージです。**

##### apiTest関数（ユーザが入力するAPIの実行）
```jsx
exports.apiTest = functions.https.onCall(async (data, context) => {
    try {
      // データからパラメータを取得
      const url = data.url;
      const apiKey = data.apiKey;
  
      if (!url) {
        throw new functions.https.HttpsError('invalid-argument', 'Required parameters are missing');
      }
  
      const config = {
        headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
      };
  
      // axiosを使用して外部APIを呼び出す
      const response = await axios.get(url, config);
  
      console.log("Request data", data);
      console.log("Response: ", response.data);
  
      // レスポンスを返す
      return response.data;
    } catch (error) {
      console.error("Error:", error);
      throw new functions.https.HttpsError('internal', 'Internal Server Error');
    }
  });
```

##### callOpenAI（OpenAI APIの実行）

```jsx
exports.callOpenAI = functions.https.onCall(async (data, context) => {
  try {
    const { prompt, max_tokens } = data;
    console.log("prompt" + prompt);
    console.log("token" + max_tokens);

    const openAIResponse = await axios({
      method: "post",
      url: "https://api.openai.com/v1/chat/completions",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,
      },
      data: {
        model: "gpt-4",
        messages: [
          {"role": "user", "content": prompt}
        ],
        max_tokens: max_tokens,
      },
    });
    console.log("Response from OpenAI: ", openAIResponse.data);
    return openAIResponse.data;
  } catch (error) {
    console.error("Error calling OpenAI:", error);
    throw new functions.https.HttpsError(
      "internal",
      "Error calling OpenAI API"
    );
  }
});
```
#### 4. *環境変数の設定*
Next.jsでは、環境変数を利用する場合
- `NEXT_PUBLIC_hogehoge`
という形式で記述する必要がありますのでご注意ください。

Functionsでは、OpenAI APIのAPI Keyを`process.env.NEXT_PUBLIC_OPENAI_API_KEY`で呼び出しているので、.envを用意する必要があります。
```bash
NEXT_PUBLIC_OPENAI_API_KEY=sk-{your openai apikey}
```
用意した.envファイルをFunctionsで利用したい場合は、先ほどのディレクトリ階層でのFunctions配下に配置する必要があるので、ご注意ください。
```:大まかなディレクトリ階層
.
├── .github
│   └──CI/CD用のworkflow
├── app
│   ├── page.tsx
│   └── auth_dir (認証用)
│       └── page.tsx
├── components_dir（各コンポーネント格納）
├── constants_dir（OpenAI APIに渡すPrompt記載）
├── firebase_dir（FirebaseのAPI Keyなど格納：.env）
└── functions_dir（Firebase Functions用の関数記載）
    ├── index.ts
    └── .env（OpenAI APIKeyを記載）
```

#### 5. *Functionsへのデプロイ*
```bash
firebase deploy --only functions
```
上記のコマンドを打つと、Firebase上のFunctionsに反映されています。
また、こちらから、Functions内の`console.log`などを確認することも可能です。
![Screenshot 2023-12-12 at 12.08.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/84eb1474-3379-600e-2229-4f56fbf05cfd.png)

#### 6. *Next.js上での注意点*
Next.jsには、以下4つのレンダリングの実装方法があります。

- Client-Side Rendering (CSR)
- Server-Side Rendering (SSR)
- Static-Site Generation (SSG)
- Incremental Static Regeneration (ISR)


今回はCSRで実装し、API操作などは都度バックエンド（Functions）で処理しています。
どの実装方法でも、デフォルトではFirebaseへのデプロイに失敗するので、コードを修正します。

```jsx:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false,
  output: 'export',
  images: {
    unoptimized: true,
  }
};

module.exports = nextConfig;
```
- CSRでの実装の場合、`output: 'export'`としなければ静的ファイルを正しく出すことができません。
- `reactStrictMode: false`はローカル開発環境下での設定です。
    - `npm run dev`時に、APIがなぜか2回処理されてしまう（Next.jsの仕様）ので、それを防いでます。
```bash:firebase.json
{
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log"
      ],
      "predeploy": [
        "npm --prefix \"$RESOURCE_DIR\" run lint",
        "npm --prefix \"$RESOURCE_DIR\" run build"
      ]
    }
  ],
  "hosting": {
    "site": "auto-api-translator",
    "public": "out",
    "cleanUrls": true,
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

```

## APIレスポンスを受け取るまでの流れ
<a name="h1-anchor"></a>
### Webサイトにアクセス
サイトURLは、 **[こちら](https://auto-api-translator.firebaseapp.com/)** にアクセスします。

:::note warn
レイアウトはそこまで手を着けれてません…ご了承ください。
:::

![Screenshot 2023-12-12 at 16.12.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/d5eea456-4e58-885f-43c7-57dd6b954da0.png)

### Googleアカウントによる認証
`Sign in with Google`をクリックすると、PopupでGoogleアカウントへのログイン画面に遷移します。

![Picture1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/56a208b8-118d-d5f7-66c9-8cd206fe16ad.png)

### 使用したいAPI（必要に応じてAPI KEY）を入力し、実行
今回は、ThousandEyesのAPI（`https://api.thousandeyes.com/v6/endpoint-tests`） を実行しようと思ったのですが、後でレスポンスを見てみると、XMLでレスポンスが返ってくるのですね…
このプロダクトはJSONが返ってくることを想定して作成したので、ThousandEyesは使用できませんでした…（気分が乗れば、XML用のコードも追加します）
そのため、[Cisco Meraki](https://meraki.cisco.com/ja-jp/)のAPI（`https://api.meraki.com/api/v1/organizations/537758/configTemplates`）を使って試したいと思います。

:::note warn
レスポンスがJSONのAPIにのみ対応
:::

![Screenshot 2023-12-12 at 16.40.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/f4d25597-a67f-74bd-bbb6-dc7c669a50e3.png)

### バックエンドのFirebase FunctionsがAPIを実行
APIをFirebase Functionsが実行し、そのAPIレスポンスを受け取っています。
**APIレスポンスを正しく受け取ることができれば**、返ってきたAPIレスポンスをOpenAI APIにPromptとして渡します。

![Screenshot 2023-12-12 at 17.09.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/8c336352-83a8-2265-0665-06467f2f874e.png)

#### 仮にうまくいかない動作しない場合
アカウントに紐付けられた1日の実行制限を超えた場合は以下のように表示されます。

![Screenshot 2023-12-12 at 19.48.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/ff01dd39-26cc-0e62-49d9-b8d617b02043.png)

これは、Firebase Firestoreを利用して、アカウントごとに、APIを実行した回数をチェックしているためです。
**アカウントは基本、UIDで利用していますし、Googleアカウントでの認証なので、機密情報が漏れることは一切ありません。** 

![Screenshot 2023-12-12 at 17.14.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3624832/af4e3105-955b-a8e1-a1dd-8d151cf96274.png)

:::note alert
アカウントの **1日** の実行回数は **10回** まで
:::


### OpenAI APIからのレスポンスを動的にDOMに挿入し、レンダリング
Promptを用いて、OpenAI APIを実行し、その内容を動的にレンダリングしています。

:::note info
毎回、AIがコードを作成しているので、APIを叩くたびに異なるデザインで表示します
:::

:::note alert
財政の問題で、APIに送受信できる総量を抑えているので、JSONが長すぎると処理できません
:::

実行結果はこのページの初めを参照してください。

# 最後に
実際にOpenAIを使用してみて感じたこととして
- 単純な命令なら、驚くほど実現してくれる
- 複雑な命令になるほど、思ってもみない挙動を起こす
- ある程度のサンプルコードは作成してくれるが、利活用できるほどではない…

すごく未来のあるAPIだなと感じるとともに、まだまだプログラマーの力が必要な時代なんだなとつくづくと思いました。特に、GenerativeAIはあくまで学習した知見と、Web検索した内容をもとに返事をするだけなので、Next.jsなどの依存関係などまでは対応できず、ほとんどがうまくいかなかったなという所感でした。
と、悪い部分も言いましたが、昔では考えられないほど作業も楽になりましたし、今回のプロダクトは中枢にOpenAIがいなければ成り立たないものなので、とんでもない恩恵を感じました。
今後の進化に期待ですね！

# 免責事項
本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。