#if UNITY_5_3_OR_NEWER using System; using System.Collections; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using GumYum.NPC; namespace GumYum.NPC.Unity { /// /// Unity-specific wrapper for GumYum Client that integrates with Unity's /// coroutine system This provides a MonoBehaviour-based interface for easier /// Unity integration /// public class GumYumUnityClient : MonoBehaviour { [Header("Configuration")] [SerializeField] private string baseUrl = "https://npc.gumyum.com"; [SerializeField] private string apiVersion = "v1"; [SerializeField] private float timeout = 75f; [SerializeField] private int maxRetries = 3; [SerializeField] private bool debugMode = false; [Header("API Credentials")] [SerializeField] private string apiKey = ""; [SerializeField] private string apiSecret = ""; // The underlying C# client private Client _client; // Unity-specific events public event Action OnCharacterSpawned; public event Action, Dictionary> OnDialogueReceived; public event Action OnDialogueChunkReceived; public event Action OnDialogueStreamStarted; public event Action, Dictionary> OnDialogueStreamEnded; public event Action OnRequestCompleted; public event Action OnRequestFailed; // Singleton pattern (optional) private static GumYumUnityClient _instance; public static GumYumUnityClient Instance { get { if (_instance == null) { _instance = FindFirstObjectByType(); if (_instance == null) { GameObject go = new GameObject("GumYumUnityClient"); _instance = go.AddComponent(); DontDestroyOnLoad(go); } } return _instance; } } /// /// Access to the underlying client /// public Client Client => _client; /// /// Manager accessors for convenience /// public NPCManager NPCs => _client?.NPCs; public UniverseManager Universes => _client?.Universes; public ChatManager Chat => _client?.Chat; private void Awake() { if (_instance != null && _instance != this) { Destroy(gameObject); return; } _instance = this; DontDestroyOnLoad(gameObject); InitializeClient(); } private void InitializeClient() { // Create the client _client = new Client() { BaseUrl = baseUrl, ApiVersion = apiVersion, Timeout = TimeSpan.FromSeconds(timeout), MaxRetries = maxRetries, DebugMode = debugMode, ApiKey = apiKey, ApiSecret = apiSecret }; // Subscribe to events and forward them _client.CharacterSpawned += (sender, npc) => OnCharacterSpawned?.Invoke(npc); _client.DialogueReceived += (sender, e) => OnDialogueReceived?.Invoke(e.Response, e.Context, e.MoodTransition); _client.DialogueChunkReceived += (sender, chunk) => OnDialogueChunkReceived?.Invoke(chunk); _client.DialogueStreamStarted += (sender, e) => OnDialogueStreamStarted?.Invoke(); _client.DialogueStreamEnded += (sender, e) => OnDialogueStreamEnded?.Invoke( e.FullResponse, e.Context, e.MoodTransition); _client.RequestCompleted += (sender, e) => OnRequestCompleted?.Invoke(e.Endpoint, e.Data); _client.RequestFailed += (sender, e) => OnRequestFailed?.Invoke(e.Endpoint, e.Error); } /// /// Update API credentials at runtime /// public void SetCredentials(string apiKey, string apiSecret) { this.apiKey = apiKey; this.apiSecret = apiSecret; if (_client != null) { _client.ApiKey = apiKey; _client.ApiSecret = apiSecret; } } /// /// Coroutine-based NPC spawning /// public IEnumerator SpawnNPCCoroutine(string universeId, int seed, long? npcId, Action callback) { var task = NPCs.SpawnAsync(universeId, seed, npcId); yield return new WaitUntil(() => task.IsCompleted); if (task.IsFaulted) { Debug.LogError( $"Failed to spawn NPC: {task.Exception?.GetBaseException().Message}"); callback?.Invoke(null); } else { callback?.Invoke(task.Result); } } /// /// Coroutine-based filtered NPC spawning /// public IEnumerator SpawnFilteredNPCCoroutine(string universeId, int seed, object filters, Action callback) { var task = NPCs.SpawnFilteredAsync(universeId, seed, filters); yield return new WaitUntil(() => task.IsCompleted); if (task.IsFaulted) { Debug.LogError( $"Failed to spawn filtered NPC: {task.Exception?.GetBaseException().Message}"); callback?.Invoke(null); } else { callback?.Invoke(task.Result); } } /// /// Coroutine-based chat completion /// public IEnumerator ChatCoroutine(NPC npc, List messages, Action callback, double temperature = 0.8, int maxTokens = 2000) { var task = npc.Chat.CompletionsAsync(messages, temperature, maxTokens); yield return new WaitUntil(() => task.IsCompleted); if (task.IsFaulted) { Debug.LogError( $"[GumYumUnityClient] Failed to complete chat: {task.Exception?.GetBaseException().Message}"); Debug.LogError($"[GumYumUnityClient] Full exception: {task.Exception}"); callback?.Invoke(null); } else { callback?.Invoke(task.Result); } } /// /// Simple chat helper that creates the message structure /// public IEnumerator SimpleChatCoroutine(NPC npc, string userMessage, Action callback, double temperature = 0.8, int maxTokens = 2000) { var messages = new List { new ChatMessage { Role = "user", Content = userMessage } }; yield return ChatCoroutine(npc, messages, response => { if (response?.Choices?.Count > 0) { callback?.Invoke(response.Choices[0].Message.Content); } else { callback?.Invoke(null); } }, temperature, maxTokens); } /// /// List public universes coroutine /// public IEnumerator ListPublicUniversesCoroutine( Action>> callback) { var task = Universes.ListPublicAsync(); yield return new WaitUntil(() => task.IsCompleted); if (task.IsFaulted) { Debug.LogError( $"Failed to list public universes: {task.Exception?.GetBaseException().Message}"); callback?.Invoke(null); } else { callback?.Invoke(task.Result); } } /// /// List user's universes coroutine /// public IEnumerator ListUniversesCoroutine(Action>> callback) { var task = Universes.ListAsync(); yield return new WaitUntil(() => task.IsCompleted); if (task.IsFaulted) { Debug.LogError( $"Failed to list user universes: {task.Exception?.GetBaseException().Message}"); callback?.Invoke(null); } else { callback?.Invoke(task.Result); } } /// /// Helper to convert async operations to coroutines /// public static IEnumerator ToCoroutine(Task task) { while (!task.IsCompleted) { yield return null; } if (task.IsFaulted) { throw task.Exception.GetBaseException(); } } /// /// Helper to convert async operations to coroutines with result /// public static IEnumerator ToCoroutine(Task task, Action resultCallback) { while (!task.IsCompleted) { yield return null; } if (task.IsFaulted) { Debug.LogError( $"Task failed: {task.Exception?.GetBaseException().Message}"); resultCallback?.Invoke(default(T)); } else { resultCallback?.Invoke(task.Result); } } private void OnDestroy() { _client?.Dispose(); } } /// /// Extension methods for easier Unity integration /// public static class GumYumUnityExtensions { /// /// Convert Task to Coroutine /// public static IEnumerator AsCoroutine(this Task task) { return GumYumUnityClient.ToCoroutine(task); } /// /// Convert Task to Coroutine /// public static IEnumerator AsCoroutine(this Task task, Action resultCallback) { return GumYumUnityClient.ToCoroutine(task, resultCallback); } } } #endif