非公式クライアントライブラリClaudiaを使って、Claude3とFunction CallingをUnityで動かす

Claude3をUnityで動かすにあたり、ChatGPTのAPIと同じくUnityWebRequestでAPIを呼び出して対話することもできるのですが、今回はLLMの出力から関数を呼び出すFunction Callingも簡単に実装できる非公式ライブラリ「Claudia」を使ってUnityで対話します。またFunction Callingも実装していきます。
非公式と言ってもUniTaskを作った会社さんのライブラリなので、今後のメンテ等も大丈夫だろうということで使っていきたいと思います。
Unityのバージョンは2022.3.12f1以上が必要です。

準備

Claudiaをインポートする

1.NugetForUnityをインポートするために、以下からUnityパッケージをダウンロードし、Unityにインポートする。
Nugetは. NET開発のためのパッケージマネージャーです

2.Unityの「NuGet」→「Manage NuGet Packages」を選択し、検索欄に「Claudia」と入力して「Search」を押す

3.「Install」を押す

4.以下からUniTaskをダウンロードし、インポートする(後ほど使います)

Claude3のAPIを設定する

5.以下のサイトからClaude3のアカウントを作成する。出た当初は個人アカウントでは使用ができなかったが、現在不明

6.左側のメニューの「API keys」をクリックし、「Create Key」をクリックして新しくAPIキーを作成する

7.以下のコードをUnityに入力する。シンプルにインスペクタに入力した質問にClaude3が答えてくれるだけです

using Claudia;
using System;
using UnityEngine;

public class ClaudeConnection : MonoBehaviour
{
    [SerializeField]
    private string apiKey = ""; // インスペクタで入力可能

    [SerializeField]
    private string modelName = "claude-3-opus-20240229"; // インスペクタで入力可能

    [SerializeField]
    private int maxTokens = 1024; // インスペクタで入力可能

    [SerializeField]
    private string userMessage = "Hello, Claude"; // インスペクタで入力可能

    async void Start()
    {
        if (string.IsNullOrEmpty(apiKey))
        {
            Debug.LogError("API Key is not set. Please enter it in the inspector.");
            return;
        }

        var anthropic = new Anthropic()
        {
            ApiKey = apiKey
        };

        Debug.Log("Start Simple Call in Unity");

        try
        {
            var message = await anthropic.Messages.CreateAsync(new()
            {
                Model = modelName,
                MaxTokens = maxTokens,
                Messages = new Message[] { new() { Role = "user", Content = userMessage } }
            });

            Debug.Log($"User: {userMessage}");
            Debug.Log($"Assistant: {message}");
        }
        catch (Exception e)
        {
            Debug.LogError($"Error occurred: {e.Message}");
        }
    }
}

8.インスペクタにAPIキー・モデル名・トークン数・質問を入力する。質問に関しては日本語でもOK。
モデル名に関してはここから確認してください。

9.実行してコンソールに回答が表示されていればOK

Function Callingの準備をする

1.メモ帳などで以下のコマンドを記載し、「csc.rsp」というファイル名+拡張子で保存する

-langVersion:10 -nullable

2.UnityのAssetsフォルダにドラッグ&ドロップする
3.メモ帳などで以下のコマンドを記載し、「LangVersion.props」というファイル名+拡張子で保存する

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <LangVersion>10</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

4.Unityを開いている場合は一度保存して閉じる
5.「LangVersion.props」ファイルをプロジェクトフォルダの直下にドラッグ&ドロップする。「Assets」や「Packages」、「ProjectSettings」などのフォルダと同じ階層に配置されていることを確認する

6.Unityを立ち上げ「Window」メニュー→「Package Manager」を選択肢、「+」ボタン→「Add package from git URL…」を選び、以下のURLを入力する

https://github.com/Cysharp/CsprojModifier.git?path=src/CsprojModifier/Assets/CsprojModifier

