using System.Linq; using System.Text; using UnityEditor; using UnityEngine; using LLM.Editor.Core; using System.Diagnostics; using UnityEngine.Networking; using System.Threading.Tasks; using System.Collections.Generic; using Debug = UnityEngine.Debug; namespace LLM.Editor.Client { public static class GeminiApiClient { [System.Serializable] private class CommandResponse { public List commands; } private static Settings.MCPSettings _settings; private static bool LoadSettings() { if (_settings) return true; var guids = AssetDatabase.FindAssets("t:MCPSettings"); if (guids.Length == 0) { Debug.LogError("[GeminiApiClient] Could not find MCPSettings asset. Please create one via Assets > Create > LLM > MCP Settings."); return false; } var path = AssetDatabase.GUIDToAssetPath(guids[0]); _settings = AssetDatabase.LoadAssetAtPath(path); return _settings; } private static string GetAuthToken() { if (!LoadSettings()) return null; try { var startInfo = new ProcessStartInfo { FileName = _settings.gcloudPath, Arguments = "auth print-access-token", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var process = Process.Start(startInfo); if (process == null) return null; var accessToken = process.StandardOutput.ReadToEnd().Trim(); var error = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode == 0) return accessToken; Debug.LogError($"[GeminiApiClient] gcloud auth error: {error}"); return null; } catch (System.Exception e) { Debug.LogError($"[GeminiApiClient] Exception while getting auth token: {e.Message}"); return null; } } 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. 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. 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; Debug.Log($"[GeminiApiClient] Getting auth token..."); var authToken = GetAuthToken(); if (string.IsNullOrEmpty(authToken)) { Debug.LogError("[GeminiApiClient] Failed to get authentication token. Is gcloud CLI installed and are you logged in?"); return; } Debug.Log($"[GeminiApiClient] Sending prompt to live API: {prompt}"); var chatHistory = SessionManager.LoadChatHistory(); chatHistory.Add(new Data.ChatEntry { role = "user", content = prompt }); SessionManager.SaveChatHistory(chatHistory); var url = $"https://{_settings.gcpRegion}-aiplatform.googleapis.com/v1/projects/{_settings.gcpProjectId}/locations/{_settings.gcpRegion}/publishers/google/models/{_settings.modelName}:generateContent"; // Construct the request payload, adding the system prompt first. var apiRequest = new Api.ApiRequest { system_instruction = new Api.SystemInstruction { parts = new List { new() { text = GetSystemPrompt() } } }, contents = chatHistory.Select(entry => new Api.Content { // The API expects "model" for the assistant's role. role = entry.role == "assistant" ? "model" : entry.role, parts = new List { new() { text = entry.content } } }).ToList() }; var jsonPayload = JsonUtility.ToJson(apiRequest); using var request = new UnityWebRequest(url, "POST"); var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload); request.uploadHandler = new UploadHandlerRaw(bodyRaw); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {authToken}"); var operation = request.SendWebRequest(); while (!operation.isDone) { await Task.Yield(); } if (request.result == UnityWebRequest.Result.Success) { var responseJson = request.downloadHandler.text; var apiResponse = JsonUtility.FromJson(responseJson); if (apiResponse.candidates != 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(commandJson); if (commandResponse is { commands: not null }) { Debug.Log($"[GeminiApiClient] Received {commandResponse.commands.Count} commands from LLM."); CommandExecutor.SetQueue(commandResponse.commands); } else { Debug.LogError($"[GeminiApiClient] Failed to parse command structure from LLM response text: {commandJson}"); } } } else { Debug.LogError($"[GeminiApiClient] API Error: {request.error}\n{request.downloadHandler.text}"); } } } }