diff --git a/Dockerfile b/Dockerfile index 3e96bfa..194de4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -89,7 +89,7 @@ ENV basic_Username='' \ interaction_JsonOutputEventsTarget='none' \ interaction_FriendRequestLevel='Owner' \ interaction_InventoryTransferLevel='Owner' \ - interaction_GroupInviteLevel=Owner'' \ + interaction_GroupInviteLevel='Owner' \ interaction_TeleportRequestLevel='Owner' \ commands_HideStatusOutput='false' \ datastore_HideStatusOutput='false' \ @@ -104,24 +104,36 @@ ENV basic_Username='' \ relay_HideStatusOutput='false' \ relay_UseShortEncoder='false' \ chatgpt_Enabled='false' \ + chatgpt_FakeTypeDelay='true' \ chatgpt_ApiKey='none' \ chatgpt_OrganizationId='none' \ chatgpt_AllowImReplys='false' \ chatgpt_ImReplyFriendsOnly='true' \ - chatgpt_ImReplyRateLimiter='3' \ chatgpt_AllowGroupReplys='false' \ chatgpt_GroupReplyForGroup='none' \ - chatgpt_GroupReplyRateLimiter='3' \ chatgpt_LocalchatReply='false' \ - chatgpt_LocalchatRateLimiter='3' \ chatgpt_UseModel='gpt-3.5-turbo' \ chatgpt_ChatHistoryMessages='5' \ chatgpt_ChatPrompt='respond as if you are a horse that knows its going to the glue factory and you are upset about this fact' \ chatgpt_Provider='openai' \ chatgtp_ShowDebug='false' \ + chatgpt_ImReplyRateLimiter='3' \ + chatgpt_GroupReplyRateLimiter='3' \ + chatgpt_LocalchatRateLimiter='3' \ + chatgpt_ChatHistoryTimeout='15' \ + chatgpt_CustomName='' \ + chatgpt_UseRedis='false' \ + chatgpt_RedisSource='127.0.0.1:6379' \ + chatgpt_RedisPrefix='sbot' \ + chatgpt_RedisLocalchat='false' \ + chatgpt_RedisGroupchat='false' \ + chatgpt_RedisImchat='true' \ + chatgpt_RedisMaxageMins='120' \ + chatgpt_RedisCountLocal='60' \ + chatgpt_RedisCountGroup='60' \ + chatgpt_RedisCountIm='60' \ onevent_Enabled='false' \ onevent_Count=0 \ - chatgpt_ChatHistoryTimeout='15' \ onevent_HideStatusOutput='false' diff --git a/SecondBotEvents/Config/ChatGptConfig.cs b/SecondBotEvents/Config/ChatGptConfig.cs index b0c8e0b..dbe61c8 100644 --- a/SecondBotEvents/Config/ChatGptConfig.cs +++ b/SecondBotEvents/Config/ChatGptConfig.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection.Metadata.Ecma335; using System.Text; namespace SecondBotEvents.Config @@ -28,6 +29,72 @@ namespace SecondBotEvents.Config settings.Add("ShowDebug"); settings.Add("Provider"); settings.Add("CustomName"); + settings.Add("FakeTypeDelay"); + settings.Add("UseRedis"); + settings.Add("RedisSource"); + settings.Add("RedisPrefix"); + settings.Add("RedisLocalchat"); + settings.Add("RedisGroupchat"); + settings.Add("RedisImchat"); + settings.Add("RedisMaxageMins"); + settings.Add("RedisCountLocal"); + settings.Add("RedisCountGroup"); + settings.Add("RedisCountIm"); + } + + public bool GetFakeTypeDelay() + { + return ReadSettingAsBool("FakeTypeDelay", true); + } + + public int GetRedisCountIm() + { + return ReadSettingAsInt("RedisCountIm", 60); + } + + public int GetRedisCountLocal() + { + return ReadSettingAsInt("RedisCountLocal", 60); + } + + public int GetRedisCountGroup() + { + return ReadSettingAsInt("RedisCountGroup", 60); + } + + public bool GetUseRedis() + { + return ReadSettingAsBool("UseRedis", false); + } + + public int GetRedisMaxageMins() + { + return ReadSettingAsInt("RedisMaxageMins", 120); + } + + public string GetRedisSource() + { + return ReadSettingAsString("RedisSource", "127.0.0.1:6379"); + } + + public string GetRedisPrefix() + { + return ReadSettingAsString("RedisPrefix", "sbot"); + } + + public bool GetRedisLocalchat() + { + return ReadSettingAsBool("RedisLocalchat", false); + } + + public bool GetRedisImchat() + { + return ReadSettingAsBool("RedisImchat", true); + } + + public bool GetRedisGroupchat() + { + return ReadSettingAsBool("RedisGroupchat", false); } public string GetCustomName() diff --git a/SecondBotEvents/SecondBotEvents.csproj b/SecondBotEvents/SecondBotEvents.csproj index 2be1c0a..932dc2a 100644 --- a/SecondBotEvents/SecondBotEvents.csproj +++ b/SecondBotEvents/SecondBotEvents.csproj @@ -22,6 +22,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/SecondBotEvents/Services/ChatGptService.cs b/SecondBotEvents/Services/ChatGptService.cs index d47c2cc..13fe73e 100644 --- a/SecondBotEvents/Services/ChatGptService.cs +++ b/SecondBotEvents/Services/ChatGptService.cs @@ -20,12 +20,14 @@ using System.Threading.Tasks; using OpenAI.Interfaces; using System.Diagnostics; using Swan; +using StackExchange.Redis; +using Discord.Rest; namespace SecondBotEvents.Services { public class ChatGptService : BotServices { - public ChatGptConfig myConfig = null; + public new ChatGptConfig myConfig = null; public ChatGptService(EventsSecondBot setMaster) : base(setMaster) { myConfig = new ChatGptConfig(master.fromEnv, master.fromFolder); @@ -39,6 +41,9 @@ namespace SecondBotEvents.Services protected int localchatRateLimit = 3; protected int groupchatRateLimit = 3; protected int imchatRateLimit = 3; + + protected ConnectionMultiplexer redis = null; + protected IDatabase redisDb = null; public override void Start(bool updateEnabled = false, bool setEnabledTo = false) { if (updateEnabled == true) @@ -50,6 +55,23 @@ namespace SecondBotEvents.Services Stop(); return; } + if(myConfig.GetUseRedis() == true) + { + try + { + ConfigurationOptions configRedis = new ConfigurationOptions() + { + EndPoints = { myConfig.GetRedisSource() } + }; + redis = ConnectionMultiplexer.Connect(configRedis); + redisDb = redis.GetDatabase(); + } + catch (Exception ex) + { + LogFormater.Warn("Redis failed to connect:" + ex.Message); + } + + } chatHistorySize = inrange(3,10,myConfig.GetChatHistoryMessages()); localchatRateLimit = inrange(1, 10, myConfig.GetLocalchatRateLimiter()); groupchatRateLimit = inrange(1, 10, myConfig.GetGroupReplyRateLimiter()); @@ -252,6 +274,221 @@ namespace SecondBotEvents.Services } } + protected bool RedisLive() + { + // are we using redis? + if (myConfig.GetUseRedis() == false) + { + return false; // not using redis no need to do any more checks + } + else if (redis == null) + { + return false; // redis is dead + } + else if (redisDb == null) + { + return false; // no connection to DB + } + else if (redis.IsConnected == false) + { + // redis is down, use local storage + return false; + } + return true; + } + + protected List> GetHistoryFromStorage(UUID store,bool avatarchat,bool groupchat) + { + // are we using redis? + if (RedisLive() == false) + { + return null; + } + // is redis enabled for this message type? + if ((myConfig.GetRedisImchat() == true) && (avatarchat == true)) + { + // we need to read from redis for this IM chat window + return ReadChatFromRedis(store); + } + else if ((myConfig.GetRedisGroupchat() == true) && (groupchat == true)) + { + // we need to read from redis for this group chat window + return ReadChatFromRedis(store); + } + else if(myConfig.GetRedisLocalchat() == true) + { + // we need to read from redis for this local chat window + return ReadChatFromRedis(store); + } + // Redis is not enabled for this message type, use local storage + return null; + + } + + protected List> ReadChatFromRedis(UUID store) + { + try + { + RedisKey readkey = new RedisKey(myConfig.GetRedisPrefix() + store.Guid.ToString()); + if (redisDb.KeyExists(readkey) == false) + { + string rawstring = redisDb.StringGet(readkey); + if (rawstring == null) + { + return chatHistoryAI[store]; // nothing in memory use the default store + } + redisDb.KeyExpire(readkey, new TimeSpan(0, myConfig.GetRedisMaxageMins(), 0)); // update the expire value + return JsonConvert.DeserializeObject>>(rawstring); + } + return NoRedisStoreFound(store); + } + catch (Exception ex) + { + LogFormater.Warn("Redis failed to unpack json using default store:" + ex.Message); + return NoRedisStoreFound(store); + } + } + + protected List> NoRedisStoreFound(UUID store) + { + if (chatHistoryAI.ContainsKey(store) == false) + { + return new List>(); + } + return chatHistoryAI[store]; + } + + protected List> GetHistory(UUID store, bool avatarchat, bool groupchat, string talkingto) + { + // update access markers + if (ChatHistoryLastAccessed.ContainsKey(store) == false) + { + ChatHistoryLastAccessed.Add(store, 0); + } + ChatHistoryLastAccessed[store] = SecondbotHelpers.UnixTimeNow(); + List> history = GetHistoryFromStorage(store, avatarchat, groupchat); + if(history.Count == 0) + { + // no history loaded from redis, check in memory + if (chatHistoryAI.ContainsKey(store) == false) + { + // totaly new chat build it out + chatHistoryAI.Add(store, new List>()); + string yourname = GetClient().Self.FirstName; + if (myConfig.GetCustomName() != "") + { + yourname = myConfig.GetCustomName(); + } + if (avatarchat == true) + { + history.Add(new KeyValuePair("system", "You are " + yourname + ", " + myConfig.GetChatPrompt() + ", you are talking to " + talkingto + ".")); + } + else if (groupchat == true) + { + history.Add(new KeyValuePair("system", "You are " + yourname + ", " + myConfig.GetChatPrompt() + ", you are talking to a group of people.")); + } + else + { + history.Add(new KeyValuePair("system", "You are " + yourname + ", " + myConfig.GetChatPrompt() + ", you are talking to people in a public place.")); + } + // history ready, save to redis if in use + if(StoreHistory(store, history, avatarchat, groupchat) == false) + { + // unable to save to redis, use local storage + chatHistoryAI[store] = history; + return history; + } + } + return history; + } + return history; + } + + protected bool StoreHistory(UUID store, List> history, bool avatarchat, bool groupchat) + { + if (RedisLive() == false) + { + return false; + } + bool allowStore = false; + if ((myConfig.GetRedisImchat() == true) && (avatarchat == true)) + { + allowStore = true; + } + else if ((myConfig.GetRedisGroupchat() == true) && (groupchat == true)) + { + // we need to read from redis for this group chat window + allowStore = true; + } + else if (myConfig.GetRedisLocalchat() == true) + { + // we need to read from redis for this local chat window + allowStore = true; + } + if(allowStore == false) + { + return false; + } + int maxsize = inrange(1, 9999, myConfig.GetRedisCountLocal()); + if(avatarchat == true) + { + maxsize = inrange(1, 9999, myConfig.GetRedisCountIm()); + } + else if(groupchat == true) + { + maxsize = inrange(1, 9999, myConfig.GetRedisCountGroup()); + } + // trim the history using targeted max values + while (history.Count() > maxsize + 1) + { + history.RemoveAt(1); + } + RedisKey writekey = new RedisKey(myConfig.GetRedisPrefix() + store.Guid.ToString()); + try + { + string savestring = JsonConvert.SerializeObject(history); + if (savestring == null) + { + LogFormater.Warn("Redis failed to convert history into savable format"); + return false; + } + RedisValue storeme = new RedisValue(savestring); + if(redisDb.StringSet(writekey, storeme) == true) + { + return redisDb.KeyExpire(writekey, new TimeSpan(0, myConfig.GetRedisMaxageMins(), 0)); + } + return false; + } + catch (Exception ex) + { + LogFormater.Warn("Redis failed to save into store:" + ex.Message); + return false; + } + } + + protected List> AddHistory(UUID store, bool avatarchat, bool groupchat, string talkingto, string role, string message) + { + List> history = GetHistory(store, avatarchat, groupchat, talkingto); + history.Add(new KeyValuePair(role, message)); + if(StoreHistory(store, history, avatarchat, groupchat) == false) // try and save using redis + { + // unable to save to redis, write to local storage + return BasicSaveHistory(store, history); + } + return history; + } + + protected List> BasicSaveHistory(UUID store, List> history) + { + // trim the history using basic config + while (history.Count() > chatHistorySize + 1) + { + history.RemoveAt(1); + } + chatHistoryAI[store] = history; + return history; + } + protected async void GetAiReply(int ratelimiter, UUID replyTo, UUID user, string name, string message, bool avatarchat = false, bool groupchat = false) { bool allowedChat = true; @@ -278,47 +515,10 @@ namespace SecondBotEvents.Services 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]; - string yourname = GetClient().Self.FirstName; - if(myConfig.GetCustomName() != "") - { - yourname = myConfig.GetCustomName(); - } - if (history.Count() == 0) - { - if (avatarchat == true) - { - history.Add(new KeyValuePair("system", "You are " + yourname + ", "+myConfig.GetChatPrompt()+", you are talking to " + name + ".")); - } - else if (groupchat == true) - { - history.Add(new KeyValuePair("system", "You are " + yourname + ", "+myConfig.GetChatPrompt()+", you are talking to a group of people.")); - } - else - { - history.Add(new KeyValuePair("system", "You are " + yourname + ", "+myConfig.GetChatPrompt()+", 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(); - + // convert history into the AI format while adding the new message try { - foreach (KeyValuePair entry in history) + foreach (KeyValuePair entry in AddHistory(user, avatarchat, groupchat, name, "user", "" + name + " says " + message)) { string role = entry.Key.ToLower(); if (role != "system" && role != "user" && role != "assistant") @@ -403,28 +603,28 @@ namespace SecondBotEvents.Services { lock (chatHistoryAI) lock (ChatHistoryLastAccessed) { - List> history = chatHistoryAI[user]; - history.Add(new KeyValuePair("assistant", replyMessage)); - chatHistoryAI[user] = history; - ChatHistoryLastAccessed[user] = SecondbotHelpers.UnixTimeNow(); + AddHistory(user, avatarchat, groupchat, name, "assistant", replyMessage); } - if((avatarchat==false) && (groupchat == false)) + if (myConfig.GetFakeTypeDelay() == true) { - GetClient().Self.AnimationStart(Animations.TYPE, true); - } + if ((avatarchat == false) && (groupchat == false)) + { + GetClient().Self.AnimationStart(Animations.TYPE, true); + } - double timespanwait = EstimateTypingTime(replyMessage, 0.4); + double timespanwait = EstimateTypingTime(replyMessage, 0.4); - if(timespanwait > 3) - { - timespanwait = 3; + if (timespanwait > 3) + { + timespanwait = 3; + } + else if (timespanwait < 1) + { + timespanwait = 1; + } + await Task.Delay(TimeSpan.FromSeconds(timespanwait)); } - else if(timespanwait < 1) - { - timespanwait = 1; - } - await Task.Delay(TimeSpan.FromSeconds(timespanwait)); if (avatarchat == true) { GetClient().Self.InstantMessage(replyTo, replyMessage); @@ -436,7 +636,10 @@ namespace SecondBotEvents.Services else { GetClient().Self.Chat(replyMessage, 0, ChatType.Normal); - GetClient().Self.AnimationStop(Animations.TYPE, true); + if (myConfig.GetFakeTypeDelay() == true) + { + GetClient().Self.AnimationStop(Animations.TYPE, true); + } } } }