7.「Edit」メニュー→「Project Settings」を開く
8.左側のメニューから「Editor」を選択し、「C# Project Modifier」セクションができていることを確認してクリックする

9.「Additional project imports」の「…」をクリックし、先ほど作成した「LangVersion.props」ファイルを選択する

Function Callingを動かす

今回は「写真を撮って」とClaude3に指示すると、スクリーンショットを撮ってUIのImageに表示してくれるものです。
UIのInputField・UIのButton・UIのImageを用意しておきます。

1.UIのInputFieldに「写真を撮って」と入力
2.ボタンを押すとClaude3に指示が飛ぶ
3.スクリーンショットをUIのImageに表示
といった流れになっています。
コードを保存後、インスペクタからAPIキーを入力し、UIのパーツをそれぞれ適用します。

using Claudia;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class ClaudeFCPhoto : MonoBehaviour
{
    [SerializeField] private InputField instructionField;
    [SerializeField] private Button captureButton;
    [SerializeField] private Image displayImage;
    [SerializeField] private string apiKey; // インスペクタからAPIキーを設定

    [TextArea(3, 10)] // 最小3行、最大10行。必要に応じて調整してください。
    [SerializeField] private string systemPrompt = "あなたは、Unity ゲームでユーザーがスクリーンショットを撮るのを支援するAIアシスタントです。"
        + "ユーザーが写真またはスクリーンショットを撮るように要求したら、CaptureScreenshot 関数を呼び出します。CaptureScreenshot関数の実行が成功したら、「写真が撮れたよ」と発言してください。";

    [SerializeField] private string model = "claude-3-haiku-20240307"; // インスペクタからModelを設定
    [SerializeField] private int maxTokens = 1024; // インスペクタからMaxTokensを設定

    private Anthropic anthropic;

    public static ClaudeFCPhoto _instance;

    void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else if (_instance != this)
        {
            Destroy(gameObject);
        }
    }

    void Start()
    {
        anthropic = new Anthropic()
        {
            ApiKey = apiKey
        };

        captureButton.onClick.AddListener(OnCaptureButtonClicked);
    }

    private async void OnCaptureButtonClicked()
    {
        await ProcessInstructionWithClaude3();
    }

    private async Task ProcessInstructionWithClaude3()
    {
        var input = new Message
        {
            Role = Roles.User,
            Content = instructionField.text,
        };

        async Task<MessageResponse> CreateMessage(Message[] messages)
        {
            var result = await anthropic.Messages.CreateAsync(new()
            {
                Model = model,
                MaxTokens = maxTokens,
                System = systemPrompt,
                StopSequences = new[] { StopSequnces.CloseFunctionCalls },
                Messages = messages,
            });
            return result;
        }

        var initialMessage = await CreateMessage(new[] { input });

        // Function Callingが必要かどうかを判断
        if (ShouldUseFunctionCalling(input.Content.ToString()))
        {
            Debug.Log($"Function Calling開始: {input.Content}");
            var partialAssistantMessage = await ClaudeFunctions.InvokeAsync(initialMessage, displayImage);
            Debug.Log($"Function Calling結果: {partialAssistantMessage}");

            if (partialAssistantMessage.Contains("失敗"))
            {
                Debug.LogError("スクリーンショットの撮影に失敗しました。詳細なエラーメッセージをログで確認してください。");
            }

            var finalResult = await CreateMessage(new[] 
            {
                input,
                new Message { Role = Roles.Assistant, Content = partialAssistantMessage },
                new Message { Role = Roles.User, Content = "関数の実行が完了しました。結果に基づいて応答してください。" }
            });

            Debug.Log("Assistant: " + finalResult.Content);
        }
        else
        {
            // Function Callingが不要な場合は、直接AIの応答を表示
            Debug.Log("Assistant: " + initialMessage.Content);
        }

        instructionField.text = "";
    }

    private bool ShouldUseFunctionCalling(string userInput)
    {
        // 「写真」または「スクリーンショット」という単語が含まれている場合にFunction Callingを使用
        return userInput.ToLower().Contains("写真") || userInput.ToLower().Contains("スクリーンショット");
    }

    public static Task EnqueueAsync(Action action)
    {
        var tcs = new TaskCompletionSource<bool>();

        if (_instance == null)
        {
            Debug.LogError("ClaudeFCPhoto is not initialized!");
            tcs.SetResult(false);
            return tcs.Task;
        }

        _instance.StartCoroutine(ExecuteOnMainThread(action, tcs));
        return tcs.Task;
    }

    private static IEnumerator ExecuteOnMainThread(Action action, TaskCompletionSource<bool> tcs)
    {
        yield return null;
        action();
        tcs.SetResult(true);
    }
}

