ChatGPT API で C# でコマンドラインで会話する (AI の二人が会話)
こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 22.04 で ChatGPT API を C# から使いコマンドラインから会話する方法を紹介します。
目的
Windows 11 の Linux でクラウド開発します。
こちらから記事の一覧がご覧いただけます。
実現すること
ローカル環境の Ubuntu で、ChatGPT API を C# から使いコマンドラインから会話します。
この記事では前回の記事の続きより、別人格の AI の二人を会話させることを試みます。
C# には本来厳格なコーディング規則がありますが、この記事では可読性のために、一部規則に沿わない表記方法を使用しています。ご注意ください。
関連記事
OpenAI API と比較してみましょう!
OpenAI API
技術トピック
ChatGPT API とは?
こちらを展開してご覧いただけます。
ChatGPT API
ChatGPT API は、OpenAI の言語モデルである GPT-3 をベースにした、自然言語による対話を実現するためのAPIです。
特徴 |
---|
ChatGPT API を利用することで、開発者は自分たちのアプリケーションやサービスに自然言語の対話機能を追加することができます。 |
ChatGPT API は、大規模なトレーニングデータを用いて学習された GPT-3 の言語理解力を利用し、自然言語での質問や会話に対して自然で流暢な回答を生成することができます。 |
また、ChatGPT API は、OpenAI が提供するプラットフォームである OpenAI Codex と組み合わせて、より高度な対話型アプリケーションを構築することも可能です。 |
開発環境
- Windows 11 Home 22H2 を使用しています。
WSL の Ubuntu を操作していきますので macOS の方も参考にして頂けます。
WSL (Microsoft Store アプリ版) ※ こちらの関連記事からインストール方法をご確認いただけます
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
Ubuntu ※ こちらの関連記事からインストール方法をご確認いただけます
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.1 LTS
Release: 22.04
.NET SDK ※ こちらの関連記事からインストール方法をご確認いただけます
$ dotnet --list-sdks
7.0.202 [/usr/share/dotnet/sdk]
$ dotnet --version
7.0.202
この記事では基本的に Ubuntu のターミナルで操作を行います。Vim を使用してコピペする方法を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
ChatGPT と会話する手順
前回までにできたこと
ここまでの手順で、二人の人格の ChatGPT と会話することができました。しかし、現在の状態では AI の二人はお互いに認識していないので、それぞれが個別に質問を返してきます。この状況を改善することが課題でした。
AI にどのようにアプローチするか?
ここでは AI に、他の人格の存在を認知させる方法を考えてみます。
No | 内容 | 埋め込み先パラメータ |
---|---|---|
1 | AI の人格にたくさんの情報をあらかじめ持たせる。(※ 会話を広げるため) | ChatGPT API chat message pole system |
2 | User と他の人格の AI の会話の内容を随時知らせる。 | ChatGPT API chat message pole system |
どちらの内容も、ChatGPT API chat message pole system に動的に埋め込む実装を想定します。
AI のペルソナを作成
Profile プロフィールを作成します。
内容を表示します。
パラメータ | 内容 |
---|---|
name | 名前 |
full name | フルネーム |
gender | 性別 |
age | 年齢 |
country | 出身地 |
job | 職業 |
style | スタイル |
Favorite 好きなものリストを作成します。
内容を表示します。
パラメータ | 内容 |
---|---|
color | 色 |
food | 食べ物 |
drink | 飲み物 |
flower | 花 |
book | 本 |
music | 音楽 |
プロジェクトフォルダに移動します。
$ cd ~/tmp/ChatGPT
AppSettings.cs を編集します。
$ vim Lib/AppSettings.cs
ファイルの内容を表示します。
ファイルの内容
namespace ChatGPT.Lib {
public class AppSettings {
public List<Persona>? Personas { get; set; }
public string? User { get; set; }
}
public class Persona {
public int? ListSize { get; set; }
public string? System { get; set; }
public Profile? Profile { get; set; }
public Favorite? Favorite { get; set; }
}
public class Profile {
public string? Name { get; set; }
public string? FullName { get; set; }
public string? Gender { get; set; }
public string? Age { get; set; }
public string? Country { get; set; }
public string? Job { get; set; }
public string? Style { get; set; }
}
public class Favorite {
public string? Color { get; set; }
public string? Food { get; set; }
public string? Drink { get; set; }
public string? Flower { get; set; }
public string? Book { get; set; }
public string? Music { get; set; }
}
}
appsettings.json を修正します。
$ vim App/appsettings.json
ファイルの内容を表示します。
ファイルの内容
{
"Personas": [
{
"ListSize": 3,
"System": "$USER\nYour name is $NAME and your favorite color is $COLOR.\nYou are a $AGE year old $GENDER from the $COUNTRY.\nYour job is $JOB and your talk and chat style is very $STYLE.\n\nAre you ready? Let's act your role! Here we go!\nKeep short chat not be long! You act as $NAME.\n",
"Profile": {
"Name": "John",
"FullName": "John Doe",
"Gender": "Male",
"Age": "20",
"Country": "USA",
"Job": "Musician",
"Style": "Flank"
},
"Favorite": {
"Color": "Green",
"Food": "Pizza",
"Drink": "Beer",
"Flower": "Rose",
"Book": "The Great Gatsby",
"Music": "Rock music"
}
},
{
"ListSize": 3,
"System": "$USER\nYour name is $NAME and your favorite color is $COLOR.\nYou are a $AGE year old $GENDER from the $COUNTRY.\nYour job is $JOB and your talk and chat style is very $STYLE.\n\nAre you ready? Let's act your role! Here we go!\nKeep short chat not be long! You act as $NAME.\n",
"Profile": {
"Name": "Emily",
"FullName": "Emily Smith",
"Gender": "Female",
"Age": "16",
"Country": "USA",
"Job": "High School Student",
"Style": "Cute"
},
"Favorite": {
"Color": "Magenta",
"Food": "Sushi",
"Drink": "Bubble Tea",
"Flower": "Sunflower",
"Book": "Harry Potter series",
"Music": "Pop music"
}
}
],
"User": "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.\n"
}
ここまでの手順で、AI に、プロフィールや好きなものを知らせるパラメータクラスと、その JSON パラメータを作成することができました。
プログラムの中で AI に他の AI の情報を伝える
ここからはプログラムを大幅に改造していきます。
system パラメータを動的に作成するジェネレータクラスを作成します。
Generator.cs を作成します。
$ vim Lib/Generator.cs
ファイルの内容
public string? GetSystem(string? profileName) {
// create a system content for the api.
_skip_user = false;
string? user = _app_settings?.User;
Persona? persona = _app_settings?.Personas?.FirstOrDefault(x => x.Profile?.Name?.Equals(profileName) == true);
string? system = persona?.System;
system = system?
.Replace("$USER", user)
.Replace("$NAME", persona?.Profile?.Name)
.Replace("$AGE", persona?.Profile?.Age)
.Replace("$GENDER", persona?.Profile?.Gender)
.Replace("$COUNTRY", persona?.Profile?.Country)
.Replace("$JOB", persona?.Profile?.Job)
.Replace("$STYLE", persona?.Profile?.Style)
.Replace("$COLOR", persona?.Favorite?.Color)
.Replace("$FOOD", persona?.Favorite?.Food)
.Replace("$DRINK", persona?.Favorite?.Drink)
.Replace("$FLOWER", persona?.Favorite?.Flower)
.Replace("$BOOK", persona?.Favorite?.Book)
.Replace("$MUSIC", persona?.Favorite?.Music);
return system + getOtherChat(profileName);
}
ファイルの内容を表示します。
ファイルの内容
namespace ChatGPT.Lib {
public class Generator {
readonly AppSettings? _app_settings;
readonly Dictionary<string, List<Service.MessageRole>> _map = new();
bool _skip_user = false;
string _name_to_answer = string.Empty;
public bool SkipUser { get => _skip_user; }
public string NameToAnswer { get => _name_to_answer; }
public Generator(AppSettings appSettings, Dictionary<string, List<Service.MessageRole>> map) {
_app_settings = appSettings;
_map = map;
}
public string? GetSystem(string? profileName) {
// create a system content for the api.
_skip_user = false;
string? user = _app_settings?.User;
Persona? persona = _app_settings?.Personas?.FirstOrDefault(x => x.Profile?.Name?.Equals(profileName) == true);
string? system = persona?.System;
system = system?
.Replace("$USER", user)
.Replace("$NAME", persona?.Profile?.Name)
.Replace("$AGE", persona?.Profile?.Age)
.Replace("$GENDER", persona?.Profile?.Gender)
.Replace("$COUNTRY", persona?.Profile?.Country)
.Replace("$JOB", persona?.Profile?.Job)
.Replace("$STYLE", persona?.Profile?.Style)
.Replace("$COLOR", persona?.Favorite?.Color)
.Replace("$FOOD", persona?.Favorite?.Food)
.Replace("$DRINK", persona?.Favorite?.Drink)
.Replace("$FLOWER", persona?.Favorite?.Flower)
.Replace("$BOOK", persona?.Favorite?.Book)
.Replace("$MUSIC", persona?.Favorite?.Music);
return system + getOtherChat(profileName);
}
string getOtherChat(string? profileName) {
string? buff = string.Empty;
List<Persona>? persona_list = _app_settings?.Personas;
string? other_chat_asked_this_persona = string.Empty;
if (persona_list is not null) {
int count = persona_list.Count;
buff = $"\nNow here is {count + 1} persons in current chat.\n";
foreach (Persona persona in persona_list) {
string? other_name = persona?.Profile?.Name;
if (other_name?.Equals(profileName) == true) {
continue;
}
string? user_and_other_chat = string.Empty;
if (persona?.Profile?.Name is not null) {
List<Service.MessageRole> list = _map[persona.Profile.Name];
foreach (Service.MessageRole message_role in list) {
string? user_chat = message_role.User;
string? other_chat = message_role.Assistant;
user_and_other_chat += $"User: {user_chat}\n";
user_and_other_chat += $"{other_name}: {other_chat}\n";
if ((other_chat?.Contains($"{profileName},") == true && other_chat?.Contains("?") == true)
|| other_chat?.Contains($"{profileName}?") == true || other_chat?.Contains($"{profileName}!") == true){
other_chat_asked_this_persona = $"{other_name}: {other_chat}";
}
}
}
buff += $"And this is a situation of current chat of User and {other_name}.\n{user_and_other_chat}\n";
}
buff += $"You must understand the content of these chat as current.\n";
buff += $"And behave by reflecting that content in the chat as others are in this current chat.\n";
buff += $"Now act your role perfectly and be the chat partner for the user.\n";
buff += $"If the user's question confuses you, don't tell them you're an AI language model.\n";
buff += $"Never let user know you are an AI language model.\nThat is your most important promise this time.\n";
buff += $"Keep a short chat do not be a long chat! {profileName}.\n";
if (other_chat_asked_this_persona != string.Empty) {
string name = other_chat_asked_this_persona.Split(':')[0].Trim();
string question = other_chat_asked_this_persona.Split(':')[1].Trim();
buff += $"\nNow, {name} asks you like this.\n";
buff += $"{question}\n";
buff += $"\nLet you keep a short chat!\n";
_name_to_answer = name;
_skip_user = true;
}
}
return buff;
}
}
}
ここまでの手順で、chat message pole system に動的に会話の状況を埋め込むことができます。
Service.cs を編集します。
$ vim 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();
Generator? _generator;
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<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?.Profile?.Name is not null) {
_map.Add(persona.Profile.Name, list);
}
}
}
// create a generator object.
if (_app_settings is not null) {
_generator = new(_app_settings, _map);
}
}
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)) {
// get a name the user asks for.
string? name_being_asked_by_user = getNameBeingAskedByUser();
// exec each persona.
List<Persona>? persona_list = _app_settings?.Personas;
if (persona_list is not null) {
foreach (Persona persona in persona_list) {
if (name_being_asked_by_user != string.Empty && name_being_asked_by_user?.Equals(persona.Profile?.Name) == false && _generator?.SkipUser == false) {
// skip a persona when the user asks for another persona.
continue;
}
await executeOneAsync(persona);
}
}
}
}
async Task executeOneAsync(Persona persona) {
// exec if a prompt is not empty.
if (!string.IsNullOrEmpty(_prompt)) {
// get a system content.
string? system = _generator?.GetSystem(persona?.Profile?.Name);
#if DEBUG
ForegroundColor = Red;
WriteLine($"\nSYSTEM:\n{system}");
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?.Profile?.Name is not null) {
list = _map[persona.Profile.Name];
#if DEBUG
ForegroundColor = Cyan;
WriteLine($"P1 list count: {list.Count}");
ResetColor();
#endif
list.ForEach(x => {
chat_message_list.Add(new ChatMessage(ChatMessageRole.User, x.User));
chat_message_list.Add(new ChatMessage(ChatMessageRole.Assistant, x.Assistant));
});
}
#if DEBUG
ForegroundColor = Red;
list.ForEach(x => {
WriteLine($"User: {x.User}");
WriteLine($"{persona?.Profile?.Name}: {x.Assistant}");
});
ResetColor();
#endif
// edit a prompt.
string edit_prompt = string.Empty;
if (_generator?.SkipUser == false) {
edit_prompt = $"{_prompt} You act as {persona?.Profile?.Name}.";
} else {
edit_prompt = $"Let you answer to {_generator?.NameToAnswer}. You act as {persona?.Profile?.Name}.";
}
#if DEBUG
ForegroundColor = Blue;
WriteLine($"USER: {edit_prompt}\n");
ResetColor();
#endif
// set current user content.
chat_message_list.Add(new ChatMessage(ChatMessageRole.User, edit_prompt));
// get a result from the api.
ChatResult? result = await _api.Chat.CreateChatCompletionAsync(new ChatRequest() {
Model = Model.ChatGPTTurbo,//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?.Favorite?.Color ?? "Red") { Value = reply });
// set conversation to a message_role object.
list.Add(new MessageRole{ User = edit_prompt, Assistant = reply });
#if DEBUG
ForegroundColor = Cyan;
WriteLine($"P2 list count: {list.Count}");
ResetColor();
#endif
list = list.Skip(Max(0, list.Count - (int) (persona?.ListSize ?? DEFAULT_LIST_SIZE))).ToList();
#if DEBUG
ForegroundColor = Cyan;
WriteLine($"P3 list count: {list.Count}");
ResetColor();
#endif
if (persona?.Profile?.Name is not null) {
_map.Remove(persona.Profile.Name);
_map[persona.Profile.Name] = list;
}
}
}
string? getNameBeingAskedByUser() {
List<Persona>? persona_list = _app_settings?.Personas;
if (persona_list is not null) {
foreach (Persona persona in persona_list) {
if (_prompt?.StartsWith($"{persona?.Profile?.Name}, ") == true) {
return persona?.Profile?.Name;
}
}
}
return string.Empty;
}
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);
public class MessageRole {
public string? User { get; set; }
public string? Assistant { get; set; }
}
}
}
ここまでの手順で、AI が、作成された chat message pole system を元に、適切な会話を行うことができます。
Program.cs を修正します。
$ vim 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);
}
}
}
プログラムを実行します。
$ dotnet run --project App/App.csproj --configuration Release
以下のコマンドで実行すると、デバッグ表示が出力されます。
$ dotnet run --project App/App.csproj
ここまでの手順で、別人格の AI である John と Emily をお互いに会話させることに成功しました。
この記事の実装は粗削りではありますが、ChatGPT API で実装できる可能性を感じることができます。特に、ユーザーが皆さん!と問いかけた後、John が自発的に Emily に問いかけをする箇所は、彼が完全に会話の場の空気を読めていることを証明しています。
まとめ
ローカル環境の Ubuntu で、ChatGPT API を C# から使いコマンドラインから会話することができました。
実際の開発では、軽量なテキストエディタである VS Code や、IDE (統合開発環境) を使用して、.NET プログラムを開発することが一般的です。しかし、dotnet コマンドでビルドしたり、実行したりすることも、.NET 開発環境を理解する上で役立ちます。
どうでしたか? Window 11 の WSL Ubuntu に、.NET の開発環境を手軽に構築することができます。ぜひお試しください。今後も .NET の開発環境などを紹介していきますので、ぜひお楽しみにしてください。
参考資料