【ハンズオン】ChatGPT?? プロンプトエンジニアリング?? Webエンジニアが必死で流行りにしがみつく

## 「手を動かさなくなったら終わり。」

私自身、決して得意ではないですが勉強は好きです。
ですので、トレンドの移り変わりが激しく、日々勉強が要求されるソフトウェアエンジニアという職業は割と性に合っていると思って~~います~~いました。

しかし、最近は転職して忙しくなりビジネス寄りの仕事も増え、勉強時間の確保が難しくなってしまいました。そんなところに、ChatGPTブームが直撃しました。もちろん、ChatGPTを使うことはありますが、結局慣れているGoogle検索に落ち着いてばかりで、イマイチ使いこなせている気がしません。

- 「最新技術が登場したらすぐさまキャッチアップして使いこなすという理想のエンジニア像からどんどん離れていってしまっている、、」
- 「このままChatGPTに関して開発者としてだけでなく利用者としても取り残されてしまい、いつかエンジニアとして終わりを迎えてしまうのか、、」

と悲観的になっていたところ、なんと社内で行われる有志のハッカソンに声をかけてもらい、手を動かしながらChatGPT APIで遊ぶ機会を得ることができました。

本記事は、私のように **自ら行動を起こすほどではないが、機会があればChatGPT APIを使って手を動かしてみたいと考えているエンジニア** の皆さんに最初の機会をご提供できればと思います。

## ハンズオン内容

Webエンジニアなら初めて使う技術でもAPIを1本開発すればかなり解像度が上がるだろうということで、私が社内ハッカソンで作成した下記機能を例に進めていきたいと思います。

1. ユーザーAとユーザーBそれぞれの情報を学習したChatGPT同士が架空の会話を行う
    -  **今回こちらは、ChatGPTを使用してダミーデータを作成します**
1. ユーザーAに対して、その架空の会話の要約と友達としてのユーザーBのおすすめ度を伝える
    -  **ChatGPT APIとAzure Logic Appsを使って、ローコードでWeb APIを開発します**

### 前提条件

このハンズオンを進めるにはあらかじめ以下のものを用意していただく必要があります。

