diff --git a/Dockerfile b/Dockerfile index 11e3362..d826d99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -101,7 +101,20 @@ ENV basic_Username='' \ rlv_HideStatusOutput='false' \ relay_count='0' \ relay_HideStatusOutput='false' \ - relay_UseShortEncoder='false' + relay_UseShortEncoder='false' \ + chatgpt_Enabled='false' \ + chatgpt_ApiKey='none' \ + chatgpt_OrganizationId='none' \ + chatgpt_AllowImReplys='false' \ + chatgpt_ImReplyFriendsOnly='true' \ + chatgpt_ImReplyRateLimiter='3' \ + chatgpt_AllowGroupReplys='false' \ + chatgpt_GroupReplyForGroup='false' \ + chatgpt_GroupReplyRateLimiter='3' \ + chatgpt_LocalchatReply='false' \ + chatgpt_LocalchatRateLimiter='3' \ + chatgpt_UseModel='gpt3' \ + chatgpt_ChatHistoryMessages='5' EXPOSE 80 diff --git a/SecondBotEvents/Config/ChatGptConfig.cs b/SecondBotEvents/Config/ChatGptConfig.cs new file mode 100644 index 0000000..0da1f3a --- /dev/null +++ b/SecondBotEvents/Config/ChatGptConfig.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SecondBotEvents.Config +{ + public class ChatGptConfig : Config + { + public ChatGptConfig(bool fromENV, string fromFolder = "") : base(fromENV, fromFolder) { } + protected override void MakeSettings() + { + filename = "chatgpt"; + settings.Add("Enabled"); + settings.Add("ApiKey"); + settings.Add("OrganizationId"); + settings.Add("AllowImReplys"); + settings.Add("ImReplyFriendsOnly"); + settings.Add("ImReplyRateLimiter"); + settings.Add("AllowGroupReplys"); + settings.Add("GroupReplyForGroup"); + settings.Add("GroupReplyRateLimiter"); + settings.Add("LocalchatReply"); + settings.Add("LocalchatRateLimiter"); + settings.Add("UseModel"); + settings.Add("ChatHistoryMessages"); + } + + public int GetImReplyRateLimiter() + { + return ReadSettingAsInt("ImReplyRateLimiter", 3); + } + public int GetGroupReplyRateLimiter() + { + return ReadSettingAsInt("GroupReplyRateLimiter", 3); + } + public int GetLocalchatRateLimiter() + { + return ReadSettingAsInt("LocalchatRateLimiter", 3); + } + public int GetChatHistoryMessages() + { + return ReadSettingAsInt("ChatHistoryMessages", 5); + } + public string GetUseModel() + { + return ReadSettingAsString("UseModel", "gpt3"); + } + public bool GetEnabled() + { + return ReadSettingAsBool("Enabled"); + } + public string GetApiKey() + { + return ReadSettingAsString("ApiKey","none"); + } + public string GetOrganizationId() + { + return ReadSettingAsString("OrganizationId","none"); + } + public bool GetAllowImReplys() + { + return ReadSettingAsBool("AllowImReplys"); + } + public bool GetImReplyFriendsOnly() + { + return ReadSettingAsBool("ImReplyFriendsOnly"); + } + public bool GetAllowGroupReplys() + { + return ReadSettingAsBool("AllowGroupReplys"); + } + public string GetGroupReplyForGroup() + { + return ReadSettingAsString("GroupReplyForGroup","none"); + } + public bool GetLocalchatReply() + { + return ReadSettingAsBool("LocalchatReply"); + } + } +} \ No newline at end of file diff --git a/SecondBotEvents/EventsSecondBot.cs b/SecondBotEvents/EventsSecondBot.cs index d567ca9..2ae1993 100644 --- a/SecondBotEvents/EventsSecondBot.cs +++ b/SecondBotEvents/EventsSecondBot.cs @@ -41,6 +41,8 @@ namespace SecondBotEvents public HomeboundService HomeboundService { get { return (HomeboundService)GetService("HomeboundService"); } } public EventsService EventsService { get { return (EventsService)GetService("EventsService"); } } public DialogService DialogService { get { return (DialogService)GetService("DialogService"); } } + + public ChatGptService ChatGptService { get { return (ChatGptService)GetService("ChatGptService"); } } public TriggerOnEventService TriggerOnEventService { get { return (TriggerOnEventService)GetService("TriggerOnEventService"); } } @@ -167,6 +169,7 @@ namespace SecondBotEvents RegisterService("CustomCommandsService"); RegisterService("TriggerOnEventService"); RegisterService("RelayService"); + RegisterService("ChatGptService"); if (BotClient.IsLoaded() == false) { LogFormater.Info("Config is not loaded :("); diff --git a/SecondBotEvents/SecondBotEvents.csproj b/SecondBotEvents/SecondBotEvents.csproj index 00c4e17..a234a8f 100644 --- a/SecondBotEvents/SecondBotEvents.csproj +++ b/SecondBotEvents/SecondBotEvents.csproj @@ -13,6 +13,7 @@ + diff --git a/SecondBotEvents/Services/ChatGptService.cs b/SecondBotEvents/Services/ChatGptService.cs new file mode 100644 index 0000000..e647978 --- /dev/null +++ b/SecondBotEvents/Services/ChatGptService.cs @@ -0,0 +1,399 @@ +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) + + } + + 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]"); + } + } +}