ソースを参照

Sujith :) ->
1. Changed command context to hold rather useful realtime information
2. Changed files to use Newtonsoft rather than JsonUtility.

Sujith:) 3 週間 前
コミット
e56e50f096

+ 29 - 13
Assets/LLM/Editor/Client/GeminiApiClient.cs

@@ -1,9 +1,9 @@
 using System.Linq;
 using System.Text;
 using UnityEditor;
-using UnityEngine;
 using LLM.Editor.Core;
 using System.Diagnostics;
+using LLM.Editor.Helper;
 using UnityEngine.Networking;
 using System.Threading.Tasks;
 using System.Collections.Generic;
@@ -71,15 +71,30 @@ namespace LLM.Editor.Client
         private static string GetSystemPrompt()
         {
             return @"You are an expert Unity development assistant. Your goal is to help the user by breaking down their request into a sequence of commands.
-You MUST ONLY respond with a single JSON object containing a 'commands' array. Do not use markdown or any other formatting.
+You MUST ONLY respond with a single JSON object containing a 'commands' array. Do not use markdown or any other formatting. The jsonData option of each command MUST be a valid JSON string.
 
-Here are the available commands:
-- DisplayMessage: Show a text message to the user. Use this for conversational replies, questions, or to provide information.
-- CreateScript: Creates a new C# script file.
-- CreatePrefab: Creates a new prefab from a source model found in the project.
-- AddComponentToAsset: Attaches a script to a prefab asset.
-- InstantiatePrefab: Creates an instance of a prefab in the current scene.
-- RequestClarification: Ask the user a follow-up question if their request is ambiguous.
+Some commands act on the 'subject' from the previous command.
+Here are the available commands and the exact `jsonData` format you must use for each:
+
+1.  **DisplayMessage**: Shows a text message to the user.
+    `""""jsonData"""": """"{\""""message\"""":\""""Your message here.\""""}""""`
+
+2.  **CreateScript**: Creates a new C# script. This sets the new script as the subject.
+    `""""jsonData"""": """"{\""""scriptName\"""":\""""MyNewScript\"""",\""""scriptContent\"""":\""""using UnityEngine;\\npublic class MyNewScript : MonoBehaviour { }\""""}""""`
+
+3.  **CreatePrefab**: Asks the user for a save location, then creates a prefab. This sets the new prefab as the subject.
+    `""""jsonData"""": """"{\""""sourceObjectQuery\"""":\""""MyModel t:Model\"""",\""""defaultName\"""":\""""MyModel.prefab\""""}""""`
+
+4.  **AddComponentToAsset**: Attaches a script to the prefab currently set as the subject.
+    `""""jsonData"""": """"{\""""scriptName\"""":\""""MyNewScript\""""}""""`
+
+5.  **InstantiatePrefab**: Creates an instance of the prefab currently set as the subject. This sets the new scene object as the subject.
+    `""""jsonData"""": """"{}""""`
+
+6.  **RequestClarification**: Asks the user a follow-up question.
+    `""""jsonData"""": """"{\""""prompt\"""":\""""Which object do you mean?\"""",\""""options\"""":[\""""ObjectA\"""",\""""ObjectB\""""]}""""`
+
+Your entire response must be a single, valid JSON object.
 
 Example of a valid response:
 {
@@ -127,7 +142,7 @@ Example of a valid response:
                 }).ToList()
             };
             
-            var jsonPayload = JsonUtility.ToJson(apiRequest);
+            var jsonPayload = apiRequest.ToJson();
 
             using var request = new UnityWebRequest(url, "POST");
             var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload);
