using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Convai.Scripts.Runtime.Core; using Convai.Scripts.Runtime.LoggerSystem; using Grpc.Core; using Service; using UnityEngine; using Random = UnityEngine.Random; namespace Convai.Scripts.Runtime.Features { /// /// Manages the conversation between two Convai powered NPC groups /// public class NPC2NPCConversationManager : MonoBehaviour { private const string GRPC_API_ENDPOINT = "stream.convai.com"; public static NPC2NPCConversationManager Instance; public List npcGroups; private readonly List _groupsWhereConversationEnded = new(); private string _apiKey = string.Empty; private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); LoadApiKey(); } private void Start() { foreach (NPCGroup group in npcGroups) group.Initialize(HandlePlayerVicinityChanged); StartConversationWithAllNPCs(); } /// /// Handles the event of player vicinity change. /// /// Indicates if the player is near. /// The NPC for which the vicinity changed. private void HandlePlayerVicinityChanged(bool isPlayerNear, ConvaiGroupNPCController npc) { if (isPlayerNear) ResumeConversation(npc); } /// /// Processes the message from the sender NPC. /// /// The NPC sending the message. /// The topic of the conversation. /// The message to be processed. /// The processed message. private string ProcessMessage(ConvaiGroupNPCController sender, string topic, string message) { string processedMessage = $"{sender.CharacterName} said \"{message}\" to you. Reply to it. "; processedMessage += Random.Range(0, 2) == 0 ? $"Talk about something related to {message}. " : $"Talk about something other than \"{message}\" but related to {topic}. Gently change the conversation topic. "; return processedMessage + "Definitely, reply to the message. Dont address speaker. Keep the reply short. Do not repeat the same message, or keep asking same question."; } /// /// Relays the message from the sender NPC. /// /// The message to be relayed. /// The NPC sending the message. /// public void RelayMessage(string message, ConvaiGroupNPCController sender) { NPCGroup npcGroup = npcGroups.Find(c => c.BelongToGroup(sender)); if (npcGroup == null) { ConvaiLogger.Warn("Conversation not found for the sender.", ConvaiLogger.LogCategory.Character); return; } npcGroup.messageToRelay = message; if (!npcGroup.CurrentSpeaker.IsPlayerNearMe()) return; StartCoroutine(RelayMessageCoroutine(message, sender, npcGroup)); } /// /// Coroutine to relay the message from the sender NPC. /// /// The message to be relayed. /// The NPC group to relay the message to. /// An IEnumerator to be used in a coroutine. private IEnumerator RelayMessageCoroutine(string message, ConvaiGroupNPCController sender, NPCGroup npcGroup) { yield return new WaitForSeconds(0.5f); ConvaiGroupNPCController receiver = npcGroup.GroupNPC1 == sender ? npcGroup.GroupNPC2 : npcGroup.GroupNPC1; while (receiver.ConvaiNPC.IsCharacterTalking) { yield return new WaitForSeconds(0.1f); } try { ConvaiLogger.DebugLog($"Relaying message from {sender.CharacterName} to {receiver.CharacterName}: {message}", ConvaiLogger.LogCategory.Character); string processedMessage = ProcessMessage(receiver, npcGroup.topic, message); receiver.SendTextDataNPC2NPC(processedMessage); } catch (Exception e) { ConvaiLogger.Warn($"Failed to relay message: {e.Message}", ConvaiLogger.LogCategory.Character); } } /// /// Switches the speaker in the conversation. /// /// The current speaker NPC. /// The new speaker NPC. public void SwitchSpeaker(ConvaiGroupNPCController currentSpeaker) { NPCGroup group = npcGroups.Find(g => g.CurrentSpeaker == currentSpeaker); if (group != null) { group.CurrentSpeaker = currentSpeaker == group.GroupNPC1 ? group.GroupNPC2 : group.GroupNPC1; ConvaiLogger.DebugLog($"Switching NPC2NPC Speaker to {group.CurrentSpeaker}", ConvaiLogger.LogCategory.Character); return; } ConvaiLogger.Warn("Failed to switch speaker. Current speaker not found in any group.", ConvaiLogger.LogCategory.Character); } private void LoadApiKey() { ConvaiAPIKeySetup.GetAPIKey(out _apiKey); } /// /// Initializes a single NPC for conversation. /// /// The NPC to initialize. /// The GRPC client to use for the NPC. private void InitializeNPC(ConvaiGroupNPCController npc, NPC2NPCGRPCClient grpcClient) { if (npc == null) { ConvaiLogger.Warn("The given NPC is null.", ConvaiLogger.LogCategory.Character); return; } npc.ConversationManager = this; npc.InitializeNpc2NpcGrpcClient(grpcClient); npc.AttachSpeechBubble(); npc.IsInConversationWithAnotherNPC = true; npc.ConvaiNPC.isCharacterActive = false; } /// /// Starts the conversation for the given NPC. /// /// The NPC to start the conversation for. private void InitializeNPCGroup(NPCGroup npcGroup) { if (npcGroup == null) { ConvaiLogger.Warn("The given NPC is not part of any group.", ConvaiLogger.LogCategory.Character); return; } ConvaiGroupNPCController npc1 = npcGroup.GroupNPC1; ConvaiGroupNPCController npc2 = npcGroup.GroupNPC2; if (npc1.IsInConversationWithAnotherNPC || npc2.IsInConversationWithAnotherNPC) { ConvaiLogger.Warn($"{npc1.CharacterName} or {npc2.CharacterName} is already in a conversation.", ConvaiLogger.LogCategory.Character); return; } NPC2NPCGRPCClient grpcClient = CreateAndInitializeGRPCClient(npcGroup); // Initialize both NPCs InitializeNPC(npc1, grpcClient); InitializeNPC(npc2, grpcClient); npcGroup.CurrentSpeaker = Random.Range(0, 10) % 2 == 0 ? npc1 : npc2; } /// /// Creates and initializes a new GRPC client for the given NPC group. /// /// The NPC group to create the GRPC client for. /// The initialized GRPC client. private NPC2NPCGRPCClient CreateAndInitializeGRPCClient(NPCGroup group) { GameObject grpcClientGameObject = new($"GRPCClient_{group.GroupNPC1.CharacterID}_{group.GroupNPC2.CharacterID}") { transform = { parent = transform } }; NPC2NPCGRPCClient grpcClient = grpcClientGameObject.AddComponent(); ConvaiService.ConvaiServiceClient serviceClient = CreateNewConvaiServiceClient(); grpcClient.Initialize(_apiKey, serviceClient, group); return grpcClient; } /// /// Creates a new ConvaiServiceClient. /// /// The new ConvaiServiceClient. private ConvaiService.ConvaiServiceClient CreateNewConvaiServiceClient() { try { SslCredentials credentials = new(); List options = new() { new ChannelOption(ChannelOptions.MaxReceiveMessageLength, 16 * 1024 * 1024) }; Channel channel = new(GRPC_API_ENDPOINT, credentials, options); return new ConvaiService.ConvaiServiceClient(channel); } catch (Exception ex) { ConvaiLogger.Error($"Failed to create ConvaiServiceClient: {ex.Message}", ConvaiLogger.LogCategory.Character); throw; } } /// /// Resumes the conversation for the given NPC. /// /// The NPC to resume the conversation for. private void ResumeConversation(ConvaiGroupNPCController sender) { NPCGroup npcGroup = npcGroups.Find(g => g.BelongToGroup(sender)); if (npcGroup.IsAnyoneTalking()) return; if (_groupsWhereConversationEnded.Contains(npcGroup)) { InitializeNPCGroup(npcGroup); _groupsWhereConversationEnded.Remove(npcGroup); } if (string.IsNullOrEmpty(npcGroup.messageToRelay)) { string message = $"Talk about {npcGroup.topic}."; npcGroup.CurrentSpeaker.SendTextDataNPC2NPC(message); npcGroup.messageToRelay = message; ConvaiLogger.DebugLog($"Starting conversation for the first time between {npcGroup.GroupNPC1.CharacterName} and {npcGroup.GroupNPC2.CharacterName}", ConvaiLogger.LogCategory.Character); } else { RelayMessage(npcGroup.messageToRelay, npcGroup.CurrentSpeaker); ConvaiLogger.DebugLog($"Resuming conversation between {npcGroup.GroupNPC1.CharacterName} and {npcGroup.GroupNPC2.CharacterName}", ConvaiLogger.LogCategory.Character); } } /// /// Ends the conversation for the given NPC. /// /// The NPC to end the conversation for. public void EndConversation(ConvaiGroupNPCController npc) { NPCGroup group = npcGroups.Find(g => g.BelongToGroup(npc)); ConvaiLogger.DebugLog($"Ending conversation between {group.GroupNPC1.CharacterName} and {group.GroupNPC2.CharacterName}", ConvaiLogger.LogCategory.Character); void EndConversationForNPC(ConvaiGroupNPCController groupNPC) { groupNPC.IsInConversationWithAnotherNPC = false; groupNPC.ConvaiNPC.InterruptCharacterSpeech(); groupNPC.GetComponent().DetachSpeechBubble(); } EndConversationForNPC(group.GroupNPC1); EndConversationForNPC(group.GroupNPC2); _groupsWhereConversationEnded.Add(group); ConvaiNPCManager.Instance.SetActiveConvaiNPC(npc.ConvaiNPC); Destroy(transform.Find($"GRPCClient_{group.GroupNPC1.CharacterID}_{group.GroupNPC2.CharacterID}").gameObject); } /// /// Starts the conversation with all NPCs. /// private void StartConversationWithAllNPCs() { IEnumerable filteredList = npcGroups .Where(npcGroup => npcGroup.BothNPCAreNotNull()) .Where(npcGroup => npcGroup.BothNPCAreNotActiveNPC()); foreach (NPCGroup npcGroup in filteredList) InitializeNPCGroup(npcGroup); } } }