From b49b21ccde929c8a7771ba87bdc252cf45a2721b Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sat, 16 Sep 2023 15:54:09 +0200 Subject: [PATCH 01/13] Remove unused GuildSettingsSerivce methods --- Kattbot/CommandModules/AdminModule.cs | 31 +++------------- Kattbot/Services/GuildSettingsService.cs | 46 ------------------------ 2 files changed, 5 insertions(+), 72 deletions(-) diff --git a/Kattbot/CommandModules/AdminModule.cs b/Kattbot/CommandModules/AdminModule.cs index b60da64..a4988fd 100644 --- a/Kattbot/CommandModules/AdminModule.cs +++ b/Kattbot/CommandModules/AdminModule.cs @@ -1,4 +1,6 @@ -using DSharpPlus.CommandsNext; +using System; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; using DSharpPlus.CommandsNext.Attributes; using DSharpPlus.Entities; using Kattbot.Attributes; @@ -7,12 +9,11 @@ using Kattbot.Helpers; using Kattbot.Services; using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; namespace Kattbot.CommandModules { - [BaseCommandCheck, RequireOwner] + [BaseCommandCheck] + [RequireOwner] [Group("admin")] [ModuleLifespan(ModuleLifespan.Transient)] public class AdminModule : BaseCommandModule @@ -93,27 +94,5 @@ public async Task SetBotChannel(CommandContext ctx, DiscordChannel channel) await ctx.RespondAsync($"Set bot channel to #{channel.Name}"); } - - [Command("set-kattgpt-channel")] - public async Task SetKattGptChannel(CommandContext ctx, DiscordChannel channel) - { - var channelId = channel.Id; - var guildId = channel.GuildId!.Value; - - await _guildSettingsService.SetKattGptChannel(guildId, channelId); - - await ctx.RespondAsync($"Set KattGpt channel to #{channel.Name}"); - } - - [Command("set-kattgptish-channel")] - public async Task SetKattGptishChannel(CommandContext ctx, DiscordChannel channel) - { - var channelId = channel.Id; - var guildId = channel.GuildId!.Value; - - await _guildSettingsService.SetKattGptishChannel(guildId, channelId); - - await ctx.RespondAsync($"Set KattGptish channel to #{channel.Name}"); - } } } diff --git a/Kattbot/Services/GuildSettingsService.cs b/Kattbot/Services/GuildSettingsService.cs index 22b10ab..c261a71 100644 --- a/Kattbot/Services/GuildSettingsService.cs +++ b/Kattbot/Services/GuildSettingsService.cs @@ -44,50 +44,4 @@ public async Task SetBotChannel(ulong guildId, ulong channelId) return parsed ? result : null; } - - public async Task SetKattGptChannel(ulong guildId, ulong channelId) - { - await _guildSettingsRepo.SaveGuildSetting(guildId, KattGptChannel, channelId.ToString()); - - var cacheKey = SharedCache.KattGptChannel(guildId); - - _cache.FlushCache(cacheKey); - } - - public async Task GetKattGptChannelId(ulong guildId) - { - var cacheKey = SharedCache.KattGptChannel(guildId); - - var channelId = await _cache.LoadFromCacheAsync( - cacheKey, - async () => await _guildSettingsRepo.GetGuildSetting(guildId, KattGptChannel), - TimeSpan.FromMinutes(60)); - - var parsed = ulong.TryParse(channelId, out var result); - - return parsed ? result : null; - } - - public async Task SetKattGptishChannel(ulong guildId, ulong channelId) - { - await _guildSettingsRepo.SaveGuildSetting(guildId, KattGptishChannel, channelId.ToString()); - - var cacheKey = SharedCache.KattGptishChannel(guildId); - - _cache.FlushCache(cacheKey); - } - - public async Task GetKattGptishChannelId(ulong guildId) - { - var cacheKey = SharedCache.KattGptishChannel(guildId); - - var channelId = await _cache.LoadFromCacheAsync( - cacheKey, - async () => await _guildSettingsRepo.GetGuildSetting(guildId, KattGptishChannel), - TimeSpan.FromMinutes(60)); - - var parsed = ulong.TryParse(channelId, out var result); - - return parsed ? result : null; - } } From 8101986559c3a06234dd9ce00fe21f043a1372ee Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 17 Sep 2023 01:22:58 +0200 Subject: [PATCH 02/13] Some refactoring and stuff --- Kattbot/Attributes/BaseCommandCheck.cs | 24 +-- .../EmoteStats/GetEmoteStats.cs | 10 +- .../CommandHandlers/Images/DallifyImage.cs | 2 +- Kattbot/CommandHandlers/Images/PetImage.cs | 2 +- .../CommandHandlers/Images/TransformImage.cs | 2 +- Kattbot/CommandModules/AdminModule.cs | 69 ++++++++- Kattbot/CommandModules/StatsCommandModule.cs | 4 +- Kattbot/EventHandlers/CommandEventHandler.cs | 73 ++++----- Kattbot/Helpers/DiscordExtensions.cs | 47 ++++-- Kattbot/Kattbot.csproj | 6 +- .../KattGptMessageHandler.cs | 142 +++--------------- Kattbot/Program.cs | 1 + Kattbot/Services/DiscordErrorLogger.cs | 11 +- Kattbot/Services/KattGpt/KattGptService.cs | 142 ++++++++++++++++++ Kattbot/Workers/BotWorker.cs | 10 +- 15 files changed, 331 insertions(+), 214 deletions(-) create mode 100644 Kattbot/Services/KattGpt/KattGptService.cs diff --git a/Kattbot/Attributes/BaseCommandCheck.cs b/Kattbot/Attributes/BaseCommandCheck.cs index 4766d93..f8c1f52 100644 --- a/Kattbot/Attributes/BaseCommandCheck.cs +++ b/Kattbot/Attributes/BaseCommandCheck.cs @@ -1,36 +1,22 @@ -using DSharpPlus.CommandsNext; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; using DSharpPlus.CommandsNext.Attributes; using DSharpPlus.Entities; -using Kattbot; -using Kattbot.Services; -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Kattbot.Attributes { /// - /// Reject commands coming from DM + /// Reject commands coming from DM. /// public class BaseCommandCheck : CheckBaseAttribute { public override Task ExecuteCheckAsync(CommandContext ctx, bool help) { - var message = ctx.Message; var channel = ctx.Channel; - if (IsPrivateMessageChannel(channel)) - return Task.FromResult(false); + bool allowCommand = !channel.IsPrivate; - return Task.FromResult(true); - } - - private bool IsPrivateMessageChannel(DiscordChannel channel) - { - return channel.IsPrivate; + return Task.FromResult(allowCommand); } } } diff --git a/Kattbot/CommandHandlers/EmoteStats/GetEmoteStats.cs b/Kattbot/CommandHandlers/EmoteStats/GetEmoteStats.cs index 4ad67e7..02d6ac3 100644 --- a/Kattbot/CommandHandlers/EmoteStats/GetEmoteStats.cs +++ b/Kattbot/CommandHandlers/EmoteStats/GetEmoteStats.cs @@ -88,25 +88,25 @@ public async Task Handle(GetEmoteStatsRequest request, CancellationToken cancell // Resolve display names foreach (ExtendedEmoteUser? emoteUser in extendedEmoteUsers) { - DiscordMember user; + DiscordMember member; if (ctx.Guild.Members.ContainsKey(emoteUser.UserId)) { - user = ctx.Guild.Members[emoteUser.UserId]; + member = ctx.Guild.Members[emoteUser.UserId]; } else { try { - user = await ctx.Guild.GetMemberAsync(emoteUser.UserId); + member = await ctx.Guild.GetMemberAsync(emoteUser.UserId); } catch { - user = null!; + member = null!; } } - emoteUser.DisplayName = user != null ? user.GetNicknameOrUsername() : "Unknown user"; + emoteUser.DisplayName = member?.DisplayName ?? "Unknown user"; } result.AppendLine(); diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index b77f7d9..8a969f7 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -122,7 +122,7 @@ public async Task Handle(DallifyUserRequest request, CancellationToken cancellat using var imageStream = imageStreamResult.MemoryStream; var fileExtension = imageStreamResult.FileExtension; - var imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); + var imageFilename = userAsMember.DisplayName.ToSafeFilename(fileExtension); DiscordMessageBuilder mb = new DiscordMessageBuilder() .AddFile(imageFilename, imageStream) diff --git a/Kattbot/CommandHandlers/Images/PetImage.cs b/Kattbot/CommandHandlers/Images/PetImage.cs index bec3904..fcf93e9 100644 --- a/Kattbot/CommandHandlers/Images/PetImage.cs +++ b/Kattbot/CommandHandlers/Images/PetImage.cs @@ -106,7 +106,7 @@ public async Task Handle(PetUserRequest request, CancellationToken cancellationT using var imageStream = imageStreamResult.MemoryStream; var fileExtension = imageStreamResult.FileExtension; - var imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); + var imageFilename = userAsMember.DisplayName.ToSafeFilename(fileExtension); var responseBuilder = new DiscordMessageBuilder(); diff --git a/Kattbot/CommandHandlers/Images/TransformImage.cs b/Kattbot/CommandHandlers/Images/TransformImage.cs index af659af..4f7fc05 100644 --- a/Kattbot/CommandHandlers/Images/TransformImage.cs +++ b/Kattbot/CommandHandlers/Images/TransformImage.cs @@ -113,7 +113,7 @@ public async Task Handle(TransformImageUserRequest request, CancellationToken ca using var imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; - string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); + string imageFilename = userAsMember.DisplayName.ToSafeFilename(fileExtension); var responseBuilder = new DiscordMessageBuilder(); diff --git a/Kattbot/CommandModules/AdminModule.cs b/Kattbot/CommandModules/AdminModule.cs index a4988fd..da31d2a 100644 --- a/Kattbot/CommandModules/AdminModule.cs +++ b/Kattbot/CommandModules/AdminModule.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using System.Threading.Tasks; using DSharpPlus.CommandsNext; using DSharpPlus.CommandsNext.Attributes; @@ -8,6 +9,7 @@ using Kattbot.Data.Repositories; using Kattbot.Helpers; using Kattbot.Services; +using Kattbot.Services.KattGpt; using Microsoft.Extensions.Logging; namespace Kattbot.CommandModules @@ -21,15 +23,21 @@ public class AdminModule : BaseCommandModule private readonly ILogger _logger; private readonly BotUserRolesRepository _botUserRolesRepo; private readonly GuildSettingsService _guildSettingsService; + private readonly KattGptChannelCache _cache; + private readonly KattGptService _kattGptService; public AdminModule( ILogger logger, BotUserRolesRepository botUserRolesRepo, - GuildSettingsService guildSettingsService) + GuildSettingsService guildSettingsService, + KattGptChannelCache cache, + KattGptService kattGptService) { _logger = logger; _botUserRolesRepo = botUserRolesRepo; _guildSettingsService = guildSettingsService; + _cache = cache; + _kattGptService = kattGptService; } @@ -45,10 +53,10 @@ await ctx.Guild.CurrentMember.ModifyAsync((props) => } [Command("add-friend")] - public async Task AddFriend(CommandContext ctx, DiscordMember user) + public async Task AddFriend(CommandContext ctx, DiscordMember member) { - var userId = user.Id; - var username = user.GetNicknameOrUsername(); + var userId = member.Id; + var username = member.DisplayName; var friendRole = BotRoleType.Friend; var hasRole = await _botUserRolesRepo.UserHasRole(userId, friendRole); @@ -65,10 +73,10 @@ public async Task AddFriend(CommandContext ctx, DiscordMember user) } [Command("remove-friend")] - public async Task RemoveFriend(CommandContext ctx, DiscordMember user) + public async Task RemoveFriend(CommandContext ctx, DiscordMember member) { - var userId = user.Id; - var username = user.GetNicknameOrUsername(); + var userId = member.Id; + var username = member.DisplayName; var friendRole = BotRoleType.Friend; var hasRole = await _botUserRolesRepo.UserHasRole(userId, friendRole); @@ -94,5 +102,52 @@ public async Task SetBotChannel(CommandContext ctx, DiscordChannel channel) await ctx.RespondAsync($"Set bot channel to #{channel.Name}"); } + + [Command("dump-prompts")] + public async Task DumpPrompts(CommandContext ctx, DiscordChannel channel) + { + var systemPromptsMessages = _kattGptService.BuildSystemPromptsMessages(channel); + var tokenCount = _kattGptService.GetTokenCount(systemPromptsMessages); + + var sb = new StringBuilder($"System prompt messages. Context size {tokenCount} tokens"); + sb.AppendLine(); + + foreach (var message in systemPromptsMessages) + { + sb.AppendLine(); + sb.AppendLine($"> {message.Content}"); + } + + await ctx.RespondAsync(sb.ToString()); + } + + [Command("dump-context")] + public async Task DumpContext(CommandContext ctx, DiscordChannel channel) + { + var cacheKey = KattGptChannelCache.KattGptChannelCacheKey(channel.Id); + + var boundedMessageQueue = _cache.GetCache(cacheKey); + + if (boundedMessageQueue == null) + { + await ctx.RespondAsync("No prompts found"); + return; + } + + var contextMessages = boundedMessageQueue.GetAll(); + + var tokenCount = _kattGptService.GetTokenCount(contextMessages); + + var sb = new StringBuilder($"Chat messages. Context size: {tokenCount} tokens"); + sb.AppendLine(); + + foreach (var message in contextMessages) + { + sb.AppendLine($"{message.Role}:"); + sb.AppendLine($"> {message.Content}"); + } + + await ctx.RespondAsync(sb.ToString()); + } } } diff --git a/Kattbot/CommandModules/StatsCommandModule.cs b/Kattbot/CommandModules/StatsCommandModule.cs index cb42908..fb4950f 100644 --- a/Kattbot/CommandModules/StatsCommandModule.cs +++ b/Kattbot/CommandModules/StatsCommandModule.cs @@ -87,7 +87,7 @@ public Task GetBestEmotesSelf(CommandContext ctx, [RemainingText] StatsCommandAr ulong userId = ctx.User.Id; - string mention = ctx.User.GetNicknameOrUsername(); + string mention = ctx.User.GetDisplayName(); return GetBestEmotesUser(ctx, userId, mention, args.Page, args.Interval); } @@ -100,7 +100,7 @@ public Task GetBestEmotesOtherUser(CommandContext ctx, DiscordUser user, [Remain ulong userId = user.Id; - string mention = user.GetNicknameOrUsername(); + string mention = user.GetDisplayName(); return GetBestEmotesUser(ctx, userId, mention, args.Page, args.Interval); } diff --git a/Kattbot/EventHandlers/CommandEventHandler.cs b/Kattbot/EventHandlers/CommandEventHandler.cs index 060e80f..d16e706 100644 --- a/Kattbot/EventHandlers/CommandEventHandler.cs +++ b/Kattbot/EventHandlers/CommandEventHandler.cs @@ -1,4 +1,6 @@ -using DSharpPlus.CommandsNext; +using System; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; using DSharpPlus.CommandsNext.Attributes; using DSharpPlus.CommandsNext.Exceptions; using DSharpPlus.Entities; @@ -7,8 +9,6 @@ using Kattbot.Services; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System; -using System.Threading.Tasks; namespace Kattbot.EventHandlers { @@ -23,8 +23,7 @@ public CommandEventHandler( IOptions options, ILogger logger, DiscordErrorLogger discordErrorLogger, - GuildSettingsService guildSettingsService - ) + GuildSettingsService guildSettingsService) { _options = options.Value; _logger = logger; @@ -53,19 +52,20 @@ private Task OnCommandExecuted(CommandsNextExtension sender, CommandExecutionEve /// /// Try to find a suitable error message to return to the user /// if command was executed in a bot channel, otherwise add a reaction. - /// Log error to discord logger + /// Log error to discord logger. /// /// /// - /// + /// A representing the asynchronous operation. private async Task OnCommandErrored(CommandsNextExtension sender, CommandErrorEventArgs e) { var ctx = e.Context; - var guildId = ctx.Guild.Id; var channelId = ctx.Channel.Id; var message = ctx.Message; var exception = e.Exception; + var commandExecutedInDm = ctx.Channel.IsPrivate; + var commandPrefix = _options.CommandPrefix; var commandHelpText = $"Type \"{commandPrefix}help\" to get some help."; @@ -79,7 +79,8 @@ private async Task OnCommandErrored(CommandsNextExtension sender, CommandErrorEv const string unknownSubcommandErrorString = "No matching subcommands were found, and this group is not executable."; const string unknownOverloadErrorString = "Could not find a suitable overload for the command."; - var isChecksFailedException = exception is ChecksFailedException; + // DM commands are handled separately + var isChecksFailedException = !commandExecutedInDm && exception is ChecksFailedException; var isUnknownCommandException = exception is CommandNotFoundException; var isUnknownSubcommandException = exception.Message == unknownSubcommandErrorString; @@ -119,11 +120,7 @@ private async Task OnCommandErrored(CommandsNextExtension sender, CommandErrorEv var failedCheck = checksFailedException.FailedChecks[0]; - if (failedCheck is BaseCommandCheck) - { - errorMessage = "I do not care for DM commands."; - } - else if (failedCheck is RequireOwnerOrFriend) + if (failedCheck is RequireOwnerOrFriend) { errorMessage = "You do not have permission to do that."; } @@ -142,47 +139,51 @@ private async Task OnCommandErrored(CommandsNextExtension sender, CommandErrorEv appendHelpText = true; } - var botChannelId = await _guildSettingsService.GetBotChannelId(guildId); + if (!commandExecutedInDm) + { + var botChannelId = await _guildSettingsService.GetBotChannelId(ctx.Guild.Id); - var isCommandInBotChannel = botChannelId != null && botChannelId.Value == channelId; + var isCommandInBotChannel = botChannelId.HasValue && botChannelId.Value == channelId; - if (isCommandInBotChannel) - { - if (string.IsNullOrWhiteSpace(errorMessage)) + if (isCommandInBotChannel) { - errorMessage = "Something went wrong."; - } + if (string.IsNullOrWhiteSpace(errorMessage)) + { + errorMessage = "Something went wrong."; + } - if (appendHelpText) - { - errorMessage += $" {commandHelpText}"; - } + if (appendHelpText) + { + errorMessage += $" {commandHelpText}"; + } - await message.RespondAsync(errorMessage); - } - else - { - if (!isUnknownCommand) + await message.RespondAsync(errorMessage); + } + else if (!isUnknownCommand) { await ctx.Message.CreateReactionAsync(DiscordEmoji.FromUnicode(EmojiMap.RedX)); } - } - // Log any unhandled exception - var shouldLogDiscordError = + var isUnhandledException = !isUnknownCommandException && !isUnknownSubcommandException && !isCommandConfigException && !isChecksFailedException - && !isPossiblyValidationException; + && !isPossiblyValidationException + && !commandExecutedInDm; - if (shouldLogDiscordError) + if (isUnhandledException) { _discordErrorLogger.LogError(ctx, exception.ToString()); } - _logger.LogWarning($"Message: {message.Content}\r\nCommand failed: {exception})"); + if (commandExecutedInDm) + { + _discordErrorLogger.LogError(ctx, "Command executed in DM"); + } + + _logger.LogWarning("Message: {MessageContent}\r\nCommand failed: {Exception})", message.Content, exception); } } } diff --git a/Kattbot/Helpers/DiscordExtensions.cs b/Kattbot/Helpers/DiscordExtensions.cs index f5230e3..ac3eafe 100644 --- a/Kattbot/Helpers/DiscordExtensions.cs +++ b/Kattbot/Helpers/DiscordExtensions.cs @@ -9,18 +9,24 @@ namespace Kattbot.Helpers; public static class DiscordExtensions { - public static string GetNicknameOrUsername(this DiscordUser user) + public static string GetDisplayName(this DiscordUser user) { - string username = user.Username; - if (user is DiscordMember member) { - username = !string.IsNullOrWhiteSpace(member.Nickname) - ? member.Nickname - : member.DisplayName; + return member.DisplayName; } - return username; + return user.GlobalName ?? user.Username; + } + + public static string GetFullUsername(this DiscordUser user) + { + return user.HasLegacyUsername() ? $"{user.Username}#{user.Discriminator}" : user.Username; + } + + private static bool HasLegacyUsername(this DiscordUser user) + { + return user.Discriminator != "0"; } public static string GetEmojiImageUrl(this DiscordEmoji emoji) @@ -30,6 +36,29 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji) return isEmote ? emoji.Url : EmoteHelper.GetExternalEmojiImageUrl(emoji.Name); } + public static string GetMessageWithTextMentions(this DiscordMessage message) + { + var newMessageContent = message.Content; + + foreach (var user in message.MentionedUsers) + { + var userMentionAsText = user.Mention.Replace("!", string.Empty); + newMessageContent = newMessageContent.Replace(userMentionAsText, user.GetDisplayName()); + } + + foreach (var role in message.MentionedRoles) + { + newMessageContent = newMessageContent.Replace(role.Mention, role.Name); + } + + foreach (var channel in message.MentionedChannels) + { + newMessageContent = newMessageContent.Replace(channel.Mention, $"#{channel.Name}"); + } + + return newMessageContent; + } + public static async Task GetImageUrlFromMessage(this DiscordMessage message) { var imgUrl = message.GetAttachmentOrStickerImage(); @@ -37,7 +66,7 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji) if (imgUrl != null) return imgUrl; - if (message.ReferencedMessage != null) + if (message.ReferencedMessage is not null) imgUrl = message.ReferencedMessage.GetAttachmentOrStickerImage(); if (imgUrl != null) @@ -45,7 +74,7 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji) var waitTasks = new List> { message.WaitForEmbedImage() }; - if (message.ReferencedMessage != null) + if (message.ReferencedMessage is not null) waitTasks.Add(message.ReferencedMessage.WaitForEmbedImage()); imgUrl = await (await Task.WhenAny(waitTasks)); diff --git a/Kattbot/Kattbot.csproj b/Kattbot/Kattbot.csproj index 080e441..88952e5 100644 --- a/Kattbot/Kattbot.csproj +++ b/Kattbot/Kattbot.csproj @@ -8,9 +8,9 @@ - - - + + + diff --git a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs index 78c9400..68a2dae 100644 --- a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs +++ b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs @@ -17,29 +17,27 @@ namespace Kattbot.NotificationHandlers; public class KattGptMessageHandler : INotificationHandler { private const string ChatGptModel = "gpt-3.5-turbo-16k"; - private const string TokenizerModel = "gpt-3.5"; private const string MetaMessagePrefix = "msg"; private const float Temperature = 1.2f; private const int MaxTokens = 8192; private const int MaxTokensToGenerate = 960; // Roughly the limit of 2 Discord messages - private const string ChannelWithTopicTemplateName = "ChannelWithTopic"; private const string MessageSplitToken = "[cont.]"; private readonly ChatGptHttpClient _chatGpt; private readonly KattGptOptions _kattGptOptions; private readonly KattGptChannelCache _cache; - private readonly TikToken _tokenizer; + private readonly KattGptService _kattGptService; public KattGptMessageHandler( ChatGptHttpClient chatGpt, IOptions kattGptOptions, - KattGptChannelCache cache) + KattGptChannelCache cache, + KattGptService kattGptService) { _chatGpt = chatGpt; _kattGptOptions = kattGptOptions.Value; _cache = cache; - - _tokenizer = TikToken.EncodingForModel(TokenizerModel); + _kattGptService = kattGptService; } public async Task Handle(MessageCreatedNotification notification, CancellationToken cancellationToken) @@ -54,17 +52,19 @@ public async Task Handle(MessageCreatedNotification notification, CancellationTo return; } - var systemPromptsMessages = BuildSystemPromptsMessages(channel); + var systemPromptsMessages = _kattGptService.BuildSystemPromptsMessages(channel); + + var systemMessagesTokenCount = _kattGptService.GetTokenCount(systemPromptsMessages); - var boundedMessageQueue = GetBoundedMessageQueue(channel, systemPromptsMessages); + var boundedMessageQueue = GetBoundedMessageQueue(channel, systemMessagesTokenCount); // Add new message from notification - var newMessageContent = message.Content; - var newMessageUser = author.GetNicknameOrUsername(); + var newMessageUser = author.GetDisplayName(); + var newMessageContent = message.GetMessageWithTextMentions(); var newUserMessage = ChatCompletionMessage.AsUser($"{newMessageUser}: {newMessageContent}"); - boundedMessageQueue.Enqueue(newUserMessage, _tokenizer.Encode(newUserMessage.Content).Count); + boundedMessageQueue.Enqueue(newUserMessage, _kattGptService.GetTokenCount(newUserMessage.Content)); if (ShouldReplyToMessage(message)) { @@ -91,7 +91,7 @@ public async Task Handle(MessageCreatedNotification notification, CancellationTo await SendReply(chatGptResponse.Content, message); // Add the chat gpt response message to the bounded queue - boundedMessageQueue.Enqueue(chatGptResponse, _tokenizer.Encode(chatGptResponse.Content).Count); + boundedMessageQueue.Enqueue(chatGptResponse, _kattGptService.GetTokenCount(chatGptResponse.Content)); } SaveBoundedMessageQueue(channel, boundedMessageQueue); @@ -109,96 +109,21 @@ private static async Task SendReply(string responseMessage, DiscordMessage messa } } - /// - /// Builds the system prompts messages for the given channel. - /// - /// The channel. - /// The system prompts messages. - private List BuildSystemPromptsMessages(DiscordChannel channel) - { - // Get core system prompt messages - var coreSystemPrompts = string.Join(" ", _kattGptOptions.CoreSystemPrompts); - var systemPromptsMessages = new List() { ChatCompletionMessage.AsSystem(coreSystemPrompts) }; - - var guild = channel.Guild; - var guildId = guild.Id; - - // Get the channel options for this guild - var guildOptions = _kattGptOptions.GuildOptions.Where(x => x.Id == guildId).SingleOrDefault() - ?? throw new Exception($"No guild options found for guild {guildId}"); - - // Get the guild system prompts if they exist - string[] guildPromptsArray = guildOptions.SystemPrompts ?? Array.Empty(); - - // add them to the system prompts messages if not empty - if (guildPromptsArray.Length > 0) - { - string guildSystemPrompts = string.Join(" ", guildPromptsArray); - systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(guildSystemPrompts)); - } - - var channelOptions = GetChannelOptions(channel); - - // if there are no channel options, return the system prompts messages - if (channelOptions == null) - { - return systemPromptsMessages; - } - - // get the system prompts for this channel - string[] channelPromptsArray = channelOptions.SystemPrompts ?? Array.Empty(); - - // add them to the system prompts messages if not empty - if (channelPromptsArray.Length > 0) - { - string channelSystemPrompts = string.Join(" ", channelPromptsArray); - systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(channelSystemPrompts)); - } - - // else if the channel options has UseChannelTopic set to true, add the channel topic to the system prompts messages - else if (channelOptions.UseChannelTopic) - { - // get the channel topic or use a fallback - var channelTopic = !string.IsNullOrWhiteSpace(channel.Topic) ? channel.Topic : "Whatever"; - - // get the text template from kattgpt options - var channelWithTopicTemplate = _kattGptOptions.Templates.Where(x => x.Name == ChannelWithTopicTemplateName).SingleOrDefault(); - - // if the temmplate is not null, format it with the channel name and topic and add it to the system prompts messages - if (channelWithTopicTemplate != null) - { - // get a sanitized channel name that only includes letters, digits, - and _ - var channelName = Regex.Replace(channel.Name, @"[^a-zA-Z0-9-_]", string.Empty); - - var formatedTemplatePrompt = string.Format(channelWithTopicTemplate.Content, channelName, channelTopic); - systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(formatedTemplatePrompt)); - } - - // else use the channelTopic as the system message - else - { - systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(channelTopic)); - } - } - - return systemPromptsMessages; - } - /// /// Gets the bounded message queue for the channel from the cache or creates a new one. /// /// The channel. - /// The system prompts messages. + /// The token count for the system messages. /// The bounded message queue for the channel. - private BoundedQueue GetBoundedMessageQueue(DiscordChannel channel, List systemPromptsMessages) + private BoundedQueue GetBoundedMessageQueue(DiscordChannel channel, int systemMessageTokenCount) { var cacheKey = KattGptChannelCache.KattGptChannelCacheKey(channel.Id); + var boundedMessageQueue = _cache.GetCache(cacheKey); + if (boundedMessageQueue == null) { - var totalTokenCountForSystemMessages = systemPromptsMessages.Select(x => x.Content).Sum(m => _tokenizer.Encode(m).Count); - - var remainingTokensForContextMessages = MaxTokens - totalTokenCountForSystemMessages; + var remainingTokensForContextMessages = MaxTokens - systemMessageTokenCount; boundedMessageQueue = new BoundedQueue(remainingTokensForContextMessages); } @@ -226,7 +151,7 @@ private bool ShouldHandleMessage(DiscordMessage message) { var channel = message.Channel; - var channelOptions = GetChannelOptions(channel); + var channelOptions = _kattGptService.GetChannelOptions(channel); if (channelOptions == null) { @@ -255,7 +180,7 @@ private bool ShouldReplyToMessage(DiscordMessage message) { var channel = message.Channel; - var channelOptions = GetChannelOptions(channel); + var channelOptions = _kattGptService.GetChannelOptions(channel); if (channelOptions == null) { @@ -285,33 +210,4 @@ private bool ShouldReplyToMessage(DiscordMessage message) // if it does, return false return !messageStartsWithMetaMessagePrefix; } - - private ChannelOptions? GetChannelOptions(DiscordChannel channel) - { - var guild = channel.Guild; - var guildId = guild.Id; - - // First check if kattgpt is enabled for this guild - var guildOptions = _kattGptOptions.GuildOptions.Where(x => x.Id == guildId).SingleOrDefault(); - if (guildOptions == null) - { - return null; - } - - var guildChannelOptions = guildOptions.ChannelOptions; - var guildCategoryOptions = guildOptions.CategoryOptions; - - // Get the channel options for this channel or for the category this channel is in - var channelOptions = guildChannelOptions.Where(x => x.Id == channel.Id).SingleOrDefault(); - if (channelOptions == null) - { - var category = channel.Parent; - if (category != null) - { - channelOptions = guildCategoryOptions.Where(x => x.Id == category.Id).SingleOrDefault(); - } - } - - return channelOptions; - } } diff --git a/Kattbot/Program.cs b/Kattbot/Program.cs index 9904480..2da4375 100644 --- a/Kattbot/Program.cs +++ b/Kattbot/Program.cs @@ -124,6 +124,7 @@ private static void AddInternalServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } private static void AddRepositories(IServiceCollection services) diff --git a/Kattbot/Services/DiscordErrorLogger.cs b/Kattbot/Services/DiscordErrorLogger.cs index e9a62cb..ada414f 100644 --- a/Kattbot/Services/DiscordErrorLogger.cs +++ b/Kattbot/Services/DiscordErrorLogger.cs @@ -1,5 +1,6 @@ using System; using DSharpPlus.CommandsNext; +using Kattbot.Helpers; using Kattbot.NotificationHandlers; using Kattbot.Workers; using Microsoft.Extensions.Options; @@ -19,9 +20,9 @@ public DiscordErrorLogger(IOptions options, DiscordLogChannel channe public void LogError(CommandContext ctx, string errorMessage) { - string user = $"{ctx.User.Username}#{ctx.User.Discriminator}"; - string channelName = ctx.Channel.Name; - string guildName = ctx.Guild.Name; + string user = ctx.User.GetFullUsername(); + string channelName = !ctx.Channel.IsPrivate ? ctx.Channel.Name : "DM"; + string guildName = ctx.Guild?.Name ?? "Unknown guild"; string command = EscapeTicks(ctx.Message.Content); string contextMessage = $"**Failed command** `{command}` by `{user}` in `{channelName}`(`{guildName}`)"; @@ -34,11 +35,11 @@ public void LogError(CommandContext ctx, string errorMessage) public void LogError(EventContext? ctx, string errorMessage) { - string user = ctx?.User != null ? $"{ctx.User.Username}#{ctx.User.Discriminator}" : "Unknown user"; + string user = ctx?.User is not null ? ctx.User.GetFullUsername() : "Unknown user"; string channelName = ctx?.Channel?.Name ?? "Unknown channel"; string guildName = ctx?.Guild?.Name ?? "Unknown guild"; string eventName = ctx?.EventName ?? "Unknown event"; - string message = ctx?.Message != null ? EscapeTicks(ctx.Message.Content) : string.Empty; + string message = ctx?.Message is not null ? EscapeTicks(ctx.Message.Content) : string.Empty; string contextMessage = $"**Failed event** `{eventName}` by `{user}` in `{channelName}`(`{guildName}`)"; diff --git a/Kattbot/Services/KattGpt/KattGptService.cs b/Kattbot/Services/KattGpt/KattGptService.cs new file mode 100644 index 0000000..ee8442b --- /dev/null +++ b/Kattbot/Services/KattGpt/KattGptService.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using DSharpPlus.Entities; +using Microsoft.Extensions.Options; +using TiktokenSharp; + +namespace Kattbot.Services.KattGpt; + +public class KattGptService +{ + private const string ChannelWithTopicTemplateName = "ChannelWithTopic"; + private const string TokenizerModel = "gpt-3.5"; + + private readonly KattGptOptions _kattGptOptions; + + public KattGptService(IOptions kattGptOptions) + { + _kattGptOptions = kattGptOptions.Value; + } + + /// + /// Builds the system prompts messages for the given channel. + /// + /// The channel. + /// The system prompts messages. + public List BuildSystemPromptsMessages(DiscordChannel channel) + { + // Get core system prompt messages + var coreSystemPrompts = string.Join(" ", _kattGptOptions.CoreSystemPrompts); + var systemPromptsMessages = new List() { ChatCompletionMessage.AsSystem(coreSystemPrompts) }; + + var guild = channel.Guild; + var guildId = guild.Id; + + // Get the channel options for this guild + var guildOptions = _kattGptOptions.GuildOptions.Where(x => x.Id == guildId).SingleOrDefault() + ?? throw new Exception($"No guild options found for guild {guildId}"); + + // Get the guild system prompts if they exist + string[] guildPromptsArray = guildOptions.SystemPrompts ?? Array.Empty(); + + // add them to the system prompts messages if not empty + if (guildPromptsArray.Length > 0) + { + string guildSystemPrompts = string.Join(" ", guildPromptsArray); + systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(guildSystemPrompts)); + } + + var channelOptions = GetChannelOptions(channel); + + // if there are no channel options, return the system prompts messages + if (channelOptions == null) + { + return systemPromptsMessages; + } + + // get the system prompts for this channel + string[] channelPromptsArray = channelOptions.SystemPrompts ?? Array.Empty(); + + // add them to the system prompts messages if not empty + if (channelPromptsArray.Length > 0) + { + string channelSystemPrompts = string.Join(" ", channelPromptsArray); + systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(channelSystemPrompts)); + } + + // else if the channel options has UseChannelTopic set to true, add the channel topic to the system prompts messages + else if (channelOptions.UseChannelTopic) + { + // get the channel topic or use a fallback + var channelTopic = !string.IsNullOrWhiteSpace(channel.Topic) ? channel.Topic : "Whatever"; + + // get the text template from kattgpt options + var channelWithTopicTemplate = _kattGptOptions.Templates.Where(x => x.Name == ChannelWithTopicTemplateName).SingleOrDefault(); + + // if the temmplate is not null, format it with the channel name and topic and add it to the system prompts messages + if (channelWithTopicTemplate != null) + { + // get a sanitized channel name that only includes letters, digits, - and _ + var channelName = Regex.Replace(channel.Name, @"[^a-zA-Z0-9-_]", string.Empty); + + var formatedTemplatePrompt = string.Format(channelWithTopicTemplate.Content, channelName, channelTopic); + systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(formatedTemplatePrompt)); + } + + // else use the channelTopic as the system message + else + { + systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(channelTopic)); + } + } + + return systemPromptsMessages; + } + + public ChannelOptions? GetChannelOptions(DiscordChannel channel) + { + var guild = channel.Guild; + var guildId = guild.Id; + + // First check if kattgpt is enabled for this guild + var guildOptions = _kattGptOptions.GuildOptions.Where(x => x.Id == guildId).SingleOrDefault(); + if (guildOptions == null) + { + return null; + } + + var guildChannelOptions = guildOptions.ChannelOptions; + var guildCategoryOptions = guildOptions.CategoryOptions; + + // Get the channel options for this channel or for the category this channel is in + var channelOptions = guildChannelOptions.Where(x => x.Id == channel.Id).SingleOrDefault(); + if (channelOptions == null) + { + var category = channel.Parent; + if (category != null) + { + channelOptions = guildCategoryOptions.Where(x => x.Id == category.Id).SingleOrDefault(); + } + } + + return channelOptions; + } + + public int GetTokenCount(string messageText) + { + var tokenizer = TikToken.EncodingForModel(TokenizerModel); + + return tokenizer.Encode(messageText).Count; + } + + public int GetTokenCount(IEnumerable systemMessage) + { + var tokenizer = TikToken.EncodingForModel(TokenizerModel); + + var totalTokenCountForSystemMessages = systemMessage.Select(x => x.Content).Sum(m => tokenizer.Encode(m).Count); + + return totalTokenCountForSystemMessages; + } +} diff --git a/Kattbot/Workers/BotWorker.cs b/Kattbot/Workers/BotWorker.cs index 24a791e..2542051 100644 --- a/Kattbot/Workers/BotWorker.cs +++ b/Kattbot/Workers/BotWorker.cs @@ -63,7 +63,7 @@ public async Task StartAsync(CancellationToken cancellationToken) _client.SocketOpened += OnClientConnected; _client.SocketClosed += OnClientDisconnected; - _client.Ready += OnClientReady; + _client.SessionCreated += OnClientReady; _commandEventHandler.RegisterHandlers(commands); _emoteEventHandler.RegisterHandlers(); @@ -89,6 +89,12 @@ private async Task OnMessageCreated(MessageCreateEventArgs args, CancellationTok return; } + // Ignore messages from DMs + if (args.Guild is null) + { + return; + } + await _eventQueue.Writer.WriteAsync(new MessageCreatedNotification(args), cancellationToken); } @@ -106,7 +112,7 @@ private Task OnClientConnected(DiscordClient sender, SocketEventArgs e) return Task.CompletedTask; } - private async Task OnClientReady(DiscordClient sender, ReadyEventArgs e) + private async Task OnClientReady(DiscordClient sender, SessionReadyEventArgs e) { _logger.LogInformation("Bot ready"); From 2a039a40ee7a9684d84b4a4dc15654cec4228a7c Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 17 Sep 2023 10:42:10 +0200 Subject: [PATCH 03/13] Split dump context message in chunks if needed --- Kattbot/CommandModules/AdminModule.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Kattbot/CommandModules/AdminModule.cs b/Kattbot/CommandModules/AdminModule.cs index da31d2a..f1276bc 100644 --- a/Kattbot/CommandModules/AdminModule.cs +++ b/Kattbot/CommandModules/AdminModule.cs @@ -147,7 +147,20 @@ public async Task DumpContext(CommandContext ctx, DiscordChannel channel) sb.AppendLine($"> {message.Content}"); } - await ctx.RespondAsync(sb.ToString()); + var responseMessage = sb.ToString(); + + if (responseMessage.Length <= DiscordConstants.MaxMessageLength) + { + await ctx.RespondAsync(responseMessage); + return; + } + + var messageChunks = responseMessage.SplitString(DiscordConstants.MaxMessageLength, string.Empty); + + foreach (var messageChunk in messageChunks) + { + await ctx.RespondAsync(messageChunk); + } } } } From 1de5a960529c19f4f4ac0995178c0476aa74a49a Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 24 Sep 2023 21:08:10 +0200 Subject: [PATCH 04/13] Refactor KattGpt models file --- .../KattGpt/ChatCompletionCreateRequest.cs | 90 +------------------ .../KattGpt/ChatCompletionCreateResponse.cs | 22 +++++ .../Models/KattGpt/ChatCompletionMessage.cs | 24 +++++ .../KattGpt/ChatCompletionResponseError.cs | 18 ++++ .../ChatCompletionResponseErrorWrapper.cs | 9 ++ Kattbot.Common/Models/KattGpt/Choice.cs | 15 ++++ Kattbot.Common/Models/KattGpt/Usage.cs | 15 ++++ .../KattGptMessageHandler.cs | 7 +- Kattbot/Services/Dalle/DalleHttpClient.cs | 2 +- Kattbot/Services/KattGpt/ChatGptClient.cs | 2 +- Kattbot/Services/KattGpt/KattGptCache.cs | 1 + Kattbot/Services/KattGpt/KattGptService.cs | 1 + 12 files changed, 111 insertions(+), 95 deletions(-) rename Kattbot/Services/KattGpt/ChatGptModels.cs => Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs (64%) create mode 100644 Kattbot.Common/Models/KattGpt/ChatCompletionCreateResponse.cs create mode 100644 Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs create mode 100644 Kattbot.Common/Models/KattGpt/ChatCompletionResponseError.cs create mode 100644 Kattbot.Common/Models/KattGpt/ChatCompletionResponseErrorWrapper.cs create mode 100644 Kattbot.Common/Models/KattGpt/Choice.cs create mode 100644 Kattbot.Common/Models/KattGpt/Usage.cs diff --git a/Kattbot/Services/KattGpt/ChatGptModels.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs similarity index 64% rename from Kattbot/Services/KattGpt/ChatGptModels.cs rename to Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs index 3128a90..425fb4e 100644 --- a/Kattbot/Services/KattGpt/ChatGptModels.cs +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs @@ -1,9 +1,7 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; -namespace Kattbot.Services.KattGpt; +namespace Kattbot.Common.Models.KattGpt; -#pragma warning disable SA1402 // File may only contain a single type public record ChatCompletionCreateRequest { /// @@ -97,87 +95,3 @@ public record ChatCompletionCreateRequest [JsonPropertyName("user")] public string? User { get; set; } } - -public record ChatCompletionMessage -{ - /// - /// Gets or sets can be either “system”, “user”, or “assistant”. - /// - [JsonPropertyName("role")] - public string Role { get; set; } = null!; - - /// - /// Gets or sets the content of the message. - /// - [JsonPropertyName("content")] - public string Content { get; set; } = null!; - - public static ChatCompletionMessage AsSystem(string content) => new() { Role = "system", Content = content }; - - public static ChatCompletionMessage AsUser(string content) => new() { Role = "user", Content = content }; - - public static ChatCompletionMessage AsAssistant(string content) => new() { Role = "assistant", Content = content }; -} - -public record ChatCompletionCreateResponse -{ - [JsonPropertyName("id")] - public string Id { get; set; } = null!; - - [JsonPropertyName("object")] - public string Object { get; set; } = null!; - - [JsonPropertyName("created")] - public int Created { get; set; } - - [JsonPropertyName("choices")] - public List Choices { get; set; } = new List(); - - [JsonPropertyName("usage")] - public Usage Usage { get; set; } = null!; -} - -public record Usage -{ - [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 record Choice -{ - [JsonPropertyName("index")] - public int Index { get; set; } - - [JsonPropertyName("message")] - public ChatCompletionMessage Message { get; set; } = null!; - - [JsonPropertyName("finish_reason")] - public string FinishReason { get; set; } = null!; -} - -public record ChatCompletionResponseErrorWrapper -{ - [JsonPropertyName("error")] - public ChatCompletionResponseError Error { get; set; } = null!; -} - -public record ChatCompletionResponseError -{ - [JsonPropertyName("code")] - public string Code { get; set; } = null!; - - [JsonPropertyName("message")] - public string Message { get; set; } = null!; - - [JsonPropertyName("param")] - public string Param { get; set; } = null!; - - [JsonPropertyName("type")] - public string Type { get; set; } = null!; -} diff --git a/Kattbot.Common/Models/KattGpt/ChatCompletionCreateResponse.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateResponse.cs new file mode 100644 index 0000000..411d36b --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateResponse.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record ChatCompletionCreateResponse +{ + [JsonPropertyName("id")] + public string Id { get; set; } = null!; + + [JsonPropertyName("object")] + public string Object { get; set; } = null!; + + [JsonPropertyName("created")] + public int Created { get; set; } + + [JsonPropertyName("choices")] + public List Choices { get; set; } = new List(); + + [JsonPropertyName("usage")] + public Usage Usage { get; set; } = null!; +} diff --git a/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs new file mode 100644 index 0000000..cee4a22 --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record ChatCompletionMessage +{ + /// + /// Gets or sets can be either “system”, “user”, or “assistant”. + /// + [JsonPropertyName("role")] + public string Role { get; set; } = null!; + + /// + /// Gets or sets the content of the message. + /// + [JsonPropertyName("content")] + public string Content { get; set; } = null!; + + public static ChatCompletionMessage AsSystem(string content) => new() { Role = "system", Content = content }; + + public static ChatCompletionMessage AsUser(string content) => new() { Role = "user", Content = content }; + + public static ChatCompletionMessage AsAssistant(string content) => new() { Role = "assistant", Content = content }; +} diff --git a/Kattbot.Common/Models/KattGpt/ChatCompletionResponseError.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionResponseError.cs new file mode 100644 index 0000000..088a56b --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionResponseError.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record ChatCompletionResponseError +{ + [JsonPropertyName("code")] + public string Code { get; set; } = null!; + + [JsonPropertyName("message")] + public string Message { get; set; } = null!; + + [JsonPropertyName("param")] + public string Param { get; set; } = null!; + + [JsonPropertyName("type")] + public string Type { get; set; } = null!; +} diff --git a/Kattbot.Common/Models/KattGpt/ChatCompletionResponseErrorWrapper.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionResponseErrorWrapper.cs new file mode 100644 index 0000000..85ea519 --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionResponseErrorWrapper.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record ChatCompletionResponseErrorWrapper +{ + [JsonPropertyName("error")] + public ChatCompletionResponseError Error { get; set; } = null!; +} diff --git a/Kattbot.Common/Models/KattGpt/Choice.cs b/Kattbot.Common/Models/KattGpt/Choice.cs new file mode 100644 index 0000000..3168aac --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/Choice.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record Choice +{ + [JsonPropertyName("index")] + public int Index { get; set; } + + [JsonPropertyName("message")] + public ChatCompletionMessage Message { get; set; } = null!; + + [JsonPropertyName("finish_reason")] + public string FinishReason { get; set; } = null!; +} diff --git a/Kattbot.Common/Models/KattGpt/Usage.cs b/Kattbot.Common/Models/KattGpt/Usage.cs new file mode 100644 index 0000000..78a6333 --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/Usage.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record Usage +{ + [JsonPropertyName("prompt_tokens")] + public int PromptTokens { get; set; } + + [JsonPropertyName("completion_tokens")] + public int CompletionTokens { get; set; } + + [JsonPropertyName("total_tokens")] + public int TotalTokens { get; set; } +} diff --git a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs index 68a2dae..7b767d8 100644 --- a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs +++ b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DSharpPlus.Entities; +using Kattbot.Common.Models.KattGpt; using Kattbot.Helpers; using Kattbot.Services.KattGpt; using MediatR; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; -using TiktokenSharp; namespace Kattbot.NotificationHandlers; diff --git a/Kattbot/Services/Dalle/DalleHttpClient.cs b/Kattbot/Services/Dalle/DalleHttpClient.cs index 56b4795..2897f93 100644 --- a/Kattbot/Services/Dalle/DalleHttpClient.cs +++ b/Kattbot/Services/Dalle/DalleHttpClient.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -using Kattbot.Services.KattGpt; +using Kattbot.Common.Models.KattGpt; using Microsoft.Extensions.Options; namespace Kattbot.Services.Dalle; diff --git a/Kattbot/Services/KattGpt/ChatGptClient.cs b/Kattbot/Services/KattGpt/ChatGptClient.cs index 0ac94fb..800421c 100644 --- a/Kattbot/Services/KattGpt/ChatGptClient.cs +++ b/Kattbot/Services/KattGpt/ChatGptClient.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; -using Kattbot.Services.Dalle; +using Kattbot.Common.Models.KattGpt; using Microsoft.Extensions.Options; namespace Kattbot.Services.KattGpt; diff --git a/Kattbot/Services/KattGpt/KattGptCache.cs b/Kattbot/Services/KattGpt/KattGptCache.cs index 878140f..12a9f11 100644 --- a/Kattbot/Services/KattGpt/KattGptCache.cs +++ b/Kattbot/Services/KattGpt/KattGptCache.cs @@ -1,4 +1,5 @@ using System; +using Kattbot.Common.Models.KattGpt; using Kattbot.Helpers; using Microsoft.Extensions.Caching.Memory; diff --git a/Kattbot/Services/KattGpt/KattGptService.cs b/Kattbot/Services/KattGpt/KattGptService.cs index ee8442b..9ba0e67 100644 --- a/Kattbot/Services/KattGpt/KattGptService.cs +++ b/Kattbot/Services/KattGpt/KattGptService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.RegularExpressions; using DSharpPlus.Entities; +using Kattbot.Common.Models.KattGpt; using Microsoft.Extensions.Options; using TiktokenSharp; From d48a906e6dc9388180c8bbdd23a5013cbbc1d100 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Mon, 25 Sep 2023 00:50:43 +0200 Subject: [PATCH 05/13] Kattbot drawz picturz nao --- .../KattGpt/ChatCompletionCreateRequest.cs | 22 ++- .../Models/KattGpt/ChatCompletionFunction.cs | 28 +++ .../Models/KattGpt/ChatCompletionMessage.cs | 61 +++++- Kattbot.Common/Models/KattGpt/FunctionCall.cs | 18 ++ Kattbot/CommandHandlers/Images/DallePrompt.cs | 6 +- Kattbot/CommandModules/AdminModule.cs | 9 +- Kattbot/Helpers/DiscordConstants.cs | 1 + .../KattGptMessageHandler.cs | 179 +++++++++++++++--- .../Services/KattGpt/DalleFunctionBuilder.cs | 36 ++++ Kattbot/Services/KattGpt/KattGptService.cs | 20 +- Kattbot/Services/KattGpt/KattGptTokenizer.cs | 68 +++++++ 11 files changed, 390 insertions(+), 58 deletions(-) create mode 100644 Kattbot.Common/Models/KattGpt/ChatCompletionFunction.cs create mode 100644 Kattbot.Common/Models/KattGpt/FunctionCall.cs create mode 100644 Kattbot/Services/KattGpt/DalleFunctionBuilder.cs create mode 100644 Kattbot/Services/KattGpt/KattGptTokenizer.cs diff --git a/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs index 425fb4e..09ab7e5 100644 --- a/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs @@ -5,7 +5,7 @@ namespace Kattbot.Common.Models.KattGpt; public record ChatCompletionCreateRequest { /// - /// Gets or sets iD of the model to use. + /// Gets or sets Id of the model to use. /// https://platform.openai.com/docs/api-reference/chat/create#chat/create-model. /// [JsonPropertyName("model")] @@ -19,7 +19,25 @@ public record ChatCompletionCreateRequest public ChatCompletionMessage[] Messages { get; set; } = null!; /// - /// Gets or sets what sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + /// Gets or sets a list of functions the model may generate JSON inputs for. + /// https://platform.openai.com/docs/api-reference/chat/create#functions. + /// + [JsonPropertyName("functions")] + public ChatCompletionFunction[]? Functions { get; set; } + + /// + /// Gets or sets the mode for controlling the model responds to function calls. none means the model does not call a function, + /// and responds to the end-user. auto means the model can pick between an end-user or calling a function. + /// Specifying a particular function via {"name": "my_function"} forces the model to call that function. + /// Defaults to "none" when no functions are present and "auto" if functions are present. + /// https://platform.openai.com/docs/api-reference/chat/create#function_call. + /// + [JsonPropertyName("function_call")] + public string? FunctionCall { get; set; } + + /// + /// Gets or sets what sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, + /// while lower values like 0.2 will make it more focused and deterministic. /// We generally recommend altering this or top_p but not both. /// Defaults to 1. /// https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature. diff --git a/Kattbot.Common/Models/KattGpt/ChatCompletionFunction.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionFunction.cs new file mode 100644 index 0000000..825b681 --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionFunction.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record ChatCompletionFunction +{ + /// + /// Gets or sets the name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + /// https://platform.openai.com/docs/api-reference/chat/create#functions-name. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = null!; + + /// + /// Gets or sets a description of what the function does, used by the model to choose when and how to call the function. + /// https://platform.openai.com/docs/api-reference/chat/create#functions-description. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Gets or sets the parameters the functions accepts, described as a JSON Schema object. + /// https://platform.openai.com/docs/api-reference/chat/create#functions-parameters. + /// + [JsonPropertyName("parameters")] + public JsonObject Parameters { get; set; } = null!; +} diff --git a/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs index cee4a22..4a9c773 100644 --- a/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs +++ b/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs @@ -4,21 +4,68 @@ namespace Kattbot.Common.Models.KattGpt; public record ChatCompletionMessage { + public ChatCompletionMessage(string role, string? content) + { + Role = role; + Content = content; + } + + public ChatCompletionMessage(string role, string name, string content) + { + Role = role; + Name = name; + Content = content; + } + + [JsonConstructor] + public ChatCompletionMessage(string role, string name, string? content, FunctionCall? functionCall) + : this(role, content) + { + FunctionCall = functionCall; + Name = name; + } + /// - /// Gets or sets can be either “system”, “user”, or “assistant”. + /// Gets the role of the messages author. One of system, user, assistant, or function. + /// https://platform.openai.com/docs/api-reference/chat/create#messages-role. /// [JsonPropertyName("role")] - public string Role { get; set; } = null!; + public string Role { get; } = null!; /// - /// Gets or sets the content of the message. + /// Gets or sets the contents of the message. content is required for all messages, and may be null for assistant messages with function calls. + /// https://platform.openai.com/docs/api-reference/chat/create#messages-content. /// [JsonPropertyName("content")] - public string Content { get; set; } = null!; + public string? Content { get; set; } // This should be private but the api is being weird and doesn't allow nulls like it says it does + + /// + /// Gets the name of the author of this message. name is required if role is function, and it should be the name of the function whose response is in the content. + /// May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters. + /// https://platform.openai.com/docs/api-reference/chat/create#messages-name. + /// + [JsonPropertyName("name")] + public string? Name { get; } - public static ChatCompletionMessage AsSystem(string content) => new() { Role = "system", Content = content }; + /// + /// Gets the name and arguments of a function that should be called, as generated by the model. + /// https://platform.openai.com/docs/api-reference/chat/create#messages-function_call. + /// + [JsonPropertyName("function_call")] + public FunctionCall? FunctionCall { get; } + + public static ChatCompletionMessage AsSystem(string content) => new("system", content); - public static ChatCompletionMessage AsUser(string content) => new() { Role = "user", Content = content }; + public static ChatCompletionMessage AsUser(string content) => new("user", content); - public static ChatCompletionMessage AsAssistant(string content) => new() { Role = "assistant", Content = content }; + public static ChatCompletionMessage AsAssistant(string content) => new("assistant", content); + + /// + /// Builds a message as a function call which contains the function result to be added to the context. + /// + /// The name of the function. + /// The result of the function. + /// A . + public static ChatCompletionMessage AsFunctionCallResult(string name, string content) + => new("function", name, content); } diff --git a/Kattbot.Common/Models/KattGpt/FunctionCall.cs b/Kattbot.Common/Models/KattGpt/FunctionCall.cs new file mode 100644 index 0000000..5f0e068 --- /dev/null +++ b/Kattbot.Common/Models/KattGpt/FunctionCall.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace Kattbot.Common.Models.KattGpt; + +public record FunctionCall +{ + public FunctionCall(string name, string arguments) + { + Name = name; + Arguments = arguments; + } + + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("arguments")] + public string Arguments { get; } +} diff --git a/Kattbot/CommandHandlers/Images/DallePrompt.cs b/Kattbot/CommandHandlers/Images/DallePrompt.cs index ac59455..64d30b4 100644 --- a/Kattbot/CommandHandlers/Images/DallePrompt.cs +++ b/Kattbot/CommandHandlers/Images/DallePrompt.cs @@ -25,8 +25,6 @@ public DallePromptCommand(CommandContext ctx, string prompt) public class DallePromptHandler : IRequestHandler { - private const int MaxEmbedTitleLength = 256; - private readonly DalleHttpClient _dalleHttpClient; private readonly ImageService _imageService; @@ -54,8 +52,8 @@ public async Task Handle(DallePromptCommand request, CancellationToken cancellat var fileName = request.Prompt.ToSafeFilename(imageStream.FileExtension); - var truncatedPrompt = request.Prompt.Length > MaxEmbedTitleLength - ? $"{request.Prompt[..(MaxEmbedTitleLength - 3)]}..." + var truncatedPrompt = request.Prompt.Length > DiscordConstants.MaxEmbedTitleLength + ? $"{request.Prompt[..(DiscordConstants.MaxEmbedTitleLength - 3)]}..." : request.Prompt; DiscordEmbedBuilder eb = new DiscordEmbedBuilder() diff --git a/Kattbot/CommandModules/AdminModule.cs b/Kattbot/CommandModules/AdminModule.cs index f1276bc..09ef527 100644 --- a/Kattbot/CommandModules/AdminModule.cs +++ b/Kattbot/CommandModules/AdminModule.cs @@ -107,7 +107,10 @@ public async Task SetBotChannel(CommandContext ctx, DiscordChannel channel) public async Task DumpPrompts(CommandContext ctx, DiscordChannel channel) { var systemPromptsMessages = _kattGptService.BuildSystemPromptsMessages(channel); - var tokenCount = _kattGptService.GetTokenCount(systemPromptsMessages); + + var tokenizer = new KattGptTokenizer("gpt-3.5"); + + var tokenCount = tokenizer.GetTokenCount(systemPromptsMessages); var sb = new StringBuilder($"System prompt messages. Context size {tokenCount} tokens"); sb.AppendLine(); @@ -136,7 +139,9 @@ public async Task DumpContext(CommandContext ctx, DiscordChannel channel) var contextMessages = boundedMessageQueue.GetAll(); - var tokenCount = _kattGptService.GetTokenCount(contextMessages); + var tokenizer = new KattGptTokenizer("gpt-3.5"); + + var tokenCount = tokenizer.GetTokenCount(contextMessages); var sb = new StringBuilder($"Chat messages. Context size: {tokenCount} tokens"); sb.AppendLine(); diff --git a/Kattbot/Helpers/DiscordConstants.cs b/Kattbot/Helpers/DiscordConstants.cs index 0375677..b4a385e 100644 --- a/Kattbot/Helpers/DiscordConstants.cs +++ b/Kattbot/Helpers/DiscordConstants.cs @@ -4,5 +4,6 @@ public class DiscordConstants { public const int MaxMessageLength = 2000; public const int MaxEmbedContentLength = 4096; + public const int MaxEmbedTitleLength = 256; public const int DefaultEmbedColor = 9648895; // #4d5cac } diff --git a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs index 7b767d8..d803d2e 100644 --- a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs +++ b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs @@ -1,19 +1,24 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using DSharpPlus.Entities; using Kattbot.Common.Models.KattGpt; using Kattbot.Helpers; +using Kattbot.Services; +using Kattbot.Services.Dalle; +using Kattbot.Services.Images; using Kattbot.Services.KattGpt; using MediatR; -using Microsoft.Extensions.Options; namespace Kattbot.NotificationHandlers; public class KattGptMessageHandler : INotificationHandler { private const string ChatGptModel = "gpt-3.5-turbo-16k"; + private const string TokenizerModel = "gpt-3.5"; private const string MetaMessagePrefix = "msg"; private const float Temperature = 1.2f; private const int MaxTokens = 8192; @@ -21,20 +26,26 @@ public class KattGptMessageHandler : INotificationHandler kattGptOptions, KattGptChannelCache cache, - KattGptService kattGptService) + KattGptService kattGptService, + DalleHttpClient dalleHttpClient, + ImageService imageService, + DiscordErrorLogger discordErrorLogger) { _chatGpt = chatGpt; - _kattGptOptions = kattGptOptions.Value; _cache = cache; _kattGptService = kattGptService; + _dalleHttpClient = dalleHttpClient; + _imageService = imageService; + _discordErrorLogger = discordErrorLogger; } public async Task Handle(MessageCreatedNotification notification, CancellationToken cancellationToken) @@ -49,9 +60,11 @@ public async Task Handle(MessageCreatedNotification notification, CancellationTo return; } + var kattGptTokenizer = new KattGptTokenizer(TokenizerModel); + var systemPromptsMessages = _kattGptService.BuildSystemPromptsMessages(channel); - var systemMessagesTokenCount = _kattGptService.GetTokenCount(systemPromptsMessages); + var systemMessagesTokenCount = kattGptTokenizer.GetTokenCount(systemPromptsMessages); var boundedMessageQueue = GetBoundedMessageQueue(channel, systemMessagesTokenCount); @@ -61,39 +74,77 @@ public async Task Handle(MessageCreatedNotification notification, CancellationTo var newUserMessage = ChatCompletionMessage.AsUser($"{newMessageUser}: {newMessageContent}"); - boundedMessageQueue.Enqueue(newUserMessage, _kattGptService.GetTokenCount(newUserMessage.Content)); + boundedMessageQueue.Enqueue(newUserMessage, kattGptTokenizer.GetTokenCount(newUserMessage.Content)); if (ShouldReplyToMessage(message)) { await channel.TriggerTypingAsync(); - // Collect request messages - var requestMessages = new List(); - requestMessages.AddRange(systemPromptsMessages); - requestMessages.AddRange(boundedMessageQueue.GetAll()); - - // Make request - var request = new ChatCompletionCreateRequest() - { - Model = ChatGptModel, - Messages = requestMessages.ToArray(), - Temperature = Temperature, - MaxTokens = MaxTokensToGenerate, - }; + var request = BuildRequest(systemPromptsMessages, boundedMessageQueue); var response = await _chatGpt.ChatCompletionCreate(request); var chatGptResponse = response.Choices[0].Message; - await SendReply(chatGptResponse.Content, message); + if (chatGptResponse.FunctionCall != null) + { + await HandleFunctionCallResponse(message, kattGptTokenizer, systemPromptsMessages, boundedMessageQueue, chatGptResponse); + } + else + { + await SendReply(chatGptResponse.Content!, message); - // Add the chat gpt response message to the bounded queue - boundedMessageQueue.Enqueue(chatGptResponse, _kattGptService.GetTokenCount(chatGptResponse.Content)); + // Add the chat gpt response message to the bounded queue + boundedMessageQueue.Enqueue(chatGptResponse, kattGptTokenizer.GetTokenCount(chatGptResponse.Content)); + } } SaveBoundedMessageQueue(channel, boundedMessageQueue); } + private static ChatCompletionCreateRequest BuildRequest(List systemPromptsMessages, BoundedQueue boundedMessageQueue) + { + // Build functions + var chatCompletionFunctions = new[] { DalleFunctionBuilder.BuildDalleImageFunctionDefinition() }; + + // Collect request messages + var requestMessages = new List(); + requestMessages.AddRange(systemPromptsMessages); + requestMessages.AddRange(boundedMessageQueue.GetAll()); + + // Make request + var request = new ChatCompletionCreateRequest() + { + Model = ChatGptModel, + Messages = requestMessages.ToArray(), + Temperature = Temperature, + MaxTokens = MaxTokensToGenerate, + Functions = chatCompletionFunctions, + }; + + return request; + } + + private static async Task SendDalleResultReply(string responseMessage, DiscordMessage messageToReplyTo, string prompt, ImageStreamResult imageStream) + { + var truncatedPrompt = prompt.Length > DiscordConstants.MaxEmbedTitleLength + ? $"{prompt[..(DiscordConstants.MaxEmbedTitleLength - 3)]}..." + : prompt; + + var filename = prompt.ToSafeFilename(imageStream.FileExtension); + + DiscordEmbedBuilder eb = new DiscordEmbedBuilder() + .WithTitle(truncatedPrompt) + .WithImageUrl($"attachment://{filename}"); + + DiscordMessageBuilder mb = new DiscordMessageBuilder() + .AddFile(filename, imageStream.MemoryStream) + .WithEmbed(eb) + .WithContent(responseMessage); + + await messageToReplyTo.RespondAsync(mb); + } + private static async Task SendReply(string responseMessage, DiscordMessage messageToReplyTo) { var messageChunks = responseMessage.SplitString(DiscordConstants.MaxMessageLength, MessageSplitToken); @@ -106,6 +157,86 @@ private static async Task SendReply(string responseMessage, DiscordMessage messa } } + private async Task GetDalleResult(string prompt, string userId) + { + var response = await _dalleHttpClient.CreateImage(new CreateImageRequest { Prompt = prompt, User = userId }); + if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result"); + + var imageUrl = response.Data.First(); + + var image = await _imageService.DownloadImage(imageUrl.Url); + + var imageStream = await _imageService.GetImageStream(image); + + return imageStream; + } + + private async Task HandleFunctionCallResponse( + DiscordMessage message, + KattGptTokenizer kattGptTokenizer, + List systemPromptsMessages, + BoundedQueue boundedMessageQueue, + ChatCompletionMessage chatGptResponse) + { + DiscordMessage? workingOnItMessage = null; + + try + { + var authorId = message.Author.Id; + + // Force a content value for the chat gpt response due the api not allowing nulls even though it says it does + chatGptResponse.Content ??= "null"; + + // Parse and execute function call + var functionCallName = chatGptResponse.FunctionCall!.Name; + var functionCallArguments = chatGptResponse.FunctionCall.Arguments; + + var parsedArguments = JsonNode.Parse(functionCallArguments) + ?? throw new Exception("Could not parse function call arguments."); + + var prompt = parsedArguments["prompt"]?.GetValue() + ?? throw new Exception("Function call arguments are invalid."); + + workingOnItMessage = await message.RespondAsync($"Kattbot used: {prompt}"); + + var dalleResult = await GetDalleResult(prompt, authorId.ToString()); + + // Send request with function result + var functionCallResult = $"The prompt itself and the image result are attached to this post."; + + var functionCallResultMessage = ChatCompletionMessage.AsFunctionCallResult(functionCallName, functionCallResult); + var functionCallResultTokenCount = kattGptTokenizer.GetTokenCount(functionCallName, functionCallArguments, functionCallResult); + + // Add chat gpt response to the context + var chatGptResponseTokenCount = kattGptTokenizer.GetTokenCount(chatGptResponse.Content); + boundedMessageQueue.Enqueue(chatGptResponse, chatGptResponseTokenCount); + + // Add function call result to the context + boundedMessageQueue.Enqueue(functionCallResultMessage, functionCallResultTokenCount); + + var request2 = BuildRequest(systemPromptsMessages, boundedMessageQueue); + + var response2 = await _chatGpt.ChatCompletionCreate(request2); + + // Handle new response + var chatGptResponse2 = response2.Choices[0].Message; + + await workingOnItMessage.DeleteAsync(); + + await SendDalleResultReply(chatGptResponse2.Content!, message, prompt, dalleResult); + } + catch (Exception ex) + { + if (workingOnItMessage is not null) + { + await workingOnItMessage.DeleteAsync(); + } + + await SendReply("Something went wrong", message); + _discordErrorLogger.LogError(ex.Message); + } + } + /// /// Gets the bounded message queue for the channel from the cache or creates a new one. /// diff --git a/Kattbot/Services/KattGpt/DalleFunctionBuilder.cs b/Kattbot/Services/KattGpt/DalleFunctionBuilder.cs new file mode 100644 index 0000000..3516fb5 --- /dev/null +++ b/Kattbot/Services/KattGpt/DalleFunctionBuilder.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Nodes; +using Kattbot.Common.Models.KattGpt; + +namespace Kattbot.Services.KattGpt; + +public static class DalleFunctionBuilder +{ + public const string FunctionName = "image_generation"; + + public static ChatCompletionFunction BuildDalleImageFunctionDefinition() + { + var function = new ChatCompletionFunction + { + Name = FunctionName, + Description = "Generate an image from a prompt.", + Parameters = new JsonObject + { + ["type"] = "object", + ["properties"] = new JsonObject + { + ["prompt"] = new JsonObject + { + ["type"] = "string", + ["description"] = "The prompt to generate an image from.", + }, + }, + ["required"] = new JsonArray + { + "prompt", + }, + }, + }; + + return function; + } +} diff --git a/Kattbot/Services/KattGpt/KattGptService.cs b/Kattbot/Services/KattGpt/KattGptService.cs index 9ba0e67..73483de 100644 --- a/Kattbot/Services/KattGpt/KattGptService.cs +++ b/Kattbot/Services/KattGpt/KattGptService.cs @@ -5,14 +5,12 @@ using DSharpPlus.Entities; using Kattbot.Common.Models.KattGpt; using Microsoft.Extensions.Options; -using TiktokenSharp; namespace Kattbot.Services.KattGpt; public class KattGptService { private const string ChannelWithTopicTemplateName = "ChannelWithTopic"; - private const string TokenizerModel = "gpt-3.5"; private readonly KattGptOptions _kattGptOptions; @@ -116,7 +114,7 @@ public List BuildSystemPromptsMessages(DiscordChannel cha if (channelOptions == null) { var category = channel.Parent; - if (category != null) + if (category is not null) { channelOptions = guildCategoryOptions.Where(x => x.Id == category.Id).SingleOrDefault(); } @@ -124,20 +122,4 @@ public List BuildSystemPromptsMessages(DiscordChannel cha return channelOptions; } - - public int GetTokenCount(string messageText) - { - var tokenizer = TikToken.EncodingForModel(TokenizerModel); - - return tokenizer.Encode(messageText).Count; - } - - public int GetTokenCount(IEnumerable systemMessage) - { - var tokenizer = TikToken.EncodingForModel(TokenizerModel); - - var totalTokenCountForSystemMessages = systemMessage.Select(x => x.Content).Sum(m => tokenizer.Encode(m).Count); - - return totalTokenCountForSystemMessages; - } } diff --git a/Kattbot/Services/KattGpt/KattGptTokenizer.cs b/Kattbot/Services/KattGpt/KattGptTokenizer.cs new file mode 100644 index 0000000..feaa298 --- /dev/null +++ b/Kattbot/Services/KattGpt/KattGptTokenizer.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using Kattbot.Common.Models.KattGpt; +using TiktokenSharp; + +namespace Kattbot.Services.KattGpt; + +public class KattGptTokenizer +{ + private readonly string _modelName; + + public KattGptTokenizer(string modelName) + { + _modelName = modelName; + } + + public int GetTokenCount(string? messageText) + { + if (string.IsNullOrWhiteSpace(messageText)) + { + return 0; + } + + var tokenizer = TikToken.EncodingForModel(_modelName); + + return tokenizer.Encode(messageText).Count; + } + + public int GetTokenCount(string component1, string component2) + { + var tokenizer = TikToken.EncodingForModel(_modelName); + + var count1 = tokenizer.Encode(component1).Count; + var count2 = tokenizer.Encode(component2).Count; + + return count1 + count2; + } + + public int GetTokenCount(string component1, string component2, string component3) + { + var tokenizer = TikToken.EncodingForModel(_modelName); + + var count1 = tokenizer.Encode(component1).Count; + var count2 = tokenizer.Encode(component2).Count; + var count3 = tokenizer.Encode(component3).Count; + + return count1 + count2 + count3; + } + + public int GetTokenCount(FunctionCall functionCall) + { + var tokenizer = TikToken.EncodingForModel(_modelName); + + var nameTokenCount = tokenizer.Encode(functionCall.Name).Count; + var argumentsTokenCount = tokenizer.Encode(functionCall.Arguments).Count; + + return nameTokenCount + argumentsTokenCount; + } + + public int GetTokenCount(IEnumerable systemMessage) + { + var tokenizer = TikToken.EncodingForModel(_modelName); + + var totalTokenCountForSystemMessages = systemMessage.Select(x => x.Content).Sum(m => tokenizer.Encode(m).Count); + + return totalTokenCountForSystemMessages; + } +} From c625ea6298a13fd44eff2a2f453a01ac22edccbe Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 1 Oct 2023 11:29:12 +0200 Subject: [PATCH 06/13] Change meta message --- Kattbot/NotificationHandlers/KattGptMessageHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs index d803d2e..4eea168 100644 --- a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs +++ b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs @@ -19,7 +19,7 @@ public class KattGptMessageHandler : INotificationHandler Date: Sun, 1 Oct 2023 11:30:02 +0200 Subject: [PATCH 07/13] Disable MessageCreated ev. for commands --- Kattbot/Program.cs | 2 +- Kattbot/Workers/BotWorker.cs | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Kattbot/Program.cs b/Kattbot/Program.cs index 2da4375..d734dcf 100644 --- a/Kattbot/Program.cs +++ b/Kattbot/Program.cs @@ -74,7 +74,7 @@ private static void AddDiscordClient(HostBuilderContext hostContext, IServiceCol services.AddSingleton((_) => { var defaultLogLevel = hostContext.Configuration.GetValue("Logging:LogLevel:Default") ?? "Warning"; - var botToken = hostContext.Configuration.GetValue("Kattbot:BotToken"); + var botToken = hostContext.Configuration.GetValue("Kattbot:BotToken") ?? throw new Exception("Bot token not found"); LogLevel logLevel = Enum.Parse(defaultLogLevel); diff --git a/Kattbot/Workers/BotWorker.cs b/Kattbot/Workers/BotWorker.cs index 2542051..a50d986 100644 --- a/Kattbot/Workers/BotWorker.cs +++ b/Kattbot/Workers/BotWorker.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -47,12 +48,11 @@ public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting bot"); - string commandPrefix = _options.CommandPrefix; - string altCommandPrefix = _options.AlternateCommandPrefix; + string[] commandPrefixes = new[] { _options.CommandPrefix, _options.AlternateCommandPrefix }; CommandsNextExtension commands = _client.UseCommandsNext(new CommandsNextConfiguration() { - StringPrefixes = new[] { commandPrefix, altCommandPrefix }, + StringPrefixes = commandPrefixes, Services = _serviceProvider, EnableDefaultHelp = false, EnableMentionPrefix = false, @@ -68,7 +68,7 @@ public async Task StartAsync(CancellationToken cancellationToken) _commandEventHandler.RegisterHandlers(commands); _emoteEventHandler.RegisterHandlers(); - _client.MessageCreated += (sender, args) => OnMessageCreated(args, cancellationToken); + _client.MessageCreated += (sender, args) => OnMessageCreated(args, commandPrefixes, cancellationToken); await _client.ConnectAsync(); } @@ -80,7 +80,7 @@ public async Task StopAsync(CancellationToken cancellationToken) await _client.DisconnectAsync(); } - private async Task OnMessageCreated(MessageCreateEventArgs args, CancellationToken cancellationToken) + private async Task OnMessageCreated(MessageCreateEventArgs args, string[] commandPrefixes, CancellationToken cancellationToken) { var author = args.Author; @@ -95,6 +95,12 @@ private async Task OnMessageCreated(MessageCreateEventArgs args, CancellationTok return; } + // Ignore message that starts with the bot's command prefix + if (commandPrefixes.Any(prefix => args.Message.Content.TrimStart().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) + { + return; + } + await _eventQueue.Writer.WriteAsync(new MessageCreatedNotification(args), cancellationToken); } From 853a4f374a61732c8d794c1e5eb6061b455ac95f Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 1 Oct 2023 14:41:31 +0200 Subject: [PATCH 08/13] Rework kattgpt prompts --- Kattbot/BotOptions.cs | 63 ------------ Kattbot/CommandModules/AdminModule.cs | 17 +++- Kattbot/CommandModules/HelpModule.cs | 1 + Kattbot/CommandModules/StatsCommandModule.cs | 1 + Kattbot/Config/BotOptions.cs | 20 ++++ Kattbot/Config/ChannelOptions.cs | 16 ++++ Kattbot/Config/GuildOptions.cs | 14 +++ Kattbot/Config/KattGptOptions.cs | 14 +++ Kattbot/Config/Template.cs | 10 ++ Kattbot/EventHandlers/CommandEventHandler.cs | 1 + Kattbot/EventHandlers/EmoteEventHandler.cs | 1 + Kattbot/Helpers/StringExtensions.cs | 10 ++ .../KattGptMessageHandler.cs | 17 +++- Kattbot/Program.cs | 1 + Kattbot/Services/Dalle/DalleHttpClient.cs | 1 + Kattbot/Services/DiscordErrorLogger.cs | 1 + Kattbot/Services/KattGpt/ChatGptClient.cs | 1 + .../Services/KattGpt/DalleFunctionBuilder.cs | 2 +- Kattbot/Services/KattGpt/KattGptService.cs | 96 +++++++++++-------- Kattbot/Workers/BotWorker.cs | 1 + Kattbot/appsettings.Development.json | 28 ++++-- Kattbot/appsettings.json | 82 ++++++++++------ 22 files changed, 250 insertions(+), 148 deletions(-) delete mode 100644 Kattbot/BotOptions.cs create mode 100644 Kattbot/Config/BotOptions.cs create mode 100644 Kattbot/Config/ChannelOptions.cs create mode 100644 Kattbot/Config/GuildOptions.cs create mode 100644 Kattbot/Config/KattGptOptions.cs create mode 100644 Kattbot/Config/Template.cs diff --git a/Kattbot/BotOptions.cs b/Kattbot/BotOptions.cs deleted file mode 100644 index 8eaf7b5..0000000 --- a/Kattbot/BotOptions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; - -namespace Kattbot; - -#pragma warning disable SA1402 // File may only contain a single type -public record BotOptions -{ - public const string OptionsKey = "Kattbot"; - - public string CommandPrefix { get; set; } = null!; - - public string AlternateCommandPrefix { get; set; } = null!; - - public string ConnectionString { get; set; } = null!; - - public string BotToken { get; set; } = null!; - - public ulong ErrorLogGuildId { get; set; } - - public ulong ErrorLogChannelId { get; set; } - - public string OpenAiApiKey { get; set; } = null!; -} - -public record KattGptOptions -{ - public const string OptionsKey = "KattGpt"; - - public string[] CoreSystemPrompts { get; set; } = Array.Empty(); - - public GuildOptions[] GuildOptions { get; set; } = Array.Empty(); - - public Template[] Templates { get; set; } = Array.Empty