diff --git a/src/NadekoBot/Common/Creds.cs b/src/NadekoBot/Common/Creds.cs index 9b4a48661..3c586584a 100644 --- a/src/NadekoBot/Common/Creds.cs +++ b/src/NadekoBot/Common/Creds.cs @@ -53,6 +53,9 @@ go to https://www.patreon.com/portal -> my clients -> create client")] [Comment(@"Official cleverbot api key.")] public string CleverbotApiKey { get; set; } + + [Comment(@"Official GPT-3 api key.")] + public string Gpt3ApiKey { get; set; } [Comment(@"Which cache implementation should bot use. 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset. @@ -118,7 +121,7 @@ Windows default public Creds() { - Version = 6; + Version = 7; Token = string.Empty; UsePrivilegedIntents = true; OwnerIds = new List(); @@ -128,6 +131,7 @@ Windows default Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty); BotListToken = string.Empty; CleverbotApiKey = string.Empty; + Gpt3ApiKey = string.Empty; BotCache = BotCacheImplemenation.Memory; RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password="; Db = new() diff --git a/src/NadekoBot/Common/IBotCredentials.cs b/src/NadekoBot/Common/IBotCredentials.cs index ab26b0fa4..a106deab6 100644 --- a/src/NadekoBot/Common/IBotCredentials.cs +++ b/src/NadekoBot/Common/IBotCredentials.cs @@ -14,6 +14,7 @@ public interface IBotCredentials int TotalShards { get; } Creds.PatreonSettings Patreon { get; } string CleverbotApiKey { get; } + string Gpt3ApiKey { get; } RestartConfig RestartCommand { get; } Creds.VotesSettings Votes { get; } string BotListToken { get; } diff --git a/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs b/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs index b6bbfe508..422925f4b 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs @@ -1,6 +1,7 @@ #nullable disable using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Db.Models; +using NadekoBot.Modules.Games.Common; using NadekoBot.Modules.Games.Common.ChatterBot; using NadekoBot.Modules.Permissions; using NadekoBot.Modules.Permissions.Common; @@ -27,6 +28,7 @@ public class ChatterBotService : IExecOnMessage private readonly IHttpClientFactory _httpFactory; private readonly IPatronageService _ps; private readonly CmdCdService _ccs; + private readonly GamesConfigService _gcs; public ChatterBotService( DiscordSocketClient client, @@ -38,7 +40,8 @@ public class ChatterBotService : IExecOnMessage IBotCredentials creds, IEmbedBuilderService eb, IPatronageService ps, - CmdCdService cmdCdService) + CmdCdService cmdCdService, + GamesConfigService gcs) { _client = client; _perms = perms; @@ -49,6 +52,7 @@ public class ChatterBotService : IExecOnMessage _httpFactory = factory; _ps = ps; _ccs = cmdCdService; + _gcs = gcs; _flKey = new FeatureLimitKey() { @@ -64,11 +68,26 @@ public class ChatterBotService : IExecOnMessage public IChatterBotSession CreateSession() { - if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey)) - return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory); + switch (_gcs.Data.ChatBot) + { + case ChatBotImplementation.Cleverbot: + if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey)) + return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory); - Log.Information("Cleverbot will not work as the api key is missing."); - return null; + Log.Information("Cleverbot will not work as the api key is missing."); + return null; + case ChatBotImplementation.Gpt3: + if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey)) + return new OfficialGpt3Session(_creds.Gpt3ApiKey, + _gcs.Data.ChatGpt.Model, + _gcs.Data.ChatGpt.MaxTokens, + _httpFactory); + + Log.Information("Gpt3 will not work as the api key is missing."); + return null; + default: + return null; + } } public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot) @@ -102,7 +121,7 @@ public class ChatterBotService : IExecOnMessage { if (guild is not SocketGuild sg) return false; - + try { var message = PrepareMessage(usrMsg, out var cbs); @@ -147,7 +166,7 @@ public class ChatterBotService : IExecOnMessage uint? monthly = quota.Quota is int mVal and >= 0 ? (uint)mVal : null; - + var maybeLimit = await _ps.TryIncrementQuotaCounterAsync(sg.OwnerId, sg.OwnerId == usrMsg.Author.Id, FeatureType.Limit, @@ -155,7 +174,7 @@ public class ChatterBotService : IExecOnMessage null, daily, monthly); - + if (maybeLimit.TryPickT1(out var ql, out var counters)) { if (ql.Quota == 0) @@ -166,7 +185,7 @@ public class ChatterBotService : IExecOnMessage "In order to use the cleverbot feature, the owner of this server should be [Patron Tier X](https://patreon.com/join/nadekobot) on patreon.", footer: "You may disable the cleverbot feature, and this message via '.cleverbot' command"); - + return true; } @@ -174,7 +193,7 @@ public class ChatterBotService : IExecOnMessage null!, $"You've reached your quota limit of **{ql.Quota}** responses {ql.QuotaPeriod.ToFullName()} for the cleverbot feature.", footer: "You may wait for the quota reset or ."); - + return true; } } @@ -185,7 +204,7 @@ public class ChatterBotService : IExecOnMessage title: null, response.SanitizeMentions(true) // , footer: counter > 0 ? counter.ToString() : null - ); + ); Log.Information(@"CleverBot Executed Server: {GuildName} [{GuildId}] diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/ChatterBotResponse.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/ChatterBotResponse.cs deleted file mode 100644 index 3836f34e2..000000000 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/ChatterBotResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable disable -namespace NadekoBot.Modules.Games.Common.ChatterBot; - -public class ChatterBotResponse -{ - public string Convo_id { get; set; } - public string BotSay { get; set; } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/Gpt3Response.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/Gpt3Response.cs new file mode 100644 index 000000000..7ec0c6186 --- /dev/null +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/Gpt3Response.cs @@ -0,0 +1,30 @@ +#nullable disable +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Games.Common.ChatterBot; + +public class Gpt3Response +{ + [JsonPropertyName("choices")] + public Choice[] Choices { get; set; } +} + +public class Choice +{ + public string Text { get; set; } +} + +public class Gpt3ApiRequest +{ + [JsonPropertyName("model")] + public string Model { get; init; } + + [JsonPropertyName("prompt")] + public string Prompt { 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/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialGpt3Session.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialGpt3Session.cs new file mode 100644 index 000000000..24eb2db98 --- /dev/null +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialGpt3Session.cs @@ -0,0 +1,69 @@ +#nullable disable +using Newtonsoft.Json; +using System.Net.Http.Json; + +namespace NadekoBot.Modules.Games.Common.ChatterBot; + +public class OfficialGpt3Session : IChatterBotSession +{ + private string Uri + => $"https://api.openai.com/v1/completions"; + + private readonly string _apiKey; + private readonly string _model; + private readonly int _maxTokens; + private readonly IHttpClientFactory _httpFactory; + + public OfficialGpt3Session( + string apiKey, + Gpt3Model model, + int maxTokens, + IHttpClientFactory factory) + { + _apiKey = apiKey; + _httpFactory = factory; + switch (model) + { + case Gpt3Model.Ada001: + _model = "text-ada-001"; + break; + case Gpt3Model.Babbage001: + _model = "text-babbage-001"; + break; + case Gpt3Model.Curie001: + _model = "text-curie-001"; + break; + case Gpt3Model.Davinci003: + _model = "text-davinci-003"; + break; + } + + _maxTokens = maxTokens; + } + + public async Task Think(string input) + { + 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, + Temperature = 1, + }); + var dataString = await data.Content.ReadAsStringAsync(); + try + { + var response = JsonConvert.DeserializeObject(dataString); + + return response?.Choices[0]?.Text; + } + catch + { + Log.Warning("Unexpected GPT-3 response received: {ResponseString}", dataString); + return null; + } + } +} + diff --git a/src/NadekoBot/Modules/Games/GamesConfig.cs b/src/NadekoBot/Modules/Games/GamesConfig.cs index a597d934c..68f620b6b 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; } + public int Version { get; set; } = 2; [Comment("Hangman related settings (.hangman command)")] public HangmanConfig Hangman { get; set; } = new() @@ -95,6 +95,27 @@ public sealed partial class GamesConfig : ICloneable Name = "Unicorn" } }; + + [Comment(@"Which chatbot API should bot use. +'cleverbot' - bot will use Cleverbot API. +'gpt3' - bot will use GPT-3 API")] + public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3; + + public ChatGptConfig ChatGpt { get; set; } = new(); +} + +[Cloneable] +public sealed partial class ChatGptConfig +{ + [Comment(@"Which GPT-3 Model should bot use. +'ada' - cheapest and fastest +'babbage' - 2nd option +'curie' - 3rd option +'davinci' - Most expensive, slowest")] + public Gpt3Model Model { get; set; } = Gpt3Model.Ada001; + + [Comment(@"The maximum number of tokens to use per GPT-3 API call")] + public int MaxTokens { get; set; } = 100; } [Cloneable] @@ -120,4 +141,18 @@ public sealed partial class RaceAnimal { public string Icon { get; set; } public string Name { get; set; } +} + +public enum ChatBotImplementation +{ + Cleverbot, + Gpt3 +} + +public enum Gpt3Model +{ + Ada001, + Babbage001, + Curie001, + Davinci003 } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/GamesConfigService.cs b/src/NadekoBot/Modules/Games/GamesConfigService.cs index f03c7e15a..690a92e0b 100644 --- a/src/NadekoBot/Modules/Games/GamesConfigService.cs +++ b/src/NadekoBot/Modules/Games/GamesConfigService.cs @@ -28,6 +28,20 @@ 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, + ConfigParsers.InsensitiveEnum, + ConfigPrinters.ToString); + AddParsedProp("gpt.max_tokens", + gs => gs.ChatGpt.MaxTokens, + int.TryParse, + ConfigPrinters.ToString, + val => val > 0); Migrate(); } @@ -45,5 +59,14 @@ public sealed class GamesConfigService : ConfigServiceBase }; }); } + + if (data.Version < 2) + { + ModifyConfig(c => + { + c.Version = 2; + c.ChatBot = ChatBotImplementation.Cleverbot; + }); + } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/GamesService.cs b/src/NadekoBot/Modules/Games/GamesService.cs index 1159a253e..435d052d5 100644 --- a/src/NadekoBot/Modules/Games/GamesService.cs +++ b/src/NadekoBot/Modules/Games/GamesService.cs @@ -23,7 +23,6 @@ public class GamesService : INService, IReadyExecutor //channelId, game public ConcurrentDictionary AcrophobiaGames { get; } = new(); - public ConcurrentDictionary RunningTrivias { get; } = new(); public Dictionary TicTacToeGames { get; } = new(); public ConcurrentDictionary RunningContests { get; } = new(); public ConcurrentDictionary NunchiGames { get; } = new(); diff --git a/src/NadekoBot/Services/Impl/BotCredsProvider.cs b/src/NadekoBot/Services/Impl/BotCredsProvider.cs index 2243666ec..a7dcd3d15 100644 --- a/src/NadekoBot/Services/Impl/BotCredsProvider.cs +++ b/src/NadekoBot/Services/Impl/BotCredsProvider.cs @@ -34,19 +34,19 @@ public sealed class BotCredsProvider : IBotCredsProvider public BotCredsProvider(int? totalShards = null, string credPath = null) { - _totalShards = totalShards; - - if (!string.IsNullOrWhiteSpace(credPath)) - { - CredsPath = credPath; - CredsExamplePath = Path.Combine(Path.GetDirectoryName(credPath), CREDS_EXAMPLE_FILE_NAME); - } - else - { - CredsPath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME); - CredsExamplePath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME); + _totalShards = totalShards; + + if (!string.IsNullOrWhiteSpace(credPath)) + { + CredsPath = credPath; + CredsExamplePath = Path.Combine(Path.GetDirectoryName(credPath), CREDS_EXAMPLE_FILE_NAME); } - + else + { + CredsPath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_FILE_NAME); + CredsExamplePath = Path.Combine(Directory.GetCurrentDirectory(), CREDS_EXAMPLE_FILE_NAME); + } + try { if (!File.Exists(CredsExamplePath)) @@ -69,8 +69,8 @@ public sealed class BotCredsProvider : IBotCredsProvider _config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true) .AddEnvironmentVariables("NadekoBot_") - .Build(); - + .Build(); + _changeToken = ChangeToken.OnChange(() => _config.GetReloadToken(), Reload); Reload(); } @@ -131,14 +131,14 @@ public sealed class BotCredsProvider : IBotCredsProvider ymlData = Yaml.Serializer.Serialize(creds); File.WriteAllText(CREDS_FILE_NAME, ymlData); - } - + } + private string OldCredsJsonPath => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json"); private string OldCredsJsonBackupPath - => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak"); - + => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak"); + private void MigrateCredentials() { if (File.Exists(OldCredsJsonPath)) @@ -177,15 +177,18 @@ public sealed class BotCredsProvider : IBotCredsProvider Log.Warning( "Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness"); - } - + } + if (File.Exists(CREDS_FILE_NAME)) { var creds = Yaml.Deserializer.Deserialize(File.ReadAllText(CREDS_FILE_NAME)); if (creds.Version <= 5) { - creds.Version = 6; creds.BotCache = BotCacheImplemenation.Redis; + } + if (creds.Version <= 6) + { + creds.Version = 7; File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); } } diff --git a/src/NadekoBot/creds_example.yml b/src/NadekoBot/creds_example.yml index 8b2b8d803..37642330d 100644 --- a/src/NadekoBot/creds_example.yml +++ b/src/NadekoBot/creds_example.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 6 +version: 7 # Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/ token: '' # List of Ids of the users who have bot owner permissions @@ -56,6 +56,8 @@ patreon: botListToken: '' # Official cleverbot api key. cleverbotApiKey: '' +# Official GPT-3 api key. +gpt3ApiKey: '' # Which cache implementation should bot use. # 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset. # 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 3be9458ef..50736c1dd 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -807,6 +807,7 @@ hentaibomb: - hentaibomb cleverbot: - cleverbot + - chatgpt shorten: - shorten wikia: diff --git a/src/NadekoBot/data/games.yml b/src/NadekoBot/data/games.yml index 0261304a0..eefbc952d 100644 --- a/src/NadekoBot/data/games.yml +++ b/src/NadekoBot/data/games.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 1 +version: 2 # Hangman related settings (.hangman command) hangman: # The amount of currency awarded to the winner of a hangman game @@ -8,8 +8,8 @@ hangman: trivia: # The amount of currency awarded to the winner of the trivia game. currencyReward: 0 - # 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. minimumWinReq: 1 # List of responses for the .8ball command. A random one will be selected every time eightBallResponses: @@ -54,3 +54,17 @@ raceAnimals: name: Crab - icon: "🦄" name: Unicorn +# Which chatbot API should bot use. +# 'cleverbot' - bot will use Cleverbot API. +# 'gpt3' - bot will use GPT-3 API +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 + # The maximum number of tokens to use per GPT-3 API call + maxTokens: 100 diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index 17e932f80..08970cc7c 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -1377,7 +1377,7 @@ listservers: args: - "3" cleverbot: - desc: "Toggles cleverbot session. When enabled, the bot will reply to messages starting with bot mention in the server. Expressions starting with %bot.mention% won't work if cleverbot is enabled." + desc: "Toggles cleverbot/chatgpt session. When enabled, the bot will reply to messages starting with bot mention in the server. Expressions starting with %bot.mention% won't work if cleverbot/chatgpt is enabled." args: - "" shorten: