GeminiApiClient.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. using System.Linq;
  2. using System.Text;
  3. using UnityEditor;
  4. using UnityEngine;
  5. using LLM.Editor.Core;
  6. using System.Diagnostics;
  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.
  64. Here are the available commands:
  65. - DisplayMessage: Show a text message to the user. Use this for conversational replies, questions, or to provide information.
  66. - CreateScript: Creates a new C# script file.
  67. - CreatePrefab: Creates a new prefab from a source model found in the project.
  68. - AddComponentToAsset: Attaches a script to a prefab asset.
  69. - InstantiatePrefab: Creates an instance of a prefab in the current scene.
  70. - RequestClarification: Ask the user a follow-up question if their request is ambiguous.
  71. Example of a valid response:
  72. {
  73. ""commands"": [
  74. {
  75. ""commandName"": ""DisplayMessage"",
  76. ""jsonData"": ""{\""message\"":\""Hello! How can I help?""\}""
  77. }
  78. ]
  79. }";
  80. }
  81. public static async Task SendPrompt(string prompt)
  82. {
  83. if (!LoadSettings()) return;
  84. Debug.Log($"[GeminiApiClient] Getting auth token...");
  85. var authToken = GetAuthToken();
  86. if (string.IsNullOrEmpty(authToken))
  87. {
  88. Debug.LogError("[GeminiApiClient] Failed to get authentication token. Is gcloud CLI installed and are you logged in?");
  89. return;
  90. }
  91. Debug.Log($"[GeminiApiClient] Sending prompt to live API: {prompt}");
  92. var chatHistory = SessionManager.LoadChatHistory();
  93. chatHistory.Add(new Data.ChatEntry { role = "user", content = prompt });
  94. SessionManager.SaveChatHistory(chatHistory);
  95. var url = $"https://{_settings.gcpRegion}-aiplatform.googleapis.com/v1/projects/{_settings.gcpProjectId}/locations/{_settings.gcpRegion}/publishers/google/models/{_settings.modelName}:generateContent";
  96. // Construct the request payload, adding the system prompt first.
  97. var apiRequest = new Api.ApiRequest
  98. {
  99. system_instruction = new Api.SystemInstruction
  100. {
  101. parts = new List<Api.Part> { new() { text = GetSystemPrompt() } }
  102. },
  103. contents = chatHistory.Select(entry => new Api.Content
  104. {
  105. // The API expects "model" for the assistant's role.
  106. role = entry.role == "assistant" ? "model" : entry.role,
  107. parts = new List<Api.Part> { new() { text = entry.content } }
  108. }).ToList()
  109. };
  110. var jsonPayload = JsonUtility.ToJson(apiRequest);
  111. using var request = new UnityWebRequest(url, "POST");
  112. var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload);
  113. request.uploadHandler = new UploadHandlerRaw(bodyRaw);
  114. request.downloadHandler = new DownloadHandlerBuffer();
  115. request.SetRequestHeader("Content-Type", "application/json");
  116. request.SetRequestHeader("Authorization", $"Bearer {authToken}");
  117. var operation = request.SendWebRequest();
  118. while (!operation.isDone)
  119. {
  120. await Task.Yield();
  121. }
  122. if (request.result == UnityWebRequest.Result.Success)
  123. {
  124. var responseJson = request.downloadHandler.text;
  125. var apiResponse = JsonUtility.FromJson<Api.ApiResponse>(responseJson);
  126. if (apiResponse.candidates != null && apiResponse.candidates.Any())
  127. {
  128. var commandJson = apiResponse.candidates[0].content.parts[0].text;
  129. Debug.Log($"[GeminiApiClient] Command received: {commandJson}.");
  130. chatHistory.Add(new Data.ChatEntry { role = "model", content = commandJson });
  131. SessionManager.SaveChatHistory(chatHistory);
  132. var commandResponse = JsonUtility.FromJson<CommandResponse>(commandJson);
  133. if (commandResponse is { commands: not null })
  134. {
  135. Debug.Log($"[GeminiApiClient] Received {commandResponse.commands.Count} commands from LLM.");
  136. CommandExecutor.SetQueue(commandResponse.commands);
  137. }
  138. else
  139. {
  140. Debug.LogError($"[GeminiApiClient] Failed to parse command structure from LLM response text: {commandJson}");
  141. }
  142. }
  143. }
  144. else
  145. {
  146. Debug.LogError($"[GeminiApiClient] API Error: {request.error}\n{request.downloadHandler.text}");
  147. }
  148. }
  149. }
  150. }