LLMPromptTool.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. using System.Diagnostics;
  2. using System.IO;
  3. using System.Text;
  4. using System.Threading.Tasks;
  5. using UnityEditor;
  6. using UnityEngine;
  7. using UnityEngine.Networking;
  8. using Debug = UnityEngine.Debug;
  9. namespace LLM.Editor.PoC
  10. {
  11. /// <summary>
  12. /// A Unity Editor Window to interact with Google Cloud's Gemini LLM.
  13. /// This tool authenticates using the gcloud CLI to get an OAuth 2.0 token.
  14. /// </summary>
  15. public class LLMPromptTool : EditorWindow
  16. {
  17. // --- PRIVATE FIELDS ---
  18. // User inputs
  19. private string _gcpProjectId = "your-gcp-project-id";
  20. private string _region = "us-central1";
  21. private string _modelName = "gemini-1.5-flash-preview-0514";
  22. private string _gcloudPath = "gcloud"; // Path to the gcloud executable
  23. private string _promptText = "Please explain what this C# script does.";
  24. private string _filePath = "";
  25. private string _fileContent = "";
  26. // Response and status
  27. private string _llmResponse = "Awaiting prompt...";
  28. private bool _isRequestInProgress;
  29. // UI layout
  30. private Vector2 _promptScrollPos;
  31. private Vector2 _responseScrollPos;
  32. // --- UNITY EDITOR WINDOW ---
  33. [MenuItem("LLM/LLM Prompt Tool")]
  34. public static void ShowWindow()
  35. {
  36. GetWindow<LLMPromptTool>("GCP LLM Prompt");
  37. }
  38. void OnGUI()
  39. {
  40. var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleCenter };
  41. var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12 };
  42. var wordWrapLabelStyle = new GUIStyle(EditorStyles.label) { wordWrap = true };
  43. var wordWrapTextAreaStyle = new GUIStyle(EditorStyles.textArea) { wordWrap = true };
  44. EditorGUILayout.LabelField("GCP LLM Prototype", titleStyle, GUILayout.Height(25));
  45. EditorGUILayout.Space(10);
  46. EditorGUILayout.LabelField("GCP Configuration", headerStyle);
  47. _gcpProjectId = EditorGUILayout.TextField("Project ID", _gcpProjectId);
  48. _region = EditorGUILayout.TextField("Region", _region);
  49. _modelName = EditorGUILayout.TextField("Model Name", _modelName);
  50. _gcloudPath = EditorGUILayout.TextField("GCloud Executable Path", _gcloudPath);
  51. EditorGUILayout.HelpBox("Because Unity doesn't know your system's PATH, you may need to provide the full path to the gcloud executable.\n- On macOS/Linux, run 'which gcloud' in terminal.\n- On Windows, run 'where gcloud' in Command Prompt.", MessageType.Info);
  52. EditorGUILayout.Space(15);
  53. EditorGUILayout.LabelField("User Prompt", headerStyle);
  54. _promptScrollPos = EditorGUILayout.BeginScrollView(_promptScrollPos, GUILayout.Height(100));
  55. _promptText = EditorGUILayout.TextArea(_promptText, wordWrapTextAreaStyle, GUILayout.ExpandHeight(true));
  56. EditorGUILayout.EndScrollView();
  57. EditorGUILayout.Space(10);
  58. EditorGUILayout.LabelField("Attach C# File (Optional)", headerStyle);
  59. EditorGUILayout.BeginHorizontal();
  60. EditorGUILayout.TextField("File Path", _filePath);
  61. if (GUILayout.Button("Browse...", GUILayout.Width(80)))
  62. {
  63. SelectFile();
  64. }
  65. EditorGUILayout.EndHorizontal();
  66. if (!string.IsNullOrEmpty(_fileContent))
  67. {
  68. EditorGUILayout.HelpBox($"Attached file '{Path.GetFileName(_filePath)}'. Length: {_fileContent.Length} characters.", MessageType.None);
  69. }
  70. EditorGUILayout.Space(15);
  71. UnityEngine.GUI.enabled = !_isRequestInProgress && !string.IsNullOrEmpty(_gcpProjectId);
  72. if (GUILayout.Button(_isRequestInProgress ? "Waiting for Response..." : "Send Prompt to Gemini", GUILayout.Height(40)))
  73. {
  74. _ = SendPromptToLLM();
  75. }
  76. UnityEngine.GUI.enabled = true;
  77. EditorGUILayout.Space(15);
  78. EditorGUILayout.LabelField("LLM Response", headerStyle);
  79. _responseScrollPos = EditorGUILayout.BeginScrollView(_responseScrollPos, EditorStyles.helpBox, GUILayout.ExpandHeight(true));
  80. EditorGUILayout.SelectableLabel(_llmResponse, wordWrapLabelStyle, GUILayout.ExpandHeight(true));
  81. EditorGUILayout.EndScrollView();
  82. }
  83. // --- PRIVATE METHODS ---
  84. /// <summary>
  85. /// Executes the 'gcloud auth print-access-token' command to get a valid OAuth 2.0 token.
  86. /// </summary>
  87. /// <returns>The access token string, or null if an error occurs.</returns>
  88. private string GetAccessToken()
  89. {
  90. try
  91. {
  92. var startInfo = new ProcessStartInfo
  93. {
  94. FileName = _gcloudPath, // Use the full path provided by the user
  95. Arguments = "auth print-access-token",
  96. RedirectStandardOutput = true,
  97. RedirectStandardError = true,
  98. UseShellExecute = false,
  99. CreateNoWindow = true
  100. };
  101. using var process = Process.Start(startInfo);
  102. if (process != null)
  103. {
  104. var accessToken = process.StandardOutput.ReadToEnd().Trim();
  105. var error = process.StandardError.ReadToEnd();
  106. process.WaitForExit();
  107. if (process.ExitCode == 0) return accessToken;
  108. Debug.LogError($"gcloud error: {error}");
  109. _llmResponse = $"Failed to get auth token. Is gcloud CLI installed and are you logged in?\nError: {error}";
  110. return null;
  111. }
  112. }
  113. catch (System.Exception e)
  114. {
  115. Debug.LogError($"Exception while getting access token: {e.Message}");
  116. _llmResponse = $"Failed to run gcloud from path '{_gcloudPath}'. Ensure the path is correct and the Google Cloud CLI is installed.";
  117. }
  118. return null;
  119. }
  120. private void SelectFile()
  121. {
  122. var path = EditorUtility.OpenFilePanel("Select C# Script", Application.dataPath, "cs");
  123. if (string.IsNullOrEmpty(path)) return;
  124. _filePath = path;
  125. try
  126. {
  127. _fileContent = File.ReadAllText(_filePath);
  128. }
  129. catch (System.Exception e)
  130. {
  131. _fileContent = "";
  132. _filePath = "";
  133. Debug.LogError($"Failed to read file: {e.Message}");
  134. _llmResponse = $"Error: Could not read file at path {path}.";
  135. }
  136. }
  137. private async Task SendPromptToLLM()
  138. {
  139. _isRequestInProgress = true;
  140. _llmResponse = "Getting auth token...";
  141. Repaint();
  142. // 1. Get the OAuth 2.0 access token from gcloud
  143. var accessToken = GetAccessToken();
  144. if (string.IsNullOrEmpty(accessToken))
  145. {
  146. _isRequestInProgress = false;
  147. Repaint();
  148. return;
  149. }
  150. _llmResponse = "Sending request to GCP...";
  151. Repaint();
  152. // 2. Construct the API request
  153. var url = $"https://{_region}-aiplatform.googleapis.com/v1/projects/{_gcpProjectId}/locations/{_region}/publishers/google/models/{_modelName}:generateContent";
  154. var requestPayload = new RequestPayload();
  155. var fullPrompt = new StringBuilder(_promptText);
  156. if (!string.IsNullOrEmpty(_fileContent))
  157. {
  158. fullPrompt.AppendLine("\n\n--- Attached C# Script ---");
  159. fullPrompt.AppendLine(_fileContent);
  160. }
  161. requestPayload.contents[0].parts[0].text = fullPrompt.ToString();
  162. requestPayload.contents[0].role = "user";
  163. var jsonPayload = JsonUtility.ToJson(requestPayload);
  164. // 3. Send the request with the new auth token
  165. using (var request = new UnityWebRequest(url, "POST"))
  166. {
  167. var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload);
  168. request.uploadHandler = new UploadHandlerRaw(bodyRaw);
  169. request.downloadHandler = new DownloadHandlerBuffer();
  170. request.SetRequestHeader("Content-Type", "application/json");
  171. request.SetRequestHeader("Authorization", $"Bearer {accessToken}"); // Use the fetched token
  172. var operation = request.SendWebRequest();
  173. while (!operation.isDone)
  174. {
  175. await Task.Yield();
  176. }
  177. if (request.result == UnityWebRequest.Result.Success)
  178. {
  179. try
  180. {
  181. var responseJson = request.downloadHandler.text;
  182. var responsePayload = JsonUtility.FromJson<ResponsePayload>(responseJson);
  183. if (responsePayload.candidates != null && responsePayload.candidates.Length > 0 && responsePayload.candidates[0].content?.parts?.Length > 0)
  184. {
  185. _llmResponse = responsePayload.candidates[0].content.parts[0].text;
  186. Debug.Log(_llmResponse);
  187. }
  188. else
  189. {
  190. _llmResponse = "Received a valid but empty or malformed response.\n\n" + responseJson;
  191. }
  192. }
  193. catch (System.Exception e)
  194. {
  195. _llmResponse = $"Failed to parse JSON response: {e.Message}\n\nRaw Response:\n{request.downloadHandler.text}";
  196. }
  197. }
  198. else
  199. {
  200. _llmResponse = $"Error: {request.error}\n\n{request.downloadHandler.text}";
  201. Debug.LogError($"GCP Request Error: {request.error}\nResponse: {request.downloadHandler.text}");
  202. }
  203. }
  204. _isRequestInProgress = false;
  205. Repaint();
  206. }
  207. // --- JSON HELPER CLASSES ---
  208. [System.Serializable] private class RequestPayload { public Content[] contents = { new() }; }
  209. [System.Serializable] private class Content { public Part[] parts = { new() }; public string role; }
  210. [System.Serializable] private class Part { public string text; }
  211. [System.Serializable] private class ResponsePayload { public Candidate[] candidates; }
  212. [System.Serializable] private class Candidate { public Content content; }
  213. }
  214. }