GeminiApiClient.cs 9.8 KB

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