今回作成したのは、OpenAI との通信部分だけで、アバターのモデル表示などは扱っていません。
本稿では、OpenAI の API を利用し、GPT 3.5 で通信する方法、OpenAI の API を利用する際の注意点などについて説明します。
本稿に掲載しているソースコードは、以下の環境で動作確認しています。
Visual Studio 2022
.Net Standard 2.0
C# 7
Windows 11 Home
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 でも足りるかも?
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 を作成するしかないです。
Visual Studio 2022 を起動して、プロジェクトを新規作成し、コンソール C# を選びます。
以降のデフォルトのターゲットフレームワークの選び方、ターゲットフレームワークを .Net Standard 2.0 に変更する方法は以下の記事で説明しています。
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 にパスを通しても良いと思います。
うーん…ツンデレ妹にしたかったんですが、ツンが足りないですね…。
距離感はあるけど、仲が悪いわけじゃない妹って感じです。
あと、徐々にキャラが崩れてる。
ここまでの手順に間違いがないなら、クレジットが残っているか確認します。
クレジットが切れた場合、課金してから新しく API Key を作る必要があります。
新しく作った API Key を使います。
クレジットが切れていなくても、たまにレスポンスが返ってこないことがあります。
長文での返答が必要になるような質問を投げると、1分くらいレスポンスが返ってこないこともあります。
5分くらい様子を見てもダメなら、プログラムを再起動します。










