【C#】OpenAI の GPT 3.5 を使って AI とチャットするプログラムを作る【.Net Standard 2.0】

興味があったので、こちらの記事を参考に ChatGPT と対話するコンソールアプリを C# で作りました。
今回作成したのは、OpenAI との通信部分だけで、アバターのモデル表示などは扱っていません。

本稿では、OpenAI の API を利用し、GPT 3.5 で通信する方法、OpenAI の API を利用する際の注意点などについて説明します。
本稿に掲載しているソースコードは、以下の環境で動作確認しています。

Visual Studio 2022
.Net Standard 2.0
C# 7
Windows 11 Home

もくじ

必要な準備

OpenAI のアカウントを取得する。
登録方法はググれば出てきます。

OpenAI の API にアクセス。
上記のブログカードから OpenAI にアクセス。


※タップまたはクリックすると大きい画像で表示します。

OpenAI のプラットフォーム選択ページで API にアクセス。

ChatGPT にウェブブラウザでアクセスすれば、AI とチャットできますが、自分が作ったプログラムで AI とチャットするには、OpenAI の API を利用する必要があります。

残りのクレジットを確認。

Manager Account > Usage にアクセスして、あとどれくらい OpenAI の API を使えるか確認します。


※タップまたはクリックすると大きい画像で表示します。

Credit used $28.00 は、アカウント登録時に無料でもらえるクレジットです。
このクレジットには有効期限があります。

私のアカウントでは Expired 2023年6月1日(UTC) なので、2023年6月1日午前9時(JST) に期限が切れていて、$18.00 分はもう使えません。
(残り $10 は来年まで待たないともらえません。)
このクレジットの期限が切れていなければ、課金しなくても $18.00 分は OpenAI の API を使えます。
期限が切れている場合は課金する必要があります。

GPT 3.5 なら、$10 課金すれば十分です。$5 でも足りるかも?

API Key を作成する。

C# のコードから OpenAI の API にアクセスするときに、API Key を使って認証します。
API Key とクレジットが紐づいているので、他人に公開しないよう注意が必要です。
自分以外の誰かが、自分の API Key を使うと、自分のクレジットが減ります。
※それを意図してやっている、かつ、第三者と API Key を共有するのが OpenAI の利用規約で許可されていれば問題ないです。

 (vi) buy, sell, or transfer API keys without our prior consent;

Restriction(禁止事項)に、「(vi) 当社の事前の同意なしに API キーを売買または譲渡すること、」

との記載がありますが、API Key を共有することについては不明です。
不明な点は OpenAI に英語で聞いて確認を取る必要があります。
API Key を共有しないなら聞く必要はありません。

API Key は作成したときしか見れないので、メモ帳などに張り付けてローカルに保存しておく必要があります。
(覚えておけるならそれが一番ですが…。)

API Key が分からなくなってしまった場合は、分からなくなった API Key を削除して、新しい API Key を作成するしかないです。

C# のコンソールアプリを作成する。

Visual Studio 2022 を起動して、プロジェクトを新規作成し、コンソール C# を選びます。
以降のデフォルトのターゲットフレームワークの選び方、ターゲットフレームワークを .Net Standard 2.0 に変更する方法は以下の記事で説明しています。

NuGet パッケージをインストール。

Json データのシリアライズ/デシリアライズに便利な NewtonSoft.Json を NuGet パッケージマネージャーでインストールします。

メニュー>ツール>NuGet パッケージマネージャー>ソリューションの NuGet パッケージの管理>参照>NewtonSoft.Json を検索>最新版をインストール

ソースファイルを作成する。

新規で cs ファイルを2つ追加します。

1. HttpRequest.cs
2. GPTChat.cs

Program.cs はプロジェクト作成時に自動的に生成されるので、合計3つの cs ファイルがプロジェクトにある状態にします。

以下をコピペします。

// HttpRequest.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Net.Http;

using ResponseTask = System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>;

public class SimpleRequest {
	readonly string accessURL;
	readonly Dictionary<string, string> headers = new Dictionary<string, string>();

