Skip to content

OpenAIのAPIを触ってみる。

unityで何か作ってみたりすることを目標にしてます。

  • プレイグラウンド色々触ってみる
  • APIキー発行して、Unityから叩いてみる

OpenAI Platform

UnityでOpenAIのAPIを使用したスクリプトを実装する - 夜風のMixedReality

↑参考にできそうなサイト

単なるおしゃべりボットは1からではないですが作ったことがあるので、私のやっている崩壊スターレイルの攻略ボットを作ろうかなと考えました。


  • 備考 崩壊スターレイルの装備品システムについてこのシステムの内容がわかる程度に解説してあります。

    今何のシステムを作ろうとしているのか、崩壊スターレイルの装備品、ステータスの扱いについて少し知識がないと全く理解できないと思うので、補足しておきます。

    崩壊スターレイルは、キャラクターを四人編成して敵と戦うターン制RPGです。ドラクエみたいにとらえてもらったらそれでよいと思います。

    各キャラクターには役割とスキルと固有ステータスが決めてあり、より強く運用するために”遺物”と”オーナメント”として装備品のようなものが用意されています。

    この遺物とオーナメントは、装備することでパッシブスキルがつき、またステータスを上乗せすることもできます。

    ▽右側に、セットすることで得られるパッシブスキルが書いてあります。

    Screenshot_20250425_114847_HonkaiStarRail.jpg

    ▽右側に、この装備品を装備することで得られるステータスの上乗せ値が書いてあります。この値は装備品それぞれで違うので、理想のステータスが振られている装備品を得るためにステージを周回したりします。

    Screenshot_20250425_115932_HonkaiStarRail (1).jpg

    ▽左の数字が固有のステータス値。右が装備品(遺物やオーナメント)で上乗せしている値。

    Screenshot_20250425_114837_HonkaiStarRail.jpg

    スキルによって伸ばすべきステータスや、適切な遺物とオーナメントの種類があるのですが、遺物の種類、オーナメントの種類が多いうえ、スキルの内容も少し難しかったりするので、スタレ攻略ではyoutubeや、hoyoverse(原神、崩壊スターレイル、崩壊3rdなどを運営している会社)が運営するゲーム攻略専門のSNSを使って詳しい人のビルド(遺物、オーナメント、目標ステータス)を参考にしたりします。

    ▽例えば、このキャラはダメージを与える値が攻撃力ではなく、HPに依存しているので、HPを優先して伸ばす必要がある。

    Screenshot_20250425_115758_HonkaiStarRail.jpg

    情報が散乱しているので、このようなシステムがあると便利、その上データをうまく抽出する処理の勉強ができる、web検索のシステムもうまく活用できそうということで、スタレ攻略ボットを作ることにしてます。


作りたいものとしては、OpenAIにキャラクター名だけ渡せば、伸ばすべきステータスの名前と目標の数値のセット三つと、装備すべき遺物とオーナメントの名前が返ってきて、文体を整えて遺物とオーナメントの画像もセットで表示させるような攻略ボットです。

jsonで答え吐かせてこっちできれいにして2D画面のUIに適応する画像選択して出力みたいな感じにできたらいいなと思ったのですが、jsonの形式もよくわからないし、とりあえず文体だけきれいに整えて吐かせるようにしました。

↓答え全然間違ってることあるのですがテストなので良しとします。

スクリーンショット 2025-04-21 162712.png

Chat Completions APIと Responses APIというのがありますが、違いは以下のような感じらしいです。

Chat Completions API

従来のチャット形式のAPIで、ユーザーとアシスタントのメッセージ履歴を基に応答を生成します。

特徴:

  • メッセージ履歴の管理: 開発者がmessages配列で会話履歴を管理し、毎回全履歴を送信する必要があります。
  • 対応モデル: gpt-3.5-turbogpt-4などのチャット特化モデルに対応しています。
  • 機能: 関数呼び出しや構造化出力など、基本的なチャット機能を提供します。

Responses API

概要: より高度なエージェント機能を提供する新しいAPIで、ツールの使用や状態管理を簡素化します。

