From 7a5c027f8f1ceee68323093be637ac66de9db886 Mon Sep 17 00:00:00 2001
From: Madpeter <57852827+Madpeterz@users.noreply.github.com>
Date: Fri, 30 Aug 2024 04:39:33 +0100
Subject: [PATCH] add support for chat GPT
---
Dockerfile | 15 +-
SecondBotEvents/Config/ChatGptConfig.cs | 81 +++++
SecondBotEvents/EventsSecondBot.cs | 3 +
SecondBotEvents/SecondBotEvents.csproj | 1 +
SecondBotEvents/Services/ChatGptService.cs | 399 +++++++++++++++++++++
5 files changed, 498 insertions(+), 1 deletion(-)
create mode 100644 SecondBotEvents/Config/ChatGptConfig.cs
create mode 100644 SecondBotEvents/Services/ChatGptService.cs
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]");
+ }
+ }
+}