GeminiApiClient.cs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. using System.Linq;
  2. using System.Text;
  3. using UnityEditor;
  4. using LLM.Editor.Core;
  5. using System.Diagnostics;
  6. using LLM.Editor.Helper;
  7. using UnityEngine.Networking;
  8. using System.Threading.Tasks;
  9. using System.Collections.Generic;
  10. using Debug = UnityEngine.Debug;
  11. namespace LLM.Editor.Client
  12. {
  13. public static class GeminiApiClient
  14. {
  15. [System.Serializable]
  16. private class CommandResponse { public List<Data.CommandData> commands; }
  17. private static Settings.MCPSettings _settings;
  18. private static bool LoadSettings()
  19. {
  20. if (_settings) return true;
  21. var guids = AssetDatabase.FindAssets("t:MCPSettings");
  22. if (guids.Length == 0)
  23. {
  24. Debug.LogError("[GeminiApiClient] Could not find MCPSettings asset. Please create one via Assets > Create > LLM > MCP Settings.");
  25. return false;
  26. }
  27. var path = AssetDatabase.GUIDToAssetPath(guids[0]);
  28. _settings = AssetDatabase.LoadAssetAtPath<Settings.MCPSettings>(path);
  29. return _settings;
  30. }
  31. private static string GetAuthToken()
  32. {
  33. if (!LoadSettings()) return null;
  34. try
  35. {
  36. var startInfo = new ProcessStartInfo
  37. {
  38. FileName = _settings.gcloudPath,
  39. Arguments = "auth print-access-token",
  40. RedirectStandardOutput = true,
  41. RedirectStandardError = true,
  42. UseShellExecute = false,
  43. CreateNoWindow = true
  44. };
  45. using var process = Process.Start(startInfo);
  46. if (process == null) return null;
  47. var accessToken = process.StandardOutput.ReadToEnd().Trim();
  48. var error = process.StandardError.ReadToEnd();
  49. process.WaitForExit();
  50. if (process.ExitCode == 0) return accessToken;
  51. Debug.LogError($"[GeminiApiClient] gcloud auth error: {error}");
  52. return null;
  53. }
  54. catch (System.Exception e)
  55. {
  56. Debug.LogError($"[GeminiApiClient] Exception while getting auth token: {e.Message}");
  57. return null;
  58. }
  59. }
  60. private static string GetSystemPrompt()
  61. {
  62. 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.
  63. 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.
  64. Some commands act on the 'subject' from the previous command.
  65. Here are the available commands and the exact `jsonData` format you must use for each:
  66. 1. **DisplayMessage**: Shows a text message to the user.
  67. `""""jsonData"""": """"{\""""message\"""":\""""Your message here.\""""}""""`
  68. 2. **CreateScript**: Creates a new C# script. This sets the new script as the subject.
  69. `""""jsonData"""": """"{\""""scriptName\"""":\""""MyNewScript\"""",\""""scriptContent\"""":\""""using UnityEngine;\\npublic class MyNewScript : MonoBehaviour { }\""""}""""`
  70. 3. **CreatePrefab**: Asks the user for a save location, then creates a prefab. This sets the new prefab as the subject.
  71. `""""jsonData"""": """"{\""""sourceObjectQuery\"""":\""""MyModel t:Model\"""",\""""defaultName\"""":\""""MyModel.prefab\""""}""""`
  72. 4. **AddComponentToAsset**: Attaches a script to the prefab currently set as the subject.
  73. `""""jsonData"""": """"{\""""scriptName\"""":\""""MyNewScript\""""}""""`
  74. 5. **InstantiatePrefab**: Creates an instance of the prefab currently set as the subject. This sets the new scene object as the subject.
  75. `""""jsonData"""": """"{}""""`
  76. 6. **RequestClarification**: Asks the user a follow-up question.
  77. `""""jsonData"""": """"{\""""prompt\"""":\""""Which object do you mean?\"""",\""""options\"""":[\""""ObjectA\"""",\""""ObjectB\""""]}""""`
  78. Your entire response must be a single, valid JSON object.
  79. Example of a valid response:
  80. {
  81. ""commands"": [
  82. {
  83. ""commandName"": ""DisplayMessage"",
  84. ""jsonData"": ""{\""message\"":\""Hello! How can I help?""\}""
  85. }
  86. ]
  87. }";
  88. }
  89. public static async Task SendPrompt(string prompt)
  90. {
  91. if (!LoadSettings()) return;
  92. Debug.Log($"[GeminiApiClient] Getting auth token...");
  93. var authToken = GetAuthToken();
  94. if (string.IsNullOrEmpty(authToken))
  95. {
  96. Debug.LogError("[GeminiApiClient] Failed to get authentication token. Is gcloud CLI installed and are you logged in?");
  97. return;
  98. }
  99. Debug.Log($"[GeminiApiClient] Sending prompt to live API: {prompt}");
  100. var chatHistory = SessionManager.LoadChatHistory();
  101. chatHistory.Add(new Data.ChatEntry { role = "user", content = prompt });
  102. SessionManager.SaveChatHistory(chatHistory);
  103. var url = $"https://{_settings.gcpRegion}-aiplatform.googleapis.com/v1/projects/{_settings.gcpProjectId}/locations/{_settings.gcpRegion}/publishers/google/models/{_settings.modelName}:generateContent";
  104. // Construct the request payload, adding the system prompt first.
  105. var apiRequest = new Api.ApiRequest
  106. {
  107. system_instruction = new Api.SystemInstruction
  108. {
  109. parts = new List<Api.Part> { new() { text = GetSystemPrompt() } }
  110. },
  111. contents = chatHistory.Select(entry => new Api.Content
  112. {
  113. // The API expects "model" for the assistant's role.
  114. role = entry.role == "assistant" ? "model" : entry.role,
  115. parts = new List<Api.Part> { new() { text = entry.content } }
  116. }).ToList()
  117. };
  118. var jsonPayload = apiRequest.ToJson();
  119. using var request = new UnityWebRequest(url, "POST");
  120. var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload);
  121. request.uploadHandler = new UploadHandlerRaw(bodyRaw);
  122. request.downloadHandler = new DownloadHandlerBuffer();
  123. request.SetRequestHeader("Content-Type", "application/json");
  124. request.SetRequestHeader("Authorization", $"Bearer {authToken}");
  125. var operation = request.SendWebRequest();
  126. while (!operation.isDone)
  127. {
  128. await Task.Yield();
  129. }
  130. if (request.result == UnityWebRequest.Result.Success)
  131. {
  132. var responseJson = request.downloadHandler.text;
  133. Debug.Log($"[GeminiApiClient] Response received: \n{responseJson}.");
  134. var apiResponse = responseJson?.FromJson<Api.ApiResponse>();
  135. if (apiResponse is { candidates: not null } && apiResponse.candidates.Any())
  136. {
  137. var commandJson = apiResponse.candidates[0].content.parts[0].text;
  138. Debug.Log($"[GeminiApiClient] Command received: {commandJson}.");
  139. chatHistory.Add(new Data.ChatEntry { role = "model", content = commandJson });
  140. SessionManager.SaveChatHistory(chatHistory);
  141. var commandResponse = commandJson?.FromJson<CommandResponse>();
  142. if (commandResponse is { commands: not null })
  143. {
  144. Debug.Log($"[GeminiApiClient] Received {commandResponse.commands.Count} commands from LLM.");
  145. CommandExecutor.SetQueue(commandResponse.commands);
  146. }
  147. else
  148. {
  149. Debug.LogError($"[GeminiApiClient] Failed to parse command structure from LLM response text: {commandJson}");
  150. }
  151. }
  152. }
  153. else
  154. {
  155. Debug.LogError($"[GeminiApiClient] API Error: {request.error}\n{request.downloadHandler.text}");
  156. }
  157. }
  158. }
  159. }