特徴:

  • 状態管理の簡素化: previous_response_idを使用することで、会話の状態をサーバー側で管理できます。
  • 組み込みツール: Web検索、ファイル検索、コンピュータ操作などのツールが統合されています。
  • イベント駆動型: セマンティックイベントを通じて、応答の進行状況や変更点を明確に把握できます。

responsesAPIだけweb検索できるらしい。

今回は攻略情報を手打ちで入れておらず、いろいろな攻略サイトから持ってきてもらうので、responsesAPIのほうにします。

思ったより用意する素材が多くて時間がかかってしまいましたが、プロンプトの設定とデータセットの下準備だけできました。

スクリーンショット 2025-04-22 115504.pngスクリーンショット 2025-04-22 115446.png
  • [x] ここからjsonに整えて吐くように修正して、
  • [x] unity側でjsonを読み取って、
  • [x] 適切な値を代入したらシーン上のUIImageの表示やTextの表示を変えるようにしようと思ってます。

jsonに整えて吐くように修正しました。

スクリーンショット 2025-04-22 120949.png

エラーが出ましたが既知のエラーっぽかったのでスルーします。

多分日本語パッケージのエラー。リンク先は中国語圏の方で、スレに日本語圏で同じエラーが出るとの書き込みもありました。

スクリーンショット 2025-04-22 135426.png

Unity 6 LTS Editor found an error

unityからのapi呼び出しについてのやり方は複数あるみたいなのですが、まず私がplaygroundで作ったプロンプトはそのまま再利用ができないとのことです。

呼び出し方について、

Assistants API(assistant_idを指定して呼び出す)という方法と

Chat Completions API(Unity のリクエストボディにシステムメッセージとして貼り付ける)という方法があるみたいです。

AssistantsAPIのほうがプロンプトの指定がopenAI上で完結しそうだったのでそちらを選びたかったのですが、現状AssistantsAPIではインターネットには出られないらしいので、今回の用途としてはChatCompletionsAPIしか選択肢がないということになりました。

