using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace GumYum.NPC { /// /// GumYum NPC Class - Elegant chat interface similar to Python client /// /// This class represents a spawned NPC and provides convenient methods /// for chatting with them. Similar to Python's npc.chat.completions() /// /// Usage: /// var npc = await client.NPCs.SpawnAsync("universe-id", 12345, 123456789); /// var response = await npc.Chat.CompletionsAsync(new[] { new ChatMessage { /// Role = "user", Content = "Hello!" } }); Console.WriteLine($"{npc.Name}: /// {response.Choices[0].Message.Content}"); /// public class NPC { // Events public event EventHandler DialogueReceived; public event EventHandler DialogueChunkReceived; public event EventHandler DialogueStreamStarted; public event EventHandler DialogueStreamEnded; // NPC Data (from API response) public long NpcId { get; set; } public string Name { get; set; } = ""; public string Profession { get; set; } = ""; public int PersonalityType { get; set; } = 5; public string UniverseId { get; set; } = ""; public int Seed { get; set; } = 0; public bool Cached { get; set; } = false; public string CacheUrl { get; set; } = ""; // Spawned data (nested) public Dictionary Spawned { get; set; } = new Dictionary(); // Current mood state public string CurrentMood { get; private set; } = ""; public string PreviousMood { get; private set; } = ""; public int StressLevel { get; private set; } = 5; public string LastMoodChangeReason { get; private set; } = ""; public double LastMoodChangeConfidence { get; private set; } = 0.0; // Client reference (not exported - set when spawned) private readonly Client _client; // Chat history for this NPC (array of message dicts with role and content) public List ChatHistory { get; } = new List(); // Internal chat manager for this NPC public NPCChatManager Chat { get; } public NPC(Dictionary npcData, Client client) { _client = client; SetupFromData(npcData); Chat = new NPCChatManager(this); ConnectClientSignals(); } /// /// Setup NPC from API response data /// private void SetupFromData(Dictionary npcData) { // Check if data is nested in "spawned_npc" first if (npcData.ContainsKey("spawned_npc") && npcData["spawned_npc"] is Dictionary spawnedData) { npcData = spawnedData; } // Handle npc_id conversion - API might return String or int if (npcData.TryGetValue("npc_id", out var npcIdRaw)) { if (npcIdRaw is JsonElement npcIdJson) { NpcId = npcIdJson.GetInt64(); } else { NpcId = Convert.ToInt64(npcIdRaw); } } if (npcData.TryGetValue("name", out var name)) Name = name.ToString(); if (npcData.TryGetValue("profession", out var profession)) Profession = profession.ToString(); if (npcData.TryGetValue("personality_type", out var personalityType)) { if (personalityType is JsonElement ptJson) { PersonalityType = ptJson.GetInt32(); } else { PersonalityType = Convert.ToInt32(personalityType); } } if (npcData.TryGetValue("universe_id", out var universeId)) UniverseId = universeId.ToString(); if (npcData.TryGetValue("seed", out var seed)) { if (seed is JsonElement seedJson) { Seed = seedJson.GetInt32(); } else { Seed = Convert.ToInt32(seed); } } if (npcData.TryGetValue("cached", out var cached)) { if (cached is JsonElement cachedJson) { Cached = cachedJson.GetBoolean(); } else { Cached = Convert.ToBoolean(cached); } } if (npcData.TryGetValue("cache_url", out var cacheUrl)) CacheUrl = cacheUrl.ToString(); // Handle spawned data if (npcData.TryGetValue("spawned", out var spawned) && spawned is Dictionary spawnedDict) { Spawned = spawnedDict; // Initialize mood from spawned data if (spawnedDict.TryGetValue("mood", out var mood)) CurrentMood = mood.ToString(); if (spawnedDict.TryGetValue("stress_level", out var stressLevel)) { if (stressLevel is JsonElement slJson) { StressLevel = slJson.GetInt32(); } else { StressLevel = Convert.ToInt32(stressLevel); } } } } /// /// Convenience method - direct completions alias (like Python /// npc.completions()) /// public Task CompletionsAsync(List messages, double temperature = 0.8, int maxTokens = 2000, bool stream = false, CancellationToken cancellationToken = default) { return Chat.CompletionsAsync(messages, temperature, maxTokens, stream, cancellationToken); } /// /// Chat with conversation history - includes all previous messages in this /// session /// public Task ChatWithHistoryAsync(string message, double temperature = 0.8, int maxTokens = 2000, bool stream = false, CancellationToken cancellationToken = default) { return Chat.ChatWithHistoryAsync(message, temperature, maxTokens, stream, cancellationToken); } /// /// Clear conversation history /// public void ClearHistory() { ChatHistory.Clear(); } /// /// Convert chat history to JSON string for saving /// public string ChatHistoryToJson() { return JsonSerializer.Serialize(ChatHistory); } /// /// Convert entire NPC data to dictionary for saving /// public Dictionary ToDict() { return new Dictionary { ["npc_id"] = NpcId, ["name"] = Name, ["profession"] = Profession, ["personality_type"] = PersonalityType, ["universe_id"] = UniverseId, ["seed"] = Seed, ["spawned"] = Spawned, ["cached"] = Cached, ["cache_url"] = CacheUrl, ["chat_history"] = ChatHistory }; } /// /// Convert NPC data to JSON string for saving /// public string ToJson() { return JsonSerializer.Serialize(ToDict()); } /// /// Load NPC data from dictionary (e.g., from saved file) /// public static NPC FromDict(Dictionary data, Client client) { return new NPC(data, client); } /// /// Load NPC data from JSON string /// public static NPC FromJson(string jsonStr, Client client) { var data = JsonSerializer.Deserialize>(jsonStr); return FromDict(data, client); } /// /// Get NPC's current location /// public string GetLocation() { return Spawned.TryGetValue("location", out var location) ? location.ToString() : "unknown"; } /// /// Get NPC's current mood /// public string GetMood() { if (!string.IsNullOrEmpty(CurrentMood)) return CurrentMood; return Spawned.TryGetValue("mood", out var mood) ? mood.ToString() : "neutral"; } /// /// Get NPC's stress level /// public int GetStressLevel() { return StressLevel; } /// /// Get previous mood (before last change) /// public string GetPreviousMood() { return PreviousMood; } /// /// Get last mood change reason /// public string GetLastMoodChangeReason() { return LastMoodChangeReason; } /// /// Get last mood change confidence /// public double GetLastMoodChangeConfidence() { return LastMoodChangeConfidence; } /// /// Check if mood has changed /// public bool HasMoodChanged() { return !string.IsNullOrEmpty(PreviousMood) && PreviousMood != CurrentMood; } /// /// Get detailed info string /// public string GetInfo() { return $"{Name} the {Profession} (Type {PersonalityType}) at {GetLocation()}, feeling {GetMood()}"; } /// /// Get Enneagram type description /// public string GetEnneagramDescription() { return PersonalityType switch { 1 => "The Reformer", 2 => "The Helper", 3 => "The Achiever", 4 => "The Individualist", 5 => "The Investigator", 6 => "The Loyalist", 7 => "The Enthusiast", 8 => "The Challenger", 9 => "The Peacemaker", _ => "Unknown Type" }; } /// /// Convert NPC to dictionary for API params /// public Dictionary ToDictionary() { return new Dictionary { ["npc_id"] = NpcId, ["universe_id"] = UniverseId, ["seed"] = Seed }; } /// /// Save this NPC to the server (persistent storage) /// public async Task SaveToServerAsync(string customName = "", CancellationToken cancellationToken = default) { var data = new Dictionary { ["universe_id"] = UniverseId, ["world_seed"] = Seed, ["npc_id"] = NpcId }; if (!string.IsNullOrEmpty(customName)) data["custom_name"] = customName; await _client.RequestAsync>( System.Net.Http.HttpMethod.Post, "npc/save", data, null, cancellationToken); } /// /// Connect to client's dialogue signals to forward them /// private void ConnectClientSignals() { if (_client != null) { _client.DialogueReceived += OnClientDialogueReceived; _client.DialogueChunkReceived += OnClientDialogueChunkReceived; _client.DialogueStreamStarted += OnClientDialogueStreamStarted; _client.DialogueStreamEnded += OnClientDialogueStreamEnded; } } private void OnClientDialogueReceived(object sender, DialogueReceivedEventArgs e) { // Only emit if this response is for our NPC if (e.Context?.TryGetValue("npc_id", out var npcId) == true) { long npcIdValue = 0; if (npcId is JsonElement npcIdJson) { npcIdValue = npcIdJson.GetInt64(); } else { npcIdValue = Convert.ToInt64(npcId); } if (npcIdValue == NpcId) { // Update mood data if transition occurred UpdateMoodFromTransition(e.MoodTransition); DialogueReceived?.Invoke(this, e); } } } private void OnClientDialogueChunkReceived(object sender, string chunk) { DialogueChunkReceived?.Invoke(this, chunk); } private void OnClientDialogueStreamStarted(object sender, EventArgs e) { DialogueStreamStarted?.Invoke(this, e); } private void OnClientDialogueStreamEnded(object sender, DialogueStreamEndedEventArgs e) { // Only emit if this response is for our NPC if (e.Context?.TryGetValue("npc_id", out var npcId) == true) { long npcIdValue = 0; if (npcId is JsonElement npcIdJson) { npcIdValue = npcIdJson.GetInt64(); } else { npcIdValue = Convert.ToInt64(npcId); } if (npcIdValue == NpcId) { // Update mood data if transition occurred UpdateMoodFromTransition(e.MoodTransition); DialogueStreamEnded?.Invoke(this, e); } } } /// /// Update internal mood state from mood transition data /// private void UpdateMoodFromTransition(Dictionary moodTransition) { if (moodTransition == null || moodTransition.Count == 0) return; // Store previous mood if we're changing if (moodTransition.TryGetValue("new_mood", out var newMood) && newMood.ToString() != CurrentMood) { PreviousMood = CurrentMood; CurrentMood = newMood.ToString(); } // Update stress level if (moodTransition.TryGetValue("stress_level", out var stressLevel)) { if (stressLevel is JsonElement slJson) { StressLevel = slJson.GetInt32(); } else { StressLevel = Convert.ToInt32(stressLevel); } } // Store mood change metadata if (moodTransition.TryGetValue("confidence", out var confidence)) { if (confidence is JsonElement confJson) { LastMoodChangeConfidence = confJson.GetDouble(); } else { LastMoodChangeConfidence = Convert.ToDouble(confidence); } } if (moodTransition.TryGetValue("reasoning", out var reasoning)) { LastMoodChangeReason = reasoning.ToString(); } } /// /// Chat manager class - provides the .Chat interface /// public class NPCChatManager { private readonly NPC _npc; public NPCChatManager(NPC npc) { _npc = npc; } /// /// Main chat completion method - matches Python API /// public async Task CompletionsAsync(List messages, double temperature = 0.8, int maxTokens = 2000, bool stream = false, CancellationToken cancellationToken = default) { // Build npc_params with this NPC's context including current mood var npcParams = new Dictionary { ["universe_id"] = _npc.UniverseId, ["world_seed"] = _npc.Seed, ["npc_id"] = _npc.NpcId, ["current_mood"] = _npc.GetMood(), ["stress_level"] = _npc.GetStressLevel() }; ChatCompletionResponse response; if (stream) { // For streaming, we need to handle it differently var tcs = new TaskCompletionSource(); string accumulatedContent = ""; Dictionary lastContext = null; Dictionary lastMoodTransition = null; await _npc._client.Chat.CompletionsStreamAsync( messages, npcParams, chunk => { if (chunk == null) // Stream ended { // Create a response object with the accumulated content var streamResponse = new ChatCompletionResponse(new Dictionary { ["choices"] = new List { new Dictionary { ["message"] = new Dictionary< string, object> { ["role"] = "assistant", ["content"] = accumulatedContent } } }, ["npc_context"] = lastContext, ["mood_transition"] = lastMoodTransition }); tcs.SetResult(streamResponse); } else { accumulatedContent += chunk; } }, temperature, maxTokens, cancellationToken); response = await tcs.Task; } else { response = await _npc._client.Chat.CompletionsAsync( messages, npcParams, temperature, maxTokens, false, cancellationToken); } // Update chat history if (response?.Choices?.Count > 0) { // Add user message(s) to history if (messages.Count > 0) { var lastUserMsg = messages[messages.Count - 1]; if (lastUserMsg.Role == "user") { _npc.ChatHistory.Add(lastUserMsg); } } // Add assistant response to history var choice = response.Choices[0]; if (choice.Message != null) { _npc.ChatHistory.Add( new ChatMessage { Role = choice.Message.Role, Content = choice.Message.Content }); } } return response; } /// /// Chat with conversation history - includes all previous messages in this /// session /// public async Task ChatWithHistoryAsync(string message, double temperature = 0.8, int maxTokens = 2000, bool stream = false, CancellationToken cancellationToken = default) { // Build messages array with history plus new message var messagesWithHistory = new List(_npc.ChatHistory); messagesWithHistory.Add( new ChatMessage { Role = "user", Content = message }); // Use regular completions which will also update history return await CompletionsAsync(messagesWithHistory, temperature, maxTokens, stream, cancellationToken); } } } }