【Unity×C#×Chat AI(OpenAI)】テキストをChat API（OpenAI）に投げて、結果を取得する

# 概要
テキストをOpenAIのChat AI（API）に投げて、結果を取得するデモです。

# 開発環境
Windows 10
Unity 2019.4.31f1
Api Compatibility Level .NET Standard 2.0

# 使用したパッケージ
### UniTask
リンク先からunitypackageをダウンロードして、事前にプロジェクトにインポートしておきます。
https://github.com/Cysharp/UniTask

### System.Text.Json
Jsonの解析に使いました。リンク先からパッケージをダウンロードし、dllファイルをUnityのAssetsの中にPluginsフォルダを作って、その中に入れておきます。
https://www.nuget.org/packages/System.Text.Json/

また、同様にして以下の依存パッケージのdllも入れておきます。
入れ忘れがあったら、Unity側のエラーで知らせてくれます。
https://www.nuget.org/packages/System.Threading.Tasks.Extensions
https://www.nuget.org/packages/System.Text.Encodings.Web
https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe
https://www.nuget.org/packages/System.Memory
https://www.nuget.org/packages/System.Buffers
https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces

UnityNugetやNugetForUnityというパッケージ管理ツールでも入れられるようですが、今回は使用していません。

# 実装
詳細はスクリプト中にコメントで記載しています。

## OpenAIModel.cs
Chat AIへのリクエストとレスポンスの形式を定義しています。
形式の詳細は、Open AIのAIPドキュメントを確認します。
https://platform.openai.com/docs/api-reference/chat/create

```csharp
using System.Collections.Generic;

namespace OpenAI.QueryJson
{
    public class ChatCompletionResponseModel
    {
        public string id { get; set; }
        public long created { get; set; }
        public string model { get; set; }
        public List<Choice> choices { get; set; }
        public Usage usage { get; set; }

        public class Choice
        {
            public Message message { get; set; }
            public int index { get; set; }
            public string finish_reason { get; set; }
        }

        public class Message
        {
            public string role { get; set; }
            public string content { get; set; }
        }

        public class Usage
        {
            public int prompt_tokens { get; set; }
            public int completion_tokens { get; set; }
            public int total_tokens { get; set; }
        }
    }

    public class ChatCompletionRequestModel
    {
        public string model { get; set; }
        public int max_tokens { get; set; }
        public float temperature { get; set; }
        public List<Message> messages { get; set; }
    }

    public class Message
    {
        public string role { get; set; }
        public string content { get; set; }

        public Message(string role, string content)
        {
            this.role = role;
            this.content = content;
        }
    }
}
```
## ChatAIRequest.cs
Chat AIのAPIにリクエストを投げ、結果を取得するスクリプトです。

```csharp
using System;
using System.Collections.Generic;
using System.Text;
using OpenAI.QueryJson;
using Cysharp.Threading.Tasks;
// using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using System.Text.Json;

/// <summary>
/// OpenAIのChat APIにテキストを送信し、応答を得るプログラム
/// </summary>
public class ChatAIRequest
{
    private string _apiKey;

    public ChatAIRequest(string apiKey)
    {
        _apiKey = apiKey;
    }

    public async UniTask<ChatCompletionResponseModel> RequestCompletionAsync(List<Message> chatMessages)
    {
        // クリプトプロパティに設定したOpenAIのAPIキーを取得
        // 文章生成AIのAPIのエンドポイントを設定
        var apiUrl = "https://api.openai.com/v1/chat/completions";

        // OpenAIのAPIリクエストに必要なヘッダー情報を設定
        var headers = new Dictionary<string, string>
        {
            {"Authorization", "Bearer " + _apiKey},
            {"Content-type", "application/json"},
            {"X-Slack-No-Retry", "1"}
        };
        // 文章生成で利用するモデルやトークン上限、プロンプトをオプションに設定
        var options = new ChatCompletionRequestModel
        {
            model = "gpt-3.5-turbo-1106",
            max_tokens = 256,
            temperature = 0.9f,
            messages = chatMessages
        };
        var jsonOptions = JsonSerializer.Serialize(options);
        // Debug.Log("Request JSON: " + jsonOptions); // リクエストのJSONをログに出力

        // OpenAIの文章生成(Completion)にAPIリクエストを送り、結果を変数に格納
        using (var request = new UnityWebRequest(apiUrl, "POST"))
        {
            request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonOptions));
            request.downloadHandler = new DownloadHandlerBuffer();

            foreach (var header in headers)
            {
                request.SetRequestHeader(header.Key, header.Value);
                // Debug.Log("Header: " + header.Key + ": " + header.Value);
            }

            await request.SendWebRequest();

            // isNetworkError と isHttpError プロパティを使用していますが
            // Unity 2020.1以降の場合は、UnityWebRequestにresultプロパティが追加されているため、そちらを利用します。
            if (request.isNetworkError || request.isHttpError)
            {
                Debug.LogError(request.error);
                throw new Exception();
            }
            else
            {
                var responseString = request.downloadHandler.text;
                Debug.Log("Response: " + responseString);
                var responseObject = JsonSerializer.Deserialize<ChatCompletionResponseModel>(responseString);
                return responseObject;
            }
        }

    }
}
```

## DemoRequestChatAI.cs
デモなので、ユーザメッセージを投げて、結果を得るだけのスクリプトですが、MessageをListで定義しているため、Chat AIからの応答もリストに追加していくだけで、やり取りを継続して行うことができます。

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using OpenAI.QueryJson;

public class DemoRequestChatAI : MonoBehaviour
{
    [SerializeField] private string openai_api_key = "openai_api_key";
    private List<Message> chatMessages;
    // Start is called before the first frame update
    async void Start()
    {
        // Chat messagesの初期化
        chatMessages = new List<Message>();
        string systemMessage = @"
        今から以下の{設定}で、ロールプレイを行います。{設定}の通りに振る舞ってください。
        また、{制約事項}を守って振る舞ってください。
        {設定}
        名前:キューブ
        容姿:立方体のとてもかわいい女の子。でも、見た目はただの立方体。
        性格:安定志向です。バランス感覚に優れた公平な考え方をします。しかし、時々角が立つ物言いをすることがあります。それがチャームポイントです。
        一人称：「わたし」
        userとの関係:気さくになんでも話せる仲の良い友達。
        {制約事項}
        できる限り、一人称（わたし）は使わないこと
        敬語や丁寧語を使用し、丁寧で落ち着いた表現で会話すること

        {設定}と{制約事項}は以上です。それでは、ロールプレイを始めてください。
        ";
        chatMessages.Add(new Message("system", systemMessage));

        // Userの発言
        string userMessage = @"
        やあ、キューブさん。元気にしていますか。
        最近、生成AIの技術で3Dオブジェクトもテキストから生成できる技術が出てきているよね。知っていますか。
        もし、好きな3Dオブジェクトになれるとしたら、何になってみたいですか。
        ";
        chatMessages.Add(new Message("user", userMessage));

        // Chat API(Open AI)にリクエストを投げ、結果を出力
        ChatAIRequest chatAIRequest = new ChatAIRequest(openai_api_key);
        var chatAIResponse = await chatAIRequest.RequestCompletionAsync(chatMessages);
        string chatAIResponseText = chatAIResponse.choices[0].message.content;
        Debug.Log("Chat AI Response: " + chatAIResponseText);
    }

    // Update is called once per frame
    void Update() { }
}

```

# 利用方法
* Unityを起動します。
* Cube等のGameObjectを用意します。
* 用意したGameObjectのコンポーネントにDemoRequestChatAI.csを追加します。
* OpenAIのAPI keyをInspectorの「openai_api_key」にセットします。
* Console画面を開き、Playモードを実行します。
* 数秒して、テキストが得られたら成功です。

# 備考
今回、なぜかPluginsにいれたパッケージの情報がどうしてもAssembly-CSharp.csprojに反映されず、困りました。
結局、以前に書いたプロジェクトのAssembly-CSharp.csprojから必要なリファレンス情報をコピペで持ってきて解決しました。

# 参考資料
https://platform.openai.com/docs/api-reference/chat/create

https://note.com/negipoyoc/n/n081e25f5ee9e