準備できました。

  • unityでリクエストしてjsonを受け取るスクリプト

    csharp
    using UnityEngine;
    using UnityEngine.Networking;
    using System.Collections;
    using System.Collections.Generic;
    using Unity.VisualScripting;
    
    public class APIRequestManager : MonoBehaviour
    {
        [Header("OpenAI")]
        [SerializeField] private string apiKey;
        [SerializeField] private string model = "gpt-4.1";
    
        public static APIRequestManager Instance { get; private set; }
        
        //外部から見るためのbuild
        public CharacterBuildDto currentBuild { get; private set; }
    
        public event System.Action onBuildSet;
    
        private void Awake()
        {
            Instance = this;
        }
    
        /// <summary>
        /// 外部から呼び出すためのメソッド
        /// </summary>
        /// <param name="characterName"></param>
        public void Load(string characterName) =>
           StartCoroutine(GetBuildCoroutine(characterName));
    
        /// <summary>
        /// 呼び出し本体
        /// </summary>
        /// <param name="characterName"></param>
        /// <returns></returns>
        private IEnumerator GetBuildCoroutine(string characterName)
        {
            const string ENDPOINT = "https://api.openai.com/v1/chat/completions";
            string systemPrompt =
            @"あなたは崩壊スターレイルの攻略チャットボットです。
                ユーザーはキャラクタの名前を言うので、
                そのキャラクタの重視される3つの目標ステータス、
                推奨されるトンネル遺物セットの名称、
                推奨される次元界オーナメントの名称、
                をwebserchでいろいろな攻略サイトから調べて最適なものを以下のルールに従ってフォーマットの通りに羅列して下さい。
    
                以下ルール
                遺物の名前は以下の中から名前のとおりに記してください。
                奇想天外のバナダイス
               ~~全18種類あって長いので割愛~~
                生命のウェンワーク
    
                次元界オーナメントの名前は以下の中から名前のとおりに記してください。
                亡霊の悲哀を詠う詩人
               ~~全24種類あって長いので割愛~~
                知識の海に溺れる学者
    
                []の中の文字を名称や数値に書き換えてください。[]の出力は不要です。[]内以外は書き換えないでください。
    
                以下フォーマット
    
                {
                    ""status"": [
                    {
                        ""status1"": ""[目標ステータス1]"",
                        ""num1"": [目標ステータス数値]
                    },
                    {
                        ""status2"": ""[目標ステータス2]"",
                        ""num2"": [目標ステータス数値]
                    },
                    {
                        ""status3"": ""[目標ステータス3]"",
                        ""num3"": [目標ステータス数値]
                    }
                    ],
                    ""Ibutsu"": ""[トンネル遺物名]"",
                    ""Kouensui"": ""[次元界オーナメント名]""
                }";
    
            string body = $@"{{
                    ""model"": ""{model}"",
                    ""temperature"": 0,
                    ""messages"": [
                    {{""role"":""system"",""content"":""{Escape(systemPrompt)}""}},
                    {{""role"":""user"",""content"":""{Escape(characterName)}""}}
                  ]
                }}";
    
            using UnityWebRequest req = PostJson(ENDPOINT, body, apiKey);
            yield return req.SendWebRequest();
    
            if (req.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"OpenAI リクエスト失敗: {req.error}");
                yield break;
            }
    
            // ④ 返信を取り出し
            var jsonStr = ExtractJson(req.downloadHandler.text);
            if (string.IsNullOrEmpty(jsonStr))
            {
                Debug.LogError("JSON が見つかりませんでした");
                yield break;
            }
    
            // ⑤ JSON → DTO
            CharacterBuildDto build;
            try { build = JsonUtility.FromJson<CharacterBuildDto>(jsonStr); }
            catch
            {
                Debug.LogError("JSON 解析失敗:\n" + jsonStr);
                yield break;
            }
    
            SubscribeCurrentBuild(build);
    
        }
    
        //参照用build登録
        private void SubscribeCurrentBuild(CharacterBuildDto build)
        { 
            currentBuild = build;
            onBuildSet?.Invoke();
        }
    
        private static string ExtractJson(string response)
        {
            int first = response.IndexOf('{');
            int last = response.LastIndexOf('}');
            return (first >= 0 && last > first) ? response.Substring(first, last - first + 1) : null;
        }
    
        private static string Escape(string src) =>
            src.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n");
    
        private static UnityWebRequest PostJson(string url, string body, string apiKey)
        {
            var req = new UnityWebRequest(url, "POST");
            byte[] bytes = System.Text.Encoding.UTF8.GetBytes(body);
            req.uploadHandler = new UploadHandlerRaw(bytes);
            req.downloadHandler = new DownloadHandlerBuffer();
            req.SetRequestHeader("Content-Type", "application/json");
            req.SetRequestHeader("Authorization", $"Bearer {apiKey}");
            return req;
        }
    }

プロンプトについて工夫した点

  • 名前のリストを列挙して指定することで出力する名前を一意にしました。

  • json形式で返してもらうときに書き換えてもらう所を[]内とし、明確にすることで文体の崩れをなくしました。

    • 更に、[]は出力するなと追加で明記しました。
  • 読み取り側スクリプト

    csharp
    using System.Collections.Generic;
    using System.Data;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class ContentsManager : MonoBehaviour
    {   
        public ContentsDataset[] KouensuiSet = new ContentsDataset[18];
        public ContentsDataset[] IbutsuSet = new ContentsDataset[24];
    
        [SerializeField]
        private Image OnamentIMage;
        [SerializeField]  
        private Image IbutsuIMage;
    
        [SerializeField]
        private Text stat1tex;
        [SerializeField]
        private Text stat2tex;
        [SerializeField]
        private Text stat3tex;
    
        private string _Ibutsu;
        private string _Onament;
    
        private string status1;
        private string status2;
        private string status3;
    
        CharacterBuildDto mybuild;
    
        private Dictionary<(string, type), Sprite> IbutsuDic;
        private Dictionary<(string, type), Sprite> OnamentDic;
    
        APIRequestManager APIManager;
    
        private void Awake()
        {
            APIManager = GetComponent<APIRequestManager>();
            APIManager.onBuildSet += SetStatus;
    
            // 辞書を初期化してキャッシュ
            IbutsuDic = new Dictionary<(string, type), Sprite>();
            foreach (var d in KouensuiSet)
            {
                var key = (d.Itemname, d.contentType);
                if (!IbutsuDic.ContainsKey(key))
                    IbutsuDic[key] = d.image;
            }
    
            OnamentDic = new Dictionary<(string, type), Sprite>();
            foreach (var d in KouensuiSet)
            {
                var key = (d.Itemname, d.contentType);
                if (!OnamentDic.ContainsKey(key))
                    OnamentDic[key] = d.image;
            }
    
        }
    
        private void SetStatus()
        {
            mybuild = APIRequestManager.Instance.currentBuild;
    
            status1 = mybuild.status[0].StatusName + " : " + mybuild.status[0].StatusInt;
            status2 = mybuild.status[1].StatusName + " : " + mybuild.status[1].StatusInt;
            status3 = mybuild.status[2].StatusName + " : " + mybuild.status[2].StatusInt;
    
            stat1tex.text = status1;
            stat2tex.text = status2;
            stat3tex.text = status3;
    
            _Ibutsu = mybuild.Ibutsu;
            _Onament = mybuild.Onament;
            SetUI();
        }
    
        private void SetUI()
        {
            Sprite i_image;
            Sprite o_image;
    
            IbutsuDic.TryGetValue((_Ibutsu, type.Ibutsu), out i_image);
            OnamentDic.TryGetValue((_Ibutsu, type.Ibutsu), out o_image);
    
            IbutsuIMage.sprite = i_image;
            OnamentIMage.sprite = o_image;
        }
    }

以前もらっていたAPIキーを使って動かしてみたのですが、エラーが出たので、以下のコードで試してみました。

APIキーは使えることが確認できたので、コードが悪い。

csharp
 static async Task Main()
{
    var apiKey = "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";  // ここに確認したいAPIキーを入れる
    using var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);

    // モデル一覧取得エンドポイントへリクエスト
    var response = await client.GetAsync("https://api.openai.com/v1/models");

    if (response.StatusCode == HttpStatusCode.OK)
    {
        Debug.Log(" APIキーは有効です。");
    }
    else if (response.StatusCode == HttpStatusCode.Unauthorized)
    {
        Debug.LogError(" APIキーが無効か期限切れです。");
    }
    else
    {
        Debug.LogWarning ($" その他のエラー: {(int)response.StatusCode} {response.ReasonPhrase}");
    }
}
スクリーンショット 2025-04-23 093421.png

エラーの内容はこれです。

DANGER

OpenAI リクエスト失敗: HTTP/1.1 400 Bad Request UnityEngine.Debug:LogError (object) APIRequestManager/<GetBuildCoroutine>d__15:MoveNext () (at Assets/Scripts/JsonTreat/APIRequestManager.cs:137) UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)

原因はmodelとしてgpt4.1を使っているからかもらしいので、gpt-4.1-nanoというモデルにしたのですが、今度は以下のようなエラーが出ました。

DANGER

OpenAI リクエスト失敗 (HTTP 400): { "error": { "message": "We could not parse the JSON body of your request. (HINT: This likely means you aren't using your HTTP library correctly. The OpenAI API expects a JSON payload, but what was sent was not valid JSON. If you have trouble figuring out how to fix this, please contact us through our help center at help.openai.com.)", "type": "invalid_request_error", "param": null, "code": null } }

UnityEngine.Debug:LogError (object) APIRequestManager/<GetBuildCoroutine>d__15:MoveNext () (at Assets/Scripts/JsonTreat/APIRequestManager.cs:138) UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr)

一度使えるモデルを確認してみることになりました。

bash
PS> $env:OPENAI_API_KEY = "sk-XXXXXXXXXXXXXXXXXXXX"
PS> curl.exe https://api.openai.com/v1/models -H "Authorization: Bearer $env:OPENAI_API_KEY"

powershellからこれで一覧が取得できます。

スクリーンショット 2025-04-23 105557.png

こんな感じ。

この中にgpt-4.1-nanoがあるので使えるはずということで、モデルの指定は間違ってないです。

jsonの文体が間違っているのかもしれないということで、

Debug.Log($"[OpenAI Request Body] \n{body}");

このコードを入れて実行して帰ってきたjsonを

JSON Online Validator and Formatter - JSON Lint

ここに入れて、

帰ってきたのがこれ

スクリーンショット 2025-04-23 113504.png

だったので、chatgptに何を言いたいのか聞いて、

スクリーンショット 2025-04-23 113543.png

だったので、言われたとおりに書き換えました。

そしたら、今度は読み取る側にエラーが出ました。


帰ってきてる文がこれ↓

{ "id": "chatcmpl-BPM9UzYEShqTH0ndqEudcdhgDAl34", "object": "chat.completion", "created": 1745383052, "model": "gpt-4.1-2025-04-14", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "{\n "status": [\n {\n "status1": "攻撃力",\n "num1": 4000\n },\n {\n "status2": "会心率",\n "num2": 70\n },\n {\n "status3": "会心ダメージ",\n "num3": 140\n }\n ],\n "Ibutsu": "劫火と蓮灯の鋳煉宮",\n "Kouensui": "灰燼を燃やし尽くす大公"\n}", "refusal": null, "annotations": [] }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 878, "completion_tokens": 120, "total_tokens": 998, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 }, "completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0, "accepted_prediction_tokens": 0, "rejected_prediction_tokens": 0 } }, "service_tier": "default", "system_fingerprint": "fp_cf18407276" }

なので、content の中身が文字列としてエスケープされた JSONになっています。

今きれいにエスケープされているのに私のコード内で手動で書き換えているからエラーが出ている?らしいです。

手動でせずに、Unity の JsonUtility を2段階でまるっとパースするべきらしいのでそっちに書き換えていこうと思います。

やっとエラー無しで通るようになりましたが、表記が思ったとおりじゃない。

スクリーンショット 2025-04-23 154956.png

さっき帰ってきていたようなjsonなら、以下のDtoに保存させて、画像みたいに表示させたい。

[System.Serializable] public class StatDto { public string StatusName; public int StatusInt; }

[System.Serializable] public class CharacterBuildDto {

//目標ステータス値が3つ入る。 public StatDto[] status;

//装備するべき遺物の名前が入る。 public string Ibutsu; //装備するべきオーナメントの名前が入る。

public string Onament; }

スクリーンショット 2025-04-23 160412.png

指定しているjsonの雛形と、こっちで指定しているDtoの関数の名前が違うせいでうまく表示できていないみたいです。

ResponsiveAPIに切り替える

responsiveAPIのほうを使ってみます。

Chatgptと相談してた時に、私の聞き方と理解力がよくなくて、

Assistants API

Chat Completions API

しかないと思っていたのですが、responsesAPIも外から呼び出せるみたいなので、そっちに切り替えます。

いままでつかっていたChatCompletionsAPIとResponsesAPIの違いも調べたので書いておきます。

【備忘録】OpenAIのResponses APIとChat Completions APIの比較 - Qiita

↑わかりやすい記事

responsesAPI

  • 外部ツールの統合ができる
    • web検索や、ファイル検索などのツールを組み込める。
  • メッセージの差分を明示的に示せる。
    • セマンティックイベントというのを使っているらしい。長期の会話状態管理もできるらしい。
  • inputフィールドにユーザからの入力を渡す

ChatCompletionsAPI

  • テキスト生成に特化している

    • 外部ツールの統合はできない。
  • message配列にロール付きのメッセージを指定して、ロールでuserかsystemかassistantを区別する。

/v1/responses エンドポイントでは、tools パラメータで組み込みツール(web_search など)を指定するだけで、モデルが自動的にウェブ検索を行い、結果を回答に組み込んでくれます

らしい。


やっと動きました!!!

右側の画像は出てないけど!!!

スクリーンショット 2025-04-25 101823.png

しかも推奨ステータスも推奨オーナメントも私の知識内では完璧にあっているものが適応されています!ちゃんと検索してくれたということ!送っているメッセージが間違っていないということ!!!

時々データの前に無駄に説明など入れてくる時があるので、プロンプトを少し修正しました。

それと、データセットのくみ上げ時に変数名が間違っていたのでそこも修正しました。

画像の名前に常用漢字ではないものが混じっているからか、APIで帰ってきたデータを使った検索

がうまく動かなくて画像が表示されないこともあるのですが、今回はそこは目的外なので置いとこうと思います。

スクリーンショット 2025-04-25 114440.png

↑画像の登録がされていて、変な漢字を使っていないものであればこんな感じで正常に表示できます。

デモ動画

https://youtu.be/tRuEMnt9K-c

完成版コード

APIリクエスト

csharp
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.Collections;
using System.Collections.Generic;
using static UnityEngine.Rendering.DebugUI.Table;

public class APIRequestManager : MonoBehaviour
{
    [Serializable]
    private class ResponseWrapper
    {
        public Output[] output;
    }

    [Serializable]
    private class Output
    {
        public string type;       
        public Content[] content;
    }

    [Serializable]
    private class Content
    {
        public string type; 
        public string text; 
    }

    [Header("OpenAI")]
    [SerializeField] private string apiKey;
    [SerializeField] private string model = "gpt-4.1";

    public static APIRequestManager Instance { get; private set; }

    //外部から結果を取得するためのプロパティ
    public CharacterBuildDto currentBuild { get; private set; }
    public event Action onBuildSet;

    private void Awake()
    {
        Instance = this;
    }

    /// <summary>
    ///外部から呼び出すメソッド:キャラ名を指定して装備ビルドを取得
    /// </summary>
    public void Load(string characterName) =>
        StartCoroutine(GetBuildCoroutine(characterName));

    /// <summary>
    ///Responses API を呼び出して DTO を受け取るコルーチン
    /// </summary>
    private IEnumerator GetBuildCoroutine(string characterName)
    {
        const string ENDPOINT = "https://api.openai.com/v1/responses";

        // システムプロンプトは改行も含めて完全に
        string systemPrompt = @"あなたは崩壊スターレイルの攻略チャットボットです。
        ユーザーはキャラクタの名前を言うので、
        そのキャラクタの重視される3つの目標ステータス、
        推奨されるトンネル遺物セットの名称、
        推奨される次元界オーナメントの名称、
        をwebsearchでいろいろな攻略サイトから調べて最適なものを以下のルールに従ってフォーマットの通りに羅列して下さい。

        以下ルール
        遺物の名前は以下の中から名前のとおりに記してください。略称は使わないでください。

        奇想天外のバナダイス
        蒼穹戦線グラモス
        顕世の出雲と高天の神国
        星々の競技場
        深慮に浸る巨樹
        夢の地ピノコニー
        劫火と蓮灯の鋳煉宮
        弄狼の都藍王朝
        海に沈んだルサカ
        折れた竜骨
        自転が止まったサルソット
        老いぬ者の仙舟
        汎銀河商事会社
        盗賊公国タリア
        天体階差機関
        荒涼の惑星ツガンニヤ
        創造者のベロブルグ
        生命のウェンワーク

        次元界オーナメントの名前は以下の中から名前のとおりに記してください。略称は使わないでください。

        亡霊の悲哀を詠う詩人
        成り上がりチャンピオン
        死水に潜る先駆者
        凱歌を掲げる英雄
        溶岩で鍛造する火匠
        再び苦難の道を歩む司祭
        宝命長存の蒔者
        昼夜の狭間を駆ける鷹
        流星の跡を追う怪盗
        草の穂ガンマン
        灰燼を燃やし尽くす大公
        荒野で盗みを働く廃土客
        風雲を薙ぎ払う勇烈
        深い牢獄の囚人
        仮想空間を漫遊するメッセンジャー
        夢を弄ぶ時計屋
        星の如く輝く天才
        雪の密林の狩人
        雷鳴轟くバンド
        流雲無痕の過客
        吹雪と対峙する兵士
        純庭協会の聖騎士
        蝗害を一掃せし鉄騎
        知識の海に溺れる学者

        []の中の文字を名称や数値に書き換えてください。[]の出力は不要です。[]内以外は書き換えないでください。
        また、フォーマット外のことはしゃべらないでください。挨拶や、補足も不要です。

        以下フォーマット

        {
            ""status"": [
                { ""StatusName"": ""[目標ステータス1]"", ""StatusInt"": [数値] },
                { ""StatusName"": ""[目標ステータス2]"", ""StatusInt"": [数値] },
                { ""StatusName"": ""[目標ステータス3]"", ""StatusInt"": [数値] }
            ],
            ""Ibutsu"": ""[トンネル遺物名]"",
            ""Onament"": ""[次元界オーナメント名]""
        }";

        //リクエストボディ組み立て
        string body = $@"{{
            ""model"": ""{model}"",
            ""temperature"": 0,
            ""instructions"": ""{Escape(systemPrompt)}"",
            ""input"": ""{Escape(characterName)}"",
            ""tools"": [ {{ ""type"": ""web_search"" }} ]
        }}";

        using var req = PostJson(ENDPOINT, body, apiKey);
        yield return req.SendWebRequest();

        if (req.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"OpenAI リクエスト失敗 (HTTP {(int)req.responseCode}):\n{req.downloadHandler.text}");
            yield break;
        }

        //生テキストを取得してラップクラスへパース
        var raw = req.downloadHandler.text;
        var wrapper = JsonUtility.FromJson<ResponseWrapper>(raw);

        if (wrapper?.output == null || wrapper.output.Length == 0)
        {
            Debug.LogError("Responses API のパースに失敗しました:\n" + raw);
            yield break;
        }

        //message タイプの最初のコンテンツから JSON 部分を取り出す
        string contentJson = null;
        foreach (var outItem in wrapper.output)
        {
            if (outItem.type == "message" && outItem.content != null && outItem.content.Length > 0)
            {
                contentJson = outItem.content[0].text;
                break;
            }
        }

        if (string.IsNullOrEmpty(contentJson))
        {
            Debug.LogError("レスポンスに message コンテンツが含まれていません:\n" + raw);
            yield break;
        }

        //DTO にパース
        CharacterBuildDto build;
        try
        {
            build = JsonUtility.FromJson<CharacterBuildDto>(contentJson);
        }
        catch (Exception ex)
        {
            Debug.LogError("CharacterBuildDto のパース失敗:\n" + ex + "\n" + contentJson);
            yield break;
        }

        if (build?.status == null || build.status.Length < 3)
        {
            Debug.LogError("build.status が不正です:\n" + contentJson);
            yield break;
        }

        Debug.Log(contentJson);

        SubscribeCurrentBuild(build);
    }

    //成功時に currentBuild に登録してイベント発火
    private void SubscribeCurrentBuild(CharacterBuildDto build)
    {
        Debug.Log("ビルド取得成功");
        currentBuild = build;
        onBuildSet?.Invoke();
    }

    //改行やダブルクオートをエスケープするユーティリティ
    private static string Escape(string src)
    {
        if (string.IsNullOrEmpty(src))
            return string.Empty;
        return src
            .Replace("\\", "\\\\")
            .Replace("\r", "\\r")
            .Replace("\n", "\\n")
            .Replace("\"", "\\\"");
    }

    //JSON POST 用ヘルパー
    private static UnityWebRequest PostJson(string url, string body, string apiKey)
    {
        var req = new UnityWebRequest(url, "POST");
        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(body);
        req.uploadHandler = new UploadHandlerRaw(bytes);
        req.downloadHandler = new DownloadHandlerBuffer();
        req.SetRequestHeader("Content-Type", "application/json");
        req.SetRequestHeader("Authorization", $"Bearer {apiKey}");
        return req;
    }
}