@@ -145,15 +160,16 @@ Example of a valid response:
             if (request.result == UnityWebRequest.Result.Success)
             {
                 var responseJson = request.downloadHandler.text;
-                var apiResponse = JsonUtility.FromJson<Api.ApiResponse>(responseJson);
+                Debug.Log($"[GeminiApiClient] Response received: \n{responseJson}.");
+                var apiResponse = responseJson?.FromJson<Api.ApiResponse>();
                     
-                if (apiResponse.candidates != null && apiResponse.candidates.Any())
+                if (apiResponse is { candidates: not null } && apiResponse.candidates.Any())
                 {
                     var commandJson = apiResponse.candidates[0].content.parts[0].text;
                     Debug.Log($"[GeminiApiClient] Command received: {commandJson}.");
                     chatHistory.Add(new Data.ChatEntry { role = "model", content = commandJson });
                     SessionManager.SaveChatHistory(chatHistory);
-                    var commandResponse = JsonUtility.FromJson<CommandResponse>(commandJson);
+                    var commandResponse = commandJson?.FromJson<CommandResponse>();
                     if (commandResponse is { commands: not null })
                     {
                         Debug.Log($"[GeminiApiClient] Received {commandResponse.commands.Count} commands from LLM.");

+ 4 - 5
Assets/LLM/Editor/Commands/AddComponentToAssetCommand.cs

@@ -1,5 +1,6 @@
 using UnityEngine;
 using UnityEditor;
+using LLM.Editor.Helper;
 using JetBrains.Annotations;
 
 namespace LLM.Editor.Commands
@@ -17,18 +18,16 @@ namespace LLM.Editor.Commands
 
         public AddComponentToAssetCommand(string jsonParams)
         {
-            _params = JsonUtility.FromJson<AddComponentToAssetParams>(jsonParams);
+            _params = jsonParams?.FromJson<AddComponentToAssetParams>();
         }
 
         public void Execute(Data.CommandContext context)
         {
-            // Use the context key defined in CreatePrefabCommand
-            if (!context.transientData.TryGetValue(CreatePrefabCommand.CONTEXT_KEY_PREFAB_PATH, out var pathObj))
+            if (context.CurrentSubject is not string prefabPath)
             {
-                Debug.LogError("[AddComponentToAssetCommand] Could not find prefab path in context.");
+                Debug.LogError("[AddComponentToAssetCommand] The current subject is not a valid prefab path.");
                 return;
             }
-            var prefabPath = pathObj as string;
 
             var prefabContents = PrefabUtility.LoadPrefabContents(prefabPath);
             var scriptType = System.Type.GetType($"{_params.scriptName}, Assembly-CSharp");

+ 45 - 18
Assets/LLM/Editor/Commands/CreatePrefabCommand.cs

@@ -1,5 +1,6 @@
 using UnityEngine;
 using UnityEditor;
+using LLM.Editor.Helper;
 using JetBrains.Annotations;
 
 namespace LLM.Editor.Commands
@@ -7,25 +8,37 @@ namespace LLM.Editor.Commands
     [System.Serializable]
     public class CreatePrefabParams
     {
-        public string sourceObjectQuery; // e.g., "Racer t:Model"
-        public string defaultName; // e.g., "Racer.prefab"
+        public string sourceObjectQuery;
+        public string defaultName;
     }
 
     [UsedImplicitly]
     public class CreatePrefabCommand : ICommand
     {
-        // This key is used to pass the chosen path to subsequent commands.
-        public const string CONTEXT_KEY_PREFAB_PATH = "lastCreatedPrefabPath";
         private readonly CreatePrefabParams _params;
 
         public CreatePrefabCommand(string jsonParams)
         {
-            _params = JsonUtility.FromJson<CreatePrefabParams>(jsonParams);
+            _params = jsonParams?.FromJson<CreatePrefabParams>();
         }
 
         public void Execute(Data.CommandContext context)
         {
-            // Step 1: Ask the user for a save path.
+            // Step 1: Find the source asset.
+            var guids = AssetDatabase.FindAssets(_params.sourceObjectQuery);
+            if (guids.Length == 0)
+            {
+                Debug.LogError($"[CreatePrefabCommand] No asset found for query: '{_params.sourceObjectQuery}'");
+                var choice = TryGetAnyModelFromUserChoice(_params.sourceObjectQuery, out var objPath);
+                if (!choice) return;
+                var relativeObjPath = "Assets" + objPath[Application.dataPath.Length..];
+                guids = new [] { AssetDatabase.AssetPathToGUID(relativeObjPath) };
+            }
+            
+            var assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
+            var sourceAsset = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
+            
+            // Step 2: Ask the user for a save path.
             var path = EditorUtility.SaveFilePanel("Save New Prefab", "Assets/", _params.defaultName, "prefab");
             if (string.IsNullOrEmpty(path))
             {
@@ -37,25 +50,39 @@ namespace LLM.Editor.Commands
             // We need a relative path for AssetDatabase
             var prefabPath = "Assets" + path[Application.dataPath.Length..];
 
-            // Step 2: Find the source asset.
-            var guids = AssetDatabase.FindAssets(_params.sourceObjectQuery);
-            if (guids.Length == 0)
-            {
-                Debug.LogError($"[CreatePrefabCommand] No asset found for query: '{_params.sourceObjectQuery}'");
-                return;
-            }
-            
-            var assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
-            var sourceAsset = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
-
             // Step 3: Create the prefab.
             var instance = (GameObject)PrefabUtility.InstantiatePrefab(sourceAsset);
             PrefabUtility.SaveAsPrefabAsset(instance, prefabPath);
             Object.DestroyImmediate(instance);
 
             // Step 4: Store the chosen path in the context for the next commands.
-            context.transientData[CONTEXT_KEY_PREFAB_PATH] = prefabPath;
+            context.CurrentSubject = prefabPath;
             Debug.Log($"[CreatePrefabCommand] Created prefab at '{prefabPath}' from source '{assetPath}'.");
         }
+
+        private static bool TryGetAnyModelFromUserChoice(string sourceObjectQuery, out string prefabPath)
+        {
+            const string message = "Do you want to select a model to create a prefab from? Choosing 'no' will fail to execute forthcoming commands as well.";
+            var result = EditorUtility.DisplayDialog($"{sourceObjectQuery} is not found", message, "OK", "No");
+            if (!result)
+            {
+                prefabPath = null;
+                return false;
+            }
+
+            prefabPath = EditorUtility.OpenFilePanel("Choose the model to create prefab from", "", "obj,fbx,stl,gltf,glb,usdz,dae");
+            if (string.IsNullOrEmpty(prefabPath))
+            {
+                prefabPath = null;
+                return false;
+            }
+            
+            Debug.Log($"[CreatePrefabCommand] User selected '{prefabPath}'.");
+            
+            var isInsideAssets = prefabPath.StartsWith(Application.dataPath);
+            if (isInsideAssets) return true;
+            prefabPath = null;
+            return false;
+        }
     }
 }

+ 4 - 3
Assets/LLM/Editor/Commands/CreateScriptCommand.cs

@@ -1,6 +1,7 @@
 using System.IO;
 using UnityEditor;
 using UnityEngine;
+using LLM.Editor.Helper;
 using JetBrains.Annotations;
 
 namespace LLM.Editor.Commands
@@ -19,8 +20,7 @@ namespace LLM.Editor.Commands
 
         public CreateScriptCommand(string jsonParams)
         {
-            Debug.Log($"[CreateScriptCommand] Received parameters: {jsonParams}");
-            _params = JsonUtility.FromJson<CreateScriptParams>(jsonParams);
+            _params = jsonParams?.FromJson<CreateScriptParams>();
         }
 
         public void Execute(Data.CommandContext context)
@@ -34,8 +34,9 @@ namespace LLM.Editor.Commands
             // For simplicity, we'll save scripts to the root of the Assets folder.
             var path = Path.Combine(Application.dataPath, $"{_params.scriptName}.cs");
             File.WriteAllText(path, _params.scriptContent);
+            context.CurrentSubject = path;
             Debug.Log($"[CreateScriptCommand] Created script at: {path}");
-            AssetDatabase.Refresh(); // This will trigger a recompile.
+            AssetDatabase.Refresh();
         }
     }
 }

+ 2 - 1
Assets/LLM/Editor/Commands/DisplayMessageCommand.cs

@@ -1,5 +1,6 @@
 using UnityEngine;
 using LLM.Editor.Data;
+using LLM.Editor.Helper;
 using JetBrains.Annotations;
 
 namespace LLM.Editor.Commands
@@ -18,7 +19,7 @@ namespace LLM.Editor.Commands
 
         public DisplayMessageCommand(string jsonParams)
         {
-            _params = JsonUtility.FromJson<DisplayMessageParams>(jsonParams);
+            _params = jsonParams?.FromJson<DisplayMessageParams>();
         }
 
         public void Execute(CommandContext context)

+ 3 - 4
Assets/LLM/Editor/Commands/InstantiatePrefabCommand.cs

@@ -11,13 +11,11 @@ namespace LLM.Editor.Commands
 
         public void Execute(Data.CommandContext context)
         {
-            // Use the context key defined in CreatePrefabCommand
-            if (!context.transientData.TryGetValue(CreatePrefabCommand.CONTEXT_KEY_PREFAB_PATH, out var pathObj))
+            if (context.CurrentSubject is not string prefabPath)
             {
-                Debug.LogError("[InstantiatePrefabCommand] Could not find prefab path in context.");
+                Debug.LogError("[InstantiatePrefabCommand] The current subject is not a valid prefab path.");
                 return;
             }
-            var prefabPath = pathObj as string;
 
             var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
             var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
@@ -25,6 +23,7 @@ namespace LLM.Editor.Commands
             Selection.activeObject = instance;
 
             Debug.Log($"[InstantiatePrefabCommand] Instantiated '{prefab.name}' into the scene.");
+            context.CurrentSubject = instance;
         }
     }
 }

+ 0 - 34
Assets/LLM/Editor/Commands/RequestSavePathCommand.cs

@@ -1,34 +0,0 @@
-using UnityEngine;
-using UnityEditor;
-
-namespace LLM.Editor.Commands
-{
-    [System.Serializable]
-    public class RequestSavePathParams
-    {
-        public string title;
-        public string defaultName;
-        public string extension;
-    }
-
-    public class RequestSavePathCommand : ICommand
-    {
-        private readonly RequestSavePathParams _params;
-        public const string CONTEXT_KEY = "lastSavePath";
-
-        public RequestSavePathCommand(string jsonParams)
-        {
-            _params = JsonUtility.FromJson<RequestSavePathParams>(jsonParams);
-        }
-
-        public void Execute(Data.CommandContext context)
-        {
-            var path = EditorUtility.SaveFilePanel(_params.title, "Assets/", _params.defaultName, _params.extension);
-            if (string.IsNullOrEmpty(path)) return;
-            // We need a relative path for AssetDatabase
-            var relativePath = "Assets" + path[Application.dataPath.Length..];
-            context.transientData[CONTEXT_KEY] = relativePath;
-            Debug.Log($"[RequestSavePathCommand] User selected path: {relativePath}");
-        }
-    }
-}

+ 0 - 3
Assets/LLM/Editor/Commands/RequestSavePathCommand.cs.meta

@@ -1,3 +0,0 @@
-fileFormatVersion: 2
-guid: a09a49c2083f45c9a12c095e5c1b398c
-timeCreated: 1751976166

+ 1 - 0
Assets/LLM/Editor/Core/CommandExecutor.cs

@@ -52,6 +52,7 @@ namespace LLM.Editor.Core
         }
 
         public static bool HasPendingCommands() => _commandQueue != null && _commandQueue.Any();
+        
         public static CommandData GetNextCommand() => HasPendingCommands() ? _commandQueue.First() : null;
 
         public static void ExecuteNextCommand()

+ 6 - 5
Assets/LLM/Editor/Core/SessionManager.cs

@@ -2,6 +2,7 @@ using System.IO;
 using UnityEngine;
 using UnityEditor;
 using LLM.Editor.Data;
+using LLM.Editor.Helper;
 using System.Collections.Generic;
 
 namespace LLM.Editor.Core
@@ -12,7 +13,7 @@ namespace LLM.Editor.Core
     /// </summary>
     public static class SessionManager
     {
-        private static string _sessionCacheRoot;
+        private static readonly string _sessionCacheRoot;
         private static string _currentSessionId;
 
         private const string SESSION_ID_KEY = "MCP_CurrentSessionID";
@@ -59,7 +60,7 @@ namespace LLM.Editor.Core
             }
 
             var wrapper = new CommandListWrapper { commands = queue };
-            var json = JsonUtility.ToJson(wrapper);
+            var json = wrapper.ToJson();
             var path = Path.Combine(_sessionCacheRoot, _currentSessionId, QUEUE_FILE);
             File.WriteAllText(path, json);
             Debug.Log($"[SessionManager] Saved {queue.Count} commands to queue.");
@@ -73,7 +74,7 @@ namespace LLM.Editor.Core
             if (!File.Exists(path)) return new List<CommandData>();
 
             var json = File.ReadAllText(path);
-            var wrapper = JsonUtility.FromJson<CommandListWrapper>(json);
+            var wrapper = json.FromJson<CommandListWrapper>();
             Debug.Log($"[SessionManager] Loaded {wrapper.commands.Count} commands from queue.");
             return wrapper.commands ?? new List<CommandData>();
         }
@@ -82,7 +83,7 @@ namespace LLM.Editor.Core
         {
             if (!HasActiveSession()) return;
             var wrapper = new ChatLogWrapper { history = history };
-            var json = JsonUtility.ToJson(wrapper);
+            var json = wrapper.ToJson();
             var path = Path.Combine(_sessionCacheRoot, _currentSessionId, CHAT_LOG_FILE);
             File.WriteAllText(path, json);
         }
@@ -93,7 +94,7 @@ namespace LLM.Editor.Core
             var path = Path.Combine(_sessionCacheRoot, _currentSessionId, CHAT_LOG_FILE);
             if (!File.Exists(path)) return new List<ChatEntry>();
             var json = File.ReadAllText(path);
-            var wrapper = JsonUtility.FromJson<ChatLogWrapper>(json);
+            var wrapper = json.FromJson<ChatLogWrapper>();
             return wrapper?.history ?? new List<ChatEntry>();
         }
     }

+ 1 - 3
Assets/LLM/Editor/Data/CommandContext.cs

@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
 namespace LLM.Editor.Data
 {
     /// <summary>
@@ -8,6 +6,6 @@ namespace LLM.Editor.Data
     /// </summary>
     public class CommandContext
     {
-        public Dictionary<string, object> transientData = new();
+        public object CurrentSubject { get; set; }
     }
 }

+ 18 - 1
Assets/LLM/Editor/GUI/MCPWindow.cs

@@ -10,7 +10,7 @@ namespace LLM.Editor.GUI
     {
         private string _promptText = "Create a script named Rotate with a public float for speed. Then, find the 'Racer' model, make a prefab from it, attach the script, and add it to the scene.";
         private Vector2 _scrollPos;
-        private bool _isRequestInProgress = false;
+        private bool _isRequestInProgress;
 
         [MenuItem("Tools/LLM/MCP Assistant")]
         public static void ShowWindow()
@@ -23,6 +23,23 @@ namespace LLM.Editor.GUI
         {
             Debug.Log(Application.persistentDataPath);
         }
+        
+        [MenuItem("Tools/LLM/Print Instance ID (Selected Object)")]
+        public static void PrintInstanceId()
+        {
+            var selection = Selection.activeObject;
+            if (selection != null)
+            {
+                Debug.Log(selection.GetInstanceID());
+            }
+        }
+
+        [MenuItem("Tools/LLM/Log object with instance ID")]
+        public static void PrintObjectWithInstanceId()
+        {
+            var obj = EditorUtility.InstanceIDToObject(20404);
+            Debug.Log(obj, obj);
+        }
 
         private void OnEnable()
         {

+ 3 - 0
Assets/LLM/Editor/Helper.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8c055c630a4b4ebdacca87302bc8fdb5
+timeCreated: 1752062166

+ 28 - 0
Assets/LLM/Editor/Helper/JsonWriter.cs

@@ -0,0 +1,28 @@
+using Newtonsoft.Json;
+
+namespace LLM.Editor.Helper
+{
+    public static class JsonWriter
+    {
+        public static string ToJson<T>(this T obj, JsonSerializerSettings settings = null) where T : class
+        {
+            settings ??= _defaultSettings;
+            return JsonConvert.SerializeObject(obj, settings);
+        }
+        
+        public static T FromJson<T>(this string json, JsonSerializerSettings settings = null) where T : class
+        {
+            settings ??= _defaultSettings;
+            return JsonConvert.DeserializeObject<T>(json, settings);
+        }
+        
+        private static readonly JsonSerializerSettings _defaultSettings = new()
+        {
+            Formatting = Formatting.None,
+            NullValueHandling = NullValueHandling.Ignore,
+            DefaultValueHandling = DefaultValueHandling.Include,
+            TypeNameHandling = TypeNameHandling.Auto,
+            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
+        };
+    }
+}

+ 3 - 0
Assets/LLM/Editor/Helper/JsonWriter.cs.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d6655d28fc0b4994b248d93761915ad1
+timeCreated: 1752062187

+ 0 - 1
Assets/LLM/Editor/Settings/MCPSettings.cs

@@ -2,7 +2,6 @@ using UnityEngine;
 
 namespace LLM.Editor.Settings
 {
-    // This creates a new asset type in Unity's "Create" menu.
     [CreateAssetMenu(fileName = "MCPSettings", menuName = "LLM/MCP Settings", order = 0)]
     public class MCPSettings : ScriptableObject
     {