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/7cb530b19d8647d840e6)の続きより、複数の人格との会話を試みます。
:::

:::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/7cb530b19d8647d840e6

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

:::note info
ここまでの手順で、 **ChatGPT API** を**カスタマイズしたサービス**の機能を再利用できるように、クラスライブラリとして切り出すことができました。
:::

:::note warn
次の試みとして、**複数の人格**の ChatGPT と同時に会話してみます。これは、例えばゲームの **NPC** (ノンプレーヤーキャラ) に **AI で人格を設定**することなどにつながります。
:::

### パラメーターを追加

:::note info
ChatGPT の人格設定のパラメータに**名前**と**イメージカラー**を設定します。
:::

AppSettings.cs を編集します。

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

ファイルの内容

```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; }
        public string? Name { get; set; }
        public string? Color { get; set; }
    }
}
```

appsettings.json を修正します。

```sh
$ vim App/appsettings.json
```

ファイルの内容

```json:App/appsettings.json
{
  "Personas": [
    {
      "ListSize": 5,
      "System": "I am Japanese and a learner of English conversation.\nYou must act on your role as a person who is having the short chat with the user.\nYou only use the most simple English words should be used.\nI want to enjoy the short chat possible as you can.\nSo you must keep your reply to use a minimum of sentences.\nYou must remember the words I said to reflect on our short chat.\nYour name is $NAME and your favorite color is $COLOR.\nDo you understand? Here we go!",
      "Name": "John",
      "Color": "Green"
    }
  ]
}
```

Service.cs を編集します。

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

ファイルの内容

```csharp:Lib/Service.cs ※一部抜粋
// create a system content for the api.
string? system = appSettings?.Personas?[0].System;
string? name = appSettings?.Personas?[0].Name;
string? color = appSettings?.Personas?[0].Color;
system = system?.Replace("$NAME", name).Replace("$COLOR", color);
// set a system content.
List<ChatMessage> chat_message_list = new();
chat_message_list.Add(new ChatMessage(ChatMessageRole.System, system));
```

<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;
                string? name = _app_settings?.Personas?[0].Name;
                string? color = _app_settings?.Personas?[0].Color;
                system = system?.Replace("$NAME", name).Replace("$COLOR", color);
#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($"{name}: {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>

実行した結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/d66c4ed9-6d12-1533-187a-a878d7ad5c7a.png)

:::note info
ここまでの手順で、ChatGPT に**彼の名前**と**好きな色**を答えさせることができました。これは、会話の中で**動的**に **system** パラメータを**更新**できることを証明しています。
:::

### 複数の人格とおしゃべりする

appsettings.json を修正します。

```sh
$ vim App/appsettings.json
```

ファイルの内容

```json:App/appsettings.json
{
  "Personas": [
    {
      "ListSize": 5,
      "System": "I am Japanese and a learner of English conversation.\nYou must act on your role as a person who is having the short chat with the user.\nYou only use the most simple English words should be used.\nI want to enjoy the short chat possible as you can.\nSo you must keep your reply to use a minimum of sentences.\nYou must remember the words I said to reflect on our short chat.\nYour name is $NAME and your favorite color is $COLOR.\nDo you understand? Here we go!",
      "Name": "John",
      "Color": "Green"
    },
    {
      "ListSize": 5,
      "System": "I am Japanese and a learner of English conversation.\nYou must act on your role as a person who is having the short chat with the user.\nYou only use the most simple English words should be used.\nI want to enjoy the short chat possible as you can.\nSo you must keep your reply to use a minimum of sentences.\nYou must remember the words I said to reflect on our short chat.\nYour name is $NAME and your favorite color is $COLOR.\nDo you understand? Here we go!",
      "Name": "Emily",
      "Color": "Magenta"
    }
  ]
}
```

:::note info
ここでは、二人分の**キャラクターの人格**を設定しています。検証段階なので、細かい性格付けは後回しとしています。
:::

Lib/Service.cs を修正します。

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

ファイルの内容

```csharp:Lib/Service.cs ※ 一部抜粋
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)) {
        // exec each personal.
        List<Persona>? persona_list = _app_settings?.Personals;
        if (persona_list is not null) {
            foreach (Persona persona in persona_list) {
                await executeOneAsync(persona);
            }
        }
    }
}
```

<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();
        Dictionary<string, List<MessageRole>> _map = 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);
            // creat a user and assistant list.
            List<Persona>? persona_list = _app_settings?.Personas;
            if (persona_list is not null) {
                foreach (Persona persona in persona_list) {
                    List<MessageRole> list = new((int) (_app_settings?.Personas?[0].ListSize ?? DEFAULT_LIST_SIZE));
                    if (persona.Name is not null) {
                        _map.Add(persona.Name, list);
                    }
                }
            }
        }
        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)) {
                // exec each personal.
                List<Persona>? persona_list = _app_settings?.Personas;
                if (persona_list is not null) {
                    foreach (Persona persona in persona_list) {
                        await executeOneAsync(persona);
                    }
                }
            }
        }
        async Task executeOneAsync(Persona persona) {
            // exec if a prompt is not empty.
            if (!string.IsNullOrEmpty(_prompt)) {
                // create a system content for the api.
                string? system = persona.System;
                string? name = persona.Name;
                string? color = persona.Color;
                system = system?.Replace("$NAME", name).Replace("$COLOR", color);
#if DEBUG
                ForegroundColor = Red;
                WriteLine($"\nSYSTEM:\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<MessageRole> list = new();
                if (persona.Name is not null) {
                    list = _map[persona.Name];
                    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(persona.Color ?? "Red") { Value = reply });
                // set conversation to a message_role object.
                list.Add(new MessageRole{ User = _prompt, Assistant = reply });
                list = list.Skip(Max(0, _map.Count - (int) (persona.ListSize ?? DEFAULT_LIST_SIZE))).ToList();
#if DEBUG
                ForegroundColor = Red;
                list.ForEach(x => {
                    WriteLine($"USER: {x.User}");
                    WriteLine($"{name}: {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
設定分の ChatGPT の人格をループ処理しています。この時点では **AI 同士はお互いの存在を知りません**。
:::

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 = (ConsoleColor) Enum.Parse(typeof(ConsoleColor), e.Name, true);
                WriteLine(e.Value);
                ResetColor();
            };
            // loop the Service object.
            service.Init();
            do {
                await service.ExecuteAsync();
            } while (true);
        }
    }
}
```

:::note info
コンソールの出力で、彼ら AI の設定の**イメージカラー**を動的に設定しています。
:::

実行した結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/39f25c4b-12b2-db94-cf8d-f4741e36d755.png)

:::note info
ここまでの手順で、二人の人格の ChatGPT と会話することができました。しかし、現在の状態では **AI の二人はお互いに認識していない**ので、それぞれが個別に質問を返してきます。この状況をどのように改善するのかは今後の課題です。※ 記事の説明では英語の会話なのでわかりづらいですが、**日本語**の場合には敬語や語尾などの違いで、**大きな人格の変化**をつけることが可能です。
:::

## まとめ

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

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

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

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/14dbee1cd9821ac182eb

## 参考資料

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