293 lines
No EOL
9.1 KiB
C#
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 |