- [Azure サブスクリプション](https://azure.microsoft.com/ja-jp/free/cognitive-services/?azure-portal=true)
- [ChatGPT（OpenAI）アカウント](https://openai.com/blog/chatgpt)
- ChatGPT APIを利用できるサービス（下記のうちどちらか一方）
    1. [Azure OpenAI Service](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/overview)
        -  **本記事ではこちらを採用します**
        - 執筆時点(2023/6/12)では、事前に[アクセス申請](https://aka.ms/oai/access)を行う必要があります
    1. [OpenAI API](https://openai.com/blog/introducing-chatgpt-and-whisper-apis)
        - アカウント登録とシークレットキーの取得が必要です

## ChatGPTを使用したダミーデータの作成

流行りに取り残されかけていた私にとって、まず最初の驚きは適当なデータの作成にChatGPTが便利すぎるという点でした。ChatGPTを使えば、それっぽい確からしいダミーデータを簡単に用意することができます。

今回はユーザーAとユーザーBの架空の会話履歴を作成してもらいます。
そして、ChatGPTに指示を出す上で以下のことに注意をします。

- トークン消費量を減らすために、**日本語ではなく英語で会話をしてもらう**
- NoSQLデータベース(Azure Cosmos DB)にそのままアップロードしたいので、**JSON形式で出力してもらう**

特に「英語で会話をしてもらう」という点は重要なポイントです。
ChatGPTはメモリを持ちません。ですので、教えたい情報のすべてを１回のリクエストに含める必要があります。そして、そのすべての情報が最大トークン数以下である必要があります。
私自身も当初は日本語のまま進めていたのですが、ChatGPT APIに会話の要約を生成してもらう際に与える入力データが最大トークン数を上回ってしまいエラーが出てしまいました。
また、トークン消費量はAPI料金に直結するため、可能な限り減らした方が良いと思います。

<details><summary>より詳細な情報</summary>

- 今回試していないですが、より多くのデータをChatGPTに教えたい場合はEmbeddingやGroundingという手法が有効のようです
    - [ChatGPTで独自データを学習させて回答してもらう方法](https://qiita.com/windows222/items/232f05bafa95a9c8874e)
- なぜ英語の方がトークン数が少ないかなど、トークンに関する詳しい情報は[こちらの記事](https://zenn.dev/umi_mori/books/chatbot-chatgpt/viewer/how_to_calculate_openai_api_prices)で解説されています
</details>

以上のことを踏まえて、ChatGPTに依頼を投げます。

> Aさん、Bさんはソフトウェア開発会社の同僚ですが、それぞれ別のプロジェクトに従事していて初対面です。
> 一人二役で10往復以上の雑談を英語でおこなってください。
> 
> 以下がそれぞれの人物の情報です。
> 
> Aさん
> ・UserId: aaa
> ・職位：一般社員、フロントエンドエンジニア
> ・プロジェクト：ヘルスケア系アプリの保守運用
> ・学生時代：サッカー部、生徒会役員
> ・休日すること：ジム、読書
> 
> Bさん
> ・UserId: bbb
> ・職位：マネージャー、デザイナー
> ・プロジェクト：教育系アプリの新規開発
> ・学生時代：吹奏楽部
> ・休日すること：美術館に行く、散歩
> 
> すべての雑談結果を下記JSONフォーマットで出力してください。
> 
> ```json
> [
>     {
>         "UserId": "aaa",
>         "PartnerId": "bbb",
>         "User": {
>             "id": "aaa",
>             "Name": "A"
>         },
>         "Partner": {
>             "id": "bbb",
>             "Name": "B"
>         },
>         "History": [
>             {
>                 "UserId": "",
>                 "Message": "",
>             },
>         ]
>     }
> ]
> ```

そうすると、以下のような返答が来ると思うので、**あとで使用するために`models.json`という名前でJSONファイルを作成して、コピペしておきます。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/60ea160e-9529-f68c-3916-e303a22a449b.png)

:::note warn
出力例のように会話の数が足らないことがよくあるので注意が必要です。
その場合、「10往復以上の会話をお願いします」などと再度依頼すると、期待した出力が得られます。
:::

同様にCさんの情報も与えてダミーデータを作成しておくと比較ができて面白いと思います。
また、今回使用したプロンプトは私の限界ではありますが、まだまだ改善の余地があると思うので、興味がある方は以下の記事を参考に、より良いダミーデータを得るためのプロンプトを設計してみてください。

https://www.promptingguide.ai/jp/introduction/tips

## Azure OpenAI Service

続いて、AzureでChatGPT APIが利用できるように準備していきます。
Azure OpenAIではなくOpenAI APIを利用する場合は、本章を読み飛ばしていただいても構いません。

### リソースの作成とモデルのデプロイ

※この先のハンズオンを進めるには、Azure OpenAI Serviceの[アクセス申請](https://aka.ms/oai/access)が承認されている必要があります。

#### リソースの作成

1. [Azure OpenAI 作成ページ](https://portal.azure.com/?microsoft_azure_marketplace_ItemHideKey=microsoft_openai_tip#create/Microsoft.CognitiveServicesOpenAI) へ移動します
1. 必要な情報を入力します
    - サブスクリプション
        - 今回用意した Azure サブスクリプション
    - リソースグループ
        - `chatgpt-hands-on`
            - 新規作成する必要があります
    - リージョン
        - `East US`
    - 名前
        - サブドメイン名になるので、一意な名前を適当に選んでください
    - 価格レベル
        - `Standard S0`
1. 「確認および作成」を押下し、確認して問題なければ「作成」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/6fd8bf9e-2322-6e89-62d2-bbb7bfe80004.png)

#### モデルのデプロイ

1. リソースの作成が完了したら、「リソースに移動」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/02969e59-3577-173f-37b9-e538f6610486.png)
1. 作成した Azure OpenAI リソースの「概要」タブにある「探索」を押下すると、Azure OpenAI Studio が起動します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/aad84eac-553d-8e17-e665-f70fc1f374d3.png)
1. Azure OpenAI Studio に移動したら、「新しいデプロイの作成」を行います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/4fc67390-b26a-70e0-50e1-92aa30f1b270.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/bc38158b-e74f-5729-85e2-98693ef4d094.png)
1. 以下の情報を入力して、「作成」を押下します
    - モデル
        - `gpt-35-turbo`
    - スケーリング
        - `1`
    - デプロイ名
        - `chatgpt-hands-on-deploy`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/a65cb93c-7d70-5a13-8b6b-99b21a0619cc.png)

### ChatGPT プレイグラウンドで遊んでみる

1. モデルが正常に作成されたら、「プレイグラウンドで開く」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/22bf777a-15ee-bb5f-ceb5-dedde6fa6d59.png)
1. ChatGPT プレイグラウンドに移動したら、適当に遊んでみてください
    - システムメッセージ
        - アシスタントに従ってもらいたい手順またはルールなどの事前情報を入力します
    - ユーザーメッセージ
        - 先ほどのダミーデータの作成依頼のようないわゆる"ChatGPT上でのユーザー入力"と同等です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/9b647b8e-0dbe-4150-f00f-2abe50727c05.png)
1. 会話を重ねるごとに現在のトークン数が増加することが確認できると思います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/275733b2-fc53-fb03-94c2-e0d5a54a78fd.png)
1. さらに「コードの表示」を押下し、「curl」を選択すると実際のリクエスト内容が確認できます
    - `messages`の中身を見ると、**１回のリクエストで会話履歴を含めすべてのデータを送信**していることがわかります
        - `"role":"system"`：システムメッセージの内容
        - `"role":"user"`：ユーザーメッセージの内容
        - `"role":"assistant"`：ChatGPTの返答内容
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/734ca052-e52a-6798-f371-427d17e35465.png)

### 実際に使用するプロンプトを考える

