npc-clients-unity/Runtime/GumYumUnityClient.cs

293 lines
No EOL
9.1 KiB
C#

#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 {
/// <summary>
/// Unity-specific wrapper for GumYum Client that integrates with Unity's
/// coroutine system This provides a MonoBehaviour-based interface for easier
/// Unity integration
/// </summary>
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<NPC> OnCharacterSpawned;
public event Action<string, Dictionary<string, object>,
Dictionary<string, object>> OnDialogueReceived;
public event Action<string> OnDialogueChunkReceived;
public event Action OnDialogueStreamStarted;
public event Action<string, Dictionary<string, object>,
Dictionary<string, object>> OnDialogueStreamEnded;
public event Action<string, object> OnRequestCompleted;
public event Action<string, string> OnRequestFailed;
// Singleton pattern (optional)
private static GumYumUnityClient _instance;
public static GumYumUnityClient Instance {
get {
if (_instance == null) {
_instance = FindFirstObjectByType<GumYumUnityClient>();
if (_instance == null) {
GameObject go = new GameObject("GumYumUnityClient");
_instance = go.AddComponent<GumYumUnityClient>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
/// <summary>
/// Access to the underlying client
/// </summary>
public Client Client => _client;
/// <summary>
/// Manager accessors for convenience
/// </summary>
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);
}
/// <summary>
/// Update API credentials at runtime
/// </summary>
public void SetCredentials(string apiKey, string apiSecret) {
this.apiKey = apiKey;
this.apiSecret = apiSecret;
if (_client != null) {
_client.ApiKey = apiKey;
_client.ApiSecret = apiSecret;
}
}
/// <summary>
/// Coroutine-based NPC spawning
/// </summary>
public IEnumerator SpawnNPCCoroutine(string universeId, int seed, long? npcId,
Action<NPC> 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);
}
}
/// <summary>
/// Coroutine-based filtered NPC spawning
/// </summary>
public IEnumerator SpawnFilteredNPCCoroutine(string universeId, int seed,
object filters,
Action<NPC> 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);
}
}
/// <summary>
/// Coroutine-based chat completion
/// </summary>
public IEnumerator ChatCoroutine(NPC npc, List<ChatMessage> messages,
Action<ChatCompletionResponse> 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);
}
}
/// <summary>
/// Simple chat helper that creates the message structure
/// </summary>
public IEnumerator SimpleChatCoroutine(NPC npc, string userMessage,
Action<string> callback,
double temperature = 0.8,
int maxTokens = 2000) {
var messages =
new List<ChatMessage> { 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);
}
/// <summary>
/// List public universes coroutine
/// </summary>
public IEnumerator ListPublicUniversesCoroutine(
Action<List<Dictionary<string, object>>> 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);
}
}
/// <summary>
/// List user's universes coroutine
/// </summary>
public IEnumerator
ListUniversesCoroutine(Action<List<Dictionary<string, object>>> 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);
}
}
/// <summary>
/// Helper to convert async operations to coroutines
/// </summary>
public static IEnumerator ToCoroutine(Task task) {
while (!task.IsCompleted) {
yield return null;
}
if (task.IsFaulted) {
throw task.Exception.GetBaseException();
}
}
/// <summary>
/// Helper to convert async operations to coroutines with result
/// </summary>
public static IEnumerator ToCoroutine<T>(Task<T> task,
Action<T> 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(); }
}
/// <summary>
/// Extension methods for easier Unity integration
/// </summary>
public static class GumYumUnityExtensions {
/// <summary>
/// Convert Task to Coroutine
/// </summary>
public static IEnumerator AsCoroutine(this Task task) {
return GumYumUnityClient.ToCoroutine(task);
}
/// <summary>
/// Convert Task<T> to Coroutine
/// </summary>
public static IEnumerator AsCoroutine<T>(this Task<T> task,
Action<T> resultCallback) {
return GumYumUnityClient.ToCoroutine(task, resultCallback);
}
}
}
#endif