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; }
}
}