From afba004d850f6fdcf6c813f4fc1570eed6176900 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Tue, 16 Jan 2024 09:12:14 +0000 Subject: [PATCH] Update GPT ChatterBot * Updates endpoint from v1/completions to v1/chat/completions * Add SharpTokens as a library to calculate input token usage * Subtract input tokens from max_tokens to ensure the API tokens don't exceed the max specified * Add Chat history support since this API supports it * Add a personality prompt to tweak the way the bot behaves * Add a min_tokens config to increase the quality of chat messages when history is enabled * Adjust the response function to throw an exception so that a null message isn't added to the list. --- .../Games/ChatterBot/ChatterbotService.cs | 8 ++- .../Games/ChatterBot/_Common/Gpt3Response.cs | 22 +++++- .../ChatterBot/_Common/IChatterBotSession.cs | 2 +- .../_Common/OfficialCleverbotSession.cs | 2 +- .../ChatterBot/_Common/OfficialGpt3Session.cs | 72 ++++++++++++++----- src/NadekoBot/Modules/Games/GamesConfig.cs | 29 +++++--- .../Modules/Games/GamesConfigService.cs | 30 ++++++-- src/NadekoBot/NadekoBot.csproj | 1 + src/NadekoBot/data/games.yml | 21 +++--- 9 files changed, 140 insertions(+), 47 deletions(-) diff --git a/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs b/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs index 422925f4b..5962a27d2 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs @@ -79,8 +79,12 @@ public class ChatterBotService : IExecOnMessage case ChatBotImplementation.Gpt3: if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey)) return new OfficialGpt3Session(_creds.Gpt3ApiKey, - _gcs.Data.ChatGpt.Model, + _gcs.Data.ChatGpt.ModelName, + _gcs.Data.ChatGpt.ChatHistory, _gcs.Data.ChatGpt.MaxTokens, + _gcs.Data.ChatGpt.MinTokens, + _gcs.Data.ChatGpt.PersonalityPrompt, + _client.CurrentUser.Username, _httpFactory); Log.Information("Gpt3 will not work as the api key is missing."); @@ -199,7 +203,7 @@ public class ChatterBotService : IExecOnMessage } _ = channel.TriggerTypingAsync(); - var response = await cbs.Think(message); + var response = await cbs.Think(message, usrMsg.Author.ToString()); await channel.SendConfirmAsync(_eb, title: null, response.SanitizeMentions(true) diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/Gpt3Response.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/Gpt3Response.cs index 7ec0c6186..80c24c1d3 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/Gpt3Response.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/Gpt3Response.cs @@ -11,7 +11,13 @@ public class Gpt3Response public class Choice { - public string Text { get; set; } + [JsonPropertyName("message")] + public Message Message { get; init; } +} + +public class Message { + [JsonPropertyName("content")] + public string Content { get; init; } } public class Gpt3ApiRequest @@ -19,12 +25,22 @@ public class Gpt3ApiRequest [JsonPropertyName("model")] public string Model { get; init; } - [JsonPropertyName("prompt")] - public string Prompt { 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/NadekoBot/Modules/Games/ChatterBot/_Common/IChatterBotSession.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/IChatterBotSession.cs index 5fff26978..15a93406d 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/IChatterBotSession.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/IChatterBotSession.cs @@ -3,5 +3,5 @@ namespace NadekoBot.Modules.Games.Common.ChatterBot; public interface IChatterBotSession { - Task Think(string input); + Task Think(string input, string username); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs index 73ed93bd2..71979d2f0 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs @@ -18,7 +18,7 @@ public class OfficialCleverbotSession : IChatterBotSession _httpFactory = factory; } - public async Task Think(string input) + public async Task Think(string input, string username) { using var http = _httpFactory.CreateClient(); var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? "")); diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialGpt3Session.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialGpt3Session.cs index 24eb2db98..1fa28d4a1 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialGpt3Session.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialGpt3Session.cs @@ -1,63 +1,101 @@ #nullable disable using Newtonsoft.Json; using System.Net.Http.Json; +using SharpToken; +using Antlr.Runtime; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace NadekoBot.Modules.Games.Common.ChatterBot; public class OfficialGpt3Session : IChatterBotSession { private string Uri - => $"https://api.openai.com/v1/completions"; + => $"https://api.openai.com/v1/chat/completions"; private readonly string _apiKey; private readonly string _model; + private readonly int _maxHistory; private readonly int _maxTokens; + private readonly int _minTokens; + private readonly string _nadekoUsername; + private readonly GptEncoding _encoding; + private List messages = new(); private readonly IHttpClientFactory _httpFactory; + + public OfficialGpt3Session( string apiKey, - Gpt3Model model, + ChatGptModel model, + int chatHistory, int maxTokens, + int minTokens, + string personality, + string nadekoUsername, IHttpClientFactory factory) { _apiKey = apiKey; _httpFactory = factory; switch (model) { - case Gpt3Model.Ada001: - _model = "text-ada-001"; + case ChatGptModel.Gpt35Turbo: + _model = "gpt-3.5-turbo"; break; - case Gpt3Model.Babbage001: - _model = "text-babbage-001"; + case ChatGptModel.Gpt4: + _model = "gpt-4"; break; - case Gpt3Model.Curie001: - _model = "text-curie-001"; - break; - case Gpt3Model.Davinci003: - _model = "text-davinci-003"; + case ChatGptModel.Gpt432k: + _model = "gpt-4-32k"; break; } - + _maxHistory = chatHistory; _maxTokens = maxTokens; + _minTokens = minTokens; + _nadekoUsername = nadekoUsername; + _encoding = GptEncoding.GetEncodingForModel(_model); + messages.Add(new GPTMessage(){Role = "user", Content = personality, Name = _nadekoUsername}); } - public async Task Think(string input) + public async Task Think(string input, string username) { + messages.Add(new GPTMessage(){Role = "user", Content = input, Name = username}); + while(messages.Count > _maxHistory + 2){ + messages.RemoveAt(1); + } + int tokensUsed = 0; + foreach(GPTMessage message in messages){ + tokensUsed += _encoding.Encode(message.Content).Count; + } + tokensUsed *= 2; //Unsure why this is the case, but the token count chatgpt reports back is double what I calculate. + //check if we have the minimum number of tokens available to use. Remove messages until we have enough, otherwise exit out and inform the user why. + while(_maxTokens - tokensUsed <= _minTokens){ + if(messages.Count > 2){ + int tokens = _encoding.Encode(messages[1].Content).Count * 2; + tokensUsed -= tokens; + messages.RemoveAt(1); + } + else{ + return "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() { Model = _model, - Prompt = input, - MaxTokens = _maxTokens, + Messages = messages, + MaxTokens = _maxTokens - tokensUsed, Temperature = 1, }); var dataString = await data.Content.ReadAsStringAsync(); try { var response = JsonConvert.DeserializeObject(dataString); - - return response?.Choices[0]?.Text; + string message = response?.Choices[0]?.Message?.Content; + //Can't rely on the return to except, now that we need to add it to the messages list. + _ = message ?? throw new ArgumentNullException(nameof(message)); + messages.Add(new GPTMessage(){Role = "assistant", Content = message, Name = _nadekoUsername}); + return message; } catch { diff --git a/src/NadekoBot/Modules/Games/GamesConfig.cs b/src/NadekoBot/Modules/Games/GamesConfig.cs index db8144648..5537f7c8f 100644 --- a/src/NadekoBot/Modules/Games/GamesConfig.cs +++ b/src/NadekoBot/Modules/Games/GamesConfig.cs @@ -8,7 +8,7 @@ namespace NadekoBot.Modules.Games.Common; public sealed partial class GamesConfig : ICloneable { [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 2; + public int Version { get; set; } = 3; [Comment("Hangman related settings (.hangman command)")] public HangmanConfig Hangman { get; set; } = new() @@ -108,14 +108,22 @@ public sealed partial class GamesConfig : ICloneable public sealed partial class ChatGptConfig { [Comment(@"Which GPT-3 Model should bot use. -'ada001' - cheapest and fastest -'babbage001' - 2nd option -'curie001' - 3rd option -'davinci003' - Most expensive, slowest")] - public Gpt3Model Model { get; set; } = Gpt3Model.Ada001; + gpt35turbo - cheapest + gpt4 - 30x more expensive, higher quality + gp432k - same model as above, but with a 32k token limit")] + public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo; + + [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(@"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-3 API call")] public int MaxTokens { get; set; } = 100; + + [Comment(@"The minimum number of tokens to use per GPT-3 API call, such that chat history is removed to make room.")] + public int MinTokens { get; set; } = 30; } [Cloneable] @@ -149,10 +157,9 @@ public enum ChatBotImplementation Gpt3 } -public enum Gpt3Model +public enum ChatGptModel { - Ada001, - Babbage001, - Curie001, - Davinci003 + Gpt35Turbo, + Gpt4, + Gpt432k } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/GamesConfigService.cs b/src/NadekoBot/Modules/Games/GamesConfigService.cs index 690a92e0b..4f08b6106 100644 --- a/src/NadekoBot/Modules/Games/GamesConfigService.cs +++ b/src/NadekoBot/Modules/Games/GamesConfigService.cs @@ -28,20 +28,33 @@ public sealed class GamesConfigService : ConfigServiceBase long.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("chatbot", gs => gs.ChatBot, ConfigParsers.InsensitiveEnum, ConfigPrinters.ToString); - AddParsedProp("gpt.model", - gs => gs.ChatGpt.Model, + AddParsedProp("gpt.modelName", + gs => gs.ChatGpt.ModelName, ConfigParsers.InsensitiveEnum, 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(); } @@ -65,7 +78,16 @@ public sealed class GamesConfigService : ConfigServiceBase ModifyConfig(c => { c.Version = 2; - c.ChatBot = ChatBotImplementation.Cleverbot; + c.ChatBot = ChatBotImplementation.Cleverbot; + }); + } + + if (data.Version < 3) + { + ModifyConfig(c => + { + c.Version = 3; + c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo; }); } } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index ea32dd82c..fdd220e1a 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -58,6 +58,7 @@ + diff --git a/src/NadekoBot/data/games.yml b/src/NadekoBot/data/games.yml index eefbc952d..5d0c369f8 100644 --- a/src/NadekoBot/data/games.yml +++ b/src/NadekoBot/data/games.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 2 +version: 3 # Hangman related settings (.hangman command) hangman: # The amount of currency awarded to the winner of a hangman game @@ -57,14 +57,19 @@ raceAnimals: # Which chatbot API should bot use. # 'cleverbot' - bot will use Cleverbot API. # 'gpt3' - bot will use GPT-3 API -chatBot: gpt3 +chatBot: Gpt3 chatGpt: - # Which GPT-3 Model should bot use. - # 'ada001' - cheapest and fastest - # 'babbage001' - 2nd option - # 'curie001' - 3rd option - # 'davinci003' - Most expensive, slowest - model: davinci003 +# Which GPT-3 Model should bot use. + # gpt35turbo - cheapest + # gpt4 - 30x more expensive, higher quality + # gp432k - same model as above, but with a 32k token limit + modelName: Gpt35Turbo + # How should the chat bot behave, whats its personality? (Usage of this counts towards the max tokens) + personalityPrompt: You are a chat bot willing to have a conversation with anyone about anything. + # The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used) + chatHistory: 5 # The maximum number of tokens to use per GPT-3 API call maxTokens: 100 + # The minimum number of tokens to use per GPT-3 API call, such that chat history is removed to make room. + minTokens: 30 \ No newline at end of file