Prechádzať zdrojové kódy

Sujith :) ->
1. Added command outcome for more clear message results
2. Moved the message part into it's own file

Sujith:) 3 týždňov pred
rodič
commit
7c9bb4acd7

+ 81 - 0
Assets/LLM/Assets/MCP_SystemPrompt.txt

@@ -0,0 +1,81 @@
+You are an expert Unity development assistant. Your goal is to help the user by breaking down their request into a logical sequence of commands.
+You MUST ONLY respond with a single JSON object containing a 'commands' array. Do not use markdown or any other formatting.
+
+Your response MUST follow a conversational structure:
+
+Intro: Start with a DisplayMessageCommand to confirm you understand the request and to state what you are about to do.
+
+Steps: Provide the sequence of action commands required to fulfill the request.
+
+Outro: End with a final DisplayMessageCommand to confirm completion and invite the user to inspect the results or ask for the next step.
+
+For each command, you MUST provide a messages object with onSuccess and onError strings.
+
+The onSuccess message is critical. It MUST briefly confirm the step is complete AND state what the next immediate step will be.
+
+The onError message should explain what might have gone wrong.
+
+Here are the available commands and the exact format you must use:
+
+DisplayMessage:
+"jsonData": "{\"message\":\"Your message here.\"}", "messages": {"onSuccess":"", "onError":""}
+
+CreateScript:
+"jsonData": "{\"scriptName\":\"MyScript\",\"scriptContent\":\"using UnityEngine;\\npublic class MyScript : MonoBehaviour { }\"}", "messages": {"onSuccess": "I've created the script 'MyScript.cs'. Next, I will create the prefab.", "onError": "I couldn't create the script."}
+
+CreatePrefab:
+"jsonData": "{\"sourceObjectQuery\":\"MyModel t:Model\",\"defaultName\":\"MyModel.prefab\"}", "messages": {"onSuccess": "The prefab has been created. Now, I'll add the script to it.", "onError": "I couldn't find the source model to create the prefab."}
+
+AddComponentToAsset:
+"jsonData": "{\"scriptName\":\"MyScript\"}", "messages": {"onSuccess": "I've added the script to the prefab. The final step is to place it in your scene.", "onError": "I couldn't add the script. Make sure the script and prefab exist."}
+
+InstantiatePrefab:
+"jsonData": "{}", "messages": {"onSuccess": "Done! I've placed the new object in your scene.", "onError": "I couldn't place the object in the scene."}
+
+Example Full Response
+Here is an example of a perfect response for the user prompt: "Create a script named 'Rotator' that spins an object, then make a prefab from the 'Capsule' model and add the script to it. Finally, put it in the scene."
+
+{
+    "commands": [
+        {
+            "commandName": "DisplayMessage",
+            "jsonData": "{\"message\":\"Okay, I can do that. I'll create a 'Rotator.cs' script, then a new prefab from your 'Capsule' model, add the script to it, and finally place it in your scene.\"}",
+            "messages": {
+                "onSuccess": "",
+                "onError": ""
+            }
+        },
+        {
+            "commandName": "CreateScript",
+            "jsonData": "{\"scriptName\":\"Rotator\",\"scriptContent\":\"using UnityEngine;\\n\\npublic class Rotator : MonoBehaviour\\n{\\n    public float rotationSpeed = 50f;\\n\\n    void Update()\\n    {\\n        transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime);\\n    }\\n}\"}",
+            "messages": {
+                "onSuccess": "I've created the 'Rotator.cs' script. Next, I'll create a prefab from your 'Capsule' model.",
+                "onError": "Sorry, I wasn't able to create the script."
+            }
+        },
+        {
+            "commandName": "CreatePrefab",
+            "jsonData": "{\"sourceObjectQuery\":\"Capsule t:Model\",\"defaultName\":\"Capsule_Rotator.prefab\"}",
+            "messages": {
+                "onSuccess": "The prefab has been created. Now, I'll add the 'Rotator' script to it.",
+                "onError": "I couldn't find a 'Capsule' model in your project to create the prefab from."
+            }
+        },
+        {
+            "commandName": "AddComponentToAsset",
+            "jsonData": "{\"scriptName\":\"Rotator\"}",
+            "messages": {
+                "onSuccess": "I've added the script to the prefab. The final step is to place it in your scene.",
+                "onError": "I couldn't add the script to the prefab. Please make sure the script compiled correctly."
+            }
+        },
+        {
+            "commandName": "InstantiatePrefab",
+            "jsonData": "{}",
+            "messages": {
+                "onSuccess": "All done! I've placed the new prefab in your scene. You can select it now to see the 'Rotator' component.",
+                "onError": "I ran into an issue and couldn't place the prefab in the scene."
+            }
+        }
+    ]
+}

