From ddf5bfdd3de1833e35eb9af4214f6aadc4799a0a Mon Sep 17 00:00:00 2001 From: Myx Date: Sat, 1 Jun 2024 13:16:19 +0200 Subject: [PATCH] Working LLM chatbot --- Bot/Handler/ChatCommand/ChatHandler.cs | 58 +++++++++---------- Bot/Handler/ChatCommand/ChatSettings.cs | 7 +++ Bot/Handler/MessageReceivedHandler.cs | 50 ++++++++-------- Bot/Handler/SlashCommandReceivedHandler.cs | 34 +++++++++++ Bot/Notification/DiscordEventListener.cs | 10 +++- .../MessageReceivedNotification.cs | 4 +- .../SlashCommandReceivedNotification.cs | 9 +++ Bot/Program.cs | 5 +- Bot/SlashCommand/Command.cs | 17 ------ Bot/SlashCommand/SlashCommandRegistration.cs | 3 +- Bot/appsettings.json | 4 ++ 11 files changed, 122 insertions(+), 79 deletions(-) create mode 100644 Bot/Handler/ChatCommand/ChatSettings.cs create mode 100644 Bot/Handler/SlashCommandReceivedHandler.cs create mode 100644 Bot/Notification/SlashCommandReceivedNotification.cs diff --git a/Bot/Handler/ChatCommand/ChatHandler.cs b/Bot/Handler/ChatCommand/ChatHandler.cs index d208ee2..d9c6c1f 100644 --- a/Bot/Handler/ChatCommand/ChatHandler.cs +++ b/Bot/Handler/ChatCommand/ChatHandler.cs @@ -1,64 +1,62 @@ using System.Text; using Discord.WebSocket; -using Lunaris2.Handler.MusicPlayer; -using Lunaris2.SlashCommand; using MediatR; +using Microsoft.Extensions.Options; using OllamaSharp; -using OllamaSharp.Models; namespace Lunaris2.Handler.ChatCommand { - public record ChatCommand(SocketSlashCommand Message) : IRequest; + public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest; public class ChatHandler : IRequestHandler - { - private readonly Uri _uri = new("http://192.168.50.54:11434"); + { private readonly OllamaApiClient _ollama; - private SocketSlashCommand _context; - - public ChatHandler() + private readonly Dictionary _chatContexts = new(); + + public ChatHandler(IOptions chatSettings) { - _ollama = new OllamaApiClient(_uri) + var uri = new Uri(chatSettings.Value.Url); + + _ollama = new OllamaApiClient(uri) { - SelectedModel = "lunaris" + SelectedModel = chatSettings.Value.Model }; } public async Task Handle(ChatCommand command, CancellationToken cancellationToken) { - _context = command.Message; - - var userMessage = _context.GetOptionValueByName(Option.Input); + var channelId = command.Message.Channel.Id; + _chatContexts.TryAdd(channelId, null); + + var userMessage = command.FilteredMessage; using var setTyping = command.Message.Channel.EnterTypingState(); - await command.Message.DeferAsync(); if (string.IsNullOrWhiteSpace(userMessage)) { - await command.Message.ModifyOriginalResponseAsync(properties => properties.Content = "Am I expected to read your mind?"); + await command.Message.Channel.SendMessageAsync("Am I expected to read your mind?"); setTyping.Dispose(); return; } - var response = await GenerateResponse(userMessage, cancellationToken); - await command.Message.ModifyOriginalResponseAsync(properties => properties.Content = response); + var response = await GenerateResponse(userMessage, channelId, cancellationToken); + await command.Message.Channel.SendMessageAsync(response); + + setTyping.Dispose(); } - private async Task GenerateResponse(string userMessage, CancellationToken cancellationToken) + private async Task GenerateResponse(string userMessage, ulong channelId, CancellationToken cancellationToken) { var response = new StringBuilder(); - ConversationContext? chatContext = null; - chatContext = await _ollama.StreamCompletion( - userMessage, - chatContext, - Streamer, - cancellationToken: cancellationToken); - + if (_chatContexts[channelId] == null) + { + _chatContexts[channelId] = _ollama.Chat(stream => response.Append(stream.Message?.Content ?? "")); + } + + await _chatContexts[channelId].Send(userMessage, cancellationToken); + return response.ToString(); - - void Streamer(GenerateCompletionResponseStream stream) => - response.Append(stream.Response); } } -} \ No newline at end of file +} diff --git a/Bot/Handler/ChatCommand/ChatSettings.cs b/Bot/Handler/ChatCommand/ChatSettings.cs new file mode 100644 index 0000000..48697fc --- /dev/null +++ b/Bot/Handler/ChatCommand/ChatSettings.cs @@ -0,0 +1,7 @@ +namespace Lunaris2.Handler.ChatCommand; + +public class ChatSettings +{ + public string Url { get; set; } + public string Model { get; set; } +} \ No newline at end of file diff --git a/Bot/Handler/MessageReceivedHandler.cs b/Bot/Handler/MessageReceivedHandler.cs index 6eecaeb..65d36ee 100644 --- a/Bot/Handler/MessageReceivedHandler.cs +++ b/Bot/Handler/MessageReceivedHandler.cs @@ -1,37 +1,37 @@ -using Lunaris2.Handler.GoodByeCommand; -using Lunaris2.Handler.MusicPlayer.JoinCommand; -using Lunaris2.Handler.MusicPlayer.PlayCommand; -using Lunaris2.Handler.MusicPlayer.SkipCommand; +using System.Text.RegularExpressions; +using Discord.WebSocket; using Lunaris2.Notification; -using Lunaris2.SlashCommand; using MediatR; namespace Lunaris2.Handler; -public class MessageReceivedHandler(ISender mediator) : INotificationHandler +public class MessageReceivedHandler : INotificationHandler { + private readonly DiscordSocketClient _client; + private readonly ISender _mediatir; + + public MessageReceivedHandler(DiscordSocketClient client, ISender mediatir) + { + _client = client; + _mediatir = mediatir; + } + public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) { - switch (notification.Message.CommandName) + await BotMentioned(notification, cancellationToken); + } + + private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken) + { + if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id)) { - case Command.Hello.Name: - await mediator.Send(new HelloCommand.HelloCommand(notification.Message), cancellationToken); - break; - case Command.Goodbye.Name: - await mediator.Send(new GoodbyeCommand(notification.Message), cancellationToken); - break; - case Command.Join.Name: - await mediator.Send(new JoinCommand(notification.Message), cancellationToken); - break; - case Command.Play.Name: - await mediator.Send(new PlayCommand(notification.Message), cancellationToken); - break; - case Command.Skip.Name: - await mediator.Send(new SkipCommand(notification.Message), cancellationToken); - break; - case Command.Chat.Name: - await mediator.Send(new ChatCommand.ChatCommand(notification.Message), cancellationToken); - break; + // The bot was mentioned + const string pattern = "<.*?>"; + const string replacement = ""; + var regex = new Regex(pattern); + var messageContent = regex.Replace(notification.Message.Content, replacement); + + await _mediatir.Send(new ChatCommand.ChatCommand(notification.Message, messageContent), cancellationToken); } } } \ No newline at end of file diff --git a/Bot/Handler/SlashCommandReceivedHandler.cs b/Bot/Handler/SlashCommandReceivedHandler.cs new file mode 100644 index 0000000..3589173 --- /dev/null +++ b/Bot/Handler/SlashCommandReceivedHandler.cs @@ -0,0 +1,34 @@ +using Lunaris2.Handler.GoodByeCommand; +using Lunaris2.Handler.MusicPlayer.JoinCommand; +using Lunaris2.Handler.MusicPlayer.PlayCommand; +using Lunaris2.Handler.MusicPlayer.SkipCommand; +using Lunaris2.Notification; +using Lunaris2.SlashCommand; +using MediatR; + +namespace Lunaris2.Handler; + +public class SlashCommandReceivedHandler(ISender mediator) : INotificationHandler +{ + public async Task Handle(SlashCommandReceivedNotification notification, CancellationToken cancellationToken) + { + switch (notification.Message.CommandName) + { + case Command.Hello.Name: + await mediator.Send(new HelloCommand.HelloCommand(notification.Message), cancellationToken); + break; + case Command.Goodbye.Name: + await mediator.Send(new GoodbyeCommand(notification.Message), cancellationToken); + break; + case Command.Join.Name: + await mediator.Send(new JoinCommand(notification.Message), cancellationToken); + break; + case Command.Play.Name: + await mediator.Send(new PlayCommand(notification.Message), cancellationToken); + break; + case Command.Skip.Name: + await mediator.Send(new SkipCommand(notification.Message), cancellationToken); + break; + } + } +} \ No newline at end of file diff --git a/Bot/Notification/DiscordEventListener.cs b/Bot/Notification/DiscordEventListener.cs index 6c4bcde..2d80b42 100644 --- a/Bot/Notification/DiscordEventListener.cs +++ b/Bot/Notification/DiscordEventListener.cs @@ -19,13 +19,19 @@ public class DiscordEventListener(DiscordSocketClient client, IServiceScopeFacto public async Task StartAsync() { - client.SlashCommandExecuted += OnMessageReceivedAsync; + client.SlashCommandExecuted += OnSlashCommandRecievedAsync; + client.MessageReceived += OnMessageReceivedAsync; await Task.CompletedTask; } - private async Task OnMessageReceivedAsync(SocketSlashCommand arg) + private async Task OnMessageReceivedAsync(SocketMessage arg) { await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); } + + private async Task OnSlashCommandRecievedAsync(SocketSlashCommand arg) + { + await Mediator.Publish(new SlashCommandReceivedNotification(arg), _cancellationToken); + } } \ No newline at end of file diff --git a/Bot/Notification/MessageReceivedNotification.cs b/Bot/Notification/MessageReceivedNotification.cs index 63802bf..2008a17 100644 --- a/Bot/Notification/MessageReceivedNotification.cs +++ b/Bot/Notification/MessageReceivedNotification.cs @@ -3,7 +3,7 @@ using MediatR; namespace Lunaris2.Notification; -public class MessageReceivedNotification(SocketSlashCommand message) : INotification +public class MessageReceivedNotification(SocketMessage message) : INotification { - public SocketSlashCommand Message { get; } = message ?? throw new ArgumentNullException(nameof(message)); + public SocketMessage Message { get; } = message ?? throw new ArgumentNullException(nameof(message)); } \ No newline at end of file diff --git a/Bot/Notification/SlashCommandReceivedNotification.cs b/Bot/Notification/SlashCommandReceivedNotification.cs new file mode 100644 index 0000000..e0358da --- /dev/null +++ b/Bot/Notification/SlashCommandReceivedNotification.cs @@ -0,0 +1,9 @@ +using Discord.WebSocket; +using MediatR; + +namespace Lunaris2.Notification; + +public class SlashCommandReceivedNotification(SocketSlashCommand message) : INotification +{ + public SocketSlashCommand Message { get; } = message ?? throw new ArgumentNullException(nameof(message)); +} \ No newline at end of file diff --git a/Bot/Program.cs b/Bot/Program.cs index ffd49a7..5f68df1 100644 --- a/Bot/Program.cs +++ b/Bot/Program.cs @@ -3,6 +3,7 @@ using Discord; using Discord.Commands; using Discord.Interactions; using Discord.WebSocket; +using Lunaris2.Handler.ChatCommand; using Lunaris2.Handler.MusicPlayer; using Lunaris2.Notification; using Lunaris2.SlashCommand; @@ -54,7 +55,9 @@ public class Program nodeConfiguration.Authorization = configuration["LavaLinkPassword"]; }) .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .Configure(configuration.GetSection("LLM")); client.Ready += () => Client_Ready(client); client.Log += Log; diff --git a/Bot/SlashCommand/Command.cs b/Bot/SlashCommand/Command.cs index ff2cde6..1c7cd3a 100644 --- a/Bot/SlashCommand/Command.cs +++ b/Bot/SlashCommand/Command.cs @@ -21,23 +21,6 @@ public static class Command public const string Description = "Say goodbye to the bot!"; } - public static class Chat - { - public const string Name = "chat"; - public const string Description = "Chat with the bot!"; - - public static readonly List? Options = new() - { - new SlashCommandOptionBuilder - { - Name = "message", - Description = "Chat with Lunaris", - Type = ApplicationCommandOptionType.String, - IsRequired = true - } - }; - } - public static class Join { public const string Name = "join"; diff --git a/Bot/SlashCommand/SlashCommandRegistration.cs b/Bot/SlashCommand/SlashCommandRegistration.cs index 7770141..11b5f58 100644 --- a/Bot/SlashCommand/SlashCommandRegistration.cs +++ b/Bot/SlashCommand/SlashCommandRegistration.cs @@ -12,8 +12,7 @@ public static class SlashCommandRegistration RegisterCommand(client, Command.Join.Name, Command.Join.Description); RegisterCommand(client, Command.Skip.Name, Command.Skip.Description); RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options); - RegisterCommand(client, Command.Stop.Name, Command.Stop.Description); - RegisterCommand(client, Command.Chat.Name, Command.Chat.Description, Command.Chat.Options); + RegisterCommand(client, Command.Stop.Name, Command.Stop.Description); } private static void RegisterCommand( diff --git a/Bot/appsettings.json b/Bot/appsettings.json index 9901e00..9c46724 100644 --- a/Bot/appsettings.json +++ b/Bot/appsettings.json @@ -10,4 +10,8 @@ "LavaLinkPassword": "youshallnotpass", "LavaLinkHostname": "127.0.0.1", "LavaLinkPort": 2333 + "LLM": { + "Url": "http://192.168.50.54:11434", + "Model": "gemma" + } }