diff --git a/Kattbot/Services/KattGpt/ChatGptModels.cs b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs
similarity index 63%
rename from Kattbot/Services/KattGpt/ChatGptModels.cs
rename to Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs
index 3128a90..09ab7e5 100644
--- a/Kattbot/Services/KattGpt/ChatGptModels.cs
+++ b/Kattbot.Common/Models/KattGpt/ChatCompletionCreateRequest.cs
@@ -1,13 +1,11 @@
-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
{
///
- /// 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")]
@@ -21,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.
@@ -97,87 +113,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/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
new file mode 100644
index 0000000..4a9c773
--- /dev/null
+++ b/Kattbot.Common/Models/KattGpt/ChatCompletionMessage.cs
@@ -0,0 +1,71 @@
+using System.Text.Json.Serialization;
+
+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 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; } = null!;
+
+ ///
+ /// 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; } // 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; }
+
+ ///
+ /// 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("user", 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/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/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.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/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/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();
-}
-
-public record GuildOptions
-{
- public ulong Id { get; set; }
-
- public string[] SystemPrompts { get; set; } = Array.Empty();
-
- public ChannelOptions[] CategoryOptions { get; set; } = Array.Empty();
-
- public ChannelOptions[] ChannelOptions { get; set; } = Array.Empty();
-}
-
-public record ChannelOptions
-{
- public ulong Id { get; set; }
-
- public bool UseChannelTopic { get; set; }
-
- public bool AlwaysOn { get; set; }
-
- public string[] SystemPrompts { get; set; } = Array.Empty();
-}
-
-public record Template
-{
- public string Name { get; set; } = null!;
-
- public string Content { get; set; } = null!;
-}
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/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/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs
index b77f7d9..5c87a20 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)
@@ -187,7 +187,7 @@ private async Task DallifyImage(string imageUrl, ulong userId
var squaredImage = _imageService.CropToSquare(imageAsPng);
- var resultSize = Math.Min(maxSize, Math.Max(ValidSizes.Reverse().FirstOrDefault(s => squaredImage.Height > s), ValidSizes[0]));
+ var resultSize = Math.Min(maxSize, Math.Max(ValidSizes.Reverse().FirstOrDefault(s => squaredImage.Height >= s), ValidSizes[0]));
var fileName = $"{Guid.NewGuid()}.png";
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 b60da64..d0b9e3e 100644
--- a/Kattbot/CommandModules/AdminModule.cs
+++ b/Kattbot/CommandModules/AdminModule.cs
@@ -1,4 +1,7 @@
-using DSharpPlus.CommandsNext;
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
using Kattbot.Attributes;
@@ -6,13 +9,13 @@
using Kattbot.Data.Repositories;
using Kattbot.Helpers;
using Kattbot.Services;
+using Kattbot.Services.KattGpt;
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
@@ -20,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;
}
@@ -44,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);
@@ -64,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,26 +103,82 @@ 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)
+ [Command("dump-prompts")]
+ public async Task DumpPrompts(CommandContext ctx, DiscordChannel channel)
{
- var channelId = channel.Id;
- var guildId = channel.GuildId!.Value;
+ var systemPromptsMessages = _kattGptService.BuildSystemPromptsMessages(channel);
+
+ 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();
+
+ foreach (var message in systemPromptsMessages)
+ {
+ sb.AppendLine();
+ sb.AppendLine(message.Content);
+ }
- await _guildSettingsService.SetKattGptChannel(guildId, channelId);
+ var responseMessage = sb.ToString();
- await ctx.RespondAsync($"Set KattGpt channel to #{channel.Name}");
+ 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);
+ }
}
- [Command("set-kattgptish-channel")]
- public async Task SetKattGptishChannel(CommandContext ctx, DiscordChannel channel)
+ [Command("dump-context")]
+ public async Task DumpContext(CommandContext ctx, DiscordChannel channel)
{
- var channelId = channel.Id;
- var guildId = channel.GuildId!.Value;
+ var cacheKey = KattGptChannelCache.KattGptChannelCacheKey(channel.Id);
- await _guildSettingsService.SetKattGptishChannel(guildId, channelId);
+ var boundedMessageQueue = _cache.GetCache(cacheKey);
- await ctx.RespondAsync($"Set KattGptish channel to #{channel.Name}");
+ if (boundedMessageQueue == null)
+ {
+ await ctx.RespondAsync("No prompts found");
+ return;
+ }
+
+ var contextMessages = boundedMessageQueue.GetAll();
+
+ var tokenizer = new KattGptTokenizer("gpt-3.5");
+
+ var tokenCount = tokenizer.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}");
+ }
+
+ 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);
+ }
}
}
}
diff --git a/Kattbot/CommandModules/HelpModule.cs b/Kattbot/CommandModules/HelpModule.cs
index 8764005..9b73d3c 100644
--- a/Kattbot/CommandModules/HelpModule.cs
+++ b/Kattbot/CommandModules/HelpModule.cs
@@ -4,6 +4,7 @@
using DSharpPlus.CommandsNext.Attributes;
using Kattbot.Attributes;
using Kattbot.CommandModules.ResultFormatters;
+using Kattbot.Config;
using Kattbot.Helpers;
using Microsoft.Extensions.Options;
diff --git a/Kattbot/CommandModules/StatsCommandModule.cs b/Kattbot/CommandModules/StatsCommandModule.cs
index cb42908..e4357b3 100644
--- a/Kattbot/CommandModules/StatsCommandModule.cs
+++ b/Kattbot/CommandModules/StatsCommandModule.cs
@@ -11,6 +11,7 @@
using Kattbot.CommandModules.TypeReaders;
using Kattbot.Common.Models;
using Kattbot.Common.Models.Emotes;
+using Kattbot.Config;
using Kattbot.Data;
using Kattbot.Helpers;
using Kattbot.Workers;
@@ -87,7 +88,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 +101,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/Config/BotOptions.cs b/Kattbot/Config/BotOptions.cs
new file mode 100644
index 0000000..ba9cf40
--- /dev/null
+++ b/Kattbot/Config/BotOptions.cs
@@ -0,0 +1,20 @@
+namespace Kattbot.Config;
+
+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!;
+}
diff --git a/Kattbot/Config/ChannelOptions.cs b/Kattbot/Config/ChannelOptions.cs
new file mode 100644
index 0000000..d7f8e08
--- /dev/null
+++ b/Kattbot/Config/ChannelOptions.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Kattbot.Config;
+
+public record ChannelOptions
+{
+ public ulong Id { get; set; }
+
+ public string? Topic { get; set; }
+
+ public bool FallbackToChannelTopic { get; set; }
+
+ public bool AlwaysOn { get; set; }
+
+ public string[] SystemPrompts { get; set; } = Array.Empty();
+}
diff --git a/Kattbot/Config/GuildOptions.cs b/Kattbot/Config/GuildOptions.cs
new file mode 100644
index 0000000..507a86c
--- /dev/null
+++ b/Kattbot/Config/GuildOptions.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Kattbot.Config;
+
+public record GuildOptions
+{
+ public ulong Id { get; set; }
+
+ public string Name { get; set; } = null!;
+
+ public ChannelOptions[] CategoryOptions { get; set; } = Array.Empty();
+
+ public ChannelOptions[] ChannelOptions { get; set; } = Array.Empty();
+}
diff --git a/Kattbot/Config/KattGptOptions.cs b/Kattbot/Config/KattGptOptions.cs
new file mode 100644
index 0000000..51a08cf
--- /dev/null
+++ b/Kattbot/Config/KattGptOptions.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Kattbot.Config;
+
+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();
+}
diff --git a/Kattbot/Config/Template.cs b/Kattbot/Config/Template.cs
new file mode 100644
index 0000000..7a09be5
--- /dev/null
+++ b/Kattbot/Config/Template.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Kattbot.Config;
+
+public record Template
+{
+ public string Name { get; set; } = null!;
+
+ public string Content { get; set; } = null!;
+}
diff --git a/Kattbot/EventHandlers/CommandEventHandler.cs b/Kattbot/EventHandlers/CommandEventHandler.cs
index 060e80f..f8ff077 100644
--- a/Kattbot/EventHandlers/CommandEventHandler.cs
+++ b/Kattbot/EventHandlers/CommandEventHandler.cs
@@ -1,14 +1,15 @@
-using DSharpPlus.CommandsNext;
+using System;
+using System.Threading.Tasks;
+using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.CommandsNext.Exceptions;
using DSharpPlus.Entities;
using Kattbot.Attributes;
+using Kattbot.Config;
using Kattbot.Helpers;
using Kattbot.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using System;
-using System.Threading.Tasks;
namespace Kattbot.EventHandlers
{
@@ -23,8 +24,7 @@ public CommandEventHandler(
IOptions options,
ILogger logger,
DiscordErrorLogger discordErrorLogger,
- GuildSettingsService guildSettingsService
- )
+ GuildSettingsService guildSettingsService)
{
_options = options.Value;
_logger = logger;
@@ -53,19 +53,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 +80,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 +121,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 +140,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/EventHandlers/EmoteEventHandler.cs b/Kattbot/EventHandlers/EmoteEventHandler.cs
index 07db9f6..8cd07b7 100644
--- a/Kattbot/EventHandlers/EmoteEventHandler.cs
+++ b/Kattbot/EventHandlers/EmoteEventHandler.cs
@@ -3,6 +3,7 @@
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
+using Kattbot.Config;
using Kattbot.NotificationHandlers;
using Kattbot.NotificationHandlers.Emotes;
using Kattbot.Services;
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/Helpers/DiscordExtensions.cs b/Kattbot/Helpers/DiscordExtensions.cs
index f5230e3..c5a63be 100644
--- a/Kattbot/Helpers/DiscordExtensions.cs
+++ b/Kattbot/Helpers/DiscordExtensions.cs
@@ -9,18 +9,19 @@ 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;
}
public static string GetEmojiImageUrl(this DiscordEmoji emoji)
@@ -30,6 +31,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 +61,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 +69,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));
@@ -53,6 +77,11 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji)
return imgUrl;
}
+ private static bool HasLegacyUsername(this DiscordUser user)
+ {
+ return user.Discriminator != "0";
+ }
+
private static string? GetAttachmentOrStickerImage(this DiscordMessage message)
{
if (message.Attachments.Count > 0)
diff --git a/Kattbot/Helpers/StringExtensions.cs b/Kattbot/Helpers/StringExtensions.cs
index ce06eb4..b6aa8ac 100644
--- a/Kattbot/Helpers/StringExtensions.cs
+++ b/Kattbot/Helpers/StringExtensions.cs
@@ -73,4 +73,14 @@ public static string ToSafeFilename(this string input, string extension)
return filename;
}
+
+ public static StringBuilder AppendLines(this StringBuilder sb, IEnumerable lines)
+ {
+ foreach (var line in lines)
+ {
+ sb.AppendLine(line);
+ }
+
+ return sb;
+ }
}
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..d7d8b6b 100644
--- a/Kattbot/NotificationHandlers/KattGptMessageHandler.cs
+++ b/Kattbot/NotificationHandlers/KattGptMessageHandler.cs
@@ -1,16 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text.RegularExpressions;
+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.Configuration;
-using Microsoft.Extensions.Options;
-using TiktokenSharp;
namespace Kattbot.NotificationHandlers;
@@ -18,28 +19,36 @@ public class KattGptMessageHandler : INotificationHandler kattGptOptions,
- KattGptChannelCache cache)
+ KattGptChannelCache cache,
+ KattGptService kattGptService,
+ DalleHttpClient dalleHttpClient,
+ ImageService imageService,
+ DiscordErrorLogger discordErrorLogger)
{
_chatGpt = chatGpt;
- _kattGptOptions = kattGptOptions.Value;
_cache = cache;
-
- _tokenizer = TikToken.EncodingForModel(TokenizerModel);
+ _kattGptService = kattGptService;
+ _dalleHttpClient = dalleHttpClient;
+ _imageService = imageService;
+ _discordErrorLogger = discordErrorLogger;
}
public async Task Handle(MessageCreatedNotification notification, CancellationToken cancellationToken)
@@ -54,49 +63,101 @@ public async Task Handle(MessageCreatedNotification notification, CancellationTo
return;
}
- var systemPromptsMessages = BuildSystemPromptsMessages(channel);
+ var kattGptTokenizer = new KattGptTokenizer(TokenizerModel);
+
+ var systemPromptsMessages = _kattGptService.BuildSystemPromptsMessages(channel);
+
+ var systemMessagesTokenCount = kattGptTokenizer.GetTokenCount(systemPromptsMessages);
+
+ var functionsTokenCount = kattGptTokenizer.GetTokenCount(DalleFunctionBuilder.BuildDalleImageFunctionDefinition());
+
+ var reservedTokens = systemMessagesTokenCount + functionsTokenCount;
- var boundedMessageQueue = GetBoundedMessageQueue(channel, systemPromptsMessages);
+ var boundedMessageQueue = GetBoundedMessageQueue(channel, reservedTokens);
// 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}");
+ bool shouldReplyToMessage = ShouldReplyToMessage(message);
- boundedMessageQueue.Enqueue(newUserMessage, _tokenizer.Encode(newUserMessage.Content).Count);
+ var recipientMarker = shouldReplyToMessage
+ ? RecipientMarkerToYou
+ : RecipientMarkerToOthers;
- if (ShouldReplyToMessage(message))
+ var newUserMessage = ChatCompletionMessage.AsUser($"{newMessageUser}{recipientMarker}: {newMessageContent}");
+
+ boundedMessageQueue.Enqueue(newUserMessage, kattGptTokenizer.GetTokenCount(newUserMessage.Content));
+
+ if (shouldReplyToMessage)
{
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, DefaultTemperature);
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, _tokenizer.Encode(chatGptResponse.Content).Count);
+ // 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, float temperature)
+ {
+ // 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);
@@ -109,96 +170,102 @@ 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)
+ private async Task GetDalleResult(string prompt, string userId)
{
- // Get core system prompt messages
- var coreSystemPrompts = string.Join(" ", _kattGptOptions.CoreSystemPrompts);
- var systemPromptsMessages = new List() { ChatCompletionMessage.AsSystem(coreSystemPrompts) };
+ var response = await _dalleHttpClient.CreateImage(new CreateImageRequest { Prompt = prompt, User = userId });
+ if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result");
- var guild = channel.Guild;
- var guildId = guild.Id;
+ var imageUrl = response.Data.First();
- // 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}");
+ var image = await _imageService.DownloadImage(imageUrl.Url);
- // Get the guild system prompts if they exist
- string[] guildPromptsArray = guildOptions.SystemPrompts ?? Array.Empty();
+ var imageStream = await _imageService.GetImageStream(image);
- // add them to the system prompts messages if not empty
- if (guildPromptsArray.Length > 0)
- {
- string guildSystemPrompts = string.Join(" ", guildPromptsArray);
- systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(guildSystemPrompts));
- }
+ return imageStream;
+ }
- var channelOptions = GetChannelOptions(channel);
+ private async Task HandleFunctionCallResponse(
+ DiscordMessage message,
+ KattGptTokenizer kattGptTokenizer,
+ List systemPromptsMessages,
+ BoundedQueue boundedMessageQueue,
+ ChatCompletionMessage chatGptResponse)
+ {
+ DiscordMessage? workingOnItMessage = null;
- // if there are no channel options, return the system prompts messages
- if (channelOptions == null)
+ try
{
- return systemPromptsMessages;
- }
+ var authorId = message.Author.Id;
- // get the system prompts for this channel
- string[] channelPromptsArray = channelOptions.SystemPrompts ?? Array.Empty();
+ // Force a content value for the chat gpt response due the api not allowing nulls even though it says it does
+ chatGptResponse.Content ??= "null";
- // add them to the system prompts messages if not empty
- if (channelPromptsArray.Length > 0)
- {
- string channelSystemPrompts = string.Join(" ", channelPromptsArray);
- systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(channelSystemPrompts));
- }
+ // Parse and execute function call
+ var functionCallName = chatGptResponse.FunctionCall!.Name;
+ var functionCallArguments = chatGptResponse.FunctionCall.Arguments;
- // 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";
+ var parsedArguments = JsonNode.Parse(functionCallArguments)
+ ?? throw new Exception("Could not parse function call arguments.");
- // get the text template from kattgpt options
- var channelWithTopicTemplate = _kattGptOptions.Templates.Where(x => x.Name == ChannelWithTopicTemplateName).SingleOrDefault();
+ var prompt = parsedArguments["prompt"]?.GetValue()
+ ?? throw new Exception("Function call arguments are invalid.");
- // 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);
+ workingOnItMessage = await message.RespondAsync($"Kattbot used: {prompt}");
- var formatedTemplatePrompt = string.Format(channelWithTopicTemplate.Content, channelName, channelTopic);
- systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(formatedTemplatePrompt));
- }
+ var dalleResult = await GetDalleResult(prompt, authorId.ToString());
- // else use the channelTopic as the system message
- else
+ // Send request with function result
+ var functionCallResult = $"The resulting image file will be attached to your next message.";
+
+ 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 request = BuildRequest(systemPromptsMessages, boundedMessageQueue, FunctionCallTemperature);
+
+ var response = await _chatGpt.ChatCompletionCreate(request);
+
+ // Handle new response
+ var functionCallResponse = response.Choices[0].Message;
+ boundedMessageQueue.Enqueue(functionCallResponse, kattGptTokenizer.GetTokenCount(functionCallResponse.Content));
+
+ await workingOnItMessage.DeleteAsync();
+
+ await SendDalleResultReply(functionCallResponse.Content!, message, prompt, dalleResult);
+ }
+ catch (Exception ex)
+ {
+ if (workingOnItMessage is not null)
{
- systemPromptsMessages.Add(ChatCompletionMessage.AsSystem(channelTopic));
+ await workingOnItMessage.DeleteAsync();
}
- }
- return systemPromptsMessages;
+ 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.
///
/// The channel.
- /// The system prompts messages.
+ /// The token count for the system messages and functions.
/// The bounded message queue for the channel.
- private BoundedQueue GetBoundedMessageQueue(DiscordChannel channel, List systemPromptsMessages)
+ private BoundedQueue GetBoundedMessageQueue(DiscordChannel channel, int reservedTokenCount)
{
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 = MaxTotalTokens - MaxTokensToGenerate - reservedTokenCount;
boundedMessageQueue = new BoundedQueue(remainingTokensForContextMessages);
}
@@ -226,7 +293,7 @@ private bool ShouldHandleMessage(DiscordMessage message)
{
var channel = message.Channel;
- var channelOptions = GetChannelOptions(channel);
+ var channelOptions = _kattGptService.GetChannelOptions(channel);
if (channelOptions == null)
{
@@ -240,7 +307,7 @@ private bool ShouldHandleMessage(DiscordMessage message)
}
// otherwise check if the message does not start with the MetaMessagePrefix
- var messageStartsWithMetaMessagePrefix = message.Content.StartsWith(MetaMessagePrefix);
+ var messageStartsWithMetaMessagePrefix = message.Content.TrimStart().StartsWith(MetaMessagePrefix);
// if it does, return false
return !messageStartsWithMetaMessagePrefix;
@@ -255,7 +322,7 @@ private bool ShouldReplyToMessage(DiscordMessage message)
{
var channel = message.Channel;
- var channelOptions = GetChannelOptions(channel);
+ var channelOptions = _kattGptService.GetChannelOptions(channel);
if (channelOptions == null)
{
@@ -280,38 +347,9 @@ private bool ShouldReplyToMessage(DiscordMessage message)
}
// otherwise check if the message does not start with the MetaMessagePrefix
- var messageStartsWithMetaMessagePrefix = message.Content.StartsWith(MetaMessagePrefix);
+ var messageStartsWithMetaMessagePrefix = message.Content.TrimStart().StartsWith(MetaMessagePrefix);
// 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..858ba6c 100644
--- a/Kattbot/Program.cs
+++ b/Kattbot/Program.cs
@@ -2,6 +2,7 @@
using System.Threading.Channels;
using DSharpPlus;
using Kattbot.CommandHandlers;
+using Kattbot.Config;
using Kattbot.Data;
using Kattbot.Data.Repositories;
using Kattbot.EventHandlers;
@@ -74,7 +75,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);
@@ -124,6 +125,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/Dalle/DalleHttpClient.cs b/Kattbot/Services/Dalle/DalleHttpClient.cs
index 56b4795..69794dd 100644
--- a/Kattbot/Services/Dalle/DalleHttpClient.cs
+++ b/Kattbot/Services/Dalle/DalleHttpClient.cs
@@ -5,7 +5,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
-using Kattbot.Services.KattGpt;
+using Kattbot.Common.Models.KattGpt;
+using Kattbot.Config;
using Microsoft.Extensions.Options;
namespace Kattbot.Services.Dalle;
diff --git a/Kattbot/Services/DiscordErrorLogger.cs b/Kattbot/Services/DiscordErrorLogger.cs
index e9a62cb..bfb2ebb 100644
--- a/Kattbot/Services/DiscordErrorLogger.cs
+++ b/Kattbot/Services/DiscordErrorLogger.cs
@@ -1,5 +1,7 @@
using System;
using DSharpPlus.CommandsNext;
+using Kattbot.Config;
+using Kattbot.Helpers;
using Kattbot.NotificationHandlers;
using Kattbot.Workers;
using Microsoft.Extensions.Options;
@@ -19,9 +21,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 +36,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/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;
- }
}
diff --git a/Kattbot/Services/KattGpt/ChatGptClient.cs b/Kattbot/Services/KattGpt/ChatGptClient.cs
index 0ac94fb..73d4b9d 100644
--- a/Kattbot/Services/KattGpt/ChatGptClient.cs
+++ b/Kattbot/Services/KattGpt/ChatGptClient.cs
@@ -5,7 +5,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
-using Kattbot.Services.Dalle;
+using Kattbot.Common.Models.KattGpt;
+using Kattbot.Config;
using Microsoft.Extensions.Options;
namespace Kattbot.Services.KattGpt;
diff --git a/Kattbot/Services/KattGpt/DalleFunctionBuilder.cs b/Kattbot/Services/KattGpt/DalleFunctionBuilder.cs
new file mode 100644
index 0000000..fa38ecc
--- /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. The prompts should be written in English for the best results.",
+ 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/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
new file mode 100644
index 0000000..8e7d612
--- /dev/null
+++ b/Kattbot/Services/KattGpt/KattGptService.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using DSharpPlus.Entities;
+using Kattbot.Common.Models.KattGpt;
+using Kattbot.Config;
+using Kattbot.Helpers;
+using Microsoft.Extensions.Options;
+
+namespace Kattbot.Services.KattGpt;
+
+public class KattGptService
+{
+ private const string ChannelContextWithTopicTemplate = "ChannelContextWithTopic";
+ private const string ChannelContextWithoutTopicTemplate = "ChannelContextWithoutTopic";
+ private const string ChannelGuidelinesHeaderTemplate = "ChannelGuidelines";
+
+ private const string TemplateGuildNameToken = "{guildName}";
+ private const string TemplateChannelNameToken = "{channelName}";
+ private const string TemplateChannelTopicToken = "{channelTopic}";
+
+ 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)
+ {
+ var systemPromptBuilder = new StringBuilder();
+
+ var coreSystemPromptStringsa = _kattGptOptions.CoreSystemPrompts.ToList();
+ systemPromptBuilder.AppendLines(coreSystemPromptStringsa);
+
+ 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 display name
+ var guildDisplayName = guildOptions.Name ?? channel.Guild.Name;
+
+ var replaceArgs = new Dictionary
+ {
+ { TemplateGuildNameToken, guildDisplayName },
+ };
+
+ var channelOptions = GetChannelOptions(channel);
+
+ // if there are no channel options, return the system prompts messages
+ if (channelOptions != null)
+ {
+ // get a sanitized channel name that only includes letters, digits, - and _
+ var channelDisplayName = Regex.Replace(channel.Name, @"[^a-zA-Z0-9-_]", string.Empty);
+
+ var channelTopic = channelOptions.Topic is not null
+ ? channelOptions.Topic
+ : channelOptions.FallbackToChannelTopic && !string.IsNullOrWhiteSpace(channel.Topic)
+ ? channel.Topic
+ : null;
+
+ var channelContextTemplateName = channelTopic is not null
+ ? ChannelContextWithTopicTemplate
+ : ChannelContextWithoutTopicTemplate;
+
+ var channelContextTemplate = _kattGptOptions.Templates.Where(x => x.Name == channelContextTemplateName).SingleOrDefault();
+
+ if (channelContextTemplate is not null)
+ {
+ systemPromptBuilder.AppendLine();
+ systemPromptBuilder.AppendLine(channelContextTemplate.Content);
+ }
+
+ var headerTemplate = _kattGptOptions.Templates.Where(x => x.Name == ChannelGuidelinesHeaderTemplate).SingleOrDefault();
+
+ // get the system prompts for this channel
+ string[] channelPromptStrings = channelOptions.SystemPrompts ?? Array.Empty();
+
+ if (headerTemplate is not null && channelPromptStrings.Length > 0)
+ {
+ systemPromptBuilder.AppendLine();
+ systemPromptBuilder.AppendLine(headerTemplate.Content);
+ systemPromptBuilder.AppendLines(channelPromptStrings);
+ }
+
+ replaceArgs.Add(TemplateChannelNameToken, channelDisplayName);
+ replaceArgs.Add(TemplateChannelTopicToken, channelTopic ?? string.Empty);
+ }
+
+ var systemPromptsString = systemPromptBuilder.ToString();
+
+ // replace variables
+ foreach (var (key, value) in replaceArgs)
+ {
+ systemPromptsString = systemPromptsString.Replace(key, value);
+ }
+
+ var systemPromptsMessages = new List() { ChatCompletionMessage.AsSystem(systemPromptsString) };
+
+ 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 is not null)
+ {
+ channelOptions = guildCategoryOptions.Where(x => x.Id == category.Id).SingleOrDefault();
+ }
+ }
+
+ return channelOptions;
+ }
+}
diff --git a/Kattbot/Services/KattGpt/KattGptTokenizer.cs b/Kattbot/Services/KattGpt/KattGptTokenizer.cs
new file mode 100644
index 0000000..9caec7f
--- /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(ChatCompletionFunction functionCall)
+ {
+ var tokenizer = TikToken.EncodingForModel(_modelName);
+
+ var nameTokenCount = tokenizer.Encode(functionCall.Name).Count;
+ var argumentsTokenCount = tokenizer.Encode(functionCall.Parameters.ToString()).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;
+ }
+}
diff --git a/Kattbot/Workers/BotWorker.cs b/Kattbot/Workers/BotWorker.cs
index 24a791e..45fd286 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;
@@ -7,6 +8,7 @@
using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using Kattbot.CommandModules.TypeReaders;
+using Kattbot.Config;
using Kattbot.EventHandlers;
using Kattbot.NotificationHandlers;
using Microsoft.Extensions.Hosting;
@@ -47,12 +49,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,
@@ -63,12 +64,12 @@ public async Task StartAsync(CancellationToken cancellationToken)
_client.SocketOpened += OnClientConnected;
_client.SocketClosed += OnClientDisconnected;
- _client.Ready += OnClientReady;
+ _client.SessionCreated += OnClientReady;
_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 +81,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;
@@ -89,6 +90,18 @@ private async Task OnMessageCreated(MessageCreateEventArgs args, CancellationTok
return;
}
+ // Ignore messages from DMs
+ if (args.Guild is null)
+ {
+ 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);
}
@@ -106,7 +119,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");
diff --git a/Kattbot/appsettings.Development.json b/Kattbot/appsettings.Development.json
index f5e9219..3feb576 100644
--- a/Kattbot/appsettings.Development.json
+++ b/Kattbot/appsettings.Development.json
@@ -19,32 +19,40 @@
"KattGpt": {
"GuildOptions": [
{
- "_name": "The Cozy Climber Cat Tower",
+ "Name": "The Cozy Climber Cat Tower",
"Id": "753161640496857149",
- "SystemPrompts": [
- "The name of the Discord server is \"The Cozy Climber Cat Tower\"."
- ],
"CategoryOptions": [
{
"_name": "KattGpt-dev",
"Id": "1124778131753025636",
- "UseChannelTopic": true
+ "FallbackToChannelTopic": true
},
{
"_name": "Hobbies-Dev",
"Id": "1124778516991463464",
- "UseChannelTopic": true
+ "FallbackToChannelTopic": true
}
],
"ChannelOptions": [
{
"_name": "gpt-always-on",
"Id": "1080612733222920332",
+ "Topic": "lolcat or lolspeak",
+ "SystemPrompts": [
+ "- In this channel all the users, including you, must write in lolcat.",
+ "- Lolspeak is a form of speaking that doesn't use proper English, often using a \"z\" instead of an \"s\" and re-arrangement of syntax.",
+ "- For example, someone may say \"I can haz cheezburgr?\" in lolspeak instead of saying \"Can I have a cheese burger?\" while typing."
+ ],
+ "AlwaysOn": true
+ },
+ {
+ "_name": "norsk-test",
+ "Id": "1158017236070576210",
+ "Topic": "Norwegian culture and the Norwegian language",
"SystemPrompts": [
- "You are now talking in \"gpt-always-on\" channel. The topic of this channel lolcat or lolspeak.",
- "In this channel all the users, including you, must write in lolcat.",
- "Lolspeak is a form of speaking that doesn't use proper English, often using a \"z\" instead of an \"s\" and re-arrangement of syntax.",
- "For example, someone may say \"I can haz cheezburgr?\" in lolspeak instead of saying \"Can I have a cheese burger?\" while typing."
+ "- In this channel all the users, including you, must write in Norwegian.",
+ "- You are allowed to answer messages written in languages other than Norwegian, but your answer will always be written in Norwegian.",
+ "- When someone is writing in a different language than Norwegian, you will gently remind that they should be writing Norwegian."
],
"AlwaysOn": true
}
diff --git a/Kattbot/appsettings.json b/Kattbot/appsettings.json
index 2721baa..89731e4 100644
--- a/Kattbot/appsettings.json
+++ b/Kattbot/appsettings.json
@@ -18,73 +18,99 @@
},
"KattGpt": {
"CoreSystemPrompts": [
- "Your name is Kattbot. You act as a talking robot-cat. Your favorite color is indigo.",
- "You are a member of a Discord server and you communicate with multiple users in the same chat channel.",
- "Messages from other users will be prefixed by the name of the user. Example for a user named \"Bob\": \"Bob: Hello, how are you?\".",
- "Your response only includes the message without a name prefix. Example: \"Hey, Bob. I'm doing fine. How about you?\"."
+ "You are a conversational partner within a Discord server, operating under the guise of Kattbot, a talking robot-cat. ",
+ "Your function is to facilitate interesting and entertaining interactions.",
+ "The name of the Discord server is \"{guildName}\".",
+ "",
+ "**Persona and Characteristics:**",
+ "- Identity: You are Kattbot, a talking robot-cat. Refrain from identifying as an AI Assistant or a Language Model unless necessary.",
+ "- Tone: Maintain an informal, friendly, and engaging tone in interactions. Be approachable, interesting, and entertaining.",
+ "- Image Generation: You can visually illustrate ideas through your AI Image Generator function, only using generated images, not sourced from elsewhere.",
+ "",
+ "**Communication in Discord Channels:**",
+ "- Interaction: Only engage in conversations when directly addressed by users.",
+ "- Addressing: Messages from users will be prefixed with their name and the recipient marker ([to others] or [to you]).",
+ " - Example: \"Alice[to others]: Hello, everyone! What’s up?\".",
+ " - Example: \"Bob[to you]: Hello, how are you?\".",
+ "- Response: Respond without prefixing your message with your name or a recipient marker. Use the name of the person addressing you to make the interaction more personal.",
+ " - Example: \"Hey, Bob! I’m doing quite well. How about you?\"",
+ "",
+ "**General Communication Guidelines:**",
+ "- Engage: Aim to be active and reactive, keeping conversations flowing and enjoyable.",
+ "- Creativity: Leverage your Image Generator to create visually appealing and relevant content, adding a dynamic layer to interactions.",
+ "- Clarification: If a message seems ambiguous or unclear, seek clarification politely.",
+ "- Informative: Share engaging and fascinating tidbits, facts, or anecdotes when appropriate.",
+ "",
+ "**Image Attachment Guidelines:**",
+ "- When attaching an image that you generate, do not include textual indicators like \"[Attaches image]\" or \"[Generated image]\" within the message. The attached image itself will suffice.",
+ " - Example: ",
+ " - User: \"John[to you]: Can you draw a cat sitting on a moon?\"",
+ " - Kattbot: \"Sure thing, John! Here’s your cat lounging on a moon!\""
],
"Templates": [
{
- "Name": "ChannelWithTopic",
- "Content": "You are now talking in \"{0}\" channel. The topic of this channel is \"{1}\"."
+ "Name": "ChannelContextWithTopic",
+ "Content": "**Current Channel Context:**\r\n- Channel Name: {channelName}\r\n- Channel Topic: {channelTopic}"
+ },
+ {
+ "Name": "ChannelContextWithoutTopic",
+ "Content": "**Current Channel Context:**\r\n- Channel Name: {channelName}"
+ },
+ {
+ "Name": "ChannelGuidelines",
+ "Content": "**Interaction Guidelines for {channelName} channel:**"
}
],
"GuildOptions": [
{
- "_name": "The Cozy Climber Cat Tower",
- "Id": "753161640496857149",
- "SystemPrompts": [
- "The name of the Discord server is \"The Cozy Climber Cat Tower\"."
- ]
+ "Name": "The Cozy Climber Cat Tower",
+ "Id": "753161640496857149"
},
{
- "_name": "NELLE",
+ "Name": "Norwegian-English Language Learning Exchange (commonly referred to as NELLE)",
"Id": "622141497332072449",
- "SystemPrompts": [
- "The name of the Discord server is \"Norwegian-English Language Learning Exchange\" commonly referred to as NELLE."
- ],
"CategoryOptions": [
{
"_name": "General chat",
"Id": "622141497332072451",
- "UseChannelTopic": true
+ "FallbackToChannelTopic": true
},
{
"_name": "Hobbies & Interests",
"Id": "624877481312124959",
- "UseChannelTopic": true
+ "FallbackToChannelTopic": true
}
],
"ChannelOptions": [
{
"_name": "lettloff",
"Id": "622146382278557696",
+ "Topic": "Norwegian for beginners",
"SystemPrompts": [
- "You are now talking in \"lettloff\" channel.",
- "Your goal is to help learners practice writing and reading Norwegian by discussing various topics. Keep sentences short and simple.",
- "In this channel all the users, including you, must write in Norwegian.",
- "You are allowed to answer messages written in languages other than Norwegian, but your answer will always be written in Norwegian.",
- "When someone is writing in a language other than Norwegian, you will gently remind that they should be writing Norwegian."
+ "- Your goal is to help learners practice writing and reading Norwegian by discussing various topics. Keep sentences short and simple.",
+ "- In this channel all the users, including you, must write in Norwegian.",
+ "- You are allowed to answer messages written in languages other than Norwegian, but your answer will always be written in Norwegian.",
+ "- When someone is writing in a language other than Norwegian, you will gently remind that they should be writing Norwegian."
]
},
{
"_name": "Linguistics",
"Id": "622158332366684173",
- "UseChannelTopic": true
+ "FallbackToChannelTopic": true
},
{
"_name": "Study discussion",
"Id": "654757921493876766",
- "UseChannelTopic": true
+ "FallbackToChannelTopic": true
},
{
"_name": "katt-gpt",
"Id": "1080628923634819123",
+ "Topic": "Norwegian culture and the Norwegian language",
"SystemPrompts": [
- "You are now talking in \"katt-gpt\" channel. The topic of this channel is Norwegian culture and the Norwegian language.",
- "In this channel all the users, including you, must write in Norwegian.",
- "You are allowed to answer messages written in languages other than Norwegian, but your answer will always be written in Norwegian.",
- "When someone is writing in a different language than Norwegian, you will gently remind that they should be writing Norwegian."
+ "- In this channel all the users, including you, must write in Norwegian.",
+ "- You are allowed to answer messages written in languages other than Norwegian, but your answer will always be written in Norwegian.",
+ "- When someone is writing in a different language than Norwegian, you will gently remind that they should be writing Norwegian."
],
"AlwaysOn": true
}