GeminiApiClient.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using LLM.Editor.Analysis;
  9. using LLM.Editor.Api;
  10. using LLM.Editor.Core;
  11. using LLM.Editor.Data;
  12. using LLM.Editor.Helper;
  13. using UnityEditor;
  14. using UnityEngine.Networking;
  15. using Debug = UnityEngine.Debug;
  16. using Object = UnityEngine.Object;
  17. namespace LLM.Editor.Client
  18. {
  19. /// <summary>
  20. /// The client responsible for communicating with the live Google Gemini API.
  21. /// </summary>
  22. public class GeminiApiClient : ILlmApiClient
  23. {
  24. [System.Serializable]
  25. private class CommandResponse { public List<CommandData> commands; }
  26. private Settings.MCPSettings _settings;
  27. private string _systemPrompt;
  28. private bool _isInitialized;
  29. public async Task SendPrompt(string prompt, List<Object> stagedContext)
  30. {
  31. if (!await Initialize()) return;
  32. var authToken = GetAuthToken();
  33. if (string.IsNullOrEmpty(authToken))
  34. {
  35. Debug.LogError("[GeminiApiClient] Failed to get authentication token.");
  36. return;
  37. }
  38. var fullPrompt = BuildInitialPrompt(prompt, stagedContext);
  39. Debug.Log("[GeminiApiClient] Sending prompt: \n" + fullPrompt);
  40. var chatHistory = SessionManager.LoadChatHistory();
  41. chatHistory.Add(new ChatEntry { role = "user", content = fullPrompt });
  42. await SendApiRequest(chatHistory, authToken);
  43. }
  44. public async Task SendFollowUp(string detailedContext)
  45. {
  46. if (!await Initialize()) return;
  47. var authToken = GetAuthToken();
  48. if (string.IsNullOrEmpty(authToken))
  49. {
  50. Debug.LogError("[GeminiApiClient] Failed to get authentication token.");
  51. return;
  52. }
  53. var chatHistory = SessionManager.LoadChatHistory();
  54. chatHistory.Add(new ChatEntry { role = "user", content = detailedContext });
  55. await SendApiRequest(chatHistory, authToken);
  56. }
  57. private Task<bool> Initialize()
  58. {
  59. if (_isInitialized) return Task.FromResult(true);
  60. LoadSettings();
  61. LoadSystemPrompt();
  62. if (!_settings || string.IsNullOrEmpty(_systemPrompt))
  63. {
  64. Debug.LogError("[GeminiApiClient] Initialization failed. Check if MCPSettings and MCP_SystemPrompt.txt exist.");
  65. return Task.FromResult(false);
  66. }
  67. _isInitialized = true;
  68. return Task.FromResult(true);
  69. }
  70. private static string BuildInitialPrompt(string prompt, List<Object> stagedContext)
  71. {
  72. var promptBuilder = new StringBuilder();
  73. promptBuilder.AppendLine("User Request:");
  74. promptBuilder.AppendLine(prompt);
  75. if (stagedContext == null || stagedContext.All(o => o == null)) return promptBuilder.ToString();
  76. var tier1Summary = ContextBuilder.BuildTier1Summary(stagedContext);
  77. promptBuilder.AppendLine("\n--- Staged Context ---");
  78. promptBuilder.AppendLine(tier1Summary);
  79. promptBuilder.AppendLine("--- End Context ---");
  80. return promptBuilder.ToString();
  81. }
  82. private async Task SendApiRequest(List<ChatEntry> chatHistory, string authToken)
  83. {
  84. var url = $"https://{_settings.gcpRegion}-aiplatform.googleapis.com/v1/projects/{_settings.gcpProjectId}/locations/{_settings.gcpRegion}/publishers/google/models/{_settings.modelName}:generateContent";
  85. var apiRequest = new ApiRequest
  86. {
  87. system_instruction = new SystemInstruction
  88. {
  89. parts = new List<Part> { new() { text = _systemPrompt } }
  90. },
  91. contents = chatHistory.Select(entry => new Content
  92. {
  93. role = entry.role == "assistant" ? "model" : entry.role,
  94. parts = new List<Part> { new() { text = entry.content } }
  95. }).ToList()
  96. };
  97. var jsonPayload = apiRequest.ToJson();
  98. using var request = new UnityWebRequest(url, "POST");
  99. var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload);
  100. request.uploadHandler = new UploadHandlerRaw(bodyRaw);
  101. request.downloadHandler = new DownloadHandlerBuffer();
  102. request.SetRequestHeader("Content-Type", "application/json");
  103. request.SetRequestHeader("Authorization", $"Bearer {authToken}");
  104. var operation = request.SendWebRequest();
  105. while (!operation.isDone)
  106. {
  107. await Task.Yield();
  108. }
  109. if (request.result == UnityWebRequest.Result.Success)
  110. {
  111. var responseJson = request.downloadHandler.text;
  112. Debug.Log($"[GeminiApiClient] API Response: \n{responseJson}");
  113. var apiResponse = responseJson.FromJson<ApiResponse>();
  114. if (apiResponse.candidates != null && apiResponse.candidates.Any())
  115. {
  116. var rawText = apiResponse.candidates[0].content.parts[0].text;
  117. var commandJson = ExtractJsonFromString(rawText);
  118. if (string.IsNullOrEmpty(commandJson))
  119. {
  120. Debug.LogError($"[GeminiApiClient] Could not extract valid JSON from the LLM response text: \n{rawText}");
  121. return;
  122. }
  123. chatHistory.Add(new ChatEntry { role = "model", content = commandJson });
  124. SessionManager.SaveChatHistory(chatHistory);
  125. CommandResponse commandResponse;
  126. try
  127. {
  128. commandResponse = commandJson.FromJson<CommandResponse>();
  129. }
  130. catch (Exception exception)
  131. {
  132. Debug.LogException(exception);
  133. return;
  134. }
  135. if (commandResponse is { commands: not null })
  136. {
  137. CommandExecutor.SetQueue(commandResponse.commands);
  138. }
  139. else
  140. {
  141. Debug.LogError($"[GeminiApiClient] Failed to parse command structure from LLM response text: {commandJson}");
  142. }
  143. }
  144. }
  145. else
  146. {
  147. Debug.LogError($"[GeminiApiClient] API Error: {request.error}\n{request.downloadHandler.text}");
  148. }
  149. }
  150. private void LoadSettings()
  151. {
  152. if (_settings) return;
  153. var guids = AssetDatabase.FindAssets("t:MCPSettings");
  154. if (guids.Length == 0) return;
  155. var path = AssetDatabase.GUIDToAssetPath(guids[0]);
  156. _settings = AssetDatabase.LoadAssetAtPath<Settings.MCPSettings>(path);
  157. }
  158. private void LoadSystemPrompt()
  159. {
  160. if (!string.IsNullOrEmpty(_systemPrompt)) return;
  161. var guids = AssetDatabase.FindAssets("MCP_SystemPrompt");
  162. if (guids.Length == 0) return;
  163. var path = AssetDatabase.GUIDToAssetPath(guids[0]);
  164. _systemPrompt = File.ReadAllText(path);
  165. }
  166. private static string ExtractJsonFromString(string text)
  167. {
  168. if (string.IsNullOrWhiteSpace(text)) return null;
  169. var firstBrace = text.IndexOf('{');
  170. var lastBrace = text.LastIndexOf('}');
  171. if (firstBrace == -1 || lastBrace == -1 || lastBrace < firstBrace)
  172. {
  173. return null;
  174. }
  175. return text.Substring(firstBrace, lastBrace - firstBrace + 1);
  176. }
  177. private string GetAuthToken()
  178. {
  179. if (!_settings) return null;
  180. try
  181. {
  182. var startInfo = new ProcessStartInfo
  183. {
  184. FileName = _settings.gcloudPath,
  185. Arguments = "auth print-access-token",
  186. RedirectStandardOutput = true,
  187. RedirectStandardError = true,
  188. UseShellExecute = false,
  189. CreateNoWindow = true
  190. };
  191. using var process = Process.Start(startInfo);
  192. if (process == null) return null;
  193. var accessToken = process.StandardOutput.ReadToEnd().Trim();
  194. var error = process.StandardError.ReadToEnd();
  195. process.WaitForExit();
  196. if (process.ExitCode == 0) return accessToken;
  197. Debug.LogError($"[GeminiApiClient] gcloud auth error: {error}");
  198. return null;
  199. }
  200. catch (System.Exception e)
  201. {
  202. Debug.LogError($"[GeminiApiClient] Exception while getting auth token: {e.Message}");
  203. return null;
  204. }
  205. }
  206. }
  207. }