.NET (C#) SDK for Azure OpenAI 開発入門 (GPT-3.5 Turbo を使いコンソールで動く chatbot)


Microsoft Learn (MSLearn) (マイクロソフトによるオンライン学習サービス) の OpenAI 系のモジュール「[Exercise - Install the .NET SDK for Azure OpenAI and create your first app](https://learn.microsoft.com/ja-jp/training/modules/open-ai-dotnet-text-completions/4-exercise-install-sdk-create-completion?WT.mc_id=dotnet-8948-machiy)」をベースに
（この記事を書いている 23/9/5 時点ではまだ日本語化されていないので）ところどころ翻訳＆補足しながら、
ランスルーしていこうと思います。

# 対象モジュール

「[Exercise - Install the .NET SDK for Azure OpenAI and create your first app](https://learn.microsoft.com/ja-jp/training/modules/open-ai-dotnet-text-completions/4-exercise-install-sdk-create-completion?WT.mc_id=dotnet-8948-machiy)」 (『演習： .NET SDK for Azure OpenAI をインストールし、はじめてのアプリを作りましょう』)

https://learn.microsoft.com/ja-jp/training/modules/open-ai-dotnet-text-completions/4-exercise-install-sdk-create-completion?WT.mc_id=dotnet-8948-machiy

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/a412df30-654c-4d21-5431-dbeb54768f01.png)

# 完成したもの

ハイキングについてなんでも答えてくれる chat bot。
C# で書かれたコンソールアプリで、裏で Azure にデプロイされた GPT-3.5 Turbo モデルをたたいている

https://twitter.com/chomado/status/1697894961143009711

## 事前に必要な環境

* Azure のサブスクリプション
* その Azure サブスクリプションは Azure OpenAI Service のアクセスが与えられている
  - 現在、Azure OpenAI を使うには申し込みが必要です。こちらからいけます https://aka.ms/oai/access
* マシンに Azure CLI がインストールされている (Azure ポータル画面ぽちぽちでやるなら、CLI 環境は無くてもいいです)

# ステップ

1. [バックエンド] 
    1. Azure に OpenAI リソースを作り、 
    1. GPT-3.5 Turbo モデルをデプロイする
1. [クライアント] 
    1. C# でコンソールアプリを作る
    1. NuGet で .NET SDK for Azure OpenAI Service を入れる
    1. API 叩いて結果を表示

# 1. バックエンド編

Azure に OpenAI リソースを作り、 GPT-3.5 Turbo モデルをデプロイしていきます。
大きく２つのやり方があります：

1. 画面ぽちぽち (GUI) でやる方法
1. Azure CLI を使う方法

↑ どっちでもいいです。個人的には私は分かりやすいので画面のほうが好きですが、自動化ガチ勢は CLI を好んでいる人が多いです。

[本家 MSLearn モジュール](https://learn.microsoft.com/ja-jp/training/modules/open-ai-dotnet-text-completions/4-exercise-install-sdk-create-completion?WT.mc_id=dotnet-8948-machiy) は CLI を使った方法で書かれていますが、
この記事では画面版で書いていきます。

## 1-1. 作業するリソースグループを作る

Azure ポータル画面を開きます。
https://portal.azure.com/

リソースグループ → 作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/ce1b7f57-521b-5349-845d-7e73d032138e.png)

任意の名前をつけて、（この例では `rg-230905-HikingConversations` と名付けています）
リージョンを指定して、（この例では東日本リージョンを指定しています）

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/95a93d52-ac30-07e7-1afb-0fe43fd7495e.png)

「作成」を押します。

ちなみに CLI 版だと以下のようになります。（bash の場合は改行文字を変えてください）

```powershell:PowerShell 
az group create `
--name rg-230905-HikingConversations `
--location japaneast
```

## 1-2. Azure OpenAI のリソースを作成

先程作ったリソースグループの中に、Azure OpenAI のリソースを作成しましょう。

CLI でやる場合はこれ。


```powershell:PowerShell
az cognitiveservices account create `
-n 230905HikingConversationsAI `
-g rg-230905-HikingConversations `
-l japaneast `
--kind OpenAI `
--sku s0
```

画面ポチポチでやる場合は、以下手順。（上のコマンド叩いたときと同じことを画面上でやります）

「リソースの作成」を押します。

![Untitled1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/5b37d6eb-6f8c-9405-5774-e09d643ee8b3.png)

検索窓に「OpenAI」で検索し、出てきた該当のものをクリックし、「作成」を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/8e04a2e2-d4dc-b662-c729-5e2b507bfced.png)

リソース名は「`230905HikingConversationsAI`」と指定しました。
価格レベルは今のところ「`Standard S0`」しか指定できないので、それにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/395033ad-863d-6a7d-c58f-f1a984e80f52.png)

:::note info

ちなみに詳しい価格についてはこちらで見れます。（今回は GPT-3.5-Turbo モデル（安いほう）を使います）
https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/018a08f4-3908-3ba5-33ab-90fe2b625364.png)

:::

「次へ」を押します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/b07d15a4-8020-3887-c7f4-b81e1dba7c24.png)


「タグ」については特に何も指定せず、また「次へ」を押します。
そして「作成」を押します。
デプロイは数分かかるので、待ちます。

## 1-3. GPT-3.5 Turbo モデルをデプロイ

Azure OpenAI リソースのデプロイが完了しましたね。

![Azure OpenAI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/6b3a030b-7e91-8737-bf52-b83423389733.png)


この「Explore」をクリックし、**Azure OpenAI Studio** を開きましょう。

![Azure OpenAI Studio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/1e7a2656-170b-5f2f-e944-b4da8797616d.png)

左のメニューの「Deployments」をクリックします。

「+ Create new deployment」をクリックします。
モデルを選択、モデル名を適当につけます。（この例では `HikingRecommendationTurbo` と付けました）

![Untitled6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/82b1d1d2-2157-1ba4-baeb-c7d6994f868d.png)


作成しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/dea0f2ff-608c-33c7-a400-532869d9d57f.png)

できました。

## 1-4: エンドポイント

エンドポイントと API key は (Azure OpenAI Studio ではなく) Azure portal の OpenAI リソースの Keys and Endpoint から取得することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/8adc8848-1935-ad0b-c3d6-412532dbd84b.png)

## 1-5: Managed ID の設定

[元記事](https://learn.microsoft.com/ja-jp/training/modules/open-ai-dotnet-text-completions/4-exercise-install-sdk-create-completion?WT.mc_id=dotnet-8948-machiy) では API キーをクライアントにベタ書きしていましたが、
せっかく Azure を使っているので、
もっとセキュアに認証できたらと思うので、今回は Managed ID を使ってみましょう。

リソースグループに移動し、「アクセス制御 (IAM)」を開きます。
(IAM:  Identity and Access Management)

![Untitled7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/38b39e40-16cb-8f69-0c0f-d93614c6ca64.png)

「＋追加」

![Untitled10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/b689170c-4c66-3bea-8ac9-a1f5b6934f14.png)

検索窓に「OpenAI」で検索して、選択して、次へ

![Untitled11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/322fda79-e1e9-54b4-f44d-5abbb29de8c7.png)

自分を検索します。
この時、この後使う Visual Studio でログインしているアカウントを選んでください。

![Untitled13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/27cc7654-8766-3233-722b-ae95a22755dc.png)

![Untitled14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/385a2286-8994-fcdf-a7fa-bcb97ca1c056.png)

「レビューと割り当て」をクリックします。


# 2. クライアント編

コンソールアプリを作っていきましょう。

## 2-1. プロジェクト新規作成

Visual Studio 2022 を起動します。（VSCode もでいいです）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/233f7963-19c4-e524-60df-0ae000cd7c33.png)

新規プロジェクト作成から、
「コンソールアプリ」をクリックします。

C# のコンソールアプリのテンプレートを選択し、進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/43924656-deb0-0894-b13c-e61a4a322047.png)

「作成 (Create)」を連打して進みます。

ハローワールドプロジェクトができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/ae2eb016-ef02-8f79-a971-611cc256aeec.png)

ちなみにこの手順を CLI でやりたかったら以下のコマンドで同じ動作です。

```shell
dotnet new console -n プロジェクト名
```

## 2-2. Azure OpenAI SDK を入れる


ちなみにこの手順を CLI でやりたかったら以下のコマンドで同じ動作です。

```shell
cd プロジェクト名
dotnet add package Azure.AI.OpenAI --prerelease
```

画面ポチポチの場合は以下のとおりです。

ソリューションエクスプローラーのプロジェクト名のところで右クリック
NuGet package マネージャーを開きましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/465e2bb9-443f-e340-b514-32afe3b0f3f4.png)

現時点でまだ Azure OpenAI SDK はプレビュー版なので、
`Include prerelease` にチェックを入れます。
検索窓で `Azure.AI.OpenAI` と検索し、出てきたものを「インストール」

![Untitled5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/86ba656b-df54-0c31-07f8-20c8a603038f.png)

するといろんなライセンスへの同意画面が出てくるので、承諾します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/9fd40272-3696-89c6-73aa-83723b4757cf.png)

無事インストールされたのを確認します。

## 2-3. 認証系のパッケージも入れる

Azure.Identity も入れておきましょう。
Azure OpenAI API 叩くときの認証に使います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/80cafd5f-e492-9b03-949d-52dc6d0f42bd.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/ebe85fe3-918d-e555-c1e3-549cd1b645a8.png)

## 2-4. Azure へのつなぎ

前半で作った Azure の環境に
クライアントから繋ぐための設定を
最初に書いていきます。

```csharp:Program.cs
using Azure.AI.OpenAI;
using Azure;
using Azure.Identity;

var openAIEndpoint = "エンドポイントのURL";
// ↓デプロイしたモデルの名前
var openAIDeploymentName = "HikingRecommendationTurbo";

var endpoint = new Uri(openAIEndpoint);
var credentials = new DefaultAzureCredential(); // 認証
var openAIClient = new OpenAIClient(endpoint, credentials);

// max トークン数どうするとかのパラメータ
var completionOptions = new ChatCompletionsOptions
{
    MaxTokens = 400,
    Temperature = 1f,
    FrequencyPenalty = 0.0f,
    PresencePenalty = 0.0f,
    NucleusSamplingFactor = 0.95f // Top P
};
```

## 2-5: システムプロンプト

システムプロンプトを書いていきましょう。
chat bot の設定。どんなキャラクターか、みたいな。
この例では、「お前はハイキング大好きオタクで、会話しながら最適なハイキング沼を勧めるんだ。日本の」と書きました。

```csharp:Program.cs
// システムプロンプト
var systemPrompt =
"""
You are a hiking enthusiast who helps people discover fun hikes. You are upbeat and friendly. 
You ask people what type of hikes they like to take and then suggest some in Japan.
""";

// ChatMessage オブジェクトを作り、システムロールに登録
ChatMessage systemMessage = new(ChatRole.System, systemPrompt);

// さっき定義したシステムメッセージを
// `completionOptions.Messages` リストに登録。
// これにより GPT くんが参照できるようになる
completionOptions.Messages.Add(systemMessage);

```

## 2-6: 会話を始める

GPT くんに投げる最初のメッセージを定義しましょう。
ユーザからのメッセージになるので、User role のものになりますね。
とりあえず最初は決め打ちでベタ書きします

```csharp:Program.cs
string userGreeting = """
こんにちは、ハイキングレコメンデーション bot さん！
""";

// ユーザーロール
ChatMessage userGreetingMessage = new(ChatRole.User, userGreeting);
// さっき定義したユーザーメッセージを
// `completionOptions.Messages` リストに登録。
// これにより GPT くんが参照できるようになる
completionOptions.Messages.Add(userGreetingMessage);

// コンソールに表示
Console.WriteLine($"あなた >>> {userGreeting}");
```

## 2-7: GPT くんからのレスポンスを受け取る

ついに GPT くんからのレスポンスを受け取るときが来ました。

```csharp:Program.cs
ChatCompletions response = await openAIClient.GetChatCompletionsAsync(openAIDeploymentName, completionOptions);

ChatMessage assistantResponse = response.Choices[0].Message;

Console.WriteLine($"GPT くん >>> {assistantResponse.Content}");

// GPT くんから返ってきたメッセージも、忘れずに
// `completionOptions.Messages` リストに登録。
// これにより GPT くんがコンテキストとして参照できるようになる
completionOptions.Messages.Add(assistantResponse);
```

## 2-8: 実行してみましょう！

`F5` で実行してみます。
コンソールが立ち上がり、
まずユーザのメッセージ（決め打ちでさっきべた書きしたあいさつ文）が表示され、
少ししてから、
(Azure から返ってきた) GPT くんからのメッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/583bafef-b350-29b5-3a58-483b8d2d2d0f.png)

動いていますね！

## 2-9: 会話を続ける

最終的には、ユーザからの発言を、ちゃんとユーザからの入力にしたいですが、
とりあえず動作確認のために、また決め打ちべた書きで書いてみます。

```csharp:Program.cs
// ユーザからの発言（とりあえず今はべた書き決め打ち）
var hikeRequest = 
"""
虫がいないところに行きたいし、日焼けもしたくないんですけど。
""";

Console.WriteLine($"あなた >>> {hikeRequest}");

ChatMessage hikeMessage = new (ChatRole.User, hikeRequest);

// さっき定義したユーザーメッセージを
// `completionOptions.Messages` リストに登録。
// これにより GPT くんが参照できるようになる
completionOptions.Messages.Add(hikeMessage);

// GPT に投げる
response = await openAIClient.GetChatCompletionsAsync(openAIDeploymentName, completionOptions); 

// 返ってきたレスポンスからメッセージを取得
assistantResponse = response.Choices[0].Message;

// 表示
Console.WriteLine($"GPT くん >>> {assistantResponse.Content}");

// GPT くんから返ってきたメッセージも、忘れずに
// `completionOptions.Messages` リストに登録。
// これにより GPT くんがコンテキストとして参照できるようになる
completionOptions.Messages.Add(assistantResponse);
```

また実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/203a8b48-a9bb-741e-4422-6eded20ced78.png)

動いていますね！
ちゃんと「虫も少ないし日焼けも少なそうなところ」をサジェストしてくれています

## 2-10: ユーザからの入力に対応する

とりあえず今までは決め打ちべた書きでしたが、
今度はユーザからの入力に対応します。

標準入力 `Console.ReadLine();` で受け取ることにして、
それをループさせることにします。

上の `2-9` で書いたコードを全部以下に入れ替えます。

```csharp:Program.cs
while (true)
{
    Console.Write("あなた >>> ");
    // ユーザ入力
    var hikeRequest = Console.ReadLine();

    ChatMessage hikeMessage = new(ChatRole.User, hikeRequest);

    completionOptions.Messages.Add(hikeMessage);

    response = await openAIClient.GetChatCompletionsAsync(openAIDeploymentName, completionOptions);

    assistantResponse = response.Choices[0].Message;

    Console.WriteLine($"GPT くん >>> {assistantResponse.Content}");

    completionOptions.Messages.Add(assistantResponse);
}

```

実行してみましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24609/d6326aff-132f-cfa7-1cb4-7f043e5ba021.png)

動きました！