using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace GumYum.NPC { /// /// Manager for NPC-related operations /// public class NPCManager { private readonly Client _client; public NPCManager(Client client) { _client = client; } /// /// Spawn a character - returns NPC object /// Pass null for npcId to spawn a random NPC /// public async Task SpawnAsync(string universeId, int seed, long? npcId = null, CancellationToken cancellationToken = default) { var queryParams = new Dictionary { ["universe_id"] = universeId, ["seed"] = seed.ToString() }; string endpoint; if (npcId.HasValue) { // Spawn specific NPC queryParams["npc_id"] = npcId.Value.ToString(); endpoint = "npc"; } else { // Spawn random NPC - let API handle the randomness endpoint = "npc/spawn"; } var response = await _client.RequestAsync>( HttpMethod.Get, endpoint, null, queryParams, cancellationToken); // Check if this is a redirect response if (response.ContainsKey("redirect_url")) { // Follow the redirect to get actual NPC data var redirectPath = response["redirect_url"].ToString(); // Strip the /v1/ prefix since client adds it if (redirectPath.StartsWith("/v1/")) { redirectPath = redirectPath.Substring(4); } // Make the redirect request var npcData = await _client.RequestAsync>( HttpMethod.Get, redirectPath, null, null, cancellationToken); var npc = new NPC(npcData, _client); _client.RaiseCharacterSpawned(npc); return npc; } else { // Direct response (shouldn't happen with /npc/spawn) var npc = new NPC(response, _client); _client.RaiseCharacterSpawned(npc); return npc; } } /// /// Spawn with advanced filters - supports any NPC field /// Examples: /// SpawnFilteredAsync("kingdom", 12345, new { profession = new[] { /// "warrior", "knight" }, location = new[] { "barracks", "castle" } }) /// SpawnFilteredAsync("blade-runner", 67890, new { personality_type = new[] /// { 8 }, sex = new[] { "female" } }) /// public async Task SpawnFilteredAsync(string universeId, int seed, object filters = null, CancellationToken cancellationToken = default) { var data = new { universe_id = universeId, world_seed = seed, filters = filters ?? new {} }; var endpoint = "npc/spawn_filtered"; // spawn_filtered uses POST with data in body var response = await _client.RequestAsync>( HttpMethod.Post, endpoint, data, null, cancellationToken); // Check if this is a redirect response if (response.ContainsKey("redirect_url")) { // Follow the redirect to get actual NPC data var redirectPath = response["redirect_url"].ToString(); // Strip the /v1/ prefix since client adds it if (redirectPath.StartsWith("/v1/")) { redirectPath = redirectPath.Substring(4); } // Make the redirect request var npcData = await _client.RequestAsync>( HttpMethod.Get, redirectPath, null, null, cancellationToken); var npc = new NPC(npcData, _client); _client.RaiseCharacterSpawned(npc); return npc; } else { // Direct response (shouldn't happen with /npc/spawn_filtered) var npc = new NPC(response, _client); _client.RaiseCharacterSpawned(npc); return npc; } } } /// /// Manager for Universe-related operations /// public class UniverseManager { private readonly Client _client; public UniverseManager(Client client) { _client = client; } /// /// List public universes (works with API keys) /// public async Task>> ListPublicAsync(CancellationToken cancellationToken = default) { // API returns {"public_universes": [...]} var response = await _client.RequestAsync>( HttpMethod.Get, "public/universes", null, null, cancellationToken); if (response != null && response.TryGetValue("public_universes", out var universesObj)) { // Handle System.Text.Json if (universesObj is System.Text.Json.JsonElement jsonElement) { return System.Text.Json.JsonSerializer .Deserialize>>( jsonElement.GetRawText()); } else if (universesObj is List> list) { return list; } } return new List>(); } /// /// List user's universes (works with JWT from API key exchange) /// public async Task>> ListAsync(CancellationToken cancellationToken = default) { // API returns {"universes": [...]} var response = await _client.RequestAsync>( HttpMethod.Get, "universes", null, null, cancellationToken); if (response != null && response.TryGetValue("universes", out var universesObj)) { // Handle System.Text.Json if (universesObj is System.Text.Json.JsonElement jsonElement) { return System.Text.Json.JsonSerializer .Deserialize>>( jsonElement.GetRawText()); } else if (universesObj is List> list) { return list; } } return new List>(); } /// /// Copy a universe (requires user authentication) /// public Task> CopyAsync(string publicUniverseId, string customName = "", CancellationToken cancellationToken = default) { throw new NotSupportedException( "[GumYum] Copy() requires user authentication. Use API keys with a " + "known universe ID instead."); } /// /// Get universe details /// public async Task> GetUniverseAsync(string universeId, CancellationToken cancellationToken = default) { return await _client.RequestAsync>( HttpMethod.Get, $"universes/{universeId}", null, null, cancellationToken); } } /// /// Manager for Chat-related operations /// public class ChatManager { private readonly Client _client; public ChatManager(Client client) { _client = client; } /// /// Generate dialogue (OpenAI-compatible chat.completions) /// public async Task CompletionsAsync( List messages, Dictionary npcParams, double temperature = 0.8, int maxTokens = 2000, bool stream = false, CancellationToken cancellationToken = default) { var data = new Dictionary { ["model"] = "gumyum-npc", ["temperature"] = temperature, ["messages"] = messages, ["stream"] = stream, ["npc_params"] = npcParams, ["max_tokens"] = maxTokens }; // Extract npc_id to top level if it exists in npc_params if (npcParams.ContainsKey("npc_id")) { data["npc_id"] = npcParams["npc_id"]; } if (stream) { throw new NotImplementedException( "Streaming is handled via CompletionsStreamAsync"); } else { var response = await _client.RequestAsync>( HttpMethod.Post, "chat/completions", data, null, cancellationToken); var completionResponse = new ChatCompletionResponse(response); if (completionResponse.Choices?.Count > 0) { var firstChoice = completionResponse.Choices[0]; if (firstChoice?.Message == null) { return completionResponse; } var content = firstChoice.Message.Content; var context = completionResponse.NpcContext; var moodTransition = completionResponse.MoodTransition; _client.RaiseDialogueReceived(new DialogueReceivedEventArgs { Response = content, Context = context, MoodTransition = moodTransition }); } return completionResponse; } } /// /// Generate dialogue with streaming support /// public async Task CompletionsStreamAsync( List messages, Dictionary npcParams, Action onChunk, double temperature = 0.8, int maxTokens = 2000, CancellationToken cancellationToken = default) { var data = new Dictionary { ["model"] = "gumyum-npc", ["temperature"] = temperature, ["messages"] = messages, ["stream"] = true, ["npc_params"] = npcParams, ["max_tokens"] = maxTokens }; // Extract npc_id to top level if it exists in npc_params if (npcParams.ContainsKey("npc_id")) { data["npc_id"] = npcParams["npc_id"]; } await _client.RequestStreamAsync(HttpMethod.Post, "chat/completions", data, null, onChunk, cancellationToken); } /// /// Generate dialogue (legacy method name for backwards compatibility) /// public Task ChatAsync(List messages, Dictionary npcParams, double temperature = 0.8, int maxTokens = 2000, CancellationToken cancellationToken = default) { return CompletionsAsync(messages, npcParams, temperature, maxTokens, false, cancellationToken); } /// /// Generate dialogue with streaming support (legacy method) /// public Task ChatStreamAsync(List messages, Dictionary npcParams, Action onChunk, double temperature = 0.8, int maxTokens = 2000, CancellationToken cancellationToken = default) { return CompletionsStreamAsync(messages, npcParams, onChunk, temperature, maxTokens, cancellationToken); } } /// /// Chat message structure /// public class ChatMessage { public string Role { get; set; } public string Content { get; set; } } /// /// Chat completion response /// public class ChatCompletionResponse { public List Choices { get; set; } public Dictionary NpcContext { get; set; } public Dictionary MoodTransition { get; set; } public ChatCompletionResponse(Dictionary data) { Choices = new List(); if (data?.ContainsKey("choices") == true) { var choicesObj = data["choices"]; if (choicesObj is JsonElement choicesJson) { // Handle JsonElement array foreach (var choiceElement in choicesJson.EnumerateArray()) { var choiceDict = JsonSerializer.Deserialize>( choiceElement.GetRawText()); Choices.Add(new ChatChoice(choiceDict)); } } else if (choicesObj is List choicesList) { Choices = choicesList ?.Select(c => new ChatChoice(c as Dictionary)) .ToList() ?? new List(); } } if (data?.ContainsKey("npc_context") == true) { NpcContext = data["npc_context"] as Dictionary; } if (data?.ContainsKey("mood_transition") == true) { MoodTransition = data["mood_transition"] as Dictionary; } } } /// /// Chat choice in completion response /// public class ChatChoice { public ChatMessage Message { get; set; } public ChatChoice(Dictionary data) { if (data?.ContainsKey("message") == true) { var msgObj = data["message"]; if (msgObj is JsonElement msgJson) { var msgDict = JsonSerializer.Deserialize>( msgJson.GetRawText()); Message = new ChatMessage { Role = msgDict?.GetValueOrDefault("role")?.ToString() ?? "assistant", Content = msgDict?.GetValueOrDefault("content")?.ToString() ?? "" }; } else if (msgObj is Dictionary msgData) { Message = new ChatMessage { Role = msgData.GetValueOrDefault("role")?.ToString() ?? "assistant", Content = msgData.GetValueOrDefault("content")?.ToString() ?? "" }; } } } } }