public static partial class ClaudeFunctions
{
    /// <summary>
    /// スクリーンショットを撮影し、指定された名前のImageコンポーネントに表示します。
    /// </summary>
    /// <param name="displayImageName">スクリーンショットを表示するImageコンポーネントを持つGameObjectの名前。</param>
    /// <returns>スクリーンショットの撮影と表示が成功したかどうかを示すブール値。</returns>
    [ClaudiaFunction]
    public static async Task<bool> CaptureScreenshot(string displayImageName)
    {
        Debug.Log("CaptureScreenshot関数が実行されました");

        string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
        string filename = $"screenshot_{timestamp}.png";
        string filePath = Path.Combine(Application.persistentDataPath, filename);

        Debug.Log($"Attempting to save screenshot to: {filePath}");

        Texture2D texture = null;

        await ClaudeFCPhoto.EnqueueAsync(() =>
        {
            ClaudeFCPhoto._instance.StartCoroutine(CaptureScreenshotCoroutine());
        });

        IEnumerator CaptureScreenshotCoroutine()
        {
            yield return new WaitForEndOfFrame();

            texture = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
            texture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
            texture.Apply();

            // テクスチャをPNGに変換
            byte[] bytes = texture.EncodeToPNG();

            // ファイルに保存
            try
            {
                File.WriteAllBytes(filePath, bytes);
                Debug.Log($"Screenshot saved successfully: {filePath}");
            }
            catch (Exception e)
            {
                Debug.LogError($"Failed to save screenshot: {e.Message}");
                yield break;
            }

            // ファイルの存在を確認
            if (File.Exists(filePath))
            {
                Debug.Log($"File exists: {File.Exists(filePath)}");
                Debug.Log($"File size: {new FileInfo(filePath).Length} bytes");
            }
            else
            {
                Debug.LogError("ファイルの作成に失敗しました。");
                yield break;
            }

            // テクスチャをSpriteに変換してImageコンポーネントに設定
            Sprite newSprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
            
            Image displayImage = GameObject.Find(displayImageName)?.GetComponent<Image>();
            if (displayImage != null)
            {
                displayImage.sprite = newSprite;
                Debug.Log("Screenshot displayed on UI");
            }
            else
            {
                Debug.LogError($"Image component not found on GameObject named {displayImageName}");
            }
        }

        // コルーチンが完了するまで待機
        while (texture == null)
        {
            await Task.Yield();
        }

        Debug.Log("CaptureScreenshot関数の実行が完了しました");
        return true;
    }

    public static async Task<string> InvokeAsync(MessageResponse message, Image displayImage)
    {
        string content = message.Content?.ToString().ToLower() ?? string.Empty;
        if (content.Contains("写真") || content.Contains("スクリーンショット"))
        {
            bool result = await ClaudeFunctions.CaptureScreenshot(displayImage.gameObject.name);
            return result ? "スクリーンショットを撮影しました。" : "スクリーンショットの撮影に失敗しました。";
        }
        return "指示を理解できませんでした。";
    }
}


この記事が気に入ったらサポートをしてみませんか?