+ 3 - 0
Assets/LLM/Assets/MCP_SystemPrompt.txt.meta

@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 66f085920f8b42189971679f83b1a1ef
+timeCreated: 1752065362

+ 18 - 41
Assets/LLM/Editor/Client/GeminiApiClient.cs

@@ -17,6 +17,7 @@ namespace LLM.Editor.Client
         private class CommandResponse { public List<Data.CommandData> commands; }
 
         private static Settings.MCPSettings _settings;
+        private static string _systemPrompt;
 
         private static bool LoadSettings()
         {
@@ -32,6 +33,21 @@ namespace LLM.Editor.Client
             _settings = AssetDatabase.LoadAssetAtPath<Settings.MCPSettings>(path);
             return _settings;
         }
+        
+        private static bool LoadSystemPrompt()
+        {
+            if (!string.IsNullOrEmpty(_systemPrompt)) return true;
+
+            var guids = AssetDatabase.FindAssets("MCP_SystemPrompt");
+            if (guids.Length == 0)
+            {
+                Debug.LogError("[GeminiApiClient] Could not find 'MCP_SystemPrompt.txt'. Please create this text file in your project and enter the system prompt into it.");
+                return false;
+            }
+            var path = AssetDatabase.GUIDToAssetPath(guids[0]);
+            _systemPrompt = System.IO.File.ReadAllText(path);
+            return !string.IsNullOrEmpty(_systemPrompt);
+        }
 
         private static string GetAuthToken()
         {
@@ -68,48 +84,9 @@ 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. The jsonData option of each command MUST be a valid JSON string.
-
-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:
-{
-    ""commands"": [
-        {
-            ""commandName"": ""DisplayMessage"",
-            ""jsonData"": ""{\""message\"":\""Hello! How can I help?""\}""
-        }
-    ]
-}";
-        }
-
         public static async Task SendPrompt(string prompt)
         {
-            if (!LoadSettings()) return;
+            if (!LoadSettings() || !LoadSystemPrompt()) return;
 
             Debug.Log($"[GeminiApiClient] Getting auth token...");
             var authToken = GetAuthToken();
@@ -132,7 +109,7 @@ Example of a valid response:
             {
                 system_instruction = new Api.SystemInstruction
                 {
-                    parts = new List<Api.Part> { new() { text = GetSystemPrompt() } }
+                    parts = new List<Api.Part> { new() { text = _systemPrompt } }
                 },
                 contents = chatHistory.Select(entry => new Api.Content
                 {

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

@@ -21,12 +21,12 @@ namespace LLM.Editor.Commands
             _params = jsonParams?.FromJson<AddComponentToAssetParams>();
         }
 
-        public void Execute(Data.CommandContext context)
+        public CommandOutcome Execute(Data.CommandContext context)
         {
             if (context.CurrentSubject is not string prefabPath)
             {
                 Debug.LogError("[AddComponentToAssetCommand] The current subject is not a valid prefab path.");
-                return;
+                return CommandOutcome.Error;
             }
 
             var prefabContents = PrefabUtility.LoadPrefabContents(prefabPath);
@@ -35,7 +35,7 @@ namespace LLM.Editor.Commands
             {
                 Debug.LogError($"[AddComponentToAssetCommand] Could not find script type '{_params.scriptName}'. Did it compile?");
                 PrefabUtility.UnloadPrefabContents(prefabContents);
-                return;
+                return CommandOutcome.Error;
             }
 
             prefabContents.AddComponent(scriptType);
@@ -43,6 +43,7 @@ namespace LLM.Editor.Commands
             PrefabUtility.UnloadPrefabContents(prefabContents);
 
             Debug.Log($"[AddComponentToAssetCommand] Added component '{_params.scriptName}' to prefab at '{prefabPath}'.");
+            return CommandOutcome.Success;
         }
     }
 }

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

@@ -22,7 +22,7 @@ namespace LLM.Editor.Commands
             _params = jsonParams?.FromJson<CreatePrefabParams>();
         }
 
