diff --git a/Ollabotica/BotConfiguration.cs b/Ollabotica/BotConfiguration.cs index 9d073a4..dd16345 100644 --- a/Ollabotica/BotConfiguration.cs +++ b/Ollabotica/BotConfiguration.cs @@ -31,7 +31,7 @@ public class BotConfiguration public string AdminChatIdsRaw { get; set; } - public List AllowedChatIds + public List AllowedChatIdsAsLong { get { @@ -41,7 +41,7 @@ public List AllowedChatIds } } - public List AdminChatIds + public List AdminChatIdsAsLong { get { @@ -50,4 +50,24 @@ public List AdminChatIds .ToList() ?? new List(); } } + + public List AllowedChatIds + { + get + { + return AllowedChatIdsRaw?.Split(',') + .Select(id => id.Trim()) + .ToList() ?? new List(); + } + } + + public List AdminChatIds + { + get + { + return AdminChatIdsRaw?.Split(',') + .Select(id => id.Trim()) + .ToList() ?? new List(); + } + } } \ No newline at end of file diff --git a/Ollabotica/BotManager.cs b/Ollabotica/BotManager.cs index 75509d3..30191ed 100644 --- a/Ollabotica/BotManager.cs +++ b/Ollabotica/BotManager.cs @@ -25,7 +25,7 @@ public async Task StartBotsAsync() foreach (var botConfig in _botConfigurations) { // Resolve TelegramBotService from IServiceProvider - var botService = _serviceProvider.GetRequiredKeyedService(ServiceTypes.Telegram.ToString()); + var botService = _serviceProvider.GetRequiredKeyedService(botConfig.ServiceType); await botService.StartAsync(botConfig); _botServices.Add(botService); } diff --git a/Ollabotica/BotServices/SlackBotService.cs b/Ollabotica/BotServices/SlackBotService.cs new file mode 100644 index 0000000..f8feddc --- /dev/null +++ b/Ollabotica/BotServices/SlackBotService.cs @@ -0,0 +1,150 @@ +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using Microsoft.AspNetCore.Mvc.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.VisualBasic; +using Newtonsoft.Json.Linq; +using Ollabotica.ChatServices; +using OllamaSharp; +using Slack.NetStandard.AsyncEnumerable; +using Slack.NetStandard.Interaction; +using Slack.NetStandard.Messages.Elements.RichText; +using Slack.NetStandard.Socket; +using SlackAPI; +using SlackAPI.WebSocketMessages; +using Telegram.Bot.Types.Enums; + +namespace Ollabotica.BotServices; + +/// +/// This class will handle a single bot's Slack and Ollama connections. +/// +public class SlackBotService : IBotService +{ + private BotConfiguration _config; + private SocketModeClient _slackClient; + private OllamaApiClient _ollamaClient; + private readonly ILogger _logger; + private readonly MessageInputRouter _messageInputRouter; + private readonly MessageOutputRouter _messageOutputRouter; + private readonly SlackChatService _slackChatService; + private ClientWebSocket _clientWebSocket; + private Chat _ollamaChat; + private CancellationTokenSource _cts; + + // Inject all required dependencies via constructor + public SlackBotService(ILogger logger, MessageInputRouter messageInputRouter, MessageOutputRouter messageOutputRouter, SlackChatService chatService) + { + _logger = logger; + _messageInputRouter = messageInputRouter; + _messageOutputRouter = messageOutputRouter; + _slackChatService = chatService; + _cts = new CancellationTokenSource(); + } + + public async Task StartAsync(BotConfiguration botConfig) + { + _config = botConfig; + _ollamaClient = new OllamaApiClient(botConfig.OllamaUrl, botConfig.DefaultModel); + _ollamaClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {botConfig.OllamaToken}"); + _ollamaChat = new Chat(_ollamaClient, ""); + + _clientWebSocket = new ClientWebSocket(); + var _slackClient = new SocketModeClient(); + await _slackClient.ConnectAsync(botConfig.ChatAuthToken); + _slackChatService.Init(_slackClient); + + await foreach (var envelope in _slackClient.EnvelopeAsyncEnumerable(_cts.Token)) + { + await HandleMessageAsync(envelope); + } + + _logger.LogInformation($"Bot {_config.Name} started for Slack."); + } + + public async Task StopAsync() + { + _cts.Cancel(); + await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "App shutting down", CancellationToken.None); + _slackClient.Dispose(); + _logger.LogInformation("Bot stopped."); + } + + private async Task HandleMessageAsync(Envelope slackMessage) + { + if (!slackMessage.Type.Equals("events_api")) + return; // Ignore bot messages + + //(Slack.NetStandard.EventsApi.EventCallback)slackMessage.Payload).Event + var payload = (slackMessage.Payload as Slack.NetStandard.EventsApi.EventCallback); + if (payload is null) + return; + + var message = (Slack.NetStandard.Messages.Message)payload.Event; + + if (message is null) + return; + + _logger.LogInformation($"Received Slack slackMessage: {message.Text} from user {message.User} in {message.Channel.NameNormalized}"); + + bool isAdmin = _config.AdminChatIds.Contains(message.User); + + var m = new ChatMessage() + { + MessageId = slackMessage.EnvelopeId, + IncomingText = message.Text, + ChatId = slackMessage.EnvelopeId, + UserIdentity = $"{message.User}" + }; + + if (_config.AllowedChatIds.Contains(message.User)) + { + if (m.IncomingText != null) + { + _logger.LogInformation( + $"Received chat slackMessage from: {m.UserIdentity} for {_slackChatService.BotId}: {m.IncomingText}"); + + try + { + // Route the slackMessage through the input processors + var shouldContinue = await _messageInputRouter.Route(m, _ollamaChat, _slackChatService, isAdmin, _config); + + if (shouldContinue) + { + var p = ""; + if (string.IsNullOrWhiteSpace(p)) p = m.IncomingText; + // Send the prompt to Ollama and gather response + await foreach (var answerToken in _ollamaChat.Send(p)) + { + await _slackChatService.SendChatActionAsync(m, "Typing"); + m.OutgoingText += p; + await _messageOutputRouter.Route(m, _ollamaChat, _slackChatService, isAdmin, answerToken, _config); + } + await _messageOutputRouter.Route(m, _ollamaChat, _slackChatService, isAdmin, "\n", _config); + } + } + catch (Exception e) + { + _logger.LogError(e, $"Error processing slackMessage {m.ChatId}"); + if (isAdmin) + { + m.OutgoingText = e.ToString(); + await _slackChatService.SendTextMessageAsync(m); + } + } + } + else + { + m.OutgoingText = "I can only process text messages."; + await _slackChatService.SendTextMessageAsync(m); + } + } + else + { + _logger.LogWarning($"Received slackMessage from unauthorized chat: {message.User}"); + } + } +} \ No newline at end of file diff --git a/Ollabotica/TelegramBotService.cs b/Ollabotica/BotServices/TelegramBotService.cs similarity index 80% rename from Ollabotica/TelegramBotService.cs rename to Ollabotica/BotServices/TelegramBotService.cs index 9290fb4..341d2ea 100644 --- a/Ollabotica/TelegramBotService.cs +++ b/Ollabotica/BotServices/TelegramBotService.cs @@ -10,7 +10,7 @@ using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -namespace Ollabotica; +namespace Ollabotica.BotServices; /// /// This class will handle a single bot's Telegram and Ollama connections. @@ -65,11 +65,11 @@ private async Task HandleUpdateAsync(ITelegramBotClient client, Update update, C if (update.Type == UpdateType.Message && update.Message != null) { var message = update.Message; - await _telegramClient.SendChatActionAsync(message.Chat.Id, ChatAction.Typing); + await _telegramClient.SendChatActionAsync(message.Chat.Id.ToString(), ChatAction.Typing); - bool isAdmin = _config.AdminChatIds.Contains(message.Chat.Id); + bool isAdmin = _config.AdminChatIdsAsLong.Contains(message.Chat.Id); - if (_config.AllowedChatIds.Contains(message.Chat.Id)) + if (_config.AllowedChatIdsAsLong.Contains(message.Chat.Id)) { if (message.Text != null) { @@ -78,8 +78,15 @@ private async Task HandleUpdateAsync(ITelegramBotClient client, Update update, C try { + var m = new ChatMessage() + { + MessageId = message.Chat.Id.ToString(), + IncomingText = message.Text, + ChatId = message.Chat.Id.ToString(), + UserIdentity = $"{message.Chat.FirstName} {message.Chat.LastName}" + }; // Route the message through the input processors - var shouldContinue = await _messageInputRouter.Route(message, _ollamaChat, _telegramChatService, isAdmin, _config); + var shouldContinue = await _messageInputRouter.Route(m, _ollamaChat, _telegramChatService, isAdmin, _config); if (shouldContinue) { @@ -88,10 +95,10 @@ private async Task HandleUpdateAsync(ITelegramBotClient client, Update update, C // Send the prompt to Ollama and gather response await foreach (var answerToken in _ollamaChat.Send(p)) { - await _telegramClient.SendChatActionAsync(message.Chat.Id, ChatAction.Typing); - await _messageOutputRouter.Route(message, _ollamaChat, _telegramChatService, isAdmin, answerToken, _config); + await _telegramClient.SendChatActionAsync(message.Chat.Id.ToString(), ChatAction.Typing); + await _messageOutputRouter.Route(m, _ollamaChat, _telegramChatService, isAdmin, answerToken, _config); } - await _messageOutputRouter.Route(message, _ollamaChat, _telegramChatService, isAdmin, "\n", _config); + await _messageOutputRouter.Route(m, _ollamaChat, _telegramChatService, isAdmin, "\n", _config); } } catch (Exception e) @@ -99,13 +106,13 @@ private async Task HandleUpdateAsync(ITelegramBotClient client, Update update, C _logger.LogError(e, $"Error processing message {message.MessageId}"); if (isAdmin) { - await _telegramClient.SendTextMessageAsync(message.Chat.Id, e.ToString(), cancellationToken: cancellationToken); + await _telegramClient.SendTextMessageAsync(message.Chat.Id.ToString(), e.ToString(), cancellationToken: cancellationToken); } } } else { - await _telegramClient.SendTextMessageAsync(message.Chat.Id, "I can only process text messages.", cancellationToken: cancellationToken); + await _telegramClient.SendTextMessageAsync(message.Chat.Id.ToString(), "I can only process text messages.", cancellationToken: cancellationToken); } } else diff --git a/Ollabotica/ChatMessage.cs b/Ollabotica/ChatMessage.cs new file mode 100644 index 0000000..1791bad --- /dev/null +++ b/Ollabotica/ChatMessage.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ollabotica; + +public class ChatMessage +{ + public string IncomingText { get; set; } + public string OutgoingText { get; set; } + public string UserIdentity { get; set; } + public string MessageId { get; set; } + public string ChatId { get; set; } +} \ No newline at end of file diff --git a/Ollabotica/ChatServices/SlackChatService.cs b/Ollabotica/ChatServices/SlackChatService.cs new file mode 100644 index 0000000..0392769 --- /dev/null +++ b/Ollabotica/ChatServices/SlackChatService.cs @@ -0,0 +1,76 @@ +using SlackAPI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.Extensions.Logging; +using Slack.NetStandard.AsyncEnumerable; +using Slack.NetStandard.Messages.Blocks; +using Slack.NetStandard.Socket; +using SlackAPI.WebSocketMessages; +using Telegram.Bot; + +namespace Ollabotica.ChatServices; + +public class SlackChatService : IChatService +{ + private readonly ILogger _log; + private SocketModeClient client = null; + + public SlackChatService(ILogger log) + { + _log = log; + } + + public void Init(T chatClient) where T : class + { + client = chatClient as SocketModeClient; + } + + public string BotId + { + get + { + return this.GetHashCode().ToString(); + } + } + + public async Task SendChatActionAsync(ChatMessage message, string action) + { + await client.Send(System.Text.Json.JsonSerializer.Serialize(new Typing() { })); + } + + public async Task SendTextMessageAsync(ChatMessage message, string text) + { + message.OutgoingText = text; + await this.SendTextMessageAsync(message); + } + + public async Task SendTextMessageAsync(ChatMessage message) + { + _log.LogInformation("Sending message to chatId: {chatId}, message: {text}", message.ChatId, message.OutgoingText); + var msg = System.Text.Json.JsonSerializer.Serialize(new Acknowledge() + { + EnvelopeId = message.ChatId, + Payload = new Slack.NetStandard.Messages.Message() + { + Blocks = new List + { + new Section(message.OutgoingText) + } + } + }); + _log.LogInformation("Message Sent: {msg}", msg); + try + { + await client.Send(msg); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + } +} \ No newline at end of file diff --git a/Ollabotica/ChatServices/TelegramChatService.cs b/Ollabotica/ChatServices/TelegramChatService.cs index f445950..39fcea8 100644 --- a/Ollabotica/ChatServices/TelegramChatService.cs +++ b/Ollabotica/ChatServices/TelegramChatService.cs @@ -11,27 +11,33 @@ public class TelegramChatService : IChatService { private TelegramBotClient telegramClient = null; + public async Task SendTextMessageAsync(ChatMessage message, string text) + { + message.OutgoingText = text; + await this.SendTextMessageAsync(message); + } + public void Init(T chatClient) where T : class { telegramClient = chatClient as TelegramBotClient; } - public long BotId + public string BotId { get { - return telegramClient.BotId; + return telegramClient.BotId.ToString(); } } - public async Task SendChatActionAsync(long chatId, string action) + public async Task SendChatActionAsync(ChatMessage message, string action) { var a = System.Enum.Parse(action); - await telegramClient.SendChatActionAsync(chatId, a); + await telegramClient.SendChatActionAsync(message.ChatId, a); } - public async Task SendTextMessageAsync(long chatId, string text) + public async Task SendTextMessageAsync(ChatMessage message) { - await telegramClient.SendTextMessageAsync(chatId, text); + await telegramClient.SendTextMessageAsync(message.ChatId, message.OutgoingText); } } \ No newline at end of file diff --git a/Ollabotica/IChatService.cs b/Ollabotica/IChatService.cs index 56b63ef..652d44e 100644 --- a/Ollabotica/IChatService.cs +++ b/Ollabotica/IChatService.cs @@ -9,11 +9,13 @@ namespace Ollabotica; public interface IChatService { - Task SendChatActionAsync(long chatId, string action); + Task SendChatActionAsync(ChatMessage message, string action); - Task SendTextMessageAsync(long chatId, string text); + Task SendTextMessageAsync(ChatMessage message); + + Task SendTextMessageAsync(ChatMessage message, string text); void Init(T chatClient) where T : class; - long BotId { get; } + string BotId { get; } } \ No newline at end of file diff --git a/Ollabotica/IMessageInputProcessor.cs b/Ollabotica/IMessageInputProcessor.cs index 3393214..e431410 100644 --- a/Ollabotica/IMessageInputProcessor.cs +++ b/Ollabotica/IMessageInputProcessor.cs @@ -10,10 +10,10 @@ namespace Ollabotica; public interface IMessageInputProcessor { - Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration); + Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration); } public interface IMessageOutputProcessor { - Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, string ollamaOutputText, BotConfiguration botConfiguration); + Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, string ollamaOutputText, BotConfiguration botConfiguration); } \ No newline at end of file diff --git a/Ollabotica/InputProcessors/ConversationManagerInputProcessor.cs b/Ollabotica/InputProcessors/ConversationManagerInputProcessor.cs index 87daccd..6bdda97 100644 --- a/Ollabotica/InputProcessors/ConversationManagerInputProcessor.cs +++ b/Ollabotica/InputProcessors/ConversationManagerInputProcessor.cs @@ -27,12 +27,12 @@ public ConversationManagerInputProcessor(ILogger Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) + public async Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) { // Logic to start a new conversation by resetting OllamaSharp context - if (message.Text.StartsWith("/listchats", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.StartsWith("/listchats", StringComparison.InvariantCultureIgnoreCase)) { - var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, message.Chat.Id.ToString()); + var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, ChatAction.Typing.ToString().ToString()); if (!System.IO.Directory.Exists(chatFolder)) System.IO.Directory.CreateDirectory(chatFolder); var chatFiles = System.IO.Directory.GetFiles(chatFolder, "*.json"); @@ -42,85 +42,85 @@ public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, ICh { chats = "No chats found.\nUse:\n\n/savechat \n\nto save your chats with the bot."; } - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Chats available to you include:\n{chats}"); + await chat.SendTextMessageAsync(message, $"Chats available to you include:\n{chats}"); return false; } - if (message.Text.StartsWith("/deletechat", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.StartsWith("/deletechat", StringComparison.InvariantCultureIgnoreCase)) { - var name = message.Text.Substring("/deletechat".Length).Trim(); + var name = message.IncomingText.Substring("/deletechat".Length).Trim(); if (string.IsNullOrWhiteSpace(name)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Please provide a name for the chat."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Please provide a name for the chat."); _log.LogInformation("Trying to delete the chat, no name provided for the chat."); return false; } - var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, message.Chat.Id.ToString()); + var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, ChatAction.Typing.ToString().ToString()); if (!System.IO.Directory.Exists(chatFolder)) System.IO.Directory.CreateDirectory(chatFolder); var chatFile = System.IO.Path.Combine(chatFolder, $"{name}.json"); _log.LogInformation($"Chat file will be deleted:{chatFile}"); - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); if (System.IO.File.Exists(chatFile)) { System.IO.File.Delete(chatFile); - await chat.SendTextMessageAsync(message.Chat.Id, $"Chat was deleted."); + await chat.SendTextMessageAsync(message, $"Chat was deleted."); } else { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Chat was not found."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Chat was not found."); } return false; } - if (message.Text.StartsWith("/savechat", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.StartsWith("/savechat", StringComparison.InvariantCultureIgnoreCase)) { - var name = message.Text.Substring("/savechat".Length).Trim(); + var name = message.IncomingText.Substring("/savechat".Length).Trim(); if (string.IsNullOrWhiteSpace(name)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Please provide a name for the chat."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Please provide a name for the chat."); _log.LogInformation("Trying to save the chat, no name provided for the chat."); return false; } var chats = System.Text.Json.JsonSerializer.Serialize(ollamaChat.Messages); - var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, message.Chat.Id.ToString()); + var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, ChatAction.Typing.ToString().ToString()); if (!System.IO.Directory.Exists(chatFolder)) System.IO.Directory.CreateDirectory(chatFolder); var chatFile = System.IO.Path.Combine(chatFolder, $"{name}.json"); _log.LogInformation($"Chat file will be saved:{chatFile}"); System.IO.File.WriteAllText(chatFile, chats); // Resetting the conversation - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Chat was saved."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Chat was saved."); return false; } - if (message.Text.StartsWith("/loadchat", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.StartsWith("/loadchat", StringComparison.InvariantCultureIgnoreCase)) { - var name = message.Text.Substring("/loadchat".Length).Trim(); + var name = message.IncomingText.Substring("/loadchat".Length).Trim(); if (string.IsNullOrWhiteSpace(name)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Please provide a name for the chat."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Please provide a name for the chat."); _log.LogInformation("Trying to load the chat, no name provided for the chat."); return false; } - var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, message.Chat.Id.ToString()); + var chatFolder = System.IO.Path.Combine(botConfiguration.ChatsFolder.FullName, ChatAction.Typing.ToString().ToString()); if (!System.IO.Directory.Exists(chatFolder)) System.IO.Directory.CreateDirectory(chatFolder); var chatFile = System.IO.Path.Combine(chatFolder, $"{name}.json"); if (!File.Exists(chatFile)) { _log.LogInformation($"Chat file not found:{chatFile}"); - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Chat was not found, try /listchats"); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Chat was not found, try /listchats"); return false; } @@ -131,8 +131,8 @@ public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, ICh ollamaChat.SetMessages(chats); // Resetting the conversation - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Chat was loaded ({chats.Count})."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Chat was loaded ({chats.Count})."); return false; } return true; diff --git a/Ollabotica/InputProcessors/DiagnosticsInputProcessor.cs b/Ollabotica/InputProcessors/DiagnosticsInputProcessor.cs index 4973707..d214c95 100644 --- a/Ollabotica/InputProcessors/DiagnosticsInputProcessor.cs +++ b/Ollabotica/InputProcessors/DiagnosticsInputProcessor.cs @@ -14,24 +14,19 @@ namespace Ollabotica.InputProcessors; [Trigger(Trigger = "debug", Description = "Dump diagnostic information.", IsAdmin = true)] public class DiagnosticsInputProcessor : IMessageInputProcessor { - public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) + public async Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) { if (!isAdmin) return true; // Logic to start a new conversation by resetting OllamaSharp context - if (message.Text.Equals("/debug", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.Equals("/debug", StringComparison.InvariantCultureIgnoreCase)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Diagnostics:\n\nTelegram:\n" + - $" ChatId: {message.Chat.Id}\n" + - $" Chat FirstName: {message.Chat.FirstName}\n" + - $" Chat LastName: {message.Chat.LastName}\n" + - $" Chat Title: {message.Chat.Title}\n" + - $" Chat Username: {message.Chat.Username}\n" + - $" Chat IsForum: {message.Chat.IsForum}\n" + - $" Chat Type: {message.Chat.Type}\n" + - $" Chat HashCode: {message.Chat.GetHashCode()}\n" + + $" ChatId: {message.UserIdentity}\n" + + $" Chat UserIdentity: {message.UserIdentity}\n" + + $" Chat HashCode: {message.GetHashCode()}\n" + $" BotId: {chat.BotId}\n" + $" Chat HashCode: {chat.GetHashCode()}\n" + "\nOllama:\n" + diff --git a/Ollabotica/InputProcessors/EchoUserTextInputProcessor.cs b/Ollabotica/InputProcessors/EchoUserTextInputProcessor.cs index e117d80..3f7f544 100644 --- a/Ollabotica/InputProcessors/EchoUserTextInputProcessor.cs +++ b/Ollabotica/InputProcessors/EchoUserTextInputProcessor.cs @@ -22,12 +22,12 @@ public EchoUserTextInputProcessor(ILogger log) _log = log; } - public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) + public async Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) { - _log.LogInformation("Received message:{messageText}", message.Text); + _log.LogInformation("Received message:{messageText}", message.IncomingText); - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"You said:\n\"{message.Text}\""); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"You said:\n\"{message.IncomingText}\""); return false; } } \ No newline at end of file diff --git a/Ollabotica/InputProcessors/ModelManagerInputProcessor.cs b/Ollabotica/InputProcessors/ModelManagerInputProcessor.cs index 72778fd..c1db4bb 100644 --- a/Ollabotica/InputProcessors/ModelManagerInputProcessor.cs +++ b/Ollabotica/InputProcessors/ModelManagerInputProcessor.cs @@ -25,16 +25,16 @@ public ModelManagerInputProcessor(ILogger log) _log = log; } - public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) + public async Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) { - if (message.Text.StartsWith("/listmodels", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.StartsWith("/listmodels", StringComparison.InvariantCultureIgnoreCase)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); var models = await ollamaChat.Client.ListLocalModels(); if (!models.Any()) { - await chat.SendTextMessageAsync(message.Chat.Id, "No models found."); + await chat.SendTextMessageAsync(message, "No models found."); } else { @@ -44,20 +44,20 @@ public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, ICh { modelList.AppendLine($" {m.Name} ({m.Details.ParameterSize})"); } - await chat.SendTextMessageAsync(message.Chat.Id, modelList.ToString()); + await chat.SendTextMessageAsync(message, modelList.ToString()); } return false; } - if (message.Text.StartsWith("/usemodel", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.StartsWith("/usemodel", StringComparison.InvariantCultureIgnoreCase)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); - var name = message.Text.Substring("/usemodel".Length).Trim(); + var name = message.IncomingText.Substring("/usemodel".Length).Trim(); if (string.IsNullOrWhiteSpace(name)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Please provide a name for the model to use."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Please provide a name for the model to use."); _log.LogInformation("Trying to use a model with no name specified."); return false; } @@ -66,14 +66,14 @@ public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, ICh var existingModel = models.FirstOrDefault(m => m.Name == name); if (existingModel is null) { - await chat.SendTextMessageAsync(message.Chat.Id, $"Model {name} not found."); + await chat.SendTextMessageAsync(message, $"Model {name} not found."); return false; } else { ollamaChat.Model = existingModel.Name; ollamaChat.Client.SelectedModel = existingModel.Name; - await chat.SendTextMessageAsync(message.Chat.Id, $"Model {name} selected."); + await chat.SendTextMessageAsync(message, $"Model {name} selected."); } return false; diff --git a/Ollabotica/InputProcessors/PromptFillingInputProcessor.cs b/Ollabotica/InputProcessors/PromptFillingInputProcessor.cs index 330cdd2..1844611 100644 --- a/Ollabotica/InputProcessors/PromptFillingInputProcessor.cs +++ b/Ollabotica/InputProcessors/PromptFillingInputProcessor.cs @@ -10,21 +10,19 @@ namespace Ollabotica.InputProcessors; public class PromptFillingInputProcessor : IMessageInputProcessor { - public Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) + public Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) { var prompt = new StringBuilder(); // Fill in the prompt information, system instructions, and user context prompt.AppendLine("System Instructions:"); prompt.AppendLine("You are helpful and knowledgeable. Respond in a friendly and professional tone, providing accurate and concise information."); prompt.AppendLine("User Information:"); - prompt.AppendLine($"- Name: {message.Chat.FirstName} {message.Chat.LastName}"); - prompt.AppendLine($"- Username: {message.Chat.Username}"); - prompt.AppendLine($"- Chat Title: {message.Chat.Title}"); + prompt.AppendLine($"- Identity: {message.UserIdentity}"); - if (message.Location is not null) - { - prompt.AppendLine($"- Location: {message.Location.Latitude}, {message.Location.Longitude}"); - } + //if (message.Location is not null) + //{ + // prompt.AppendLine($"- Location: {message.Location.Latitude}, {message.Location.Longitude}"); + //} prompt.AppendLine($"- Current Date: {DateTimeOffset.Now}"); prompt.AppendLine("- Preferences: Prefers concise, detailed responses, with a casual tone."); diff --git a/Ollabotica/InputProcessors/StartNewConversationInputProcessor.cs b/Ollabotica/InputProcessors/StartNewConversationInputProcessor.cs index d706962..29d44a5 100644 --- a/Ollabotica/InputProcessors/StartNewConversationInputProcessor.cs +++ b/Ollabotica/InputProcessors/StartNewConversationInputProcessor.cs @@ -16,22 +16,22 @@ namespace Ollabotica.InputProcessors; [Trigger(Trigger = "new", Description = "Clears the message history and starts a new conversation with the model.", IsAdmin = false)] public class StartNewConversationInputProcessor : IMessageInputProcessor { - public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) + public async Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) { // Logic to start a new conversation by resetting OllamaSharp context - if (message.Text.Equals("/new", StringComparison.InvariantCultureIgnoreCase) || message.Text.Equals("/newchat", StringComparison.InvariantCultureIgnoreCase) || message.Text.Equals("/start", StringComparison.InvariantCultureIgnoreCase) || message.Text.Equals("/clear", StringComparison.InvariantCultureIgnoreCase)) + if (message.IncomingText.Equals("/new", StringComparison.InvariantCultureIgnoreCase) || message.IncomingText.Equals("/newchat", StringComparison.InvariantCultureIgnoreCase) || message.IncomingText.Equals("/start", StringComparison.InvariantCultureIgnoreCase) || message.IncomingText.Equals("/clear", StringComparison.InvariantCultureIgnoreCase)) { // Resetting the conversation ollamaChat.SetMessages(new List()); - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, $"Chat was cleared."); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, $"Chat was cleared."); if (!string.IsNullOrWhiteSpace(botConfiguration.NewChatPrompt)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); await foreach (var answerToken in ollamaChat.Send(botConfiguration.NewChatPrompt)) { - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, answerToken); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + await chat.SendTextMessageAsync(message, answerToken); } } diff --git a/Ollabotica/MessageInputRouter.cs b/Ollabotica/MessageInputRouter.cs index dcf4755..09671e8 100644 --- a/Ollabotica/MessageInputRouter.cs +++ b/Ollabotica/MessageInputRouter.cs @@ -24,7 +24,7 @@ public MessageInputRouter(IEnumerable processors, ILogge } // Routes a message to all processors - public async Task Route(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) + public async Task Route(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, BotConfiguration botConfiguration) { foreach (var processor in _processors) { @@ -34,7 +34,7 @@ public async Task Route(Message message, OllamaSharp.Chat ollamaChat, ICha } catch (Exception ex) { - _logger.LogError(ex, $"Error processing message {message.MessageId} with {processor.GetType().Name}"); + _logger.LogError(ex, $"Error processing message {message} with {processor.GetType().Name}"); } } return true; @@ -54,7 +54,7 @@ public MessageOutputRouter(IEnumerable processors, ILog } // Routes a message to all processors - public async Task Route(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, string ollamaOutputText, BotConfiguration botConfiguration) + public async Task Route(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, string ollamaOutputText, BotConfiguration botConfiguration) { foreach (var processor in _processors) { diff --git a/Ollabotica/Ollabotica.csproj b/Ollabotica/Ollabotica.csproj index 1c8e9d2..d1d9562 100644 --- a/Ollabotica/Ollabotica.csproj +++ b/Ollabotica/Ollabotica.csproj @@ -16,6 +16,8 @@ + + diff --git a/Ollabotica/OutputProcessors/BasicChatOutputProcessor.cs b/Ollabotica/OutputProcessors/BasicChatOutputProcessor.cs index ca411ce..1a3e96e 100644 --- a/Ollabotica/OutputProcessors/BasicChatOutputProcessor.cs +++ b/Ollabotica/OutputProcessors/BasicChatOutputProcessor.cs @@ -24,9 +24,9 @@ public BasicChatOutputProcessor(ILogger log) private string text = ""; - public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, string ollamaOutputText, BotConfiguration botConfiguration) + public async Task Handle(ChatMessage message, OllamaSharp.Chat ollamaChat, IChatService chat, bool isAdmin, string ollamaOutputText, BotConfiguration botConfiguration) { - _log.LogInformation("Received message:{messageText}, ollama responded with: {ollamaOutputText}", message.Text, ollamaOutputText); + _log.LogInformation("Received message:{messageText}, ollama responded with: {ollamaOutputText}", message.IncomingText, ollamaOutputText); text += ollamaOutputText; if (text.Contains("\n")) @@ -34,8 +34,9 @@ public async Task Handle(Message message, OllamaSharp.Chat ollamaChat, ICh foreach (var line in text.Split("\n")) { if (string.IsNullOrWhiteSpace(line)) continue; - await chat.SendChatActionAsync(message.Chat.Id, ChatAction.Typing.ToString()); - await chat.SendTextMessageAsync(message.Chat.Id, line); + await chat.SendChatActionAsync(message, ChatAction.Typing.ToString()); + message.OutgoingText = line; + await chat.SendTextMessageAsync(message); } text = ""; } diff --git a/Ollabotica/Program.cs b/Ollabotica/Program.cs index 9338cce..332e962 100644 --- a/Ollabotica/Program.cs +++ b/Ollabotica/Program.cs @@ -7,6 +7,7 @@ using System.Net.Http.Headers; using Ollabotica.ChatServices; using Telegram.Bot; +using Ollabotica.BotServices; namespace Ollabotica; @@ -81,7 +82,8 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaul services.AddSingleton>(botConfigurations); services.AddSingleton(); // Register TelegramBotService as transient to create a new instance for each BotConfiguration - services.AddKeyedTransient(ServiceTypes.Telegram.ToString()); + services.AddKeyedTransient(ServiceTypes.Telegram); + services.AddKeyedTransient(ServiceTypes.Slack); // Register factories for TelegramBotClient and OllamaClient based on each BotConfiguration services.AddTransient(provider => @@ -108,7 +110,8 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaul //services.AddTransient(); services.AddTransient(); - services.AddKeyedTransient("Telegram"); + services.AddTransient(); + services.AddTransient(); services.AddHostedService(); }); diff --git a/Ollabotica/SlackBotService.cs b/Ollabotica/SlackBotService.cs deleted file mode 100644 index 7e46aec..0000000 --- a/Ollabotica/SlackBotService.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Logging; -using OllamaSharp; -using SlackAPI; -using SlackAPI.WebSocketMessages; - -namespace Ollabotica; - -/// -/// This class will handle a single bot's Slack and Ollama connections. -/// -public class SlackBotService : IBotService -{ - private BotConfiguration _config; - private SlackSocketClient _slackClient; - private OllamaApiClient _ollamaClient; - private readonly ILogger _logger; - private readonly MessageInputRouter _messageInputRouter; - private readonly MessageOutputRouter _messageOutputRouter; - private OllamaSharp.Chat _ollamaChat; - private CancellationTokenSource _cts; - - // Inject all required dependencies via constructor - public SlackBotService(ILogger logger, MessageInputRouter messageInputRouter, MessageOutputRouter messageOutputRouter) - { - _logger = logger; - _messageInputRouter = messageInputRouter; - _messageOutputRouter = messageOutputRouter; - _cts = new CancellationTokenSource(); - } - - public async Task StartAsync(BotConfiguration botConfig) - { - _config = botConfig; - _ollamaClient = new OllamaApiClient(botConfig.OllamaUrl, botConfig.DefaultModel); - _ollamaClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {botConfig.OllamaToken}"); - _ollamaChat = new OllamaSharp.Chat(_ollamaClient, ""); - - // Connect to Slack via Socket API - _slackClient = new SlackSocketClient(botConfig.ChatAuthToken); // Replace with your actual Slack token - - _slackClient.OnMessageReceived += HandleMessageAsync; - _slackClient.Connect((connected) => - { - _logger.LogInformation($"Bot {_config.Name} connected to Slack."); - }, () => - { - _logger.LogInformation($"Bot {_config.Name} disconnected from Slack."); - }); - - _logger.LogInformation($"Bot {_config.Name} started for Slack."); - } - - public Task StopAsync() - { - _cts.Cancel(); - _slackClient.CloseSocket(); - _logger.LogInformation("Bot stopped."); - return Task.CompletedTask; - } - - private async void HandleMessageAsync(NewMessage message) - { - if (message.subtype == "bot_message") - return; // Ignore bot messages - - _logger.LogInformation($"Received Slack message: {message.text} from user {message.user}"); - - bool isAdmin = _config.AdminChatIds.Contains(long.Parse(message.user)); // Assuming user IDs are long, adjust if needed - - if (_config.AllowedChatIds.Contains(long.Parse(message.user))) - { - var prompt = new StringBuilder(); - - try - { - // Route the message through the input processors - //var shouldContinue = await _messageInputRouter.Route(message, prompt, _ollamaChat, _slackClient, isAdmin, _config); - - //if (shouldContinue) - //{ - // var p = prompt.ToString(); - // if (string.IsNullOrWhiteSpace(p)) p = message.text; - - // // Send the prompt to Ollama and gather response - // await foreach (var answerToken in _ollamaChat.Send(p)) - // { - // await SendTypingIndicatorAsync(message.channel); // Slack doesn't have a "typing" action like Telegram - // await _messageOutputRouter.Route(message, prompt, _ollamaChat, _slackClient, isAdmin, answerToken, _config); - // } - // await _messageOutputRouter.Route(message, prompt, _ollamaChat, _slackClient, isAdmin, "\n", _config); - //} - } - catch (Exception e) - { - _logger.LogError(e, $"Error processing Slack message {message.ts}"); - if (isAdmin) - { - await SendMessageAsync(message.channel, e.ToString()); - } - } - } - else - { - _logger.LogWarning($"Received message from unauthorized user: {message.user}"); - } - } - - private async Task SendMessageAsync(string channel, string text) - { - //await _slackClient.SendMessageAsync((response) => - //{ - // if (response.ok) - // { - // _logger.LogInformation($"Message sent to channel {channel}: {text}"); - // } - // else - // { - // _logger.LogError($"Failed to send message to Slack channel {channel}: {response.error}"); - // } - //}, channel, text); - } - - private async Task SendTypingIndicatorAsync(string channel) - { - //await _slackClient.IndicateTyping(channel); - } -} \ No newline at end of file