Dtoにしたものを読み取ってUIを変更したりするコード

csharp
using System.Collections.Generic;
using System.Data;
using UnityEngine;
using UnityEngine.UI;

public class ContentsManager : MonoBehaviour
{   
    public ContentsDataset[] KouensuiSet = new ContentsDataset[18];
    public ContentsDataset[] IbutsuSet = new ContentsDataset[24];

    [SerializeField]
    private Image OnamentIMage;
    [SerializeField]  
    private Image IbutsuIMage;

    [SerializeField]
    private Text stat1tex;
    [SerializeField]
    private Text stat2tex;
    [SerializeField]
    private Text stat3tex;

    private string _Ibutsu;
    private string _Onament;

    private string status1;
    private string status2;
    private string status3;

    CharacterBuildDto mybuild;

    private Dictionary<(string, type), Sprite> IbutsuDic;
    private Dictionary<(string, type), Sprite> OnamentDic;

    APIRequestManager APIManager;

    private void Awake()
    {
        APIManager = GetComponent<APIRequestManager>();
        APIManager.onBuildSet += SetStatus;

        // 辞書を初期化してキャッシュ
        IbutsuDic = new Dictionary<(string, type), Sprite>();
        foreach (var d in IbutsuSet)
        {
            var key = (d.Itemname, d.contentType);
            if (!IbutsuDic.ContainsKey(key))
                IbutsuDic[key] = d.image;
        }

        OnamentDic = new Dictionary<(string, type), Sprite>();
        foreach (var d in KouensuiSet)
        {
            var key = (d.Itemname, d.contentType);
            if (!OnamentDic.ContainsKey(key))
                OnamentDic[key] = d.image;
        }

    }

    private void SetStatus()
    {
        mybuild = APIRequestManager.Instance.currentBuild;

        Debug.Log($"stat1tex={stat1tex}, stat2tex={stat2tex}, stat3tex={stat3tex}");
        Debug.Log($"mybuild={mybuild}, status={mybuild?.status}");

        status1 = mybuild.status[0].StatusName + " : " + mybuild.status[0].StatusInt;
        status2 = mybuild.status[1].StatusName + " : " + mybuild.status[1].StatusInt;
        status3 = mybuild.status[2].StatusName + " : " + mybuild.status[2].StatusInt;

        stat1tex.text = status1;
        stat2tex.text = status2;
        stat3tex.text = status3;

        _Ibutsu = mybuild.Ibutsu;
        _Onament = mybuild.Onament;
        SetUI();
    }

    private void SetUI()
    {
        Sprite i_image;
        Sprite o_image;

        IbutsuDic.TryGetValue((_Ibutsu, type.Ibutsu), out i_image);
        OnamentDic.TryGetValue((_Onament, type.Onament), out o_image);

        IbutsuIMage.sprite = i_image;
        OnamentIMage.sprite = o_image;
    }
}

Dto

csharp
[System.Serializable]
public class StatDto
{
    public string StatusName;
    public int StatusInt;
}

[System.Serializable]
public class CharacterBuildDto
{
    public StatDto[] status;
    public string Ibutsu;
    public string Onament;
}

コンテンツのデータセット

csharp
using UnityEngine;
using System;
using UnityEngine.UI;
public enum type
{
    Kouensui,
    Ibutsu,
    Onament,
    None,
}

[System.Serializable]
public class ContentsDataset
{
    public string Itemname;
    public Sprite image;
    public type contentType;
}

検索ボタンのイベント登録用

csharp
using UnityEngine;
using UnityEngine.UI;

public class ButtonManager : MonoBehaviour
{
    [SerializeField]
    InputField charainputField;

    string charaname;

    public void OnSerch()
    {
        charaname = charainputField.text;
        if (charainputField == null)
        { Debug.Log("ない"); }
        
        APIRequestManager.Instance.Load(charaname);
    }
}

Author: 松崎 | Source: 松崎\OpenAIのAPIを触ってみる。 1dcaba435ee780e5ad2ef5e6a5398237.md