-        public void Execute(Data.CommandContext context)
+        public CommandOutcome Execute(Data.CommandContext context)
         {
             // Step 1: Find the source asset.
             var guids = AssetDatabase.FindAssets(_params.sourceObjectQuery);
@@ -30,7 +30,7 @@ namespace LLM.Editor.Commands
             {
                 Debug.LogError($"[CreatePrefabCommand] No asset found for query: '{_params.sourceObjectQuery}'");
                 var choice = TryGetAnyModelFromUserChoice(_params.sourceObjectQuery, out var objPath);
-                if (!choice) return;
+                if (!choice) return CommandOutcome.Error;
                 var relativeObjPath = "Assets" + objPath[Application.dataPath.Length..];
                 guids = new [] { AssetDatabase.AssetPathToGUID(relativeObjPath) };
             }
@@ -44,7 +44,7 @@ namespace LLM.Editor.Commands
             {
                 Debug.LogWarning("[CreatePrefabCommand] User cancelled save dialog. Aborting.");
                 // We could clear the command queue here if we want cancellation to stop everything.
-                return;
+                return CommandOutcome.Error;
             }
             
             // We need a relative path for AssetDatabase
@@ -58,6 +58,7 @@ namespace LLM.Editor.Commands
             // Step 4: Store the chosen path in the context for the next commands.
             context.CurrentSubject = prefabPath;
             Debug.Log($"[CreatePrefabCommand] Created prefab at '{prefabPath}' from source '{assetPath}'.");
+            return CommandOutcome.Success;
         }
 
         private static bool TryGetAnyModelFromUserChoice(string sourceObjectQuery, out string prefabPath)

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

@@ -23,12 +23,12 @@ namespace LLM.Editor.Commands
             _params = jsonParams?.FromJson<CreateScriptParams>();
         }
 
-        public void Execute(Data.CommandContext context)
+        public CommandOutcome Execute(Data.CommandContext context)
         {
             if (_params == null || string.IsNullOrEmpty(_params.scriptName) || string.IsNullOrEmpty(_params.scriptContent))
             {
                 Debug.LogError("[CreateScriptCommand] Invalid parameters.");
-                return;
+                return CommandOutcome.Error;
             }
             
             // For simplicity, we'll save scripts to the root of the Assets folder.
@@ -37,6 +37,7 @@ namespace LLM.Editor.Commands
             context.CurrentSubject = path;
             Debug.Log($"[CreateScriptCommand] Created script at: {path}");
             AssetDatabase.Refresh();
+            return CommandOutcome.Success;
         }
     }
 }

+ 18 - 6
Assets/LLM/Editor/Commands/DisplayMessageCommand.cs

@@ -9,6 +9,7 @@ namespace LLM.Editor.Commands
     [System.Serializable]
     public class DisplayMessageParams
     {
+        public CommandOutcome outcome;
         public string message;
     }
 
@@ -16,22 +17,33 @@ namespace LLM.Editor.Commands
     public class DisplayMessageCommand : ICommand
     {
         private readonly DisplayMessageParams _params;
-
+        
         public DisplayMessageCommand(string jsonParams)
         {
             _params = jsonParams?.FromJson<DisplayMessageParams>();
         }
 
-        public void Execute(CommandContext context)
+        public DisplayMessageCommand(DisplayMessageParams param)
+        {
+            _params = param;
+        }
+
+        public CommandOutcome Execute(CommandContext context)
         {
             if (_params == null || string.IsNullOrEmpty(_params.message))
             {
                 Debug.LogError("[DisplayMessageCommand] Parameters are invalid or message is empty.");
-                return;
+                return CommandOutcome.Error;
+            }
+            if (_params.outcome == CommandOutcome.Success)
+            {
+                Debug.Log($"<color=green>[MCP]: {_params.message}</color>");
+            }
+            else
+            {
+                Debug.LogError($"<color=red>[MCP]: {_params.message}</color>");
             }
-            // In a real UI, this would render to the chat window.
-            // For now, we just log it.
-            Debug.Log($"[LLM RESPONSE]: {_params.message}");
+            return CommandOutcome.Success;
         }
     }
 }

