diff --git a/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs b/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs index a8c06d8..2129d3b 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/ChatterbotService.cs @@ -1,6 +1,5 @@ #nullable disable using EllieBot.Common.ModuleBehaviors; -using EllieBot.Db.Models; using EllieBot.Modules.Games.Common; using EllieBot.Modules.Games.Common.ChatterBot; using EllieBot.Modules.Patronage; @@ -58,18 +57,21 @@ public class ChatterBotService : IExecOnMessage Log.Information("Cleverbot will not work as the api key is missing"); return null; - case ChatBotImplementation.Gpt: + case ChatBotImplementation.OpenAi: + var data = _gcs.Data; if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey)) - return new OfficialGptSession(_creds.Gpt3ApiKey, - _gcs.Data.ChatGpt.ModelName, - _gcs.Data.ChatGpt.ChatHistory, - _gcs.Data.ChatGpt.MaxTokens, - _gcs.Data.ChatGpt.MinTokens, - _gcs.Data.ChatGpt.PersonalityPrompt, + return new OpenAiApiSession( + data.ChatGpt.ApiUrl, + _creds.Gpt3ApiKey, + data.ChatGpt.ModelName, + data.ChatGpt.ChatHistory, + data.ChatGpt.MaxTokens, + data.ChatGpt.MinTokens, + data.ChatGpt.PersonalityPrompt, _client.CurrentUser.Username, _httpFactory); - Log.Information("Gpt3 will not work as the api key is missing"); + Log.Information("Openai Api will likely not work as the api key is missing"); return null; default: return null; @@ -88,15 +90,15 @@ public class ChatterBotService : IExecOnMessage public string PrepareMessage(IUserMessage msg) { - var ellieId = _client.CurrentUser.Id; - var normalMention = $"<@{ellieId}> "; - var nickMention = $"<@!{ellieId}> "; + var nadekoId = _client.CurrentUser.Id; + var normalMention = $"<@{nadekoId}> "; + var nickMention = $"<@!{nadekoId}> "; string message; if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture)) message = msg.Content[normalMention.Length..].Trim(); else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture)) message = msg.Content[nickMention.Length..].Trim(); - else if (msg.ReferencedMessage?.Author.Id == ellieId) + else if (msg.ReferencedMessage?.Author.Id == nadekoId) message = msg.Content; else return null; diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs new file mode 100644 index 0000000..db71eee --- /dev/null +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/Choice.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Games.Common.ChatterBot; + +public class Choice +{ + [JsonPropertyName("message")] + public Message Message { get; init; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/Gpt3Response.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/Gpt3Response.cs deleted file mode 100644 index e983338..0000000 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/Gpt3Response.cs +++ /dev/null @@ -1,61 +0,0 @@ -#nullable disable -using System.Text.Json.Serialization; - -namespace EllieBot.Modules.Games.Common.ChatterBot; - -public class OpenAiCompletionResponse -{ - [JsonPropertyName("choices")] - public Choice[] Choices { get; set; } - - [JsonPropertyName("usage")] - public OpenAiUsageData Usage { get; set; } -} - -public class OpenAiUsageData -{ - [JsonPropertyName("prompt_tokens")] - public int PromptTokens { get; set; } - - [JsonPropertyName("completion_tokens")] - public int CompletionTokens { get; set; } - - [JsonPropertyName("total_tokens")] - public int TotalTokens { get; set; } -} - -public class Choice -{ - [JsonPropertyName("message")] - public Message Message { get; init; } -} - -public class Message { - [JsonPropertyName("content")] - public string Content { get; init; } -} - -public class Gpt3ApiRequest -{ - [JsonPropertyName("model")] - public string Model { get; init; } - - [JsonPropertyName("messages")] - public List Messages { get; init; } - - [JsonPropertyName("temperature")] - public int Temperature { get; init; } - - [JsonPropertyName("max_tokens")] - public int MaxTokens { get; init; } -} - -public class GPTMessage -{ - [JsonPropertyName("role")] - public string Role {get; init;} - [JsonPropertyName("content")] - public string Content {get; init;} - [JsonPropertyName("name")] - public string Name {get; init;} -} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs new file mode 100644 index 0000000..04532f5 --- /dev/null +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/Message.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Games.Common.ChatterBot; + +public class Message +{ + [JsonPropertyName("content")] + public string Content { get; init; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs new file mode 100644 index 0000000..0fdaf71 --- /dev/null +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiMessage.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Games.Common.ChatterBot; + +public class OpenAiApiMessage +{ + [JsonPropertyName("role")] + public string Role { get; init; } + + [JsonPropertyName("content")] + public string Content { get; init; } + + [JsonPropertyName("name")] + public string Name { get; init; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs new file mode 100644 index 0000000..1ea5d69 --- /dev/null +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiRequest.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Games.Common.ChatterBot; + +public class OpenAiApiRequest +{ + [JsonPropertyName("model")] + public string Model { get; init; } + + [JsonPropertyName("messages")] + public List Messages { get; init; } + + [JsonPropertyName("temperature")] + public int Temperature { get; init; } + + [JsonPropertyName("max_tokens")] + public int MaxTokens { get; init; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiUsageData.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiUsageData.cs new file mode 100644 index 0000000..1525dac --- /dev/null +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiApiUsageData.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Games.Common.ChatterBot; + +public class OpenAiApiUsageData +{ + [JsonPropertyName("prompt_tokens")] + public int PromptTokens { get; set; } + + [JsonPropertyName("completion_tokens")] + public int CompletionTokens { get; set; } + + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiCompletionResponse.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiCompletionResponse.cs new file mode 100644 index 0000000..1b7bdcf --- /dev/null +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApi/OpenAiCompletionResponse.cs @@ -0,0 +1,13 @@ +#nullable disable +using System.Text.Json.Serialization; + +namespace EllieBot.Modules.Games.Common.ChatterBot; + +public class OpenAiCompletionResponse +{ + [JsonPropertyName("choices")] + public Choice[] Choices { get; set; } + + [JsonPropertyName("usage")] + public OpenAiApiUsageData Usage { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/OfficialGptSession.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApiSession.cs similarity index 69% rename from src/EllieBot/Modules/Games/ChatterBot/_common/OfficialGptSession.cs rename to src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApiSession.cs index e822b53..4511988 100644 --- a/src/EllieBot/Modules/Games/ChatterBot/_common/OfficialGptSession.cs +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/OpenAiApiSession.cs @@ -1,77 +1,78 @@ #nullable disable using Newtonsoft.Json; using OneOf.Types; -using System.Net.Http.Json; using SharpToken; -using System.CodeDom; +using System.Net.Http.Json; using System.Text.RegularExpressions; namespace EllieBot.Modules.Games.Common.ChatterBot; -public partial class OfficialGptSession : IChatterBotSession +public partial class OpenAiApiSession : IChatterBotSession { - private string Uri - => $"https://api.openai.com/v1/chat/completions"; - + private readonly string _baseUrl; private readonly string _apiKey; private readonly string _model; private readonly int _maxHistory; private readonly int _maxTokens; private readonly int _minTokens; - private readonly string _ellieUsername; + private readonly string _nadekoUsername; private readonly GptEncoding _encoding; - private List messages = new(); + private List messages = new(); private readonly IHttpClientFactory _httpFactory; - public OfficialGptSession( + public OpenAiApiSession( + string url, string apiKey, - ChatGptModel model, + string model, int chatHistory, int maxTokens, int minTokens, string personality, - string ellieUsername, + string nadekoUsername, IHttpClientFactory factory) { - _apiKey = apiKey; - _httpFactory = factory; - - _model = model switch + if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _)) { - ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo", - ChatGptModel.Gpt4o => "gpt-4o", - _ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model)) - }; + throw new ArgumentException("Invalid OpenAi api url provided", nameof(url)); + } + _baseUrl = url.TrimEnd('/'); + + _apiKey = apiKey; + _model = model; + _httpFactory = factory; _maxHistory = chatHistory; _maxTokens = maxTokens; _minTokens = minTokens; - _ellieUsername = UsernameCleaner().Replace(ellieUsername, ""); - _encoding = GptEncoding.GetEncodingForModel(_model); - messages.Add(new() + _nadekoUsername = UsernameCleaner().Replace(nadekoUsername, ""); + _encoding = GptEncoding.GetEncodingForModel("gpt-4o"); + if (!string.IsNullOrWhiteSpace(personality)) { - Role = "system", - Content = personality, - Name = _ellieUsername - }); + messages.Add(new() + { + Role = "system", + Content = personality, + Name = _nadekoUsername + }); + } } [GeneratedRegex("[^a-zA-Z0-9_-]")] private static partial Regex UsernameCleaner(); - + public async Task>> Think(string input, string username) { username = UsernameCleaner().Replace(username, ""); - + messages.Add(new() { Role = "user", Content = input, Name = username }); - + while (messages.Count > _maxHistory + 2) { messages.RemoveAt(1); @@ -92,28 +93,29 @@ public partial class OfficialGptSession : IChatterBotSession } else { - return new Error("Token count exceeded, please increase the number of tokens in the bot config and restart."); + return new Error( + "Token count exceeded, please increase the number of tokens in the bot config and restart."); } } using var http = _httpFactory.CreateClient(); http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey); - - var data = await http.PostAsJsonAsync(Uri, - new Gpt3ApiRequest() + + var data = await http.PostAsJsonAsync($"{_baseUrl}/v1/chat/completions", + new OpenAiApiRequest() { Model = _model, Messages = messages, MaxTokens = _maxTokens - tokensUsed, Temperature = 1, }); - + var dataString = await data.Content.ReadAsStringAsync(); try { var response = JsonConvert.DeserializeObject(dataString); - - Log.Information("Received response: {response} ", dataString); + + // Log.Information("Received response: {Response} ", dataString); var res = response?.Choices?[0]; var message = res?.Message?.Content; @@ -121,14 +123,14 @@ public partial class OfficialGptSession : IChatterBotSession { return new Error("ChatGpt: Received no response."); } - + messages.Add(new() { Role = "assistant", Content = message, - Name = _ellieUsername + Name = _nadekoUsername }); - + return new ThinkResult() { Text = message, @@ -142,11 +144,4 @@ public partial class OfficialGptSession : IChatterBotSession return new Error("Unexpected response received"); } } -} - -public sealed class ThinkResult -{ - public string Text { get; set; } - public int TokensIn { get; set; } - public int TokensOut { get; set; } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/ChatterBot/_common/ThinkResponse.cs b/src/EllieBot/Modules/Games/ChatterBot/_common/ThinkResponse.cs new file mode 100644 index 0000000..a5b0b5f --- /dev/null +++ b/src/EllieBot/Modules/Games/ChatterBot/_common/ThinkResponse.cs @@ -0,0 +1,11 @@ +#nullable disable +using System.CodeDom; + +namespace EllieBot.Modules.Games.Common.ChatterBot; + +public sealed class ThinkResult +{ + public string Text { get; set; } + public int TokensIn { get; set; } + public int TokensOut { get; set; } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/GamesConfig.cs b/src/EllieBot/Modules/Games/GamesConfig.cs index e56cc70..9308329 100644 --- a/src/EllieBot/Modules/Games/GamesConfig.cs +++ b/src/EllieBot/Modules/Games/GamesConfig.cs @@ -8,7 +8,7 @@ namespace EllieBot.Modules.Games.Common; public sealed partial class GamesConfig : ICloneable { [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 4; + public int Version { get; set; } = 5; [Comment("Hangman related settings (.hangman command)")] public HangmanConfig Hangman { get; set; } = new() @@ -103,10 +103,13 @@ public sealed partial class GamesConfig : ICloneable } ]; - [Comment(@"Which chatbot API should bot use. -'cleverbot' - bot will use Cleverbot API. -'gpt' - bot will use GPT API")] - public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt; + [Comment( + """ + Which chatbot API should bot use. + 'cleverbot' - bot will use Cleverbot API. + 'openai' - bot will use OpenAi API + """)] + public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.OpenAi; public ChatGptConfig ChatGpt { get; set; } = new(); } @@ -114,19 +117,38 @@ public sealed partial class GamesConfig : ICloneable [Cloneable] public sealed partial class ChatGptConfig { - [Comment(@"Which GPT Model should bot use. - gpt35turbo - cheapest - gpt4o - more expensive, higher quality -")] - public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo; + [Comment(""" + Url to any openai api compatible url. + Make sure to modify the modelName appropriately + DO NOT add /v1/chat/completions suffix to the url + """)] + public string ApiUrl { get; set; } = "https://api.openai.com"; - [Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")] - public string PersonalityPrompt { get; set; } = "You are a chat bot willing to have a conversation with anyone about anything."; + [Comment(""" + Which GPT Model should bot use. + gpt-3.5-turbo - cheapest + gpt-4o - more expensive, higher quality - [Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")] + If you are using another openai compatible api, you may use any of the models supported by that api + """)] + public string ModelName { get; set; } = "gpt-3.5-turbo"; + + [Comment(""" + How should the chatbot behave, what's its personality? + This will be sent as a system message. + Usage of this counts towards the max tokens. + """)] + public string PersonalityPrompt { get; set; } = + "You are a chat bot willing to have a conversation with anyone about anything."; + + [Comment( + """ + The maximum number of messages in a conversation that can be remembered. + This will increase the number of tokens used. + """)] public int ChatHistory { get; set; } = 5; - [Comment(@"The maximum number of tokens to use per GPT API call")] + [Comment(@"The maximum number of tokens to use per OpenAi API call")] public int MaxTokens { get; set; } = 100; [Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")] @@ -147,9 +169,9 @@ public sealed partial class TriviaConfig public long CurrencyReward { get; set; } [Comment(""" - Users won't be able to start trivia games which have - a smaller win requirement than the one specified by this setting. - """)] + Users won't be able to start trivia games which have + a smaller win requirement than the one specified by this setting. + """)] public int MinimumWinReq { get; set; } = 1; } @@ -163,18 +185,11 @@ public sealed partial class RaceAnimal public enum ChatBotImplementation { Cleverbot, + OpenAi = 1, + + [Obsolete] Gpt = 1, + [Obsolete] Gpt3 = 1, -} - -public enum ChatGptModel -{ - [Obsolete] - Gpt4, - [Obsolete] - Gpt432k, - - Gpt35Turbo, - Gpt4o, } \ No newline at end of file diff --git a/src/EllieBot/Modules/Games/GamesConfigService.cs b/src/EllieBot/Modules/Games/GamesConfigService.cs index 06b75c3..f6ff635 100644 --- a/src/EllieBot/Modules/Games/GamesConfigService.cs +++ b/src/EllieBot/Modules/Games/GamesConfigService.cs @@ -32,29 +32,21 @@ public sealed class GamesConfigService : ConfigServiceBase gs => gs.ChatBot, ConfigParsers.InsensitiveEnum, ConfigPrinters.ToString); + + AddParsedProp("gpt.apiUrl", + gs => gs.ChatGpt.ApiUrl, + ConfigParsers.String, + ConfigPrinters.ToString); + AddParsedProp("gpt.modelName", gs => gs.ChatGpt.ModelName, - ConfigParsers.InsensitiveEnum, + ConfigParsers.String, ConfigPrinters.ToString); + AddParsedProp("gpt.personality", gs => gs.ChatGpt.PersonalityPrompt, ConfigParsers.String, ConfigPrinters.ToString); - AddParsedProp("gpt.chathistory", - gs => gs.ChatGpt.ChatHistory, - int.TryParse, - ConfigPrinters.ToString, - val => val > 0); - AddParsedProp("gpt.max_tokens", - gs => gs.ChatGpt.MaxTokens, - int.TryParse, - ConfigPrinters.ToString, - val => val > 0); - AddParsedProp("gpt.min_tokens", - gs => gs.ChatGpt.MinTokens, - int.TryParse, - ConfigPrinters.ToString, - val => val > 0); Migrate(); } @@ -78,7 +70,7 @@ public sealed class GamesConfigService : ConfigServiceBase ModifyConfig(c => { c.Version = 3; - c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo; + c.ChatGpt.ModelName = "gpt35turbo"; }); } @@ -89,11 +81,40 @@ public sealed class GamesConfigService : ConfigServiceBase c.Version = 4; #pragma warning disable CS0612 // Type or member is obsolete c.ChatGpt.ModelName = - c.ChatGpt.ModelName == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k - ? ChatGptModel.Gpt4o - : c.ChatGpt.ModelName; + c.ChatGpt.ModelName.Equals("gpt4", StringComparison.OrdinalIgnoreCase) + || c.ChatGpt.ModelName.Equals("gpt432k", StringComparison.OrdinalIgnoreCase) + ? "gpt-4o" + : "gpt-3.5-turbo"; #pragma warning restore CS0612 // Type or member is obsolete }); } + + if (data.Version < 5) + { + ModifyConfig(c => + { + c.Version = 5; + c.ChatBot = c.ChatBot == ChatBotImplementation.OpenAi + ? ChatBotImplementation.OpenAi + : c.ChatBot; + + if (c.ChatGpt.ModelName.Equals("gpt4o", StringComparison.OrdinalIgnoreCase)) + { + c.ChatGpt.ModelName = "gpt-4o"; + } + else if (c.ChatGpt.ModelName.Equals("gpt35turbo", StringComparison.OrdinalIgnoreCase)) + { + c.ChatGpt.ModelName = "gpt-3.5-turbo"; + } + else + { + Log.Warning( + "Unknown OpenAI api model name: {ModelName}. " + + "It will be reset to 'gpt-3.5-turbo' only this time", + c.ChatGpt.ModelName); + c.ChatGpt.ModelName = "gpt-3.5-turbo"; + } + }); + } } } \ No newline at end of file