Compare commits

..

15 Commits

Author SHA1 Message Date
f8e6854569 Create readme.md 2024-06-02 00:04:14 +02:00
03150a3d04 Update README.md 2024-06-01 23:53:15 +02:00
32b6e09336 Update README.md 2024-06-01 23:42:17 +02:00
e3df4505fe Update README.md 2024-06-01 23:37:47 +02:00
3daf18e053 Update README.md 2024-06-01 23:36:07 +02:00
54c5c68ba6 Update README.md 2024-06-01 23:35:40 +02:00
e16ff9cfaf Update README.md 2024-06-01 23:35:16 +02:00
a1d20fd732 Add chat functionality (#1)
* Working chatbot

* Clean

* Working LLM chatbot

---------

Co-authored-by: Myx <info@azaaxin.com>
2024-06-01 23:22:47 +02:00
3d7655a902 Update readme.md 2024-04-15 00:00:38 +02:00
8ddcf31da7 Update readme.md 2024-04-14 23:59:03 +02:00
80a7c19b20 Update readme.md 2024-04-14 23:58:10 +02:00
713715901b Create readme.md 2024-04-14 23:54:26 +02:00
1a3a00f4ed Update README.md 2024-04-14 23:33:44 +02:00
Myx
0673653491 Fix broken lavalink version 2024-04-14 23:02:51 +02:00
a650fd431e Create appsettings.json 2024-04-14 23:01:58 +02:00
20 changed files with 256 additions and 45 deletions

View File

@@ -0,0 +1,62 @@
using System.Text;
using Discord.WebSocket;
using MediatR;
using Microsoft.Extensions.Options;
using OllamaSharp;
namespace Lunaris2.Handler.ChatCommand
{
public record ChatCommand(SocketMessage Message, string FilteredMessage) : IRequest;
public class ChatHandler : IRequestHandler<ChatCommand>
{
private readonly OllamaApiClient _ollama;
private readonly Dictionary<ulong, Chat?> _chatContexts = new();
public ChatHandler(IOptions<ChatSettings> chatSettings)
{
var uri = new Uri(chatSettings.Value.Url);
_ollama = new OllamaApiClient(uri)
{
SelectedModel = chatSettings.Value.Model
};
}
public async Task Handle(ChatCommand command, CancellationToken cancellationToken)
{
var channelId = command.Message.Channel.Id;
_chatContexts.TryAdd(channelId, null);
var userMessage = command.FilteredMessage;
using var setTyping = command.Message.Channel.EnterTypingState();
if (string.IsNullOrWhiteSpace(userMessage))
{
await command.Message.Channel.SendMessageAsync("Am I expected to read your mind?");
setTyping.Dispose();
return;
}
var response = await GenerateResponse(userMessage, channelId, cancellationToken);
await command.Message.Channel.SendMessageAsync(response);
setTyping.Dispose();
}
private async Task<string> GenerateResponse(string userMessage, ulong channelId, CancellationToken cancellationToken)
{
var response = new StringBuilder();
if (_chatContexts[channelId] == null)
{
_chatContexts[channelId] = _ollama.Chat(stream => response.Append(stream.Message?.Content ?? ""));
}
await _chatContexts[channelId].Send(userMessage, cancellationToken);
return response.ToString();
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Lunaris2.Handler.ChatCommand;
public class ChatSettings
{
public string Url { get; set; }
public string Model { get; set; }
}

View File

@@ -0,0 +1,8 @@
## Ollama - Large Language Model Chat - Handler
This handler "owns" the logic for accessing the ollama api, which runs the transformer model.
> How to get started with a local chat bot see: [Run LLMs Locally using Ollama](https://marccodess.medium.com/run-llms-locally-using-ollama-8f04dd9b14f9)
Assuming you are on the same network as the Ollama server you should configure it to be accessible to other machines on the network, however this is only required if you aren't running it from localhost relative to the bot.
See: [How do I configure Ollama server?](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)

View File

@@ -1,34 +1,37 @@
using Lunaris2.Handler.GoodByeCommand; using System.Text.RegularExpressions;
using Lunaris2.Handler.MusicPlayer.JoinCommand; using Discord.WebSocket;
using Lunaris2.Handler.MusicPlayer.PlayCommand;
using Lunaris2.Handler.MusicPlayer.SkipCommand;
using Lunaris2.Notification; using Lunaris2.Notification;
using Lunaris2.SlashCommand;
using MediatR; using MediatR;
namespace Lunaris2.Handler; namespace Lunaris2.Handler;
public class MessageReceivedHandler(ISender mediator) : INotificationHandler<MessageReceivedNotification> public class MessageReceivedHandler : INotificationHandler<MessageReceivedNotification>
{ {
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) public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken)
{ {
switch (notification.Message.CommandName) await BotMentioned(notification, cancellationToken);
}
private async Task BotMentioned(MessageReceivedNotification notification, CancellationToken cancellationToken)
{ {
case Command.Hello.Name: if (notification.Message.MentionedUsers.Any(user => user.Id == _client.CurrentUser.Id))
await mediator.Send(new HelloCommand.HelloCommand(notification.Message), cancellationToken); {
break; // The bot was mentioned
case Command.Goodbye.Name: const string pattern = "<.*?>";
await mediator.Send(new GoodbyeCommand(notification.Message), cancellationToken); const string replacement = "";
break; var regex = new Regex(pattern);
case Command.Join.Name: var messageContent = regex.Replace(notification.Message.Content, replacement);
await mediator.Send(new JoinCommand(notification.Message), cancellationToken);
break; await _mediatir.Send(new ChatCommand.ChatCommand(notification.Message, messageContent), cancellationToken);
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;
} }
} }
} }

View File

@@ -0,0 +1,35 @@
```mermaid
flowchart TD
PlayHandler --> EnsureConnected
EnsureConnected --> GetPlayer
GetPlayer --> SearchAsync
SearchAsync --> SearchResponse
SearchResponse --> PlayTrack
PlayTrack --> NowPlayingEmbed
```
## Steps in the code
| Name | Description |
|--|--|
| PlayHandler | Holds the logic for playing songs |
| GetPlayer | Joins voice channel, produces chat resposne |
| EnsureConnected | Makes sure the client is connected |
| SearchAsync | Searches for songs information |
| SearchResponse | Handling possible errors from the response of SearchAsync |
| PlayTrack | Plays the song |
There is also OnTrackEnd, when it get called an attempt is made to play the next song in queue.
## Short explaination for some of the variables used:
| Variable | Type | Description |
| --- | --- | --- |
| `_lavaNode` | `LavaNode` | An instance of the `LavaNode` class, used to interact with the LavaLink server for playing music in Discord voice channels. |
| `_client` | `DiscordSocketClient` | An instance of the `DiscordSocketClient` class, used to interact with the Discord API for sending messages, joining voice channels, etc. |
| `_musicEmbed` | `MusicEmbed` | An instance of a custom `MusicEmbed` class, used to create and send embed messages related to the music player's current status. |
| `context` | `SocketSlashCommand` | An instance of the `SocketSlashCommand` class, representing a slash command received from Discord. Used to get information about the command and to respond to it. |
| `player` | `LavaPlayer` | An instance of the `LavaPlayer` class, representing a music player connected to a specific voice channel. Used to play, pause, skip, and queue tracks. |
| `guildMessageIds` | `Dictionary<ulong, List<ulong>>` | A dictionary that maps guild IDs to lists of message IDs. Used to keep track of messages sent by the bot in each guild, allowing the bot to delete its old messages when it sends new ones. |
| `songName` | `string` | A string that represents the name or URL of a song to play. Used to search for and queue tracks. |
| `searchResponse` | `SearchResponse` | An instance of the `SearchResponse` class, representing the result of a search for tracks. Used to get the tracks that were found and queue them in the player. |

View File

@@ -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<SlashCommandReceivedNotification>
{
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;
}
}
}

View File

@@ -19,6 +19,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="OllamaSharp" Version="1.1.10" />
<PackageReference Include="Victoria" Version="6.0.23.324" /> <PackageReference Include="Victoria" Version="6.0.23.324" />
</ItemGroup> </ItemGroup>

View File

@@ -19,13 +19,19 @@ public class DiscordEventListener(DiscordSocketClient client, IServiceScopeFacto
public async Task StartAsync() public async Task StartAsync()
{ {
client.SlashCommandExecuted += OnMessageReceivedAsync; client.SlashCommandExecuted += OnSlashCommandRecievedAsync;
client.MessageReceived += OnMessageReceivedAsync;
await Task.CompletedTask; await Task.CompletedTask;
} }
private async Task OnMessageReceivedAsync(SocketSlashCommand arg) private async Task OnMessageReceivedAsync(SocketMessage arg)
{ {
await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
} }
private async Task OnSlashCommandRecievedAsync(SocketSlashCommand arg)
{
await Mediator.Publish(new SlashCommandReceivedNotification(arg), _cancellationToken);
}
} }

