CommandExecutor.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. using System;
  2. using System.Linq;
  3. using System.Threading.Tasks;
  4. using UnityEditor;
  5. using UnityEngine;
  6. using LLM.Editor.Data;
  7. using System.Reflection;
  8. using LLM.Editor.Client;
  9. using LLM.Editor.Commands;
  10. using LLM.Editor.Helper;
  11. using System.Collections.Generic;
  12. namespace LLM.Editor.Core
  13. {
  14. /// <summary>
  15. /// Responsible for finding and executing commands from the session queue.
  16. /// </summary>
  17. [InitializeOnLoad]
  18. public static class CommandExecutor
  19. {
  20. private static List<CommandData> _commandQueue;
  21. private static CommandContext _currentContext;
  22. private static string _currentUserPrompt;
  23. private static int _initialCommandCount;
  24. public static Action OnQueueUpdated;
  25. public static event Action<string> OnContextReadyForNextTurn;
  26. public static event Action<List<string>> OnPlanUpdated;
  27. private static readonly HashSet<string> AutoExecutableCommands = new()
  28. {
  29. nameof(GatherContextCommand),
  30. nameof(UpdateWorkingContextCommand)
  31. };
  32. static CommandExecutor()
  33. {
  34. // This will run when the editor loads, including after a recompile.
  35. EditorApplication.delayCall += Initialize;
  36. }
  37. private static void Initialize()
  38. {
  39. Debug.Log("[CommandExecutor] Initializing...");
  40. if (SessionManager.HasActiveSession())
  41. {
  42. _commandQueue = SessionManager.LoadCommandQueue();
  43. if (!_commandQueue.Any())
  44. {
  45. return;
  46. }
  47. Debug.Log($"[CommandExecutor] Resuming session with {_commandQueue.Count} command(s) in queue.");
  48. _currentContext = new CommandContext
  49. {
  50. IdentifierMap = SessionManager.LoadIdentifierMap()
  51. };
  52. _currentUserPrompt = SessionManager.LoadCurrentUserPrompt();
  53. _initialCommandCount = _commandQueue.Count;
  54. OnQueueUpdated?.Invoke();
  55. TriggerAutoExecution();
  56. }
  57. else
  58. {
  59. _commandQueue = new List<CommandData>();
  60. }
  61. }
  62. public static void SetQueue(List<CommandData> commands, string userPrompt)
  63. {
  64. _commandQueue = commands;
  65. _currentContext = new CommandContext
  66. {
  67. IdentifierMap = SessionManager.LoadIdentifierMap()
  68. };
  69. _currentUserPrompt = userPrompt;
  70. _initialCommandCount = commands.Count;
  71. SessionManager.SaveCommandQueue(_commandQueue);
  72. SessionManager.SaveCurrentUserPrompt(_currentUserPrompt);
  73. OnQueueUpdated?.Invoke();
  74. Debug.Log($"[CommandExecutor] Queue set with {_commandQueue.Count} commands.");
  75. foreach (var commandData in _commandQueue)
  76. {
  77. Debug.Log($"<color=cyan>[CommandExecutor] {commandData.commandName}</color>");
  78. }
  79. TriggerAutoExecution();
  80. }
  81. private static void ClearQueue()
  82. {
  83. _commandQueue.Clear();
  84. SessionManager.SaveCommandQueue(_commandQueue);
  85. OnQueueUpdated?.Invoke();
  86. }
  87. public static bool HasPendingCommands() => _commandQueue != null && _commandQueue.Any();
  88. public static CommandData GetNextCommand() => HasPendingCommands() ? _commandQueue.First() : null;
  89. private static void TriggerAutoExecution()
  90. {
  91. EditorApplication.delayCall += async () =>
  92. {
  93. if (IsNextCommandAutoExecutable())
  94. {
  95. await ExecuteNextCommand();
  96. }
  97. };
  98. }
  99. public static async Task ExecuteNextCommand()
  100. {
  101. if (!HasPendingCommands())
  102. {
  103. Debug.LogWarning("[CommandExecutor] No commands to execute.");
  104. return;
  105. }
  106. var commandData = _commandQueue.First();
  107. var outcome = CommandOutcome.Error;
  108. float[] promptEmbedding = null;
  109. try
  110. {
  111. EditorApplication.LockReloadAssemblies();
  112. // Generate the embedding *before* executing the command, but only for non-dummy clients.
  113. var apiClient = ApiClientFactory.GetClient();
  114. if (apiClient is not DummyApiClient)
  115. {
  116. if (!string.IsNullOrEmpty(_currentUserPrompt))
  117. {
  118. if (apiClient != null)
  119. {
  120. promptEmbedding = await EmbeddingHelper.GetEmbedding(_currentUserPrompt, apiClient.GetAuthToken);
  121. }
  122. else
  123. {
  124. Debug.LogError("[CommandExecutor] Could not get API client to generate embedding.");
  125. }
  126. }
  127. }
  128. var commandInstance = CreateCommandInstance(commandData);
  129. if (commandInstance != null)
  130. {
  131. Debug.Log($"[CommandExecutor] Executing: {commandData.commandName}");
  132. var planBefore = _currentContext.Plan;
  133. outcome = commandInstance.Execute(_currentContext);
  134. var planAfter = _currentContext.Plan;
  135. if (planAfter != null && !planAfter.SequenceEqual(planBefore ?? Enumerable.Empty<string>()))
  136. {
  137. OnPlanUpdated?.Invoke(planAfter);
  138. }
  139. switch (outcome)
  140. {
  141. case CommandOutcome.Success:
  142. _commandQueue.RemoveAt(0);
  143. TriggerAutoExecution();
  144. break;
  145. case CommandOutcome.Error:
  146. var errorMessage = $"Command '{commandData.commandName}' failed.";
  147. Debug.LogError($"[CommandExecutor] {errorMessage}. Message:\n{_currentContext.ErrorMessage ?? "An unspecified error occurred."}");
  148. var errorContext = new
  149. {
  150. lastActionStatus = "Error",
  151. failedCommand = commandData.commandName,
  152. errorMessage = _currentContext.ErrorMessage ?? "An unspecified error occurred."
  153. };
  154. OnContextReadyForNextTurn?.Invoke(errorContext.ToJson(true));
  155. ClearQueue();
  156. break;
  157. case CommandOutcome.AwaitingNextTurn:
  158. Debug.Log("[CommandExecutor] Pausing queue. Awaiting next turn with LLM.");
  159. if (_currentContext.CurrentSubject is string detailedContextJson)
  160. {
  161. OnContextReadyForNextTurn?.Invoke(detailedContextJson);
  162. }
  163. break;
  164. }
  165. }
  166. else
  167. {
  168. Debug.LogError($"[CommandExecutor] Could not create instance for command: {commandData.commandName}");
  169. ClearQueue();
  170. }
  171. }
  172. catch(Exception e)
  173. {
  174. Debug.LogError($"[CommandExecutor] Failed to execute command '{commandData.commandName}'");
  175. Debug.LogException(e);
  176. ClearQueue();
  177. }
  178. finally
  179. {
  180. var record = new InteractionRecord
  181. {
  182. UserPrompt = _currentUserPrompt,
  183. LLMResponse = commandData,
  184. Outcome = outcome,
  185. Feedback = outcome == CommandOutcome.Error ? _currentContext.ErrorMessage : "Success",
  186. isMultiStep = _initialCommandCount > 1,
  187. PromptEmbedding = promptEmbedding
  188. };
  189. MemoryLogger.AddRecord(record);
  190. SessionManager.SaveCommandQueue(_commandQueue);
  191. OnQueueUpdated?.Invoke();
  192. EditorApplication.UnlockReloadAssemblies();
  193. }
  194. }
  195. private static bool IsNextCommandAutoExecutable()
  196. {
  197. if (!HasPendingCommands()) return false;
  198. var nextCommandName = GetNextCommand().commandName;
  199. var cleanName = nextCommandName.EndsWith("Command") ? nextCommandName : nextCommandName + "Command";
  200. return AutoExecutableCommands.Contains(cleanName);
  201. }
  202. private static ICommand CreateCommandInstance(CommandData data)
  203. {
  204. var commandClassName = data.commandName.EndsWith("Command") ? data.commandName : $"{data.commandName}Command";
  205. var type = Assembly.GetExecutingAssembly().GetTypes()
  206. .FirstOrDefault(t => t.Namespace == "LLM.Editor.Commands" && t.Name == commandClassName);
  207. if (type != null)
  208. {
  209. var jsonString = data.jsonData?.ToString() ?? "{}";
  210. return (ICommand)Activator.CreateInstance(type, jsonString);
  211. }
  212. Debug.LogError($"[CommandExecutor] Command type '{commandClassName}' not found.");
  213. return null;
  214. }
  215. }
  216. }