using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OpenMetaverse; using SecondBotEvents.Config; using System; using System.Collections.Generic; using System.Text; using System.Linq; using System.Reflection; using System.Net.Http; using OpenMetaverse.ImportExport.Collada14; using OpenMetaverse.Packets; using OpenAI.Managers; using OpenAI; using Microsoft.Extensions.DependencyInjection; using OpenAI.ObjectModels.RequestModels; using OpenAI.ObjectModels; using OpenAI.ObjectModels.ResponseModels; namespace SecondBotEvents.Services { public class ChatGptService : BotServices { public ChatGptConfig myConfig = null; public ChatGptService(EventsSecondBot setMaster) : base(setMaster) { myConfig = new ChatGptConfig(master.fromEnv, master.fromFolder); if (myConfig.GetEnabled() == false) { return; } } protected int chatHistorySize = 5; protected int localchatRateLimit = 3; protected int groupchatRateLimit = 3; protected int imchatRateLimit = 3; public override void Start(bool updateEnabled = false, bool setEnabledTo = false) { if (updateEnabled == true) { myConfig.setEnabled(setEnabledTo); } if (myConfig.GetEnabled() == false) { return; } chatHistorySize = inrange(3,10,myConfig.GetChatHistoryMessages()); localchatRateLimit = inrange(1, 10, myConfig.GetLocalchatRateLimiter()); groupchatRateLimit = inrange(1, 10, myConfig.GetGroupReplyRateLimiter()); imchatRateLimit = inrange(1, 10, myConfig.GetImReplyRateLimiter()); running = true; master.BotClientNoticeEvent += BotClientRestart; } protected int inrange(int value, int min, int max) { if (value < min) return min; else if(value > max) return max; return value; } public override void Stop() { running = false; } public override string Status() { if (myConfig == null) { return "No Config"; } else if (myConfig.GetEnabled() == false) { return "Disabled"; } upkeep(); if (chatHistoryAI.Count() > 0) { return "Enabled running: " + chatHistoryAI.Count().ToString() + " chat windows"; } return "Enabled No chat history"; } readonly string[] hard_blocked_agents = new[] { "secondlife", "second life" }; protected void BotLocalchat(object o, ChatEventArgs e) { if (e.SourceID == GetClient().Self.AgentID) { return; } if (myConfig.GetLocalchatReply() == false) { return; } switch (e.Type) { case ChatType.OwnerSay: case ChatType.Whisper: case ChatType.Normal: case ChatType.Shout: case ChatType.RegionSayTo: { if (hard_blocked_agents.Contains(e.FromName.ToLowerInvariant()) == true) { break; } if (e.SourceType == ChatSourceType.Object) { break; } else if (e.SourceType == ChatSourceType.System) { break; } if (e.Type == ChatType.OwnerSay) { break; } // trigger localchat GetAiReply(localchatRateLimit, UUID.Zero, e.SourceID, e.FromName, e.Message, false, false); break; } default: { break; } } } protected void BotImMessage(object o, InstantMessageEventArgs e) { if (e.IM.FromAgentID == GetClient().Self.AgentID) { return; } switch (e.IM.Dialog) { case InstantMessageDialog.MessageFromObject: { // object IM break; } case InstantMessageDialog.MessageFromAgent: // shared with SessionSend case InstantMessageDialog.SessionSend: { if (master.DataStoreService.GetIsGroup(e.IM.IMSessionID) == false) { // trigger avatar IM if (myConfig.GetAllowImReplys() == false) { break; } if (myConfig.GetImReplyFriendsOnly() == true) { if (GetClient().Friends.FriendList.ContainsKey(e.IM.FromAgentID) == false) { break; } } // request GPT reply to avatar IM GetAiReply(imchatRateLimit, e.IM.FromAgentID, e.IM.FromAgentID, e.IM.FromAgentName, e.IM.Message, true); break; } // trigger group IM if (myConfig.GetAllowGroupReplys() == true) { break; } if (myConfig.GetGroupReplyForGroup() != e.IM.IMSessionID.ToString()) { break; } GetAiReply(groupchatRateLimit, e.IM.FromAgentID, e.IM.FromAgentID, e.IM.FromAgentName, e.IM.Message, false, true); break; } default: { break; } } } Dictionary>> chatHistoryAI = new Dictionary>>(); Dictionary ChatHistoryLastAccessed = new Dictionary(); Dictionary ChatRateLimiter = new Dictionary(); protected long lastUpkeep = 0; protected void upkeep() { long dif = SecondbotHelpers.UnixTimeNow() - lastUpkeep; if (dif < 30) { return; // upkeep not needed } lastUpkeep = SecondbotHelpers.UnixTimeNow(); // check for expired chat windows (longer than 2 mins from last message) lock (ChatHistoryLastAccessed) lock (chatHistoryAI) lock (ChatRateLimiter) { List needcleaning = new List(); long now = SecondbotHelpers.UnixTimeNow(); foreach (KeyValuePair entry in ChatHistoryLastAccessed) { dif = now - entry.Value; if(dif > (7*60)) { needcleaning.Add(entry.Key); } } foreach(UUID a in needcleaning) { if(chatHistoryAI.ContainsKey(a)) { chatHistoryAI.Remove(a); } if(ChatHistoryLastAccessed.ContainsKey(a)) { ChatHistoryLastAccessed.Remove(a); } if (ChatRateLimiter.ContainsKey(a)) { ChatRateLimiter.Remove(a); } } } } protected async void GetAiReply(int ratelimiter, UUID replyTo, UUID user, string name, string message, bool avatarchat = false, bool groupchat = false) { bool allowedChat = true; lock(ChatRateLimiter) { if (ChatRateLimiter.ContainsKey(user) == false) { ChatRateLimiter.Add(user, 0); } long dif = SecondbotHelpers.UnixTimeNow() - ChatRateLimiter[user]; if(dif <= ratelimiter) { allowedChat = false; } } if(allowedChat == false) { return; } lock (ChatRateLimiter) { ChatRateLimiter[user] = SecondbotHelpers.UnixTimeNow() + 1; } List messages = new List(); lock (chatHistoryAI) lock (ChatHistoryLastAccessed) { if (chatHistoryAI.ContainsKey(user) == false) { chatHistoryAI.Add(user, new List>()); } if (ChatHistoryLastAccessed.ContainsKey(user) == false) { ChatHistoryLastAccessed.Add(user, SecondbotHelpers.UnixTimeNow()); } // trim history List> history = chatHistoryAI[user]; if (history.Count() == 0) { if (avatarchat == true) { history.Add(new KeyValuePair("system", "You are " + GetClient().Self.FirstName + ", A secondlife assistant. you are talking to " + name + ".")); } else if (groupchat == true) { history.Add(new KeyValuePair("system", "You are " + GetClient().Self.FirstName + ", A secondlife assistant. you are talking to a group of people.")); } else { history.Add(new KeyValuePair("system", "You are " + GetClient().Self.FirstName + ", A secondlife assistant. you are talking to people in a public place.")); } } history.Add(new KeyValuePair("user", "" + name + " says " + message)); while (history.Count() > chatHistorySize + 1) { history.RemoveAt(1); } chatHistoryAI[user] = history; ChatHistoryLastAccessed[user] = SecondbotHelpers.UnixTimeNow(); try { foreach (KeyValuePair entry in history) { string role = entry.Key.ToLower(); if (role != "system" && role != "user" && role != "assistant") { throw new ArgumentException("Invalid role provided in history. Role must be 'system', 'user', or 'assistant'."); } string thismessage = entry.Value; if(thismessage == null) { throw new ArgumentException("Message can not be empty"); } if (role == "system") messages.Add(ChatMessage.FromSystem(thismessage)); else if (role == "user") messages.Add(ChatMessage.FromUser(thismessage)); else if (role == "assistant") messages.Add(ChatMessage.FromAssistant(thismessage)); } } catch (Exception ex) { LogFormater.Warn("error building AI request:" + ex.Message); return; } } if(messages.Count() == 0) { LogFormater.Warn("No messages given"); return; } try { string replyMessage = ""; var openAiService = new OpenAIService(new OpenAiOptions() { ApiKey = myConfig.GetApiKey(), }); if((myConfig.GetOrganizationId() != null) && (myConfig.GetOrganizationId() != "none")) { openAiService = new OpenAIService(new OpenAiOptions() { ApiKey = myConfig.GetApiKey(), Organization = myConfig.GetOrganizationId(), }); } ChatCompletionCreateResponse completionResult = null; if (myConfig.GetUseModel() == "gpt3") { completionResult = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest { Messages = messages, Model = Models.Gpt_3_5_Turbo }); } else if (myConfig.GetUseModel() == "gpt-3.5-turbo") { completionResult = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest { Messages = messages, Model = Models.Gpt_3_5_Turbo }); } else if(myConfig.GetUseModel() == "gpt-4-mini") { completionResult = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest { Messages = messages, Model = Models.Gpt_4o_mini }); } else if (myConfig.GetUseModel() == "gpt-4-turbo") { completionResult = await openAiService.ChatCompletion.CreateCompletion(new ChatCompletionCreateRequest { Messages = messages, Model = Models.Gpt_4_turbo }); } if(completionResult == null) { return; } if (completionResult.Successful) { replyMessage = completionResult.Choices.First().Message.Content; } if (replyMessage != "") { lock (chatHistoryAI) lock (ChatHistoryLastAccessed) { List> history = chatHistoryAI[user]; history.Add(new KeyValuePair("assistant", replyMessage)); chatHistoryAI[user] = history; ChatHistoryLastAccessed[user] = SecondbotHelpers.UnixTimeNow(); } if (avatarchat == true) { GetClient().Self.InstantMessage(replyTo, replyMessage); } else if (groupchat == true) { GetClient().Self.InstantMessageGroup(replyTo, replyMessage); } else { GetClient().Self.Chat(replyMessage, 0, ChatType.Normal); } } } catch (Exception ex) { // Handle other potential errors Console.WriteLine("An error occurred: " + ex.Message); } } protected void BotClientRestart(object o, BotClientNotice e) { LogFormater.Info("ChatGpt [Attached to new client]"); GetClient().Network.LoggedOut += BotLoggedOut; GetClient().Network.SimConnected += BotLoggedIn; } protected void BotLoggedOut(object o, LoggedOutEventArgs e) { GetClient().Network.SimConnected += BotLoggedIn; LogFormater.Info("ChatGpt [Waiting for connect]"); } protected void BotLoggedIn(object o, SimConnectedEventArgs e) { GetClient().Network.SimConnected -= BotLoggedIn; GetClient().Self.IM += BotImMessage; GetClient().Self.ChatFromSimulator += BotLocalchat; LogFormater.Info("ChatGpt [accepting chat input]"); } } }