123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- using System;
- using System.Linq;
- using System.Threading.Tasks;
- using UnityEditor;
- using UnityEngine;
- using LLM.Editor.Data;
- using System.Reflection;
- using LLM.Editor.Client;
- using LLM.Editor.Commands;
- using LLM.Editor.Helper;
- using System.Collections.Generic;
- namespace LLM.Editor.Core
- {
- /// <summary>
- /// Responsible for finding and executing commands from the session queue.
- /// </summary>
- [InitializeOnLoad]
- public static class CommandExecutor
- {
- private static List<CommandData> _commandQueue;
- private static CommandContext _currentContext;
- private static string _currentUserPrompt;
- private static int _initialCommandCount;
- public static Action OnQueueUpdated;
- public static event Action<string> OnContextReadyForNextTurn;
- public static event Action<List<string>> OnPlanUpdated;
-
- private static readonly HashSet<string> AutoExecutableCommands = new()
- {
- nameof(GatherContextCommand),
- nameof(UpdateWorkingContextCommand)
- };
- static CommandExecutor()
- {
- // This will run when the editor loads, including after a recompile.
- EditorApplication.delayCall += Initialize;
- }
- private static void Initialize()
- {
- Debug.Log("[CommandExecutor] Initializing...");
- if (SessionManager.HasActiveSession())
- {
- _commandQueue = SessionManager.LoadCommandQueue();
- if (!_commandQueue.Any())
- {
- return;
- }
- Debug.Log($"[CommandExecutor] Resuming session with {_commandQueue.Count} command(s) in queue.");
-
- _currentContext = new CommandContext
- {
- IdentifierMap = SessionManager.LoadIdentifierMap()
- };
- _currentUserPrompt = SessionManager.LoadCurrentUserPrompt();
-
- _initialCommandCount = _commandQueue.Count;
- OnQueueUpdated?.Invoke();
- TriggerAutoExecution();
- }
- else
- {
- _commandQueue = new List<CommandData>();
- }
- }
- public static void SetQueue(List<CommandData> commands, string userPrompt)
- {
- _commandQueue = commands;
- _currentContext = new CommandContext
- {
- IdentifierMap = SessionManager.LoadIdentifierMap()
- };
- _currentUserPrompt = userPrompt;
- _initialCommandCount = commands.Count;
-
- SessionManager.SaveCommandQueue(_commandQueue);
- SessionManager.SaveCurrentUserPrompt(_currentUserPrompt);
-
- OnQueueUpdated?.Invoke();
- Debug.Log($"[CommandExecutor] Queue set with {_commandQueue.Count} commands.");
- foreach (var commandData in _commandQueue)
- {
- Debug.Log($"<color=cyan>[CommandExecutor] {commandData.commandName}</color>");
- }
- TriggerAutoExecution();
- }
- 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;
-
- private static void TriggerAutoExecution()
- {
- EditorApplication.delayCall += async () =>
- {
- if (IsNextCommandAutoExecutable())
- {
- await ExecuteNextCommand();
- }
- };
- }
- public static async Task ExecuteNextCommand()
- {
- if (!HasPendingCommands())
- {
- Debug.LogWarning("[CommandExecutor] No commands to execute.");
- return;
- }
- var commandData = _commandQueue.First();
- var outcome = CommandOutcome.Error;
- float[] promptEmbedding = null;
-
- try
- {
- EditorApplication.LockReloadAssemblies();
-
- // Generate the embedding *before* executing the command, but only for non-dummy clients.
- var apiClient = ApiClientFactory.GetClient();
- if (apiClient is not DummyApiClient)
- {
- if (!string.IsNullOrEmpty(_currentUserPrompt))
- {
- if (apiClient != null)
- {
- promptEmbedding = await EmbeddingHelper.GetEmbedding(_currentUserPrompt, apiClient.GetAuthToken);
- }
- else
- {
- Debug.LogError("[CommandExecutor] Could not get API client to generate embedding.");
- }
- }
- }
- var commandInstance = CreateCommandInstance(commandData);
- if (commandInstance != null)
- {
- Debug.Log($"[CommandExecutor] Executing: {commandData.commandName}");
-
- var planBefore = _currentContext.Plan;
- outcome = commandInstance.Execute(_currentContext);
- var planAfter = _currentContext.Plan;
- if (planAfter != null && !planAfter.SequenceEqual(planBefore ?? Enumerable.Empty<string>()))
- {
- OnPlanUpdated?.Invoke(planAfter);
- }
- switch (outcome)
- {
- case CommandOutcome.Success:
- _commandQueue.RemoveAt(0);
- TriggerAutoExecution();
- break;
-
- case CommandOutcome.Error:
- var errorMessage = $"Command '{commandData.commandName}' failed.";
- Debug.LogError($"[CommandExecutor] {errorMessage}. Message:\n{_currentContext.ErrorMessage ?? "An unspecified error occurred."}");
- var errorContext = new
- {
- lastActionStatus = "Error",
- failedCommand = commandData.commandName,
- errorMessage = _currentContext.ErrorMessage ?? "An unspecified error occurred."
- };
- OnContextReadyForNextTurn?.Invoke(errorContext.ToJson(true));
- ClearQueue();
- break;
-
- case CommandOutcome.AwaitingNextTurn:
- Debug.Log("[CommandExecutor] Pausing queue. Awaiting next turn with LLM.");
- if (_currentContext.CurrentSubject is string detailedContextJson)
- {
- OnContextReadyForNextTurn?.Invoke(detailedContextJson);
- }
- break;
- }
- }
- else
- {
- Debug.LogError($"[CommandExecutor] Could not create instance for command: {commandData.commandName}");
- ClearQueue();
- }
- }
- catch(Exception e)
- {
- Debug.LogError($"[CommandExecutor] Failed to execute command '{commandData.commandName}'");
- Debug.LogException(e);
- ClearQueue();
- }
- finally
- {
- var record = new InteractionRecord
- {
- UserPrompt = _currentUserPrompt,
- LLMResponse = commandData,
- Outcome = outcome,
- Feedback = outcome == CommandOutcome.Error ? _currentContext.ErrorMessage : "Success",
- isMultiStep = _initialCommandCount > 1,
- PromptEmbedding = promptEmbedding
- };
- MemoryLogger.AddRecord(record);
-
- SessionManager.SaveCommandQueue(_commandQueue);
- OnQueueUpdated?.Invoke();
- EditorApplication.UnlockReloadAssemblies();
- }
- }
-
- private static bool IsNextCommandAutoExecutable()
- {
- if (!HasPendingCommands()) return false;
- var nextCommandName = GetNextCommand().commandName;
- var cleanName = nextCommandName.EndsWith("Command") ? nextCommandName : nextCommandName + "Command";
- return AutoExecutableCommands.Contains(cleanName);
- }
- private static ICommand CreateCommandInstance(CommandData data)
- {
- var commandClassName = data.commandName.EndsWith("Command") ? data.commandName : $"{data.commandName}Command";
- var type = Assembly.GetExecutingAssembly().GetTypes()
- .FirstOrDefault(t => t.Namespace == "LLM.Editor.Commands" && t.Name == commandClassName);
- if (type != null)
- {
- var jsonString = data.jsonData?.ToString() ?? "{}";
- return (ICommand)Activator.CreateInstance(type, jsonString);
- }
- Debug.LogError($"[CommandExecutor] Command type '{commandClassName}' not found.");
- return null;
- }
- }
- }
|