ChatGPT API で C# でコマンドラインで会話する (機能切り出し)

# ChatGPT API で C# でコマンドラインで会話する (機能切り出し)

こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 22.04 で ChatGPT API を C# から使いコマンドラインから会話する方法を紹介します。
![chat-gpt_with_csharp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e76ecf1d-a6e7-9f9e-75bf-8c291cf1eb6a.png)

## 目的

Windows 11 の Linux でクラウド開発します。

こちらから記事の一覧がご覧いただけます。

https://qiita.com/studio_meowtoon/items/6119ab05d8b13527efd8

## 実現すること

ローカル環境の Ubuntu で、ChatGPT API を C# から使いコマンドラインから会話します。

:::note warn
この記事では[前回の記事](https://qiita.com/studio_meowtoon/items/8af59ab0a7a52724d064)の続きより、作成した機能をライブラリに切り出していきます。
:::

:::note alert
C# には本来[厳格なコーディング規則](https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/coding-style/coding-conventions)がありますが、この記事では可読性のために、一部規則に沿わない表記方法を使用しています。ご注意ください。
:::

## 関連記事

**OpenAI API** と比較してみましょう！

#### OpenAI API

https://qiita.com/studio_meowtoon/items/2960383dc750ba761362

## 技術トピック

### ChatGPT API とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [ChatGPT API](https://openai.com/blog/introducing-chatgpt-and-whisper-apis)
ChatGPT API は、OpenAI の言語モデルである GPT-3 をベースにした、自然言語による対話を実現するためのAPIです。

:::note info

|特徴|
|:--|
|ChatGPT API を利用することで、開発者は自分たちのアプリケーションやサービスに自然言語の対話機能を追加することができます。|
|ChatGPT API は、大規模なトレーニングデータを用いて学習された GPT-3 の言語理解力を利用し、自然言語での質問や会話に対して自然で流暢な回答を生成することができます。|
|また、ChatGPT API は、OpenAI が提供するプラットフォームである OpenAI Codex と組み合わせて、より高度な対話型アプリケーションを構築することも可能です。|

:::
</details>

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  
:::

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
```

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
```

.NET SDK [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/d68031c91462f6808dff)

```sh
$ dotnet --list-sdks
7.0.202 [/usr/share/dotnet/sdk]
$ dotnet --version
7.0.202
```

</details>

:::note warn
この記事では基本的に Ubuntu のターミナルで操作を行います。**Vim を使用してコピペする方法**を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
:::

https://qiita.com/studio_meowtoon/items/7771ad48676926e41463

## ChatGPT と会話する手順

### 前回までにできたこと

https://qiita.com/studio_meowtoon/items/8af59ab0a7a52724d064

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/d86c2638-3482-9e94-1148-0f8fe07f4eb0.png)

:::note info
ここまでの手順で、**appsettings.json** を作成してパラメータを設定することができました。また、そこに ChatGPT に**演じてもらいたい性格** を記述することにより、ChatGPT との会話を**より楽しむ**ことができました。
:::

:::note warn
今後、この **ChatGPT API** を**カスタマイズしたサービス**を Azure などのクラウド環境にデプロイすることを見据えて**機能を再利用**できるように切り出していきます。
:::

### ソリューションの作成

:::note info
現在の **ChatGPTApp** プロジェクトを、**App** プロジェクトと **Lib** プロジェクトに切り分けます。
:::

|No|ソリューション/プロジェクト|役わり|テンプレート|
|:--|:--|:--|:--|
|1|**ChatGPT/App**|コマンドラインインタフェース|コンソール アプリ|
|2|**ChatGPT/Lib**|ライブラリ|クラス ライブラリ|
|<font color="gray">3</font>|<font color="gray">ChatGPT/WebAPI ※ この記事では言及しません。</font>|<font color="gray">Web API</font>|<font color="gray">ASP.NET Core Web API</font>|
|<font color="gray">4</font>|<font color="gray">ChatGPT/WebApp ※ この記事では言及しません。</font>|<font color="gray">Web クライアント</font>|<font color="gray">Blazor WebAssembly</font>|

ソリューションを作成します。

```sh
$ mkdir -p ~/tmp/ChatGPT
$ cd ~/tmp/ChatGPT
$ dotnet new sln -n ChatGPT
```

:::note info 
ここまでの手順で、**ChatGPT** という名前の **.NET ソリューション**が作成できました。
:::

### プロジェクトの移動

**前回までに**作成した **ChatGPTApp プロジェクト**をソリューションに移動します。

```sh
$ mv ~/tmp/ChatGPTApp ~/tmp/ChatGPT/App
```

App/ChatGPTApp.csproj をリネームします。