プレイグラウンドで遊んだ結果、システムメッセージとユーザーメッセージを入力としてChatGPT APIにリクエストできることがわかりました。

そして、今回ユーザーメッセージには先ほどChatGPTで作成したダミーの会話履歴のJSONを指定します。
ですので、そのJSONから会話の要約と友達としてのおすすめ度をChatGPT APIに返してもらうように、システムメッセージのプロンプトを設計する必要があります。

#### 今回使用するシステムメッセージ

> アシスタントは、会話履歴から友達になることができるかを判定するプロです。
> ユーザー情報と会話履歴が入ったJSONを受け取って、"User"にとって"Partner"が友達として相応しいかを判定します。
> "User"から"Partner"についてのおすすめ度を聞かれたので、その質問に対して回答します。
> 
> \# 制限事項
> \- 出力は日本語
> \- 重要な情報のみ
> \- 小学生でもわかるように
> \- 文字数は日本語で300文字程度
> \- "User"は"Partner"と会話した内容を知らない
> \- "User"への返答です
> \- おすすめ度を100点満点評価する
> \- おすすめ度は厳しく評価する
> 
> \# 出力
> \- "Partner"の情報も含めた会話のAbstract
> \- 友達としてのおすすめ度

トークン消費量を減らすため、実際にはChatGPTに英語に直してもらったものを使用します。

> The assistant is a professional in determining if someone could become a friend based on conversation history. It receives a JSON containing user information and conversation history, then decides whether the 'Partner' is suitable as a friend for the 'User'. It responds to the question asked by the 'User' regarding the recommendation level of the 'Partner'.
> \# Limitations
> \- Output must be in Japanese
> \- Only important information
> \- Easy enough for a primary schooler
> \- About 300 characters in Japanese
> \- The 'User' is unaware of the content of conversations with the 'Partner'
> \- This is a response to the 'User'
> \- Rate the recommendation on a strict scale out of 100 points
> \- The recommendation should be evaluated strictly
> \# Output
> \- An abstract of the conversation, including information about the 'Partner'
> \- A recommendation score for the 'Partner' as a friend

では、実際に試してみましょう。
上記をシステムメッセージに入力して、先ほどChatGPTで作成したダミーの会話履歴のJSON（一番外側の`[`, `]`は含めないように注意してください）をユーザーメッセージに入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/f2ee58b1-9c8d-5807-9765-0a9371d074c9.png)
会話の要約とユーザーBのおすすめ度に関するユーザーAに対してのメッセージをChatGPT APIから取得することができました。

あとは、これをWeb APIに組み込むことができれば完成です。

## Azure Cosmos DB

Web APIの作成に取りかかる前に最後の下準備を行います。
会話履歴のJSONデータを永続化して、Web APIから呼び出せるようにするために、Azure Cosmos DBをデプロイします。

Azure Cosmos DBはNoSQLデータベースで、非構造化データや半構造化データをJSON形式で格納します。
そこで各データを分散させた上で一意に特定できるようにするために、パーティションキー（とパーティション内で一意性を担保するために自動で振られる*item ID*[`id`]）を設定する必要があります。

今回格納する会話履歴のJSONデータ（ChatGPTに作成してもらったダミーデータ）は、`UserId`と`PartnerId`が決まればデータを一意に特定できます。
はじめは、[階層パーティションキー](https://learn.microsoft.com/ja-jp/azure/cosmos-db/hierarchical-partition-keys?tabs=net-v3%2Cbicep)を使用して`UserId`と`PartnerId`をパーティションキーに指定しようと思ったのですが、Web APIで使用する[Azure Logic Appsのコネクタ](https://learn.microsoft.com/ja-jp/connectors/documentdb/)が階層パーティションキーに対応していなかったため断念しました。
ですので、今回は`UserId`のみをパーティションキーに指定して進めたいと思います。

<details><summary>参考情報</summary>

- [Azure Cosmos DB のデータ モデリング](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/modeling-data)
- [Azure Cosmos DB でのパーティション分割と水平スケーリング](https://learn.microsoft.com/ja-jp/azure/cosmos-db/partitioning-overview)
- `UserId`と`PartnerId`をパーティションキーに指定するためのもう一つの方法
    - [合成パーティション キーの作成](https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/synthetic-partition-keys)
</details>

### リソースの作成とコンテナーの追加

それでは、Azure Cosmos DB アカウントを作成して、データ格納場所であるコンテナーを追加していきます。

#### リソースの作成

1. [Azure Cosmos DB アカウント 作成ページ](https://portal.azure.com/?microsoft_azure_marketplace_ItemHideKey=microsoft_openai_tip#create/Microsoft.DocumentDB) へ移動します
1. 「コア (SQL)」の「作成」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/d689819f-b877-37b1-f82e-e5245cfcd3f1.png)
1. 必要な情報を入力します
    - サブスクリプション
        - 今回用意した Azure サブスクリプション
    - リソースグループ
        - `chatgpt-hands-on`
            - 今回作成したものを選択します
    - アカウント名
        - サブドメイン名になるので、一意な名前を適当に選んでください 
    - 場所
        - `(US) East US`
    - 容量モード
        - `プロビジョニングされたスループット`
    - Free レベル割引の適用
        - `適用`
            - 今回用意したAzureサブスクリプション内に既にFreeレベルのAzure Cosmos DBアカウントが存在する場合は選択できません
    - 合計アカウント スループットを制限する
        - チェックします
1. 「レビュー + 作成」を押下し、確認して問題なければ「作成」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/4db6a2c2-65c1-8ac3-7f46-8099f74fe91a.png)

#### コンテナーの追加

1. リソースの作成が完了したら、「リソースに移動」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/d7540426-b4ec-c55f-e9a5-b705e6809bc3.png)
1. 作成した Azure Cosmos DBアカウントの「概要」タブにある「コンテナーの追加」を押下すると、コンテナー作成ページに遷移します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/cebe3f62-5e31-828d-f128-34e3b11ae44f.png)
1. 必要な情報を入力します
    - Database id
        - `Create new`を選択します
        - `chatgpt-hands-on-db`を入力します
        - `Share throughput across containers`をチェックします
    - Database throughput (autoscale)
        - `Autoscale`を選択します
        - Database Max RU/sを`1000`のままにします
    - Container id
        - 会話データを格納するので`Conversations`とします
    - Indexing
        - `Automatic`を選択します
    - Partition key
        - 先ほど決めた`/UserId`を入力します