	public SimpleRequest(string url) {
		if (string.IsNullOrEmpty(url)) { throw new ArgumentNullException("url"); }
		this.accessURL = url;
	}
	public void AddHeader(string name, string value) {
		this.headers.Add(name, value);
	}
	public void ClearHeaders() {
		this.headers.Clear();
	}
	public ResponseTask Post(StringContent content) {
		var client  = new HttpClient();
		var request = new HttpRequestMessage(HttpMethod.Post, this.accessURL);
		foreach (KeyValuePair<string, string> header in this.headers) {
			request.Headers.Add(header.Key, header.Value);
		}
		this.ClearHeaders();
		request.Content = content;
		return client.SendAsync(request);
	}
}
// GPTChat.cs
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;

using ResponseTask = System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>;

public class GPTChatV35 {
	// json 形式にシリアライズする。
	[Serializable]
	public class MessageV35 {
		public string role;
		public string content;
	}

	// json 形式にシリアライズする。
	[Serializable]
	public class CompletionRequestV35 {
		public string           model;
		public List<MessageV35> messages;
	}

	// OpenAI からの返答を受け取るためのデータ。
	// 階層構造とフィールド変数名が受け取った json データと完全に一致している必要がある。
	// バージョンアップ時に変更される可能性があるので、利用する GPT のバージョンに合わせて
	// 修正が必要になる場合がある。
	// json 形式からデシリアライズする。
	[Serializable]
	public class ResponseV35 {
		public string   id;
		public string   @object;
		public int      created;
		public string   model;
		public Choice[] choices;
		public Usage    usage;

		[Serializable]
		public class Choice {
			public int        index;
			public MessageV35 message;
			public string     finish_reason;
		}

		[Serializable]
		public class Usage {
			public int prompt_tokens;
			public int completion_tokens;
			public int total_tokens;
		}
	}

	public string APIKey    { get; protected set; }
	public string AccessURL { get; protected set; }
	public string Character { get; protected set; }

	protected virtual string DefaultURL { get { return "https://api.openai.com/v1/chat/completions"; } }
	protected virtual string Version    { get { return "gpt-3.5-turbo"; } }

	//	他のバージョンでも GPT 3.5 と構造が変わらないなら継承先で使用できる。
	//	変わる場合は MessageV40 などを新しく作成し、MessageV35 を使用している
	//	全てのメソッドをオーバーライドする必要がある。
	protected List<MessageV35> MessagesV35 { get; set; } = new List<MessageV35>();

	//	チャットが終わったら true になる。
	//	AI との通信が終わったかどうかを判定する目的で使う。
	//	AI と通信を行う前に例外がスローされた場合は true にならない。
	public bool IsFinished { get; protected set; } = false;

	static readonly string Default_Character = "語尾に「ヒャッハー!」をつけて";

	public GPTChatV35(string api_key, string url, string character) {
		if (string.IsNullOrEmpty(api_key)) { throw new System.ArgumentNullException("api_key"); }
		this.APIKey    = api_key;
		this.AccessURL = url;
		this.Character = character;
	}

	public virtual async void Send(string prompt, Func<ResponseV35, bool> response_callback) {
		this.AddSystemContentToMessages();

		if (string.IsNullOrEmpty(this.AccessURL)) { this.AccessURL = this.DefaultURL; }

		var request = new SimpleRequest(this.AccessURL);

		this.AddHeadersToRequest(request);
		this.AddUserContentToMessages(prompt);

		StringContent       send_content = this.GetStringContentFromMessages();
		HttpResponseMessage response     = await request.Post(send_content);

		if (response.StatusCode == System.Net.HttpStatusCode.OK) {
			string response_content = await response.Content.ReadAsStringAsync();
			if (!string.IsNullOrEmpty(response_content)) {
				ResponseV35 deserialized_content = JsonConvert.DeserializeObject<ResponseV35>(response_content);
				if (deserialized_content != null ) {
					//	success
					this.IsFinished = true;
					_ = response_callback.Invoke(deserialized_content);
				}
			}
		} else {
			//	error
			_ = response_callback.Invoke(new ResponseV35());
		}
	}

	protected virtual void AddSystemContentToMessages() {
		string character = this.Character;
		if (string.IsNullOrEmpty(this.Character)) {
			character = GPTChatV35.Default_Character;
		}
		var message = new MessageV35() {
			role    = "system",
			content = character,
		};
		this.MessagesV35.Add(message);
	}

	protected virtual void AddHeadersToRequest(SimpleRequest request) {
		request.AddHeader("Authorization"   , "Bearer " + this.APIKey);
		request.AddHeader("X-Slack-No-Retry", "1");
	}

	protected virtual void AddUserContentToMessages(string prompt) {
		var message = new MessageV35() {
			role    = "user",
			content = prompt,
		};
		this.MessagesV35.Add(message);
	}

	protected virtual StringContent GetStringContentFromMessages() {
 		var options = new CompletionRequestV35() {
			model    = this.Version,
			messages = this.MessagesV35,
		};
		string json    = JsonConvert.SerializeObject(options);
		var    content = new StringContent(json, Encoding.UTF8, "application/json");
		return content;
	}
}
// Program.cs
using System;

class Program {
	static readonly string Default_Api_Key = "ここに自分の API Key をコピペ";

	static bool IsReceived { get; set; } = true;

	static void Main(string[] args) {
		Console.WriteLine("Start OpenAI GPT chat" + System.Environment.NewLine);

		bool must_input_api_key      = false;
		bool must_input_ai_character = false;

		foreach (string arg in args) {
			if (arg == "-k") { must_input_api_key      = true; }
			if (arg == "-c") { must_input_ai_character = true; }
		}
		do {
			bool must_finish = false;
			string api_key   = string.Empty;
			string character = string.Empty;

			if (must_input_api_key) {
				api_key = Program.InputAPIKey(ref must_finish);
				if (must_finish) { break; }
			} else {
				if (string.IsNullOrEmpty(Default_Api_Key)) {
					Console.WriteLine("Default_Api_Key が未設定のため、コマンドライン引数に -k を指定する必要があります。");
					break;
				}
			}
			if (must_input_ai_character) {
				character = Program.InputAICharacter(ref must_finish);
				if (must_finish) { break; }
			}
			if (string.IsNullOrEmpty(api_key)) { api_key = Default_Api_Key; }

			Program.InputPrompt(api_key, character);
		}
		while (false);

		Console.WriteLine(System.Environment.NewLine + "Finished");
	}

	static bool MustFinishOrNot(string prompt) {
		if (string.IsNullOrEmpty(prompt)) { return false; }
		if (prompt.ToLower() == "q" || prompt.ToLower() == "quit" || prompt.ToLower() == "exit") {
			return true;
		}
		return false;
	}

	static string GetInputString(ref bool must_finish, string prompt_message) {
		while (!must_finish) {
			Console.Write("{0} ? ", prompt_message);

			string prompt = Console.ReadLine();
			if (string.IsNullOrEmpty(prompt)) { continue; }

			prompt = prompt.Trim();
			must_finish = MustFinishOrNot(prompt);

			return prompt;
		}
		return string.Empty;
	}

	static string InputAPIKey(ref bool must_finish) {
		return Program.GetInputString(ref must_finish, "OpenAI API Key");
	}

	static string InputAICharacter(ref bool must_finish) {
		return Program.GetInputString(ref must_finish, "AI Character");
	}

	static void InputPrompt(string api_key, string character) {
		var  chat        = new GPTChatV35(api_key, string.Empty, character);
		bool must_finish = false;

		while (!must_finish) {
			if (!Program.IsReceived) { continue; }

			string prompt = Program.GetInputString(ref must_finish, "Prompt");

			if (must_finish) { continue; }
			if (string.IsNullOrEmpty(prompt)) { continue; }

			Program.IsReceived = false;
			Console.WriteLine("waiting for reply...");

			chat.Send(prompt, Program.OnReceived);
		}

		Program.WaitForFinish(chat);
	}

	static bool OnReceived(GPTChatV35.ResponseV35 response) {
		if (response == null) { return false; }
		Console.WriteLine(response.choices[0].message.content);
		Program.IsReceived = true;
		return true;
	}

