using System.IO; using System.Text; using UnityEditor; using UnityEngine; using System.Diagnostics; using UnityEngine.Networking; using System.Threading.Tasks; using Debug = UnityEngine.Debug; namespace LLM.Editor.PoC { /// /// A Unity Editor Window to interact with Google Cloud's Gemini LLM. /// This tool authenticates using the gcloud CLI to get an OAuth 2.0 token. /// public class LLMPromptTool : EditorWindow { // --- PRIVATE FIELDS --- // User inputs private string _gcpProjectId = "your-gcp-project-id"; private string _region = "us-central1"; private string _modelName = "gemini-1.5-flash-preview-0514"; private string _gcloudPath = "gcloud"; // Path to the gcloud executable private string _promptText = "Please explain what this C# script does."; private string _filePath = ""; private string _fileContent = ""; // Response and status private string _llmResponse = "Awaiting prompt..."; private bool _isRequestInProgress; // UI layout private Vector2 _promptScrollPos; private Vector2 _responseScrollPos; // --- UNITY EDITOR WINDOW --- [MenuItem("Tools/LLM Prompt Tool")] public static void ShowWindow() { GetWindow("GCP LLM Prompt"); } void OnGUI() { var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleCenter }; var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12 }; var wordWrapLabelStyle = new GUIStyle(EditorStyles.label) { wordWrap = true }; var wordWrapTextAreaStyle = new GUIStyle(EditorStyles.textArea) { wordWrap = true }; EditorGUILayout.LabelField("GCP LLM Prototype", titleStyle, GUILayout.Height(25)); EditorGUILayout.Space(10); EditorGUILayout.LabelField("GCP Configuration", headerStyle); _gcpProjectId = EditorGUILayout.TextField("Project ID", _gcpProjectId); _region = EditorGUILayout.TextField("Region", _region); _modelName = EditorGUILayout.TextField("Model Name", _modelName); _gcloudPath = EditorGUILayout.TextField("GCloud Executable Path", _gcloudPath); 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); EditorGUILayout.Space(15); EditorGUILayout.LabelField("User Prompt", headerStyle); _promptScrollPos = EditorGUILayout.BeginScrollView(_promptScrollPos, GUILayout.Height(100)); _promptText = EditorGUILayout.TextArea(_promptText, wordWrapTextAreaStyle, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); EditorGUILayout.Space(10); EditorGUILayout.LabelField("Attach C# File (Optional)", headerStyle); EditorGUILayout.BeginHorizontal(); EditorGUILayout.TextField("File Path", _filePath); if (GUILayout.Button("Browse...", GUILayout.Width(80))) { SelectFile(); } EditorGUILayout.EndHorizontal(); if (!string.IsNullOrEmpty(_fileContent)) { EditorGUILayout.HelpBox($"Attached file '{Path.GetFileName(_filePath)}'. Length: {_fileContent.Length} characters.", MessageType.None); } EditorGUILayout.Space(15); UnityEngine.GUI.enabled = !_isRequestInProgress && !string.IsNullOrEmpty(_gcpProjectId); if (GUILayout.Button(_isRequestInProgress ? "Waiting for Response..." : "Send Prompt to Gemini", GUILayout.Height(40))) { _ = SendPromptToLLM(); } UnityEngine.GUI.enabled = true; EditorGUILayout.Space(15); EditorGUILayout.LabelField("LLM Response", headerStyle); _responseScrollPos = EditorGUILayout.BeginScrollView(_responseScrollPos, EditorStyles.helpBox, GUILayout.ExpandHeight(true)); EditorGUILayout.SelectableLabel(_llmResponse, wordWrapLabelStyle, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); } // --- PRIVATE METHODS --- /// /// Executes the 'gcloud auth print-access-token' command to get a valid OAuth 2.0 token. /// /// The access token string, or null if an error occurs. private string GetAccessToken() { try { var startInfo = new ProcessStartInfo { FileName = _gcloudPath, // Use the full path provided by the user Arguments = "auth print-access-token", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var process = Process.Start(startInfo); if (process != null) { var accessToken = process.StandardOutput.ReadToEnd().Trim(); var error = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode == 0) return accessToken; Debug.LogError($"gcloud error: {error}"); _llmResponse = $"Failed to get auth token. Is gcloud CLI installed and are you logged in?\nError: {error}"; return null; } } catch (System.Exception e) { Debug.LogError($"Exception while getting access token: {e.Message}"); _llmResponse = $"Failed to run gcloud from path '{_gcloudPath}'. Ensure the path is correct and the Google Cloud CLI is installed."; } return null; } private void SelectFile() { var path = EditorUtility.OpenFilePanel("Select C# Script", Application.dataPath, "cs"); if (string.IsNullOrEmpty(path)) return; _filePath = path; try { _fileContent = File.ReadAllText(_filePath); } catch (System.Exception e) { _fileContent = ""; _filePath = ""; Debug.LogError($"Failed to read file: {e.Message}"); _llmResponse = $"Error: Could not read file at path {path}."; } } private async Task SendPromptToLLM() { _isRequestInProgress = true; _llmResponse = "Getting auth token..."; Repaint(); // 1. Get the OAuth 2.0 access token from gcloud var accessToken = GetAccessToken(); if (string.IsNullOrEmpty(accessToken)) { _isRequestInProgress = false; Repaint(); return; } _llmResponse = "Sending request to GCP..."; Repaint(); // 2. Construct the API request var url = $"https://{_region}-aiplatform.googleapis.com/v1/projects/{_gcpProjectId}/locations/{_region}/publishers/google/models/{_modelName}:generateContent"; var requestPayload = new RequestPayload(); var fullPrompt = new StringBuilder(_promptText); if (!string.IsNullOrEmpty(_fileContent)) { fullPrompt.AppendLine("\n\n--- Attached C# Script ---"); fullPrompt.AppendLine(_fileContent); } requestPayload.contents[0].parts[0].text = fullPrompt.ToString(); requestPayload.contents[0].role = "user"; var jsonPayload = JsonUtility.ToJson(requestPayload); // 3. Send the request with the new auth token using (var request = new UnityWebRequest(url, "POST")) { var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload); request.uploadHandler = new UploadHandlerRaw(bodyRaw); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {accessToken}"); // Use the fetched token var operation = request.SendWebRequest(); while (!operation.isDone) { await Task.Yield(); } if (request.result == UnityWebRequest.Result.Success) { try { var responseJson = request.downloadHandler.text; var responsePayload = JsonUtility.FromJson(responseJson); if (responsePayload.candidates != null && responsePayload.candidates.Length > 0 && responsePayload.candidates[0].content?.parts?.Length > 0) { _llmResponse = responsePayload.candidates[0].content.parts[0].text; Debug.Log(_llmResponse); } else { _llmResponse = "Received a valid but empty or malformed response.\n\n" + responseJson; } } catch (System.Exception e) { _llmResponse = $"Failed to parse JSON response: {e.Message}\n\nRaw Response:\n{request.downloadHandler.text}"; } } else { _llmResponse = $"Error: {request.error}\n\n{request.downloadHandler.text}"; Debug.LogError($"GCP Request Error: {request.error}\nResponse: {request.downloadHandler.text}"); } } _isRequestInProgress = false; Repaint(); } // --- JSON HELPER CLASSES --- [System.Serializable] private class RequestPayload { public Content[] contents = { new() }; } [System.Serializable] private class Content { public Part[] parts = { new() }; public string role; } [System.Serializable] private class Part { public string text; } [System.Serializable] private class ResponsePayload { public Candidate[] candidates; } [System.Serializable] private class Candidate { public Content content; } } }