```sh
$ mv App/ChatGPTApp.csproj App/App.csproj
```

App プロジェクトを ChatGPT ソリューションに追加します。

```sh
$ dotnet sln ChatGPT.sln add App/App.csproj
プロジェクト `App/App.csproj` をソリューションに追加しました。
```

### ソリューションを実行

前回までのアプリをソリューションとしてビルド、実行してみます。

**Ubuntu** に **OPENAI_API_KEY** 環境変数を作成します。

```sh
$ export OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

アプリをビルド・実行します。

```txt
$ dotnet run --project App/App.csproj --configuration Release
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/246c411e-f0ca-bebe-6ea6-63f373e79860.png)

:::note info
ここまでの手順で、前回までに作成したプロジェクトを**ソリューションに移動**することができました。
:::

### クラス ライブラリ プロジェクトの作成

Lib プロジェクトを作成します。

```sh
$ dotnet new classlib -n Lib
```

必要なパッケージを [**NuGet**](https://learn.microsoft.com/ja-jp/nuget/what-is-nuget) で取得します。

```sh
$ dotnet add Lib/Lib.csproj package OpenAI
$ dotnet add Lib/Lib.csproj package Microsoft.Extensions.Configuration
$ dotnet add Lib/Lib.csproj package Microsoft.Extensions.Configuration.Json
$ dotnet add Lib/Lib.csproj package Microsoft.Extensions.Configuration.Binder
```

Lib プロジェクトを ChatGPT ソリューションに追加します。

```sh
$ dotnet sln ChatGPT.sln add Lib/Lib.csproj
プロジェクト `Lib/Lib.csproj` をソリューションに追加しました。
```

ソリューションの構成を確認します。

```sh
$ pwd
/home/$USER/tmp/ChatGPT
$ tree -I 'obj|bin'
.
├── App
│   ├── App.csproj
│   ├── AppSettings.cs
│   ├── Program.cs
│   └── appsettings.json
├── ChatGPT.sln
└── Lib
    ├── Class1.cs
    └── Lib.csproj
```

:::note info
ここまでの手順で、**クラスライブラリ** プロジェクトを**ソリューションに作成**することができました。
:::

## コードの修正 その1

クラスライブラリに前回までのコードを切り出します。

Class1.cs をリネームします。

```sh
$ mv Lib/Class1.cs Lib/Service.cs 
```

AppSettings.cs を移動します。

```sh
$ mv App/AppSettings.cs Lib/AppSettings.cs
```

AppSettings.cs を修正します。

```sh
$ vim Lib/AppSettings.cs
```

<details>
<summary>ファイルの内容を表示します。</summary>

ファイルの内容

```csharp:Lib/AppSettings.cs
namespace ChatGPT.Lib {
    public class AppSettings {
        public List<Persona>? Personas { get; set; }
    }
    public class Persona {
        public int? ListSize { get; set; }
        public string? System { get; set; }
    }
}
```

</details>

Service.cs を修正します。

```sh
$ vim Lib/Service.cs
```

<details>
<summary>ファイルの内容を表示します。</summary>

ファイルの内容

```csharp:Lib/Service.cs
using static System.Environment;
using static System.Math;
#if DEBUG
using static System.Console;
using static System.ConsoleColor;
#endif
using Microsoft.Extensions.Configuration;
using OpenAI_API;
using OpenAI_API.Chat;
using OpenAI_API.Models;
namespace ChatGPT.Lib {
    public class Service {
        static int DEFAULT_LIST_SIZE = 5;
        AppSettings? _app_settings;
        OpenAIAPI _api = new();
        List<MessageRole> _list = new();
        string? _prompt;
        public string? Prompt { set { _prompt = value; }}
        public event Changed? OnStart;
        public event Changed? OnEnter;
        public event Changed? OnResult;
        public void Init() {
            // load an appsettings.
            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();
            _app_settings = configuration.Get<AppSettings>();
            // get an api key.
            string? api_key = GetEnvironmentVariable("OPENAI_API_KEY");
            // create an api object.
            _api = new(api_key);
            // create a user and assistant list.
            _list = new((int) (_app_settings?.Personas?[0].ListSize ?? DEFAULT_LIST_SIZE));
        }
        public async Task ExecuteAsync() {
            // on start event.
            OnStart?.Invoke(this, new EvtArgs("start") { Value = string.Empty });
            // on enter event.
            OnEnter?.Invoke(this, new("enter"));
            // exec if a prompt is not empty.
            if (!string.IsNullOrEmpty(_prompt)) {
                // create a system content for the api.
                string? system = _app_settings?.Personas?[0].System;
#if DEBUG
                ForegroundColor = Red;
                WriteLine($"SYSTEM:\n{system}\n");
                ResetColor();
#endif
                // set a system content.
                List<ChatMessage> chat_message_list = new();
                chat_message_list.Add(new ChatMessage(ChatMessageRole.System, system));
                // set previous user and assistant contents.
                _list.ForEach(x => {
                    chat_message_list.Add(new ChatMessage(ChatMessageRole.User, x.User));
                    chat_message_list.Add(new ChatMessage(ChatMessageRole.Assistant, x.Assistant));
                });
                // set current user content.
                chat_message_list.Add(new ChatMessage(ChatMessageRole.User, _prompt));
                // get a result from the api.
                ChatResult? result = await _api.Chat.CreateChatCompletionAsync(new ChatRequest() {
                    Model = Model.ChatGPTTurbo0301,
                    Messages = chat_message_list
                });
                // get a reply.
                string? reply = result.Choices[0].Message.Content.Trim();
                // on result event.
                OnResult?.Invoke(this, new EvtArgs("result") { Value = reply });
                // set conversation to a message_role object.
                _list.Add(new MessageRole{ User = _prompt, Assistant = reply });
                _list = _list.Skip(Max(0, _list.Count - (int) (_app_settings?.Personas?[0].ListSize ?? DEFAULT_LIST_SIZE))).ToList();
#if DEBUG
                ForegroundColor = Red;
                _list.ForEach(x => {
                    WriteLine($"USER: {x.User}");
                    WriteLine($"AI  : {x.Assistant}");
                });
                ResetColor();
#endif
            }
        }
        public class EvtArgs : EventArgs {
            public EvtArgs(string name) {
                Name = name;
            }
            public string Name { get; }
            public string? Value { get; set; }
        }
        public delegate void Changed(object sender, EvtArgs e);
        class MessageRole {
            public string? User { get; set; }
            public string? Assistant { get; set; }
        }
    }
}
```

</details>

:::note info
ここまでの手順で、これまでに作成した処理を**クラスライブラリ**に切り出すことができました。
:::

## コードの修正 その2

コンソールアプリの修正を行います。

App.csproj を修正します。

```sh
$ vim App/App.csproj
```

ファイルの内容

```sh:App/App.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Lib\Lib.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
    <PackageReference Include="OpenAI" Version="1.6.0" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>
```

:::note info
**App** プロジェクトから、**Lib** プロジェクトを参照するように設定しています。
:::

Program.cs を修正します。

```sh
$ vim App/Program.cs
```

ファイルの内容

```csharp:App/Program.cs
using static System.Console;
using static System.ConsoleColor;
using ChatGPT.Lib;
namespace ChatGPT.App {
    class Program {
        static async Task Main(string[] args) {
            // crate a service object.
            Service service = new();
            service.OnStart += (sender, e) => {
                // print a message to input or exit.
                ForegroundColor = Yellow;
                WriteLine("Enter a prompt (or press Ctrl + C to exit):");
                ResetColor();
            };
            service.OnEnter += (sender, e) => {
                // read a prompt from the console.
                ((Service) sender).Prompt = ReadLine();
            };
            service.OnResult += (sender, e) => {
                // print a result.
                ForegroundColor = Green;
                WriteLine(e.Value);
                ResetColor();
            };
            // loop the Service object.
            service.Init();
            do {
                await service.ExecuteAsync();
            } while (true);
        }
    }
}
```

ソリューションをビルド・実行します。

```txt
$ dotnet run --project App/App.csproj --configuration Release
```

:::note warn
以下のコマンドで実行すると、デバッグ表示が出力されます。
```txt
$ dotnet run --project App/App.csproj
```
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/7a69ab8e-e977-ace8-0482-392c6c67c539.png)

:::note info
ここまでの手順で、**ChatGPT API** を使用するコードを、**クラスライブラリ**に切り出すことができました。プログラムは前回までと**同じように動作**しています。この記事ではこれまでの機能を変更せずに処理を切り出したため、その効果が分かりにくいことをご了承ください。
:::

## まとめ

ローカル環境の Ubuntu で、ChatGPT API を C# から使いコマンドラインから会話することができました。

:::note warn
実際の開発では、軽量なテキストエディタである **VS Code** や、**IDE (統合開発環境)** を使用して、.NET プログラムを開発することが一般的です。しかし、dotnet コマンドでビルドしたり、実行したりすることも、.NET 開発環境を理解する上で役立ちます。
:::

どうでしたか？ Window 11 の WSL Ubuntu に、.NET の開発環境を手軽に構築することができます。ぜひお試しください。今後も .NET の開発環境などを紹介していきますので、ぜひお楽しみにしてください。

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/1c61064fc46bc77beab4

## 参考資料

https://github.com/OkGoDoIt/OpenAI-API-dotnet