1. 「OK」を押下して、コンテナーを作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/8ab5a64d-275c-2290-0c9f-e8ac0a463cfe.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/36064872-884d-168c-0d85-227e9acff3c2.png)

### ダミーデータのアップロード

1. 「データエクスプローラー」タブを開いて、作成した「Conversations」コンテナーの「Items」を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/fcf9739c-0bf0-fde5-10c2-3d44091cc879.png)
1. 「Upload Item」を押下し、ChatGPTでダミーの会話履歴を作成したときにコピペした`models.json`を選択して、「Upload」します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/7fcedfb6-8ab1-9cab-80ee-55444e7984f0.png)
1. データの作成に成功したら、ページを更新します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/83224ab5-270c-f24c-50a6-6985812fedd3.png)
1. 再び「Conversations」コンテナーの「Items」を選択すると、ダミーデータが追加されていることが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/03c382bf-4baa-3809-04d4-07f1fead2e85.png)

## Azure Logic Apps

全ての準備が整いましたので、いよいよ Web API の作成に入ります！
本ハンズオンでは、Azure Logic Appsを使用してローコードでWeb APIを開発します。

処理の大まかな流れは以下の通りです。

1. HTTP POST リクエスト で、`userId`と`partnerId`を受け取ります
1. 受け取った`userId`と`partnerId`に紐づく会話履歴をAzure Cosmos DBから取得します
1. 取得した会話履歴のJSONデータ（ユーザーメッセージ）と事前に用意したシステムメッセージを入力としてChatGPT API（Azure OpenAI Service）にリクエストを送ります
1. ChatGPT APIのレスポンスを`Summary`として追加し、Azure Cosmos DBのデータを更新します
1. `user`, `partner`, `summary` を Web API のレスポンスとして返却します

### リソースの作成とLogic Apps デザイナーの開始

まず、Logic Apps リソースを作成して、ワークフローを視覚的に作成するためのLogic Apps デザイナーを開始します

#### リソースの作成

1. [Azure Logic Apps 作成ページ](https://portal.azure.com/?microsoft_azure_marketplace_ItemHideKey=microsoft_openai_tip#create/Microsoft.LogicApp) へ移動します
1. 必要な情報を入力します
    - サブスクリプション
        - 今回用意した Azure サブスクリプション
    - リソースグループ
        - `chatgpt-hands-on`
            - 今回作成したものを選択します
    - ロジック アプリ名
        - サブドメイン名になるので、一意な名前を適当に選んでください 
    - 地域
        - `East US`
    - ログ分析を有効化
        - `いいえ`
    - プランの種類
        - `消費`
    - ゾーン冗長
        - `無効`
1. 「確認および作成」を押下し、確認して問題なければ「作成」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/6c10421e-a49c-ab4f-378f-6e64254a1d85.png)

#### Logic Apps デザイナーの開始

1. リソースの作成が完了したら、「リソースに移動」を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/444d2b20-5bfa-6345-c1b5-8929b9c05690.png)
1. Logic Apps デザイナーの初期画面に移動するので、Web API用トリガーである「HTTP 要求の受信時」を選択して、Logic Apps デザイナーを開始します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/b98ccdf1-b6c9-d630-c49e-deed1231a7b4.png)

### ワークフローの作成

それでは、実際にLogic Apps デザイナーを用いて、ローコードでWeb APIを開発していきます。

#### HTTPリクエストを処理する