View File

@@ -3,7 +3,7 @@ using MediatR;
namespace Lunaris2.Notification; 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));
} }

View File

@@ -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));
}

View File

@@ -3,6 +3,7 @@ using Discord;
using Discord.Commands; using Discord.Commands;
using Discord.Interactions; using Discord.Interactions;
using Discord.WebSocket; using Discord.WebSocket;
using Lunaris2.Handler.ChatCommand;
using Lunaris2.Handler.MusicPlayer; using Lunaris2.Handler.MusicPlayer;
using Lunaris2.Notification; using Lunaris2.Notification;
using Lunaris2.SlashCommand; using Lunaris2.SlashCommand;
@@ -54,7 +55,9 @@ public class Program
nodeConfiguration.Authorization = configuration["LavaLinkPassword"]; nodeConfiguration.Authorization = configuration["LavaLinkPassword"];
}) })
.AddSingleton<LavaNode>() .AddSingleton<LavaNode>()
.AddSingleton<MusicEmbed>(); .AddSingleton<MusicEmbed>()
.AddSingleton<ChatSettings>()
.Configure<ChatSettings>(configuration.GetSection("LLM"));
client.Ready += () => Client_Ready(client); client.Ready += () => Client_Ready(client);
client.Log += Log; client.Log += Log;

View File

@@ -2,9 +2,14 @@
```mermaid ```mermaid
flowchart TD flowchart TD
Program[Program] -->|Register| EventListener Program[Program] -->|Register| EventListener
EventListener[DiscordEventListener] --> A EventListener[DiscordEventListener] --> A[MessageReceivedHandler]
A[MessageReceivedHandler] -->|Message| C{Send to correct command by EventListener[DiscordEventListener] --> A2[SlashCommandReceivedHandler]
A --> |Message| f{If bot is mentioned}
f --> |ChatCommand| v[ChatHandler]
A2[SlashCommandReceivedHandler] -->|Message| C{Send to correct command by
looking at commandName} looking at commandName}
C -->|JoinCommand| D[JoinHandler] C -->|JoinCommand| D[JoinHandler]
@@ -12,9 +17,33 @@ flowchart TD
C -->|HelloCommand| F[HelloHandler] C -->|HelloCommand| F[HelloHandler]
C -->|GoodbyeCommand| G[GoodbyeHandler] C -->|GoodbyeCommand| G[GoodbyeHandler]
``` ```
Program registers an event listener ```DiscordEventListener``` which publish a message : Program registers an event listener ```DiscordEventListener``` which publish a message :
```c# ```c#
await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken);
``` ```
|Name| Description |
|--|--|
| SlashCommandReceivedHandler | Handles commands using ``/`` from any Discord Guild/Server. |
| MessageReceivedHandler| Listens to **all** messages. |
## Handler integrations
```mermaid
flowchart TD
D[JoinHandler] --> Disc[Discord Api]
E[PlayHandler] --> Disc[Discord Api]
F[HelloHandler] --> Disc[Discord Api]
G[GoodbyeHandler] --> Disc[Discord Api]
v[ChatHandler] --> Disc[Discord Api]
v --> o[Ollama Server]
o --> v
E --> Lava[Lavalink]
```
|Name| Description |
|--|--|
| JoinHandler| Handles the logic for **just** joining a voice channel. |
| PlayHandler| Handles the logic for joining and playing music in a voice channel. |
| HelloHandler| Responds with Hello. (Dummy handler, will be removed)|
| GoodbyeHandler| Responds with Goodbye. (Dummy handler, will be removed)|
| ChatHandler| Handles the logic for LLM chat with user. |

17
Bot/appsettings.json Normal file
View File

@@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"Token": "discordToken",
"LavaLinkPassword": "youshallnotpass",
"LavaLinkHostname": "127.0.0.1",
"LavaLinkPort": 2333
"LLM": {
"Url": "http://192.168.50.54:11434",
"Model": "gemma"
}
}

View File

@@ -10,10 +10,12 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
## Setup ## Setup
1. Clone the repository. 1. Clone the repo.
2. Install the required packages by running `dotnet restore`. 2. Extract.
3. Build the project using `dotnet build`. 3. If there isn't already a appsettings.json file in there, create one.
4. Run the bot using `dotnet run`. 4. Set the discord bot token. How the file should look (without token): [appsettings.json](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json)]
5. Make sure you got docker installed. And run the file ``start-services.sh``, make sure you got git-bash installed.
6. Now you can start the project and run the application.
## Usage ## Usage

View File

@@ -1,21 +1,16 @@
server: # REST and WS server server: # REST and WS server
port: 2333 port: 2333
address: 0.0.0.0 address: 0.0.0.0
http2:
enabled: false # Whether to enable HTTP/2 support
plugins: plugins:
# name: # Name of the plugin # name: # Name of the plugin
# some_key: some_value # Some key-value pair for the plugin # some_key: some_value # Some key-value pair for the plugin
# another_key: another_value # another_key: another_value
lavalink: lavalink:
plugins: plugins:
# - dependency: "com.github.example:example-plugin:1.0.0" # required, the coordinates of your plugin # - dependency: "group:artifact:version"
# repository: "https://maven.example.com/releases" # optional, defaults to the Lavalink releases repository by default # repository: "repository"
# snapshot: false # optional, defaults to false, used to tell Lavalink to use the snapshot repository instead of the release repository
# pluginsDir: "./plugins" # optional, defaults to "./plugins"
# defaultPluginRepository: "https://maven.lavalink.dev/releases" # optional, defaults to the Lavalink release repository
# defaultPluginSnapshotRepository: "https://maven.lavalink.dev/snapshots" # optional, defaults to the Lavalink snapshot repository
server: server:
password: "youshallnotpass"
sources: sources:
youtube: true youtube: true
bandcamp: true bandcamp: true

View File

@@ -1,7 +1,7 @@
services: services:
lavalink: lavalink:
# pin the image version to Lavalink v4 # pin the image version to Lavalink v4
image: ghcr.io/lavalink-devs/lavalink:3.7.10 image: ghcr.io/lavalink-devs/lavalink:3.7.11
container_name: lavalink container_name: lavalink
restart: unless-stopped restart: unless-stopped
environment: environment: