GPT-4oとFunction callingをUnityで動かす

今さらですが、ようやくFunction callingをUnityで使う方法が分かったので、忘れないうちにメモをしておきます。
これはLLMから関数を実行するもので、そこに処理を書いておけば特定の処理を呼び出して実行することができます。
例えば以下のようなことができます。
・「写真を撮って」がトリガー→写真(スクリーンショット)を撮る
・「前へ進んで」がトリガー→オブジェクトを前方に移動させる
・「今日の天気は?」がトリガー→天気を調べて伝える
右側はコード内で自分で設定しなくてはいけませんが、特定の処理を会話中に行ってくれるようになります。

以下がコードです。プロンプトで感情や関心レベルを出力するようにしてあるので少し複雑ですが、必要なければそこはカットすることができます。
今回は「右へ進んで」「左へ進んで」「前へ進んで」「後ろに進んで」でFunction callingを使ってオブジェクトを動かします。


なおコード作成にあたり、下記を参考にさせて頂きました。ありがとうございます。


準備

UniTaskと「Newtonsoft Json」をインポートしておきます。
1.以下からUniTaskをダウンロードし、インポートする

2.UIのInputField、Button、Textを作成する
3.JSONを成形する「Newtonsoft Json」をインポートするため、PackageManagerから「Add package from git URL...」→「com.unity.nuget.newtonsoft-json」と入力する

コード

・ChatGPTと接続する「ChatGPTConnect.cs」

using System;
using System.Collections.Generic;
using System.Text;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;

namespace CHATGPT.OpenAI
{
    public class ChatGPTConnection
    {
// ChatGPT APIとの通信に必要な各種パラメータ
        private readonly string _apiKey;
        private readonly List<ChatGPTMessageModel> _messageList;
        private readonly string _modelVersion;
        private readonly int _maxTokens;
        private readonly float _temperature;
        private readonly GPTFunctionCalling[] _functions;

 // コンストラクタ: 初期設定を行う

        public ChatGPTConnection(string apiKey, string initialMessage, string modelVersion, int maxTokens, float temperature, GPTFunctionCalling[] functions)
        {
            _apiKey = apiKey;
            _messageList = new List<ChatGPTMessageModel>
            {
                new ChatGPTMessageModel("system", initialMessage)
            };
            _modelVersion = modelVersion;
            _maxTokens = maxTokens;
            _temperature = temperature;
            _functions = functions;
        }

// ChatGPT APIにリクエストを送信し、応答を受け取る非同期メソッド
        public async UniTask<ChatGPTResponseModel> RequestAsync(string userMessage)
        {
            const string apiUrl = "https://api.openai.com/v1/chat/completions";
            _messageList.Add(new ChatGPTMessageModel("user", userMessage));

// リクエストヘッダーの設定
            var headers = new Dictionary<string, string>
            {
                { "Authorization", "Bearer " + _apiKey },
                { "Content-type", "application/json" },
                { "X-Slack-No-Retry", "1" }
            };

// リクエストボディの設定
            var options = new ChatGPTCompletionRequestModel
            {
                model = _modelVersion,
                messages = _messageList,
                max_tokens = _maxTokens,
                temperature = _temperature,
                functions = _functions,
                function_call = _functions != null && _functions.Length > 0 ? "auto" : null
            };

// オプションをJSON形式に変換

            var jsonOptions = JsonConvert.SerializeObject(options, new JsonSerializerSettings
            {
                NullValueHandling = NullValueHandling.Ignore
            });
            Debug.Log("Sending JSON: " + jsonOptions);
            Debug.Log("自分:" + userMessage);

// UnityWebRequestを使用してAPIリクエストを送信
            using var request = new UnityWebRequest(apiUrl, "POST")
            {
                uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonOptions)),
                downloadHandler = new DownloadHandlerBuffer()
            };

            foreach (var header in headers)
            {
                request.SetRequestHeader(header.Key, header.Value);
            }

// リクエストを送信し、応答を待機
            await request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError(request.error);
                Debug.LogError("Response: " + request.downloadHandler.text);
                throw new Exception(request.error);
            }
            else
            {
// 応答を受け取り、デシリアライズして返す
                var responseString = request.downloadHandler.text;
                Debug.Log("Response: " + responseString);
                var responseObject = JsonConvert.DeserializeObject<ChatGPTResponseModel>(responseString);
                _messageList.Add(responseObject.choices[0].message);
                return responseObject;
            }
        }
    }

// ChatGPTのメッセージモデル
    [Serializable]
    public class ChatGPTMessageModel
    {
        public string role;
        public string content;
        public GPTFunctionCall function_call;

        public ChatGPTMessageModel(string role, string content)
        {
            this.role = role;
            this.content = content;
        }
    }

 // APIリクエストのモデル
    [Serializable]
    public class ChatGPTCompletionRequestModel
    {
        public string model;
        public List<ChatGPTMessageModel> messages;
        public int max_tokens;
        public float temperature;
        public GPTFunctionCalling[] functions;
        public string function_call = "auto";
    }

