今回作成したのは、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分くらい様子を見てもダメなら、プログラムを再起動します。