トリガーに指定した「HTTP 要求の受信時」のみが初期表示されるので、こちらを押下して設定していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/aaa28592-35a0-8b8a-3a4c-5bf1ef9d848e.png)
`userId`と`partnerId`をパラメータとして受け取るために、「要求本文の JSON スキーマ」を以下のように設定して保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/48767634-589c-fbfa-1aef-d925950ead03.png)
```json
{
    "type": "object",
    "properties": {
        "userId": {
            "type": "string"
        },
        "partnerId": {
            "type": "string"
        }
    },
    "required": [
        "userId",
        "partnerId"
    ],
    "additionalProperties": false
}
```
次に「設定」に移動して、「スキーマ検証」を`オン`に変更して「完了」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/b9334d40-6cc9-3da1-8edf-5ceded9cd80f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/0198458d-e146-19b3-f476-ec5c7d10e168.png)

#### Azure Cosmos DBから会話履歴を取得する

＋ボタンを押下して、「アクションの追加」をしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/c73e48f2-189f-15d5-a72d-2e7300a06b78.png)
検索欄に`cosmos`と入力し、表示されたAzure Cosmos DBの「ドキュメントをクエリする (V5)」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/9a8063fd-fe17-0258-d0f4-c6509a807e15.png)
すると、Azure Cosmos DBとの接続を設定する入力欄が表示されるので、必要な情報を入力して「作成」します。
- 接続名
    - `cosmos-db-connection`
- Authentication Type
    - `Access Key`
- Account ID
    - 今回作成したAzure Cosmos DBのアカウント名を入力します
- Access Key to your Azure Cosmos DB account
    - Azure Cosmos DBの「キー」タブにある「プライマリ キー」の値をコピペします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/53118b68-56bc-6d5e-d727-952d14171f60.png)
アカウント名（URIのサブドメイン）とプライマリキーは、Azure Cosmos DBアカウントの「キー」タブで確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/c75d93a6-d39e-08c7-cf2f-a713e57c5d8c.png)

接続の作成が完了すると、「ドキュメントをクエリする (V5)」の設定ができるようになるので、今回作成したコンテナーの情報と会話履歴を取得するクエリを入力します。
- 名前
    - `会話履歴をクエリする`
- Azure Cosmos DB アカウント名
    - `Use connection settings`
- データベース ID
    - `chatgpt-hands-on-db`
- コンテナー ID
    - `Conversations`
- SQL 構文クエリ
    - `SELECT * FROM c WHERE c.UserId= '@{triggerBody()['userId']}' AND c.PartnerId= '@{triggerBody()['partnerId']}'`
        - `triggerBody()`を使用してリクエストボディ内の値を指定しています
- パーティション キーの値
    - `"@{triggerBody()['userId']}"`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/2e498b31-4059-8a48-cb7e-1a3633c8be46.png)

これで、HTTPリクエストで受け取った`userId`と`partnerId`に紐づく会話履歴をAzure Cosmos DBから取得できました。

:::note warn
[今回使用したAzure Cosmos DBコネクタ](https://learn.microsoft.com/en-us/connectors/documentdb/#query-documents-v5)では、パラメーター化されたクエリがサポートされていないため、下記リクエストでSQL インジェクションされてしまいます。
```json
{
    "userId": "aaa",
    "partnerId": "' OR '1'='1"
}
```
ですので、本運用時はSDKが使用できるストアドプロシージャやWeb APIなどでデータ取得を代用する必要があります。
:::

#### 取得したデータを後続の処理で使えるようにする

まず、会話履歴が正しく取得できているか、より具体的には取得したドキュメントの数が0より大きいかを検証します。
「アクションの追加」から「制御」を選択して、さらに「条件」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/9e42e9e6-11b0-48fd-a377-6ff049413ef5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/e2b96d68-2c29-a246-8fd6-da398c9805b0.png)
[「ドキュメントをクエリする (V5)」のレスポンスの型](https://learn.microsoft.com/ja-jp/connectors/documentdb/#documentscollection)を確認すると、`Count`が0より大きいかの条件を追加すれば良いことがわかるので、以下のように入力します。
- 名前
    - `会話履歴が存在するか`
- AND
    - `body('会話履歴をクエリする')?['Count']`
        - 「値の選択」を押下し、「式」タブに値を入力して「OK」します
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/ce9130a3-d8e0-d2e3-0b50-576ee6a6447d.png)
    - `次の値より大きい`
    - `0`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/7dc0e6c3-b187-1817-a33d-56674568c45d.png)

続いて、条件分岐後のワークフローを組み立てていきます。
まず「False」の場合、つまり HTTPリクエストで受け取った`userId`と`partnerId`に紐づく会話履歴が存在しなかった場合、エラーレスポンスを返却して終了するというワークフローを追加します。

「False」側の「アクションの追加」から HTTPレスポンスを返却するための「応答」を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/b21eff58-8e8f-13d6-c10c-2b96b953a53f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/8c7037c3-1267-1bb1-460a-26afa24633cf.png)
404エラーを返すように設定します。
- 名前
    - `404 Not Found`
- 状態コード
    - `404`
