Compare commits

...

18 Commits

Author SHA1 Message Date
d72676c7e0 Improve chat (#2)
Co-authored-by: Myx <info@azaaxin.com>
2024-08-11 01:18:29 +02:00
b30d47e351 Add LLM info into main Readme 2024-06-19 12:33:19 +02:00
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
3d7655a902 Update readme.md 2024-04-15 00:00:38 +02:00
18 changed files with 298 additions and 53 deletions

View File

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

View File

@@ -0,0 +1,68 @@
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();
private readonly ChatSettings _chatSettings;
public ChatHandler(IOptions<ChatSettings> chatSettings)
{
_chatSettings = chatSettings.Value;
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;
var randomPersonality = _chatSettings.Personalities[new Random().Next(_chatSettings.Personalities.Count)];
userMessage = $"{randomPersonality.Instruction} {userMessage}";
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,14 @@
namespace Lunaris2.Handler.ChatCommand;
public class ChatSettings
{
public string Url { get; set; }
public string Model { get; set; }
public List<Personality> Personalities { get; set; }
}
public class Personality
{
public string Name { get; set; }
public string Instruction { 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

@@ -8,6 +8,8 @@ flowchart TD
PlayTrack --> NowPlayingEmbed PlayTrack --> NowPlayingEmbed
``` ```
## Steps in the code
| Name | Description | | Name | Description |
|--|--| |--|--|
| PlayHandler | Holds the logic for playing songs | | PlayHandler | Holds the logic for playing songs |
@@ -19,7 +21,7 @@ flowchart TD
There is also OnTrackEnd, when it get called an attempt is made to play the next song in queue. 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: ## Short explaination for some of the variables used:
| Variable | Type | Description | | Variable | Type | Description |
| --- | --- | --- | | --- | --- | --- |

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

@@ -16,9 +16,11 @@
<PackageReference Include="Discord.Net.Rest" Version="3.13.1" /> <PackageReference Include="Discord.Net.Rest" Version="3.13.1" />
<PackageReference Include="MediatR" Version="12.2.0" /> <PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<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;
@@ -41,7 +42,8 @@ public class Program
.AddJsonFile("appsettings.json") .AddJsonFile("appsettings.json")
.Build(); .Build();
services.AddSingleton(client) services
.AddSingleton(client)
.AddSingleton(commands) .AddSingleton(commands)
.AddMediatR(configuration => configuration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) .AddMediatR(configuration => configuration.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()))
.AddSingleton<DiscordEventListener>() .AddSingleton<DiscordEventListener>()
@@ -54,7 +56,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. |

View File

@@ -9,5 +9,15 @@
"Token": "discordToken", "Token": "discordToken",
"LavaLinkPassword": "youshallnotpass", "LavaLinkPassword": "youshallnotpass",
"LavaLinkHostname": "127.0.0.1", "LavaLinkHostname": "127.0.0.1",
"LavaLinkPort": 2333 "LavaLinkPort": 2333,
"LLM": {
"Url": "http://localhost:7869",
"Model": "gemma",
"personalities": [
{
"name": "Lunaris",
"instruction": "Let's role play. Your name is Lunaris and can be quite rude. You are supposed to answer on following question with a maximum of 2 sentences:"
}
]
}
} }

View File

@@ -7,6 +7,7 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
- Play music from YouTube directly in your Discord server. - Play music from YouTube directly in your Discord server.
- Skip tracks, pause, and resume playback. - Skip tracks, pause, and resume playback.
- Queue system to line up your favorite tracks. - Queue system to line up your favorite tracks.
- Local LLM (AI chatbot) that answers on @mentions in Discord chat. See more about it below.
## Setup ## Setup
@@ -17,6 +18,11 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
5. Make sure you got docker installed. And run the file ``start-services.sh``, make sure you got git-bash installed. 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. 6. Now you can start the project and run the application.
## LLM
Lunaris supports AI chat using a large language model, this is done by hosting the LLM locally, in this case Docker will set it up for you when you run the start-services script.
The LLM is run using Ollama see more about Ollama [here](https://ollama.com/). Running LLM locally requires much resources from your system, minimum requirements is at least 8GB of ram. If your don't have enought ram, select a LLM model in the [appsettings file](https://github.com/Myxelium/Lunaris2.0/blob/master/Bot/appsettings.json#L15) that requires less of your system.
## Usage ## Usage
- `/play <song>`: Plays the specified song in the voice channel you're currently in. - `/play <song>`: Plays the specified song in the voice channel you're currently in.
@@ -25,7 +31,3 @@ Lunaris2 is a Discord bot designed to play music in your server's voice channels
## Contributing ## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 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,7 +24,52 @@ services:
ports: ports:
# you only need this if you want to make your lavalink accessible from outside of containers # you only need this if you want to make your lavalink accessible from outside of containers
- "2333:2333" - "2333:2333"
ollama:
image: ollama/ollama:latest
ports:
- 7869:11434
volumes:
- .:/code
- ./ollama/ollama:/root/.ollama
container_name: ollama
pull_policy: always
tty: true
restart: always
environment:
- OLLAMA_KEEP_ALIVE=24h
- OLLAMA_HOST=0.0.0.0
networks:
- ollama-docker
ollama-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: ollama-webui
volumes:
- ./ollama/ollama-webui:/app/backend/data
depends_on:
- ollama
ports:
- 8080:8080
environment: # https://docs.openwebui.com/getting-started/env-configuration#default_models
- OLLAMA_BASE_URLS=http://host.docker.internal:7869 #comma separated ollama hosts
- ENV=dev
- WEBUI_AUTH=False
- WEBUI_NAME=valiantlynx AI
- WEBUI_URL=http://localhost:8080
- WEBUI_SECRET_KEY=t0p-s3cr3t
extra_hosts:
- host.docker.internal:host-gateway
restart: unless-stopped
networks:
- ollama-docker
volumes:
ollama: {}
networks: networks:
# create a lavalink network you can add other containers to, to give them access to Lavalink # create a lavalink network you can add other containers to, to give them access to Lavalink
lavalink: lavalink:
name: lavalink name: lavalink
ollama-docker:
external: false

View File

@@ -1 +1,3 @@
docker compose up -d docker compose up -d
read -p "Press enter to continue"