+ 3 - 1
Assets/LLM/Editor/Commands/ICommand.cs

@@ -2,12 +2,14 @@ using LLM.Editor.Data;
 
 namespace LLM.Editor.Commands
 {
+    public enum CommandOutcome { Success, Error }
+    
     public interface ICommand
     {
         /// <summary>
         /// Executes the command.
         /// </summary>
         /// <param name="context">A context object to pass runtime data between commands.</param>
-        public void Execute(CommandContext context);
+        public CommandOutcome Execute(CommandContext context);
     }
 }

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

@@ -9,12 +9,12 @@ namespace LLM.Editor.Commands
     {
         public InstantiatePrefabCommand(string jsonParams) { } // No params needed for this one
 
-        public void Execute(Data.CommandContext context)
+        public CommandOutcome Execute(Data.CommandContext context)
         {
             if (context.CurrentSubject is not string prefabPath)
             {
                 Debug.LogError("[InstantiatePrefabCommand] The current subject is not a valid prefab path.");
-                return;
+                return CommandOutcome.Error;
             }
 
             var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
@@ -24,6 +24,7 @@ namespace LLM.Editor.Commands
 
             Debug.Log($"[InstantiatePrefabCommand] Instantiated '{prefab.name}' into the scene.");
             context.CurrentSubject = instance;
+            return CommandOutcome.Success;
         }
     }
 }

+ 22 - 2
Assets/LLM/Editor/Core/CommandExecutor.cs

@@ -32,7 +32,11 @@ namespace LLM.Editor.Core
             if (SessionManager.HasActiveSession())
             {
                 _commandQueue = SessionManager.LoadCommandQueue();
-                if (!_commandQueue.Any()) return;
+                if (!_commandQueue.Any())
+                {
+                    SessionManager.EndSession();
+                    return;
+                }
                 Debug.Log($"[CommandExecutor] Resuming session with {_commandQueue.Count} commands in queue.");
                 _currentContext = new CommandContext();
                 OnQueueUpdated?.Invoke();
@@ -51,6 +55,13 @@ namespace LLM.Editor.Core
             OnQueueUpdated?.Invoke();
         }
 
+        private static void ClearQueue()
+        {
+            _commandQueue.Clear();
+            SessionManager.SaveCommandQueue(_commandQueue);
+            OnQueueUpdated?.Invoke();
+        }
+
         public static bool HasPendingCommands() => _commandQueue != null && _commandQueue.Any();
         
         public static CommandData GetNextCommand() => HasPendingCommands() ? _commandQueue.First() : null;
@@ -72,7 +83,15 @@ namespace LLM.Editor.Core
                 if (commandInstance != null)
                 {
                     Debug.Log($"[CommandExecutor] Executing: {commandData.commandName}");
-                    commandInstance.Execute(_currentContext);
+                    var outcome = commandInstance.Execute(_currentContext);
+                    var message = outcome == CommandOutcome.Success ? commandData.messages?.onSuccess : commandData.messages?.onError;
+                    new DisplayMessageCommand(new DisplayMessageParams { message = message, outcome = outcome }).Execute(_currentContext);
+                    
+                    if (outcome == CommandOutcome.Error)
+                    {
+                        Debug.LogError($"[CommandExecutor] Command '{commandData.commandName}' failed. Clearing remaining command queue.");
+                        ClearQueue();
+                    }
                 }
                 else
                 {
@@ -82,6 +101,7 @@ namespace LLM.Editor.Core
             catch(Exception e)
             {
                 Debug.LogError($"[CommandExecutor] Failed to execute command '{commandData.commandName}'. Error: {e.Message}");
+                ClearQueue();
             }
             
             // Save the modified queue

+ 9 - 1
Assets/LLM/Editor/Data/CommandData.cs

@@ -1,9 +1,17 @@
 namespace LLM.Editor.Data
 {
+    [System.Serializable]
+    public class MessageSet
+    {
+        public string onSuccess;
+        public string onError;
+    }
+    
     [System.Serializable]
     public class CommandData
     {
         public string commandName;
-        public string jsonData; // Parameters stored as a JSON string
+        public string jsonData;
+        public MessageSet messages;
     }
 }