From 969d98adfe049299d0973e6c794008be9e75a2f4 Mon Sep 17 00:00:00 2001
From: Madpeter <57852827+Madpeterz@users.noreply.github.com>
Date: Wed, 29 Jan 2025 20:38:51 +0000
Subject: [PATCH] adds redis support to chat AI
---
Dockerfile | 22 +-
SecondBotEvents/Config/ChatGptConfig.cs | 67 +++++
SecondBotEvents/SecondBotEvents.csproj | 1 +
SecondBotEvents/Services/ChatGptService.cs | 317 +++++++++++++++++----
4 files changed, 345 insertions(+), 62 deletions(-)
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);
+ }
}
}
}