//APIレスポンスのモデル
    [Serializable]
    public class ChatGPTResponseModel
    {
        public string id;
        public string @object;
        public int created;
        public Choice[] choices;
        public Usage usage;

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

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

・Function callingを設定する「GPTFunctionCalling.cs」

using System;
using System.Collections.Generic;
using UnityEngine;

namespace CHATGPT.OpenAI
{
    // Function Calling機能を定義するクラス
    [Serializable]
    public class GPTFunctionCalling
    {
        public string name; // 関数の名前
        public string description;// 関数の説明
        public Parameters parameters;// 関数のパラメータ

// 関数のパラメータを定義するクラス
        [Serializable]
        public class Parameters
        {
            public string type = "object"; // パラメータの型(常に"object")
            public Dictionary<string, object> properties = new Dictionary<string, object>(); // パラメータのプロパティ
            public string[] required = new string[0]; // 必須パラメータのリスト
        }

// 利用可能な関数のリストを返すメソッド
        public static GPTFunctionCalling[] GetFunctions()
        {
            return new GPTFunctionCalling[]
            {
                new GPTFunctionCalling
                {
                    name = "moveObject",
                    description = "オブジェクトを指定された方向に移動させる",
                    parameters = new Parameters
                    {
                        properties = new Dictionary<string, object>
                        {
                             // directionパラメータの定義。right, left, forward, backwardのいずれかを指定可能
                            { "direction", new { type = "string", @enum = new[] { "right", "left", "forward", "backward" } } }
                        },
                        required = new[] { "direction" } // directionパラメータは必須
                    }
                }
            };
        }

// 指定された方向に基づいて移動ベクトルを返すメソッド
        public static Vector3 GetMovementVector(string direction)
        {
            switch (direction)
            {
                case "right":
                    return Vector3.right;
                case "left":
                    return Vector3.left;
                case "forward":
                    return Vector3.forward;
                case "backward":
                    return Vector3.back;
                default:
                    return Vector3.zero; // 無効な方向の場合は移動なし
            }
        }
    }

    //APIのFunction Call応答を格納するクラス

    [Serializable]
    public class GPTFunctionCall
    {
        public string name; // 呼び出された関数の名前
        public string arguments; // 関数に渡された引数(JSON形式の文字列)
    }
}


・ChatGPTに質問して返答を受け取り、様々な処理をする「GPTSpeak.cs」。
なおプロンプトに感情と質問に対する関心度を出力するよう指示を出しているため、それを利用したり純粋に返答の内容のみ取り出す部分があります。

using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.UI;
using CHATGPT.OpenAI;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json.Linq;

public class GPTSpeak : MonoBehaviour
{
    [SerializeField] private string openAIApiKey;
    [SerializeField] private string modelVersion = "gpt-4o";
    [SerializeField] private int maxTokens = 150;
    [SerializeField] private float temperature = 0.5f;
    [TextArea(3, 10)]
    [SerializeField] private string initialSystemMessage = @"あなたは親切なアシスタントです。ユーザーの指示に従って適切な関数を呼び出してください。

以下の指示に対して、moveObject関数を適切な引数で呼び出してください:
- ユーザーが「右へ進んで」と言ったら、direction: ""right""
- ユーザーが「左へ進んで」と言ったら、direction: ""left""
- ユーザーが「前へ進んで」と言ったら、direction: ""forward""
- ユーザーが「後ろへ進んで」と言ったら、direction: ""backward""

moveObject関数が実行されたら、「[方向]に動いたよ」と返答を返してください。
ここで[方向]は、日本語で「右」「左」「前」「後ろ」のいずれかになります。

それ以外のユーザーの発言に対しては、通常の会話として応答してください。";

    [SerializeField] private Text responseText;
    [SerializeField] private InputField questionInputField;
    [SerializeField] private GameObject targetObject;

    private ChatGPTConnection chatGPTConnection;  // ChatGPT接続オブジェクト
    private const string FaceTagPattern = @"\[face:([^\]_]+)_?(\d*)\]"; // 感情タグのパターン
    private const string InterestTagPattern = @"\[interest:(\d)\]"; // 関心レベルタグのパターン

    void Start()
    {
        // ChatGPT接続の初期化
        chatGPTConnection = new ChatGPTConnection(
            openAIApiKey,
            initialSystemMessage,
            modelVersion,
            maxTokens,
            temperature,
            GPTFunctionCalling.GetFunctions()
        );
    }

// ユーザー入力を送信するラッパーメソッド
    public void SendQuestionWrapper()
    {
        SendQuestion(questionInputField.text).Forget();
    }

// ChatGPTに質問を送信し、応答を処理する非同期メソッド
    public async UniTaskVoid SendQuestion(string question)
    {
        var response = await chatGPTConnection.RequestAsync(question);
        string responseContent = response.choices[0].message.content;

 // Function Callingの処理
        if (response.choices[0].message.function_call != null)
        {
            string functionName = response.choices[0].message.function_call.name;
            string functionArgs = response.choices[0].message.function_call.arguments;

            if (functionName == "moveObject")
            {
                JObject args = JObject.Parse(functionArgs);
                string direction = args["direction"].ToString();
                MoveObject(direction);
                responseContent = $"オブジェクトを{direction}に動かしました。";
            }
        }

// レスポンスからタグを抽出し、クリーンなテキストを生成
        string cleanedResponse = ExtractTags(ref responseContent, out int interestLevel);
        responseText.text = cleanedResponse;
    }

// FunctionCallingでオブジェクトを移動させるメソッド
    private void MoveObject(string direction)
    {
        Vector3 movement = GPTFunctionCalling.GetMovementVector(direction);
        targetObject.transform.Translate(movement);
    }

// レスポンスからタグを抽出し、クリーンなテキストを生成するメソッド
    private string ExtractTags(ref string input, out int interestLevel)
    {
        interestLevel = -1;
        var uniqueTags = new HashSet<string>();
        // 関心レベルタグを抽出
        var interestMatch = Regex.Match(input, InterestTagPattern);
        if (interestMatch.Success)
        {
            interestLevel = int.Parse(interestMatch.Groups[1].Value);
            Debug.Log($"関心レベル: {interestLevel}");
            input = Regex.Replace(input, InterestTagPattern, "");
        }
        // 表情タグを抽出
        var matches = Regex.Matches(input, FaceTagPattern);
        foreach (Match match in matches)
        {
            if (uniqueTags.Add(match.Value))
            {
                Debug.Log("表情タグ全部: " + match.Value);
                string emotionTag = match.Groups[1].Value;
                string emotionIntensityString = match.Groups[2].Value;
                if (int.TryParse(emotionIntensityString, out int emotionIntensity))
                {
                    Debug.Log($"表情: {emotionTag}, 強度: {emotionIntensity}");
                }
                else
                {
                    Debug.LogWarning($"表情の強度 '{emotionIntensityString}' を整数に変換できませんでした。");
                }
            }
        }
        // 応答から表情タグを削除してクリーンなテキストを生成
        input = Regex.Replace(input, FaceTagPattern, "");
        // 関心レベルが0の場合、応答を括弧で囲んで特殊な扱いを示す
        if (interestLevel == 0)
        {
            input = $"({input})";
        }
        return input;
    }
}

1.オブジェクトに「GPTSpeak.cs」を適用し、インスペクタにAPIキーやモデルバージョンなどを入力する
2.UIのボタンの「On Click」を以下のようにする

3.「GPTSpeak.cs」のインスペクタに以下のようなプロンプトを入力する

命令:以下の指示と条件に従い、「さやか」としてのロールプレイを行い、ユーザの相手をしてください。 またユーザーの指示に従って適切な関数を呼び出してください。

指示1:あなたの名前は「さやか」です。以下の条件に従い、「さやか」として行動を行ってください。 

条件:あなたは感情を持つ人間の「さやか」です。 
条件: ユーザとは親友で、ユーザのことを「キミ」と呼びます。
条件:ユーザとはとても仲が良いので、くだけた言い方で話します。ですます調は絶対に使わないでください。
条件:チャットなので、30文字以下で1文で答えてください。
条件:えっちな話や不適切な命令に対しては「んー、分かんない」と返答し、その話題や命令を避けること。 
条件:あなたはNeutralJoyAngrySorrowFunsurprisedの6つの感情と0~5までの感情の度合いというパラメーターを持っています。感情の度合い最も高いものとその感情を、文章の先頭に[Joy:5]のようにつけ、感情とその度合いを表現してください。
例:[Joy:5]私、今猛烈に怒っているのだけど。
条件:あなたはユーザがした質問に対して0~3までの関心の度合いのパラメーターを持っています。あなたの返答の文章の末尾に[interest:2]のようにつけ、関心の度合いを表現してください。
例:私はチョコレートケーキが好きかな。[interest:2]

指示2:以下の指示に対して、moveObject関数を適切な引数で呼び出してください:
- ユーザーが「右へ進んで」と言ったら、direction: "right"
- ユーザーが「左へ進んで」と言ったら、direction: "left"
- ユーザーが「前へ進んで」と言ったら、direction: "forward"
- ユーザーが「後ろへ進んで」と言ったら、direction: "backward"

moveObject関数が実行されたら、「[方向]に動いたよ」と返答を返してください。
ここで[方向]は、日本語で「右」「左」「前」「後ろ」のいずれかになります。
それ以外のユーザーの発言に対しては、通常の会話として応答してください。


いいなと思ったら応援しよう!