- ヘッダー
    - `Content-Type`: `application/json`
- 本文
    - `{ "message": "The requested conversation does not exist." }`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/d06b0564-a4e2-4544-1a85-bbbfed66f435.png)

さらに、処理を終えるために「制御」の一つである「終了」アクションを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/281dcf0e-9f8a-1b92-1ad9-4bdc40926a50.png)
必要な情報を入力して、完成です。
- 名前
    - `404 終了`
- 状態
    - `失敗`
- コード
    - `404`
- メッセージ
    - `The requested conversation does not exist.`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/79cee421-8395-0245-d150-2b658e0db94a.png)

これで「False」側のワークフローが完成したので、次は「True」側の処理の作成に移ります。
「True」の場合は、会話履歴が存在する場合なので、後続の処理で各種データを使用するために会話履歴のJSONをパースしておく必要があります。

「True」側の「アクションの追加」を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/edb55e0d-990f-d428-2888-9944ac4394a6.png)
検索欄に`json`と入力し、「ビルトイン」タブに表示された「JSON の解析」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/f3c73210-17c7-a429-a389-cc8f4a474794.png)
ここで[「ドキュメントをクエリする (V5)」のレスポンスの型](https://learn.microsoft.com/ja-jp/connectors/documentdb/#documentscollection)を確認すると、`value`にはドキュメントの配列が入っています。
しかし、Azure Cosmos DB 作成時に考察したように`UserId`と`PartnerId`が決まればデータを一意に特定できるはずなので、配列の要素数は必ず１つとなります。
つまり、配列の先頭のデータを取り出せば、該当の会話履歴を取得したことになります。

以上のことを踏まえて、次のように入力します。
- 名前
    - `会話履歴(JSON)の解析`
- コンテンツ
    - `first(body('会話履歴をクエリする')?['value'])`
        - 「式」タブで入力します
        - `first()`で配列の最初の要素を取得します
- スキーマ
    - 以下のJSONをコピペします
    - `Conversations`コンテナー内の*item*のスキーマに対応します
```json
{
    "type": "object",
    "properties": {
        "UserId": {
            "type": "string"
        },
        "PartnerId": {
            "type": "string"
        },
        "User": {
            "type": "object",
            "properties": {
                "id": {
                    "type": "string"
                },
                "Name": {
                    "type": "string"
                }
            }
        },
        "Partner": {
            "type": "object",
            "properties": {
                "id": {
                    "type": "string"
                },
                "Name": {
                    "type": "string"
                }
            }
        },
        "History": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "UserId": {
                        "type": "string"
                    },
                    "Message": {
                        "type": "string"
                    }
                },
                "required": [
                    "UserId",
                    "Message"
                ]
            }
        },
        "id": {
            "type": "string"
        },
        "_rid": {
            "type": "string"
        },
        "_self": {
            "type": "string"
        },
        "_etag": {
            "type": "string"
        },
        "_attachments": {
            "type": "string"
        },
        "_ts": {
            "type": "integer"
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/ba2e6503-37c8-f27a-2083-5f515e0842d5.png)

これで「True」側の処理も完成しました。
現段階でのワークフローの全体像は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/2a9a0075-10a7-718e-57b1-f09ab0080758.png)

#### ChatGPT API にリクエストを送る

ワークフローを組み立てる前に、ChatGPT API のリクエストに必要な情報を洗い出します。
Azure OpenAI Studio の ChatGPT プレイグラウンドに戻り、「コードの表示」で「curl」の内容を表示します。
※タブを閉じてしまった場合はシステムメッセージとユーザーメッセージに先ほどと同じものを再度入力してください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/dcc6e7b4-702a-64ba-f8b2-2ea7185192f0.png)
エンドポイント、APIキー、リクエストヘッダー、リクエストボディの内容が確認できるので、こちらを参考に Azure Logic Apps からも同じ形でリクエストを送るようにワークフローを組み立てていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/4c29b6c0-fe97-708e-3b86-b567643f89a1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/45e50900-1ebe-fff3-96e5-d40977b8e8b2.png)

それでは、まずシステムメッセージとユーザーメッセージを格納する`messages`配列を作成します。
末尾の＋ボタンから「アクションの追加」を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/db9efa5b-d922-7088-fd85-c6c12191a3d8.png)
検索欄に`変数`と入力し、「ビルトイン」タブに表示された「変数を初期化する」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/86a83896-c9e4-b929-2e6a-71bf0257dbb3.png)
以下のように入力します。
- （アクションの）名前
    - `ChatGPT入力配列を初期化する`
- 名前
    - `ChatGPT入力配列`
- 種類
    - `アレイ`
- 値
    - （空欄のまま）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/461b244b-f58c-3187-0f4a-3d03f1385fd1.png)

空の`messages`配列の作成が完了したので、続いてシステムメッセージを追加していきます。
「アクションの追加」から「配列変数に追加」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/5f829730-d7d5-5382-7a8c-6d25519df22b.png)
ChatGPT プレイグラウンドで確認した通りに、システムメッセージを入力します。
- （アクションの）名前
    - `ChatGPT入力配列にシステムメッセージの追加`
- 名前
    - `ChatGPT入力配列`
- 値
    - 以下のJSONをコピペします
```json
{
    "role": "system",
    "content": "The assistant is a professional in determining if someone could become a friend based on conversation history. It receives a JSON containing user information and conversation history, then decides whether the 'Partner' is suitable as a friend for the 'User'. It responds to the question asked by the 'User' regarding the recommendation level of the 'Partner'.\n# Limitations\n- Output must be in Japanese\n- Only important information\n- Easy enough for a primary schooler\n- About 300 characters in Japanese\n- The 'User' is unaware of the content of conversations with the 'Partner'\n- This is a response to the 'User'\n- Rate the recommendation on a strict scale out of 100 points\n- The recommendation should be evaluated strictly\n# Output\n- An abstract of the conversation, including information about the 'Partner'\n- A recommendation score for the 'Partner' as a friend"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/1add4a77-3002-094b-1af8-442b5214ac66.png)

同様にして、ユーザーメッセージも追加します。
ここでユーザーメッセージに指定するのは、Azure Cosmos DBから取得した会話履歴であることに留意してください。
「アクションの追加」から「配列変数に追加」を行います。
- （アクションの）名前
    - `ChatGPT入力配列にユーザーメッセージ(会話履歴)の追加`
- 名前
    - `ChatGPT入力配列`
- 値
    - 以下のJSONをコピペします
    - 「会話履歴(JSON)の解析」でパースした値を使用します
```json
{
    "role": "user",
    "content": "{\"User\":@{body('会話履歴(JSON)の解析')?['User']},\"Partner\":@{body('会話履歴(JSON)の解析')?['Partner']},\"History\":@{body('会話履歴(JSON)の解析')?['History']}}"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/5b67ba8e-9aa2-d6c4-72ec-7b9610960a6c.png)

`messages`配列が完成したので、いよいよ ChatGPT API へリクエストを送ります。
正しく入力できていたら下図のようになっているはずです。末尾の「アクションの追加」を押下して、HTTPリクエスト用のアクションを追加していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/b08f6a7c-c5ff-4d2b-ca00-518250159bcc.png)
検索欄に`http`と入力し、「ビルトイン」タブに表示された「HTTP」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/94960da8-9de5-8298-4508-a963f42dda0a.png)
ChatGPT プレイグラウンドで確認した通りに入力していきます。
- 名前
    - `ChatGPTへのHTTPリクエスト`
- 方法
    - `POST`
- URI
    - ChatGPT プレイグラウンドの「エンドポイント」の値をコピペします
- ヘッダー
    - `Content-Type`: `application/json`
    - `api-key`: ChatGPT プレイグラウンドの「キー」の値をコピペします
- 本文
    - 以下のJSONをコピペします
```json
{
  "messages":  @{variables('ChatGPT入力配列')},
  "max_tokens": 800,
  "temperature": 0.5,
  "frequency_penalty": 0,
  "presence_penalty": 0,
  "top_p": 0.95,
  "stop": null
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/e3c34342-fdd2-5bd2-ce20-5e0af1a75a1d.png)

<details><summary>OpenAI APIを利用する場合</summary>

- エンドポイントやヘッダーが異なりますので、下記をご確認ください。
    - [Create chat completion - API reference](https://platform.openai.com/docs/api-reference/chat/create)
</details>

ここまでで、Azure Cosmos DBから取得した会話履歴をもとに会話の要約と友達としてのおすすめ度を生成するようChatGPT APIへリクエストを送るところまでが完成しました。

#### ChatGPT API のレスポンスをAzure Cosmos DBに保存する

ChatGPT APIのレスポンスはJSON形式なので、後続の処理でレスポンス内容を使用するにはJSONをパースしておく必要があります。
Azure Cosmos DBのクエリ結果のときと同じように「アクションの追加」から「JSON の解析」を追加します。
- 名前
    - `ChatGPT出力結果(JSON)の解析`
- コンテンツ
    - `@{body('ChatGPTへのHTTPリクエスト')}`
- スキーマ
    - 以下のJSONをコピペします
    - [レスポンスの型](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/reference#example-response-2)とスキーマが対応する必要があります
```json
{
    "type": "object",
    "properties": {
        "id": {
            "type": "string"
        },
        "object": {
            "type": "string"
        },
        "created": {
            "type": "integer"
        },
        "model": {
            "type": "string"
        },
        "choices": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "index": {
                        "type": "integer"
                    },
                    "finish_reason": {
                        "type": "string"
                    },
                    "message": {
                        "type": "object",
                        "properties": {
                            "role": {
                                "type": "string"
                            },
                            "content": {
                                "type": "string"
                            }
                        }
                    }
                },
                "required": [
                    "index",
                    "finish_reason",
                    "message"
                ]
            }
        },
        "usage": {
            "type": "object",
            "properties": {
                "completion_tokens": {
                    "type": "integer"
                },
                "prompt_tokens": {
                    "type": "integer"
                },
                "total_tokens": {
                    "type": "integer"
                }
            }
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/113144bc-4841-a5d2-e0cb-17624e2b15b8.png)

それでは、ChatGPT APIのレスポンスをパースできたので、ChatGPTの返信内容をAzure Cosmos DBに保存していきます。
具体的には、「会話履歴をクエリする」で取得したドキュメントに、`Summary`をキーとしてChatGPTの返信内容を追加して、更新をかけます。

「アクションの追加」からAzure Cosmos DBの「ドキュメントを置換 (V2)」を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/26746bd2-9fc2-049e-303b-0ade3988eddb.png)
以下のように入力します。
- 名前
    - `ChatGPT返信内容をCosmosDBに追加する`
- Azure Cosmos DB アカウント名
    - `Use connection settings`
        - 作成済みの接続設定を使用します
- データベース ID
    - `chatgpt-hands-on-db`
- コレクション ID
    - `Conversations`
- ドキュメント ID
    - `@{body('会話履歴(JSON)の解析')?['id']}`
- パーティション キーの値
    - `"@{body('会話履歴(JSON)の解析')?['UserId']}"`
        - 「Add new parameter」から入力項目を追加する必要があります
- ドキュメント
    - 以下のJSONをコピペします
    - ChatGPTの返信内容は`@{trim(body('ChatGPT出力結果(JSON)の解析')?['choices'][0]['message']['content'])}`で取得できます
```json
{
    "id": @{body('会話履歴(JSON)の解析')?['id']},
    "UserId": @{body('会話履歴(JSON)の解析')?['UserId']},
    "PartnerId": @{body('会話履歴(JSON)の解析')?['PartnerId']},
    "User": @{body('会話履歴(JSON)の解析')?['User']},
    "Partner": @{body('会話履歴(JSON)の解析')?['Partner']},
    "History": @{body('会話履歴(JSON)の解析')?['History']},
    "Summary": @{trim(body('ChatGPT出力結果(JSON)の解析')?['choices'][0]['message']['content'])}
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/c79cd704-fa56-5934-0fdb-ef5d09861979.png)
これで、会話履歴のJSONデータにChatGPTの返信内容を追加して更新することができました。

#### HTTPレスポンスを返却する

最後に、Web APIのレスポンスを作成します。
レスポンスには、`user`, `partner`, `summary`を含めます。

「アクションの追加」から HTTPレスポンスを返却するための「応答」を追加します。
- 名前
    - `200 OK`
- 状態コード
    - `200`
- ヘッダー
    - `Content-Type`: `application/json`
- 本文
    - 以下のJSONをコピペします
```json
{
    "user": @{body('会話履歴(JSON)の解析')?['User']},
    "partner": @{body('会話履歴(JSON)の解析')?['Partner']},
    "summary": @{trim(body('ChatGPT出力結果(JSON)の解析')?['choices'][0]['message']['content'])}
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/d188a8e7-26f8-66ae-144d-d19bcc218171.png)

そして、ワークフロー全体を正常に終了するために「終了」アクションを追加します。
- 名前
    - `200 終了`
- 状態
    - `成功`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/ab2a17fc-24d8-7c63-3048-a960283b28ce.png)

これで、ワークフローの作成は終了です。
ついに、ChatGPT API を利用した Web API が完成しました！！

ワークフローの全体像は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/b61687bd-6779-1428-c581-7e0571a3b558.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/f6356cc7-2845-6639-e4c5-8309c286b280.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/247de90a-2da5-26a3-70c8-8f49b8a2c7c4.png)

### ワークフローの実行

最後に作成したWeb APIの動作確認を行います。
Logic Apps デザイナー上部の「トリガーの実行」を押下して、「ペイロードで実行」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/4866d982-5905-2d49-4e18-324d3f16fc40.png)
事前にAzure Cosmos DBにアップロードしたダミーデータの`userId`と`partnerId`を「本文」で指定して「実行」を押下します。
```json
{
    "userId": "aaa",
    "partnerId": "bbb"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/1b8881d0-b0a4-3c43-bd24-e107ff773406.png)
レスポンスの「状態コード」が`200`で返ってきたら成功です。
また、`summary`には会話の要約とユーザーBのおすすめ度に関するユーザーAに対してのメッセージが入っていることが確認できるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/c79b48c9-1f0e-763b-a4be-892a65240826.png)
さらに、Azure Cosmos DBの会話履歴データにも`Summary`が追加されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326627/db3cae11-a7fc-a292-4f0f-e9786ac125a6.png)

Web APIの動作確認も問題なかったので、これでハンズオンは終了です。

## 手を動かした者だけが到達する境地

おつかれさまでした。

実際に手を動かしながらWeb APIを1本開発することで、流行りのChatGPTに対する解像度がかなり上がったのではないでしょうか。

そして、もうすでにChatGPT APIを使って次に作るものが決まっていたら、あなたは「私の理想のエンジニア」です。