	static void WaitForFinish(GPTChatV35 chat) {
		while (!chat.IsFinished) { System.Threading.Thread.Sleep(1); }
	}
}

掲載したコードについて。

掲載したコードは .Net Standard 2.0 に合わせて作成したものです。
対応する C# のバージョンは 7 です。
そのため、文法が古いです。
これは、Unity のマルチプラットフォーム対応 DLL に移植することを想定しているためです。

Program.Default_API_Key に、自分の API Key をコピペしてください。
デフォルトの API Key を指定したくない場合は、string.Empty を指定してください。

AI とチャットする仕組みは単純で、HTTP プロトコルを使って、シリアライズした json 形式のデータを POST しているだけです。
レスポンスも json で返って来るので、デシリアライズしてから必要なデータを抽出するだけです。
プロトコルの詳細は公式のドキュメントを参照。

プログラムの使い方。

プログラムを終了するには、q か quit か exit を入力します。
Ctrl + C で強制終了しても構いませんし、コンソールウィンドウの閉じるボタンで終了しても構いません。


コマンドライン引数に -k を指定することで、プログラム起動時に API Key の入力ができるようになりますが、プログラムを起動する度に入力する必要があり、少々面倒なのでお勧めはしません。
-k を指定しない場合は、Program.Default_Api_Key が使われます。


コマンドライン引数に -c を指定することで、AI の性格を入力できるようになります。
-c を指定しない場合はデフォルトの性格(語尾に「ヒャッハー!」をつける)になります。

例えば、以下のような性格を指示することもできます。

あなたは私の妹で、お兄ちゃんが大好きですが、恥ずかしがり屋で素直に気持ちを伝えることができず、ついつい厳しい言葉を使ってしまいます。たまに素直な気持ちを伝えることがあります。

AI の性格をどのようなものにするかは、この指示が AI にとって適切かどうかにかかっています。
自分の出した指示が AI にとって適切かどうかは、しばらく会話しないと分かりません。

こだわりだすと時間とクレジットが溶けます。

動作確認。

実行中に例外が出たとき。

プログラムを実行して、AI に送信する会話を入力してエンターしたとき、下記のような例外が出てプログラムが中断するかも知れません。

ハンドルされていない例外: System.IO.FileNotFoundException: ファイルまたはアセンブリ 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。
   場所 GPTChatV35.d__29.MoveNext()
   場所 System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Start[TStateMachine](TStateMachine& stateMachine)
   場所 GPTChatV35.Send(String prompt, Func`2 response_callback)
   場所 Program.InputPrompt(String api_key, String character) 場所 C:\Users\ユーザー名\Desktop\OpenAI Test\Program.cs:行 92
   場所 Program.Main(String[] args) 場所 C:\Users\ユーザー名\Desktop\OpenAI Test\Program.cs:行 39

この例外が出たら、以下の対応が必要です。

C:\Users\ユーザー名\.nuget\packages\newtonsoft.json\バージョン\lib\netstandard2.0\Newtonsoft.Json.dll

上記のファイルを、プログラムの実行ファイルがあるフォルダにコピーします。
Debug ビルドした場合は、プロジェクトのフォルダ/bin/Debug/netstandard2.0/ になります。

環境変数で上記の dll にパスを通しても良いと思います。

実行結果。

※タップまたはクリックすると大きい画像で表示します。

うーん…ツンデレ妹にしたかったんですが、ツンが足りないですね…。
距離感はあるけど、仲が悪いわけじゃない妹って感じです。
あと、徐々にキャラが崩れてる。


こんな感じの妹を想像していました(後ろにいるのは誰だろう?)

OpenAI からレスポンスが返ってこないとき。


ここまでの手順に間違いがないなら、クレジットが残っているか確認します。

クレジットが切れた場合、課金してから新しく API Key を作る必要があります。
新しく作った API Key を使います。

クレジットが切れていなくても、たまにレスポンスが返ってこないことがあります。
長文での返答が必要になるような質問を投げると、1分くらいレスポンスが返ってこないこともあります。
5分くらい様子を見てもダメなら、プログラムを再起動します。

※本稿のアイキャッチ画像と、いくつかの画像は Microsoft Bing Image Creator で生成したものです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です