using System.Linq; using System.Text; using UnityEditor; using LLM.Editor.Core; using System.Diagnostics; using LLM.Editor.Helper; 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. 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; 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 = apiRequest.ToJson(); 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; Debug.Log($"[GeminiApiClient] Response received: \n{responseJson}."); var apiResponse = responseJson?.FromJson(); 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 = commandJson?.FromJson(); 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}"); } } } }