GeminiApiClient.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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. [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 region = _settings.gcpRegion == "global" ? string.Empty : $"{_settings.gcpRegion}-";
  85. var url = $"https://{region}aiplatform.googleapis.com/v1/projects/{_settings.gcpProjectId}/locations/{_settings.gcpRegion}/publishers/google/models/{_settings.modelName}:generateContent";
  86. var systemPromptWithContext = GetWorkingContext();
  87. var apiRequest = new ApiRequest
  88. {
  89. system_instruction = new SystemInstruction
  90. {
  91. parts = new List<Part> { new() { text = systemPromptWithContext } }
  92. },
  93. contents = chatHistory.Select(entry => new Content
  94. {
  95. role = entry.role == "assistant" ? "model" : entry.role,
  96. parts = new List<Part> { new() { text = entry.content } }
  97. }).ToList()
  98. };
  99. var jsonPayload = apiRequest.ToJson();
  100. using var request = new UnityWebRequest(url, "POST");
  101. var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload);
  102. request.uploadHandler = new UploadHandlerRaw(bodyRaw);
  103. request.downloadHandler = new DownloadHandlerBuffer();
  104. request.SetRequestHeader("Content-Type", "application/json");
  105. request.SetRequestHeader("Authorization", $"Bearer {authToken}");
  106. var operation = request.SendWebRequest();
  107. while (!operation.isDone)
  108. {
  109. await Task.Yield();
  110. }
  111. if (request.result == UnityWebRequest.Result.Success)
  112. {
  113. var responseJson = request.downloadHandler.text;
  114. Debug.Log($"[GeminiApiClient] API Response: \n{responseJson}");
  115. var apiResponse = responseJson.FromJson<ApiResponse>();
  116. if (apiResponse.candidates != null && apiResponse.candidates.Any())
  117. {
  118. var rawText = apiResponse.candidates[0].content.parts[0].text;
  119. var commandJson = ExtractJsonFromString(rawText);
  120. if (string.IsNullOrEmpty(commandJson))
  121. {
  122. Debug.LogError($"[GeminiApiClient] Could not extract valid JSON from the LLM response text: \n{rawText}");
  123. return;
  124. }
  125. chatHistory.Add(new ChatEntry { role = "model", content = commandJson });
  126. SessionManager.SaveChatHistory(chatHistory);
  127. CommandResponse commandResponse;
  128. try
  129. {
  130. commandResponse = commandJson.FromJson<CommandResponse>();
  131. }
  132. catch (Exception exception)
  133. {
  134. Debug.LogException(exception);
  135. return;
  136. }
  137. if (commandResponse is { commands: not null })
  138. {
  139. CommandExecutor.SetQueue(commandResponse.commands);
  140. }
  141. else
  142. {
  143. Debug.LogError($"[GeminiApiClient] Failed to parse command structure from LLM response text: {commandJson}");
  144. }
  145. }
  146. }
  147. else
  148. {
  149. Debug.LogError($"[GeminiApiClient] API Error: {request.error}\n{request.downloadHandler.text}");
  150. }
  151. }
  152. private void LoadSettings()
  153. {
  154. if (_settings) return;
  155. var guids = AssetDatabase.FindAssets("t:MCPSettings");
  156. if (guids.Length == 0) return;
  157. var path = AssetDatabase.GUIDToAssetPath(guids[0]);
  158. _settings = AssetDatabase.LoadAssetAtPath<Settings.MCPSettings>(path);
  159. }
  160. private void LoadSystemPrompt()
  161. {
  162. if (!string.IsNullOrEmpty(_systemPrompt)) return;
  163. var guids = AssetDatabase.FindAssets("MCP_SystemPrompt");
  164. if (guids.Length == 0) return;
  165. var path = AssetDatabase.GUIDToAssetPath(guids[0]);
  166. _systemPrompt = File.ReadAllText(path);
  167. }
  168. private static string ExtractJsonFromString(string text)
  169. {
  170. if (string.IsNullOrWhiteSpace(text)) return null;
  171. var firstBrace = text.IndexOf('{');
  172. var lastBrace = text.LastIndexOf('}');
  173. if (firstBrace == -1 || lastBrace == -1 || lastBrace < firstBrace)
  174. {
  175. return null;
  176. }
  177. return text.Substring(firstBrace, lastBrace - firstBrace + 1);
  178. }
  179. private string GetAuthToken()
  180. {
  181. if (!_settings) return null;
  182. try
  183. {
  184. var startInfo = new ProcessStartInfo
  185. {
  186. FileName = _settings.gcloudPath,
  187. Arguments = "auth print-access-token",
  188. RedirectStandardOutput = true,
  189. RedirectStandardError = true,
  190. UseShellExecute = false,
  191. CreateNoWindow = true
  192. };
  193. using var process = Process.Start(startInfo);
  194. if (process == null) return null;
  195. var accessToken = process.StandardOutput.ReadToEnd().Trim();
  196. var error = process.StandardError.ReadToEnd();
  197. process.WaitForExit();
  198. if (process.ExitCode == 0) return accessToken;
  199. Debug.LogError($"[GeminiApiClient] gcloud auth error: {error}");
  200. return null;
  201. }
  202. catch (Exception e)
  203. {
  204. Debug.LogError($"[GeminiApiClient] Exception while getting auth token: {e.Message}");
  205. return null;
  206. }
  207. }
  208. private string GetWorkingContext()
  209. {
  210. var workingContext = SessionManager.LoadWorkingContext();
  211. var systemPromptWithContext = new StringBuilder(_systemPrompt);
  212. systemPromptWithContext.AppendLine("\n\n## CURRENT WORKING CONTEXT");
  213. systemPromptWithContext.AppendLine("This is your short-term memory. Use the data here before asking for it again.");
  214. systemPromptWithContext.AppendLine("```json");
  215. systemPromptWithContext.AppendLine(workingContext.ToString(Newtonsoft.Json.Formatting.Indented));
  216. systemPromptWithContext.AppendLine("```");
  217. return systemPromptWithContext.ToString();
  218. }
  219. }
  220. }