Compare commits

..

15 Commits

Author SHA1 Message Date
3ce0df7eaf Added Ollama and Ollama Web UI 2024-06-19 12:21:05 +02:00
e88e67f913 Fix broken appsettings file
Invalid json
2024-06-19 12:12:05 +02:00
5053553182 Update dotnet.yml 2024-06-02 19:12:45 +02:00
327ccc9675 Update dotnet.yml 2024-06-02 18:57:56 +02:00
cbc99c2773 Update dotnet.yml 2024-06-02 18:55:14 +02:00
d56215f685 Update README.md 2024-06-02 18:53:02 +02:00
967bee923a Update dotnet.yml 2024-06-02 18:51:38 +02:00
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
16 changed files with 252 additions and 49 deletions

View File

@@ -7,10 +7,13 @@ on:
jobs:
build:
runs-on: windows-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0 # required for github-action-get-previous-tag
- name: Setup .NET
uses: actions/setup-dotnet@v1
@@ -27,15 +30,19 @@ jobs:
run: dotnet publish ./Bot/Lunaris2.csproj --configuration Release --output ./out
- name: Zip the build
run: 7z a -tzip ./out/Bot.zip ./out/*
run: 7z a -tzip ./out/Lunaris.zip ./out/*
- name: Get the tag name
id: get_tag
run: echo "::set-output name=tag::${GITHUB_REF#refs/tags/}"
- name: Get previous tag
id: previoustag
uses: 'WyriHaximus/github-action-get-previous-tag@v1'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get the version
id: get_version
run: echo "::set-output name=version::$(date +%s).${{ github.run_id }}"
- name: Get next minor version
id: semver
uses: 'WyriHaximus/github-action-next-semvers@v1'
with:
version: ${{ steps.previoustag.outputs.tag }}
- name: Create Release
id: create_release
@@ -43,8 +50,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ steps.get_version.outputs.version }}
release_name: Release v${{ steps.get_version.outputs.version }}
tag_name: ${{ steps.semver.outputs.patch }}
release_name: Release ${{ steps.semver.outputs.patch }}
draft: false
prerelease: false
@@ -55,6 +62,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./out/Bot.zip
asset_name: Bot.zip
asset_path: ./out/Lunaris.zip
asset_name: Lunaris.zip
asset_content_type: application/zip

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 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<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)
{
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;
// 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);
}
}
}

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.DependencyInjection" 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" />
</ItemGroup>

View File

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

View File

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

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.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<LavaNode>()
.AddSingleton<MusicEmbed>();
.AddSingleton<MusicEmbed>()
.AddSingleton<ChatSettings>()
.Configure<ChatSettings>(configuration.GetSection("LLM"));
client.Ready += () => Client_Ready(client);
client.Log += Log;

View File

@@ -2,9 +2,14 @@
```mermaid
flowchart TD
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}
C -->|JoinCommand| D[JoinHandler]
@@ -12,9 +17,33 @@ flowchart TD
C -->|HelloCommand| F[HelloHandler]
C -->|GoodbyeCommand| G[GoodbyeHandler]
```
Program registers an event listener ```DiscordEventListener``` which publish a message :
```c#
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. |

View File

@@ -12,7 +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.Stop.Name, Command.Stop.Description);
}
private static void RegisterCommand(

View File

@@ -9,5 +9,9 @@
"Token": "discordToken",
"LavaLinkPassword": "youshallnotpass",
"LavaLinkHostname": "127.0.0.1",
"LavaLinkPort": 2333
"LavaLinkPort": 2333,
"LLM": {
"Url": "http://192.168.50.54:11434",
"Model": "gemma"
}
}

View File

@@ -25,7 +25,3 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## License
[MIT](https://choosealicense.com/licenses/mit/)

View File

@@ -24,6 +24,40 @@ services:
ports:
# you only need this if you want to make your lavalink accessible from outside of containers
- "2333:2333"
ollama:
volumes:
- ollama:/root/.ollama
# comment below to not expose Ollama API outside the container stack
ports:
- 11434:11434
container_name: ollama
pull_policy: always
tty: true
restart: unless-stopped
image: ollama/ollama:latest
ollama-webui:
build:
context: .
args:
OLLAMA_API_BASE_URL: '/ollama/api'
dockerfile: Dockerfile
image: ollama-webui:latest
container_name: ollama-webui
depends_on:
- ollama
ports:
- 3000:8080
environment:
- "OLLAMA_API_BASE_URL=http://ollama:11434/api"
extra_hosts:
- host.docker.internal:host-gateway
restart: unless-stopped
volumes:
ollama: {}
networks:
# create a lavalink network you can add other containers to, to give them access to Lavalink
lavalink: